[关闭]
@huanghaian 2020-11-17T08:12:57.000000Z 字数 6226 阅读 1590

mmdetection:RetinaNet分析

mmdetection


0 摘要

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。
image.png-16.1kB

以二分类问题为例,输出通道是1,如果y=1则表示正样本,y=0表示负样本,p是经过sigmoid后的输出概率值,可以发现其对正负样本的惩罚是一样的。为了统一成一个公式,可以设置
image.png-9.8kB
此时loss函数可以写成
为了能够突出正负样本不平衡问题,可以引入加权ce loss即:
image.png-6.9kB
对正负样本设置不同的(一般正负权重加起来是1)即可,可以通过交叉验证确定。但是上述损失函数只关注于正负样本类别不平衡问题,而无法关注难易样本不平衡问题,故focal loss的定义如下:
image.png-7.4kB
是超参,并且两者相互影响,作者通过实验设置。loss可视化如下:

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

我们可以自己通过仿真调整参数看下具体效果:
image.png-101.4kB

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

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

image.png-34.4kB

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

  1. pred_sigmoid = pred.sigmoid()
  2. # one-hot格式
  3. target = target.type_as(pred)
  4. pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)
  5. focal_weight = (alpha * target + (1 - alpha) *
  6. (1 - target)) * pt.pow(gamma)
  7. loss = F.binary_cross_entropy_with_logits(
  8. pred, target, reduction='none') * focal_weight
  9. loss = weight_reduce_loss(loss, weight, reduction, avg_factor)
  10. return loss

1.3 RetinaNet网络结构

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

(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特征图。
image.png-39.1kB

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

  1. # x是p3-p7中的某个特征图
  2. cls_feat = x
  3. reg_feat = x
  4. # 4层不共享参数卷积
  5. for cls_conv in self.cls_convs:
  6. cls_feat = cls_conv(cls_feat)
  7. for reg_conv in self.reg_convs:
  8. reg_feat = reg_conv(reg_feat)
  9. # 输出特征图
  10. cls_score = self.retina_cls(cls_feat)
  11. bbox_pred = self.retina_reg(reg_feat)
  12. return cls_score, bbox_pred

1.4 正负样本定义

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

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

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

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

  1. #双阈值策略
  2. assigner=dict(
  3. type='MaxIoUAssigner',
  4. pos_iou_thr=0.5,
  5. neg_iou_thr=0.4,
  6. min_pos_iou=0,
  7. 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

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

1.5 bbox编解码

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

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

1.6 loss计算

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

  1. loss_cls=dict(
  2. type='FocalLoss',
  3. use_sigmoid=True,
  4. gamma=2.0,
  5. alpha=0.25,
  6. loss_weight=1.0),
  7. loss_bbox=dict(type='L1Loss', loss_weight=1.0)))

1.7 分类分支bias初始化

在Retinanet中,其分类分支初始化bias权重设置非常关键。那么原因是啥?
image.png-2.3kB
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过滤重叠框就得到最终结果。

23 实验分析

(1) focal loss VS Ohem
image.png-64.2kB

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

(2) 性能对比
image.png-102.8kB

4 总结

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注