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实例分割任务中获得第一,在目标检测任务中获得第二。
本文的贡献
bottom-up path augmentationadaptive feature poolingfully-connected feature fusion其中前两点既可用在实例分割任务中,也可用在目标检测任务中,对这两个任务的性能提升都有很大的帮助。
方法介绍
Bottom-up Path Aggregation
在神经网络中,高层包含更丰富的语义信息,低层包含更丰富的细节信息,因此通过一个top-down augmenting path将语义较强的特征信息传播到所有特征层级中,从而增强所有特征的语义分类能力是很有必要的,这也是FPN中做的事情。
在实例分割任务中,需要精确的识别出物体的边缘,浅层网络中包含了大量边缘等细节特征,对实例分割任务非常有用。而在传统的CNN和FPN中,浅层的信息传播到顶层需要很长的路径甚至要经过上百层,这会造成很多细节信息的丢失,因此作者新增了一条bottom-up的增强路径,如下图(b)所示,这条路径不到10层,起到shortcut的作用,可以保留更多的细节信息。
具体的实现比较简单和FPN类似,一个大分辨率的特征图 \(N_{i}\) 经过一个stride=2的3x3卷积层减小spatial size后和FPN中的 \(P_{i+1}\) 层的分辨率大小相等,将两者进行element-wise add后再经过一个3x3卷积就得到了 \(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的第三个卷积后,增加了一个分支,首先经过两个3x3卷积,为了减少计算量第二个卷积的输出通道减半,然后经过一个全连接层,这个fc层是用来预测一个class-agnostic前景/背景mask。因为最终mask的大小为28x28,因此这里fc层输出一个784x1x1的向量,然后reshape成28x28大小的特征图。最后,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的处理过程比较相似只不过方向相反,但具体实现也有一些区别:
FPN中有lateral_conv,即对于backbone中的C2~C5首先分别经过1x1卷积将输出通道统一为256,然后再进行top-down特征融合。而bottom-up path中没有这个lateral_conv,直接用P2~P5,比如N2就是P2。FPN中不同层级特征图融合需要先对齐spatial size,具体通过最近邻插值进行上采样。而bottom-up path是通过stride=2的3x3卷积进行下采样。最后的输出除了N2~N5,上面还多加了一层N6,通过对N5进行stride=2的max pooling得到。而在FPN中增加一个P6,比如在retinanet中是通过stride=2的3x3卷积得到的,并且卷积的输入也不一定是P5,有'on_input', 'on_lateral', 'on_output'这几种选择,见下面的代码。
# Copyright (c) OpenMMLab. All rights reserved.import torch.nn as nnimport torch.nn.functional as Ffrom mmcv.cnn import ConvModulefrom mmcv.runner import auto_fp16from ..builder import NECKSfrom .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)