ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Focal Loss与Circle Loss:交叉熵、样本不平衡与度量学习选型

Focal Loss与Circle Loss:交叉熵、样本不平衡与度量学习选型 昨天调一个密集小目标的检测任务训练日志漂亮得不像话分类损失从 0.92 一路掉到 0.03可验证集上的 mAP 卡在 0.41 一动不动。把梯度打出来才看明白——绝大部分梯度来自几万个负样本里那些模型早就判对的背景真正需要它学的难样本贡献不到百分之五。这就是我在 Day 39 把 Focal Loss 和 Circle Loss 这两个损失函数翻出来重新啃一遍的直接原因。它们解决的问题其实不是同一个Focal Loss 管的是样本之间严重不平衡时谁该被重点照顾Circle Loss 管的是度量空间里一对相似度该怎么被推开或拉近。前者来自检测后者来自人脸和检索但两者的底层思路——用连续可导的权重替换硬性的样本筛选——是同一件事。这篇记录我打算按我自己的理解顺序写先讲清楚交叉熵到底漏了什么再把 Focal Loss 拆到梯度层面手算一遍然后是工程落地时踩过的坑Circle Loss 部分会把那个看起来很唬人的统一公式还原成一句人话再给能跑的代码和调参手感。最后聊选型、损失曲线的读法以及一份我自用的自查清单。无论你是刚学完交叉熵、准备往检测或度量学习走的新手还是已经在改 YOLO 损失函数的老手应该都能捞到点东西。1. 交叉熵在长尾和度量学习里到底漏了什么1.1 损失函数是目标的代理不是目标本身很多人学损失函数时的第一印象是损失越小模型越好这个印象在教科书数据上是成立的在真实项目里经常失效。原因在于损失函数从来都不是我们真正想要的东西它只是一个可导、好算、数值稳定的代理。分类任务真正的目标是准确率或者 F1但这两个指标对参数不可导没法直接做梯度下降所以我们退而求其次优化交叉熵这个代理。代理和目标之间一定存在缝隙差别只在于缝隙有多大。在类别均衡的数据上这个缝隙很小优化交叉熵基本等价于优化准确率所以大家感觉不出来。但在长尾数据上缝隙会被放大到离谱的程度。假设一个检测任务里正样本占 0.1%负样本占 99.9%交叉熵对每个样本都是等权求平均。模型只需要把所有位置都判成负样本就能让损失降到 0.001 量级对应的准确率是 99.9%可召回率是零。这不是模型偷懒这是交叉熵在忠实地完成你交给它的任务——它优化的是全体样本上的平均对数似然而全体样本里绝大多数是负样本。头歌那类平台上让你手写cross_entropy的练习题核心是让你搞明白 softmax、log、取负号这三步的数值含义这是必须过的一关。但练习题不会告诉你的是同一个交叉熵公式搬到 1:1000 的正负比上就会失效。所以我在学损失函数时给自己定了一条规矩看到任何一个损失函数先问它的隐含假设是什么。交叉熵的隐含假设是每个样本同等重要而这个假设在长尾场景下大概率不成立。1.2 两类典型失效检测里的背景淹没检索里的边界粗糙先说检测。密集检测的候选框动辄几万个其中真的目标可能只有十几个。这些框按模型当前的判断可以分成四类容易的正样本已经判对且置信度很高、难的正样本、容易的负样本、难的负样本。实际训练中数量上占绝对优势的是容易的负样本它们在总损失里占了大头但它们对模型的提升几乎为零——模型已经把它们学明白了再给梯度就是浪费算力甚至带偏方向。难的正样本数量少得可怜它们才是决定召回率的关键可它们的梯度被淹没在背景的汪洋里。这就是常说的难例挖掘问题。再说检索和人脸。Softmax 的目标是让每个样本在超球面上落到正确的方向它只要求分类正确不要求类内紧致。结果就是同一类的人脸特征在球面上摊成一大片扇形类与类之间只隔了一条很窄的缝。而度量学习真正想要的是类内抱团、类间分离这是一个几何目标Softmax 并不直接优化它。传统做法是用 Triplet Loss显式地拿一个正对和一个负对来构造损失。但 Triplet 有一个很硬的问题margin 是固定的它对已经分得很开的对和差一点就要分不开的对施加同样的推力导致梯度大量消耗在已经满足约束的对上。三元组的数量是 O(N³) 级别靠随机采样又容易采到无信息的简单三元组采样策略的调参成本有时候比模型本身还高。这两个问题看起来八竿子打不着但它们的解法思路是一模一样的不要用固定的、统一的权重去处理所有样本或所有对而是根据模型当前的判断状态动态地决定谁值得更多的梯度。Focal Loss 用概率做这个判断Circle Loss 用相似度做这个判断。2. Focal Loss 的公式拆到梯度层面2.1 γ 到底压的是什么一次手算Focal Loss 的公式只有一行FL(p_t) -α_t * (1 - p_t)^γ * log(p_t)其中p_t是模型对真实类别的预测概率α_t是类别权重γ是聚焦参数。当γ 0时它退化成带类别权重的交叉熵。真正起作用的是(1 - p_t)^γ这个调制因子——p_t越接近 1也就是模型越有把握(1 - p_t)越接近 0整个调制因子就被压得越小。光看公式没感觉手算一遍就清楚了。取γ 2、α 1对二分类的正样本算损失和梯度p_t交叉熵损失Focal 损失交叉熵梯度模长Focal 梯度模长梯度比0.90.1050.001050.1000.00290.0290.60.5110.08180.4000.1620.4050.31.2040.5900.7000.6970.9960.12.3031.8650.9001.1021.224这张表里最反直觉的是最后一行对于 p_t 0.1 的难样本Focal Loss 的损失值比交叉熵小但梯度反而比交叉熵大了 22%。很多人以为 Focal Loss 就是把难样本的梯度放大、容易样本的梯度压小前半句对但不是靠概率曲线实现的而是靠调制因子的导数。我把梯度完整求一遍设正样本损失为L -(1-p)^γ log p其中p σ(z)则dL/dp γ(1-p)^(γ-1) log p - (1-p)^γ / p dL/dz γ * p * (1-p)^γ * log p - (1-p)^(γ1)对比γ 0时交叉熵的梯度dL/dz p - 1可以看出 Focal 的梯度多了一项γ * p * (1-p)^γ * log p。因为log p是负数这一项是负的它随着p变小而绝对值变大。所以当p很小的时候这一项会把梯度拉得比交叉熵更大当p接近 1 的时候(1-p)^(γ1)这一项把梯度迅速压到 0。交叉点大概在p ≈ 0.3附近比我预想的低不少。2.2 α 和 γ 为什么不能各自独立地调大α的作用是平衡正负样本γ的作用是聚焦难例这两个参数是耦合的不能分开看。论文原文里推荐的组合是α 0.25、γ 2很多人第一次看到α 0.25会懵正样本不该权重更高吗怎么反而给了 0.25原因就是γ已经在压负样本了——负样本数量巨大但每一个的(1-p_t)^γ很小总体贡献被压下来了。所以需要用α把正负样本的相对权重往回调一点最终在总损失里正负样本的占比才是合理的。这两个参数是在互相补偿。如果同时把α往上推、γ也往上推会发生什么正样本权重变大、负样本的梯度被压得更狠模型会变得只关心正样本在早期就出现大量误检——因为你把负样本的约束几乎取消了。反过来α 0.75配γ 2在没有预训练权重的情况下前几个 epoch 经常直接不收敛。我的经验是先把γ定在 1.5 到 2.5 之间再动α如果正样本比例低到千分之一以下α可以放宽到 0.5 甚至 0.75但这时候γ要降到 1 附近否则训练会非常不稳。2.3 和 OHEM、GHM 摆在一起看同样是解决难例问题还有两种经典思路放在一起对比就明白 Focal 的取舍在哪里。方法加权方式是否需要额外前向对离群点的鲁棒性实现复杂度OHEM硬截断只取 loss 最大的前 k 个需要要算完所有样本再筛差容易被脏标和离群点主导中显存开销大GHM按梯度模长的分布做密度加权不需要额外前向好梯度极大的样本权重被下调高需要统计梯度分布Focal按预测概率做连续软加权不需要中对极端离群点没有特殊处理低改一个公式OHEM 的问题在于硬被选中的样本和没被选中的样本之间有一个断层训练后期容易只在少数几个样本上来回震荡。GHM 的想法更细它认为梯度模长本身服从某种分布模长特别大的那一小撮大概率是标注噪声或者离群点应该降权而不是重点学这个思路在处理脏数据时确实更稳。Focal Loss 的优势是简单——不需要额外的前向传播、不需要维护梯度直方图、不增加显存一个公式改两行代码就能上。所以它在工程上活得最好。3. Focal Loss 的工程落地与六个坑3.1 一份数值稳定的 PyTorch 实现实现上最关键的一点是不要在概率空间里算log。如果你先sigmoid再log当预测概率接近 0 时会直接得到-inf反向传播就废了。正确做法是用binary_cross_entropy_with_logits它内部用 log-sum-exp 技巧把log(σ(z))算稳了。import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0, reductionmean): super().__init__() self.alpha alpha self.gamma gamma self.reduction reduction def forward(self, logits, targets): # logits: (N, C) 未经 sigmoid 的原始输出 # targets: (N, C) one-vs-all 的 0/1 标签 p torch.sigmoid(logits) ce F.binary_cross_entropy_with_logits(logits, targets, reductionnone) p_t p * targets (1.0 - p) * (1.0 - targets) alpha_t self.alpha * targets (1.0 - self.alpha) * (1.0 - targets) loss alpha_t * (1.0 - p_t).pow(self.gamma) * ce if self.reduction mean: return loss.mean() if self.reduction sum: return loss.sum() return loss注意这个写法针对的是one-vs-all 的多标签设定也就是检测里分类分支的标准形式每个类别独立做一次 sigmoid独立算二分类损失。如果你做的是单标签多分类比如 ImageNet 那种p_t要取的是 softmax 之后目标类的那一维写法不一样不能直接套上面的代码。3.2 我在项目里踩过的六个坑坑一和 label smoothing 叠加使用。标签平滑会把目标从 1 变成 0.9 之类的软标签这时候p_t的含义就变了——模型输出 0.9 时p_t只有 0.9(1-p_t)^γ永远压不到 0Focal 的容易样本降权机制就部分失效了。这两个技巧我一般二选一硬要叠加的话γ要调小。坑二γ太大且没有 warmup。训练最开始模型输出接近 0.5正样本的梯度本来就小如果γ 3再加上较大的初始学习率模型前几个 epoch 几乎学不动表现为分类损失长时间卡在一个值上不动。我的处理是前 3 到 5 个 epoch 用γ 0等价于普通 BCE等模型有了基本的判别能力再切到γ 2。坑三推理阈值忘了重扫。这个最容易忽略。Focal Loss 改变了损失的尺度模型输出的置信度分布也跟着变了。我见过不止一次训练完直接拿默认的 0.25 置信度阈值去推理结果召回掉了一大截。正确做法是训练完在验证集上重新扫一遍置信度阈值和 NMS 的 IoU 阈值画一条 P-R 曲线挑最优点。坑四分布式训练时的 reduction 偏差。如果用mean做 reduction每张卡上先求均值再 all-reduce当各卡的正样本数量差异很大时会引入偏差。稳妥的做法是先用sum求出本卡的总损失和总样本数all-reduce 之后再相除。坑五损失量级变了早停和权重系数没跟着改。Focal 的损失绝对值比交叉熵小一到两个数量级如果多任务里分类损失和框回归损失是加权求和的原来的权重比例就失衡了。我在 YOLO 里改过一次分类分支从 BCE 换成 Focal 之后没调权重结果框回归被压得几乎不收敛。坑六类别数很多的单标签场景反而更差。分类数上千、每个类都只有几十张图的时候Focal 容易欠拟合。因为每个类本身样本就稀再降权就更学不到了。这种场景更适合用 logit adjustment 或者重采样。3.3 挂到 YOLO 的训练流程里YOLOv5 的损失实现里留了一个fl_gamma参数把它从 0 改成 2 就相当于在分类和置信度分支上启用了 Focal Loss改动成本极低。YOLOv8 的默认分类分支用的是BCEWithLogitsLoss想做 Focal 得自己改损失计算的部分。这里要特别提醒分类分支的损失和回归分支的损失是两回事。回归分支用的是 IoU 系列CIoU、DIoU 等它处理的是连续坐标没有概率这个概念把 Focal 硬套上去是没有理论依据的。有人问 Inner-IoU 能不能和 Focal 一起用——可以它们作用在不同的分支上一个是回归损失里的 IoU 计算方式一个是分类损失的加权方式互不冲突。4. Circle Loss 的圆具体圆在哪里4.1 那个长公式其实只说了一件事Circle Loss 的原始形式看着吓人L log[1 Σ_j exp(γ * α_n^j * (s_n^j - Δ_n)) * Σ_i exp(-γ * α_p^i * (s_p^i - Δ_p))]其中s_p是正样本对的相似度s_n是负样本对的相似度α_p [O_p - s_p]α_n [s_n - O_n]O_p 1 mO_n -mΔ_p 1 - mΔ_n m。[x]表示取max(x, 0)。把它翻译成人话对每一个负样本对按它的糟糕程度加权后放进一个 softmax 分母对每一个正样本对同样加权后放进另一个 softmax 的分子部分然后用log(1 ...)把它们缝在一起。这个结构和 InfoNCE 的-log(exp(s/τ) / Σexp(s-/τ))是同一家族只不过 InfoNCE 把所有负样本对同一对待权重都是 1而 Circle Loss 给每一对配了一个自己的权重。名字里的圆来自论文的几何解释把(s_n, s_p)画成二维平面AM-Softmax 这类加性 margin 的损失在这个平面上画出的决策边界是一条直线而 Circle Loss 的等损失线在有效区间内呈现为一段圆弧所以叫 Circle Loss。这个命名有点浪漫但核心贡献不是几何形状而是那个自适应权重。4.2 α 的自定步长到底在自适应什么α_p [1 m - s_p]意味着当正样本对的相似度s_p已经很高、接近1 m时α_p趋近于 0这一对基本被放过了当s_p很低、离目标还很远时α_p接近1 m这一对获得很大的权重。负样本那边同理α_n [s_n m]负样本对的相似度越低已经分得很开权重越小越接近-m快要混淆了权重越大。这就是所谓的self-paced翻译成自定步长有点绕其实就是按难度分配权重。和 Triplet Loss 的差别非常明显Triplet 对s_p的梯度是一个常数只要还没满足 margin不管这一对是已经快收敛了还是刚开始学推力一样大。结果就是训练后期绝大部分梯度都花在那些已经满足约束的正对和负对上真正需要推进的那几对反而被稀释了。Circle Loss 用α把这个问题解掉了而且解得很平滑——它不是硬截断是连续加权所以不会出现 OHEM 那种训练后期震荡的问题。4.3 和 Triplet、AM-Softmax、InfoNCE 摆在一起损失函数监督粒度权重方式主要优势主要痛点Triplet一个正对 一个负对固定无自适应直观、几何意义清晰依赖难例挖掘margin 敏感三元组爆炸AM-Softmax样本与类中心固定 margin加在角度上训练稳定类中心开销小只约束正类方向类内紧致度靠间接效果InfoNCE一个正对 一批负对负对等权温度 τ 控制无需标签自监督友好负样本数量直接影响效果队列维护复杂Circle Loss全部正对 全部负对按难度自定步长统一了三元组和分类两种范式计算量随对数量平方增长对比下来Circle Loss 的定位很清楚它把基于对的度量学习和基于分类的 softmax 学习统一到了一个公式里。当正负对各只有一对、且α恒等于 1 时它退化成带 margin 的 Triplet当把相似度换成类中心的内积、并且用 softmax 的归一化形式时它和 AM-Softmax 家族接上了。这也是它被称为统一的原因。InfoNCE 则是另一条路它不要求标签靠大量负样本和温度系数来塑造特征空间在自监督预训练里更常见。5. Circle Loss 的实现细节与调参手感5.1 代码以及为什么 α 要 detachimport torch import torch.nn as nn import torch.nn.functional as F class CircleLoss(nn.Module): def __init__(self, margin0.25, gamma256): super().__init__() self.m margin self.gamma gamma self.O_p 1 margin # 正对相似度的目标值 self.O_n -margin # 负对相似度的目标值 self.D_p 1 - margin self.D_n margin def forward(self, sp, sn): # sp: (B, P) 正对相似度已归一化 # sn: (B, K) 负对相似度已归一化 ap F.relu(self.O_p - sp).detach() an F.relu(sn - self.O_n).detach() logit_p -self.gamma * ap * (sp - self.D_p) logit_n self.gamma * an * (sn - self.D_n) loss F.softplus( torch.logsumexp(logit_n, dim1) torch.logsumexp(logit_p, dim1) ) return loss.mean()detach()这一步很多人会漏掉然后发现训练莫名其妙地不稳。原因在于如果不 detachα也会参与求导梯度里会多出一项∂α/∂s。这一项的符号和主梯度是相反的——比如正对相似度s_p越小α_p O_p - s_p越大而∂α_p/∂s_p -1它会在更新时把难对的权重往下拉正好抵消了原本想要的效果。论文的官方实现里就是把α当作每一步固定的样本权重不参与梯度回传。另一个必须注意的是相似度必须先归一化。O_p 1 m、O_n -m这些阈值只有在余弦相似度的量纲下才有意义取值范围[-1, 1]。如果直接把没归一化的特征内积丢进去相似度可能是几十甚至几百O_p和O_n就完全失去意义了。标准做法是特征先做 L2 normalize再乘一个 scale 提上去或者直接算余弦相似度。5.2 γ 和 m 的手感以及采样比损失本身更重要γ控制的是 logsumexp 的锐度。γ太小比如 32logsumexp 近似于对所有对求平均Circle Loss 就退化成对所有对等权收敛会明显变慢γ太大比如 1024logsumexp 近似于取最大值也就是只关注最难的那一对这时候遇到几个标注错误或者脏对训练就会剧烈震荡。我实测下来 128 到 512 之间比较稳常用值是 256。m是间隔一般取值在 0.1 到 0.5常用 0.25。m设太大会让正负样本对都很难满足约束训练早期几乎不收敛设太小则类间分不开验证集上的类间距离会明显偏小。判断m是否合适有个土办法训练稳定后统计验证集上正对的平均相似度和负对的平均相似度健康的分布应该是正对集中在 0.7 以上、负对集中在 0.1 以下中间有清晰的空档。但说实话在度量学习里采样策略对最终指标的影响经常比损失函数选哪个更大。Circle Loss 的P和K每类的正对数量、负对数量直接决定计算量和效果。我的人脸实验里每个 ID 至少采 4 张以上图一个 batch 里 16 个 ID 乘 8 张比 32 个 ID 乘 4 张效果更好即便后者总样本数一样。ReID 任务里用 PK 采样是标配。还有一个反直觉的点负样本对不是越多越好尤其训练早期。虽然α_n会自动给简单负对降权但 logsumexp 里的 log 项仍然会累积几千个简单负对加起来的偏置项会把损失的整体水平抬高间接影响学习率的效果。6. 检测、检索、长尾三种场景的选型对照6.1 目标检测分类和回归分开看检测的损失一定得分两支看这是很多新手最容易混的地方。分支常用损失为什么这么选分类BCE、Focal Loss、VFL正负样本极度不平衡需要难例聚焦机制置信度BCE、Focal Loss同上且负样本数量比分类还大框回归GIoU、DIoU、CIoU、Inner-IoU目标是连续坐标用 IoU 度量重叠度分布型回归Wasserstein 距离损失把边界建模成分布对模糊边界更鲁棒框回归这块多说两句。Wasserstein 距离损失的做法是不把框的边界当成一个确定的数而是建模成一个分布比如高斯分布用两个分布之间的 Wasserstein 距离作为损失。这样做的好处是当目标边界本身就模糊低分辨率、运动模糊、遮挡时硬性要求回归到某一个精确坐标会导致梯度方向摇摆而分布视角天然允许不确定训练更稳。Inner-IoU 则是另一个方向它不换度量换的是在哪个框上算 IoU用一对缩放系数构造出比原始框小一点的内框在内框上计算 IoU 项再和原框的 IoU 项按比例混合。直觉上内框的梯度更集中在目标中心区域对大框和小框的收敛速度差异有调节作用。它的优势是能直接替换 CIoU 里的 IoU 项其它项不动改动量极小。这两个和 Focal、Circle 是正交的理论上可以叠加但一次只改一个变量否则消融实验做不出来。6.2 检索与人脸有标签用 Circle无标签用 InfoNCE如果任务有明确的类别 ID 标签人脸、行人重识别、商品检索Circle Loss 或者 ArcFace 是首选。这两种都属于基于对的度量损失对类内紧致度的直接约束比 Softmax 强得多。如果数据没有标签或者你想先做一个预训练底座那 InfoNCE 更合适——它的结构天然适合自监督对比学习用数据增强构造正对batch 内其它样本做负对靠温度系数τ调节对难负样本的关注程度。有一个中间态值得提有标签但标签很粗比如只有大类没有细类这时候 Circle Loss 的m要设小一点或者干脆用带标签平滑的 AM-Softmax避免模型在粗标签内部强行拉开距离把细粒度特征学坏。6.3 长尾分类别急着上 Focal长尾分类场景下 Focal 不是万能的。如果是单标签、多类别、每类样本都很少长尾的尾很长重采样、logit adjustment、或者把分类头和度量学习头联合训练通常比单纯改损失更有效。Focal 的优势场景是样本总量足够大但正负极度不平衡也就是检测那种情形。判断标准很简单看你的正样本绝对数量如果正样本过万Focal 值得一试如果正样本只有几百先解决数据问题。7. 训练监控损失曲线怎么画、怎么读7.1 从 YOLOv8 的 results.csv 出图YOLOv8 训练完会在runs/detect/train/下生成results.csv里面每行是一个 epoch列名形如train/box_loss、train/cls_loss、train/dfl_loss、val/box_loss等。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) df.columns [c.strip() for c in df.columns] # 列名带空格务必先去干净 fig, ax plt.subplots(1, 2, figsize(14, 5)) for col in [train/box_loss, train/cls_loss, train/dfl_loss]: ax[0].plot(df[epoch], df[col], labelcol, alpha0.4, linewidth1) ax[0].plot(df[epoch], df[col].ewm(span10).mean(), linewidth2) # 平滑曲线 ax[0].set_xlabel(epoch); ax[0].set_ylabel(train loss) ax[0].legend(); ax[0].grid(alpha0.3) for col in [metrics/mAP50(B), metrics/mAP50-95(B)]: if col in df.columns: ax[1].plot(df[epoch], df[col], labelcol, linewidth2) ax[1].set_xlabel(epoch); ax[1].set_ylabel(mAP) ax[1].legend(); ax[1].grid(alpha0.3) plt.tight_layout() plt.savefig(loss_curve.png, dpi150)一个小技巧原始曲线的抖动很容易掩盖趋势我习惯把原始曲线用低透明度画一遍、指数移动平均用实线再画一遍两条叠在一起看。抖动幅度本身就是信息——如果抖动幅度随 epoch 增大而不是减小多半是学习率调度有问题或者 batch 太小。7.2 四种典型形态对应的病因曲线形态大概率原因验证方式分类损失断崖下降mAP 不动背景淹没正样本没学到打印正负样本的梯度占比损失整体震荡幅度不收敛学习率过大或 γ 过大或 batch 过小把 γ 降到 0 跑一次对照损失长时间卡住不降γ 太大且无 warmup或 α 让正样本权重过小前 3 个 epoch 用 γ0 试train 降 val 升过拟合或验证集标注口径不一致检查验证集标注加正则第一行那种情况我最近就遇到过一次处理方式是把分类损失从 BCE 换成 Focal同时把α定在 0.25、γ定在 2重跑之后 mAP 从 0.41 涨到 0.47召回提升比精度提升更明显说明确实是难正样本之前没被学到。第二行的情况我也踩过γ 3加初始学习率 0.01前期震荡得没法看降到γ 1.5就稳了。8. 我自用的排查清单改损失函数这件事翻车大多不是出在数学上而是出在周边配置没跟着改。下面这份清单是我每次都过一遍的数值稳定性所有涉及log(p)的地方一律用 logits 版本的接口不要先 sigmoid 再 log。概率是否归一化度量学习的相似度必须是余弦或做过 L2 归一化的内积否则阈值参数全部失效。α 要不要 detachCircle Loss 里必须 detachFocal Loss 里的p_t不能 detach它本来就是梯度的载体。和 label smoothing 是否冲突二选一。训练前期是否需要 warmupγ 大于 2 时前几个 epoch 用 γ0 过渡。推理阈值是否重扫改过分类损失就必须重扫置信度和 NMS 阈值。损失权重是否失衡多任务里改了一支损失权重要重新标定。分布式 reduction跨卡样本数不均时用 sum 而非 mean。消融是否单变量一次只改一个损失分支否则出了效果也不知道是谁的功劳。指标是否同向损失降了指标不涨先怀疑是代理和目标的缝隙问题而不是超参。最后分享一个我个人觉得最省时间的习惯任何损失函数改造先在一个小规模子集上跑 20 个 epoch把正负样本的梯度占比、正对的平均相似度、负对的平均相似度这三个数打出来看比盯着总损失曲线有用得多。总损失是一个被平均过的、丢失了大量信息的标量它降得漂亮不代表模型学到了你想让它学的东西——这一点从交叉熵到 Focal Loss 再到 Circle Loss其实一直没变。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进