PANet(CVPR 2018)原理与代码解析

paper:Path Aggregation Network for Instance Segmentation

official implementation:GitHub – ShuLiu1993/PANet: PANet for Instance Segmentation and Object Detection 

third party implementation:mmdetection/pafpn.py at master · open-mmlab/mmdetection · GitHub 

前言

信息在神经网络中的传播方式是非常重要的。本文提出的路径聚合网络(Path Aggregation Network, PANet)旨在促进proposal-based实例分割框架中的信息流动。具体来说,通过自底向上的路径增强,利用底层中精确的定位信息来增强整个特征层次,缩短了下层与最上层之间的信息路径。本文还提出了自适应特征池化(adaptive feature pooling),它将特征网格和所有层级的特征连接起来,使每个特征层级上有用的信息直接传播到后面的proposal subnetworks。此外还增加了一个互补分支来为每个proposal捕获不同视野的信息以进一步提升掩膜预测的效果。 

基于上述优化,本文提出的PANet在COCO 2017实例分割任务中获得第一,在目标检测任务中获得第二。

本文的贡献

  1. bottom-up path augmentation
  2. adaptive feature pooling
  3. fully-connected feature fusion

其中前两点既可用在实例分割任务中,也可用在目标检测任务中,对这两个任务的性能提升都有很大的帮助。

方法介绍

Bottom-up Path Aggregation

在神经网络中,高层包含更丰富的语义信息,低层包含更丰富的细节信息,因此通过一个top-down augmenting path将语义较强的特征信息传播到所有特征层级中,从而增强所有特征的语义分类能力是很有必要的,这也是FPN中做的事情。

在实例分割任务中,需要精确的识别出物体的边缘,浅层网络中包含了大量边缘等细节特征,对实例分割任务非常有用。而在传统的CNN和FPN中,浅层的信息传播到顶层需要很长的路径甚至要经过上百层,这会造成很多细节信息的丢失,因此作者新增了一条bottom-up的增强路径,如下图(b)所示,这条路径不到10层,起到shortcut的作用,可以保留更多的细节信息。

具体的实现比较简单和FPN类似,一个大分辨率的特征图 \(N_{i}\) 经过一个stride=2的3×3卷积层减小spatial size后和FPN中的 \(P_{i+1}\) 层的分辨率大小相等,将两者进行element-wise add后再经过一个3×3卷积就得到了 \(N_{i+1}\),如下图所示

Adaptive Feature Pooling

在FPN中,proposals根据大小分配到不同的feature level,将小的proposal分配到较低的特征层,将大的proposal分配到较高的特征层。虽然简单有效,但结果可能不是最优的,比如两个大小只有10个像素差异的proposal有可能被分配到不同的特征层级,即使它们非常相似。

此外,特征的重要性和它所属的层级可能没有很强的关联性。High-level的特征由大的感受野生成并且提取了更丰富的上下文信息,让小的proposal获取这些特征可以更好的利用有用的上下文信息来进行预测。同样,low-level的特征包含丰富的细节信息以及更好的定位精度,让大的proposal获取这些信息显然也是有益的。因此,作者提出对于每个proposal,提取所有层级的特征并进行融合,然后基于此融合特征进行后续的分类和回归。 

实现过程如图(1)(c)所示,对于每个proposal,首先将它映射到所有的特征层级,如图(1)(b)中的灰色区域。然后延续Mask R-CNN的做法,对每个特征层级进行ROIAlign操作,接着对不同层级的feature grid进行融合(element-wise max or sum)。

具体实现中,pooled feature grids首先分别经过一个参数层后,然后再进行融合,使网络能够适应特征。例如FPN中的box分支有两个fc层,我们在第一个fc层后进行融合操作。Mask R-CNN中的mask预测分支有四个卷积层,我们在第一个和第二个卷积层之间进行融合操作。下图是box分支上adaptive feature pooling

Fully-connected Fusion

原始的Mask R-CNN中,mask预测分支是FCN的结构,因为全连接层可以提取到和卷积层不同的信息,作者在mask预测分支新增了一个fc分支,结构如下。

具体而言,在原始的FCN的第三个卷积后,增加了一个分支,首先经过两个3×3卷积,为了减少计算量第二个卷积的输出通道减半,然后经过一个全连接层,这个fc层是用来预测一个class-agnostic前景/背景mask。因为最终mask的大小为28×28,因此这里fc层输出一个784x1x1的向量,然后reshape成28×28大小的特征图。最后,FCN分支预测的每个类别的的mask都与fc分支预测的前景/背景mask相加,得到最终的输出mask。

代码解析

在YOLO v4以及后面的版本中大都用到了PANet,但那里面的PANet特指neck中的bottom-up path augmentation结构,因此这里只解析一下这部分的代码。下面是mmdetection中的实现,其中输入batch_size=2,预处理后input_shape=(2, 3, 300, 300)。backbone为resnet-50,输入图片经过backbone后进入neck也就是这里的PAFPN中,代码进行了一些注释,主要是打印了一些操作以及一些中间输出结果。

bottom-up path和原始的top-down FPN的处理过程比较相似只不过方向相反,但具体实现也有一些区别:

  1. FPN中有lateral_conv,即对于backbone中的C2~C5首先分别经过1×1卷积将输出通道统一为256,然后再进行top-down特征融合。而bottom-up path中没有这个lateral_conv,直接用P2~P5,比如N2就是P2。
  2. FPN中不同层级特征图融合需要先对齐spatial size,具体通过最近邻插值进行上采样。而bottom-up path是通过stride=2的3×3卷积进行下采样。

最后的输出除了N2~N5,上面还多加了一层N6,通过对N5进行stride=2的max pooling得到。而在FPN中增加一个P6,比如在retinanet中是通过stride=2的3×3卷积得到的,并且卷积的输入也不一定是P5,有’on_input’, ‘on_lateral’, ‘on_output’这几种选择,见下面的代码。

# Copyright (c) OpenMMLab. All rights reserved.
import torch.nn as nn
import torch.nn.functional as F
from mmcv.cnn import ConvModule
from mmcv.runner import auto_fp16

from ..builder import NECKS
from .fpn import FPN


@NECKS.register_module()
class PAFPN(FPN):
    """Path Aggregation Network for Instance Segmentation.

    This is an implementation of the `PAFPN in Path Aggregation Network
    <https://arxiv.org/abs/1803.01534>`_.

    Args:
        in_channels (List[int]): Number of input channels per scale.
        out_channels (int): Number of output channels (used at each scale)
        num_outs (int): Number of output scales.
        start_level (int): Index of the start input backbone level used to
            build the feature pyramid. Default: 0.
        end_level (int): Index of the end input backbone level (exclusive) to
            build the feature pyramid. Default: -1, which means the last level.
        add_extra_convs (bool | str): If bool, it decides whether to add conv
            layers on top of the original feature maps. Default to False.
            If True, it is equivalent to `add_extra_convs='on_input'`.
            If str, it specifies the source feature map of the extra convs.
            Only the following options are allowed

            - 'on_input': Last feat map of neck inputs (i.e. backbone feature).
            - 'on_lateral':  Last feature map after lateral convs.
            - 'on_output': The last output feature map after fpn convs.
        relu_before_extra_convs (bool): Whether to apply relu before the extra
            conv. Default: False.
        no_norm_on_lateral (bool): Whether to apply norm on lateral.
            Default: False.
        conv_cfg (dict): Config dict for convolution layer. Default: None.
        norm_cfg (dict): Config dict for normalization layer. Default: None.
        act_cfg (str): Config dict for activation layer in ConvModule.
            Default: None.
        init_cfg (dict or list[dict], optional): Initialization config dict.
    """

    def __init__(self,
                 in_channels,
                 out_channels,
                 num_outs,
                 start_level=0,
                 end_level=-1,
                 add_extra_convs=False,
                 relu_before_extra_convs=False,
                 no_norm_on_lateral=False,
                 conv_cfg=None,
                 norm_cfg=None,
                 act_cfg=None,
                 init_cfg=dict(
                     type='Xavier', layer='Conv2d', distribution='uniform')):
        super(PAFPN, self).__init__(
            in_channels,
            out_channels,
            num_outs,
            start_level,
            end_level,
            add_extra_convs,
            relu_before_extra_convs,
            no_norm_on_lateral,
            conv_cfg,
            norm_cfg,
            act_cfg,
            init_cfg=init_cfg)
        # add extra bottom up pathway
        self.downsample_convs = nn.ModuleList()
        self.pafpn_convs = nn.ModuleList()
        for i in range(self.start_level + 1, self.backbone_end_level):
            d_conv = ConvModule(
                out_channels,
                out_channels,
                3,
                stride=2,
                padding=1,
                conv_cfg=conv_cfg,
                norm_cfg=norm_cfg,
                act_cfg=act_cfg,
                inplace=False)
            pafpn_conv = ConvModule(
                out_channels,
                out_channels,
                3,
                padding=1,
                conv_cfg=conv_cfg,
                norm_cfg=norm_cfg,
                act_cfg=act_cfg,
                inplace=False)
            self.downsample_convs.append(d_conv)
            self.pafpn_convs.append(pafpn_conv)

    @auto_fp16()
    def forward(self, inputs):
        # [torch.Size([2, 256, 75, 75])
        #  torch.Size([2, 512, 38, 38])
        #  torch.Size([2, 1024, 19, 19])
        #  torch.Size([2, 2048, 10, 10])]
        """Forward function."""
        assert len(inputs) == len(self.in_channels)

        # build laterals
        laterals = [
            lateral_conv(inputs[i + self.start_level])  # 0
            for i, lateral_conv in enumerate(self.lateral_convs)
        ]
        # print(self.lateral_convs)
        # ModuleList(
        #   (0): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))
        #   )
        #   (1): ConvModule(
        #     (conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
        #   )
        #   (2): ConvModule(
        #     (conv): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
        #   )
        #   (3): ConvModule(
        #     (conv): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))
        #   )
        # )
        # print(laterals)
        # [torch.Size([2, 256, 75, 75])
        #  torch.Size([2, 256, 38, 38])
        #  torch.Size([2, 256, 19, 19])
        #  torch.Size([2, 256, 10, 10])]

        # build top-down path
        used_backbone_levels = len(laterals)  # 4
        for i in range(used_backbone_levels - 1, 0, -1):
            prev_shape = laterals[i - 1].shape[2:]
            # fix runtime error of "+=" inplace operation in PyTorch 1.10
            laterals[i - 1] = laterals[i - 1] + F.interpolate(
                laterals[i], size=prev_shape, mode='nearest')

        # build outputs
        # part 1: from original levels
        inter_outs = [
            self.fpn_convs[i](laterals[i]) for i in range(used_backbone_levels)
        ]
        # print(self.fpn_convs)
        # ModuleList(
        #   (0): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        #   )
        #   (1): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        #   )
        #   (2): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        #   )
        #   (3): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        #   )
        # )

        # print(self.downsample_convs)
        # ModuleList(
        #   (0): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
        #   )
        #   (1): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
        #   )
        #   (2): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
        #   )
        # )

        # part 2: add bottom-up path
        for i in range(0, used_backbone_levels - 1):
            inter_outs[i + 1] += self.downsample_convs[i](inter_outs[i])

        # print(self.pafpn_convs)
        # ModuleList(
        #   (0): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        #   )
        #   (1): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        #   )
        #   (2): ConvModule(
        #     (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        #   )
        # )
        outs = []
        outs.append(inter_outs[0])
        outs.extend([
            self.pafpn_convs[i - 1](inter_outs[i])
            for i in range(1, used_backbone_levels)
        ])

        # part 3: add extra levels
        if self.num_outs > len(outs):  # 5,4
            # use max pool to get more levels on top of outputs
            # (e.g., Faster R-CNN, Mask R-CNN)
            if not self.add_extra_convs:  # False
                for i in range(self.num_outs - used_backbone_levels):
                    outs.append(F.max_pool2d(outs[-1], 1, stride=2))
            # add conv layers on top of original feature maps (RetinaNet)
            else:
                if self.add_extra_convs == 'on_input':
                    orig = inputs[self.backbone_end_level - 1]
                    outs.append(self.fpn_convs[used_backbone_levels](orig))
                elif self.add_extra_convs == 'on_lateral':
                    outs.append(self.fpn_convs[used_backbone_levels](
                        laterals[-1]))
                elif self.add_extra_convs == 'on_output':
                    outs.append(self.fpn_convs[used_backbone_levels](outs[-1]))
                else:
                    raise NotImplementedError
                for i in range(used_backbone_levels + 1, self.num_outs):
                    if self.relu_before_extra_convs:
                        outs.append(self.fpn_convs[i](F.relu(outs[-1])))
                    else:
                        outs.append(self.fpn_convs[i](outs[-1]))
        # print(outs)
        # [torch.Size([2, 256, 75, 75])
        #  torch.Size([2, 256, 38, 38])
        #  torch.Size([2, 256, 19, 19])
        #  torch.Size([2, 256, 10, 10])]
        return tuple(outs)

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年9月19日
下一篇 2023年9月19日

相关推荐