多任务学习里最折磨人的不是网络结构怎么搭,而是那几个损失函数怎么配平。我见过太多项目卡在这一步:模型结构选了最新的,数据预处理也做到位了,但就是训练的时候损失函数权重怎么调都不对,A任务涨一点B任务就崩,手动调参调了上百轮,最后发现最优解其实是一个“谁都别太满意”的妥协状态。这篇文章想聊的就是这个事——从手动调参的泥潭里走出来,看看多损失函数平衡到底有没有更聪明的解法。
项目标题里写的“从手动调参到自适应优化”,其实点出了整个领域的发展主线。最初大家都是拍脑袋定权重,后来有人研究数学性质做不确定性加权,再到后面出现了基于梯度、基于帕累托最优的自适应方案。这篇文章我按自己的实操经验,把这个演进过程拆开揉碎讲一遍,包括每种方法的适用场景、实现细节、踩坑教训,以及我目前在真实项目里推荐的做法。适合正在做多任务学习的算法工程师、研究生,还有那些被“多损失调参”折磨过但还没找到系统化思路的从业者。
1. 多损失函数的问题本质:不只是“调权重”这么简单
1.1 什么场景才会用到多损失函数
先给场景画个像。多损失函数不是炫技,是被需求逼出来的。常见的几种情况:
第一类是目标本身就有多个维度。比如一个内容推荐模型,既要预测点击率(二分类交叉熵),又要预测观看时长(回归损失),还可能叠加一个多样性指标和用户满意度建模。你把三个任务硬塞进一个共享底层网络,输出层各管各的,这时损失函数天然就是三个。
第二类是辅助监督信号。比如语义分割模型,主任务是像素级分类,但你会顺手加一个边缘检测的辅助损失,或者加一个深度估计的辅助分支,目的是让中间层学到更有判别性的特征。辅助损失和主损失之间怎么平衡,就是个经典问题。
第三类是GAN结构化建模。生成器的损失往往包含了对抗损失、感知损失(比如LPIPS)、像素重建损失(L1/L2)、还有特征匹配损失。这里面的权重比例之复杂,做超分辨率的同学应该深有体会——对抗损失太重图会花又有伪影,感知损失太重又会把纹理磨平。
第四类是引入正则化或先验约束。比如在损失里加显式的KL散度、对比学习项、物理约束项,本质上是“数据驱动”和“先验知识”两股力量的配平。
这些场景的共同点是:最终结果不是一个标量简单拼出来的,每个损失都在拽着模型往一个方向走,模型落在哪个位置取决于这些力的合力方向。所以问题表面上看是“怎么给权重”,本质其实是“这些力的方向冲突时,该听谁的”。
1.2 三个隐藏的坑:尺度、梯度、冲突
手动调参之所以痛苦,是因为有三层东西叠在一起干扰你,很多人只看到了最表面的那一层。
第一层是尺度问题。两个损失函数如果量纲不同,光数值范围就差出好几个数量级。比如一个L2回归损失动辄几十上百,一个交叉熵损失只有个位数,你直接相加,回归损失直接主导了梯度方向。传统做法是“标准化loss再相加”,但问题是两个loss的动态变化轨迹完全不同——一个在头50步内有剧烈波动,另一个在之后500步才慢慢启动,静态权重根本追不上这种动态变化。
第二层是梯度量级问题,这个更隐蔽。即使两个loss数值差不多,它们反向传播到共享层的时候,梯度范数也可能差出10倍甚至100倍。因为梯度不只是由loss数值决定的,还跟各分支网络的反传路径有关——分支网络越深,梯度消减越严重;输出层激活函数斜率差异也会放大这个差别。所以很多人发现“loss曲线都正常,但梯度方向早被一个大头绑架了”。
第三层是方向冲突问题。我做过一次可视化实验,把两个任务的梯度方向余弦相似度打成曲线。理想情况是两个任务梯度方向应该有一定一致性,但实际训练中经常出现负的余弦值,也就是梯度方向基本上是相反的。这种情况下无论你怎么调权重,A任务往前走一步,B任务就后退一点,模型最终只能在中间某个点上摇摆。这不是权重能解决的,必须用梯度手术这类方法来处理方向冲突。
所以说,多损失函数平衡要做好,必须同时处理“尺度”“梯度量级”“方向冲突”三层问题。单一手段往往只能解决其中一层,这也是为什么路线演进着演进着,就从“调标量权重”走向“操作梯度向量”。
2. 手动调参路线:固定权重的判断框架和实操套路
2.1 网格搜索之外的“参数联动”陷阱
先聊最朴素的做法——固定权重相加,然后网格搜索。总loss写成:
[ L = \sum_{i=1}^{n} w_i L_i ]
权重 ( w_i ) 无非是0.1、0.2、1、2、10这种量级。我真见过有人把这几个数排列组合,跑几十组实验天天盯着曲线看。问题不在于网格搜索本身,而在于参数之间是联动的,单独搜一个权重的局部最优没有意义。
举一个我自己的例子。之前做一个多任务模型,主任务是回归,辅助任务是分类。单独调回归权重时发现 ( w_{reg}=1.0 ) 最好,单独调分类权重时发现 ( w_{cls}=0.5 ) 最好,于是俩一起设上,结果整体效果反而比两个单项都差的组合还要差。原因很简单:这两个任务的梯度在共享层有冲突,单看每个权重的“边际效应”是好的,叠加起来冲突区域被放大,整体就崩了。
后来我用了一种更有效的粗筛逻辑:先把所有权重初始化成等量级(让每个loss的第一轮回传梯度大致同尺度),然后固定一个主线loss,其余loss统一乘一个很小的系数(比如0.01),先看主任务能不能稳住。稳住之后,再逐个放大辅助loss的系数,每次只动一个,观察主任务效果出现明显下降的临界点。这个临界点,才是这个辅助loss最合理的权重上界。
2.2 手动调参时真正应该盯的指标
手动调参阶段只看total loss曲线是不够的,你根本看不出哪个任务在拖后腿。我的习惯是至少盯着四个东西:
- 分任务loss曲线。不光看绝对值,还要看归一化后的变化趋势。
- 各任务在验证集上的指标。这是最终目的,loss曲线只是中间信号。
- 共享层各任务梯度的范数比。这一步需要你手动在反传时做一次hook,把梯度抓到。
- 梯度方向余弦相似度。这是发现冲突最快的方式。
我在实操中发现一个很实用的现象:如果某个任务在训练后期开始出现loss下降但验证指标不升反降的情况,大概率不是你过拟合,而是其它任务梯度把共享表征带偏了。这个时候你去抓一下共享层的梯度占比,基本能确认——某个任务的梯度范数远超其它任务。
用手动调参解决这个问题的常用手段是给这个“大头”任务降权重,同时看其它任务变化情况。这个过程极其耗时,因为一轮实验要等训练完毕才能看到结果,反馈回路很长。我后来把反馈回路缩短了:测梯度范数比不需要等完整训练,训练到200步左右(只要能越过初始化期)就能比较稳定地反映整体趋势。这样你想试探一个权重组合,跑个几百步就能判断大方向,不用等完整的数小时训练。
2.3 一个被低估的细节:权重的动态退化
固定权重最大的问题是“退化场景”。所谓退化,是指训练中后期某个辅助损失已经学得差不多了,loss收敛到很小的值,但它仍然占着一个固定权重的梯度配额,反向传播时依然在给模型施加“推力”。这个推力可能已经毫无意义,甚至会产生破坏性的噪声。
我印象很深的一个例子:一个OCR项目里用了CTC loss和注意力loss双路监督,训练前中期一切正常,到了后期CTC的loss已经降得很低,但它仍然占据了将近一半的梯度能量。结果就是模型在注意力路径上表现持续提升,却因为CTC那部分“微调式”的梯度干扰,始终无法突破一个精度瓶颈。我当时的解决办法很粗暴——训练到一定轮数后,手动把CTC权重乘0.1。效果立竿见影,但这也暴露了固定权重策略的天花板:它没有办法感知“哪个loss的使命已经完成了”。
这个“动态退化”问题,正是自适应优化方法存在的第一个理由。
3. 自适应优化的三大流派:不确定性加权、GradNorm、梯度手术
3.1 不确定性加权:数学优雅但灵活性不足
先讲最有名也最容易被滥用的方法——基于同方差不确定性(homoscedastic uncertainty)的加权策略。它的核心假设是每个任务都有自身的观测噪声,用一个可学习参数来衡量噪声水平,从而实现自动加权。
公式不细写,直觉理解:那个可学习参数相当于“自动调权重”的把手,优化器在更新模型参数的同时,也在调整这个噪声参数。噪声大的任务自动降权重,噪声小的任务自动获得高权重,非常符合直觉。
我实际跑下来,这个方法的优点是几乎不需要初始化经验——初始权重设成1也不会出大问题,训练中它会自己调整。缺点也很明显:它假设损失服从高斯分布(或拉普拉斯分布),对于交叉熵这种非对称损失,数学上的解释力比较弱。实操中另一个问题是它调整权重的速度太慢,训练500轮以后它才慢慢把权重调到合理区间,前期的训练质量基本取决于初始权重——这就有点本末倒置了。
还有一个更隐蔽的问题:如果某个任务的loss已经被优化到很低,它的噪声参数会变得非常小,反过来导致权重变得极大。这是一个正反馈循环——权重一大,梯度推得猛,loss更低,噪声参数更小,权重更大。一旦陷入这种循环,其它任务基本就废了。我见过好几篇开源代码里用了不确定性加权但训练不稳定,最后作者默默固定了那个噪声参数,实际上就是手动封死了这个正反馈。印象里有个项目里加上之后整个行业“一个偶然的X,改变了原本的正常演进路径”。这是后话。
结论:不确定性加权适合做“温和的自动调节”,适合那些各任务loss尺度本身比较接近、不会出现极端情况的场景。如果任务间尺度差出几个数量级,它调节的速度和幅度都不够。
3.2 GradNorm:直接对梯度做文章的关键一步
GradNorm是我个人认为多损失平衡的最重要里程碑。它的核心洞察非常直接:不要再盯着loss数值了,要盯着梯度范数。因为模型最终是靠梯度更新的,你真正要平衡的是梯度对共享层的影响,而不是损失函数本身。
算法每步的计算逻辑大致是:计算各任务loss对共享层的梯度范数,取平均得到目标梯度范数,然后用一个可学习的权重参数去逼近这个目标。它是反传两次的——一次正常反传更新模型参数,另一次只更新权重参数。
实操中的几个经验:
第一,GradNorm在训练初期比不确定性加权更灵敏,它能在几十步内就感受到不同任务梯度的差异并开始调整权重,不像不确定性加权那么迟缓。
第二,GradNorm有一个超参数α,作用是控制“任务间梯度范数差距被拉得多平”。α=0时所有任务梯度范数会被拉到完全相等,α越大表示允许更大的差距。我一般推荐0.5到1之间,不要用太小的值,因为完全拉平会让“本来就难学的任务”丧失梯度优势,反而不利于收敛。
第三,GradNorm最容易被忽略的是“恢复机制”。因为权重是连续可学习的,理论上它可能退化到某个任务权重过高或过低的极端状态而不能自行恢复。我的做法是给权重加一个“保险丝”:定期检查各任务权重的相对比例,如果某个权重的指数级滑动平均值偏离初始值超过预设阈值,就手动把它重新映射回合理范围。用大白话说,GradNorm毕竟是局部贪心算法,偶尔需要外部干预一下。
GradNorm的局限在于它只考虑“梯度范数”,不考虑“梯度方向”。如果两个任务梯度方向夹角大于90度,即便范数被拉平,方向冲突依然存在——模型更新的方向是两股力的矢量合成,有可能谁都照顾不好。
3.3 梯度手术:面对冲突的直接解法
如果说GradNorm管“大小”,那梯度手术管的是“方向”。这类方法的灵感来自一个朴素的观察:如果两个任务的梯度在共享层方向上是相反或夹钝角的,那么无论怎么调权重,合力的方向都不理想。不如直接在反向传播的时候把其中一个梯度朝另一个梯度方向投影,剔除掉冲突的分量。
这类方法的典型代表有PCGrad(Projecting Conflicting Gradients)和GradDrop。我实际用下来PCGrad的效果更稳定,而且实现也不复杂。核心操作是:计算两个任务的梯度,如果方向夹角为负(余弦值为负),就把一个梯度投影到另一个正交方向上去,表达式形式上写就是梯度的冲突分量被减掉。
这里有一个重要的经验:梯度手术不能单独用,必须和“梯度范数平衡”配合。原因很简单——如果只做方向剔除而不管大小,一个大尺度loss的梯度即便把所有冲突分量都剔掉,它剩下的“无害”分量依然可能比另一个任务的完整梯度大很多,模型还是会被它带偏。所以正确组合我一般是:
- 先用GradNorm之类的范数平衡方法,让各任务梯度在量级上先对齐。
- 然后用PCGrad处理方向冲突,剔除因为冲突而互相抵消的部分。
- 最后正常更新。
这套组合我跑过多个项目,几乎都是效果稳定的。但代价是实现复杂度上去了——调试GNN要看的不是单个loss曲线,而是梯度分量张量。这个“把梯度可视化”的习惯我会在后面详细说。
3.4 降级方案与工程折中:动态加权怎么做才不崩
自适应优化听起来很完美,但工程上不一定每次都有条件上马完整方案。特别是你已经有一套训练框架,要改动反传流程,涉及框架内的hook和自定义梯度处理,团队的代码基础如果不支持,这个改动成本可能很高。
这种情况下我推荐两个“打折版”方案。
第一种是“分段动态加权”。不需要改实时梯度,你按固定间隔(比如每50步)统计各任务loss的滑动平均值,然后直接缩放权重。缩放的方式可以简单到按“当前loss除以初始loss”的比例来调节——哪个任务下降得慢就给它更大权重,下降得快就降一点。这个方法虽然不精细,但非常稳,尤其适合任务间复杂度差异本来就比较大的场景。
第二种是“自适应调度”。用一个外部控制器(可以是简单的PID控制器),输入是各任务当前梯度范数与目标梯度范数的偏差,输出是对应权重的调整量。这个思路本质上模拟了GradNorm的作用,但实现完全在训练循环外面做,不需要动反向传播的任何代码。我在很多工业项目里就是先用这种降级方案跑通基线,后续有精力再换成真正端到端的GradNorm。
工程折中的核心原则只有一条:先解决“能不能稳定跑”,再追求“最优解”。很多团队一上来就折腾复杂自适应方案,结果连基线都复现不了,收益远小于成本。
4. 实操记录:从一个具体的多任务模型说起
4.1 任务定义与初始配置
给一个我自己反复调过的例子,方便大家理解前面这些方法到底怎么落到代码上。任务是一个短视频推荐场景下的多任务模型,三个输出头:一个预测点击率(CTR,二分类交叉熵),一个预测观看时长(WATCH,回归,用均方误差),一个预测点赞率(LIKE,二分类交叉熵)。
共享底层是一个简单的多层全连接网络,这是为了便于复现,真实场景换成Transformer、DeepFM任意结构都可以。初始配置我用的是等权重相加——每个权重视作1。这个配置的问题非常典型:MSE那个loss初始值大约在几十的量级,交叉熵loss初始值在0.6到0.7左右,两者直接相加,梯度几乎完全由MSE主导,CTR和LIKE在刚开始的几百步就等于在陪跑。
4.2 梯度可视化:先把问题看透再动手
前面说过,我建议任何多损失项目都先做一次“梯度可视化”诊断,而不是直接上方法。具体做法是,在完成backward之后,把共享层(或者最后一个共享特征层)的梯度张量拿出来,计算每个任务这部分梯度的L2范数,并记录到日志里。
伪代码如下:
def record_grad_norm(model, task_id, grad_dict): shared_layer_grad = grad_dict[idx] norm = torch.norm(shared_layer_grad).item() print(f"task {task_id} grad norm: {norm}")一个训练周期跑下来,你会得到类似这样的表格:
| 训练阶段 | CTR梯度范数 | WATCH梯度范数 | LIKE梯度范数 |
|---|---|---|---|
| 初始100步 | 0.8 | 11.2 | 1.1 |
| 中期1000步 | 0.6 | 4.3 | 0.8 |
| 后期5000步 | 0.4 | 1.9 | 0.7 |
第一眼就能确认:WATCH任务的梯度范数一直是CTR的几倍到十几倍,说明预测时长这个任务在共享层握着极大话语权。如果不处理,最终模型学到的共享表征会过度偏向“时长预测”相关的特征。
再进一步,如果算CTR和WATCH两个任务梯度的余弦夹角,会发现中后期夹角在正负之间剧烈震荡,说明存在明显的方向冲突。看到这个结果,你就知道光靠调固定权重解决不了问题——因为两个任务的方向在打架。
4.3 逐步实施自适应方案
我的实际步骤是分阶段进行的。
第一阶段,先用GradNorm替换固定权重。实现步骤大致是:
- 定义每个任务的可学习权重参数(初始为1或等量级)。
- 反向传播时分别计算各任务对共享层的梯度范数。
- 计算平均梯度范数与相对下降速率(即当前loss相对初始loss的下降倍数)。
- 构造GradNorm损失函数,只优化权重参数,不更新模型主参数。
- 主模型参数仍然用原始总loss进行反向传播更新。
这里要注意的是,PyTorch中做这件事需要非常小心地把两个反向传播的时机错开。我习惯的做法是,先用torch.autograd.grad手动计算各个任务loss对共享层参数的梯度,然后利用这些手动梯度做GradNorm的权重更新,再正常执行loss.sum().backward()更新模型参数。
第二阶段,在GradNorm稳定之后,我再叠加PCGrad处理方向冲突。PCGrad的核心代码如下,非常短:
import torch def pcgrad_update(grads): # grads 是所有任务的梯度向量 pc_grads = [] for i, g_i in enumerate(grads): for j, g_j in enumerate(grads): if i != j: dot = torch.dot(g_i, g_j) if dot < 0: g_i = g_i - torch.dot(g_i, g_j) / torch.dot(g_j, g_j) * g_j pc_grads.append(g_i) return pc_grads注意这里有个前提:必须先把每个任务的梯度从共享层“抽”出来,才能做向量间的投影运算。很多框架里默认总梯度的shape是平铺后的参数向量,你直接拿着用就行,但要注意各任务分支参数要排除在外——PCGrad一般只对共享层参数做冲突消解,各任务独立分支的梯度不该被“手术”掉。
我实际操作中发现,叠加PCGrad之后,训练曲线并不会立刻变得更好看,反而可能在前几百步显得更“保守”——因为冲突分量被剔掉,模型更新更稳但步子更小。但一般跑到中期之后,你的验证集指标会不知不觉超过之前的所有方案。这个“先慢后快”的特点,需要在用的时候心里有底。
4.4 参数选择与训练细节备忘
针对GradNorm的α参数,我在这个例子里用0.5。有一个细节可能很多文章没提:GradNorm的权重初始化会影响它的调节方式,一般建议初始权重让各loss的反传梯度在共享层大致等量级,而不是简单地初始化为1。所以我实际代码是先跑一小段等权重warmup,看一下各任务的初始梯度范数,然后反推一个合适的初始权重,再启动GradNorm。
训练上,我还会额外加一个“权重平滑”操作——对GradNorm输出的权重做指数滑动平均(EMA),避免权重突变导致训练震荡。这个滑动平均的系数用0.95到0.99都可以,太大会让权重几乎不更新,太小又没有平滑效果。
这个项目的最终配置大概是:GradNorm + PCGrad + 权重EMA,主损失在总loss中的占比最后稳定在0.5左右,辅助的两个任务合起来另外一半。相比最初等权重相加的方案,CTR和LIKE这几个任务的最终验证指标都有明显提升,而WATCH反而略降了一点——这就是多任务平衡的本质:允许单一任务做出一点牺牲,换整体效果的最大化。
5. 常见问题与排查技巧实录
5.1 问题一:自适应权重越调越极端,甚至出现负值或NaN
这个是我被问过最多的问题。GradNorm或不确定性加权训练过程中,权重参数可能被推到几十甚至几百,或者出现NaN。原因多数是优化器更新幅度太大,或者梯度范数的计算在某个分支上产生了极大值。还有一个很少被提到但很容易中招的坑:早期训练时某些任务loss恰好为0或者极小,导致相对下降速率项爆炸,进而带动权重爆炸。
排查思路:
- 先检查各任务loss有没有出现0或极小的异常值,有的话在计算相对下降速率时加上一个epsilon。
- 对权重更新步长降一个数量级,观察权重曲线波动。
- 给权重加一个clip,比如限制在[0.01, 10]之间,牺牲一点自适应能力换稳定性。实践中这个保守方案的效果往往不会比完全不限制差多少。
5.2 问题二:加了PCGrad之后总loss不降反升
这几乎是我用PCGrad必经历的一关。原因是PCGrad剔除了冲突分量,而部分冲突分量其实是“良性冲突”——包含了对不同任务都有帮助的共享信息。全剔除会丢失这部分信息量,导致总loss的降低速度变慢。
我踩过几次坑之后摸索出来的一个技巧是:不要在全训练周期都启用PCGrad,而是只在训练中后期启用。前期让各任务自由“探索”一下共性特征,后期再用PCGrad解决恶性冲突。启用时机大概在总训练步数的40%到60%之间,具体看验证集指标的增幅变化。这个“后期启用”的改动,在我几个项目里都明显改善了下垂。
5.3 问题三:多任务模型在验证集上A涨B跌,怎么办
这个问题最让人头疼,也最容易让人直接放弃自适应方法。我的经验是:先分清“涨跌”的原因,再判断是否需要解决。
如果A涨B跌的幅度都在几个百分点内,这可能只是多任务优化中常见的“震荡”,说明两个任务的难度接近、模型在不同时期偏向不同。这时你不需要干预,继续训练可能自己就会回到均衡点。
如果B连续多个epoch都在跌,那就不是震荡而是“竞争”。此时首先检查B任务本身的loss——如果B的loss在降但验证指标在跌,说明B可能是过拟合,这时候调权重反而无解;如果B的loss在涨或者不降,大概率是共享表征被A任务带偏了。这时候可以用GradNorm把B任务的权重稍微提上去,并观察共享层的梯度比例变化。
5.4 问题四:自适应方法在分布式训练中不稳定
这个方法很少有人讲,但真实场景里分布式训练非常常见。GradNorm这类方法极其依赖梯度范数的计算,而梯度范数是batch级别统计量。在数据并行场景下,每个设备上的batch不同,各任务梯度范数是同分布的随机变量,自适应权重更新会因此出现额外的方差。
我的处理方式是:在分布式场景下,把自适应权重参数的更新频率降低,原来每步更新,改成每5步或每10步更新一次,并且权重更新时用的梯度范数取多个本地batch的均值,降低方差。如果框架足够灵活,更推荐把所有rank的梯度范数全部聚合到主卡计算权重更新,然后再广播回去,这样权重更新是完全一致的。
5.5 问题五:什么时候根本不该用多损失自适应
最后这个问题可能比很多人想象的更重要。不是所有多loss场景都需要自适应方案。如果两个任务本身高度相关,梯度方向基本同向,那固定权重完全够用,你把时间花在别的地方更划算。自适应方法的核心价值在于处理“动态变化的、冲突明显的”多梯度关系,如果你的任务关系简单且静态,自适应就是额外增加系统复杂度而非价值。
这也是我在文章开头强调掌握判断力的原因——多任务学习的问题不是“用什么方法”,而是“这个场景值不值得上方法”。对复杂度有限的场景,用最简答的等权重开始,持续监测梯度表现,不行再升级,往往比一上来就堆新鲜技术更靠谱。
6. 实操心得与未来演进方向
回顾这个多损失平衡策略的发展脉络,我很明显的一个感受是:这个领域一直在从“手动经验”走向“自动系统”。手动调参的瓶颈不只是“累”和“慢”,更是因为它无法处理动态变化和多目标冲突的真正复杂性。GradNorm和PCGrad代表了两条方向的尝试——从数值优化层面和从冲突化解层面,各有侧重点。
我个人在实际项目里的做法,已经逐步固化成一套模板:先做梯度可视化诊断,如果梯度范数和方向都正常,就用固定权重,保持系统简单;如果有明显的梯度失衡和方向冲突,就启用GradNorm + PCGrad的组合,并把最后的权重更新频率放低,保证稳定性。这套流程的好处是每一步都有明确的决策依据,不会陷入盲目调参。
这个方向后续还可以往几个地方扩展。一个是把多任务权重的自适应和超参数优化框架结合,用更全局的视角来做,而不是每一时刻的局部贪心。另一个是探索任务的停止和忆失控制——正如前面提到的“动态退化”问题,多任务学习中如果一个任务已经学好,理想的做法是让它退出竞争,而不是永远占着梯度配额,现在很多方法并没有很好处理这个生命周期问题。
每次被朋友问到多损失怎么调参,我都会说,你可以从手动调参开始找感觉,但最终要认真看一下GradNorm这类方案。多损失平衡不是一个一次性设定的事,而是一个跟训练节奏同步变化的动态过程。理解了这个动态,你的模型才能真的“多任务协作”而不是“多任务内耗”。