论文:feature pyramid networks for object detection
论文链接
论文概述:
作者提出的多尺度的object detection算法:FPN(feature pyramid networks)。原来多数的object detection算法都是只采用顶层特征做预测,但我们知道低层的特征语义信息比较少,但是目标位置准确;高层的特征语义信息比较丰富,但是目标位置比较粗略。另外虽然也有些算法采用多尺度特征融合的方式,但是一般是采用融合后的特征做预测,而本文不一样的地方在于预测是在不同特征层独立进行的。
特征金字塔
下图展示了4种利用特征的形式:
(a)图像金字塔,即将图像做成不同的scale,然后不同scale的图像生成对应的不同scale的特征。这种方法的缺点在于增加了时间成本。有些算法会在测试时候采用图像金字塔。
(b)像SPP net,Fast RCNN,Faster RCNN是采用这种方式,即仅采用网络最后一层的特征。
(c)像SSD(Single Shot Detector)采用这种多尺度特征融合的方式,没有上采样过程,即从网络不同层抽取不同尺度的特征做预测,这种方式不会增加额外的计算量。作者认为SSD算法中没有用到足够低层的特征(在SSD中,最低层的特征是VGG网络的conv4_3),而在作者看来足够低层的特征对于检测小物体是很有帮助的。
(d)本文作者是采用这种方式,顶层特征通过上采样和低层特征做融合,而且每层都是独立预测的。
如下图所示,我们可以看到图像中存在不同尺寸的目标,而不同的目标具有不同的特征,利用浅层的特征就可以将简单的目标的区分开来;利用深层的特征可以将复杂的目标区分开来;这样我们就需要这样的一个特征金字塔来完成这件事。图中我们在第1层(请看绿色标注)输出较大目标的实例分割结果,在第2层输出次大目标的实例检测结果,在第3层输出较小目标的实例分割结果。检测也是一样,我们会在第1层输出简单的目标,第2层输出较复杂的目标,第3层输出复杂的目标。
作者的主网络采用ResNet。
作者的算法大致结构如下图:一个自底向上的线路,一个自顶向下的线路,横向连接(lateral connection)。图中放大的区域就是横向连接,这里1*1的卷积核的主要作用是减少卷积核的个数,也就是减少了feature map的个数,并不改变feature map的尺寸大小。
自底向上其实就是网络的前向过程。在前向过程中,feature map的大小在经过某些层后会改变,而在经过其他一些层的时候不会改变,作者将不改变feature map大小的层归为一个stage,因此每次抽取的特征都是每个stage的最后一个层输出,这样就能构成特征金字塔。
自顶向下的过程采用上采样(upsampling)进行,而横向连接则是将上采样的结果和自底向上生成的相同大小的feature map进行融合(merge)。在融合之后还会再采用3*3的卷积核对每个融合结果进行卷积,目的是消除上采样的混叠效应(aliasing effect)。并假设生成的feature map结果是P2,P3,P4,P5,和原来自底向上的卷积结果C2,C3,C4,C5一一对应。
利用FPN构建Faster R-CNN检测器
步骤:
- 首先,选择一张需要处理的图片,然后对该图片进行预处理操作;
- 然后,将处理过的图片送入预训练的特征网络中(如ResNet等),即构建所谓的bottom-up网络;
- 接着,如下图所示,构建对应的top-down网络(即对层4进行上采样操作,先用1x1的卷积对层2进行降维处理,然后将两者相加(对应元素相加),最后进行3x3的卷积操作,最后);
- 接着,在图中的4、5、6层上面分别进行RPN操作,即一个3x3的卷积后面分两路,分别连接一个1x1的卷积用来进行分类和回归操作;
- 接着,将上一步获得的候选ROI分别输入到4、5、6层上面分别进行ROI Pool操作(固定为7x7的特征);
-
最后,在上一步的基础上面连接两个1024层的全连接网络层,然后分两个支路,连接对应的分类层和回归层;
FPN整体架构
注:层1、2、3对应的支路就是bottom-up网络,就是所谓的预训练网络,文中使用了ResNet网络;由于整个流向是自底向上的,所以我们叫它bottom-up;层4、5、6对应的支路就是所谓的top-down网络,是FPN的核心部分,名字的来由也很简单。
FPN构建Faster R-CNN检测器的Faster R-CNN+FPN细节图:
FPN性能评估
- FPN效果定量评估
表1 Faster R-CNN+FPN结果
如上表所示,我们可以看到当我们使用相同的预训练网络、相同的RPN网络、Fast R-CNN使用不同的方法时,我们的特征层由原来的C4或者C5变化为现在的特征集合Pk,同时FPN方案使用了横向连接(lateral)和top-down模型,算法的性能有了大幅度上升,与a相比提升了2.2个百分点,与b相比提升了4.0个百分点([email protected]);对于APs,提升了5.9个百分点;对于APm,提升了5.3个百分点。这也说明了FPN能够提升小目标的检测效果。
表2 FPN高效训练结果
观察表2,我们可以看到使用FPN的Fast R-CNN+FPN和没有使用FPN的Fast R-CNN方案之间的差别,首先我们的特征维度由1024减少到256维(这样可以大大的减少一些运算量,包括前向和反向运算);我们利用2个MLP层取代了Conv5,作为我们的头分类器;我们的训练时间由原来的44.6小时减少到现在的10.6小时,速度大概提升了4倍;我们的推理时间由原来的0.32s减少到现在的0.15s;最后,我们的准确率提升了2.0个百分点。主要的原因是因为我们通过FPN获得了更加鲁邦的高层语义特征,它使得我们的学习过程更加高效。
表3 COCO数据集结果展示
如上表所示,我们测试了FPN算法在COCO数据集上面的性能表现,使用了FPN的Faster R-CNN方法获得了很多的最佳指标,尤其是在APs指标上面。总之,获得了最好的单模型准确率。
表4 使用了FPN的RPN效果
如上表所示,我们比较了比较了FPN+RPN和RPN这两种方案,我们可以看到我们的性能有了大幅度的提升。
表5 top-down和lateral的重要性
在表5中,我们验证了top-down模型和Lateral连接的重要性,同时使用两者的FPN方案取得了最好的结果。相对来讲,Lateral连接起到了更好的效果。
表7 FPN在实例分割上面的效果
由于FPN是一个特征金字塔,具有很好地泛华能力,可以利用到很多利用深度学习网络的方法中去,包括目标检测、实例分割等。如上表所示,使用了FPN的DeepMask方法可以不仅可以获得性能的提升,同时可以获得速度的提升。(不同的目标在不同的层上面生成对应的Mask)!
总结
作者提出的FPN(Feature Pyramid Network)算法同时利用低层特征高分辨率和高层特征的高语义信息,通过融合这些不同层的特征达到预测的效果。并且预测是在每个融合后的特征层上单独进行的,这和常规的特征融合方式不同。
自己实现的代码,仅供参考:
import torch.nn as nn
import torch.nn.functional as F
import math
__all__ = ['FPN']
class Bottleneck(nn.Module):
expansion = 4
#这里,我们先实现的是Bottleneck的左半边(1*1->3*3->1*1),expansion = 最后一层的输出通道数/第一层的输入通道数
def __init__(self,in_planes,planes,stride=1,downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_planes,planes,kernel_size=1,bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes,planes,kernel_size=3,stride=stride,padding=1,bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes,self.expansion*planes,kernel_size=1,stride=stride,bias=False)
self.bn3 = nn.BatchNorm2d(self.expansion*planes)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
#这里加上了residual
def forward(self,x):
residual = x #右边的short_cut
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.con3(out)
out = self.bn3(out)
out = self.relu(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out += self.relu(out)
return out
class FPN(nn.Module):
#这里传入的参数block用于传入Bottleneck
def __init__(self,block,layers):
super(FPN, self).__init__()
self.inplanes = 64
#这部分是FPN最开始的一个Conv1,为了节省内存,没有将这部分纳入金字塔
self.conv1 = nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
#Bottom-up layers
self.layer1 = self._make_layer(block,layers[0])
self.layer2 = self._make_layer(block,layers[1],stride=2)
self.layer3 = self._make_layer(block,layers[2],stride=2)
self.layer4 = self._make_layer(block,layers[3],stride=2)
#Top layer
self.toplayer = nn.Conv2d(2048,256,kernel_size=1,stride=1,padding=0)
#FPN朝右边延伸的,Smooth layers
self.smooth1 = nn.Conv2d(256,256,kernel_size=3,stride=1,padding=1)
self.smooth2 = nn.Conv2d(256,256,kernel_size=3,stride=1,padding=1)
self.smooth3 = nn.Conv2d(256,256,kernel_size=3,stride=1,padding=1)
#FPN的左边的特征金字塔连接到右边
self.latlayer1 = nn.Conv2d(1024,256,kernel_size=1,stride=1,padding=0)
self.latlayer2 = nn.Conv2d(512,256,kernel_size=1,stride=1,padding=0)
self.latlayer3 = nn.Conv2d(256,256,kernel_size=1,stride=1,padding=0)
#这部分是初始化conv的值,还有BN的值
for m in self.modules():
if isinstance(m,nn.Conv2d):
n = m.kernel_size[0]*m.kernel_size[1]*m.out_channels
m.weight.data.normal_(0,math.sqrt(2./n))
elif isinstance(m,nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
#blocks是Bottleneck的数量
def _make_layer(self,block,planes,blocks,stride=1):
downsample = None
if stride != 1 or self.inplanes!= block.expansion*planes:
#这里的downsample用来实现Bottleneck的右边
downsample = nn.Sequential(
nn.Conv2d(self.inplanes,block.expansion*planes,kernel_size=1,stride=stride,bias=False),
nn.BatchNorm2d(block.expansion*planes)
)
layers = []
layers.append(block(self.inplanes,planes,stride,downsample))
self.inplanes = planes * block.expansion
for i in range(1,blocks):
layers.append(block(self.inplanes,planes))
return nn.Sequential(*layers)
#通过这个操作来结合左边过来的1*1+top-bottom
def _upsample_add(self,x,y):
_,_,H,W = y.size()
return F.upsample(x,size=(H,W),mode='bilinear')+y
def forward(self,x):
#Bottom-up
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
c1 = self.maxpool(x)
c2 = self.layer1(c1)
c3 = self.layer2(c2)
c4 = self.layer3(c3)
c5 = self.layer4(c4)
#top-bottom
p5 = self.toplayer(c5)
p4 = self._upsample_add(p5,self.latlayer1(c4))
p3 = self._upsample_add(p4,self.latlayer2(c3))
p2 = self._upsample_add(p3,self.latlayer3(c2))
#smooth
p4 = self.smooth1(p4)
p3 = self.smooth2(p3)
p2 = self.smooth3(p2)
return p2,p3,p4,p5
#101以上的才为Bottleneck
def FPN101():
return FPN(Bottleneck,[2,2,2,2])
代码的话有以下参考:
fpn.pytorch
FPN_Tensorflow