把 Focal Loss 论文重新翻出来读了一遍距离第一次看已经有好几年。当年为了赶实验只急着把它塞进检测代码里调参后来手头项目一忙就放下了这次趁着空闲把公式从头推了一遍又对着论文里的实验表格逐行看才发现很多当年踩过的坑答案其实都写在论文的细节里只是当时的自己读得太糙。这篇被广泛称作 RetinaNet 的论文核心贡献可以拆成两件事第一用 Focal Loss 解决密集目标检测中极端类别不平衡的问题第二配合特征金字塔网络FPN把单阶段检测器的精度拉升了一个档次。如果你正在被正负样本比例失衡搞得训练不稳定或者在目标检测、长尾分类的任务里琢磨怎么给难样本加权重这篇学习笔记应该能给你一些落到实处的参考。我不只讲公式结论还会把推导过程、实现细节、复现时容易踩的坑一并写出来。1. 为什么密集检测器会被类别不平衡卡死1.1 一阶段与两阶段检测器在样本分布上的差异目标检测里一阶段和两阶段的争论表面上是精度与速度的取舍但本质差别在于拿到手的训练样本分布完全不同。两阶段检测器比如经典的先提区域再分类路线在第一阶段用区域提案网络先把可能有目标的区域筛出来第二阶段再对这些区域做精细分类和回归。这个流程里第二阶段拿到的候选区域数量是可控的常见做法是每张图采样 256 或 512 个 proposal并且强制保证正负样本比例在 1:3 左右。也就是说两阶段检测器在训练阶段已经把最难缠的海量背景问题压住了。一阶段检测器就没有这个缓冲。它直接在整张图的密集网格上预测以常见的 anchor 方式为例输入一张 800 像素左右的图经过多层特征金字塔后产生的 anchor 数量经常是十万甚至更多。而一张图里的真实目标通常只有几个到几十个算下来正负样本比例随便就达到 1:1000。你可以把训练过程想象成一场课堂考试布置了一万道一眼就会的送分题真正有区分度的难题只有几十道如果总分主要由送分题决定那无论你难题怎么答最后排名都差不多。如果这里只靠随机抽样把负样本数量压下来场景会变好一些吗有一定作用但随即带来的问题是抽样不固定、模型对不同场景的负样本覆盖不充分而且你没法保证抽出来的负样本就有代表性。论文作者当时盯住的就是这种结构性问题一阶段想保持快速就必须在密集预测的框架下找到一种让大部分简单负样本不那么影响训练的办法。1.2 简单负样本才是真正的梯度刺客很多人以为类别不平衡的麻烦主要来自正样本太少、负样本太多于是第一时间想给负样本降个权不就行了。但 Focal Loss 论文里的分析更细一层真正的问题不只是负样本数量多而是负样本里有大量已经被正确分类、且非常确信的简单样本。用交叉熵损失训练时一个预测置信度很高的简单负样本比如 p_t 0.9单个样本的 loss 只有 0.1 出头但十万个这样的样本加起来贡献出的梯度绝对量非常大。相比之下一个真正难分的样本比如 p_t 0.5模棱两可单个 loss 是 0.69数量又少。于是在最后一次梯度累加时简单负样本的权重通过“数量优势”完全压制了难样本和正样本模型就被带偏了。这种简单样本压制难样本的现象普通样本均衡的数据集里也有但没那么致命。一旦到了密集目标检测这种极端不平衡场景就变成主要矛盾。论文的高明之处在于不抛弃任何样本不显式采样而是用一条连续的曲线把每个样本的贡献按难度重新分配——这正是 Focal Loss 的思路起点。2. 从交叉熵一步步走到 Focal Loss2.1 交叉熵的符号问题与 p_t 的提法为了把公式的来历讲清楚先把二分类交叉熵写出来。对于一个真实标签 y1 或 0模型对正类的预测概率为 p交叉熵可以写成CE(p, y) -log(p)当 y1CE(p, y) -log(1-p)当 y0在目标检测里很多时候用 sigmoid 对每个 anchor 做独立二分类也就是多标签的形式。为了方便统一论文引入一个记号 p_tp_t p当 y1 时p_t 1-p当 y0 时这样一来交叉熵就成了一句话CE(p_t) -log(p_t)。p_t 越接近 1说明模型对这个样本越有把握分类越简单p_t 越小说明模型越犹豫或直接错了样本越难。我一开始不太习惯这个记号总觉得它就是换了个写法。后来发现p_t 是理解 Focal Loss 的一把钥匙因为后面整个调制因子都建立在 p_t 之上而不是建立在模型原始输出 p 上。如果你拿着 p 而不是 p_t 去算很容易在正负样本上写出两个分支公式就变得啰嗦。2.2 关键一步给每个样本加上难度调制因子交叉熵的问题是一视同仁不管 p_t 是 0.9 还是 0.5每个样本按自己的 loss 参与求和简单样本靠数量获胜。Focal Loss 做的改动只有一处却非常关键在交叉熵前面乘一个调制因子FL(p_t) -(1 - p_t)^γ * log(p_t)这里 γ 是聚焦参数focal parameter通常取 2。拆开看这个调制因子 (1-p_t)^γ当 p_t 很接近 1比如 0.9(1-0.9)^2 0.01原来 0.1 的 loss 被压到 0.001。简单样本的贡献被大幅削减。当 p_t 在 0.5 附近(1-0.5)^2 0.25loss 从 0.69 变成约 0.17。中等难度的样本保留了一部分。当 p_t 很小比如 0.1也就是模型把正类分得很糟(1-0.1)^2 0.81loss 从 2.3 变成约 1.87。难样本几乎原样保留。也就是说这个因子像一个难度筛子自动把训练焦点从堆积如山的简单背景转移到少数难样本上。关键是这个筛子不是硬性的丢与不丢而是连续且可微的所以整个模型依然能用标准反向传播训练不需要额外设计样本选择逻辑。为什么 γ2 而不是 γ1 或者 γ0γ0 时 Focal Loss 退化成普通交叉熵γ 越大简单样本被压得越狠但难样本的权重并没有无限放大因为 (1-p_t)^γ 对难样本趋近 1只是保留不放大。到 γ5 的时候作者观察到简单样本几乎不提供梯度训练变得困难AP 反而下降。你可以在自己的数据上从 γ2 起步然后往 1 和 3 两边试。2.3 α-balanced 变体把类别权重和难度权重解耦只看调制因子的话它只调整了难易程度并没有处理正负类别数量本身的不平衡。Focal Loss 里还有一个经常被人忽略的配套项α-balanced 版本FL(p_t) -α_t * (1 - p_t)^γ * log(p_t)其中 α_t 是一个与类别相关的权重正样本用 α负样本用 1-α。论文里建议 α0.25、γ2 组合效果最好。α 负责正负类别的数量差异γ 负责简单难样本的权重差异两个维度是正交的最好不要混着说。为什么有了 γ 还需要 α因为调制因子只改变难易样本之间的相对权重没有改变正负类别之间的总体比例。同一难度下负样本基数依然可能是正样本的上百倍不引入 α 的话整体梯度依然会偏向负样本方向。论文实验里只调 γ 不调 α或者只调 α 不调 γ效果都不如两个一起用。这一点我在复现的时候也验证过单独把 γ 拉到 2 不加 α 时检测 AP 会低一些尤其是小物体那一栏掉得明显。2.4 看一个数值例子就全明白了口说无凭我列一个简单的数值表假设 γ2、α1也就是先不看 α 的版本样本类型p_t 值CE 损失调制因子 (1-p_t)^2Focal Loss 值直观结论简单背景0.90.1050.010.001损失被压到几乎不贡献中等样本0.50.6930.250.173损失保留约 25%困难正样本0.12.3030.811.865损失几乎原样保留看到这个表就明白 Focal Loss 为什么能稳定压制简单负样本了它不是把负样本删掉而是让它们在梯度求和里的发言权指数级下降。这样即使负样本数量是正样本的几百倍只要大部分负样本已经学得好、p_t 高它们就不会再继续霸占梯度。反过来也说明一个实际问题如果训练初期所有样本都还没分好p_t 普遍不高那 Focal Loss 的压制效应还体现不出来loss 会显得很高。这不是 bug而是因为调制因子的目标是学得好的别再抢戏而不是一开始就把总 loss 压低。很多第一次用的人看训练曲线大跌眼镜以为模型坏了其实是没理解这个动态特性。2.5 手写一个 PyTorch 实现理解了公式之后写实现就是水到渠成的事。下面是一个贴合论文公式的 PyTorch 版本我习惯把它封装成函数方便在训练循环里直接调用import torch import torch.nn.functional as F def focal_loss(logits, targets, alpha0.25, gamma2.0): # logits: [N, num_classes]targets: one-hot 或 float 标签 [N, num_classes] ce F.binary_cross_entropy_with_logits(logits, targets, reductionnone) p torch.sigmoid(logits) # p_t: 正样本取 p负样本取 1-p p_t p * targets (1 - p) * (1 - targets) # alpha_t: 正样本用 alpha负样本用 1-alpha alpha_t alpha * targets (1 - alpha) * (1 - targets) # focal weight focal_weight (1 - p_t) ** gamma loss alpha_t * focal_weight * ce return loss.mean()有几个实现细节说明一下这里用binary_cross_entropy_with_logits因为分类子网络输出的是未过 sigmoid 的 logits直接在 loss 里做 sigmoid 更稳定。targets我传的是与 logits 形状一致的 one-hot 浮点数因为检测任务中一个 anchor 只分配一个类别但训练时也可以用多标签。真正要小心的是 loss 的归一化很多开源实现不会直接把所有样本的 focal loss 求平均而是只除以正样本的数量这样能避免负样本数量影响 loss 的绝对值尺度。上面的代码为了演示简洁用了 mean实际项目里建议参考检测框架的归一化方式按正样本数归一化。3. RetinaNet 里那些容易忽略的实现细节3.1 架构FPN 负责定位在哪里看分类子网络负责看到什么Focal Loss 不是一个孤立技巧它是嵌在 RetinaNet 整个框架里起作用的。RetinaNet 的结构可以拆成三块一个主干网络提取特征一个特征金字塔网络FPN输出多层不同尺度的特征图两个任务子网络分别做分类和回归。FPN 在这里很重要因为密集检测要在多种尺度下进行。RetinaNet 用 P3 到 P7 五层金字塔每层 anchor 的尺度从 32×32 到 512×512基本覆盖了从小到大各种目标。如果没有 FPN只靠最后一层高层特征做检测小目标定位信息早就被下采样丢光了Focal Loss 再厉害也救不回来。分类子网络的设计也值得留意它不是把特征图拉平后过一个全连接而是用一组小卷积一层一层把语义信息抽出来。具体来说分类塔用 4 个 3×3 卷积输出通道保持 256最后再用一个 3×3 卷积输出 K×A 个分数K 是类别数A 是每个位置的 anchor 数量。论文里所有分类都用 sigmoid不用 softmax。我一开始觉得奇怪常规分类不都应该 softmax 吗但在密集检测的多标签场景下sigmoid 更合理因为一个位置上的几个 anchor 可能属于不同类别而且 sigmoid 对每个类别独立建模训练效率也更高。3.2 分类分支的 bias 初始化其实比想象中重要这是我复现时吃了大亏的一个细节。论文里专门提了一句分类子网络的最后一层卷积bias 要初始化成b -log((1 - π) / π)其中 π 0.01。算一下就知道 b 大概是 -4.595。这么做的目的是让训练刚开始的每个 anchor 都被预测为背景的概率接近 0.99也就是 p_t 接近 0.99Focal Loss 的调制因子一开始就处于强压低状态。如果不做这个初始化训练第一个 step 的损失可能高到离谱梯度从一堆还没分清的样本上涌入前几百步训练会非常不稳定。我踩过的坑是当时偷懒没设置 bias结果 loss 前期震荡剧烈后来 AP 一直上不去。最初我还以为是学习率的问题后来对照论文才发现是分类头初始预测置信度太中立导致每一轮都有海量背景样本参与梯度计算把优化方向搅乱了。这个初始化技巧其实暴露了 Focal Loss 的一个特性它是一个训练动态相关的损失。样本权重不是固定的而是随着模型当前置信度变化。因此初始化状态会影响后续所有样本的权重分配不能只顾着把网络结构搭出来分类分支的起点也要符合整体设计。3.3 Anchor 与匹配规则RetinaNet 的 anchor 设计是金字塔的每一层都使用 9 个 anchor即 3 个尺度scale乘以 3 个长宽比ratio长宽比通常取 1:2、1:1、2:1。不同金字塔层级对应不同基础尺度这样同一张图的候选框能够覆盖各种大小的目标。正负样本的分配规则在论文里用的是 IoU 阈值和某个真实目标 IoU 超过 0.5 的 anchor 算正样本与所有真实目标 IoU 都低于 0.4 的算负样本落在 0.4 到 0.5 之间的不参与训练。这个“灰色地带”很关键因为这些 anchor 说它是正也不是、说负也不是强行分配只会给训练塞噪声。很多复现效果差的代码库就是没有仔细处理这个区间把 0.4-0.5 都当负样本来算结果引入大量边界模糊样本Focal Loss 反而被这些难样本带偏。与两阶段检测器不同RetinaNet 不会强制限制每张图正负样本的比例而是靠 Focal Loss 来自动调节贡献。所以 anchor 匹配规则对最终效果的影响比想象中更大正样本太苛刻就会造成匹配不上的目标对模型完全不可见负样本太宽又会让灰色地带的难样本大量涌入。建议复现时先按论文的 0.5/0.4 来不要凭感觉改。3.4 训练超参和实验效果论文里的实验配置大致是这样的主干网络用 ResNet-50 或 ResNet-101前面接 FPN输入图像缩放到最短边 800、最长边不超过 1333SGD 优化器momentum 0.9weight decay 0.0001初始学习率 0.01batch size 16 左右训练步数在 90k 步上下。分类和回归两个子网络共享 FPN 特征但各自保持独立权重。关于实验结果我记得读论文时对比表里印象最深的一组当时的单阶段检测器在公开检测数据集上的 AP 普遍在 30 上下而 RetinaNet 用 ResNet-101 当骨干时AP 能到 39 附近跨过了一段不小的精度鸿沟甚至能跟许多两阶段检测器打平或更好。这里不要拿我记忆里的数字当标准答案不同输入尺度和训练设置下会有浮动但趋势是确定的引入 Focal Loss 之后单阶段检测器从速度快但精度差变成了速度和精度都能打。3.5 γ 的选择效果最好的是 2不是 5论文里专门做了 γ 的消融实验从 0 扫到 5结论是 γ2 时 AP 最高。γ0 就是普通的 α-balanced 交叉熵相当于没加调制因子γ1 能压低一部分简单样本但对那些已经很确信的背景样本压得不到位γ5 时简单样本几乎完全不贡献梯度好处是难样本主导训练坏处是模型容易被个别标注错误或者极其难分的样本牵着走AP 反而下降。这个趋势在不同数据集上会有细微差异但我建议调参时把 γ 当作“注意力强度”来理解太小模型注意力分散简单背景仍然参与度高太大注意力过于集中模型可能钻牛角尖。我自己试下来γ2 对大多数检测任务都是个稳定起点α 可以优先试 0.25然后根据结果微调。4. Focal Loss 与其他困难样本方法的关系4.1 OHEM 是硬挖Focal Loss 是软加权在读 Focal Loss 论文之前我最熟悉的困难样本处理方法是 OHEM在线困难样本挖掘。OHEM 的思路很直接计算完一轮 loss 之后把所有样本按 loss 从大到小排序只挑出 loss 最高的那部分样本做反向传播其余样本的梯度直接置零。这样模型每轮都只看最难的样本。OHEM 和 Focal Loss 的目的一致但实现哲学完全不同。OHEM 是一个离散的选择过程样本要么被选中、梯度完整计算要么被丢弃、梯度为零。Focal Loss 则是连续的每个样本都保留只是根据难度乘上一个 0 到 1 之间的系数。换句话说Focal Loss 可以看作 OHEM 的一个光滑近似它把前 k 个的硬阈值换成了按难度衰减的软权重。这种连续性带来的好处是训练过程平稳。OHEM 在样本排序边界附近会有突然的梯度跳变而且一旦某一轮选中的难样本存在标注噪声整个更新方向很容易被带歪。Focal Loss 给难样本的权重虽然大但不是非 0 即 1对噪声的耐受性要好一些。论文里对比过这种做法在密集检测框架里的效果OHEM 在区域提案式的检测器里有效但直接搬到密集预测框架里AP 提升有限Focal Loss 提升明显得多。4.2 与常规类别加权、负样本降采样的区别还有一种常见的做法是给正负样本分配不同权重比如正样本权重设为负样本的 3 倍。这解决的是类别数量不平衡但解决不了简单样本数量优势的问题。你可以做这样一个思想实验即使把负样本总权重压到和正样本一个量级这堆权重还是会被几万个简单负样本瓜分每个简单负样本依然分到一点梯度模型觉得反正我大体分对了没必要专门去改那些少数难样本。负样本随机降采样也有类似局限。降采样把负样本总数砍下来之后难样本占比会相对提高但采样的随机性意味着很多有难度的负样本可能压根没被采到模型没见过自然学不好。Focal Loss 不采样它让全部样本都参与前向计算只是反向传播时给梯度按难度重新分配。听起来是一个很小的差别实际操作上却省去了很多调采样比例的麻烦。4.3 出了目标检测它还能用在哪Focal Loss 的设计并不绑定目标检测只要任务面临极度不平衡 大量简单样本就可以尝试。比如长尾图像分类里头部类别样本多、尾部类别样本少直接用交叉熵训练很容易让尾部类别被忽略这时候给每个样本按置信度加调制因子模型就会把注意力往尾部类别转移。语义分割里也有类似的例子图像中一个小目标区域可能只占几个像素但背景像素占了绝大部分。把 Focal Loss 用在分割的像素级分类头上能让模型更关注那些难分的边缘像素和稀少类别。我在自己的模拟实验里也验证过Focal Loss 对识别小目标的 recall 提升立竿见影但也要注意它会让错误标注的样本权重变大标注质量差的数据集上要谨慎。5. 复现过程中的常见问题与排查心得5.1 如果损失贴地但 AP 上不去先看 bias 初始化复现 Focal Loss 时最诡异的现象之一是训练 loss 收敛得很漂亮曲线一路下降但验证集 AP 就是上不去。这种情况多半不是 Focal Loss 本身的问题而是分类分支的初始化或正负样本匹配有问题。先说 bias 初始化。如果没有把分类头的 bias 设成 -log((1-0.01)/0.01)训练初期模型对所有 anchor 的初始置信度就是 0.5 上下Focal Loss 的调制因子都是 0.25 左右的中间值简单背景和难样本权重没有拉开等于 Focal Loss 没有进入预期的“压制简单样本”状态。虽然训练后期模型自己也会慢慢调整但前期优化轨迹已经偏了最终 AP 会差一截。再说 anchor 匹配。检查一下你的代码里灰色地带IoU 在 0.4 到 0.5 之间的 anchor 是被正确忽略了还是被错误地归为负样本。如果错误归为负样本这些既有点目标影子又算背景的 anchor 一开始 p_t 就低Focal Loss 会给它们很大的权重模型花大量精力去学这些模糊样本干净的正样本反而被冷落。这两处是我最常踩的坑。5.2 γ 和 α 的手感我调参的一些经验先记住一个原则γ 调难度权重α 调类别权重不要同时大改。我一般先固定 γ2、α0.25 复现出论文量级的基线然后再单变量地去调。如果 γ 调大后发现 loss 曲线波动剧烈就看是不是难样本里有太多标注噪声。因为 γ 越大模型越盯着 p_t 小的样本而这些样本里很可能混着边界模糊的 anchor。一个可行的缓解方式是把 γ 退回 1.5 或者搭配更严格的 anchor 匹配阈值把模糊负样本挡在训练之外。α 的手感更直接如果检测结果里漏检多正样本拉不起来试着把 α 从 0.25 调向 0.5给正样本更高权重如果误检多负样本压制不住就调向 0.1 或 0.15。但注意 α 和 γ 有轻微耦合α0.25 之所以是论文推荐值是因为它在公开的密集检测场景下平衡得比较好你的数据分布跟它差别大时最好做个小网格搜索。5.3 有关 Loss 数值和收敛曲线的几个盲区用 Focal Loss 之后训练 loss 的绝对数值会比交叉熵低很多这是正常的别一看到 loss 变低就觉得模型变好。因为调制因子把大量简单样本的贡献压到了 0.001 量级总 loss 自然好看但你要关注的是验证集上的 AP 或精确率召回率而不是 loss 大小。还有一个盲区是Focal Loss 对正样本的贡献几乎是“全程保护”的但正样本数量太少的时候即便权重很高梯度也扛不住海量负样本的总体量。这时候除了调 α还要检查是不是 anchor 匹配阈值太严、正样本太少或者 FPN 某些层没有分配到正样本常见于小物体层。我习惯在训练时打印每个金字塔层级的正样本数量一旦发现某层长期为 0首先怀疑 anchor 尺度设置和真实目标尺寸分布不匹配。5.4 一个建议的调试清单我把排查经验整理成一张表方便你对照现象优先检查项建议动作训练初期 loss 爆炸或震荡分类头 bias 初始化b -log((1-0.01)/0.01)loss 很低但 AP 不高anchor 匹配阈值、灰色地带处理确认 IoU0.5 为正0.4 为负中间忽略漏检严重α 过小、正样本太少α 从 0.25 上调检查每层正样本数误检严重α 过大、γ 过小α 下调γ 上调到 2 左右某个尺度目标检测很差FPN 对应层 anchor 尺度不匹配调整该层 anchor 基础尺度难样本噪声干扰γ 过大γ 退回 1.5复查标注质量这份清单不是银弹每条都需要结合你自己的数据显示来验证。但按照这个顺序查能省下不少“对着空气调参”的时间。最后我再说说个人体会。把 Focal Loss 论文从头到尾推一遍之后我最大的感触是它真正的贡献不是发明了一个 loss 公式而是把一个训练动态问题显式地建模进了目标函数。你在读的时候光背结论不够最好自己把 p_t 的定义、调制因子的梯度行为、bias 初始化的数值影响都手推一遍。哪怕只是对着公式算三分钟后面调参手感都不一样。如果你正准备在项目里用它建议先不要急着换大模型大框架而是用一个小数据集把 γ2、α0.25 的基线跑通再逐步试 1、3 和 0.1、0.5。这样踩过一轮坑之后你对 Focal Loss 的每个旋钮都会有自己的直觉。