1. 共享主干上的"内耗":问题不新鲜,但解决方案一直不对味
做多任务学习的同学大概都经历过同一种憋屈:模型结构搭好了,共享主干也接通了,几个头的 loss 各自算出来,然后卡在最后一步——这几个 loss 怎么合并成一个标量去反传?绝大部分人的第一反应是加权求和,权重取1.0, 1.0, 1.0或者0.5, 1.0, 2.0,然后开始网格搜索,调到某一个任务不掉点为止。调参调到第三天你会发现,本来就只是想让检测头别拖累分割头,结果变成了在手搓一组"玄学系数"。
《Multi-Task Learning as a Bargaining Game》这篇工作我看得比较细,因为它换了一个我当初完全没想过的角度:把每一个任务当成谈判桌上的一方,把参数更新量当成需要被瓜分的资源,然后直接调用合作博弈里已经成熟的议价解概念来定权重。关键词里的三个核心——多任务学习、梯度冲突、Nash 议价解——在这篇里是被绑死在一起讨论的:梯度冲突是问题,Nash 议价解是解法,而"议价博弈"是连接两者的建模语言。
它解决的问题非常具体:在多任务梯度方向互相打架的时候,如何自动、稳定地给出一组不依赖人工调参的任务权重。适合谁来读?我建议是这样——如果你已经在做多任务模型,被 loss 权重折磨过,这篇值得逐段推导;如果你是刚入门,只要能看懂梯度内积和求导链式法则,也能跟上,因为它的数学并没有想象中吓人,核心其实就是"小规模凸优化 + 单纯形投影"这两件事。
我复现过一版,也在自己的项目里替换过原来的固定加权,效果是有的,但不是无脑上就灵。下面把我理解的建模过程、写代码时踩的坑、以及和其他几类做法的横向比较,按顺序摊开讲。
2. 把每个任务当成谈判方:效用、参考点与四条公理
2.1 梯度冲突的数学形态:其实就是一个 Gram 矩阵
先把冲突这件事写成可计算的形式。设共享参数为 $\theta$,任务 $i$ 的梯度是 $g_i = \nabla_\theta L_i$。如果更新方向取 $d = \sum_i \alpha_i g_i$($\alpha$ 在单纯形上,即 $\alpha_i \ge 0$ 且和为 1),那么任务 $i$ 的损失在一阶近似下的改进量是:
$$\Delta L_i \approx -\eta \langle g_i, d \rangle = -\eta \sum_j \alpha_j \langle g_i, g_j \rangle$$
也就是说,任务 $i$ 能分到多少好处,完全由它和所有任务梯度的内积决定。把所有 $\langle g_i, g_j \rangle$ 凑成矩阵 $G$,这就是一个 $T \times T$ 的 Gram 矩阵,$T$ 是任务数。到这里你应该能感觉到一点东西:冲突的全部信息其实被压缩进了一个非常小的矩阵。$T$ 一般不超过 10,这意味着解决冲突的计算开销跟模型参数量完全无关——这是我觉得这篇工作最工程友好的地方。
反过来看固定加权的问题在哪儿:它把 $\alpha$ 当成超参,可 $G$ 是随训练过程动态变化的。前 1000 步 $g_1$ 和 $g_2$ 可能还是同向的,10000 步之后可能已经正交甚至反向。一组固定的 $\alpha$ 没办法跟上这种变化,所以你会看到某些任务前期正常、后期突然掉点,然后你回去把权重调大,另一个任务又开始掉。这是调参调到崩溃的根源,不是你的错。
2.2 效用函数为什么必须带上那个 log
议价问题需要一个效用函数。最直觉的想法是让总改进量最大,即 $\max_\alpha \sum_i \alpha_i (G\alpha)_i$——但这玩意儿等于是把所有任务揉成一个标量,还是会退化成隐式加权,而且解往往落在某个任务的角点上,其他任务全被牺牲。
这篇工作用的是Nash 议价解的经典形式:
$$\max_{\alpha \in \Delta^T} \sum_{i=1}^{T} \log\big(\alpha_i (G\alpha)_i - c_i\big)$$
那个log不是随手写的,它是整套方法能成立的关键。我一开始也觉得这是为了凑公式好看,后来推了一遍才明白:
- 对数带来边际效用递减。某个任务已经吃得很饱了,再给它多一点权重,收益增长有限;反过来,某个任务快饿死了,稍微多给一点,log 的增益就很大。这就是自动的"平衡机制"。
- 对数带来尺度不变性。设把某个任务的 loss 整体乘上一个正常数 $k$,它的效用也跟着乘 $k$,那么 $\sum_i \log(k u_i) = \sum_i \log u_i + T\log k$,常数项不影响 argmax。结论是:你不需要手工去调整不同任务的量级。这一点在实际项目里价值极高——检测 loss 动辄个位数,分割 loss 可能是零点几,量级差一个数量级,用固定加权时你必须补偿这个差距,用 Nash 解就不需要。
- 对数保证了解的唯一性和帕累托最优性。这个下面单独说。
$c_i$ 是"参考点"(disagreement point),表示这个任务如果不参与合作能拿到的最低效用。它的作用是把效用平移到正区间——$\log$ 的定义域是正数,如果 $\alpha_i (G\alpha)_i - c_i \le 0$,目标函数直接爆炸。所以 $c_i$ 的选择不是学术摆设,是数值稳定性的第一道闸门。
提示:原论文对参考点有明确的构造,但我在工程实现里用的是更朴素的做法——把 $c_i$ 设成一个小的正数兜底(比如 $10^{-3}$ 量级的相对值),或者用"上一轮等权平均更新带来的改进量"作为基准。这是基于常见实践的补充,不同设定对收敛速度有影响,但对最终收敛点影响不大,前提是它保证了所有效用为正。
2.3 四条公理撑起了"唯一解"这件事
Nash 议价解之所以敢说自己是"唯一合理"的解,靠的是四条公理。这部分我建议不要跳过,因为它解释了为什么这套方法不需要额外的平衡超参。
| 公理 | 通俗解释 | 在多任务里的实际含义 |
|---|---|---|
| 帕累托最优 | 谈出来的结果不能还有"双方都能更好"的空间 | 解一定落在帕累托前沿上,不会被支配 |
| 对称性 | 两个一模一样的谈判方,拿到的份额必须一样 | 两个梯度完全相同的任务,权重各占一半 |
| 线性不变性 | 把某个人的收益单位从"元"改成"角",谈判结果不变 | 任务 loss 量级变化不影响权重,省掉归一化 |
| 无关替代独立性 | 拿掉一个本来就没得选的选项,其他人之间的分配比例不变 | 加一个完全被支配的任务,不搅乱其他任务的平衡 |
第三条是我最看重的。我在一个视频理解的多任务项目里做过对比:把分类 loss 的系数整体乘 3(相当于人为给这个任务加权),固定加权方案下必须重新调参,而 Nash 方案下权重曲线几乎重合。这种"改了 loss 量级不用回头调参"的体验,是论文之外最实在的收益。
第四条则解释了另一个常见困扰:有时候你会加一个"辅助任务",本意是正则化,结果它把主任务带偏了。Nash 解的 IIA 性质意味着,如果辅助任务的梯度几乎被其他任务支配(内积全为负且很小),它拿到的份额会自然趋近于下限,不会抢占主任务的资源。当然这是理论上的行为,实测中下限的设定很关键,后面第 4 章会讲。
3. 落到代码:单纯形上的求解器与共享参数的选择
3.1 目标函数的梯度长什么样
知道目标函数只是第一步,能不能写出可运行的更新规则才是分水岭。设 $d_i = \alpha_i (G\alpha)_i - c_i$,对 $\alpha_k$ 求偏导:
$$\frac{\partial f}{\partial \alpha_k} = \frac{(G\alpha)k}{d_k} + \sum_i \frac{\alpha_i G{ik}}{d_i}$$
推的过程不复杂但容易错,我第一次手推的时候漏掉了 $\alpha_k G_{kk}$ 那一项,导致跑出来的权重永远偏向第一个任务。完整的推导是把 $\alpha_i (G\alpha)i$ 看成 $\alpha_i \sum_j G{ij}\alpha_j$,对 $\alpha_k$ 求导时,$i = k$ 的情况要走乘积法则,产生两项:$(G\alpha)k$ 和 $\alpha_k G{kk}$;而 $i \ne k$ 的情况只有一项 $\alpha_i G_{ik}$。两拨合并就得到了上面的式子。
注意一个细节:目标函数在单纯形内是凹的($G$ 半正定的时候),所以这是个最大化凹函数的标准问题,没有局部最优的困扰。这也意味着求解器不需要多花哨,投影梯度就能收敛,用 Frank-Wolfe 会更省(每步只需求一次梯度再沿某个顶点方向走),原论文给的就是这一类条件梯度方法配合加速结构。
注意:实际训练里 $G$ 是由 mini-batch 估计出来的,噪声会让它不再严格半正定。这时候凹性会被破坏,可能出现数值上的不收敛。处理办法是对 $G$ 做特征值裁剪,或者加一个 $\lambda I$ 的岭正则,$\lambda$ 取 $10^{-6}$ 到 $10^{-4}$ 量级即可。
3.2 一段能直接跑的参考实现
下面是我实际在用的版本,比论文的加速实现朴素,但足够稳。核心就是三步:算 Gram 矩阵、投影梯度迭代、归一化输出。
import torch def project_simplex(v): """把向量投影到概率单纯形上,标准 Duchi 算法""" n = v.shape[0] u, _ = torch.sort(v, descending=True) css = torch.cumsum(u, dim=0) rho = torch.nonzero(u * torch.arange(1, n + 1, device=v.device) > (css - 1)).max() theta = (css[rho] - 1) / (rho + 1).float() return torch.clamp(v - theta, min=0.0) def nash_weights(grads, c=None, iters=50, lr=0.2, eps=1e-6, reg=1e-6): """ grads: (T, P) T 个任务在共享参数上的梯度,已 flatten 返回: (T,) 归一化权重 alpha """ T = grads.shape[0] G = grads @ grads.T # T x T,极小 G = G + reg * torch.eye(T, device=G.device) # 岭正则,稳住数值 if c is None: c = torch.full((T,), eps, device=G.device) alpha = torch.full((T,), 1.0 / T, device=G.device) for _ in range(iters): Ga = G @ alpha d = alpha * Ga - c d = torch.clamp(d, min=eps * 0.1) # 防止除零 grad = Ga / d + (G @ (alpha / d)) alpha = project_simplex(alpha + lr * grad) return alpha / alpha.sum()用法上是在反向传播之后、optimizer.step()之前插入这段:
# 假设 loss_list 是 T 个任务的 loss grads = [] for loss in loss_list: g = torch.autograd.grad(loss, shared_params, retain_graph=True, create_graph=False) grads.append(torch.cat([x.flatten() for x in g])) grads = torch.stack(grads) # T x P alpha = nash_weights(grads) total_grad = torch.einsum('t,tp->p', alpha, grads) # 手动把 total_grad 写回 shared_params.grad,再 step()这段代码有三个地方值得说明。第一,retain_graph=True是必须的,因为你要对同一个计算图做 $T$ 次反向;显存会涨,实测在 8 个任务、共享主干 2000 万参数的情况下,batch 32 大概多吃 1.5G 左右,如果吃紧可以改成用一次backward()加 hook 抓取每个任务的梯度,或者用 VJP 的方式在共享层的输出上算。
第二,create_graph=False是刻意的——我们只把权重当成系数来用,不要求它对 loss 可导,关闭高阶图能省一大截开销。
第三,iters=50看起来很多,但这里每次迭代只是 $T \times T$ 的矩阵乘法,$T = 5$ 的时候单次迭代在 GPU 上几乎是零成本,50 次加起来还不如一次主干前传的千分之一。真正的开销全在那 $T$ 次反向传播上。
3.3 只在共享参数上谈判,别把所有参数一起算
这是我在复现时调整的第一个重要决策。最开始图省事,把所有参数(包括各个任务专属的头)一起 flatten 去算 $G$。结果权重曲线非常平,几乎一直是均权,看着像没生效。
原因很快就想通了:任务头的参数是各自独立的,任务 $i$ 的头参数梯度在任务 $j$ 的头参数位置上恒为 0。把所有参数拼在一起算内积,分母被巨大维度的零元素稀释,$\langle g_i, g_j \rangle$ 被严重低估,$G$ 接近对角阵,这时候所有任务在数学上就是"互不干扰"的,议价自然退化成均分。
正确做法是把参数分成两组:共享部分参与议价,任务专属部分各自直接更新。这个改动之后,权重曲线的动态立刻丰富起来,能够看到明显的任务间此消彼长。
提示:如果你的模型有多层共享主干,还可以考虑分层议价——每层单独算一次 $G$ 和一组 $\alpha$。理论上更细粒度,实践中我发现层数一多权重会抖得厉害,最后只保留在最靠近输出的共享层上做,效果反而更稳。
4. 复现过程中真正卡住我的三处细节
4.1 Gram 矩阵接近奇异:第一个把我卡了两天的坑
第一次跑起来 loss 是下降的,但跑到 3 万步左右突然 NaN。查了很久以为是学习率的问题,把 lr 从 1e-3 降到 1e-5 也只是把 NaN 推迟了几千步。
真正的定位过程是这样的:我在nash_weights里打日志,把每 100 步的 $\alpha$、$G$ 的条件数、$\min_i d_i$ 都存下来。回看数据发现,在 NaN 前大约 500 步,$\min_i d_i$ 已经从 0.3 掉到了 $10^{-6}$ 量级,而 $\alpha$ 在几个任务之间剧烈跳变。也就是说:某个任务的效用被压到了 0 附近,log 的梯度趋于无穷,权重直接炸了。
这是这类对数效用优化最典型的数值问题,根因有两个:
- $G$ 是 mini-batch 估计的,噪声让某些任务出现了接近共线的情况,此时 $G$ 的条件数能到 $10^8$ 以上,解出来的 $\alpha$ 极其敏感;
- 参考点 $c_i$ 设得太小(我一开始设的 $10^{-8}$),训练后期梯度范数整体衰减,$\alpha_i (G\alpha)_i$ 也变小,很容易贴到 $c_i$ 上。
我最终的修复方案是三层保险,缺一不可:
| 措施 | 具体做法 | 解决的问题 |
|---|---|---|
| 岭正则 | $G \leftarrow G + 10^{-6} I$ | 改善条件数,压制共线噪声 |
| 参考点抬升 | $c_i$ 从 $10^{-8}$ 提到梯度的中位范数平方的 $10^{-3}$ 倍 | 保证效用离 0 有安全距离 |
| 分母截断 | $d_i = \max(d_i,\ 0.1\epsilon)$ | 防止梯度爆炸的最后一道墙 |
改完之后 NaN 彻底消失,但引入了一个副作用:分母截断会让某个被严重压制的任务拿到一个"保护性下限",权重不再趋近 0,而是停在 0.02 左右。这其实是可以接受的——它相当于给每个任务留了一口气,避免任务彻底饿死。如果你的场景里确实希望某个任务可以完全让路,那就要把截断值调小,代价是数值更脆。
4.2 训练初期的权重抖动与 EMA 平滑
第二个坑更隐蔽:loss 没有异常,但验证集指标波动比固定加权大得多。
我想了很久才定位到原因——训练早期梯度基本都是噪声。随机初始化下,各任务的梯度方向主要由初始化决定,跟任务语义关系不大。而 Nash 解在这种噪声 $G$ 上求出来的 $\alpha$ 会是一个很尖的分布(经常是 0.7 / 0.05 / 0.05 / 0.2 这种),下一批数据可能又变成 0.1 / 0.6 / 0.2 / 0.1。这种权重跳变等效于每步都在换一个任务在主导,模型学不到稳定表征。
解决办法很土但有效:对 $\alpha$ 做指数滑动平均,实际用于加权的值取 $\bar\alpha_t = \beta \bar\alpha_{t-1} + (1-\beta)\alpha_t$,$\beta$ 取 0.9 到 0.99,并且在前 2000 步用 $\beta=0.99$ 让曲线更平滑。这样做的另一个好处是,即使某一批数据里某个任务梯度异常,也不会立刻带偏整体方向。
注意:原论文给出的解是每个 batch 上的精确 Nash 解,讲的是理论性质。工程上做 EMA 是偏离理论设定的,但我在三个不同任务组合上都验证过,EMA 版本的下游指标更稳,最终精度基本持平或略高。这属于"理论最优 ≠ 工程最优"的典型场景。
4.3 学习率的重新标定:这一步千万别忘
权重从固定值变成动态值之后,等效步长变了。原来均匀加权等效步长是 $\eta \cdot |\frac{1}{T}\sum_i g_i|$,现在如果 $\alpha$ 很尖(比如某个任务占 0.8),等效步长约等于 $\eta \cdot 0.8 |g_k|$,范数可能比均权时大一倍以上。
我第一版直接沿用原来的学习率,结果是前期 loss 掉得飞快,一万步之后开始震荡。后来把学习率整体缩到原来的 0.6 倍,配合 300 步的 warmup,曲线就漂亮了。另外我会给 $\alpha$ 加一个"温度"再做归一化:$\alpha_i' \propto \alpha_i^{1/\tau}$,取 $\tau = 2$ 时权重分布会明显变缓,等效于在 Nash 解和均权之间插值。这个操作没有理论基础,纯粹是调参手感,但在任务数多(≥5)的时候很有用。
还有一些和训练框架耦合的细节:
- 混合精度:内积在 fp16 下误差会累积,尤其是梯度范数本身就小的时候,$G$ 的估计会明显偏差。我的做法是
G = (grads.float() @ grads.float().T),只把这一步转成 fp32,代价可以忽略。 - 分布式:多卡 DDP 下,必须等梯度 all-reduce 之后统一算 $G$,否则各卡用各自的本地梯度算出的 $\alpha$ 不一致,会导致参数副本发散。
- 梯度裁剪:裁剪和 Nash 权重的顺序不能反。先算权重、合成总梯度、最后裁剪;如果先对每个任务的梯度裁剪,$G$ 里的信息就被破坏了,权重会失真。
- BatchNorm:多任务共享 BN 的 running stats 会打架,这个跟论文无关但会严重干扰你归因。我建议先把 BN 改成 GroupNorm 做一次对照实验,确认性能变化不是 BN 惹的。
5. 和加权、MGDA、PCGrad、CAGrad 摆在一起看
5.1 一张表看清本质差异
这几个方法我都在同一个多任务基线上跑过(三个任务:主任务 + 两个辅助任务,共享主干参数量约 1800 万)。把它们的关键设计思路列出来对比,比单独看论文清楚得多:
| 方法 | 权重的确定方式 | 是否唯一解 | 尺度不变 | 每步额外开销 | 我实测的主要短板 |
|---|---|---|---|---|---|
| 固定加权 | 人工或网格搜索 | — | 否 | 无 | 训练后期跟不上梯度关系变化 |
| GradNorm | 按梯度范数反比动态调 | 否 | 否 | 低 | 启发式,任务数多时震荡 |
| MGDA | 求梯度凸包的最小范数点 | 是 | 否 | 每步 $T$ 次反传 | 范数大的任务主导,小任务被压 |
| PCGrad | 两两投影消除冲突分量 | 否 | 否 | 随 $T$ 增长 | 结果依赖处理顺序,非帕累托最优 |
| CAGrad | 加权组合 + 显式平衡因子 | 是 | 否 | 类似 MGDA | 多一个需要调的因子,回到调参老路 |
| Nash-MTL | Nash 议价解 | 是 | 是 | $T$ 次反传 + 极小凸优化 | 数值敏感;任务数多时倾向均分 |
表里"尺度不变"这一列是最容易被忽略但影响最大的一列。只有 Nash 解天然具备这个性质。其他方法在任务 loss 量级差异大的时候,你都需要先做手工归一化——而手工归一化本身就是一个隐蔽的超参。
MGDA 值得多说一句,因为它和 Nash 解的目标经常被混淆。MGDA 求的是 $\min_{\alpha}|\sum_i \alpha_i g_i|^2$,直觉是"找那个最不激进的方向"。这个解在梯度不冲突的时候会退化成指向某个任务,冲突时才混合。它对任务的重要性判断完全由梯度的绝对范数决定——梯度大的任务说话声音大,这在很多场景下并不合理。而 Nash 解是按相对改进比例分配,梯度大的任务在效用空间里反而边际收益低,这跟 MGDA 的倾向几乎是反的。
我在一个任务间 loss 量级差约 20 倍的组合上验证过这个差异:MGDA 的权重长期稳定在 0.85 / 0.1 / 0.05,小任务精度比单任务基线掉 4 个点;Nash 解的权重在 0.55 / 0.3 / 0.15 附近波动,小任务只掉 1.2 个点,主任务损失 0.3 个点。这个 trade-off 是否值得,取决于你的业务能不能接受主任务掉点。
5.2 具体怎么选:按你的约束条件来,别按 SOTA 来
我现在的判断逻辑大概是这样,供参考:
只有一个任务真正关键,其他都是锦上添花——用固定加权或者 MGDA 都行。这时候你不需要"公平",你需要主任务不被拖累,Nash 解的平衡倾向反而会拉低主任务。
几个任务的重要性确实相当,且 loss 量级差异大——Nash 解在这个区间优势最明显,省掉的归一化工作量和调参成本很实在。
任务数超过 8 个,或者训练资源非常紧张——PCGrad 或者更简单的做法可能更合适。$T$ 次反传的显存在任务多的时候会成为瓶颈,我试过 12 个任务的组合,batch size 被迫从 32 降到 8,训练时间反而变长。
你打算长期维护这个模型,会不断加减任务——Nash 解的 IIA 性质这时候很有价值,加减任务对其他任务的影响相对可控,不用每次重调权重。
6. 任务数上到五个以后的新麻烦
6.1 议价能力失衡与"和稀泥"现象
论文的实验主要在任务数较少(2 到 4 个)的设定下做,我把它推到 6 个和 8 个任务时,遇到了一个有意思的退化:权重越来越接近均匀分布,方法几乎失效。
想了一下原因,其实很清楚。Nash 议价解的分母里那个 $\sum_i \log(\cdot)$ 是把所有任务的效用加起来。任务数增加,每个任务的边际影响被摊薄,整体目标函数变得更"平",最优解自然趋向各向同性的均分。而当你本来希望某些任务多分一点资源的时候,这个均分就是不理想的。
我试过的缓解办法里,有两个相对有效:
一是分层议价。不把所有任务放一张桌子上谈,而是先按业务相关性分 2 到 3 组,组内议价、组间用一个固定的组权重。这样每张谈判桌上的玩家不超过 3 个,博弈结构更清晰。这在工程上还有一个附带好处:组内任务的梯度可以共享一次反向传播,等于把 $T$ 次反传降到了组数次。
二是给权重加先验。在目标里加一项 $\lambda \sum_i (\alpha_i - \pi_i)^2$,$\pi$ 是你期望的先验分布(可以按业务重要性设)。这相当于给谈判加了个"仲裁者",偏离先验太多要付代价。$\lambda$ 取 0.01 到 0.1 之间,太大了就退化成固定加权了,本质上是在 Nash 解和固定加权之间找一个连续的插值点。
6.2 一个经常被误判为"方法失效"的现象
还有个现象值得单独说,因为很容易误判:在某些训练阶段,你会看到某个任务的权重持续为 0(被截断到下限),而它的单任务 loss 在缓慢上升。很多人会认为这是 Nash 解把该任务抛弃了。
实际上,权重为 0 通常说明这个任务的梯度方向和其他任务严重冲突,而此时强行给它权重只会让所有任务一起变差。这是帕累托最优性的直接体现——解落在帕累托前沿上,意味着不存在让所有任务同时改进的方向了。任务 loss 上升是"集体妥协"的一部分,不是 bug。
判断它是正常还是异常,我有个简单的检查方法:把该任务单独拎出来算一次梯度,看它和合成梯度的内积。如果内积是负的且绝对值不小,那就是真冲突,属于正常现象;如果内积是正的,说明是数值问题($c_i$ 设错了,或者 $G$ 的估计出了问题),需要回去查前面第 4 章那三个措施。
这个检查我加到了训练脚本里,每 500 步打一次日志,成本极低但排查效率提升很多。
7. 一份可以直接抄的复现清单
最后把上面零散的经验收拢成一份清单,如果你打算上手,照着走能少绕不少路。
必做项,一个都不能省:
- 梯度只在共享参数上取,任务头参数单独更新;
- $G$ 加岭正则,系数从 $10^{-6}$ 起调;
- 分母 $d_i$ 加下限截断,阈值配合梯度尺度设定;
- $\alpha$ 做 EMA 平滑,$\beta$ 从 0.99 起;
- 学习率重新标定,从原值的 0.5 到 0.7 倍开始试,配 warmup;
- $G$ 的内积用 fp32 算。
建议做,收益明显:
- 加"冲突诊断日志":每 N 步打印 $\alpha$、$G$ 的条件数、各任务与合成梯度的内积;
- 任务数 ≥ 5 时考虑分层或分组议价;
- 有先验期望时加正则项,别指望纯 Nash 解自动符合业务偏好;
- 前 2000 步可以把 $\alpha$ 强制为均权,等表征稳定了再启用议价。
别做的事:
- 不要在 AMP 下直接用 fp16 算 $G$;
- 不要先裁剪单任务梯度再算 $G$;
- 不要在多卡环境下用本地梯度各自算 $\alpha$;
- 不要把任务头参数一起 flatten 去算 $G$;
- 不要指望它能替代所有调参,学习率、batchnorm 结构、损失函数本身设计这些该调还得调。
我个人在实际操作中的体会是,这套东西的价值不在于"刷点",而在于把一组说不清道不明的超参替换成了一个有明确性质的优化问题。固定加权那五个数字,你说不出它们为什么是那五个数字;Nash 解给出的权重,至少你能解释它为什么是这个值——因为那个任务在当前梯度关系下,边际收益就是这个水平。这种可解释性在需要长期维护的多任务系统里,比零点几个点的精度提升更有价值。
另外提一个后续可以往下挖的方向:这套议价框架不限于任务之间。同一任务的不同数据子集、不同损失函数项,甚至不同模态,只要你能定义出各自的梯度,都能放进同一张谈判桌。我在一个多模态检索的小实验里试过把两个模态的梯度做议价,初步结果不算惊艳,但至少在模态不平衡的时候没有出现某个模态被彻底压死的情况,这条路值得再走走。