本文主要介绍两个用于物体检测的 backbone:FPN 和 DetNet
多尺度问题
- 多尺度问题: 为了增强语义性,传统的物体检测模型通常只在深度卷积网络的最后一个特征图上进行后续操作,而这一层对应的下采样率通常又比较大,如 16、32,造成小物体在特征图上的有效信息较少,小物体的检测性能会急剧下降
特征金字塔: FPN (Feature Pyramid Network, 特征金字塔)
- 解决多尺度问题的关键在于如何提取多尺度的特征。传统的方法有图像金字塔 (Image Pyramid),主要思路是将输入图片做成多个尺度,不同尺度的图像生成不同尺度的特征,这种方法简单而有效,大量使用在了 COCO 等竞赛上,但缺点是非常耗时,计算量也很大
- 而卷积神经网络不同层的大小与语义信息不同,本身就类似一个金字塔结构,FPN 就利用了这一特点,将深层的语义信息传到底层,来补充浅层的语义信息,从而获得了高分辨率、强语义的特征,在小物体检测、实例分割等领域有着非常不俗的表现
FPN 的总体架构
- 自下而上:最左侧为普通的卷积网络,默认使用 ResNet 结构,用作提取语义信息。 C 1 , . . . , C 5 C_1,...,C_5 C1,...,C5 代表了不同的 ResNet 卷积组,这些卷积组包含了多个 Bottleneck 结构,组内的特征图大小相同,组间大小递减
- 自上而下:首先对 C 5 C_5 C5 进行 1 × 1 1×1 1×1 卷积降低通道数得到 P 5 P_5 P5,然后依次进行上采样得到 P 4 P_4 P4、 P 3 P_3 P3 和 P 2 P_2 P2,目的是得到与 C 4 C_4 C4、 C 3 C_3 C3 与 C 2 C_2 C2 长宽相同的特征,以方便下一步进行逐元素相加。这里采用 2 倍最邻近上采样,即直接对临近元素进行复制,而非线性插值
- 横向连接 (Lateral Connection):目的是为了将上采样后的高语义特征与浅层的定位细节特征进行融合。高语义特征经过上采样后,其长宽与对应的浅层特征相同,而通道数固定为 256,因此需要对底层特征 C 2 C_2 C2 至 C 4 C_4 C4 进行 1 × 1 1\times1 1×1 卷积使得其通道数变为 256,然后两者进行逐元素相加得到 P 4 P_4 P4、 P 3 P_3 P3 与 P 2 P_2 P2。由于 C 1 C_1 C1 的特征图尺寸较大且语义信息不足,因此没有把 C 1 C_1 C1 放到横向连接中
- 卷积融合:在得到相加后的特征后,利用 3 × 3 3×3 3×3 卷积对生成的 P 2 P_2 P2 至 P 4 P_4 P4 再进行融合,目的是消除上采样过程带来的重叠效应,以生成最终的特征图
FPN 用于物体检测算法
- 对于实际的物体检测算法,需要在特征图上进行 RoI 提取,而 FPN 有 4 个输出的特征图,选择哪一个特征图上面的特征也是个问题。FPN 给出的解决方法是,对于不同大小的 RoI,使用不同的特征图,大尺度的 RoI 在深层的特征图上进行提取,小尺度的 RoI 在浅层的特征图上进行提取
import torch.nn as nn
import torch.nn.functional as F
import math
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
class FPN(nn.Module):
def __init__(self, layers=[3, 4, 6, 3]):
super(FPN, self).__init__()
self.inplanes = 64
# Block C1
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)
self.layer1 = self._make_layer(64, layers[0]) # C2
self.layer2 = self._make_layer(128, layers[1], 2) # C3 (步长为 2 时特征图大小减半)
self.layer3 = self._make_layer(256, layers[2], 2) # C4
self.layer4 = self._make_layer(512, layers[3], 2) # C5
self.toplayer = nn.Conv2d(2048, 256, 1, 1, 0) # P5
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)
# lateral connection
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
# adjust #channel or feature map size in the skip connection
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):
# x: (3, w, h)
c1 = self.maxpool(self.relu(self.bn1(self.conv1(x)))) # (64, w/4, h/4)
c2 = self.layer1(c1) # (64*4, w/4, h/4)
c3 = self.layer2(c2) # (128*4, w/8, h/8)
c4 = self.layer3(c3) # (256*4, w/16, h/16)
c5 = self.layer4(c4) # (512*4, w/32, h/32)
p5 = self.toplayer(c5) # (256, w/32, h/32)
p4 = self._upsample_add(p5, self.latlayer1(c4)) # (256, w/16, h/16)
p3 = self._upsample_add(p4, self.latlayer2(c3)) # (256, w/8, h/8)
p2 = self._upsample_add(p3, self.latlayer3(c2)) # (256, w/4, h/4)
p4 = self.smooth1(p4) # (256, w/16, h/16)
p3 = self.smooth2(p3) # (256, w/8, h/8)
p2 = self.smooth3(p2) # (256, w/4, h/4)
return p2, p3, p4, p5
为检测而生: DetNet
- 传统的 VGG、ResNet 等用于图像分类的 backbone 更加侧重于全图的特征提取,深层的特征图分辨率很低;而物体检测需要定位出物体位置,特征图分辨率不宜过小。而 FPN 虽然提取出了多尺度特征,但仍存在不足。总体可归结为如下两个缺陷:
- (1) 大物体难以定位:对于 FPN 和 ResNet 等其他网络,大物体对应在较深的特征图上检测,由于网络较深时下采样率较大,物体的边缘难以精确预测,增加了回归边界的难度
- (2) 小物体难以检测:对于传统网络,由于下采样率大造成小物体在较深的特征图上几乎不可见;FPN 虽从较浅的特征图来检测小物体,但浅层的语义信息较弱,且融合深层特征时使用的上采样操作也会增加物体检测的难度
- 也就是说,用于大物体检测的深层特征图也应该具有较大的分辨率,用于小物体检测的浅层特征图应该更加充分地融合深层特征信息。为此,DetNet 引入了空洞卷积,使得模型兼具较大感受野与较高分辨率,同时避免了 FPN 的多次上采样,实现了较好的检测效果
DetNet 网络结构
- DetNet 仍然选择性能优越的 ResNet-50 作为基础结构,并保持前 4 个 stage 与 ResNet-50 相同,具体的结构细节有以下几点:
- (1) 修改了 Stage 5 并加入新的 Stage 6 用于物体检测。Stage 5 与 Stage 6 使用了新的 Bottleneck 结构,最大的特点是利用空洞数为 2 的 3 × 3 3×3 3×3 卷积取代了步长为 2 2 2 的 3 × 3 3×3 3×3 卷积,在保持感受野增大的同时还能保证特征图大小不变
- (2) Stage 5 与 Stage 6 的每一个 Bottleneck 输出的特征图尺寸都为原图的 1 16 \frac{1}{16} 161,通道数都为 256,因此在组成特征金字塔时,由于特征图大小和通道数完全相同,因此可以直接从右向左传递相加,避免了 FPN 的上采样操作
- (3) 为了进一步融合各通道的特征,需要对每一个阶段的输出进行
1
×
1
1×1
1×1 卷积后再与后一 Stage 传回的特征相加
注意, B B B 相比于 A A A,在恒等映射部分增加了一个 1 × 1 1×1 1×1 卷积,这样做可以区分开不同的 Stage,并且实验发现这种做法对于特征金字塔式的检测非常重要
from torch import nn
class DetBottleneck(nn.Module):
def __init__(self, inplanes, planes, extra=False):
super(DetBottleneck, self).__init__()
self.bottleneck = nn.Sequential(
nn.Conv2d(inplanes, planes, 1, bias=False),
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=2,
dilation=2, bias=False), # 保持特征图大小不变
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes, planes, 1, bias=False),
nn.BatchNorm2d(planes),
)
self.relu = nn.ReLU(inplace=True)
self.extra = extra
if self.extra:
self.extra_conv = nn.Sequential(
nn.Conv2d(inplanes, planes, 1, bias=False),
nn.BatchNorm2d(planes)
)
def forward(self, x):
if self.extra:
identity = self.extra_conv(x)
else:
identity = x
out = self.bottleneck(x)
out += identity
out = self.relu(out)
return out
bottleneck_b = DetBottleneck(1024, 256, True)
bottleneck_a1 = DetBottleneck(256, 256)
bottleneck_a2 = DetBottleneck(256, 256)
References
- 《深度学习之 PyTorch 物体检测实战》
- D i v e Dive Dive I n t o Into Into D e e p Deep Deep L e a r n i n g Learning Learning