FPN(Feature Pyramid Network)
引言
一种兼顾准确率(检测小物体)和速度(开销)的卷积方式
FPN架构
上图左侧模型叫bottom-up,右侧模型叫top-down,特征金字塔网络(FPN)相当于先进行传统的bottom-up(自上而下)的特征卷积,然后FPN试图融合左侧特征图的相邻的特征图。横向的箭头叫横向连接(lateral connections),目的是因为高层的特征语义多,低层的特征语义少但位置信息多。
左侧模型特征图大小相差1倍,每在同样大小的feature上卷积几次才进行一次池化操作,我们把在同样大小feature上的卷积称之为一个stage。图中画的是每个stage的最后一个卷积层,因为每个stage的最后一层feature语义信息最多。
具体做法是两个特征层中的较高层特征2倍上采样(上采样几乎都是内插值方法,即在原有图像像素的基础上在像素点之间采用合适的插值算法插入新的元素,总之是把feature大小扩大了一倍)。较低层特征通过1×1卷积改变一下低层特征的通道数,然后简单地把将上采样和1×1卷积后的结果对应元素相加。
为什么横向连接要使用1×1卷积呢?
为什么不能直接拿过来利用呢?原因在于作者想通过1×1卷积来改变通道数,以达到各个level处理结果的channel都为256-d,便于后面对加起来的特征进行分类。
FPN只是提取特征的一种方法!!!
上图中只画了3个stage,以上图为例,左侧模型从低到高的卷积结果记为C2,C3,C4,同理右侧模型从低到高记为P2,P3,P4。
FPN架构及其Pytorch实现
FPN总体架构如下图所示,主要包含自下而上网络、自上而下网络、横向连接与卷积融合4个部分。
自下而上:C2到C5代表不同的ResNet卷积Stage,这些卷积组包含了多个Bottleneck结构,组内的特征图大小相同,组间大小递减。
自下而上:首先对C5进行1x1卷积降低通道数得到P5,然后依次进行上采样得到P4、P3、P2。目的是得到与C4、C3、C2大小相同的特征,以便进行逐元素相加。【采用2倍最近邻上采样(直接对临近元素进行复制,而非线性插值)】。
横向连接:目的是将上采样后的高语义特征与浅层的定位细节进行融合。高语义特征经过上采样后,其长宽与对应的浅层特征相同,而通道数固定为256。因此需要对特征C2—C4进行1x1卷积使得其通道数变为256.,然后两者进行逐元素相加得到P4、P3与P2。
卷积融合:在得到相加后的特征后,利用3x3卷积对生成的P2,P3,P4进行融合。目的是消除上采样过程中带来的重叠效应,以生成最终的特征图。
import torch.nn as nn
import torch.nn.functional as F
import math
# ResNet的基本模块-Bottleneck类
class Bottleneck(nn.Module):
expansion=4#通道倍增数
def __init__(self,in_planes,planes,stride=1,downsample=None):
super(Bottleneck,self).__init__()
self.bottleneck=nn.Sequential(
nn.Conv2d(in_planes,planes,1,bias=False),
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes,planes,3,stride,1,bias=False),
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes,self.expansion*planes,1,bias=False),
nn.BatchNorm2d(self.expansion*planes),
)
self.relu=nn.ReLU(inplace=True)
self.downsample=downsample
def forward(self,x):
identity=x
out=self.bottleneck(x)
if self.downsample is not None:
identity=self.downsample(x)
out+=identity
out=self.relu(out)
return out
#FPN的类,初始化需要一个list,代表ResNet的每一个阶段的Bottleneck的数量
class FPN(nn.Module):
def __init__(self,layers):
super(FPN,self).__init__()
self.inplanes=64
#处理输入的C1模块(C1代表了RestNet的前几个卷积与池化层)
self.conv1=nn.Conv2d(3,64,7,2,3,bias=False)
self.bn1=nn.BatchNorm2d(64)
self.relu=nn.ReLU(inplace=True)
self.maxpool=nn.MaxPool2d(3,2,1)
#搭建自下而上的C2,C3,C4,C5
self.layer1=self._make_layer(64,layers[0])
self.layer2=self._make_layer(128,layers[1],2)
self.layer3=self._make_layer(256,layers[2],2)
self.layer4=self._make_layer(512,layers[3],2)
#对C5减少通道数,得到P5
self.toplayer=nn.Conv2d(2048,256,1,1,0)
#3x3卷积融合特征
self.smooth1=nn.Conv2d(256,256,3,1,1)
self.smooth2=nn.Conv2d(256,256,3,1,1)
self.smooth3=nn.Conv2d(256,256,3,1,1)
#横向连接,保证通道数相同
self.latlayer1=nn.Conv2d(1024,256,1,1,0)
self.latlayer2=nn.Conv2d(512,256,1,1,0)
self.latlayer3=nn.Conv2d(256,256,1,1,0)
def _make_layer(self,planes,blocks,stride=1):
downsample=None
if stride!=1 or self.inplanes != Bottleneck.expansion*planes:
downsample=nn.Sequential(
nn.Conv2d(self.inplanes,Bottleneck.expansion*planes,1,stride,bias=False),
nn.BatchNorm2d(Bottleneck.expansion*planes)
)
layers=[]
layers.append(Bottleneck(self.inplanes,planes,stride,downsample))
self.inplanes=planes*Bottleneck.expansion
for i in range(1,blocks):
layers.append(Bottleneck(self.inplanes,planes))
return nn.Sequential(*layers)
#自上而下的采样模块
def _upsample_add(self,x,y):
_,_,H,W=y.shape
return F.upsample(x,size=(H,W),mode='bilinear')+y
def forward(self,x):
#自下而上
c1=self.maxpool(self.relu(self.bn1(self.conv1(x))))
c2=self.layer1(c1)
c3=self.layer2(c2)
c4=self.layer3(c3)
c5=self.layer4(c4)
#自上而下
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))
#卷积的融合,平滑处理
p4=self.smooth1(p4)
p3=self.smooth2(p3)
p2=self.smooth3(p2)
return p2,p3,p4,p5
# 利用上述类函数创建一个FPN网络
net = FPN([3,4,6,3])
# (3+4+6+3)*3 + 2 = 50,即基于ResNet50的框架建立了相对应的FPN网络
上述基于ResNet50建立的FPN([3,4,6,3])网络详细构型(参数详情)如下:
FPN(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): Bottleneck(
(bottleneck): Sequential(
(0): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding