☰
Focal Loss与RetinaNet:破解单阶段检测器精度瓶颈的关键
2026/10/15 9:54:24 网站建设 项目流程

我在读这篇论文之前一直有个疑惑:当时单阶段检测器明显比两阶段检测器快,精度却被压着打,几乎成了行业共识。SSD 已经用多尺度特征做了很多努力,Faster R-CNN 系的两阶段方法依然稳稳领先一到两个点。多数人把这个差距归因于"两阶段有区域提议做粗筛,单阶段直接回归太粗暴"。直到我把 Focal Loss 和 RetinaNet 这篇论文真正读懂,才发现差距的根源不在网络结构,而在训练时正负样本的极端不平衡,以及损失函数对困难样本的态度。

这篇论文不长,信息量却很大。Focal Loss 不是简单给损失加个权重,而是从一个非常简单的问题出发重新设计了分类损失;RetinaNet 也不是另起炉灶的新网络,而是把一个标准 FPN 检测器认真组装了一遍,用工程细节证明"损失函数改对了,结构不用太花哨也能追平甚至超过两阶段"。今天不少分割、跟踪、文本检测任务里的损失函数改良,追根溯源都能落到这篇论文上。如果你手头有检测模型想提点,或者始终理解不了"单阶段差口气"的根因,这篇文章值得反复读几遍。下面按我自己的理解,从问题源头、数学设计、网络结构、训练细节一路拆到工程调参。

1. 单阶段检测器被两阶段压着打的真正原因

1.1 精度鸿沟到底差在哪里

两阶段检测器的流程是"先粗筛候选框,再逐个精修"。RPN 阶段会从几十万个锚框里筛出几百个候选,这些候选已经过滤了绝大多数背景,第二阶段分类器面对的是相对均衡的正负样本。单阶段检测器没有这个粗筛过程,它直接在特征图的每个位置上预测所有锚框的类别和框偏移。

在训练的时候,这种结构差异会带来一个极其悬殊的数量对比:一张图几十万个锚框里,真正与物体相交的正样本可能只有几十个,剩下全是背景。用当时的标准交叉熵损失训练,模型梯度几乎被背景样本淹没。困难负样本和大量易分负样本混在一起,网络很快学会了"输出低置信度"这个最优解——因为把所有框都判成背景,损失反而最低。

我当时在几个公开数据集上做过统计,训练早期超过九成的分类损失都来自那些与物体无关的纯背景锚框。这些样本置信度很低,但数量太大,累计贡献的梯度足以主导优化方向。这跟"网络不够深"或者"特征不够强"没有关系,纯粹是损失函数层面的失衡。

1.2 为什么在线难例挖掘也救不回来

在这篇论文之前,业界的标准对策已经有两类:一类是采样策略,比如设定正负样本比例 1:3 或者做在线难例挖掘(OHEM);另一类是给损失函数直接加权重。OHEM 的做法是算完损失后只保留损失最大的那一批样本反向传播,本质上是"硬筛选"。

但 OHEM 有个明显的副作用:它筛掉的是"好分"的样本,思路偏极端,容易在训练后期陷入抖动。更关键的是,"难例"和"正样本"并不等价。大量背景框里也存在一些让模型困惑的难负样本,它们确实值得重点学;而正样本里也有不少已经分类正确且置信度很高的简单样本,它们不应该继续霸占梯度。OHEM 只能按损失排序粗暴截取,无法在难易和正负这两个维度上做精细区分。

因此,真正需要的是一个能对"每一个样本"按难易程度连续降权的损失,而不是把样本切成两拨。Focal Loss 的思路恰好就是这个方向:既然难例挖掘的本质是让容易样本少贡献梯度,那干脆在损失公式里加一个随置信度连续衰减的调制因子。

2. Focal Loss 的三个关键决策,以及它们各自解决什么

2.1 从交叉熵到调制因子:公式变了一小步,行为变了一大步

二分类交叉熵的标准写法是

CE(p, y) = -log(p_t)

其中 p_t 表示该样本被模型正确预测的概率:正样本取 p,负样本取 1-p。这个损失对难易样本一视同仁:分类得很对的样本 p_t 接近 1,损失趋近 0,但它数量实在庞大,总和起来仍然很大;分类得差的硬样本 p_t 小,损失大,但数量少,容易被淹没。

Focal Loss 只做了一处修改:

FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)

加号后面多了两项。alpha_t 是类别平衡权重,属于常规操作;真正决定行为的是 (1 - p_t)^gamma 这个调制因子。当一个样本已经被分得很准、p_t 接近 1 时,调制因子趋近 0,这条样本的损失被压低;当一个样本分得很差、p_t 很小,调制因子接近 1,损失几乎不受影响。gamma 越大,对容易样本的压制越狠。

我记得第一次看到这个式子时的直观感受是:它其实就是把困难样本挖掘的思想写成了可微的连续函数,不用排序、不用截断、不用采样,所有样本都参与梯度计算,只是"话语权"不同。

2.2 alpha 与 gamma 的分工:一个平衡数量,一个平衡难度

alpha_t 和 gamma 很容易让人混淆,但它们解决的问题完全不同。

alpha_t 解决的是正负样本数量差。正样本几十个,负样本几十万个,光靠调制因子还不够,因为即使单个背景样本被压得很低,几十万条加起来仍可能盖过正样本。论文里取 alpha = 0.25,也就是正样本保留 0.25 的权重,每个负样本只拿 0.75 的权重。看起来正样本权重"更低",但结合样本数量,正样本的实际总贡献反而被抬上来了。这个值多少有点违反直觉,我第一次看表格时也愣了一下:alpha 给正样本 0.75 不是更合理吗?实验结果却说明,在 gamma = 2 时,正样本的难易分布已经主导了梯度,alpha 不宜过大,0.25 附近效果最好。

gamma 解决的是难易样本的贡献分配。它把大量 easy negative 的损失压到几乎可以忽略。论文给的例子足够直观:p_t = 0.9 的简单样本,gamma = 2 时损失会被缩小 100 倍;而 p_t = 0.5 的模糊样本只缩小 4 倍。于是训练重心自然滑向那些分不清的框。

所以正确的理解方式是:alpha 调整正负两类的总体权重,gamma 调整困难样本 vs 容易样本的权重。两者作用在不同维度上,互相配合。只用 alpha 不加 gamma,相当于把交叉熵的全局权重改了;只用 gamma 不加 alpha,再去掉数量不平衡的正负样本,容易样本被压掉之后,背景仍然可能靠数量取胜。

2.3 为什么用连续调制而不是硬截断

这是我觉得整个设计里最巧妙的地方。OHEM 的硬截断会在训练过程中造成样本集的骤变,模型刚学到一点,下一波样本又换了。Focal Loss 的调制因子是置信度 p_t 的连续函数,不会把任何一条样本的梯度变成零,只会让它越来越小。这保证了优化的平滑性。

另外,调制因子直接作用于梯度。对 sigmoid 输出求导后,FL 的梯度里同样含有一个 (1 - p_t)^gamma 项,也就是说梯度也会随置信度升高而衰减。高置信度的正样本不再持续提供大的更新方向,模型不会反复调节那些已经学会的简单目标;低置信度的困难目标始终保持较强的学习信号。这个特性让训练过程比交叉熵稳定得多。

值得一提的是,gamma 不是越大越好。gamma 太大,简单样本几乎不贡献梯度,而初期模型的预测能力很差,几乎所有正样本的置信度都不高,会同时压住正样本的优化;同时 gamma 过大会让训练早期梯度信号极度稀疏,收敛变慢。论文里默认 gamma = 2,在多个实验设置下都接近最优。

2.4 数值实现里的两个细节

实现 Focal Loss 本身不难,但有两个细节容易踩坑。一是 p 必须经过 sigmoid 归一化后再算,尤其要配合"logits + 稳定 log"的实现,避免 log(0)。二是多分类检测里每个锚框对每个类都独立算一个 sigmoid 二分类损失,而不是对所有类做 softmax。原因在于背景类并不是一个真实的"类",而是"不属于任何目标",用 sigmoid 可以在每个类别维度上独立拉高或压低响应。论文里类别损失是逐类 sigmoid focal loss 的求和,这一点和很多分类任务的 softmax 交叉熵不一样。

提供一个便于理解的 PyTorch 实现:

import torch def focal_loss(logits, targets, alpha=0.25, gamma=2.0): # logits: [N, K], targets: [N, K], 0/1 标签 p = torch.sigmoid(logits) p_t = p * targets + (1 - p) * (1 - targets) alpha_t = alpha * targets + (1 - alpha) * (1 - targets) loss = -alpha_t * (1 - p_t) ** gamma * torch.log(p_t + 1e-12) return loss.mean()

生产环境建议用binary_cross_entropy_with_logits的reduction='none'后再乘调制因子,让 logits 在函数内部做数值稳定处理。

3. RetinaNet:把 Focal Loss 装进一个合理的目标检测器

3.1 骨架与 FPN 多尺度:从 P3 到 P7

RetinaNet 本身没什么结构创新,它的价值在于把 Focal Loss 验证得非常干净。骨干网络用的是带 FPN 的 ResNet,FPN 输出五个金字塔层,从 P3 到 P7,每层对应的特征分辨率分别是原图的 1/8、1/16、1/32、1/64、1/128。P6 和 P7 由 P5 依次降采样得到,主要覆盖大物体。

每层特征图上的每个位置都预设锚框。RetinaNet 的锚框配置是 3 个宽高比(1:2、1:1、2:1)乘 3 个尺度(2^(0/3)、2^(1/3)、2^(2/3)),每层 9 个锚框。锚框的基础尺度从 P3 的 32 像素逐层翻倍到 P7 的 512 像素。算下来,一张 800×800 的输入在五个金字塔层里总共会产生约十万个锚框。这就是训练时正负样本比例悬殊的物理来源。

我把这组参数反复算过几遍,它的核心思路是让不同层各管各的尺度范围。P3 层管小物体,P7 层管大物体,每层的特征感受野和锚框尺度大致对应。FPN 在这里不只是提升小目标检测,更是在给每层限定"可管范围",降低后续分类头的负担。

3.2 分类头与回归头:两个平行的卷积子网络

RetinaNet 在 FPN 每一层之后都接两个平行的子网络:分类子网和回归子网。两个子网都是先过四层 3×3 卷积,通道数 256,每层带 ReLU;分类子网最后再过一个 3×3 卷积,输出 K×9 个通道,K 是类别数;回归子网最后输出 4×9 个通道,4 代表框的中心偏移、宽高缩放。

一个值得关注的细节是,这两个子网刻意没有共享参数,论文里的说法是分类和框回归关注的特征层次不同。我在实践中认同这一点:分类容易受纹理和语义信息影响,框回归更依赖边缘和空间结构,硬共享参数反而两头都拖累。这两个子网在所有金字塔层之间是共享参数的,这样可以控制参数量,同时让各层学到统一语义。

分类子网最后一层的初始化也很有讲究,这个细节我放到训练部分专门讲。整个网络结构看下来,没有任何花哨的模块,所有创新都集中在损失函数,这正是论文说服力的来源。

3.3 锚框匹配规则与 ignore 区

训练时锚框先和真实框做 IoU 匹配,规则是三段式:IoU 大于 0.5 的锚框视为正样本,IoU 小于 0.4 的视为负样本,介于 0.4 到 0.5 之间的直接忽略,不参与损失计算。另外保证每个真实框至少匹配一个最高 IoU 的锚框,防止某些目标没有正样本。

这个 [0.4, 0.5) 的 ignore 区间不是随便划的。它是为了让分类头不用纠结那些"沾到了一点边"的模糊锚框。如果把这些框强行归为正样本,噪声会很大;归为负样本,又会误伤正确的粗略定位。留一个缓冲区,训练会稳很多。

我在自己复现时曾经把 ignore 区改成 [0.3, 0.6],发现收敛速度没有明显差异,但最终 AP 轻微下降。后来想通了:ignore 区过宽会让大量信息被浪费,模型学到的判别边界不够精细;过窄又会让标签噪声变大。论文的默认值在多数数据集上是稳健的选择。

4. 复现时最容易踩的坑和训练细节

4.1 最后一层 bias 初始化:一个让训练节省大量时间的技巧

RetinaNet 对分类子网最后一层有一个神奇的初始化操作:把偏置 b 初始化为

b = -log((1 - pi) / pi),pi = 0.01

pi = 0.01 表示模型在训练开始时就认为每个锚框属于背景的概率是 99%。这个初始化的必要性在于,训练初期交叉熵会猛烈推动模型把所有锚框往背景上推,如果不加这个先验,早期损失会非常大,梯度容易不稳定。有了这个 bias,模型起步时就"假装"已经学会了输出低置信度,Focal Loss 的调制因子在早期也能直接生效。

我在别处的项目里看到有人复现时漏掉这行初始化,训练 loss 前期奇高,需要额外多跑很多轮才能追上。这个三行代码的技巧,性价比极高。

4.2 alpha 与 gamma 的默认参数为什么靠谱

论文用大量实验扫过 alpha 和 gamma 的组合,经验结论很明确:alpha = 0.25、gamma = 2 在多数任务里是最稳的组合。当 gamma 固定为 0(也就是退化成带 alpha 的交叉熵)时,最优 alpha 会在 0.75 附近;当 gamma 增大到 2,最优 alpha 反而落到 0.25 附近。这两者之间存在明显的耦合关系。

设置调参观察适用场景
gamma = 0等价于交叉熵 + alpha 平衡类别不平衡不严重的小模型
gamma = 2, alpha = 0.25论文默认组合,全面均衡大多数检测任务起点
gamma > 2容易样本贡献极低,早期收敛慢简单样本占比极大、噪声标注少
alpha < 0.25对正样本总权重的压制更明显正样本很多、难负样本也多的场景

我不知道别人怎么调,我自己复现时第一轮直接用了默认参数,训练曲线很快就进入合理区间,后续需要改的往往不是这两个数,而是数据增强和学习率。

4.3 实现里常见的三个 Bug

第一个是 alpha 放反。很多实现里 alpha_t 的公式写成alpha * target + (1 - alpha) * (1 - target),但如果你把 alpha 反过来定义成"负样本权重",整个训练方向就偏了。这个 bug 很隐蔽,因为训练 loss 还是会下降,最后的 AP 却低一个多点。

第二个是标签维度。RetinaNet 分类头输出的是 [N, K] 的 logits,其中 K 是类别数,不包含背景类。每张特征图上的锚框如果和真实框匹配不上,分类标签就是全 0 向量。有的实现为了方便,把背景也当成一个类输出 [N, K+1],再用 softmax,结果和 sigmoid focal loss 的语义对不上,训练出来的模型非常不稳定。

第三个是调制因子的计算位置。如果先用binary_cross_entropy_with_logits得到 loss,再乘(1 - p_t)^gamma,功能上没有问题,但要注意数值稳定性,尤其是 p_t 接近 1 时 log 可能溢出。更稳妥的做法是先算 sigmoid 再算 log,或者调用 PyTorch 的log_sum_exp。我见过一个项目因为 log(0) 导致早期 loss 出现 NaN,就是这个原因。

4.4 训练设置与推理后处理的默认值

论文默认用 SGD,初始学习率 0.01,mini-batch 16,动量 0.9,权重衰减 0.0001,总共训练约 90k 步,前 500 步做线性 warmup。这个设置本身不神秘,但它和 Focal Loss 的配合有个微妙的点:warmup 阶段不用太长,因为 bias 初始化已经让早期梯度很温和了,warmup 主要是避免学习率突变。

推理时,模型先对每张特征图的锚框按分类置信度排序,每一层保留排名前 1000 的锚框,五层合并后按类别做 NMS,IoU 阈值 0.5,最后每张图保留最多 100 个检测框。分类阈值默认取 0.05,比较低,因为它后面还有 NMS 兜底。Focal Loss 训练出的模型在低阈值下依然不会输出太多虚警,这也是它相对交叉熵训练出的模型更"干净"的原因之一。

回归分支没有用 Focal Loss,而是沿用 smooth L1。原因很简单:框回归面对的正样本数量本身就少,不存在类别不平衡问题;而且回归误差不具备"置信度"这种天然的分级信号,强行调制只会让回归头对大偏移的样本不敏感,反而不利。

5. 我的调参体会:什么时候改 alpha,什么时候改 gamma

5.1 类别极度不平衡时,先动 alpha 而不是 gamma

我在一个业务项目里遇到过很极端的情况:某个稀有类别在整个训练集里只有几百个实例,而常见类别有几万个。直接用默认的 alpha = 0.25、gamma = 2,稀有类别的 AP 几乎起不来。我给每个类别单独分配了一个 alpha 权重,稀有类别调高、常见类别调低,gamma 保持不变,稀有类别的召回率明显上升。

原因是 gamma 解决的是"难易样本"的分配,但一个稀有类别的样本再难,它在全部损失里的绝对数量也太小。alpha 是唯一能直接改变类别总量占比的手段。分开理解这两个参数,比盲目搜参快得多。

但要注意,类别权重也不能调得太大。我试过把某个稀有类别的 alpha 调到 0.9,发现它开始占用过多损失,挤压了其他类别的优化,整体 mAP 反而下降。类别权重本质上是在"总量不变"的预算里做再分配,调成一个方向的代价是其他方向变弱。

5.2 数据噪声大时,gamma 反而要保守一点

另一个反直觉的经验是:当标注噪声比较大时,gamma 不宜开太高。Focal Loss 会把模型注意力集中到那些 low p_t 的样本上,而噪声标注恰恰容易产生 low p_t 的困难样本。gamma = 2 时,这些噪声样本的权重被放大,模型会花大量精力去拟合错误标签。

我在一个标注质量一般的数据集上把 gamma 降到 1.5,AP 回升了约 0.5 个点。噪声大时,模型需要更多"普通样本"来稳住整体方向,过度关注难例并非总是好事。这也是为什么"默认参数"只在默认质量的数据集上成立,实际工程里还是要根据标签质量灵活调整。

5.3 和后来方法的对照:GHM 等改进到底改了什么

读这篇论文的时候,可以顺带看一眼后续的 GHM(梯度协调)思路,理解会更立体。Focal Loss 完全按置信度 p_t 降权,但置信度低并不等于"值得学",某些标注错误或不可学的样本会持续产生小但稳定的梯度。GHM 改成按梯度密度来降权,会同时压掉"非常容易"和"梯度极大到像噪声"两类样本。这个对照能帮你意识到,Focal Loss 的调制因子是一个精心设计但并非完美的近似——它选择了可微、连续的数学形式,牺牲了对噪声的区分能力。

5.4 我的最后一条实操建议

把这个模型用在自己的数据集上,最省力的方式是从论文默认参数出发,先不调任何东西,完整跑一个基准。看训练曲线的时候重点观察正样本的平均 p_t 有没有逐步接近 0.7 以上,负样本的损失有没有被压到接近零。如果这两个现象都有,说明 Focal Loss 正常工作;如果没有,先查实现,再查数据,最后才动 alpha 和 gamma。我见过大量"调参调不上去"的情况,最后都归因于初始化遗漏或者标签维度写错。

过了这一关后,如果 AP 仍然不满意,我的建议顺序是:先调分类阈值和 NMS 参数,再调数据增强,然后调 alpha,最后才调 gamma。Focal Loss 的参数对最终 AP 的影响往往小于数据质量和增强方式,它解决的是训练过程中的梯度分配,不能凭空创造信息。

Focal Loss 和 RetinaNet 这篇论文最值得学的地方,是它把"难例挖掘"这个工程经验变成了一个漂亮、可微、可扩展的数学形式,又用一个足够通用的检测器验证到极致。我后来在不止一个任务里借鉴了"连续调制代替硬筛选"的思路,收益都不错。它的影响之所以持续到今天,是因为它提供了一个真正可以复用的思想方法,而不只是一组调好的数字。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询