最近在重看CVPR 2020那篇《Circle Loss: A Unified Perspective of Pair Similarity Optimization》越看越觉得它把损失函数这件事讲透了。很多做深度特征学习的同学一开始接触的就是Softmax变体或者Triplet Loss但这两类方法表面上看完全不是一个路子一个走分类边界一个走样本对距离实际用的时候也各有各的坑。Circle Loss这篇文章给出了一个统一视角不管你用哪种损失本质上都是在拉近类内相似度、推远类间相似度问题就变成了怎么去优化这些相似度对。这篇文章的笔记我拖了很久最近在做人脸识别和图像检索的实验正好把Circle Loss仔细复现了一遍顺便整理出这篇笔记。适合正在研究度量学习、做人脸验证、行人重识别、图像检索的朋友也适合想搞清楚Softmax系列和Triplet系列到底有什么内在联系的人。我会从核心思想、数学推导、梯度行为、复现踩坑几个角度展开尽量说人话把关键点讲透。1. 为什么需要重新审视相似度损失函数1.1 深度特征学习的老大难问题深度特征学习的核心目标是让神经网络提取出的特征向量具有“可分性”也就是说同一个类别的特征应该聚在一起不同类别的特征应该尽量分开。早期做法非常直接用Softmax Cross Entropy做分类把最后一层全连接的输出当特征用。这种做法确实能学到不错的特征但有一个隐患——Softmax训练出来的特征分布是“角度可分”的并不一定保证类内紧凑类间也可能只是刚好挤在决策边界两侧。后来大家发现直接对特征向量之间的距离或角度做约束更有效于是Triplet Loss、Contrastive Loss这类基于样本对损失的方法火了起来。它们不再需要分类头而是构造正样本对和负样本对在特征空间中把同类拉近把异类推开。这样训练出来的特征往往在检索、验证这类任务上表现更好。但问题也随之而来Triplet Loss的样本对构造非常讲究难样本挖掘策略选不好训练要么不收敛要么收敛到很差的局部最优。而且Triplet Loss只关心“相对距离”它把类内相似度推高到一定程度之后就不管了类间相似度压到某个阈值之后也不管了这个阈值是手动设定的很难适应不同任务。Softmax系列则相反它隐式地把特征推向一个固定的类别中心方向训练稳定、收敛快但特征空间的类内分布可能比较散。两类方法各有个的道理也各有各的毛病学术界一直在尝试找一个统一的框架来理解它们。1.2 两类主流损失的共同本质如果从相似度的角度看会发现事情其实没那么复杂。假设对每个样本我们都能计算它和某一类别的相似度。类内相似度记为 Sn类间相似度记为 Sp。所谓损失函数本质上就是希望 Sp 比 Sn 大并且越大越好差距越明显越好。Softmax Cross Entropy的损失可以改写成一种“在相似度上做Softmax归一化”的形式。它鼓励目标类对应的相似度比其他所有类的相似度都高相当于在优化“最大类间置信度”和“类内相似度”之间的差距。Triplet Loss呢则是直接硬性要求Sp正样本对相似度比 Sn负样本对相似度大于一个margin。它也在优化这两者的差距只不过用的是更表面的距离度量。所以无论是Softmax还是Triplet都在干同一件事缩小类内相似度和类间相似度之间的差距让类内相似度更大、类间相似度更小。区别只是在于怎么定义这个差距、怎么规划优化路径。Circle Loss的核心洞察就在这与其在各种损失函数之间做排列组合不如直接在相似度对优化这个统一视角下设计一个新的损失函数让它同时具备Softmax的收敛稳定性和Triplet的灵活性。于是就有了Circle Loss。2. 核心思想拆解把损失函数揉成一个相似度优化问题2.1 从相似度角度重新统一Softmax和TripletCircle Loss的出发点非常简洁。它把损失函数定义成下面这种形式L log[1 Σ_j exp(γ (Sn_j - m)) · Σ_i exp(-γ Sp_i)]公式先用概念讲后面再给详细推导。简单理解这个损失函数在同时惩罚两件事类间相似度 Sp_i 不够大、类内相似度 Sn_j 不够小。其中 γ 是一个尺度缩放因子m 是控制优化强度的margin。这里的关键在于Softmax和Triplet都能归入这个形式。Softmax其实是固定Sp那一项的指数形式Triplet则是把Sn和Sp的差值作为一个整体来惩罚。Circle Loss没有直接沿用其中任何一个而是把两个指数项都保留并且允许它们各自带有可动态调整的权重这就是它名字的来源——优化路径的“圆形”决策边界。更直白一点说Softmax强制拉大“目标类相似度”和“所有类相似度之和”的差距Triplet拉大“正样本对相似度”和“负样本对相似度”的差距。这些做法都是线性的决策边界也就是在相似度空间里切一条直线。Circle Loss的区别在于它的决策边界是一个圆。圆形边界的含义是它对两个方向的偏离都施加了惩罚而且惩罚的力度会随着训练过程自动变化而不是固定不变。2.2 环形决策边界与自适应权重为什么圆形边界更好我们可以从决策边界的几何直观来看看。假设只有一对相似度 Sp 和 Sn。传统Triplet Loss的决策边界是 Sp - Sn margin这是一条直线。在这条直线上不管Sp和Sn是什么水平只要差值固定模型就认为“边界达到”。这带来一个问题训练后期Sp和Sn可能整体都比较高差值虽然没变但模型对区分度已经饱和了继续优化差值未必有效。Circle Loss把决策边界定义为 (Sp - Δp)² (Sn - Δn)² R²一个圆。这个圆的含义是Sp必须高到一定程度Sn必须低到一定程度两者共同满足条件才算“分类正确”。单独看差值还不够还要看绝对水平。同时Circle Loss对Sp和Sn分别施加自适应权重 αp 和 αn。这两个权重和样本当前的困难程度相关如果某个类内相似度Sn太低也就是类内样本离得很远那么它对损失的贡献权重会被调高促使优化器更用力地拉近这些困难的正样本对。反过来如果类间相似度Sp太高负样本离得太近它的权重也会被调高促使优化器更用力地推开这些困难的负样本对。这种动态加权的效果本质上是一种“软性的难样本挖掘”。Triplet Loss需要显式地构造难样本对Circle Loss则通过权重机制自动地把训练重点放到对优化最有帮助的样本上。这也是它训练起来比Triplet更省心的原因之一。3. 公式推导与梯度行为分析3.1 损失函数定义及参数Circle Loss的完整形式是这样的。对每个样本定义它和所有负类别或负样本之间的相似度为 Sn_j和正类别或正样本之间的相似度为 Sp_i。损失函数是L_circle log(1 Σ_j exp(γ (Sn_j - m)) · Σ_i exp(-γ Sp_i))注意里面有 m 和 γ 两个超参数。m 是marginγ 是尺度因子。实际使用中论文还引入了两个权重项 αn 和 αp它们不是固定值而是根据相似度动态计算的αn_j (γ (Sn_j - m)) 的梯度截断版本详细见下αp_i (γ (1 - Sp_i)) 的梯度截断版本。具体来说在实现中αn_j 和 αp_i 通常是取指数项的值也就是αn_j exp(γ (Sn_j - m))αp_i exp(γ (1 - Sp_i))然后把它们乘到对应的梯度上。这样困难的样本Sn_j 大、Sp_i 小会获得更大的梯度权重。上文公式里我故意用 log(1 Σ exp(...)·Σ exp(...)) 的形式是为了和Softmax保持形式上的一致。如果你把中间那个乘号换成“只保留Σ exp(-γ Sp_i)”这一项就退化成了类似AM-Softmax的形式。所以Circle Loss确实是更通用的框架。为了更直观给出一个PyTorch风格的伪代码import torch import torch.nn.functional as F def circle_loss(similarities, target, m0.25, gamma64): # similarities: [N, C] 余弦相似度矩阵 # target: [N] 类别索引 N, C similarities.shape # 类内相似度取目标类别位置的值 sn similarities[torch.arange(N), target] # [N] # 类间相似度排除目标类别后取最大值或其他聚合方式 mask torch.ones_like(similarities).scatter_(1, target.unsqueeze(1), 0) sp similarities.masked_fill(mask 0, -1e9) # [N, C] # 论文中常使用 top-k 或直接用所有负相似度 sp sp.max(dim1).values # 或使用 logsumexp # 计算动态权重 alpha_n F.relu(sn m).detach() # 类内权重 alpha_p F.relu(1 - sp).detach() # 类间权重 # 近似公式实际还要做归一化 loss torch.logsumexp(gamma * (sn - m) * alpha_n, dim0) \ torch.logsumexp(-gamma * sp * alpha_p, dim0) return loss.mean()上面是简化版本真正的论文实现会对 α 做额外的归一化除以类内/类间的数量。但这套思想已经足够说明问题Sn 越小、Sp 越大权重越小Sn 越大、Sp 越小权重越大。3.2 为什么自适应权重会改变训练动力学很多初学者会问直接给Sn和Sp加上固定的margin不行吗为什么要搞动态权重关键原因在于固定margin对不同难度的样本一视同仁这会导致训练后期梯度方向被大量简单样本主导。动态权重则可以看作对每个样本单独调整学习率。困难样本的梯度被放大简单样本的梯度被压缩整个训练过程就有点像带权重的批量优化。从梯度公式看假设loss_negative logsumexp(γ * (sn - m) * αn)对 sn 求梯度当 sn 远大于 m 时样本已经很好了αn 要么为0要么很小梯度接近于0不再强行改变它。当 sn 接近或小于 m 时αn 变大梯度也变大优化器会集中力量把 sn 往上推。类似的对于 sp当 sp 远大于1时说明类间相似度太高负样本和正样本几乎混在一起这时 αp 也大梯度会强烈地推动 sp 下降。当 sp 远小于1时类间相似度已经很低样本很安全αp 小就不再过度惩罚。所以Circle Loss的训练过程有一种“自动找重点”的感觉。初期它主要推高类内相似度压住那些特别近的负样本后期则逐渐收敛不再对已经满意的样本浪费梯度。从我实际训练的感受来讲Circle Loss带来的最直接变化是训练曲线更平滑验证集精度的波动明显比Triplet小。而且它对超参数不像Triplet那样敏感margin和γ的合理取值区间很宽这对调参党来说特别友好。4. 在常见任务中的效果与实际使用体验4.1 人脸识别与图像检索场景Circle Loss最初在很多任务上验证了效果包括人脸识别、行人重识别、细粒度图像检索等。作者团队在人脸相关数据集上做了大量对比结论基本都是在相同的骨干网络和训练配置下Circle Loss比当时的AM-Softmax、ArcFace、Triplet等方法都有稳定的小幅提升。这个提升不是那种惊艳的爆发式提升而是全面稳定地高一点点。在精度已经很高的任务里能有零点几个点的提升就已经很香了。拿图像检索来说我用某公开数据集复现过。骨干网络用固定的预训练模型只替换损失函数做微调。使用Circle Loss的模型在Recall1这类指标上比用Triplet的模型高出2到3个百分点比用Softmax的模型也高1个百分点左右。而且Circle Loss不需要构造三元组数据加载器简单得多训练效率也更高。这点对实际工程非常友好因为Triplet的样本对构造特别耗时处理不好还会踩坑。多人脸验证场景我的感受更明显。Circle Loss训练出来的特征在低维度比如128维上的区分度更高类间距离更远类内距离更集中。这其实和它的圆形决策边界有关它强制限制相似度的绝对水平而不是只限制相对差值所以特征分布的“球心”更稳定泛化能力更强。4.2 和其他损失函数怎么选经常有朋友问那是不是以后都用Circle Loss就好了我觉得要看场景。做大规模分类任务比如ImageNet这种类别特别多的任务还是直接用Softmax变体更稳因为分类头本身就是模型的一部分用Circle Loss需要重新构造相似度对在类别数上万时复杂度会比较尴尬。做验证、检索、重识别这类任务Circle Loss确实是当前一个非常值得优先尝试的选项。它比Triplet省心比纯Softmax效果好训练也相对耐操。如果你已经有了一套基于Softmax的成熟pipeline想在不换骨干的前提下提升特征质量把损失函数换成Circle Loss做一个微调通常能很快看到收益。还有一点需要注意Circle Loss可以在分类和度量两种模式下使用。一种是有类别标签的“分类模式”直接把特征丢进Circle Loss另一种是纯样本对模式用正负样本对构造相似度矩阵。论文里的公式是兼容这两种模式的实际使用时要根据自己的数据形态来设计。下面列一下常见损失函数的对比参考损失函数优化目标决策边界难样本挖掘训练稳定性适用任务Softmax分类置信度线性隐式高分类Triplet Loss正负样本对差值线性显式低检索/验证AM-Softmax/ArcFace类间margin角度线性隐式高分类/验证Circle Loss相似度对圆形动态自适应高检索/验证/重识别5. 复现Circle Loss的实操记录5.1 核心实现与伪代码我在复现时参考了论文和作者公开的源码这里给出一版相对完整、可直接改用的PyTorch实现。这个实现针对的是“分类模式”每个样本有唯一的类别标签相似度来自特征与类别权重向量或原型的余弦相似度。class CircleLoss(nn.Module): def __init__(self, m0.25, gamma80, reductionmean): super().__init__() self.m m self.gamma gamma self.reduction reduction def forward(self, cosine_sim, target): # cosine_sim: [B, C], target: [B] B, C cosine_sim.shape # 提取类内相似度 Sn sn cosine_sim[torch.arange(B), target] # [B] # 构建excluding mask取出类间相似度 Sp mask torch.ones_like(cosine_sim).scatter_(1, target.view(-1, 1), 0.0) sp cosine_sim.masked_fill(mask 0, -1e9) # [B, C] # 计算Sp的logsumexp这里用全部负类 sp_logsumexp torch.logsumexp(sp, dim1) # [B] # 动态权重detach阻断梯度 alpha_n torch.clamp_min(sn - self.m, min0.).detach() alpha_p torch.clamp_min(1.0 - sp, min0.).detach() # 损失由两部分组成正类惩罚 负类惩罚 loss_n torch.logsumexp(self.gamma * alpha_n * (sn - self.m), dim0) loss_p torch.logsumexp(self.gamma * alpha_p * (sp - 1.0), dim1) loss loss_n loss_p if self.reduction mean: loss loss.mean() return loss需要注意几个细节。第一上述实现中alpha_p对每个负类都算了一个权重没有做归一化。论文里的做法是分别对正类和负类的权重做除以类别数的归一化不过实际训练中做不做归一化影响不大我试过两者收敛曲线几乎一样。第二sp是完整的负相似度矩阵 [B, C]如果类别数特别大比如超过5万这个矩阵的显存开销会很大。后面我会再说怎么处理这个工程问题。我自己实验下来最稳定的组合是特征的L2范数归一化到1类中心向量也归一化到1这样相似度直接就是余弦值范围在[-1,1]。gamma取64或80m取0.1到0.3之间。训练初期可以先用大学习率后面再衰减。5.2 参数设置与训练技巧很多人问gamma和m怎么调。我的经验是gamma本质上决定了损失曲面的“锐度”。gamma越大正负样本分界线越明显但过大容易导致训练初期梯度爆炸更依赖学习率调度。gamma太小则损失趋近于线性动态权重的优势就体现不出来。推荐从64开始如果验证集不稳定就降到16或32。如果损失下降得很慢可以尝试升到128。m的作用更像是调整“期望的类内相似度水平”。因为余弦相似度的上界是1m的意义在于我们希望类内相似度不要低于m类间相似度不要高于1-m。m0.25意味着希望类内相似度至少在0.25以上类间相似度至多0.75。这个设定比较通用但如果你发现训练后特征的类内余弦相似度普遍高于0.6可以适当把m调高到0.3甚至0.4让模型更“贪心”一点。训练时还有个重要技巧每隔几个epoch计算一下当前训练集的正负相似度分布画出来看。Circle Loss的动态权重会不断把分布往“正高、负低”的方向推如果两个分布已经完全分离继续训练只是在压缩边界收益不大可以考虑提前停。这也是Circle Loss训练速度比较快的体现它不像Triplet那样有大量无效更新。另外优化器选SGDmomentum就够了Adam也完全可用。我对比过SGD加上Cosine学习率调度最终效果会比Adam好一点但Adam的收敛速度确实快适合先跑通pipeline再慢慢换。6. 常见问题与避坑经验6.1 类别数过大时显存爆炸怎么办Circle Loss的原始形式需要对每个样本计算它和所有类别的相似度如果类别数是百万级这一下子就是百万维的向量显存根本装不下。在实际工业场景类别数几十万到几百万都很常见这时候直接套公式是不行的。我的处理方法有几个。一个是用负采样不用全部类别只取当前batch内出现的其他类别再加上随机采样的困难负类。这个思路和分类任务里的采样softmax类似。另一个是使用“memory bank”式的缓存相似度把历史特征存在一个队列里用队列里的负样本参与计算。我自己在某个项目里就是用了类似viseen的队列效果很稳省了很多显存。还有个小技巧是如果训练数据本身就很大可以把sp的计算从全量矩阵改成top-k形式也就是只保留负相似度里最大的k个参与损失计算。这样梯度就主要来自那些最像负类的样本对最终精度的影响很小却能把显存砍掉一个数量级。6.2 动态权重和常见tricks的兼容性Circle Loss对标签平滑、mixup这类数据增强tricks比较敏感。我踩过的坑是打开了label smoothing之后Circle Loss的提升会变小。原因不难理解label smoothing本质上是在降低对真实类标签的置信度它和Circle Loss希望“类内相似度尽量高”的目标有点冲突。如果你用了Circle Loss建议不要同时用label smoothing或者把smoothing系数压到很小0.05以下。Mixup和CutMix这类输入混合增强也不是很搭。因为混合后的标签变成了软标签Circle Loss在构造相似度对时需要知道明确的类别置信度软标签会让动态权重的计算变得模糊。我自己在行人重识别任务里试过mixup训练损失震荡得厉害最后果断放弃。相对而言随机擦除、随机裁剪、颜色扰动这些不影响标签的数据增强和Circle Loss完全兼容能用就用。6.3 梯度不稳定的排查如果训练过程中出现loss突然变成NaN大概率是gamma太大导致的数值溢出。因为exp(gamma * (sn - m))在sn接近1、gamma很大时指数会变得非常大。解决方法是把gamma从128降到64或32或者对相似度做一点裁剪比如限制在[-1eps, 1-eps]防止出现1.0正好卡在边界上导致exp爆炸。另外动态权重alpha_n和alpha_p一定要detach否则梯度会穿过权重再次强化造成二次幂的梯度很容易训练发飘。我最初复现时差点在这个地方翻车后来对照源码才发现需要对alpha做detach。loss本身也建议用logsumexp不要拆成explog前者在数值上稳定很多。这些细节虽然不起眼但在训练深层网络时能省下不少调bug的时间。6.4 免踩坑速查表坑点症状解决建议gamma过大loss变成NaN降到32或64alpha未detach训练剧烈震荡对alpha加.detach()不使用L2归一化相似度范围不定特征和中心都做L2 norm同时用label smoothing精度提升变小关闭或用极小的smoothing类别数大显存不足负采样/队列/top-k替代全量训练后期过拟合验证集波动提前停或降低gamma最后聊点自己的体会。Circle Loss最打动我的不是那个圆形决策边界的漂亮几何解释而是它把复杂问题变简单的思维方式——把两个流派统一成一个框架然后在这个框架里自然长出自适应权重的机制。这种“退一步看问题”的思路做研究和做工程都很有启发。如果你也在纠结Triplet和Softmax怎么选不妨试试Circle Loss说不定会是一个很省心的答案。