FPN
特征金字塔是目标检测识别系统中的一个基础组件,但是最近深度目标检测器避免使用金字塔的表示方式,部分原因是因为特征金字塔是强计算和强内存的,计算非常昂贵。
原有的目标检测算法通常都是只采用顶层特征做检测,原因是网络顶层特征的语义信息比较丰富。然而,虽顶层特征的语义信息丰富,但其中的目标位置信息却比较粗略,不利于目标包围框的准确定位;相反,虽然底层特征的语义信息比较少,但其中目标的位置信息却非常准确。
再次背景下,作者利用深度卷积神经网络固有的多尺度、多层级的金字塔结构去构建特征金字塔网络
FPN构建了一个自顶向下,横向连接的结构,以此来构建一个多尺度,高级语义的特征图
现有的几种特征金字塔结构
- 由
图像金字塔
来构建特征金字塔
,其中每一个特征独立的在每一个图片尺度上计算。缺点:慢 - 只采用单一尺度的特征图用于快速检测,这里只使用了最高层级的特征图进行预测
- 由卷积网络计算得到的特征金字塔层次结构,分层预测,例如SSD
- FPN方式:和2,3速度相同,但是使得每一层有更强的语义信息
注:其中蓝色线条的粗细表示了语义特征的强弱
建立在图像金字塔上的特征金字塔
在识别任务中,特征工程大量的被深度卷积网络计算的特征所替代,除了能够表示更高层次的语义外,深度卷积网络对尺度规模也更健壮,因此有助于从单一输入尺度上计算的特征进行识别。
其中图像金字塔的每一层的特征的优势是其产生的特征每一层的语义信息都是很强的,包括高分辨率的低层。其缺点也很明显,无法做到实时监测,并且在图片金字塔训练深度的端到端的网络是不可行的,由于内存的局限性。图像金字塔只有在测试时间使用,这也导致了训练和测试的不连续。由此,在fast rcnn和faster rcnn中并没有使用基于图片的特征金字塔。
但是,图片金字塔并不是唯一构建多尺度特征表示的方式,深层卷积网络逐层计算特征层次结构,通过下采样层,特征层次结构具有内在的多尺度金字塔形状。这种网络内的特征层次结构产生了不同空间分辨率的特征图,但由于深度不同,引入了很大的语义差距。这种高分辨率的特征图有低水平的特征,这可能会导致损失目标识别的表现能力。
SSD是以上方式的尝试,理想情况下,SSD金字塔在前向传播的过程中,将会重用来自不同特征层的特征图,这时便没有多余的消耗。但是也有明显的缺点,避免使用低层次的特征,SSD将其丢弃,从网络的高层开始构建金字塔网络结构。它错过了重用特征层次结构的高分辨率图的机会。
作者目标
利用深度网络特征层次结构的金字塔形状,创建一个特征金字塔,使得每一尺度都有很强的语义信息。
现有情况:
低特征层:高分辨率,语义不强,利于小目标检测
高特征层:低分辨率,语义强
思想:利用低层次的高分辨率和高层次的强语义进行融合
对比跳连接和作者思路:
图一目标: 他们的目标是生成一个高分辨率的单一高级特征图,并据此进行预测
图二:利用上采样和横向连接结构构建特征金字塔,在每一层独立的进行预测,实现语义从低到高,建立一个高语义的特征图像金字塔,并进行预测。可以在所有尺度上达到端到端,并且保持训练和测试连续,这是使用图片金字塔所达不到的。
FPN主干
自底向上:即深度卷积网络主干前向传播的过程,它计算一个由多个尺度的特征图组成的特征层次结构,有许多层产生相同大小的输出特征图,将其视为在网络的同一个阶段,将每个阶段定义为一个金字塔层级,选择该阶段的最后一层,作为输出,由于其每个阶段的最后一层拥有最强的语义信息。
ResNet:
将每一个block视为一个stage,将stage的最后一层进行输出,其中[c2,c3,c4,c5]对应网络的[2,3,4,5]的输出。其中每层的步幅为[4,8,16,32]
自顶向下,侧连接:高层次特征图与低层次特征图融合
自上而下路径将从主干网络提取到的最强语义信息进行上采样,使得下层的空间信息更加粗糙但是语义更强。
再通过横向连接,将自底向上的时的特征图,与上采样得到的特征图进行融合,将其融合成一个特征图,横向链接的特征图具有低级特征,但是融合会使得其定位更加准确(其下采样次数较少)
1x1的卷积网络会使得该层变得的粗糙,并调整通道数为256.
3x3的卷积操作得到最终的特征图,这样会降低混叠效应
输出特征图对应的预测结果:[c2,c3,c4,c5]得到[p2,p3,p4,p5]
与faster rcnn结构类似:
Fpn–fixed
import math
from collections import OrderedDict
import torch
from torch import nn
class BasicBlock(nn.Module):
def __init__(self, in_channel, out_channel):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channel, out_channel, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(out_channel)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(out_channel, out_channel, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(out_channel)
def forward(self, x):
residual = x
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return self.relu(out + residual)
class ResNet(nn.Module):
def __init__(self, layer):
super(ResNet, self).__init__()
self.inplanes = 64
self.b1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False),
nn.BatchNorm2d(64),
nn.ReLU()
)
# backbone
self.c2 = self._make_layer([64, 256], layer[0])
self.c3 = self._make_layer([256, 512], layer[1])
self.c4 = self._make_layer([512, 1024], layer[2])
self.c5 = self._make_layer([1024, 2048], layer[3])
self.max = nn.MaxPool2d(kernel_size=1)
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_()
def _make_layer(self, planes, blocks):
layer = []
# 下采样
layer.append(("down_sample", nn.Conv2d(self.inplanes, planes[1], kernel_size=1, stride=2, bias=False)))
layer.append(("bn", nn.BatchNorm2d(planes[1])))
layer.append(("relu", nn.ReLU()))
self.inplanes = planes[1]
for i in range(0, blocks):
layer.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes[1])))
# print(layer)
return nn.Sequential(OrderedDict(layer))
def forward(self, x):
out = self.b1(x)
out = self.max(out)
# 提取特征
c2 = self.c2(out)
c3 = self.c3(c2)
c4 = self.c4(c3)
c5 = self.c5(c4)
return c2, c3, c4, c5
class Fpn(nn.Module):
def __init__(self, layer):
super(Fpn, self).__init__()
self.backbone = ResNet(layer)
# 改变通道数
self.lateral2 = nn.Conv2d(256, 256, kernel_size=1)
self.lateral3 = nn.Conv2d(512, 256, kernel_size=1)
self.lateral4 = nn.Conv2d(1024, 256, kernel_size=1)
self.lateral5 = nn.Conv2d(2048, 256, kernel_size=1)
# last conv
self.smooth = nn.Conv2d(256, 256, kernel_size=3)
self.max = nn.MaxPool2d(kernel_size=1)
def upSample(self, x, y):
trans = nn.Upsample(scale_factor=2, mode="nearest")
x = trans(x)
return x + y
def forward(self, x):
c2, c3, c4, c5 = self.backbone(x)
# fpn lateral
c5 = self.lateral5(c5)
c4 = self.lateral4(c4)
c3 = self.lateral3(c3)
c2 = self.lateral2(c2)
# up sample smooth
p5 = self.smooth(c5)
p6 = self.max(p5)
c4 = self.upSample(c5, c4)
p4 = self.smooth(c4)
c3 = self.upSample(c4, c3)
p3 = self.smooth(c3)
c2 = self.upSample(c3, c2)
p2 = self.smooth(c2)
return p2, p3, p4, p5, p6
net = Fpn([2, 2, 2, 2])
X = torch.rand(size=(1, 3, 224, 224))
print(net(X))