# RetinaNet

RetinaNet 是 FAIR 出品，目标检测领域意义重大的精品算法，其犀利的问题解决思路和简洁优雅的网络设计，深深的影响了整个目标检测算法的发展方向，到目前为止依然是 one-stage 主流算法。其核心创新点可以简单归纳为：

1. 深入分析了何种原因导致 one-stage 检测器精度低于 two-stage 检测器
2. 针对上述问题，提出了一种简单但是极其实用的 Focal Loss 焦点损失函数，并且 focal 思想可以推广到其他领域
3. 针对目标检测特定问题，专门设计了一个 RetinaNet 网络，结合 Focal Loss 使得 one-stage 检测器在精度上能够达到乃至超过 two-stage 检测器，在速度上和一阶段相同

## 1 算法分析

### 1.1 one-stage 性能不如 two-stage 原因分析

目标检测算法一般分为 two-stage 和 one-stage，通常 two-stage 检测器精度较高、速度较慢，而 one-stage 检测器速度较快、精度较低。作者试图分析何种原因导致 one-stage 检测器精度低于 two-stage 检测器算法？通过实验分析发现原因是：**极度不平衡的正负（前景背景）样本比例**。对于 one-stage 检测器，例如 YOLO、SSD 等，anchor 近似于滑动窗口的方式铺设在原图上，由于每张图片 gt bbox 比较少，会导致正负样本极度不平衡，而且**绝大部分样本都是易学习样本 easy example**(包括 easy positive 和 easy negative，但主要是 easy negative)，这些易学习样本虽然每一个带来的 loss 会比较小，但是因为数量巨大，最终依然主导了 loss，导致最后训练出来的是一个比较差的模型，也就是**梯度被易学习样本主导，且基本上都是易学习背景样本**。

对于 Faster R-CNN 这类 two stage 模型，第一阶段的 RPN 可以过滤掉很大一部分负样本，最终第二阶段的检测模块只需要处理少量的候选框，这些后续框不是随机产生的，而是高质量样本，而且检测模块还采用正负样本固定比例抽样（比如 1:3）或者 OHEM 方法（online hard example mining）来进一步解决正负样本不平衡问题，所以在 two-stage 算法中正负样本不平衡问题没有 one-stage 那么严重。

### 1.2 focal loss

常用的解决正负样本不平衡办法是 OHEM，它通过对 loss 排序，选出 loss 最大的前 topk 个样本来进行训练，这样就能保证训练的都是难样本。作者指出该方法的缺陷是把所有的 easy example 都去除掉了，造成易学习正样本无法进一步提升训练的精度(易学习正样本其实非常关键，在 pisa 论文中有详细分析)，而且复杂度高影响检测效率。

为此作者提出一个简单且高效的方法：**Focal Loss 焦点损失函数**，用于替代 OHEM，功能是一样的。其解决了两个问题：样本不平衡问题以及突出难样本，需要强调的是：**FL 本质上是将大量易学习样本的 loss 权重降低，突出难学习样本的 loss 权重，但是因为大部分易学习样本都是负样本，所以还有一个附加功能即克服正负样本不平衡问题**。

为了说明 focal loss 思想，需要回顾下 ce loss 和加权 ce loss。

![](http://static.zybuluo.com/huanghaian/b5yi35i8rxjkozng0ty6iopt/image.png)

以二分类问题为例，输出通道是 1，如果 y=1 则表示正样本，y=0 表示负样本，p 是经过 sigmoid 后的输出概率值，可以发现其对正负样本的惩罚是一样的。为了统一成一个公式，可以设置 $$p\_t$$ ：

![](http://static.zybuluo.com/huanghaian/3hp1hqucxw4isizj52oy0pko/image.png)

此时 loss 函数可以写成 $$CE(p,y)=CE(p\_t,y)=-log(p\_t)$$。

为了能够突出正负样本不平衡问题，可以引入加权 ce loss 即：

![](http://static.zybuluo.com/huanghaian/zrbqb74p7cpdtqomqr5lqhud/image.png)

对正负样本设置不同的 $$\alpha$$ （一般正负权重加起来是 1）即可，可以通过交叉验证确定。但是上述损失函数只关注于正负样本类别不平衡问题，而无法关注难易样本不平衡问题，故 focal loss 的定义如下：

![](http://static.zybuluo.com/huanghaian/oiixdo7ps8tiooa0e52sr21v/image.png)

$$\alpha\_t$$和$$\gamma \in \[0,5]$$是超参，并且两者相互影响，作者通过实验设置 $$\gamma=2$$ 、 $$\alpha\_t=0.25$$ 。loss 可视化如下：

![](http://static.zybuluo.com/huanghaian/x557tbdcfq9eugc7vzsiztm9/image.png)

对于任何一个类别的样本，本质上是希望学习出概率为 1，当预测输出接近 1 时候，该样本 loss 权重是很低的，当预测的结果越接近 0，该样本 loss 权重就越高。而且相比于原始的 ce loss，这种差距会进一步拉开。由于大量样本都是属于 well-classified examples，故这部分样本的 loss 全部都需要往下拉，就可以达到聚焦难样本且不抛弃任何可能有用的样本效果。

我们可以自己通过仿真调整参数看下具体效果：

![](http://static.zybuluo.com/huanghaian/eq8eadpz1cs05wcs8qmh3kuj/image.png)

先看$$\alpha$$，随着$$\alpha$$增加，整个梯度会变大，也就是$$\alpha$$属于正负样本的加权参数，值越大，正样本的权重越大。再看 $$\beta$$，其具有 focal 效应，可以控制难易样本权重，值越大，对分类错误样本梯度越大(难样本权重大)，focal 效应越大，这个参数非常关键。

通过上述分析可以发现：focal loss 是根据交叉熵改进而来，本质是 dynamically scaled cross entropy loss，直接按照 loss decay 掉那些 easy example 的权重，这样使训练更加 bias 到更有意义的样本中去，说通俗点就是一个解决**分类问题中类别不平衡、分类难度差异**的一个 loss。

![](http://static.zybuluo.com/huanghaian/a352valf4inzv8yhpo4cxnyo/image.png)

代码实现方面也比较简单：

```python
pred_sigmoid = pred.sigmoid()
# one-hot格式
target = target.type_as(pred)
pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)
focal_weight = (alpha * target + (1 - alpha) *
            (1 - target)) * pt.pow(gamma)
loss = F.binary_cross_entropy_with_logits(
        pred, target, reduction='none') * focal_weight
loss = weight_reduce_loss(loss, weight, reduction, avg_factor)
return loss
```

### 1.3 RetinaNet 网络结构

focal loss 配合 RetinaNet 网络可以实现非常不错的 one-stage 检测性能。RetinaNet 网络是目前主流的 backbone+neck+head 设计模式。

![](http://static.zybuluo.com/huanghaian/k7g5df5xcnn854feqq84zpog/image.png)

**(1) backbone**

backbone 可以选择任意分类模型，默认是采用 resnet，其 4 个特征图，按照特征图从大到小排列，分别是 c2 c3 c4 c5，stride=4,8,16,32，考虑到计算量仅仅用了 c3 c4 c5 3 个输出特征图。

**(2) neck**

neck 模块是标准的 FPN 结构,其作用是特征融合，其细节是：先对这 c3 c4 c5 三层进行 1x1 改变通道，全部输出 256 个通道；然后经过从高层到底层的最近邻 2x 上采样+add 操作进行特征融合，细节见下图，最后对每个层进行 3x3 的卷积，得到 p3,p4,p5 特征图。

![](http://static.zybuluo.com/huanghaian/n4r871ptesvp1j2dwu3kok9y/image.png)

考虑到大物体可能感受野不够，故还需要构建两个额外的输出层 stride=64,128,首先对 c5 进行 3x3 卷积且 stride=2 进行下采样得到 P6，然后对 P6 进行同样的 3x3 卷积且 stride=2，得到 P7，整个 FPN 层都不含 BN 和 Relu。

**(3) head**

作者认为 one-stage 算法中 head 设计比较关键，对最终性能影响较大，相比于其余 one-stage 算法，retinanet 的 head 模块比较大，其输出头包括分类和检测 head 两个分支，且每个分支都包括 4 个卷积层，不进行参数共享，分类 head 输出通道是 num\_class\*K，检测 head 输出通道是 4\*K,K 是 anchor 个数。虽然每个 head 的分类和回归分支权重不共享，但是 5 个输出特征图的 head 是权重共享的。

```python
# x是p3-p7中的某个特征图
cls_feat = x
reg_feat = x
# 4层不共享参数卷积
for cls_conv in self.cls_convs:
     cls_feat = cls_conv(cls_feat)
for reg_conv in self.reg_convs:
     reg_feat = reg_conv(reg_feat)
# 输出特征图
cls_score = self.retina_cls(cls_feat)
bbox_pred = self.retina_reg(reg_feat)
return cls_score, bbox_pred
```

### 1.4 正负样本定义

在介绍正负样本定义前，需要先说明下 anchor 设置规则：

```python
anchor_generator=dict(
            type='AnchorGenerator',
            # 每层特征图的base anchor scale,如果变大，则整体anchor都会放大
            octave_base_scale=4,
            # 每层有3个尺度 2**0 2**(1/3) 2**(2/3)
            scales_per_octave=3,
             # 每层的anchor有3种长宽比 故每一层每个位置有9个anchor
            ratios=[0.5, 1.0, 2.0],
            # 每个特征图层输出stride,故anchor范围是4x8=32,4x128x2**(2/3)=812.7
            strides=[8, 16, 32, 64, 128]),
```

retinanet 采用了密集 anchor 设定规则，每个输出特征图位置都输出 K=9 个 anchor，非常密集。其含义和 faster rcnn 里面完全相同，此处就不赘述了。

retinanet 匹配策略非常简单就是 iou 规则，和 faster rcnn 中的完全相同，仅仅阈值设置不同而已。配置如下：

```python
#双阈值策略
assigner=dict(
     type='MaxIoUAssigner',
     pos_iou_thr=0.5,
     neg_iou_thr=0.4,
     min_pos_iou=0,
     ignore_iof_thr=-1),
```

大意是：

* 初始所有 anchor 都定义为忽略样本
* 遍历所有 anchor,每个 anchor 和所有 gt 的最大 iou 大于 0.5，则该 anchor 是正样本且最大 iou 对应的 gt 是匹配对象
* 遍历所有 anchor,每个 anchor 和所有 gt 的最大 iou 小于 0.4，则该 anchor 是背景
* 遍历所有 gt，每个 gt 和所有 anchor 的最大 iou 大于 0，则该对应的 anchor 也是正样本，负责对于 gt 的匹配，可以发现 min\_pos\_iou=0 表示每个 gt 都一定有 anchor 匹配，且会出现忽略样本，可能引入低质量 anchor

匹配情况可视化如下：

![](http://static.zybuluo.com/huanghaian/5v98brpwq98aol41lvfzrhxx/image.png)

白色 bbox 是 gt 值，其余颜色是正样本 anchor。从 0-4 是从大特征图(检测小物体)到小特征图(检测大物体)的。可以发现图中有两个 gt bbox，其中网球排被分配到第 1 层负责预测，人分配到第 2 层负责预测了，可能存在某个 gt bbox 分配到多个层进行预测。

### 1.5 bbox 编解码

和 faster rcnn 中的 rpn 一样，为了使得各分支 Loss 更加稳定，需要对 gt bbox 进行编解码，其编解码过程也是基于 gt bbox 和 anchor box 的中心偏移+宽高比规则，对应的代码是：

```python
bbox_coder=dict(  # 基于anchor的中心点平移，wh缩放预测，编解码函数
            type='DeltaXYWHBBoxCoder',
            target_means=[.0, .0, .0, .0],
            target_stds=[1.0, 1.0, 1.0, 1.0]),
```

### 1.6 loss 计算

虽然前面正负样本定义阶段存在极度不平衡，但是由于 focal loss 的引入可以在很大程度克服。故分类分支采用 focal loss，回归分支可以采用 l1 loss 或者 smooth l1 loss，实验效果表明 l1 loss 好一些。

```python
loss_cls=dict(
            type='FocalLoss',
            use_sigmoid=True,
            gamma=2.0,
            alpha=0.25,
            loss_weight=1.0),
loss_bbox=dict(type='L1Loss', loss_weight=1.0)))
```

### 1.7 分类分支 bias 初始化

在 Retinanet 中，其分类分支初始化 bias 权重设置非常关键。那么原因是啥？

![](http://static.zybuluo.com/huanghaian/q6rx89tcq8nwfjkcrmxllx6r/image.png)

$$\pi$$ 默认为 0.01。这个操作非常关键，原因是 anchor 太多了，且没有 faster rcnn 里面的 sample 操作，故负样本远远大于正样本，也就是说分类分支，假设负样本：正样本数=1000:1，分类是 sigmod 输出，其输出的负数表示负样本 label，如果某个 batch 的分类输出都是负数，那么也就是预测全部是负类，这样算 loss 时候就会比较小，相当于强制输出的值偏向负类。

简单来说就是对于一个分类任务，一个 batch 内部几乎全部是负样本，如果预测的时候没有偏向，那么 Loss 肯定会非常大，因为大部分输出都是错误的，现在强制设置预测为负类，这样开始训练时候 loss 会比较小，这个操作会影响初始训练过程。

我们可以通过对分类分支的特征图进行计算 norm max min 三个值，并打印操作就看出： 当 bias=0,也就是没有偏向，则开始训练时候：

```
tensor(33.2100) tensor(0.0621) tensor(-0.0606)
tensor(16.8493) tensor(0.0466) tensor(-0.0516)
tensor(7.5828) tensor(0.0308) tensor(-0.0274)
tensor(3.4662) tensor(0.0240) tensor(-0.0219)
tensor(1.3125) tensor(0.0169 tensor(-0.0148)
```

当 bias 采用上述公式：

```
tensor(30403.4023) tensor(-4.5390>) tensor(-4.6472)
tensor(15201.4629) tensor(-4.5452) tensor(-4.6417)
tensor(7600.7085) tensor(-4.5635) tensor(-4.6303)
tensor(3976.3052) tensor(-4.5704) tensor(-4.6225)
tensor(2063.2097) tensor(-4.5809) tensor(-4.6108)
tensor(30403.4824 tensor(-4.5447) tensor(-4.6500）
```

可以明显发现，设置 0.01 的参数后输出 tensor 的值基本上都是负数，符合预期。

## 2 推理流程

在推理阶段，对 6 个输出 head 的预测首先取 top 1K 的预测值，然后用 0.05 的阈值过滤掉背景，此时得到的检测结果已经大大降低，此时再对检测结果的 box 分支进行解码，最后把输出 head 的检测结果拼接在一起，通过 IoU=0.5 的 NMS 过滤重叠框就得到最终结果。

## 3 实验分析

**(1) focal loss VS Ohem**

![](http://static.zybuluo.com/huanghaian/n78vht6lq13qo1ezrxfxrmtu/image.png)

可以看出在如此密集预测情况下，采用 ohem 策略会丢弃大量样本，导致训练过程不充分，效果明显不如 focal loss。

**(2) 性能对比**

![](http://static.zybuluo.com/huanghaian/g410prsz4mqvwwwf7bgnme2n/image.png)

## 4 总结

RetinaNet 是非常优秀的目标检测算法，其提出的 focal loss 和 backbone+neck+head 网络构建模型深深影响了整个领域的发展。其没有花里胡哨的设计，没有太多的 trick，也不存在难以理解的地方，个人觉得应该成为每一位目标检测算法初学者的首选。
