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实例分割任务中获得第一,在目标检测任务中获得第二。
其中前两点既可用在实例分割任务中,也可用在目标检测任务中,对这两个任务的性能提升都有很大的帮助。
在神经网络中,高层包含更丰富的语义信息,低层包含更丰富的细节信息,因此通过一个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}\),如下图所示
在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
原始的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的处理过程比较相似只不过方向相反,但具体实现也有一些区别:
最后的输出除了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 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
`_.
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)