资讯详情

增量目标检测的梯度分解与对齐:化解新旧类优化冲突

📅 2026/10/19 1:49:56 | 华诺云谱 👁 阅读
增量目标检测的梯度分解与对齐:化解新旧类优化冲突
增量目标检测是我觉得所有持续学习任务里最拧巴的一个场景。分类模型做增量学习只需要让模型记住新类别的判别边界但检测器不一样同一个画面里可以同时出现旧类和新类模型得一边学新东西一边还得保住旧东西而且目标检测的分类头在联合预测时天然就存在竞争。最近看了这篇关于梯度分解与对齐Gradient Decomposition and Alignment的增量目标检测论文我觉得它把优化层面的冲突问题讲得很直观适合所有跟目标检测增量场景打交道的同学参考。这篇工作的核心不是加多少条分支也不是堆数据集回放而是从梯度这个最底层的东西入手新类训练时产生的梯度里哪部分在破坏旧类哪部分是无害的怎么把这些方向理清楚。下面我用自己的理解把这个方法从头到尾拆开来聊。1. 增量目标检测的麻烦比你想的更“上头”1.1 从图像分类到目标检测问题为什么升级增量学习里最经典的问题是灾难性遗忘这个在图像分类上已经研究很多了。模型学完A类再学B类学B类的过程中A类的表征被覆盖最后A类精度崩盘。分类任务里一张图只有一个标签类别之间的竞争发生在全局特征层面你只要保证新类学习时不把旧类的决策边界冲掉问题就解决了一大半。但目标检测不一样。检测器要同时回答两个问题这个框里有没有物体、它是什么这个物体的边界框在哪里。前者是分类头的事后者是回归头的事。增量场景下新类别出现时模型不仅要学会识别新物体还要保持对旧物体的定位能力。更麻烦的地方在于目标检测的输入是一整张图图里可能同时存在旧类物体和新类物体。比如一个自动驾驶场景之前学会了检测小轿车现在要学检测卡车结果一张训练图里既有小轿车又有卡车模型对整张图做前向传播时分类头的梯度会把两个类别的信号混在一起。这种多标签共现的情况在分类任务里几乎不会遇到但在检测里是常态。你拿一张含新旧类物体的图去训练分类损失会把旧类物体也当作负样本或正样本计算一遍梯度信号互相影响旧类分类面的漂移就不可避免。所以很多在分类上有效的增量方法搬到检测上效果立刻缩水原因就在这里问题的结构和梯度耦合方式完全不同。还有一个隐蔽的坑是回归头。大多数人做增量检测只盯着分类精度但其实边界框回归也会遗忘。旧类物体的位置分布如果比较特殊学新类时回归层的参数被新类样本带偏旧类的定位精度也会下降。只是回归头的遗忘没有分类头那么明显因为回归任务本身是连续的漂移一点可能也就几个像素的IoU差距但不代表它不存在。1.2 直接微调为什么一定翻车我自己刚接触增量检测时干过一件蠢事拿新类数据直接在训练好的检测器上微调。当时觉得旧类数据反正还在硬盘里随时可以拿来混合训练应该问题不大。结果旧类mAP掉了十几个点新类还学得一般。原因其实不复杂。检测器的分类头通常是一个全连接层或一组卷积输出每个anchor或proposal在各个类别上的得分。微调新类时新类样本的梯度会让分类权重朝着“把新类分对”的方向移动这个方向和之前“把旧类分对”的方向往往不一致。尤其是新类数量远小于旧类数量时梯度更新会偏向新类旧类权重被快速覆盖。更坑的是检测训练里的正负样本极度不均衡。一张图里几万个anchor真正匹配到目标的只有几十个。新类刚开始训练时模型对它们完全不敏感大部分新类物体被当作背景梯度主要被背景样本主导。这种梯度对旧类和旧类的背景区分都是有害的因为它本质上在强化“物体背景”的错误判决。如果你不做任何约束微调几个epoch后模型就会变成一个“只认新类旧类全部漏检”的偏科选手。所以增量目标检测的核心矛盾变成了新类特征的引入一定会改动共享特征提取器而共享特征的任何改动都可能波及其他类别的分类超平面。怎么让改动朝着不伤害旧类的方向走这就是梯度分解与对齐这篇论文最本质的出发点。2. 论文核心出发点先把优化方向“打架”这件事讲清楚2.1 梯度方向冲突的几何直觉梯度是什么它告诉你损失函数下降最快的方向。在检测器训练里一个训练批次算出来的梯度本质上是一堆参数更新的合力。旧类训练时模型学到的权重处在某个局部最优附近新类训练时新的梯度会把这个权重往另一个方向推。这里有个生活化的类比。两个人一起拉一块很重的木板一个人想往东拉一个人想往西拉。如果两个人的力完全相反木板基本不动但两个人的力气都白费了。如果一个人的力稍微偏向南另一个偏向北木板就会走一条歪歪扭扭的路线最后到的位置谁都不满意。深度网络训练也是这个道理。旧任务把模型参数推到了一个能识别旧类的位置新任务的梯度想把参数推到能识别新类的位置。如果这两个方向夹角大于90度它们在共享层上的更新就是互相抵消的。抵消的过程中旧类的特征分布被破坏表现为旧类精度下降。直观上看这种现象就是“梯度冲突”梯度向量之间的余弦相似度是负的方向互相拉扯。这种冲突在检测里尤其剧烈因为一个batch里的新旧类梯度不是分开算的。你输入一张含新旧物体的图分类头同时对新旧类产生预测误差误差反传后在特征层叠加出的梯度方向完全不可控。你可能以为自己在学新类但一部分梯度其实在拆旧类的台。2.2 梯度分解把新类梯度拆成“有害分量”和“无害分量”论文标题里的“梯度分解”就是冲着上面这个问题去的。它要做的事就是把新类样本产生的梯度在参考方向上拆成两个分量一个分量和旧类优化的方向同向或者垂直不会破坏旧类另一个分量和旧类方向相反是真正的有害部分。怎么拆思路其实很朴素。你可以把新类梯度看作一个向量把旧类梯度或者旧类分类权重的方向看作另一个参考向量。任何一个向量都可以分解为相对于参考向量的两个正交部分投影分量和垂直分量。如果投影方向和参考方向相反说明这个梯度在主动撤销旧类学到的东西那就把这一部分抑制掉或者反转垂直分量则基本不影响旧类方向可以放心保留。这里的“旧类梯度”从哪来训练时通常拿一小部分旧类样本或者旧类分类头的权重做参考。更简单一点的做法是记录模型在旧类任务收敛时分类头的权重方向把它当作旧类决策面的法向量。新类梯度对这个方向的投影如果为负就说明它在破坏旧类决策面。这个判断起来很直接不需要额外的网络结构也不需要额外的标注。需要说明的是具体到论文实现里分解的形式比我这里描述的要精细一些。它可能不仅把梯度分成二维还会考虑多个类别之间的交互关系分解出跟所有旧类都相容的部分和跟某个特定旧类冲突的部分。但核心思想不变找到冲突然后把冲突分量削弱或剔除。2.3 梯度对齐提醒模型“旧任务的方向不能反着走”分解只是第一步。你只把有害分量丢掉剩下的梯度方向还是可能跟旧类优化方向偏离。打个比方你把往西拉的力去掉了剩下的力可能往北偏虽然在东西方向上不跟别人冲突了但总方向还是歪的。要让模型整体走得更顺还需要一个“对齐”动作把当前梯度的方向往旧类梯度的方向拉近一些。梯度对齐的做法通常是用一个正则项惩罚当前梯度与参考梯度之间的负余弦相似度或者直接奖励它们的余弦相似度。这个正则项会在训练损失之外增加一个约束你可以学新类但你的优化方向不要跟旧类的优化方向对着干。实际操作里对齐项的强度不能太大。太大会导致新类学不动因为模型每次更新都想往旧类方向靠太小又起不到约束作用。这个强度系数的调节是复现整篇论文时最关键的超参数之一后面我会专门聊。所以把这两个词连起来看就清楚了梯度分解是“先把打架的成分摘出来”梯度对齐是“让剩下的成分顺着旧方向走”。一个管当下一个管方向两者配合才能既学新类又保旧类。3. 方法机制的通俗拆解3.1 训练流程全貌我按自己的理解把这篇论文的训练流程还原成下面这个步骤序列拿到一个已经在旧类上训练好的检测器模型准备一小批旧类样本或者用旧类分类器权重的近似向量前向一遍算出旧类方向的参考梯度输入新类训练样本正常前向、正常算分类损失和回归损失在分类头位置对分类损失求梯度得到当前新类训练方向把当前梯度分解到参考方向上识别出有害分量对有害分量做抑制同时对整体梯度方向做对齐约束用处理后的梯度更新模型参数。整个过程里推理阶段的代码完全不用动。模型结构也不增加额外分支不增加任何推理开销。所有工作都发生在训练阶段的反向传播过程中本质上是一种训练时的梯度修正策略。这跟很多增量方法有本质区别。像知识蒸馏这类方法需要在训练时额外拉一个教师模型做输出对齐占用显存和计算资源像特征回放这类方法需要额外存特征缓存而梯度分解与对齐的思路更像是在原有优化器上做了一次“梯度清洗”成本很低插入也很灵活。3.2 分类头是主战场回归头为什么一般不做论文把主要注意力放在分类头上我觉得是非常合理的判断。回归头虽然也会遗忘但它面临的问题跟分类头不同回归的输出是连续的坐标偏移它的更新方向基本由目标框的真实位置决定不存在“类别决策面互相挤压”的问题。用一次增量训练的常用策略——限制学习率、对回归损失做梯度裁剪甚至简单地对回归头做特征对齐——就能把回归遗忘控制在一个可接受范围内。分类头则是完全另一回事。分类头的每个类别对应一个权重向量所有类别共享同一个特征空间。新类加入后分类头的权重矩阵直接多了一行这个新增维度的训练势必挤压原有维度的几何关系。可以说增量检测的灾难性遗忘大头就在分类头尤其是最后的线性分类层。论文聚焦分类梯度等于直接打在问题的最痛点上。我在复现时通常先只改分类头把分解对齐模块挂在分类损失的反传路径上回归头一切照旧。实测下来这样改动最小而且效果提升点基本都来自分类头。如果后续想进一步压遗忘再考虑给回归头加一个轻量的旧类样本约束。3.3 一个PyTorch风格的简化伪代码下面这段是我为了讲清楚思路写的PyTorch风格示意代码不代表论文原始实现但结构上能帮助你理解梯度分解和对齐是怎么挂进训练流程的。# 伪代码梯度分解与对齐的简化流程 # old_grad_ref 可以用少量旧类样本计算也可以用旧类分类权重向量代替 for batch in new_task_loader: images, targets batch # 1. 正常前向计算新类分类损失 logits model(images) loss_cls classification_loss(logits, targets) loss_reg regression_loss(logits, targets) # 2. 对共享特征或分类头参数求新类梯度 new_grad torch.autograd.grad( loss_cls, model.cls_head.parameters(), retain_graphTrue ) # 3. 计算当前梯度与参考梯度旧类方向的余弦相似度 cos_sim cosine_similarity(flatten(new_grad), flatten(old_grad_ref)) # 4. 分解如果投影方向与旧类方向相反抑制冲突分量 proj projection(new_grad, old_grad_ref) conflict clamp(proj, max0) # 只保留反向冲突部分 safe_grad new_grad - conflict # 去掉有害分量 # 5. 对齐增大余弦相似度让当前方向尽量靠近旧类方向 align_loss - cos_sim # 6. 用修正后的梯度更新参数 model.cls_head.zero_grad() # 手动更新或用 modified_grad 组装优化器 # 具体来说可以先用 safe_grad 做一次参数更新再额外用 align_loss 反传一次代码里的projection和clamp就是分解的核心把当前梯度投影到旧类参考方向找出投影中反向的部分然后从原梯度里减掉。这样做的效果是如果当前梯度在旧类方向上有反向分量这部分不会真正加到模型参数上从而减少对旧类决策面的破坏。当然直接对参数梯度做修改需要小心你需要确定是对哪一部分参数求梯度是在哪个网络层做分解以及怎么跟优化器的更新步骤衔接。这些细节我在第5节会展开。3.4 超参数少、插入成本低这类方法最讨喜的一点是超参数少。核心的就是一个对齐强度系数通常记为lambda范围一般在0.1到1左右。你可以把它理解成“模型多听旧类方向的话”lambda太大新类梯度几乎被旧方向锁死新类学不进去lambda太小对齐约束形同虚设旧类照样遗忘。我自己的经验是先用0.1起步观察旧类mAP会掉多少。如果旧类mAP掉得厉害就加大lambda如果新类mAP明显偏低就减小lambda。调几次就能找到一个平衡点。这个调参过程比调一堆蒸馏温度、特征对齐权重省心太多。另外梯度分解的部分基本没有额外超参抑制就是抑制不需要权衡强度因为你只丢掉反向冲突分量。这也是这个方法工程上友好的原因你想引入它不需要对原有训练代码做大手术只要在反向传播时多算几步梯度操作。4. 实验怎么看别被绝对值带偏4.1 增量检测的两种典型评测协议增量目标检测的实验设置通常很严格。常见做法是把一个公开检测数据集的类别分成几批比如初始给20类之后每批新增5类或10类模型按批次依次学习。每一批学习完成后测试时会对所有已经见过的类别做mAP评估看旧类掉了多少、新类涨了多少。这个协议里有几个残酷设定。第一新批次训练时不能访问旧类原始数据最多允许保留极小比例的缓存样本否则就是rehearsal方法要单独说明。第二批次之间不能回退模型学完第二批就不能再去调整第一批的权重除非通过增量方法本身的约束机制。第三测试时新旧类混合评估mAP变化能直接反映遗忘程度。所以读这类论文的实验结果时不要只看最后一行的“总体mAP”那个数字容易掩盖问题。某些方法可能新类mAP很高但旧类mAP已经崩了总体数字看起来还行实际不可用。要特别关注“旧类mAP的保持率”和“每批结束时旧类mAP的轨迹”这两个才是增量检测的灵魂指标。4.2 效果指标怎么读遗忘率与增量精度我读增量检测论文时一般先看三个数初始批次训练完后的旧类mAP基线最后一轮训练结束后的旧类mAP新增类别最终mAP。第一个和第二个的差值就是遗忘量。如果遗忘量控制得很低比如只掉两三个点说明方法的梯度保护是有效的如果掉了十个点以上那不管新类学得多好都有点得不偿失。第三个数字衡量的是新类学习能力。增量检测天然存在新旧类之间的跷跷板你特别保护旧类新类可能学不进去你放开手脚学新类旧类又崩。好的方法应该让两个数字都处在可接受水平而不是寅吃卯粮。这里要特别提一句警告不要执着于论文里的绝对数值。同一个方法换一个检测骨架、换一个优化器、换一个随机种子绝对mAP能差出好几个点。复现时真正要对比的是相对趋势——加了这个模块和不加这个模块在完全相同的训练设置下旧类遗忘是不是变少了新类学习是不是没有明显变差。4.3 与蒸馏、回放、参数正则这些方法放在一起看梯度分解与对齐不是一个“取代一切”的方案它更像是增量学习工具箱里的一把扳手。知识蒸馏类方法解决的是“输出保持”问题适合在模型层面做软约束回放类方法解决的是“数据缺失”问题适合在数据层面补信息梯度类方法解决的是“方向冲突”问题适合在优化层面做干预。三者完全可以叠加。我自己的经验是蒸馏和梯度对齐一起用效果最好。蒸馏负责把旧模型在旧类上的输出分布留住梯度负责防止新类训练时共享特征的优化方向跑偏。这两个机制一个管结果一个管过程并不冲突。相比之下回放类方法因为要额外维护缓存和采样策略工程复杂度更高但它在旧类数据非常稀缺时依然是最稳定的兜底方案。5. 复现和工程落地时的实操细节5.1 把梯度算对retain_graph、梯度归一化、参考梯度的来源复现这类方法第一步坑就藏在torch.autograd.grad里。你在计算完新类损失后想对分类头参数求梯度但这个操作会把计算图释放掉后续如果想再反向传播一次做对齐项就会报错。解决办法是用retain_graphTrue或者把第一次求梯度改成create_graphTrue以便后续继续对梯度本身求导。还有一个细节是梯度归一化。分类损失和回归损失的尺度不一样如果直接算所有参数的拼接梯度分类部分的梯度分量可能被回归部分的数值淹没。所以我建议做梯度分解前先把参与分解的梯度向量做L2归一化。这样余弦相似度才有意义不然数值范围一变对齐约束就形同虚设。参考梯度的来源也有讲究。最省事的做法是存旧类分类权重向量但这个向量只能代表分类头的静态方向不包含特征层的信息。更准确的做法是从验证集里抽一小批旧类样本比如32张在训练开始时前向一次对分类头参数求一个平均梯度把它当作旧类参考方向。这个参考梯度可以固定不动也可以每隔几个epoch重新计算一次后者能更好地反映旧类决策面随训练发生的变化。5.2 超参数调节的先后次序我调这个方法的超参数时会按固定顺序来避免多个变量同时改动导致不知道是谁的功劳。第一步固定对齐强度为0相当于只做梯度分解不做对齐先看分解本身能把旧类遗忘压到什么程度。这一步能帮你确认代码里的分解逻辑是不是真的生效。第二步以0.1为起点逐步调大对齐强度观察旧类mAP和新类mAP的交点。找到一个“新类不掉太多、旧类掉得可接受”的值。第三步如果旧类mAP还是掉先不要无脑加大对齐强度而是检查参考梯度的质量。参考梯度如果来自类别严重不均衡的旧类样本方向本身就有偏差对齐反而会引入错误约束。换一批均衡的旧类样本往往比调lambda更有效。5.3 现有检测器框架怎么低成本接入如果你想在现成的检测框架里接入这类梯度操作不需要改模型结构只需要在训练函数里动手。大致思路是计算完分类损失后在反传之前插入梯度分析逻辑。第一种做法是手动计算分类头参数的梯度做分解和修正然后用optimizer.zero_grad()和自定义参数更新代替正常反传第二种做法是把对齐损失直接加进总损失让框架自动反向传播但这样分解逻辑就不好插进去因为你要在梯度层面做操作。我更推荐第一种做法也就是把梯度当成一个可以被修改的中间变量来处理。现代深度学习框架都支持在反传前拿到梯度你完全可以“截胡”一下改完再交给优化器。本质上这跟GAN训练里的梯度惩罚操作是同一套路子写过相关代码的应该很快能上手。5.4 三个容易踩的深坑第一个坑是batch里没有旧类样本。增量训练的新批次数据通常只含新类如果你在计算旧类参考梯度时试图从当前batch里找旧类样本会发现一个都找不到。解决办法只能是外部维护一个旧类缓存。没有旧类缓存的情况下就只能用旧类分类权重向量做参考效果会打折但不会完全失效。第二个坑是梯度分解的作用对象没选对。如果你对整张网络的参数做分解计算量巨大且容易把底层的通用特征梯度也处理掉反而损害旧类在浅层的共性表征。我建议只在分类头附近或特征金字塔的顶层做分解效果更精准计算开销也可控。第三个坑是对齐项和蒸馏损失互相“打架”。如果同时用了蒸馏蒸馏损失本身会要求模型输出接近旧模型这已经是一个很强的约束再叠加梯度对齐可能会让模型在新类上学习速度过慢。遇到这种情况把对齐强度适当调低或者采用分段策略训练前期侧重对齐保护旧类训练后期逐步放松对齐让新类更充分学习。6. 常见问题排查速查6.1 旧类mAP掉得依然很快先检查是不是参考梯度本身有问题。旧类参考梯度如果是从单一类别、少量样本算出来的方向可能被个别样本主导并不代表全体旧类的优化方向。建议每类至少保留8到16个样本算平均梯度。另外检查分解是否真正生效可以在训练日志里打印冲突分量的模长如果一直是0说明新旧梯度方向根本没有反向冲突问题可能不在梯度冲突而在蒸馏约束缺失这时应该叠加输出蒸馏。6.2 新类mAP迟迟上不去大概率是对齐强度过大模型每一步更新都被拽向旧类方向。先把lambda降到接近0确认新类能正常学习了再逐步回调。还有一种情况是参考梯度更新频率太高每隔几步就重新算一次导致模型刚往新类方向走一点就被拉回去这种情况可以减少参考梯度的更新频率甚至固定不变。6.3 梯度对齐项数值不稳定出现在训练开始阶段很常见因为初始模型对新类完全无感分类损失很小算出来的新类梯度方向噪声大余弦相似度忽高忽低。我通常会在前几个epoch跳过对齐约束等新类分类损失稳定后再加上。如果数值仍然剧烈抖动检查梯度归一化是否写对尤其要确认拼接梯度时是否把不同尺度的参数层混在了一起。6.4 分类头改了之后回归头拖后腿说明你已经把分类遗忘控制住了但回归头的旧类定位精度还在掉。这种情况很正常因为你的梯度操作对分类头生效后共享特征还是被新类数据带着变化回归头在共享特征上收到的输入分布变了定位自然受影响。可以尝试给回归损失加一个梯度裁剪或者每隔几个epoch用小批量旧类缓存数据对回归头做一次“复习”。如果缓存都找不到就靠低学习率的联合微调硬扛也能压住一部分回归遗忘。我在实际使用中发现这种从梯度方向入手的方法最大的价值在于给增量检测提供了一个非常清晰的诊断视角模型训练为什么遗忘不再是玄学而是可以量化为梯度方向的夹角问题。不必照搬论文的复杂实现哪怕只是在新类训练时多算一步新旧梯度的余弦相似度把它打到日志里观察你都能对自己模型的病根有更具体的判断。做增量检测实验尤其是连续跑很多个批次的长期任务建议每隔一段时间就检查一下旧类mAP的趋势走向不要等到全部训练完才看结果。这里面最容易忽略的其实是参考梯度的质量我踩过几次坑之后现在做实验都先固定一批质量均衡的旧类样本作为参考后续所有增量批次都用它效果稳定了再谈调优。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑