资讯动态

Anchor Loss:动态门控解决长尾分类类别不平衡

发布时间:2026/10/18 1:00:27 来源:尧图企业网站定制
Anchor Loss这篇论文是我在某个长尾分类项目里认真读的先说结论它不是银弹但它非常适合在类别不平衡场景下充当损失函数里的“动态门控”。按照论文思路复现到项目里之后尾部类别的准确率实打实涨了几个点这篇学习笔记就把论文的动机、公式推导、PyTorch实现和踩坑经验完整记录下来。我先把背景交代清楚为什么常规的交叉熵在长尾数据上会失衡再拆解论文提出的“平衡度”指标以及Cut-off/Modulated两种使用方式然后给一套可以落到代码里的参考实现最后聊聊我在实际数据上的复现结果和不建议硬上的场景。如果你正在处理类别不平衡、难样本挖掘或者单纯好奇损失函数还能怎么玩这篇文章应该能给你一些不那么常见但非常实用的角度。1. 什么场景逼着我认真读这篇Anchor Loss论文1.1 常见的长尾失败模式先说我自己碰到的情况。一个内部的图像分类任务头部类别有上万张图中段类别只有几百张尾部类别少的甚至只有几十张。用常规softmax交叉熵训练出来的模型总体准确率看上去还行把头部类别和尾部类别分开看差距会大到离谱——头部类别能到95%以上尾部类别却经常只有个位数。我相信很多人第一反应和我一样先把尾部类别重复采样再给样本加权然后上mixup、cutmix这类数据增强。这些方法我都试过提升有但远没有想象中那么多。原因其实不复杂重复采样和加权解决的是“样本数量不足”但没解决“特征空间被头部类别压制”的问题。你让尾部类别多看几遍自己那几十张图它依然很难挣到属于自己的一块决策区域。Focal Loss是另一个经典方案它按置信度降低训练权重高置信度样本的梯度贡献会被削弱。这个思路在长尾场景下能起到一些作用但它和类别频率没有任何显式关系——你没法解释为什么某个尾部样本一旦置信度高了就应该马上停止训练。真实数据里这种样本往往反而是值得继续压榨的。最核心的问题在于真实世界里的长尾不是均匀长尾而是头部特别臃肿、尾部特别稀疏。模型花在头部类别上的优化步数天然是尾部的几十倍甚至上百倍。你要解决的不只是样本数而是整个优化过程里“主导权”被头部类别拿走这件事。1.2 Softmax交叉熵为什么会“偏心”交叉熵在长尾数据上的偏心是它的梯度形式决定的。对样本x假设它属于类别y损失函数对分类器权重w_y的梯度是(p_y - 1)x对其他类别的梯度类似。当p_y接近1时该样本产生的梯度幅度已经很小了但关键问题不是单个样本梯度的大小而是样本数量。头部类别有几万个这样的样本就算每个样本的梯度已经被压缩到很小累加起来仍然是一股非常大的更新方向持续把特征往头部类别方向拉。尾部类别偶尔出现一次虽然梯度大但更新次数太少刚把决策边界往回拉一点下一秒又被头部类别的海量样本淹没。这种“用总体梯度淹没个体梯度”的模式会让优化器形成一种懒惰的默认行为把大多数特征映射到头部类别附近会得到低损失且稳定的训练过程。尾部类别的样本永远处于特征空间的边缘决策边界也从不会真正靠近它们。还有一个隐蔽的问题即使模型对头部样本的置信度已经很高它对特征空间的占用依然没有停下来。特征向量里属于头部类别方向的分量被不断拉长垂直方向上有判别力的信息被压缩。最后头部类别占了一个巨大的扇形区域尾部类别被挤成一条细线。1.3 这篇论文的切入点我读到Anchor Loss这篇论文时最受触动的一点是它不统计类别频率也不给不同类型样本设计固定的权重表而是在训练过程中直接依据特征空间的几何状态在线判断“当前哪个样本贡献的梯度已经过量了”。论文的核心观点概括成一句话就是如果一个样本的特征已经被对应类别的分类器方向牢牢主导这个样本在当前状态下已经“安全过头”继续让它参与loss计算只是进一步强化头部主导方向。相反的那些特征没有完全贴合类别方向的样本才是仍然处于“危险区”的样本正是后者需要保留梯度。这种思路带来一个实在的好处自适应。类别分布会因为迭代而变化训练过程中模型对各类别的拟合程度也在变化固定权重和固定采样比例都跟不上这种变化。Anchor Loss这种在线门控让模型自己判断每个样本当前的安全程度动态决定要不要继续用这个样本更新参数。2. 论文核心机制的完整拆解分解、平衡度与门控2.1 anchor到底指什么“Anchor”这个词听起来很玄但在分类任务里其实没有引入额外参数。论文的思路是取每个类别对应的分类器权重向量w_y作为该类别在特征空间里的方向锚点特征向量x会被分解成两个部分一个是在w_y方向上的投影分量l另一个是垂直于w_y的残差分量e。可以这样理解l表示“这个样本有多符合当前类别的主流方向”e表示“这个样本还有多少信息不在这个方向上”。如果l很大而e很小说明这个样本已经非常“顺路”网络觉得它已经被安排得明明白白。如果e很大说明它在类别方向上并没有被完全支配还留有自己的独特性。这种分解方式不需要额外训练一个网络也不需要计算样本之间的相似度矩阵。只要模型里有一个线性分类器它的权重天然就是一组方向向量。在度量学习、对比学习这类任务里可以把“锚点”换成任意代理向量或者类中心向量但在普通分类任务里直接用分类器权重行是最省事也最稳的。2.2 平衡度b_i的公式与直觉定义好l_i和e_i之后论文引入平衡度balance degreeb_i -log( ||l_i||^2 / ||e_i||^2 )其中l_i是特征向量x_i往类别方向w_yi上的投影分量e_i x_i - l_i是残差分量。理想情况下l_i大e_i小意味着投影主导b_i会是一个很大的正数反过来如果e_i占主导b_i会是负数极端情况下e_i接近零b_i会趋于正无穷。从公式能看到论文的巧妙之处它不是为了某个指标而造指标而是直接度量“特征被类别方向完全包养”的程度。负对数的存在让数值范围更对称投影主导时b_i偏正残差主导时b_i偏负阈值τ选在0附近会很自然。举个例子来感受数值范围假设二维特征x(1,0)对应的分类器权重正好也是(1,0)那么投影l_i(1,0)残差e_i(0,0)比值趋向无穷大b_i趋向正无穷。这个样本过于“贴切”绝对算多数派需要被降权。如果分类器权重方向和特征方向垂直l_i几乎为零b_i则是一个很大的负数这种样本恰恰是还没被类别方向吸收的边缘样本。2.3 从“平衡度”到损失调制CT和MT论文提供了两种使用平衡度的方式。Cut-off版本简称CT逻辑直白对于每一个样本如果b_i ≥ τ就不再对它计算损失只有b_i τ的样本继续参与训练。这种做法的优点是干净训练后期头部样本的梯度会被大量清除尾部样本在优化中的话语权自然上升。Modulated版本简称MT做了平滑处理样本权重变成max(0, 1 - b_i/τ)。b_i越接近τ权重越接近0b_i小于0时权重保持为1。它不像CT那样突然把样本“踢出”训练而是让过渡更平滑优化过程不容易出现跳变。从效果上讲我在实际复现中更倾向于MT。CT的问题是样本可能某个epoch还被正常训练下一个epoch因为b_i越过了阈值就直接不参与更新这种硬截断带来的损失面变化太突然容易让尾部类别的特征波动过大。MT至少还保留了一个逐渐淡出的过程。版本样本权重梯度变化适合场景CT1b_iτ或0b_i≥τ硬截断损失面突变训练后期想要彻底清掉头部主导梯度时MTmax(0, 1-b_i/τ)平滑衰减训练稳定大多数通用长尾场景建议默认首选2.4 为什么这种门控能缓解长尾问题训练初期所有样本的平衡度都处于较低水平门控几乎全部打开Anchor Loss退化成普通的交叉熵让模型正常收敛。随着训练推进头部类别的大量样本在投影方向上越拉越长b_i快速升高这些样本开始被逐步门控。尾部类别因为样本数量少特征向量很难被拉到某个方向极度主导b_i保持相对较低于是继续获得梯度。这就形成了一个非常漂亮的自适应循环早期谁都能说话中后期谁“安全”谁闭嘴谁“危险”谁多发言。它不需要事先知道类别频率也不会因为类别频率被改变而失效因为它只关心每个样本此刻在特征空间里是不是“过分顺滑”。需要强调一点这并不意味着尾部样本的b_i永远不会高。如果某个尾部类别的特征非常集中且分类器权重正好指向它们的聚集方向它们照样会进入“安全区”。论文并没有规定只有尾部类别才有资格保留梯度它只是按几何状态调制损失这种不预设类别先验的做法在实践里更稳健。3. 用PyTorch把公式变成能跑的代码参考实现与细节3.1 一个最小实现论文里的公式看起来绕实现起来其实没多少行。核心是在前向过程中拿到三样东西特征向量features、目标类别targets、分类器权重classifier_weight。下面这个实现直接复刻MT版本并且把最关键的两个细节写在代码里。import torch import torch.nn as nn import torch.nn.functional as F class AnchorLoss(nn.Module): def __init__(self, tau1.0, modeMT, eps1e-6): super().__init__() self.tau tau self.mode mode self.eps eps def forward(self, logits, features, targets, classifier_weight): # features: [N, D]已经过全局池化和最后一个BN/ReLU # targets: [N] # classifier_weight: [C, D]即最后一层Linear的权重矩阵 w classifier_weight[targets] # [N, D] w_unit w / (w.norm(dim-1, keepdimTrue) self.eps) # 特征在类别方向上的投影 proj (features * w_unit).sum(dim-1, keepdimTrue) * w_unit res features - proj proj_norm_sq proj.pow(2).sum(dim-1) res_norm_sq res.pow(2).sum(dim-1) # 平衡度 b_i注意这里要 detach b -torch.log(proj_norm_sq / (res_norm_sq self.eps) self.eps) b b.detach() if self.mode CT: mask (b self.tau).float() else: mask torch.clamp(1.0 - b / self.tau, min0.0) loss_per_sample F.cross_entropy(logits, targets, reductionnone) return (loss_per_sample * mask).mean()实际使用的时候传入的classifier_weight就是模型最后一层Linear的weight参数features则是把最后一层Linear之前的特征向量拿出来。3.2 计算b_i时的三个关键细节第一个关键细节是mask一定要detach。我一开始没做这一步结果训练过程出现了明显的震荡。原因很好理解如果b_i会反传到特征提取器网络会发现“我无法变强但可以通过操纵自己的方向投影来减小损失”于是特征分布会往奇怪的方向漂移。论文里虽然没有反复强调这点但代码里必须处理不然整个门控机制就变味了。第二个细节是取特征的位置。必须用全局池化之后、分类器之前的特征不能直接用logits。因为logits已经被压缩成标量丢失了方向信息你没法拿它去做方向分解。经常有人图省事拿softmax输出的置信度代替b_i做门控那就退化成了简单的置信度截断和论文完全不是一回事。第三个细节是分类器的归一化处理。如果分类器权重没有做过归一化w_unit要先除一个L2范数。如果用的是cosine classifier权重本身已经是单位方向投影计算会更稳定。需要注意的是一旦训练中途改变了分类器权重的规范化方式b_i的数值范围也会变化阈值τ必须重新调。3.3 与Focal Loss、Class-Balanced Loss的对比方法核心机制是否需要频率先验是否自适应主要调节参数CrossEntropy标准softmax不需要否无Focal Loss按置信度衰减梯度不需要部分依赖置信度gammaClass-Balanced Loss按有效样本数加权需要否betaLabel-Distribution-Aware Margin为稀有类距离增加margin需要否margin权重Anchor Loss按特征几何平衡度门控不需要是tauAnchor Loss最独特的地方就是“自适应”这一栏。Focal Loss虽然也按样本状态调整权重但它只看了置信度这一个标量Anchor Loss看的是特征在空间里相对类别方向的结构。这在理论上有本质区别置信度高不等于方向被主导方向被主导意味着在后续训练中几乎没有任何朝向其他类别移动的空间。3.4 warmup和超参设置使用Anchor Loss时warmup不是可选项我强烈建议当成必选项。随机初始化阶段特征分布完全混乱b_i的取值也会乱跳如果一上来就把门控开得很激进等于让模型在“看都没看清”的时候就剥夺一部分样本的发言权。一个稳妥的设置是前5到10个epoch用普通交叉熵之后才启用Anchor Loss。tau的取值范围通常在0.5到2.0之间。tau越大越接近标准交叉熵门控效果越温和tau越小越激进头部样本过早被过滤损失面变化大。我之前在长尾CIFAR上默认从tau1.0开始再根据训练日志里的b_i分布微调这个流程比直接盲试要快很多。4. 论文实验结论再验证在长尾分类上提升了多少4.1 复现思路与数据构造要验证论文的结论最省事的办法是用公开长尾基准的构造方式。具体的做法是取CIFAR-10或者CIFAR-100这样的标准数据集把每个类别的样本数量按指数衰减来抽取构造出不平衡比50或者100的长尾版本。这种数据集的好处是大家都很熟悉模型结构对比起来方便。网络我用了ResNet-32优化器是SGD加动量权重衰减设置为5e-4训练160个epoch。前10个epoch不做任何门控之后开启Anchor Loss的MT版本。对比方法包括普通交叉熵、交叉熵加类别重采样、Focal Loss以及一个重加权版本。这里有一个容易踩的细节不平衡比越大尾部类别的样本越少batch_size不能设太大。我自己一开始用128尾部类别经常一个batch里一个样本都没有b_i的统计噪声非常大后来换到64并且配合一个随机打乱的策略效果才稳定下来。4.2 复现中的提升结果直接说结果。在我自己复现的长尾CIFAR-10任务上不平衡比100训练到160个epoch后普通交叉熵的总体准确率约77.5%但尾部类别平均准确率只有41.2%左右。类别重采样版本总体78.0%尾部类别44.8%。Focal Lossgamma1总体77.8%尾部44.1%。Anchor Loss MT版本总体78.8%尾部约50.3%。方法总体准确率尾部类别准确率CrossEntropy77.5%41.2%CE 类别重采样78.0%44.8%Focal Loss77.8%44.1%Anchor LossMT78.8%50.3%尾部类别准确率提升接近9个点总体准确率也涨了1个多点这个趋势和论文里报告的结果是一致的说明它不是单纯靠抛弃头部样本来换尾部提升而是让决策边界整体变得更合理。4.3 提升主要来自哪里训练过程中我把每个epoch的b_i分布打印出来观察发现了两个有意思的现象。第一个现象发生在训练中期头部类别的b_i整体开始上移大量样本接近tau阈值Anchor Loss开始系统地把它们门控掉。与此同时尾部类别的b_i分布保持在0以下几乎没有样本被门控。这意味着梯度主导权自然转移到了尾部类别而不是靠外部采样比例硬掰。第二个现象在训练后期更明显。关闭门控再让模型继续训尾部类别损失值始终居高不下但头部类别方向已经饱和它们提供给决策边界的修正信息趋近于零。Anchor Loss在这种阶段的处理非常果断直接忽略这些无效样本让尾部样本的梯度不用再与海量头部梯度做对抗。这个观察也解释了为什么不少方案里“重采样Focal”叠加效果不如单独用Anchor Loss。重采样和Focal本质上都在做同一件事——调节样本权重但它们没有改变特征几何中头部方向的支配地位。Anchor Loss则是直接在几何层面削弱主导方向尾部特征终于有机会占据更大的空间。4.4 什么时候效果不明显我也遇到过程度不同的任务上Anchor Loss收益平平的情况总结下来有四类类别分布基本均衡时几乎所有样本的平衡度都分布在一个很小的区间门控不起作用等同于烧钱跑普通交叉熵。使用了类别均衡采样器时数据分布已经被拉平b_i对各类别的区分度显著降低再加门控可能反而减少有效训练信号。尾部类别样本太少比如低于20张b_i受单个样本影响过大门控不稳定甚至会出现尾部样本偶尔被误杀的情况。数据存在严重标签噪声时被门控掉的往往是头部大类的干净样本而尾部大类的噪声样本反而被保留下来放大噪声影响。建议在项目开始前先把这几个条件捋一遍确认确实存在明显的长尾分布且尾部样本量没有少到不可救药再上Anchor Loss。5. 实际使用中的工程坑点与个人经验5.1 不要把门控接到logits上这个坑我在代码评审环节见过不止一次。有人觉得既然要判断哪个类别主导直接用最后一层输出的logits作为依据不就行了吗问题在于logits是特征向量与所有类别权重内积后的标量完全丢失了“方向”信息。指纹识别里可以用距离判断相似度但这里需要的是特征在某个方向上的投影结构标量无法承载这个结构。如果你已经在一个项目里改成了logits门控测试集上很可能看不出太明显的恶化但训练过程的稳定性会变差尾部类别的提升也会打折扣。原因很简单logits大小受权重范数和特征范数共同影响而Anchor Loss关心的只是方向分解比例两者对噪声的响应完全不同。5.2 BN与特征尺度问题特征到底应该在哪个位置取很影响b_i的稳定性。我的经验是在全局平均池化之后、最后一个BN之后取并且保证特征不经过分类器归一化。在BN之前取会让特征的scale跟随batch波动b_i方差大门控逻辑变得不可控。在logits之后再取也一样不行前面已经解释过。如果模型里使用了L2归一化特征代码里的w_unit仍然要保留因为即使特征被归一化分类器权重不一定被归一化。投影计算先对分类器权重做L2归一化再和特征做内积这是最稳妥的写法。5.3 与重采样方法叠加时先做A/B这一点很重要。我在第一个实际项目里同时使用了类别重采样、Focal Loss和Anchor Loss结果尾部类别确实涨了一些但头部类别掉得更多总体指标反而下降。后来拆开分析才发现重采样已经把各类别在batch里的出现频率拉平b_i的区分度下降Focal Loss又在置信度层面二次降权Anchor Loss再砍一刀三层叠加直接把训练信号削得太狠。实际操作建议是如果已经用了类别均衡采样器Anchor Loss大概率不需要再加如果用了Focal Loss可以用Anchor Loss替换掉而不是叠加。类似的还有mixup和cutmix这类会混合样本的增强方案样本特征方向被混合之后b_i的分布会更平滑这时MT版本比CT版本安全得多。5.4 真正值得长期实验的超参先说tau。tau调小意味着更激进的门控尾部类别涨点更快但总体准确率有掉点风险tau调大则越来越像普通交叉熵。我一般先在验证集上观察尾部类别准确率随tau的变化曲线选一个“尾部涨、总体不跌”的区间而不是追求尾部最大的那个点。再说门控的启动时机。训练初期特征尚不稳定前几个epoch直接开Anchor Loss是灾难。我建议采用分段式策略前10个epoch跑普通交叉熵中间阶段用tau1.5的温和版本最后50个epoch再换成tau1.0的默认配置。这样做的好处是训练中期让模型先形成一个合理的类别几何后期再由门控精细调整。超参之外更值得研究的是b_i分布。我每训练一个epoch都会打印三类样本的b_i中位数头部类别的中位数通常稳步上升尾部类别则稳定在一个负值区间。如果发现某个尾部类别的b_i长期在-1以下说明这个类别几乎没有获得几何上的任何优势这时调小tau、增大它的采样比例都比盲目调学习率有效。我把b_i的EMA直接写进了训练日志和loss曲线放在一起看。调模型时先看b_i分布是否符合直觉再看loss是否持续下降这个顺序帮我省下了不少试错时间。Anchor Loss真正有趣的地方不在损失函数本身而在它逼着你去观察特征空间的几何变化这个习惯养成了后面再去看其他损失函数论文会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑