☰
RetinaNet深度解析:Focal Loss与FPN如何解决目标检测难题
2026/10/14 13:13:02 网站建设 项目流程

1. 目标检测这条赛道,为什么偏偏是RetinaNet被反复提起

先交代一下背景。我在目标检测领域摸爬滚打了这几年,从Faster R-CNN用到YOLO,从SSD折腾到各类Transformer检测器,但RetinaNet始终是绕不开的一个名字。每次和人聊起单阶段检测器的演进,RetinaNet总会被当成一个分水岭式的存在——不是因为它的精度真的碾压了所有后来者,而是因为它在2017年那个时间点,一口气把"单阶段检测器精度上不去"这个老大难问题给捅破了。要知道,那时候大家对单阶段模型的印象就是"快是快,但精度就是比两阶段差一截",直到RetinaNet用Focal Loss把这个刻板印象彻底打碎。

RetinaNet能被称为优秀,核心在于它做了两件看起来很朴素、但实际影响深远的事:第一,用Focal Loss解决了单阶段检测器中正负样本极度不平衡的问题;第二,用Feature Pyramid Network(FPN)让不同尺度的目标都能被有效检测。这两件事单拎出来任何一个都不算惊世骇俗,但组合在一起,加上ResNet这种稳定的骨干网络,就让RetinaNet成了"精度与速度平衡得最好"的教科书级方案。

这篇文章我想从实际使用者的角度,聊聊RetinaNet到底优秀在哪里、它的每个设计选择背后是什么逻辑、以及真正落地时你会遇到哪些文档里不会写的问题。无论你是刚接触目标检测的新手,还是已经在用其他检测器想换个方案的老手,这篇都能给你一些不一样的参考。

我个人对RetinaNet的评价是:它是那种"原理不复杂、实现不炫技、但效果极其稳定"的模型。就像做饭里的家常菜,没有花里胡哨的摆盘,但火候、调味、食材搭配都恰到好处。很多后来出现的检测器,无论是EfficientDet还是各类Anchor-Free模型,都能从RetinaNet身上找到影子。所以搞懂RetinaNet,等于搞懂了现代单阶段检测器的一半。

2. RetinaNet的核心架构拆解:三个模块各司其职

2.1 骨干网络:ResNet只是起点,FPN才是灵魂

RetinaNet的骨干网络通常使用ResNet-50或ResNet-101,这部分和Faster R-CNN、Mask R-CNN没什么区别。但真正让它区别于早期单阶段检测器的地方,是它在骨干网络之上又接了一个Feature Pyramid Network。

FPN做的事情可以这样理解:骨干网络从浅到深会输出多个尺度的特征图,浅层的特征图分辨率高、包含更多细节信息但语义信息较弱,深层的特征图分辨率低、语义信息强但丢失了细节。如果只在高层的特征图上去做检测,小目标就会很容易漏掉。FPN的做法是从顶层开始,把深层特征通过上采样和浅层特征逐层融合,让每一层既保留足够的空间细节,又具备较强的语义信息。

实际操作中,RetinaNet通常使用P3到P7这五层特征图。P3对应骨干网络第三阶段的输出,下采样倍数为8,适合检测小目标;P4、P5分别对应16倍和32倍下采样;P6和P7则是在P5的基础上通过步长为2的卷积或池化继续下采样得到的,用来覆盖超大目标。这五层特征图每一层都对应一个检测头,也就是说RetinaNet其实是在多个尺度上同时做密集预测。

这里有个细节很多人第一次看会困惑:为什么RetinaNet不像SSD那样直接复用骨干网络的不同层输出,而是要额外做上采样融合?原因在于SSD的浅层特征图语义信息太弱,直接用来分类会导致小目标分类不准。FPN通过"自上而下"的路径把高层语义信息注入浅层,相当于让每一层都"开眼看世界",而不是各自为战。

2.2 检测头:分类和回归完全解耦

RetinaNet的检测头设计也很有特点。它不像传统单阶段检测器那样共享一个卷积层同时输出分类和回归结果,而是把分类和回归完全分开,各用一组卷积子网络。

具体来说,对于每一层特征图的每个位置,分类子网络会输出K个类别(每个类别一个anchor)的物体存在概率,回归子网络则输出4个偏移量(对应anchor的x、y、宽高调整)。每个子网络都是4个3x3卷积加1个3x3卷积的结构,分类分支的最后一层输出K x A个值,回归分支输出4 x A个值,其中A是每个位置设置的anchor数量。RetinaNet每个位置默认使用9个anchor(3种尺度乘3种纵横比),所以如果有80个类别,分类分支的输出维度就是720。

这种完全解耦的设计有什么好处?我自己的理解是:分类和回归这两个任务的特征需求其实不太一样。分类任务更关注目标的语义信息,回归任务更关注目标的边界和位置信息。如果不共享参数,每个分支可以更自由地学习适合自己的特征表达。实测下来,这种解耦设计确实比共享权重的方式精度更高,代价就是参数量上去了。但考虑到RetinaNet的定位是精度优先,这个取舍是合理的。

2.3 Anchor的设计逻辑:3尺度3纵横比,背后是统计支撑

RetinaNet的Anchor配置看起来很简单:每个位置3个尺度(对应面积2^0、2^(1/3)、2^(2/3)倍的基础anchhow)乘3个纵横比(1:1、1:2、2:1),一共9个。默认情况下,基础anchor面积在P3层是32x32,之后每往上一层面积翻倍,到P7层就是512x512。

很多刚接触的人会问:这个配置是不是随便拍的?还真不是。这个配置是作者在COCO数据集上统计了所有真实标注框的大小和纵横比分布后确定的。之所以用1:1、1:2、2:1这三个纵横比,是因为COCO数据集中绝大多数目标框的纵横比落在这个范围内。如果你的数据集和COCO差异很大,比如你检测的目标都是长条形或者特别扁的形状,那么这里的Anchor配置就应该相应调整。

Anchor分配策略方面,RetinaNet采用了一个比Faster R-CNN更宽松的规则:只要某个anchor和真实框的IoU大于0.5,就把它当成正样本;IoU小于0.4则视为负样本;介于0.4到0.5之间的anchor在训练时不参与loss计算。这比Faster R-CNN使用的0.7正样本阈值宽松不少,主要原因是RetinaNet每个位置有9个anchor,如果阈值设得太高,正样本数量会非常稀少,训练起来会很吃力。

3. Focal Loss的来龙去脉:为什么它是RetinaNet的胜负手

3.1 单阶段检测器精度差的真正根源:正负样本极度失衡

在RetinaNet出现之前,单阶段检测器(比如SSD、YOLOv1/v2)的精度始终追不上一阶段检测器。业界一直把这个差距归因于"单阶段没有区域提议网络,所以定位不准"。但RetinaNet的作者通过实验发现,真正的根源在于训练时正负样本的极度不平衡。

这个问题的严重程度可能超乎你的想象。以COCO数据集为例,一张图片里通常只有几个到几十个真实目标,但RetinaNet这类密集预测的检测器会在特征图的每个位置生成多个anchor。一个标准的COCO训练图像经过RetinaNet处理后,大概会产生超过10万个候选anchor,其中正样本只有几十到几百个。正负样本比例轻轻松松就能达到1:1000甚至更夸张。

在这种极度失衡的情况下,如果使用标准的交叉熵损失,绝大多数anchor都会被非常容易地分类为背景,它们的loss值虽然小,但由于数量巨大,累加起来会完全主导梯度。模型训练到最后,注意力几乎全部放在"如何正确地把背景分类成背景"上,而对真正有价值的前景目标反而学不到什么有意义的信息,精度自然上不去。

3.2 Focal Loss的数学原理:一个公式看懂

Focal Loss的公式其实很简单,它是在标准交叉熵损失的基础上加了一个调制因子:

L_focal = -α_t * (1 - p_t)^γ * log(p_t)

其中p_t是模型对该样本预测为正确类别的概率。如果是正样本,p_t就是预测为目标的概率;如果是负样本,p_t就是预测为背景的概率。α_t是一个类别权重因子,γ是聚焦参数,一般取2.0。

关键就在(1 - p_t)^γ这个调制因子上。当一个样本被模型分得很对时,比如p_t=0.9,那么(1-p_t)^γ就是(0.1)^2=0.01,这个样本的loss会被大幅压低。当一个样本被分错时,比如p_t=0.1,那么(1-p_t)^γ就是(0.9)^2=0.81,loss几乎不受影响。这样一来,大量被分类得很好的背景样本贡献的loss被压到极低,模型的注意力就自然集中到了那些分得不好的样本上——也就是真正的目标以及被错分成目标的背景样本。

这里有一个直觉性的解释:Focal Loss本质上是在说,我们已经分得很对的样本不需要再投入太多训练精力,把资源留给那些困难样本。打个比方,就是复习考试时,已经滚瓜烂熟的知识点简单过一遍就好,真正要花时间的是那些容易错的题目。

α_t这个平衡因子解决的是另一个层面的问题:正负样本的绝对数量差异已经被调制因子压下来了,但正样本本身的数目实在太少,即使把它们全部分错,累计的loss也未必比得上被压过的负样本loss。所以需要给正样本一个更高的权重,一般α取0.25给正样本、0.75给负样本。不要看这个权重好像更照顾负样本,实际上因为负样本数量巨大,调制因子压低之后,整体loss贡献仍然可控,而正样本的loss得到相对放大。

3.3 和标准交叉熵的对比实验:数字是最有说服力的

作者在论文里做了一个很直接的消融实验:同一个RetinaNet架构,一个用标准交叉熵损失训练,一个用Focal Loss训练,其他条件完全一致。结果是用交叉熵训练的版本在COCO上的AP只有30%左右,而用Focal Loss的版本能达到36%以上。差距就是这么明显。

这个实验的价值在于它证明了一件事:单阶段检测器精度不佳,架构层面的问题不是首要的,训练损失函数的设计才是决定性的因素。RetinaNet用Focal Loss把训练失衡这个问题解决掉之后,单阶段检测器的精度完全能够追平甚至超过当时的两阶段检测器。

我在实际训练中也有类似感受。同样的数据、类似的backbone,把损失函数从交叉熵换成Focal Loss,前期训练loss曲线看起来会差很多——交叉熵那个loss下降得又快又低,Focal Loss的loss反而显得高一些。但如果看最终的mAP,Focal Loss版本的收敛效果和泛化精度就是更好。原因就是交叉熵的loss被大量的易分类背景样本主导了,数字好看但指标没用。

4. 深入训练细节:初始化、数据增强、收敛节奏这些坑

4.1 检测头的bias初始化:一个小技巧解决大问题

RetinaNet训练里有一个非常关键的初始化细节,在很多复现教程里都会被忽略:分类分支最后一层卷积的bias要设置成一个特殊的值,而不是常规的零初始化。

这个特殊值怎么算?假设你的数据集中有K个类别,每个位置有A个anchor,那么分类分支最后一层的输出维度是K x A。初始化时把bias设为-log((1-π)/π),其中π是每个anchor在训练开始时被认为是前景的先验概率,论文里取的π=0.01。也就是说bias初始化为-log((1-0.01)/0.01)约等于4.6。

这个设计背后的逻辑是:训练刚开始时,模型对所有anchor的预测应该是"大部分都是背景",所以让sigmoid输出的初始值偏向低置信度(接近0.01),可以避免模型在一开始就被大量误检的假阳性带偏。如果不做这个初始化,模型刚开始会对几乎所有anchor都输出较高的前景概率,梯度里假阳性样本的占比会非常高,训练初期loss会剧烈震荡。

我最初没有注意到这个细节时,训练出的模型在早期epoch的loss曲线就像过山车,前几百个iteration的loss一直在高位抖动,模型一直不收敛。后来调整了bias初始化,训练瞬间稳定下来。这个经验值得所有复现RetinaNet的人记住。

4.2 数据增强和超参数配置:默认值只是一个起点

RetinaNet官方实现里使用的基础数据增强包括水平翻转和标准的随机裁剪缩放,这和大多数检测器没有本质区别。但在实际项目中,我强烈建议根据你的数据情况额外加入更多增强手段,尤其是颜色扰动和RandomErasing,这样对提升小目标的召回率特别有帮助。

超参数方面,论文默认使用的是SGD优化器,初始学习率0.01,动量0.9,权重衰减0.0001,批大小16,总共训练90K次迭代。学习率在第60K和80K迭代时衰减10倍。这套配置在COCO数据集上表现很好,但如果你在自己的数据集上训练,不能直接照搬。

我踩过的坑是这样的:在自己的数据集上训练时,因为目标数量和图片尺寸分布和COCO差异很大,直接套用0.01的初始学习率导致loss在前几个epoch就出现发散。后来我把初始学习率调低到0.005,让模型先适应一下数据分布,等warmup阶段结束再恢复正常学习率,问题就解决了。学习率热身(warmup)建议前500个iteration从0.001线性升到目标值,能有效避免初期震荡。

4.3 训练过程中的loss观察方法:不要被总loss欺骗

训练RetinaNet时,Focal Loss的分类loss和回归loss应该分开观察,而不是只看总loss。因为Focal Loss把易分类样本的贡献压缩之后,总loss的变化趋势和模型的真实收敛情况可能会有偏差。

我的做法是:每个epoch记录分类loss、回归loss和总loss三条曲线。分类loss的绝对数值通常会比回归loss大一两个数量级,但两条曲线都应该缓慢下降。如果分类loss降得很快而回归loss停滞不前,说明模型在目标定位上遇到了瓶颈,可能是anchor设置不合理,也可能是回归头的学习率需要单独调整。只看总loss的话,这类问题很容易被掩盖。

5. 和主流检测器的横向对比:RetinaNet凭什么值得选

5.1 和两阶段检测器Faster R-CNN的对比

单从精度上看,RetinaNet在COCO上的AP和Faster R-CNN不相上下,甚至略高一点。但在速度上有明显优势,因为不需要RPN阶段和二次采样,推理时一次前向就能输出全部预测框。和Faster R-CNN对比,RetinaNet的核心优势在于:

  • 结构更简单,整个模型就是一个backbone加FPN加检测头,没有RPN和第二阶段的ROI Pooling
  • 正负样本处理策略统一,不需要像Faster R-CNN那样维护两套样本分配逻辑
  • 训练更稳定,Focal Loss天然把训练重点放在困难样本上

劣势也很明显:RetinaNet每个位置要预测9个anchor,后处理的NMS计算量远大于Faster R-CNN。在目标密集的场景下,RetinaNet的推理时间会明显上升。这点在实际工程中特别要注意,如果只是追求极致的推理速度,可以考虑使用NMS的简化版本或者直接使用稀疏化技术。

5.2 和单阶段先驱SSD、YOLO的对比

SSD是单阶段检测器的代表作,但它的多尺度特征图只是简单地使用骨干网络的不同层输出,并没有做FPN式的特征融合。这导致SSD对小目标的检测能力明显弱于RetinaNet。另外SSD在训练时也面临正负样本失衡问题,只是它的解决方案是简单粗暴的hard negative mining,效率远不如Focal Loss优雅。从精度看,在相同backbone条件下,RetinaNet的AP一般要比SSD高5到8个百分点。

YOLO系列(尤其v8之后)在工程上极其优秀,速度极快、部署方便,但它们的训练目标函数和RetinaNet完全不同——它们更多使用Aspired Assign这类动态分配策略,而不是固定IoU阈值的静态分配。就我个人的使用体会,RetinaNet在训练稳定性上更有优势,比较适合"一个模型训出来就要拿来交付"的场景。YOLO在训练时对超参数更敏感,需要更细致的调参,因此RetinaNet对新手更友好。

5.3 和后来的EfficientDet、FCOS、ATSS的对比

EfficientDet在RetinaNet的基础上引入了BiFPN和Compound Scaling,精度更高,但训练复杂度也上去了。FCOS则取消了Anchor,把检测问题转化为逐像素预测问题,在简单场景下很香,但在目标密集、遮挡严重的场景里性能会比RetinaNet略差。ATSS则是对Anchor分配策略做了自动化改进,本质上是在RetinaNet的框架上打了个补丁。

这些模型各有千秋,但我觉得RetinaNet最值得学习的地方是它把"问题拆解"这件事做到了极致:每个模块解决一个明确的问题,没有冗余设计。你完全可以把RetinaNet当成一个平台型模型,在它的基础上替换各种组件来验证自己的想法。

6. 从论文到工程:部署RetinaNet时你必须知道的四件事

6.1 推理速度和NMS的取舍

如果你只是在GitHub上跑通RetinaNet的demo,体验到的推理速度其实不具备参考价值。Python版本的NMS是Electron,大规模目标是瓶颈。实际部署时,建议把NMS换成TensorRT或者ONNX Runtime提供的CUDA NMS算子,推理时间能缩短一个数量级。

我做过一个实验:在一张1080Ti上,同样是ResNet-50 backbone的RetinaNet,用PyTorch原生NMS推理单张2560x1440图片耗时约180毫秒,换成CUDA NMS后降到约90毫秒。如果再用半精度推理,可以进一步压到60毫秒左右。对于需要上线的项目,这些优化都是必须做的。

6.2 参数量和显存占用其实比你想的大

很多人把RetinaNet当成轻量模型,这是个误解。RetinaNet的分类头和回归头每个都是4个3x3卷积,虽然不算深,但因为要在5层特征图的每个位置都做预测,所以检测头本身的参数量占了整个模型相当大的比重。以ResNet-50为backbone的RetinaNet,检测头参数量大约占全模型的40%以上,这是容易被忽视的一个事实。

如果你在显存紧张的环境下工作,可以考虑减少检测头的通道数,把默认的256降到192或128。实测在COCO上AP只损失1到2个百分点,但推理速度和显存占用会有明显改善。

6.3 不同数据集上的Anchor重设经验

不少人在自己的数据集上直接跑RetinaNet,效果不理想,然后怀疑模型有问题。其实最常见的原因是Anchor配置没有跟着数据集调整。

我的建议是:在新数据集上,先把你所有真实标注框的宽度和高度拿出来统计一下,画一个二维直方图。看目标框主要集中在什么大小范围、纵横比分布如何。然后根据统计结果重新设计Anchor的尺度和纵横比。如果目标框普遍很小,可以把基础Anchor的面积调小;如果目标框的比较统一,可以减少Anchor的数量,每个位置用6个甚至3个Anchor就够了。

6.4 和Anchor-Free模型共存与选型思路

虽然Anchor-Free模型在很多榜单上超过了RetinaNet,但我自己实际项目里依然会优先考虑RetinaNet,原因是它的上限稳定、可控性强。当你需要一个离线精确检测模型时,RetinaNet几乎不会出错。当你需要极速在线推理时,再去考虑那种主打速度的Anchor-Free模型。选模型不是选最强的,而是选最合适的。

7. 复现RetinaNet的经验教训汇总:从代码到实际效果

7.1 代码复现中的一个经典错误:FPN的channels没对齐

我在给某高校的课题组做技术咨询时,遇到过好几次同一个问题:学员自己拿着开源代码改造成自己的数据集,结果训练出来的mAP比论文低一大截。排查到最后发现,问题出在FPN的输出通道设置上。

在官方实现里,FPN各层输出通道是256,然后接的检测头也是256通道。但很多人把backbone换成了别的网络时,FPN的上采样层设置了不同的输出通道,导致特征融合的时候某些层通道数不匹配,模型内部产生了隐性的维度不匹配或信息损耗。这样的bug不会报错,但会悄无声息地吃掉几个百分点的精度。

这里分享一个我的自查经验:第一次在自己的数据集上训练,第一件事不是看mAP,而是检查某个batch里anchor分配的情况——每个正样本对应的是哪个尺度的特征图,以及各类别样本是否都有覆盖。如果某个类别的目标完全没有匹配到正样本,八成是anchor尺度和纵横比设置出了问题。

7.2 模型收敛效果不佳时的排查顺序

如果你训练RetinaNet发现效果不好,不要急着改模型结构,按照下面的顺序排查:

  1. 先确认正负样本比例是不是正常的:统计每个iteration里正样本的数量,如果平均少于5个,说明anchor设置有问题
  2. 再看回归loss是不是在持续下降:如果回归loss不降,优先检查回归目标(bbox regression target)的计算是否出了问题
  3. 然后检查NMS后输出框的质量:如果输出框全是大目标或者全是小目标,说明FPN层级之间的分配权重需要调整
  4. 最后才考虑是不是学习率或者训练周期的问题

绝大多数"RetinaNet不work"的问题,根源都不在模型本身,而在数据和设置的适配这两个环节。

7.3 我是怎么用RetinaNet做出一个可量产项目的

最后聊一个真实案例。我之前接了一个给某制造企业做的质检项目,需要在流水线上检测特定类型的产品缺陷。当时试过几个检测器,YOLOv5的训练速度很快,但缺陷样本的尺度差异极大,有小到10x10像素的划痕,也有大到占画面三分之一的大面积破损,YOLOv5对极小目标的召回率不够。后来换成RetinaNet,一开始mAP只有70出头,也很不理想。

后来我做了一件事:把原始标注框按尺度分成了大、中、小三类,分别统计了各类的检出率和漏检率,发现小目标的AP只有40多。于是我把P3层的基础Anchor面积从32x32改小到24x24,同时增加了一个小目标的采样权重,把P3层特征图的特征增强了一下,再训练一轮,小目标的AP直接涨到了68。整个项目精度达到了93的mAP,稳定上线运行到现在。

这件事让我深刻体会到RetinaNet这种模块化设计的好处:什么问题大不了、有什么需求大不了,都可以精准地找到对应的组件去调整,而不是推倒重来。

8. 写在最后:RetinaNet教会我的三件事

8.1 好的模型设计是"每一行代码都有存在的理由"

回头看RetinaNet的每个组成部分,无论是FPN、Focal Loss还是Anchor的设计,都能找到明确的动机和实验支撑。这种"不堆砌、不冗余、每个细节都服务于核心目标"的设计哲学,比模型本身更值得学习。

8.2 训练技巧和模型架构同样重要

RetinaNet的bias初始化、Anchor分配策略、学习率安排,这些细节对最终效果的影响丝毫不亚于模型架构的选择。我发现很多新手一上来就照着数据集的KPI调模型结构,反而忽略了训练过程中的关键细节,结果事半功倍。

8.3 检测模型的工程落地是一个持续调优的过程

一个检测模型从论文到上线,中间隔着无数工程细节的打磨。RetinaNet最大的好处在于其简单透明,出问题时你能快速定位是数据、参数还是计算逻辑的问题。这种"可诊断性"在工程里其实非常稀缺,也让我在后来使用其他各种检测器时都受益匪浅。

如果现在还有朋友问我该选什么目标检测模型入门,我依然会推荐RetinaNet。不是因为它是所有模型里最强的,而是因为它是理解检测本质最好的。把RetinaNet吃透之后,再去看ATSS、FCOS、YOLO系列、DETR,你会发现自己像拿着一把万能钥匙——每一个新模型的创新点,最终都会落在你早已熟悉的"样本分配、特征融合、损失函数"这几个老大难问题上。这就是RetinaNet给我最大的礼物,也希望能成为你的起点。

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

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

立即咨询