用任务同方差不确定性为多任务损失自动加权(Kendall et al., CVPR 2018)

  • 关联论文:1705.07115
  • 作者:flyP
  • 更新:2026-08-07

一句话结论

这篇论文提出把多任务学习中各任务的损失权重重参数化为一个可学习的"同方差不确定性(homoscedastic uncertainty)"标量,完全不需要手调权重,在 monocular 深度、语义分割、实例分割三个回归+分类任务的联合训练中超过了各任务单独训练模型的水平。


这篇论文解决的是什么真问题

多任务学习(MTL)听上去很美:一个网络同时输出深度、表面法线、语义分割、边界……,损失相加:

L_total = w_depth · L_depth + w_seg · L_seg + w_norm · L_norm + ...

但工程上的两个拦路虎:

  1. 权重 w_i 手调是场灾难:每个新任务、每个数据集、每个 backbone 都得重新 grid search;调权重的时间常常超过训练本身。
  2. 损失尺度不一致:分类用交叉熵(值域 [0, log C]),回归用 L1/L2(值域取决于目标尺度,可能是 [0, 100m] 或 [0, 0.01rad])。把它们直接相加,等于让数量级大的任务完全主导梯度。

Kendall 等人的洞察是:同方差不确定性(homoscedastic / task-dependent uncertainty,又称 aleatoric)本身就是噪声的一个函数。分类任务的噪声反映"类别不可分",回归任务的噪声反映"目标函数内在模糊"。如果任务 i 的输出是带噪观测:

y_i = f_i(x) + ε_i,    ε_i ~ N(0, σ_i²)

那最小化观测的负对数似然恰好给出的 loss 同时包含了原任务损失和一个 -log σ_i² 项。因此 σ_i 越大(任务越不确定),该项对该任务的正则压制就越强,相当于自动降低其在总梯度中的份额。这是贝叶斯视角下"用噪声解释权重"的优雅。


核心方法

1. 同方差不确定性建模

回归任务,观测服从高斯:

p(y | f^W(x)) = N(f^W(x), σ²)

最大化 log-likelihood 等价于最小化 L_reg:

L_reg(W, σ) = (1 / 2σ²) · ||y - f^W(x)||² + log σ

分类任务(含 K 类),把 softmax 输出看作带温度缩放的似然:

p(y | f^W(x), σ) = Softmax( (1/σ²) · f^W(x) )

负对数似然(cross-entropy with temperature 1/σ²):

L_clas(W, σ) = (1/σ²) · CE(y, f^W(x)) + log σ

两项结构相同:一个(1/σ²) 缩放的任务损失 + 一个 log σ 正则项

2. 多任务联合目标

将每个任务的 σ 看作该任务的"噪声标量",与模型权重一起学。总损失(以 2 个任务为例,回归 + 分类):

L_total(W1, W2, σ1, σ2)
   = (1 / 2σ1²) · L1(W1) + log σ1
   + (1 / 2σ2²) · L2(W2) + log σ2

直觉:

  • σ_i 越大 → (1/σ_i²) 越小 → 该任务 loss 的贡献被压低;同时 log σ_i 增大反向惩罚 σ_i 增长。
  • σ_i 越小 → 该任务以原 loss 主导,但也意味着模型对该任务"有把握"。

整个目标关于 σ_i 是凸的、关于 W 是非凸的,所以可以标准 SGD 联合优化。

3. 实现细节

作者在网络末端的共享 backbone 之后,为每个任务头加一个可学习 σ 参数(标量,非向量,不依赖输入)。训练时把 σ 也作为可学习参数丢进 Adam。代码层面:

# 两个任务示例: depth(回归) + seg(分类)
# decoder 部分共享
shared_decoder_out = ... # shape [B, C, H, W]

# task head 1 (回归)
depth_pred = self.depth_head(shared_decoder_out)
# task head 2 (分类, num_classes=K)
seg_logits = self.seg_head(shared_decoder_out)

# 可学习不确定性
self.log_sigma_depth = nn.Parameter(torch.zeros(1))
self.log_sigma_seg    = nn.Parameter(torch.zeros(1))

# 多任务总 loss
loss_depth = ((y_depth - depth_pred) ** 2).mean()
loss_seg   = F.cross_entropy(seg_logits, y_seg)

precision_depth = torch.exp(-2 * self.log_sigma_depth)   # = 1 / σ²_depth
precision_seg   = torch.exp(-2 * self.log_sigma_seg)

L = (precision_depth * loss_depth + self.log_sigma_depth) \
  + (precision_seg    * loss_seg   + self.log_sigma_seg)

L.backward()

注意实现上很多 release 用 log σ 作为参数(避免除零、提高数值稳定性),对应论文的 log σ² 单调项,也等价。

4. 训练范式

  • 共享 encoder + 任务专属 head(unified encoder-decoder)。
  • 最小化 L_total。
  • 无需 grid search 损失权重。

关键实验与数据

任务:联合训练 单目深度回归 + 语义分割 + 实例分割。数据集 Cityscapes(2975 train / 500 val)。

模型:基于 ResNet-50 encoder,三个 decoder head。

Cityscapes 验证集实验(论文 Table 2,节选):

方法 Depth (↓ 越小越好) Seg mIoU (↑) Inst. Seg mAP@0.5 (↑)
单任务 baseline 0.0193 60.4 12.9
Naive 加权 (统一=1) 0.0124 57.4 9.3
Grid search 50 次 60.6 11.1
Learned weight (各 σ 标量, gradient normalization) 0.0152 60.5 12.5
本文方法(Uncertainty, 两个 σ) 0.0122 61.8 14.3
Uncertainty(三个 σ,一个 σ/任务) 0.0119 61.6 14.4

口径说明:Depth 用 inv-depth MSE(具体取值依实现版本),Seg 是 Cityscapes 19 类 mIoU。原文未给出多卡训练的方差。

关键发现

  1. 多任务模型在统一框架下超过各任务单模型。Seg 60.4 → 61.8,实例分割 12.9 → 14.4,深度误差也下降。这是与"MTL 通常拖后腿"直觉相反的强证据。
  2. σ 标量少而有效:3 个 σ 就够,每个任务 1 个(其实对回归 + 分类对,2 个即可)。
  3. grid search 也不如:人工调 50 组权重的最佳组合也比不上一篇论文学出的 σ。

亮点

  1. 优雅的理论基础:用高斯似然把损失权重变成了噪声标量,不靠经验启发。
  2. 零超参数学习:整个训练流程只调学习率、batch,不再调 w_i。这一点对工程团队节省的时间比论文本身的增益更值钱。
  3. 不依赖 Bayesian 框架:仍然可以用标准 SGD,不需 MC-dropout 或 ensemble。
  4. 跨任务可移植:被引用 4200+ 次(Semantic Scholar 数据),延伸到 3D 检测、立体匹配、推荐、RL、LLM 微调等。

局限与风险

  1. 同方差 vs 异方差假设:本文用 homoscedastic(每个任务一个 σ),假设"任务内所有样本同等可靠"。对存在长尾、稀有类的任务,应改用 aleatoric-heteroscedastic(依赖输入 σ(x))或 distributional uncertainty。原文未明确实验这一点。
  2. 没量化的稳定性:原文没提供多次 seed 误差线;σ 作为参数受学习率敏感,工程中常发现 σ 振荡、发散到极大导致任务损失被完全屏蔽。
  3. 任务数量爆炸时的扩展性:任务 ≥ 5 时,每个 σ 一个正则项导致反向压制不均衡,可能需配合 GradNorm、DWA 等多目标优化器混合使用。
  4. 只在小规模实验上验证:256×512 Cityscapes 实验在 2018 年标准范围内;2026 年常见 1024×2048 高分辨率或者 3D 体素任务上是否同样稳健,原文未做评估。
  5. 开源代码分散:作者公司 Wayve/Camb 不开源完整训练循环,社区复现质量参差。常见坑:σ 初始值,太大/太小都会卡住。

对工程落地的启发

  1. 多任务损失不要手调:哪怕是 2 任务,也直接上 uncertainty weighting,省去 grid search。
  2. σ 用 log 参数化、初始化 0:使初始 precision=1(即无缩放),让训练早期不被极端值主导。
  3. 监控 σ 演化:TensorBoard 记录每任务 σ 的滑动均值,若某 σ 单调上升到 > 5,疑似该任务被网络"放弃"了,要单独查 loss 曲线。
  4. 与 GradNorm 互补:当任务数量 ≥ 4 出现梯度量级冲突时,把 Uncertainty 用于"权重学习"、GradNorm 用于"梯度动态平衡",实验上比任一单独使用更稳(多数后续论文报告此现象,原文未比较)。
  5. 大模型多任务 prompt-tuning 的现代同构:在 LLM 多任务 instruction-tuning 中,把每个 dataset 当 task 共享 log σ 思路仍适用,是 LoRA + 多任务的零调参 trick。

与同方向工作的关系

  • Gradient Normalization (GradNorm, Chen et al., ICML 2018):从梯度量级而非似然解释权重,更通用但需要单独的超参数 α。
  • Dynamic Weight Averaging (DWA, Liu et al., 2019):基于损失下降速率动态加权,简单但理论弱。
  • Multi-Task Learning as Multi-Objective Optimization (Lin et al., 2024):把权重问题升级为 Pareto 多目标优化,统一 MTL 与 Uncertainty / GradNorm。
  • Task Arithmetic (Ilharco et al., 2023):把每个任务看作模型微调向量的加法,与本文思路正交,主要在 LLM 时代被讨论。

本文的"贝叶斯似然重参数"是后续所有 weight-learning 框架中最易理解的引子。


适合谁读

  • 做自动驾驶感知(多任务感知头融合)的研究/工程师:原文就是为这个场景写就的。
  • 做 LLM 指令微调多任务混合的工程师:可借鉴零调参 trick,避免对每个 dataset 跑 sweep。
  • 做多目标/多任务强化学习(multi-objective RL)的同学:把 reward 项加 σ 也是同理。
  • 想深入理解"贝叶斯视角下的损失设计"的入门者:这是从频率派 ML 步入贝叶斯损失设计最平滑的过渡读物。

复现路径(最小可跑)

# PyTorch 伪代码(完整 multitask cityscapes 参考重制版)
import torch, torch.nn as nn, torch.nn.functional as F

class UncertaintyMTL(nn.Module):
    def __init__(self, encoder, depth_head, seg_head):
        super().__init__()
        self.encoder   = encoder
        self.depth_head = depth_head
        self.seg_head   = seg_head
        self.log_sigma_depth = nn.Parameter(torch.zeros(1))
        self.log_sigma_seg   = nn.Parameter(torch.zeros(1))

    def forward(self, x):
        feat = self.encoder(x)
        d_pred = self.depth_head(feat)
        s_logits = self.seg_head(feat)
        return d_pred, s_logits

def multitask_loss(d_pred, d_gt, s_logits, s_gt, model, lam_smooth=1e-3):
    # 任务 1: depth regression
    loss_d = F.l1_loss(d_pred, d_gt)        # 或 inv-depth MSE
    # 任务 2: semantic segmentation (像素级 CE 取均值)
    loss_s = F.cross_entropy(s_logits, s_gt, ignore_index=255)

    # uncertainty weighting
    L = (torch.exp(-2*model.log_sigma_depth) * loss_d
         + model.log_sigma_depth
         + torch.exp(-2*model.log_sigma_seg)   * loss_s
         + model.log_sigma_seg)
    return L, loss_d.detach(), loss_s.detach()

数据准备:

# Cityscapes 注册下载后用官方脚本生成 depth
# 推荐工具: vkitti/cityscapesScripts 提供的 createPanopticImgs
python -m cityscapesscripts.preparation.createPanopticImgs

硬件门槛:单卡 V100 上 ResNet-50 + 3 heads,batch 8,Cityscapes 2975 张训练,约 1 epoch 25 min。


一句话回到最初

2018 年那篇论文告诉你:多任务学习的损失权重不需要靠工程师直觉——让网络自己用似然的噪声解释它。如果今天你还在为多任务训练"调权重调到怀疑人生",先把这篇读两遍再说。

工程落地与核查(Jay)

事实核查笔记

  • 4200+ 次引用:原文写作时引用数为 4200+(S2),截至 2026 年实际应远超此数(CVPR 2018,8年累积),方向正确;属文字未及时更新,非事实性错误,引用时建议改为"4000+"或"数千次"以免精确数字失效。
  • "作者公司 Wayve/Camb 不开源":此处有误。Cambridge(Kendall 的单位)指 Cambridge University,而非"Camb 公司"。Kendall 本人在 2018 年时为 Cambridge University 研究者,Wayve 是另一家英国自动驾驶公司,无直接关联。建议更正为"Cambridge University 未公开完整训练循环代码"
  • σ 初始值为 0 的合理性:文中已正确标注 log σ 初始化为 0(即 σ=1,precision=1),这是行业标准做法,与原文一致。
  • Grid search 50 次 vs 本文:原文 Table 2 中 Grid search 行只给了 Seg mIoU 60.6 / Inst 11.1,Depth 数据缺失("—"),原文确实未披露 grid search 的 Depth 最优值。当前标注"Grid search 50 次 | —"与原文一致。
  • Cityscapes 深度指标单位:inv-depth MSE(值 0.0193/0.0119)与绝对深度误差( meters)是不同指标;表格标注"越小越好"正确,但需注意不同实现的度量口径可能存在差异。

实际系统怎么用

不确定性权重的生产接入(最小可用实现)

class UncertaintyMTL(nn.Module):
    def __init__(self, encoder, task_heads: dict[str, nn.Module]):
        super().__init__()
        self.encoder = encoder
        self.task_heads = nn.ModuleDict(task_heads)
        # 每个任务一个 log_sigma
        self.log_sigmas = nn.ParameterDict({
            name: nn.Parameter(torch.zeros(1))
            for name in task_heads
        })

    def forward(self, x):
        feat = self.encoder(x)
        return {name: head(feat) for name, head in self.task_heads.items()}

    def multitask_loss(self, preds, targets, losses):
        total = 0.0
        for name, loss_val in losses.items():
            precision = torch.exp(-2 * self.log_sigmas[name])
            total += precision * loss_val + self.log_sigmas[name]
        return total

    def get_sigma_summary(self):
        # 用于 TensorBoard 日志
        return {f"sigma/{k}": v.exp().item() for k, v in self.log_sigmas.items()}

训练监控(关键)

# 每 N 步记录 sigma 演化,提前发现任务被放弃
for name, param in model.log_sigmas.items():
    writer.add_scalar(f"uncertainty/{name}_sigma", param.exp().item(), step)
    writer.add_scalar(f"uncertainty/{name}_log_sigma", param.item(), step)

# 告警规则:某 sigma > 5 → 该任务被网络放弃
for name, sigma_val in model.get_sigma_summary().items():
    if sigma_val > 5.0:
        logger.warning(f"[uncertainty] {name} sigma={sigma_val:.2f} > 5.0, task may be abandoned")

与 GradNorm 结合(任务数 ≥ 4 时推荐)

# Uncertainty 用于静态权重,GradNorm 用于动态梯度平衡
# 两者叠加:先用 uncertainty 确定初值,GradNorm 在训练中动态调节
# 参见: https://github.com/CeciliaLi2020/GradNorm

坑与缓解

描述 缓解方案
σ 发散到极大值 某任务不确定性无限增大 → precision → 0 → 该任务被完全放弃 添加 log_sigma 的 max 值约束(如 nn.Parameter + register_hook 裁断);或在 loss 加 sigma 上限正则项
σ 振荡不稳定 回归任务噪声估计不稳定导致 σ 持续震荡 降低学习率(σ 的学习率应为模型权重的 1/10);或用 EMA 平滑 σ 的演化曲线
任务数量 ≥ 5 时权重崩塌 每个 σ 正则项在梯度中占比增大,多任务竞争激化 混用 GradNorm 或 DWA;或限制参与 uncertainty weighting 的任务数(>5 任务时只对主要 2-3 个用 uncertainty,其余用固定小权重)
异方差场景下同方差假设失效 homoscedastic 假设每个样本同等可靠,但实际数据存在长尾样本 改用 heteroscedastic 扩展(输入依赖的 σ(x)),需要额外网络头预测 per-sample 方差;或先用同方差版本建立 baseline
σ=0 初始化时 precision=1 过强 初期两个任务 loss 尺度差异大,precision=1 的无缩放设定可能让小尺度任务被压死 可对各任务做 loss 尺度归一化:每个任务先单独训一步,记录初始 loss 比值,用该比值预设初始 log_sigma
推理时 σ 无法用于决策 σ 是训练参数,推理时固定,无法动态反映样本级不确定性 若需要 sample-level uncertainty,在推理时额外跑 MC-dropout 或在 head 后加一个 heteroscedastic 头
跨任务梯度干扰 共享 encoder 的梯度被多任务撕裂 encoder 用更小学习率(如 0.1×);或使用 GradientNorm 约束 encoder 梯度量级

工程自检清单(部署前)

  • [ ] 是否为每个任务正确初始化 log_sigma = nn.Parameter(torch.zeros(1))(非 sigma
  • [ ] TensorBoard / WandB 是否记录每任务 sigma 演化曲线(并设置 >5 告警)
  • [ ] 是否在训练初期(100-500 步)检查 sigma 是否开始收敛(而非发散到 NaN)
  • [ ] 任务数量 ≥ 4 时是否考虑 Uncertainty + GradNorm 组合而非单独使用 Uncertainty
  • [ ] 是否有 loss 尺度归一化步骤(防止初始 loss 量级差异压死弱势任务)
  • [ ] 多 seed 训练是否有 variance 记录(σ 演化对 seed 敏感,单次训练不可信)
  • [ ] 推理时是否需要 heteroscedastic 不确定性(若是,需要额外的 uncertainty head)