经过了前面对目标检测基础以及实验所用VOC数据集的学习和了解,接下来就进入到了此任务的关键步骤——网络设计
本专栏笔记用于记录学习DataWhale开源CV教程动手学CV-Pytorch遇到的问题及思考
教程链接如下:动手学CV-Pytorch
重要内容文章会以黄色背景或者蓝色字体标出
锚框
在特征图上生成锚框是目标检测任务中常见的手段。在输入图像经过网络的特征提前后,通常在生成的特征图的每个cell(cell可以由自己设定,意为将一幅图像分成
n
×
n
n \times n
n×n个方格,每个方格称为一个cell)上生成锚框,一般来说每个cell上存在三种尺度的锚框,而且对应于每个尺度大小又各有三种不同大小比例于其对应,因此对于特征图的每个cell,一共存在九个锚框。
至于为什么不在原输入图像上生成锚框,而在特征图上生成。这是因为特征图经过网络的特征提取后,其输出尺寸会远远小于原图,以教程中的实验为例,原始图像的尺寸为224x224在经过vgg16的骨干网络进行特征提取后就只剩下7x7,这样可以大大减少需要生成的锚框的数量,加快网络速度
特征提取后特征图与原图尺寸对比如图所示:
锚框的取舍
在特征图生成锚框后,我们需要对其进行一个适当的取舍,因为所有生成的锚框中,我们最后只要取其中置信度最高的一个。所以采用的方法就是设定一个阈值,用特征图上生成的锚框与目标物体标注信息里的标注框来计算两者的IOU,当IOU大于阈值时,我们保留该锚框,并作为网络可以学习的一部分。
过程如图:
生成锚框的代码部分
"""
设置细节介绍:
1. 离散程度 fmap_dims = 7: VGG16最后的特征图尺寸为 7*7
2. 在上面的举例中我们是假设了三种尺寸的先验框,然后遍历坐标。在先验框生成过程中,先验框的尺寸是提前设置好的,
本教程为特征图上每一个cell定义了共9种不同大小和形状的候选框(3种尺度*3种长宽比=9)
生成过程:
0. cx, cy表示中心点坐标
1. 遍历特征图上每一个cell,i+0.5是为了从坐标点移动至cell中心,/fmap_dims目的是将坐标在特征图上归一化
2. 这个时候我们已经可以在每个cell上各生成一个框了,但是这个不是我们需要的,我们称之为base_prior_bbox基准框。
3. 根据我们在每个cell上得到的长宽比1:1的基准框,结合我们设置的3种尺度obj_scales和3种长宽比aspect_ratios就得到了每个cell的9个先验框。
4. 最终结果保存在prior_boxes中并返回。
需要注意的是,这个时候我们的到的先验框是针对特征图的尺寸并归一化的,因此要映射到原图计算IOU或者展示,需要:
img_prior_boxes = prior_boxes * 图像尺寸
"""
def create_prior_boxes():
"""
Create the 441 prior (default) boxes for the network, as described in the tutorial.
VGG16最后的特征图尺寸为 7*7
我们为特征图上每一个cell定义了共9种不同大小和形状的候选框(3种尺度*3种长宽比=9)
因此总的候选框个数 = 7 * 7 * 9 = 441
:return: prior boxes in center-size coordinates, a tensor of dimensions (441, 4)
"""
fmap_dims = 7
obj_scales = [0.2, 0.4, 0.6]
aspect_ratios = [1., 2., 0.5]
prior_boxes = []
for i in range(fmap_dims):
for j in range(fmap_dims):
cx = (j + 0.5) / fmap_dims
cy = (i + 0.5) / fmap_dims
for obj_scale in obj_scales:
for ratio in aspect_ratios:
prior_boxes.append([cx, cy, obj_scale * sqrt(ratio), obj_scale / sqrt(ratio)])
prior_boxes = torch.FloatTensor(prior_boxes).to(device) # (441, 4)
prior_boxes.clamp_(0, 1) # (441, 4)
return prior_boxes
在教程的代码中,对于锚框的生成流程已经解释的很清楚了,并且也对为什么取锚框中心点坐标时要除以一个fmap_dims作出了解答,但是对于FOR循环的最后一步:
prior_boxes.append([cx, cy, obj_scale * sqrt(ratio), obj_scale / sqrt(ratio)])
这里面的逻辑关系教程没有多言,在此做一个解释,为什么需要 obj_scale * sqrt(ratio), obj_scale / sqrt(ratio) 这样计算
如图,我们设置两个锚框,锚框1、2具有同一尺度以及不同的长宽比
若规定锚框1的长宽都为2,要求在不改变面积的情况下求出锚框2在长宽比为2;1时的长和宽的值,则我们可以设
x
x
x为锚框2的长,
y
y
y为锚框2的宽。根据关系我们可以列出下列式子:
x
y
=
4
xy = 4
xy=4
x
=
2
y
x=2y
x=2y
可以解得
x
=
2
2
,
y
=
2
x = 2 \sqrt2,y= \sqrt2
x=22,y=2
对应与代码为:
x = obj_scale * sqrt(ratio), y = obj_scale / sqrt(ratio)
而这其中的obj_scale、ratio正是我们提前设置的尺度和比例
obj_scales = [0.2, 0.4, 0.6]
aspect_ratios = [1., 2., 0.5]
于是我们就不难理解这里代码的含义
网络设计
深度学习任务中使用的网络模型异常关键,有时候同一个任务,用不同的网络来完成,可能就是质的飞跃。
马老师请问VGG16长甚么样子?
教程实验中采用的VGG网络结构如图:
在Pytorch中定义该VGG网络的步骤较简单,代码如下:
class VGGBase(nn.Module):
"""
VGG base convolutions to produce feature maps.
完全采用vgg16的结构作为特征提取模块,丢掉fc6和fc7两个全连接层。
因为vgg16的ImageNet预训练模型是使用224×224尺寸训练的,因此我们的网络输入也固定为224×224
"""
def __init__(self):
super(VGGBase, self).__init__()
# Standard convolutional layers in VGG16
self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1) # stride = 1, by default
self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # 224->112
self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # 112->56
self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2) # 56->28
self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2) # 28->14
self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.pool5 = nn.MaxPool2d(kernel_size=2, stride=2) # 14->7
# Load pretrained weights on ImageNet
self.load_pretrained_layers()
def forward(self, image):
"""
Forward propagation.
:param image: images, a tensor of dimensions (N, 3, 224, 224)
:return: feature maps pool5
"""
out = F.relu(self.conv1_1(image)) # (N, 64, 224, 224)
out = F.relu(self.conv1_2(out)) # (N, 64, 224, 224)
out = self.pool1(out) # (N, 64, 112, 112)
out = F.relu(self.conv2_1(out)) # (N, 128, 112, 112)
out = F.relu(self.conv2_2(out)) # (N, 128, 112, 112)
out = self.pool2(out) # (N, 128, 56, 56)
out = F.relu(self.conv3_1(out)) # (N, 256, 56, 56)
out = F.relu(self.conv3_2(out)) # (N, 256, 56, 56)
out = F.relu(self.conv3_3(out)) # (N, 256, 56, 56)
out = self.pool3(out) # (N, 256, 28, 28)
out = F.relu(self.conv4_1(out)) # (N, 512, 28, 28)
out = F.relu(self.conv4_2(out)) # (N, 512, 28, 28)
out = F.relu(self.conv4_3(out)) # (N, 512, 28, 28)
out = self.pool4(out) # (N, 512, 14, 14)
out = F.relu(self.conv5_1(out)) # (N, 512, 14, 14)
out = F.relu(self.conv5_2(out)) # (N, 512, 14, 14)
out = F.relu(self.conv5_3(out)) # (N, 512, 14, 14)
out = self.pool5(out) # (N, 512, 7, 7)
# return 7*7 feature map
return out
def load_pretrained_layers(self):
"""
we use a VGG-16 pretrained on the ImageNet task as the base network.
There's one available in PyTorch, see https://pytorch.org/docs/stable/torchvision/models.html#torchvision.models.vgg16
We copy these parameters into our network. It's straightforward for conv1 to conv5.
"""
# Current state of base
state_dict = self.state_dict()
param_names = list(state_dict.keys())
# Pretrained VGG base
pretrained_state_dict = torchvision.models.vgg16(pretrained=True).state_dict()
pretrained_param_names = list(pretrained_state_dict.keys())
# Transfer conv. parameters from pretrained model to current model
for i, param in enumerate(param_names):
state_dict[param] = pretrained_state_dict[pretrained_param_names[i]]
self.load_state_dict(state_dict)
print("\nLoaded base model.\n")
其中Pytorch中的torch.nn模块非常重要,包含了构造网络常用的一些工具,例如一些卷积层Conv2d、Conv3d、池化层:Maxpool2d、Avgpool2d、还有一些常用的激活函数层Relu,正则化层啊等等
马老师,这个anchor到底行不行?
在经过网络的特征提取之后,原始输入图像现在的SIZE变成了 7 x 7,然后我们在特征图上进行了锚框的生成,接下来,我们要怎么使用这么锚框呢?
马老师说,你这个生成不操作的锚框没用,我这个有用,我这有混元形意回归分类头,传统功夫,是讲究化劲儿的
对于每个anchor,我们需要预测两类信息,一个是这个anchor的类别信息,一个是物体的边界框信息
对于anchor的位置微调
模型要预测anchor与目标框的偏移,并且这个偏移会进行某种形式的归一化,这个过程我们称为边界框的编码,此处实验中的编码步骤参考的是SSD的编码步骤,即:
其中 c x , c y , g w , g h c_x,c_y,g_w,g_h cx,cy,gw,gh为目标框的 x y w h xywh xywh,其余为anchor的 x y w h xywh xywh
需要注意的是模型预测并输出的是这个编码后的偏移量 ( g c x , g c y , g w , g h ) (g_{cx},g_{cy},g_w,g_h) (gcx,gcy,gw,gh) 而不是直接预测目标框。
而对于每个anchor所属分类的预测,我们可以在特征图后使用一个卷积层来完成。
综述
为了得到我们想预测的类别和偏移量,我们需要在feature map后分别接上两个卷积层:
1)一个分类预测的卷积层采用3x3卷积核padding和stride都为1,每个anchor需要分配21个卷积核,每个位置有9个anchor,因此需要21x9个卷积核。
2)一个定位预测卷积层,每个位置使用3x3卷积核padding和stride都为1,每个anchor需要分配4个卷积核,因此需要4x9个卷积核。
于是我们,接上两个3x3的卷积层,在7x7的feature map后,即可分别完成分类和回归的预测。
之后我们将分类头的输出 batch_size x 7 x 7 x 189,回归头的输出 batch_size x 7 x 7 x 36,进行reshape,变成分类头 batch_size x 441 x 21,回归头 batch_size x 441 x 4,使用pytorch.cat将两者拼接在一起就形成下图所示结构:
至此马老师的练死劲儿到此结束 : 》