RetinaNet: Focal Loss for Dense Object Detection

论文: Focal Loss for Dense Object Detection (ICCV 2017, best paper)
作者: Tsung-Yi Lin, Priya Goyal, Ross Girshick, Kaiming He, Piotr Dollár (FAIR)
代码: facebookresearch/Detectron

一句话总结:这篇论文找到了”一阶段检测器精度不如两阶段”的根因–类别极度不平衡导致训练被大量易分负样本主导–并用 Focal Loss 一举解决,使 RetinaNet 成为第一个精度超越两阶段的一阶段检测器。

⚠️ 论文的主角其实是 Focal Loss,RetinaNet 网络本身只是”顺便”提出、用来验证损失函数有效性的载体–结构上并无颠覆性创新(ResNet + FPN + anchor 双头都是已有组件)。


一、背景与动机:一阶段为什么打不过两阶段?

1
2
3
4
5
两阶段 (Faster R-CNN 等):  RPN 筛选 -> ~1-2k 候选框 -> 第二阶段精修
✅ 精度高 ❌ 慢

一阶段 (YOLO / SSD 等): 密集预测 -> 一次出 ~10^4~10^5 个候选框
✅ 快 ❌ 精度差 (mAP 落后两阶段 10~20%)

当时的困惑:一阶段能直接回归出框、速度又快,为什么精度就是追不上两阶段?本文给出不同答案–不是结构不行,是训练被易分负样本主导

根因:极端的类别不平衡

候选框数量 正负样本比 问题
两阶段 ~1-2k(RPN 已过滤) 较均衡 训练目标清晰
一阶段 10^410^5(密集 anchor) ~1:1000 负样本远多,且大量是”一眼就能认出的背景”
  • 两阶段的 RPN 把候选框压到千级,第二阶段还做启发式采样(如前景:背景=1:3)或 OHEM,正负比例可控;
  • 一阶段对每个位置都预测,~100k 个位置中正样本寥寥无几,且大部分负样本是易分背景(纯天空、空地)。

易分负样本如何”淹没”训练

标准交叉熵对每个样本一视同仁:即使是 $p=0.9$ 的易分样本,仍贡献 $-\log 0.9 \approx 0.105$ 的损失。当易分负样本数量是正样本的 1000 倍时:

1
2
3
总损失 = Σ(少量正样本损失) + Σ(海量易分负样本损失)
↑↑↑
这一项主导了梯度方向, 真正难样本被淹没

这导致两个问题:训练效率低下(易分样本不提供有用学习信号)+ 模型退化(易分负样本压倒训练)。

之前的缓解手段是 OHEM(Hard Negative Mining):按 loss 排序只取最难的负样本。但它粗暴丢弃了所有易分样本,且需额外选择逻辑。本文提出更优雅的 Focal Loss


二、核心创新:Focal Loss 🔥

2.1 从交叉熵说起

二分类中,定义 $p_t$ 为”模型对真实类别的预测概率”:

$$
p_t = \begin{cases} p & \text{if } y = 1 \ 1-p & \text{if } y = 0 \end{cases}
$$

标准交叉熵:$CE(p_t) = -\log(p_t)$

问题:$p_t \to 1$(易分样本)时 $CE$ 仍不为 0,且易分样本数量巨大,累加后主导总损失。下图中蓝色 CE 曲线变化最平缓–即使 $p_t$ 已很大,损失仍偏高:

2.2 Focal Loss 公式

在交叉熵前加一个调制因子 $(1-p_t)^\gamma$:

$$
\boxed{;FL(p_t) = -(1-p_t)^\gamma \log(p_t);}
$$

调制因子的作用:

1
2
p_t -> 1  (易分样本):  (1-p_t)^γ -> 0  -> 损失被大幅压低 (几乎不算它了)
p_t 小 (难分样本): (1-p_t)^γ -> 1 -> 损失几乎不变 (重点学它!)

直觉:已经分对的样本就少管它,把训练”焦点”(focal) 集中到难分样本上。 这与 Huber 等鲁棒损失的思路相反–鲁棒损失是降低大错误样本的权重,而 Focal Loss 是降低易分(小错误)样本的权重。

2.3 γ 的作用(focusing parameter)

$\gamma \ge 0$ 控制下压的力度。下表对比 $\gamma=0$(即标准 CE)与 $\gamma=2$ 时的单样本损失:

$p_t$(预测置信度) $CE = -\log p_t$ $(1-p_t)^2$ $FL_{\gamma=2}$ 变化
0.9(易分) 0.105 0.01 0.00105 ↓ ~100 倍
0.5(临界) 0.693 0.25 0.173 ↓ ~4 倍
0.1(难分) 2.303 0.81 1.866 ↓ 仅 0.8 倍
  • $\gamma=0$:退化为标准 CE;
  • $\gamma$ 越大,对易分样本压得越狠,但过大会降低整体学习信号;
  • 直观地说,调制因子减少了简单样本的损失贡献,并扩大了样本获得低损失的范围:$\gamma=2$ 时 $p_t=0.9$ 的样本损失降 100 倍,而 $p_t=0.5$ 的难样本仅降 4 倍;
  • 论文最优 $\gamma = 2$

2.4 α 平衡因子

类别不平衡的另一维度是正负数量悬殊,可再加权重 $\alpha$:

$$
FL(p_t) = -\alpha_t (1-p_t)^\gamma \log(p_t)
$$

  • $\alpha$ 对前景/背景加权,典型 $\alpha = 0.25$(前景);
  • ⚠️ $\alpha$ 与 $\gamma$ 会相互影响:加入 $\gamma$ 后最优 $\alpha$ 从 0.5 降到 0.25。两者需联合调参,不能各调各的。论文给出 $\gamma=2,\ \alpha=0.25$ 时 ResNet-101+FPN 最优。
  • 注意:前景权重小(0.25)、背景权重大(0.75)看似反直觉,但因为背景数量远多于前景,这样能压低负样本的总损失贡献。

2.5 为什么 Focal Loss 优于 OHEM

OHEM(Hard Negative Mining) Focal Loss
做法 按 loss 排序,只取 top-K 最难负样本 连续加权,所有样本都参与
易分样本 完全丢弃 软性下压(仍保留少量信号)
是否可微 需额外选择逻辑,非端到端 完全可微,端到端
粒度 硬阈值(一刀切) 连续(按置信度平滑过渡)

Focal Loss 是 OHEM 思想的”软化、可微”版本:不粗暴丢弃易分样本,而是让它们的贡献随置信度自动衰减。

2.6 难易正负样本的关注排序

Focal Loss 让训练对不同样本的关注度自然形成排序:

1
正难 > 负难 > 正易 > 负易

即:难分的正样本最受关注,其次是难分负样本,易分样本权重最低(其中易分负样本因数量最大、被压得最狠)。

📌 后续反思(GHM):本文”易分样本对模型提升很小”的假设其实有争议–GHM(Generalized Focal Loss 的相关工作)指出,数量极多的”极易分样本”和”极难分样本”(离群点)都应被抑制,这是对 Focal Loss 的改进方向之一。


三、网络架构

3.1 总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入图像

┌─────────────────────────────────────────┐
│ Backbone: ResNet-50 / ResNet-101 │ (ImageNet 预训练)
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ Neck: FPN 特征金字塔 │
│ 生成 P3, P4, P5, P6, P7 │ (stride = 8,16,32,64,128)
└─────────────────────────────────────────┘
↓↓↓↓↓ (每个金字塔层各接两个并行子网)
┌──────────────────┬──────────────────┐
│ 分类子网 (cls) │ 回归子网 (box) │
│ 4×(3×3 conv) │ 4×(3×3 conv) │
│ + 3×3 conv │ + 3×3 conv │
│ -> K×A 通道 │ -> 4×A 通道 │
│ (类别概率) │ (框偏移) │
└──────────────────┴──────────────────┘

RetinaNet = ResNet + FPN + 两个解耦的预测头。结构本身并不新奇,论文真正的贡献是 Focal Loss 让这套密集预测结构第一次训得准

详细的网络结构(含 P3-P7 各层 scale 与 ratio)如下:

3.2 Backbone:ResNet

  • 使用 ResNet-50 或 ResNet-101,ImageNet 预训练初始化;
  • 取 $C_3, C_4, C_5$ 三个 stage 的输出送入 FPN(不用 $C_2$,因为生成 $P_2$ 占用计算资源过多);
  • ResNet 的残差结构解决了深层网络的梯度消失/爆炸。两种残差块如下:

左图为 basic block(用于 ResNet-18/34),右图为 bottleneck block(用于 ResNet-50/101/152)。RetinaNet 用 ResNet-50,配置 [3,4,6,3] 为各 stage 残差块数量。

工程细节:检测网络较大时,Backbone 前面部分通常冻结不训练,BN 也不更新参数(OpenMMLab 经验)。

3.3 Neck:FPN 特征金字塔

  • 自下而上 + 自上而下 + 横向连接,构建多尺度特征金字塔;
  • 输入 $C_3, C_4, C_5$,输出 $P_3$-$P_7$ 共 5 级,通道数均为 256,stride = (8,16,32,64,128);
  • $P_6$ 由 $C_5$ 经 stride=2 的 3×3 卷积得到;$P_7$ 由 $P_6$ 经 ReLU + stride=2 的 3×3 卷积得到(PyTorch 官方实现;原论文 $P_6$ 用 maxpool 下采样);
  • 大 stride(小特征图)检测大物体,小 stride(大特征图)检测小物体;$P_6, P_7$ 提供大感受野强语义特征,利于大/超大物体;
  • ⚠️ RetinaNet 的 FPN 只含卷积,不含 BN 和 ReLU(除了生成 $P_7$ 时的那个 ReLU)。

FPN 代码(PyTorch,来自社区实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class PyramidFeatures(nn.Module):
def __init__(self, C3_size, C4_size, C5_size, feature_size=256):
super().__init__()
# C5 -> P5, 并上采样与 C4 融合 -> P4, 再上采样与 C3 融合 -> P3
self.P5_1 = nn.Conv2d(C5_size, feature_size, 1, 1, 0)
self.P5_upsampled = nn.Upsample(scale_factor=2, mode='nearest')
self.P5_2 = nn.Conv2d(feature_size, feature_size, 3, 1, 1)
self.P4_1 = nn.Conv2d(C4_size, feature_size, 1, 1, 0)
self.P4_2 = nn.Conv2d(feature_size, feature_size, 3, 1, 1)
self.P3_1 = nn.Conv2d(C3_size, feature_size, 1, 1, 0)
self.P3_2 = nn.Conv2d(feature_size, feature_size, 3, 1, 1)
# P6: 3x3 stride-2 conv on C5; P7: ReLU + 3x3 stride-2 conv on P6
self.P6 = nn.Conv2d(C5_size, feature_size, 3, 2, 1)
self.P7_1 = nn.ReLU()
self.P7_2 = nn.Conv2d(feature_size, feature_size, 3, 2, 1)

def forward(self, inputs):
C3, C4, C5 = inputs
P5_x = self.P5_1(C5)
P5_up = self.P5_upsampled(P5_x)
P5_x = self.P5_2(P5_x)
P4_x = self.P4_1(C4) + P5_up # add 融合 (element-wise)
P4_up = self.P4_upsampled(P4_x)
P4_x = self.P4_2(P4_x)
P3_x = self.P3_1(C3) + P4_up
P3_x = self.P3_2(P3_x)
P6_x = self.P6(C5)
P7_x = self.P7_2(self.P7_1(P6_x))
return [P3_x, P4_x, P5_x, P6_x, P7_x]

注意:FPN 融合用 add(逐元素相加),而非 YOLOv3 的 concat。FPN 输出特征图大小由 Backbone 决定(stride 列表由 Backbone 确定)。

3.4 Head:分类与回归子网(解耦)

RetinaNet 对 5 张特征图各接两个并行、参数独立的子网

  • 分类子网:4×(3×3 conv + ReLU) → 3×3 conv → K×A 通道 → sigmoid(多标签,每类独立);
  • 回归子网:4×(3×3 conv + ReLU) → 3×3 conv → 4×A 通道(预测 4 个偏移);
  • 两个子网结构相同但参数独立(实验发现共享参数会降精度 ~0.9 mAP);
  • 同一子网在 5 个金字塔层间共享参数
  • $K$ 为类别数(不含背景,COCO 为 80),$A=9$ 为每位置 anchor 数;
  • 回归是类别不可知的(class-agnostic):所有类共享同一个框回归器,输出 4×A 而非 4×K×A

Head 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RegressionModel(nn.Module):           # 框回归子网
def __init__(self, num_features_in, num_anchors=9, feature_size=256):
super().__init__()
self.conv1..conv4 = 4×(Conv2d(in,256,3,1,1) + ReLU)
self.output = nn.Conv2d(256, num_anchors * 4, 3, 1, 1) # 4×A
# forward: 长宽不变, 通道 -> 4*num_anchors, reshape 为 (B, anchors, 4)

class ClassificationModel(nn.Module): # 分类子网
def __init__(self, num_features_in, num_anchors=9, num_classes=80,
prior=0.01, feature_size=256):
super().__init__()
self.conv1..conv4 = 4×(Conv2d(in,256,3,1,1) + ReLU)
self.output = nn.Conv2d(256, num_anchors * num_classes, 3, 1, 1) # K×A
self.output_act = nn.Sigmoid() # 多标签
# forward: 通道 -> K*A, reshape 为 (B, anchors, num_classes)

3.5 Anchor 设计

每个金字塔层、每个位置预设 9 个 anchor = 3 尺度 × 3 宽高比

1
2
3
4
5
6
pyramid_levels = [3, 4, 5, 6, 7]
strides = [8, 16, 32, 64, 128] # 各层下采样率
sizes = [32, 64, 128, 256, 512] # 各层 base_size (2^(x+2))
ratios = [0.5, 1, 2] # 宽高比 (1:2, 1:1, 2:1)
scales = [2^0, 2^(1/3), 2^(2/3)] # ≈ 1.0, 1.26, 1.587
# 3 ratios × 3 scales = 9 anchors/位置
  • 小 anchor 配高分辨率层($P_3$,stride 8,base 32,检测小目标);大 anchor 配低分辨率层($P_7$,stride 128,base 512,检测大目标);
  • 最小 scale 约 32(面积 $32^2$),最大接近 813。

特征图 cell 与原图的对应关系:stride=8 的特征图上每个 cell 对应原图 $32\times32$ 区域(人为近似设置,非实际感受野):

一个 cell 的通道维与 anchor 的对应关系(颜色对应 9 个 anchor):

Anchor 平移与铺满特征图generate_anchors 生成 9 个 base anchor,shift 按固定步长平移铺满整张特征图:

Anchor 生成核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def generate_anchors(base_size=16, ratios=None, scales=None):
# 在 base_size 正方形上, 按 ratios 调长宽、按 scales 缩放 -> 9 个 anchor
anchors[:, 2:] = base_size * np.tile(scales, (2, len(ratios))).T
areas = anchors[:, 2] * anchors[:, 3]
anchors[:, 2] = np.sqrt(areas / np.repeat(ratios, len(scales)))
anchors[:, 3] = anchors[:, 2] * np.repeat(ratios, len(scales))
# (x_ctr,y_ctr,w,h) -> (x1,y1,x2,y2)
...

def shift(shape, stride, anchors):
# 把 9 个 base anchor 按每个 cell 中心平移, 铺满特征图 -> (K*9, 4)
shift_x = (np.arange(0, shape[1]) + 0.5) * stride
shift_y = (np.arange(0, shape[0]) + 0.5) * stride
shift_x, shift_y = np.meshgrid(shift_x, shift_y)
...

四、训练

4.1 Anchor 标签分配

对每个 anchor,按与 GT 的最大 IoU 分配:

IoU 范围 标签 参与损失
$\ge 0.5$ 正样本 cls + box
$< 0.4$ 负样本 仅 cls
$[0.4, 0.5)$ 忽略 不参与
  • 每个 anchor 取与其 IoU 最大的 GT 匹配;
  • 此外,每个 GT 至少匹配一个 anchor(保证所有 GT 都被覆盖);
  • 这种”中间忽略”区间避免边界 anchor 歧义(与 YOLOv3 的 ignore 思路类似,见 [[YOLOv3 详解]])。

4.2 BBox 编解码(DeltaXYWHBBoxCoder)

为利用 anchor 信息加速收敛、平衡四个回归值的 loss,对 head 输出的 4 个值做编解码(同 SSD/Faster R-CNN 的 match+encode):

编码(GT -> target):

$$
t_x = \frac{g_x - p_x}{p_w},\quad t_y = \frac{g_y - p_y}{p_h},\quad t_w = \log\frac{g_w}{p_w},\quad t_h = \log\frac{g_h}{p_h}
$$

其中 $g$ 为 GT、$p$ 为 anchor(中心坐标与宽高)。$t_{x,y}$ 是中心相对 anchor 中心的偏移(除以 anchor 宽高归一化),$t_{w,h}$ 是宽高比取 log。

解码(预测 -> 框,推理时用):

$$
g_w = p_w e^{t_w},\quad g_h = p_h e^{t_h},\quad g_x = p_x + p_w t_x,\quad g_y = p_y + p_h t_y
$$

实际还叠了 target_means=[0,0,0,0]target_stds=[1,1,1,1](或代码中的 /[0.1,0.1,0.2,0.2])做标准化。

4.3 损失函数

$$
L = \frac{1}{N_{pos}} \Big( \underbrace{FL_{cls}}{\text{分类(正+负样本)}} + \lambda \cdot \underbrace{SmoothL1{box}}_{\text{回归(仅正样本)}} \Big)
$$

  • 分类损失:Focal Loss,对所有 anchor(正+负)计算–正是 FL 让海量负样本不致淹没训练;
  • 回归损失:Smooth L1,仅对正样本计算;
  • $N_{pos}$:正样本数,用于归一化。

Focal Loss 核心实现(mmdetection 版,最简洁):

1
2
3
4
5
6
7
pred_sigmoid = pred.sigmoid()
target = target.type_as(pred)
# pt = 模型对真实类别的预测概率
pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)
# focal_weight = α_t * (1-pt)^γ
focal_weight = (alpha * target + (1 - alpha) * (1 - target)) * pt.pow(gamma)
loss = F.binary_cross_entropy_with_logits(pred, target, reduction='none') * focal_weight

完整训练损失 forward 流程(社区实现要点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def forward(self, classifications, regressions, anchors, annotations):
alpha, gamma = 0.25, 2.0
for j in range(batch_size):
# 1. 计算所有 anchor 与 GT 的 IoU, 取每个 anchor 的最大 IoU 及对应 GT
IoU = calc_iou(anchors[0], bbox_annotation[:, :4])
IoU_max, IoU_argmax = torch.max(IoU, dim=1)
# 2. 分配标签: IoU<0.4 -> 负(0); IoU>=0.5 -> 正(对应类别 one-hot); 中间 -> 忽略(-1)
targets = torch.ones(classification.shape) * -1
targets[IoU_max < 0.4] = 0
positive_indices = IoU_max >= 0.5
targets[positive_indices, assigned_annotations[positive_indices, 4].long()] = 1
# 3. 分类损失 = α_t * (1-p_t)^γ * BCE (忽略样本置 0)
alpha_factor = torch.where(targets == 1, alpha, 1 - alpha)
focal_weight = alpha_factor * torch.pow(
torch.where(targets == 1, 1 - classification, classification), gamma)
bce = -(targets * torch.log(classification) +
(1 - targets) * torch.log(1 - classification))
cls_loss = torch.where(targets != -1, focal_weight * bce, 0)
classification_losses.append(cls_loss.sum() / num_positive_anchors)
# 4. 回归损失 = Smooth L1 (仅正样本), target 先编码到输出空间
...

4.4 初始化的关键 trick:先验偏置

直接随机初始化会出问题:初始时网络对几乎所有 anchor 都预测”有物体”,而实际负样本占绝大多数 -> 第一轮 loss 爆炸,训练崩溃。

解法:分类子网最后一层的 bias 用特殊初始化:

$$
b = -\log!\frac{1-\pi}{\pi}, \quad \pi = 0.01
$$

  • 让初始输出前景概率 $\approx 0.01$(很低的”有物体”先验);
  • 使初始 loss 降到合理范围,训练稳定启动;
  • ⚠️ 这个 trick 对 Focal Loss 训练至关重要,论文专门强调。(代码中 ClassificationModelprior=0.01 即此。)

4.5 优化与推理

1
2
3
4
5
6
7
训练: SGD + momentum 0.9, weight decay 1e-4
学习率 0.01 起步, 分段衰减
输入尺度: 短边 800 (训练), 1333 (测试)
训练 ~60-100 epoch

推理: 单次前向 -> 取每层 top-k(默认 1k) 预测
-> 阈值 0.05 过滤 -> 跨层 NMS(0.5)

推理时取 top-k 是为控制 NMS 输入规模。Focal Loss 在推理阶段完全不用,只在训练时生效。


五、实验结果

5.1 主结果:一阶段首次超越两阶段

模型 Backbone mAP (COCO) FPS 类型
YOLOv2 Darknet-19 21.6 67 一阶段
SSD300 VGG-16 25.1 46 一阶段
SSD512 VGG-16 28.8 19 一阶段
Faster R-CNN ResNet-101 34.9 6 两阶段
RetinaNet-50 ResNet-50 36.3 12 一阶段
RetinaNet-101 ResNet-101 40.4 5 一阶段

RetinaNet-101 以 40.4 mAP 首次让一阶段在精度上超越当时最强的两阶段检测器,同时保持可接受速度。

5.2 速度-精度曲线

以精度为纵轴、速度为横轴,RetinaNet 曲线整体位于 SSD/YOLO 系列之上,并穿过了两阶段检测器的精度上界–即不存在”一阶段精度天花板”,之前的差距是 loss 设计问题,不是架构问题。

5.3 Focal Loss 消融

γ 的影响(α 固定 0.25):

γ mAP
0(标准 CE) 崩溃(被负样本主导)
0.5 33.0
1.0 35.4
2.0 36.0
5.0 35.6

α 与 γ 的交互:无 γ 时最优 α≈0.25-0.5;加入 γ=2 后最优 α 仍≈0.25,但单独调 α 的增益变小(因为 γ 已在处理难易样本,α 主要处理数量平衡)。

5.4 架构消融

  • Backbone 越深精度越高(ResNet-50 → 101:+1.7 mAP);
  • 金字塔层数 $P_3$-$P_7$ 比只用 $P_3$-$P_5$ 更好,尤其大目标;
  • 分类/回归头解耦比共享参数高约 0.9 mAP。

六、意义与影响

  1. 诊断了一阶段精度瓶颈的根因:不是架构不行,是 dense 训练下的易分负样本主导问题。这篇诊断本身比 Focal Loss 更有价值。
  2. Focal Loss 成为标准工具:被广泛用于分类不平衡、检测等任务。
  3. 但后续有反思:后来的一些工作(如 GHM、GFL、FreeAnchor,以及 YOLOv5/v8 的实践)发现,有了更好的标签分配(如 OTA、TAL)后,Focal Loss 的必要性下降–这反过来说明,类不平衡问题也可以从”样本匹配”侧解决,而非只能在”loss 加权”侧解决。

📌 有趣的对照:YOLOv3 也试过 Focal Loss,结果 mAP 反而下降约 2 个点(见 [[YOLOv3 详解]] 第十一节)。原因正是 YOLOv3 用了”1 GT = 1 正样本 + ignore_mask”的严格分配,正负比例已不像 RetinaNet 那样极端,Focal Loss 的收益消失反而带来调参负担。这说明 Focal Loss 是针对”密集分配+极端不平衡”场景的解药,不是万能药


七、优缺点

优点 ✅

  1. 诊断精准:首次清晰指出一阶段精度瓶颈是类不平衡/易分负样本
  2. 方案优雅:Focal Loss 一行公式、完全可微、端到端
  3. 首个超越两阶段的一阶段:40.4 mAP,里程碑
  4. 分析扎实:消融实验充分,γ/α/初始化 trick 都有验证
  5. 结构清晰:ResNet + FPN + 双头,成为后续一阶段的范式模板

局限 ❌

  1. 仍不够快:5 FPS(ResNet-101)远不及 YOLO 的实时水准,工程实时部署仍首选 YOLO
  2. 超参敏感:γ、α、π 需联合调,换数据集/任务常需重调
  3. Focal Loss 非万能:在更好的标签分配策略下收益有限(甚至如 YOLOv3 般负作用)
  4. 后处理仍依赖 NMS:未摆脱 anchor + NMS 范式(直到 DETR 才真正去除)
  5. 训练 trick 依赖:bias 初始化等 trick 缺一不可,复现门槛较高

总结

RetinaNet 的核心贡献不是网络结构,而是 Focal Loss 这一损失函数以及它背后**”一阶段精度差是因为易分负样本淹没训练”的诊断**。它一举弥合了一阶段与两阶段的精度鸿沟,是目标检测史上的关键节点;同时也提醒我们:损失设计与样本分配策略往往是”同一问题的两面”,后续检测器多在这两条线上继续演进。


八、相关链接

  • 📝 [[YOLOv3 详解]] - YOLOv3 性能表中含 RetinaNet 对比;且 YOLOv3 试 Focal Loss 失败的对照很有启发
  • 📝 [[YOLO 系列总览]] - 一阶段检测器谱系
  • 📝 [[YOLO 核心概念]] - Anchor / IoU / NMS / FPN 共享概念
  • 📋 [[10.clippings/感知算法/2D检测/单阶段/RetinaNet/RetinaNet 论文和代码详解]] - 知乎 (周威, 含 PyTorch 代码)
  • 📋 [[10.clippings/感知算法/2D检测/单阶段/RetinaNet/一阶段目标检测器-RetinaNet 详解]] - 知乎 (真真锋, 含 BBox 编解码)
  • 📋 [[10.clippings/感知算法/2D检测/单阶段/RetinaNet/深度学习之目标检测(五)– RetinaNet网络结构详解-CSDN博客]] - CSDN (霹雳吧啦Wz 视频笔记)
  • 📋 论文原文: arxiv.org/abs/1708.02002
  • 📋 官方代码: facebookresearch/Detectron