微调多模态模型时,如果只是把视觉分支和文本分支的损失做简单加权求和,训练曲线常常出现一种矛盾:总损失稳定下降,但某个单模态的验证指标持续变差。损失权重在这里不只是一个超参数,它决定了优化器在共享编码器上优先沿着哪个模态的梯度方向更新。更关键的是,不同模态的梯度尺度可能相差一个数量级,即使权重相等,实际贡献也完全不同。要解决模态冲突,需要先判断冲突来自数值量级还是方向对抗,再选择对应的加权或梯度协调方案。

一、模态冲突的来源:梯度尺度差异与方向冲突
多模态模型通常包含视觉编码器、文本编码器和跨模态融合模块。视觉编码器往往经过大规模图像预训练,参数已经落在较平坦的损失区域,微调时产生的梯度范数相对较小;文本编码器如果对当前任务更敏感,或者预训练任务与下游任务差距较大,梯度范数会明显更大。假设两个损失权重都设为 1,实际反向传播时文本分支的更新量会主导共享参数,视觉分支的监督信号被稀释。这种尺度差异可以通过打印不同参数组的梯度范数快速确认。
import torch
def print_grad_norms(model, keywords):
for name, p in model.named_parameters():
if p.grad is None:
continue
if any(k in name for k in keywords):
print(f"{name}: {p.grad.norm().item():.4f}")
# 分别查看视觉与文本编码器的梯度规模
# print_grad_norms(model, ["vision"])
# print_grad_norms(model, ["text"])
除了尺度差异,方向冲突更隐蔽。两个任务对共享参数分别求出的梯度可能形成钝角,也就是点积小于 0。此时无论怎么调整标量权重,都无法完全消除互相抵消的问题。一个典型表现是:单独训练视觉任务或文本任务都有效,但联合训练后两个任务都变差。可以通过计算两组梯度的余弦相似度来判断冲突程度,如果长期接近 -1,说明模型在共享层上同时收到了相反方向的更新需求。
因此,损失权重平衡需要分两层看待:第一层是让各模态的损失数值进入相近的范围,第二层是处理共享参数上的方向冲突。只做第一层有时能缓解问题,但如果梯度方向本身对抗,后期仍然会出现负迁移。
二、从固定权重到不确定性加权:让损失自动调整
固定比例是最直观的方案,例如 total_loss = lambda_v * vision_loss + lambda_t * text_loss。它的优势是计算开销小、实现简单,适合快速验证。但缺点也很明显:不同任务、不同数据集的最优 lambda 差异很大,每次更换数据分布都要重新网格搜索。更麻烦的是,训练过程中损失的比例会变化,训练初期文本损失可能很大,后期视觉损失占比上升,固定权重很难适配整个过程。
不确定性加权将每个任务的损失看作一个高斯似然项,把同方差不确定性建模为可学习参数 sigma。实际实现时通常不直接学习 sigma,而是学习 log_var,避免参数变成负数。损失函数可以写成 precision 乘以原始损失再加上 log_var 正则项。下面的代码给出了一个轻量实现:
import torch
import torch.nn as nn
class UncertaintyWeightedLoss(nn.Module):
def __init__(self, num_tasks=2):
super().__init__()
self.log_var = nn.Parameter(torch.zeros(num_tasks))
def forward(self, losses):
total = 0.0
for i, loss in enumerate(losses):
precision = torch.exp(-self.log_var[i])
total += precision * loss + self.log_var[i]
return total
# loss_fn = UncertaintyWeightedLoss(2)
# total_loss = loss_fn([vision_loss, text_loss])
# total_loss.backward()
这种方式的优点是权重会随着训练动态变化,不需要手动搜索固定比例。但它也有局限:log_var 参数自身需要合适的学习率,如果更新过快,训练初期可能出现某个损失权重迅速趋向 0,让该模态被过早忽略。通常会把 log_var 单独放在一个参数组中,使用较小学习率,并在前几个 epoch 冻结权重学习。此外,不确定性加权只改变损失标的量,不影响共享参数上的梯度方向,当方向冲突占主导时效果有限。
一个常见的落地经验是,先用固定权重训练几个 epoch,观察各损失的量级和梯度范数比,再切换到不确定性加权。这样 log_var 从一个相对稳定的起点开始学习,可以避免冷启动阶段权重剧烈震荡。
三、梯度协调方法:PCGrad 与方向投影
如果监控发现两个任务的梯度余弦值长期为负,说明问题不在权重比例,而在更新方向。PCGrad 的思路很直接:对每一个任务的梯度,如果它与其他任务的梯度方向冲突,就移除它在冲突方向上的分量。具体做法是计算两个梯度的点积,当点积小于 0 时,把当前梯度投影到另一个梯度的法平面上。这样每个任务贡献给共享参数的部分不再相互对抗。
import torch
def pcgrad_step(optimizer, losses):
params = [p for group in optimizer.param_groups for p in group['params']]
# 为每个任务保存独立梯度
grad_list = []
for loss in losses:
optimizer.zero_grad()
loss.backward(retain_graph=True)
grad_list.append([p.grad.clone() if p.grad is not None else None
for p in params])
# 方向冲突投影
for i in range(len(grad_list)):
for j in range(len(grad_list)):
if i == j:
continue
for k, (gi, gj) in enumerate(zip(grad_list[i], grad_list[j])):
if gi is None or gj is None:
continue
dot = torch.dot(gi.flatten(), gj.flatten())
if dot < 0:
gj_norm_sq = torch.dot(gj.flatten(), gj.flatten()) + 1e-8
grad_list[i][k] = gi - (dot / gj_norm_sq) * gj
# 累加梯度并更新
optimizer.zero_grad()
for k, p in enumerate(params):
if p.grad is None:
p.grad = torch.zeros_like(p.data)
for g in grad_list:
if g[k] is not None:
p.grad += g[k]
optimizer.step()
这里的实现为了聚焦核心逻辑,把所有参数放在一个列表中处理。实际项目中如果存在不同参数组和学习率分组,需要根据参数组结构做映射。PCGrad 的额外开销主要来自多次反向传播,任务数量为 2 或 3 时可接受,任务很多时开销会线性增长。它的效果在图文检索、视觉问答等需要共享视觉和文本编码器的场景中比较明显。
不过 PCGrad 只处理两个梯度之间的冲突,当任务数量增多时,投影顺序可能影响最终梯度。后续的 CAGrad 等方法引入了一个约束参数,允许在平均梯度和冲突消解之间做折中。如果 PCGrad 仍然不够稳定,可以尝试用 CAGrad,并结合梯度裁剪来防止投影后的梯度范数过大。
四、落地实践:学习率分组、权重初始化与监控指标
损失权重策略上线前,先要确定参数分组。视觉编码器和文本编码器一般使用更低的学习率,跨模态融合模块可以使用更高学习率;不确定性加权中的 log_var 参数建议单独分组,用 1e-4 到 1e-3 量级的学习率。权重初始化可以选择 0,对应初始 sigma 约等于 1;也可以根据单模态损失的量级手动设置初始 log_var,让两个任务的初始权重接近。
训练中需要记录三类指标:第一类是两个分支各自的损失值,第二类是损失权重或 log_var 的变化,第三类是共享层上两组梯度的范数比和余弦相似度。下面是一个简单的监控函数示例:
def monitor_conflict(grad_a, grad_b):
flat_a = torch.cat([g.flatten() for g in grad_a if g is not None])
flat_b = torch.cat([g.flatten() for g in grad_b if g is not None])
norm_a = flat_a.norm().item()
norm_b = flat_b.norm().item()
cos = torch.cosine_similarity(flat_a.unsqueeze(0),
flat_b.unsqueeze(0)).item()
return norm_a, norm_b, cos
# 每 N 个 step 调用一次,观察 norm_a / norm_b 和 cos
# 若 cos 持续小于 -0.5,优先使用梯度协调方法
最后要避免一个常见陷阱:不要只盯着总损失。多模态任务中总损失可能因为某个模态权重降低而下降,但这并不代表该模态的表示学好了。每次调整权重策略后,要同时查看各模态验证指标。可以先固定一个简单基线,例如两个损失 1:1,训练到验证指标不再提升,再尝试不确定性加权或 PCGrad。复杂度应该逐步引入,先确认问题来自量级还是方向,再决定是否值得付出额外的反向传播开销。
总体来说,模态冲突没有单一的万能权重。固定比例适合作为快速基线,不确定性加权适合损失量级随时间变化的情况,梯度协调方法适合共享层出现方向对抗的情况。把这三种思路组合起来,并根据梯度监控数据选择策略,通常能让多模态微调的联合表示更加稳定。

