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。

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

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

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

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

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

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 设计模式。

(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 特征图。

考虑到大物体可能感受野不够,故还需要构建两个额外的输出层 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 是权重共享的。

# 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 设置规则:

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 中的完全相同,仅仅阈值设置不同而已。配置如下:

#双阈值策略
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

匹配情况可视化如下:

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

1.5 bbox 编解码

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

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 好一些。

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 权重设置非常关键。那么原因是啥?

简单来说就是对于一个分类任务,一个 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

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

(2) 性能对比

4 总结

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

Last updated