FPN

FPN 全名是 Feature Pyramid Networks,特征金字塔现在可以说是必备组件了,在不同领域都有相关做法,其主要作用是提升小物体检测精度,同时也可以通过特征融合提升整体性能,属于即插即用模块,通用性非常强。

作者首先分析当前目标检测常用解决方案有:

(1) 图像金字塔,也可以称为多尺度训练和测试,典型算法是 DPM,如图 a 所示。对原始图片进行多尺度采样构成图像金字塔,然后在每个层级上面进行预测,缺点是计算量极大,无法实际使用。在实际中为了平衡,有些算法会在测试时候采用图像金字塔,而训练时候不使用;

(2) 单层预测,典型算法是 RCNN 系列和 YOLO,如图 b 所示。该类算法本质是利用卷积网络具有的类似于金字塔功能的特征提取能力,然后在最高层进行预测,缺点是只使用最高层语义信息会丢失小目标精度,因为 stride 步骤的存在,高分辨率的低层特征很难有代表性的检测能力;

(3) 多层预测,典型算法是 SSD,如图 c 所示。该类算法在不同层后进行预测,部分弥补了(2)类方法的不足,缺点是直接强行让不同层学习相似的语义信息会存在语义间隙。对于卷积神经网络而言,不同深度对应着不同层次的语义特征,浅层网络分辨率高,学的更多是细节特征,深层网络分辨率低,学的更多是语义特征。SSD 还存在一个问题:它并没有利用较低高分辨率特征图,而是在高层特征图后面再加了几层,作者认为这样对小目标检测不利。

基于以上三种方法存在的不足,作者提出如图 d 所示结构,通过 top-down pathway 从上到下路径和横向连接 lateral connections,结合高分辨率、弱语义信息的特征层和低分辨率、强语义信息的特征融合,实现类似图像金字塔效果,顶层特征通过上采样和低层特征做融合,而且每层都是独立预测的,效果显著。

1 算法分析

1.1 FPN 网络结构

FPN 模块也就是我们常说的 neck,其配置如下:

neck=dict(
    type='FPN',
    in_channels=[256, 512, 1024, 2048],
    out_channels=256,
    num_outs=5),

为了充分发挥 FPN 功效,骨架网络输出就不再是一个特征图了,而是多个尺度的特征图,配置如下所示:

backbone=dict(
    type='ResNet',
    depth=50,
    num_stages=4,
    out_indices=(0, 1, 2, 3),
    frozen_stages=1,
    norm_cfg=dict(type='BN', requires_grad=True),
    norm_eval=True,
    style='pytorch'),

核心就是 out_indices 参数,其表示输出 4 个特征图,按照特征图从大到小排列,分别是 c2 c3 c4 c5,stride=4,8,16,32,而 num_outs 表示经过 FPN 后需要输出 5 个特征图。FPN 结构的细节图如下:

代码实现流程是:

  • 将 c2 c3 c4 c5 4 个特征图全部经过各自 1x1 卷积进行通道变换变成 m2~m5,输出通道统一为 256

  • 从 m5 开始,先进行 2 倍最近邻上采样,然后和 m4 进行 add 操作,得到新的 m4

  • 将新 m4 进行 2 倍最近邻上采样,然后和 m3 进行 add 操作,得到新的 m3

  • 将新 m3 进行 2 倍最近邻上采样,然后和 m2 进行 add 操作,得到新的 m2

  • 对 m5 和新的融合后的 m4~m2,都进行各自的 3x3 卷积,得到 4 个尺度的最终输出 P5 ~ P2

  • 将 c5 进行 3x3 且 stride=2 的卷积操作,得到 P6,目的是提供一个感受野非常大的特征图,有利于检测超大物体

到目前为止就实现了 c2~c5 4 个特征图输入,P2~P6 5 个特征图输出。

1.2 RPN 改动

原始 faster rcnn 的 RPN 部分是一个输出特征图,现在变成了 5 个输出图了,为了适应这种变换,anchor 的设置进行了适当修改,原因是 FPN 输出的多尺度信息可以帮助区分不同大小的问题,也就是说有了多尺度输出后可以实现 stride 大的输出图仅仅检测大物体,stride 小的输出图仅仅检测小物体,每一层就不再需要那么多 anchor 了,其配置如下:

anchor_generator=dict(
        type='AnchorGenerator',
        scales=[8],
        ratios=[0.5, 1.0, 2.0],
        strides=[4, 8, 16, 32, 64]),

一共包括 5 个输出特征图,且 stride 小的特征图负责检测小物体,那么其 anchor 也要设置的比较小,方便匹配感受野。可以看出,每个特征图位置都是预测 3 个 anchor,不再是原始的 15 个。整个 RPN 网络由原来的单尺度预测变成了多尺度预测,其余地方没有进行任何修改。

1.3 RCNN 改动

其改动发生在 roipool 层运行前。原来 RPN 输出就是一个特征图,假设其输出 1000 个 roi,直接去该特征图上面切割即可,但是现在特征图变成了 5 个,那么对于每个 roi 应该切割哪个特征图呢?其可视化图如下:

很多人可能有疑问:假设某个 proposal 是由第 4 个输出层检测出来的,为啥该 proposal 不是直接去该特征图层切割就行,还需要重新映射?其实原因非常简单,因为这些 proposal 相当于是 RPN 测试阶段检测出来的,大部分 proposal 可能符合前面设定,但是也有很多不符合的,不然为啥后面有很多论文提出自适应 gt bbox 分配对应预测层的改进呢?也就是说测试阶段上述一致性设定不一定满足,还需要重新映射。

本文所提映射规则公式为:

上述设置 k0=4,通过公式可以算出 Pk,具体是:

  • wh>=448x448,则分配给 P5

  • wh=224x224,则分配给 P4

  • wh=112x112,则分配给 P3

  • 其余分配给 P2

可以看出不包括 P6,原因是 P6 在 rcnn 中是不用的(rpn 用),原则依然是 roi 面积大的分配给 stride 大的层。

def map_roi_levels(self, rois, num_levels):
    """Map rois to corresponding feature levels by scales.

    - scale < finest_scale * 2: level 0
    - finest_scale * 2 <= scale < finest_scale * 4: level 1
    - finest_scale * 4 <= scale < finest_scale * 8: level 2
     - scale >= finest_scale * 8: level 3
    """
    scale = torch.sqrt(
        (rois[:, 3] - rois[:, 1]) * (rois[:, 4] - rois[:, 2]))
    target_lvls = torch.floor(torch.log2(scale / self.finest_scale + 1e-6))
    target_lvls = target_lvls.clamp(min=0, max=num_levels - 1).long()
    return target_lvls

其中 finest_scale=56,num_level=5,后面的操作就没有任何区别了。

2 总结

FPN 本质上实现了不同尺度特征图中的信息融合。 低层特征分辨率高,位置信息强,但语义信息弱;高层特征则相反。 FPN 通过上采样、旁支卷积等手段,将这些不同层次的特征融合到了一起,得到了一系列同时包含位置信息、语义信息的融合特征。

另一方面,不同层次的特征本身又蕴含了代表了不同的尺度, 通过 ROI 尺寸与特征图之间的映射关系,FPN 也实现了单一网络、单一图片尺寸下的多尺度检测。

FPN 基于神经网络自带的层次化特征,因此 FPN 或可以很容易集成到各种检测框架中。 nowadays,FPN 已经成为 RPN、Faster RCNN、RetinaNet、Yolo V3 等常用的检测算法中的基本模块。 由于 FPN 在 backbone 和 head 之间,因此也被称为 neck。

FPN 所采用的自上而下的叠加只是融合方式的一种。 如果考虑其他融合方式就会得到其他的特征融合方法。 FPN 的设计也成为目标检测领域的一个研究方向,这个方向中也产生了许多其他的 FPN 模块。

faster rcnn 引入 FPN 后整个算法就比较成熟了,也就是我们现在经常看到的模样。一旦引入 FPN,那么多尺度预测就自然而然会考虑,对整个目标检测算法都带来了巨大的提升,特别是小物体检测性能,并且 FPN 作为一个通用即插即用组件不仅可以应用于目标检测,还可以应用于其余领域。不得不佩服 FAIR 的论文,简洁优雅且通用。

Last updated