为了增强语义性, 传统的物体检测模型通常只在深度卷积网络的最后一个特征图上进行后续操作, 而这一层对应的下采样率(图像缩小倍数) 通常又比较大, 如16、 32, 造成小物体在特征图上的有效信息较少, 小物体的检测性能会急剧下降, 这个问题也被称为多尺度问题.
解决多尺度问题的关键在于如何提取多尺度的特征。 传统的方法有图像金字塔(Image Pyramid) , 主要思路是将输入图片做成多个尺度,不同尺度的图像生成不同尺度特征, 这种方法简单而有效, 大量使用在了COCO等竞赛上, 但缺点是非常耗时, 计算量也很大。
从前面几节内容可以知道, 卷积神经网络不同层的大小与语义信息不同, 本身就类似一个金字塔结构。 2017年的FPN(Feature Pyramid
Network) 方法融合了不同层的特征, 较好地改善了多尺度检测问题。
FPN的总体架构如图3.21所示, 主要包含自下而上网络、 自上而下网络、 横向连接与卷积融合4个部分。
·自下而上: 最左侧为普通的卷积网络, 默认使用ResNet结构, 用作提取语义信息。 C1代表了ResNet的前几个卷积与池化层, 而C2至C5分别为不同的ResNet卷积组, 这些卷积组包含了多个Bottleneck结构,组内的特征图大小相同, 组间大小递减。
·自上而下: 首先对C5进行1×1卷积降低通道数得到P5, 然后依次进行上采样得到P4、 P3和P2, 目的是得到与C4、 C3与C2长宽相同的特征, 以方便下一步进行逐元素相加。 这里采用2倍最邻近上采样, 即直接对临近元素进行复制, 而非线性插值。
·横向连接(Lateral Connection) : 目的是为了将上采样后的高语义特征与浅层的定位细节特征进行融合。 高语义特征经过上采样后, 其长宽与对应的浅层特征相同, 而通道数固定为256, 因此需要对底层特征C2至C4进行11卷积使得其通道数变为256, 然后两者进行逐元素相加得到P4、 P3与P2。 由于C1的特征图尺寸较大且语义信息不足, 因此没有把C1放到横向连接中。
·卷积融合: 在得到相加后的特征后, 利用3×3卷积对生成的P2至P4再进行融合, 目的是消除上采样过程带来的重叠效应, 以生成最终的特征图。
对于实际的物体检测算法, 需要在特征图上进行RoI(Region of Interests, 感兴趣区域) 提取, 而FPN有4个输出的特征图, 选择哪一个特征图上面的特征也是个问题。 FPN给出的解决方法是, 对于不同大小的RoI, 使用不同的特征图, 大尺度的RoI在深层的特征图上进行提取,如P5, 小尺度的RoI在浅层的特征图上进行提取, 如P2, 具体确定方法, 感兴趣的读者可以自行查看。
FPN将深层的语义信息传到底层, 来补充浅层的语义信息, 从而获得了高分辨率、 强语义的特征, 在小物体检测、 实例分割等领域有着非常不俗的表现。
1 import torch.nn as nn 2 import torch.nn.functional as F 3 import math 4 5 # ResNet的基本Bottleneck类 6 class Bottleneck(nn.Module): 7 8 expansion = 4 # 通道倍增数 9 10 def __init__(self, in_planes, planes, stride=1, downsample = None): 11 super(Bottleneck, self).__init__() 12 self.bottleneck = nn.Sequential( 13 nn.Conv2d(in_planes, planes, 1, bias=False), 14 nn.BatchNorm2d(planes), 15 nn.ReLU(inplace= True), 16 nn.Conv2d(planes, planes, 3, stride, 1, bias=False), 17 nn.BatchNorm2d(planes), 18 nn.ReLU(inplace=True), 19 nn.Conv2d(planes, self.expansion*planes, 1, bias=False), 20 nn.BatchNorm2d(self.expansion*planes) 21 ) 22 self.relu = nn.ReLU(inplace=True) 23 self.downsample = downsample 24 25 def forward(self, x): 26 identity = x 27 out = self.bottleneck(x) 28 if self.downsample is not None: 29 identity = self.downsample(x) 30 out += identity 31 out = self.relu(out) 32 33 return out 34 35 # FPN的类, 初始化需要一个list, 代表ResNet每一个阶段的Bottleneck的数量 36 class FPN(nn.Module): 37 38 def __init__(self, layers): 39 super(FPN, self).__init__() 40 self.inplanes = 64 41 42 # 处理输入的C1模块 43 self.conv1 = nn.Conv2d(3, 64, 7, 2, 3, bias=False) 44 self.bn1 = nn.BatchNorm2d(64) 45 self.relu = nn.ReLU(inplace=True) 46 self.maxpool = nn.MaxPool2d(3, 2, 1) 47 48 # 搭建自下而上的C2、 C3、 C4、 C5 49 self.layer1 = self._make_layer(64, layers[0]) 50 self.layer2 = self._make_layer(128, layers[1], 2) 51 self.layer3 = self._make_layer(256, layers[2], 2) 52 self.layer4 = self._make_layer(512, layers[3], 2) 53 54 # 对C5减少通道数, 得到 P5 55 self.toplayer = nn.Conv2d(2048, 256, 1, 1, 0) 56 # 3×3卷积融合特征 57 self.smooth1 = nn.Conv2d(256, 256, 3, 1, 1) 58 self.smooth2 = nn.Conv2d(256, 256, 3, 1, 1) 59 self.smooth3 = nn.Conv2d(256, 256, 3, 1, 1) 60 61 # 横向连接, 保证通道数相同 62 self.latlayer1 = nn.Conv2d(1024, 256, 1, 1, 0) 63 self.latlayer2 = nn.Conv2d(512, 256, 1, 1, 0) 64 self.latlayer3 = nn.Conv2d(256, 256, 1, 1, 0) 65 66 # 构建C2到C5, 注意区分stride值为1和2的情况 67 def _make_layer(self, planes, blocks, stride=1): 68 downsample = None 69 if stride != 1 or self.inplanes != Bottleneck.expansion*planes: 70 downsample = nn.Sequential( 71 nn.Conv2d(self.inplanes, Bottleneck.expansion*planes, 1, 72 stride, bias=False), 73 nn.BatchNorm2d(Bottleneck.expansion*planes) 74 ) 75 layers = [] 76 layers.append(Bottleneck(self.inplanes, planes, stride, downsample)) 77 self.inplanes = planes*Bottleneck.expansion 78 for i in range(1, blocks): 79 layers.append(Bottleneck(self.inplanes, planes)) 80 81 return nn.Sequential(*layers) 82 83 # 自上而下的上采样模块 84 def _upsample_add(self, x, y): 85 _,_,H,W = y.shape 86 return F.unsample(x, size(H, W), mode='bilinear') + y 87 88 def forward(self, x): 89 # 自下而上 90 c1 = self.maxpool(self.relu(self.bn1(self.conv1(x)))) 91 c2 = self.layer1(c1) 92 c3 = self.layer2(c2) 93 c4 = self.layer3(c3) 94 c5 = self.layer4(c4) 95 # 自上而下 96 p5 = self.toplayer(c5) 97 p4 = self._upsample_add(p5, self.latlayer1(c4)) 98 p3 = self._upsample_add(p4, self.latlayer2(c3)) 99 p2 = self._upsample_add(p3, self.latlayer3(c2)) 100 # 卷积融合, 平滑处理 101 p4 = self.smooth1(p4) 102 p3 = self.smooth2(p3) 103 p2 = self.smooth3(p2) 104 105 return p2, p3, p4, p5 106
1 import torch 2 from fpn import FPN 3 4 # 利用list来初始化FPN网络 5 net_fpn = FPN([3, 4, 6, 3]).cuda() 6 7 print(net_fpn.conv1) # 查看FPN的第一个卷积 8 # >> Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) 9 10 print(net_fpn.bn1) # 查看FPN的第一个BN层 11 # >> BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 12 13 print(net_fpn.relu) # 查看FPN的第一个ReLU层 14 # >> ReLU(inplace=True) 15 16 print(net_fpn.maxpool) # 查看FPN的第一个池化层, 使用最大值池化 17 # >> MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False) 18 19 print(net_fpn.layer1) # 查看FPN的第一个layer, 即前面的C2, 包含了3个Bottleneck 20 >> Sequential( 21 # layer1中第1个Bottleneck模块 22 (0): Bottleneck( 23 (bottleneck): Sequential( 24 (0): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False) 25 (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 26 (2): ReLU(inplace=True) 27 (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) 28 (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 29 (5): ReLU(inplace=True) 30 (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False) 31 (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 32 ) 33 # 34 # 这里存在一个通道增加模块 35 (relu): ReLU(inplace=True) 36 (downsample): Sequential( 37 (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False) 38 (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 39 ) 40 ) 41 # 42 # layer1中第2个Bottleneck模块 43 (1): Bottleneck( 44 (bottleneck): Sequential( 45 (0): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False) 46 (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 47 (2): ReLU(inplace=True) 48 (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) 49 (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 50 (5): ReLU(inplace=True) 51 (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False) 52 (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 53 ) 54 (relu): ReLU(inplace=True) 55 ) 56 # 57 # layer1中第3个Bottleneck模块 58 (2): Bottleneck( 59 (bottleneck): Sequential( 60 (0): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False) 61 (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 62 (2): ReLU(inplace=True) 63 (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) 64 (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 65 (5): ReLU(inplace=True) 66 (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False) 67 (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 68 ) 69 (relu): ReLU(inplace=True) 70 ) 71 ) 72 73 print(net_fpn.layer2) # 查看fpn的layer2, 即上面的C3, 包含了4个Bottleneck 74 >>Sequential( 75 # layer2中第1个Bottleneck 76 (0): Bottleneck( 77 (bottleneck): Sequential( 78 (0): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 79 (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 80 (2): ReLU(inplace=True) 81 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) 82 (4): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 83 (5): ReLU(inplace=True) 84 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False) 85 (7): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 86 ) 87 (relu): ReLU(inplace=True) 88 (downsample): Sequential( 89 (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False) 90 (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 91 ) 92 ) 93 # 94 # layer2中第2个Bottleneck 95 (1): Bottleneck( 96 (bottleneck): Sequential( 97 (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 98 (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 99 (2): ReLU(inplace=True) 100 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) 101 (4): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 102 (5): ReLU(inplace=True) 103 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False) 104 (7): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 105 ) 106 (relu): ReLU(inplace=True) 107 ) 108 # 109 # layer2中第3个Bottleneck 110 (2): Bottleneck( 111 (bottleneck): Sequential( 112 (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 113 (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 114 (2): ReLU(inplace=True) 115 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) 116 (4): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 117 (5): ReLU(inplace=True) 118 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False) 119 (7): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 120 ) 121 (relu): ReLU(inplace=True) 122 ) 123 # 124 # layer2中第4个Bottleneck 125 (3): Bottleneck( 126 (bottleneck): Sequential( 127 (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 128 (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 129 (2): ReLU(inplace=True) 130 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) 131 (4): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 132 (5): ReLU(inplace=True) 133 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False) 134 (7): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 135 ) 136 (relu): ReLU(inplace=True) 137 ) 138 ) 139 140 print(net_fpn.toplayer) # 1×1的卷积, 以得到P5 141 # >> Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1)) 142 143 print(net_fpn.smooth1) # 对P4进行平滑的卷积层 144 # >> Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) 145 146 print(net_fpn.latlayer1) # 对C4进行横向处理的卷积层 147 # >> Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1)) 148 149 input = torch.randn(1, 3, 224, 224).cuda() 150 output = FPN(input) 151 # 返回的P2、 P3、 P4、 P5, 这4个特征图通道数相同, 但特征图尺寸递减 152 print(output[0].shape) 153 >> torch.Size([1, 256, 56, 56]) 154 print(output[1].shape) 155 >> torch.Size([1, 256, 28, 28]) 156 print(output[2].shape) 157 >> torch.Size([1, 256, 14, 14]) 158 print(output[3].shape) 159 >> torch.Size([1, 256, 7, 7])