【SSD论文解读】 模型部分
- 一、骨干网络
- 1、原始的骨干网络——VGG16
- 2、SSD对VGG16的改进
- 3、代码
- 二、Extra Feature Layers
- 1、使用多尺度的特征图进行检测
- 代码
- 2、使用卷积预测器产生预测边界框(相比于YOLO的全连接层)
- 代码
- 3、每个特征图生成一组固定的预测边界框
- 代码
- 三、SSD网络结构
- 代码
一、骨干网络
1、原始的骨干网络——VGG16
VGGNet是继AlexNet后的一个隐含层更多的深度卷积神经网络。VGGNet结构根据层数的不同有不同的版本,常用结构是VGG16和VGG19。SSD使用的是VGG16。
VGG16有13个卷积层,5个最大池化层以及3个全连接层。
- 卷积层卷积核的大小为3×3,步长为1,通过卷积层可以实现通道数的增加
- 池化层大小为2×2,步长为2,作用是降低特征图尺寸并能提高网络抗干扰能力
- 3个全连接层中的前两层通道数为4096,第三层通道数为1000,代表1000个标签类别
所有隐藏层后都带有ReLU非线性激活函数。VGG16网络结构图如下图:
VGG16网络结构图
2、SSD对VGG16的改进
【论文内容】
- 为了能够与在骨干网络之后增加特征提取层,将全连接层fc6和fc7转换为卷积层conv6和conv7,并对fc6和fc7的参数进行二次采样,并移除了fc8层;
- 将池化层pool5从2×2大小,步长为2更改为3×3大小,步长为1,并使用atrous算法来填充“漏洞”;
- 由于SSD网络结构移除了VGG16的全连接层,因此防止过拟合的dropout层也被移除;
- 由于conv4_3与其他特征层相比具有不同的特征比例,因此使用L2归一化将特征图中每个位置的特征比例进行缩放,并在反向传播过程中学习该比例。
L1 norm、L2 norm:
L1 norm是绝对值相加,又称曼哈顿距离
L2 norm是平方和,即欧几里德距离之和
L1归一化和L2归一化范数的详解和区别
每一层详细的更改,如图:来源【SSD算法】史上最全代码解析-核心篇
3、代码
#数字代表卷积层的通道数,'M'代表普通池化层,'C'代表池化层但ceil_mode=True
cfg= {
'300': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',
512, 512, 512],
'512': [],
}
# vgg网络初始通道数为“原图像通道数”:3
i = 3
def vgg(cfg, i, batch_norm=False):
layers = []
in_channels = i
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
elif v == 'C':
layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)
conv7 = nn.Conv2d(1024, 1024, kernel_size=1)
layers += [pool5, conv6,
nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]
return layers
输出骨干网的网络结构,如下所示:
Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=True)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace)
(30): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
(31): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(6, 6), dilation=(6, 6))
(32): ReLU(inplace)
(33): Conv2d(1024, 1024, kernel_size=(1, 1), stride=(1, 1))
(34): ReLU(inplace)
)
二、Extra Feature Layers
相比于YOLO和Faster-RCNN,SSD网络结构也提出了一些新的策略
- 使用多尺度的特征图进行检测
- 使用卷积预测器产生预测边界框
- 每个特征图生成一组固定的预测边界框
1、使用多尺度的特征图进行检测
【论文内容】
SSD算法在截断后的VGG16之后,增加了大小逐层减小的卷积层进行特征提取,然后在不同大小的特征图上进行检测,从而允许在多个尺度上进行检测。
因为不同大小的特征图感受野不同,因此可以检测不同大小的物体:
- 较大的特征图,感受野较小,适合检测相对较小的物体。如下图中,8×8的特征图中蓝色框更适合检测猫
- 较小的特征图,感受野较大,适合检测相对较大的物体。如下图中,4×4的特征图中红色框更适合检测狗。
图片的真实值(左),8×8特征图(中),4×4特征图(右)
代码
#数字表示卷积层的通道数,'S'表示采用stride=2, padding=1的池化层
extras = {
'300': [256, 'S', 512, 128, 'S', 256, 128, 256, 128, 256],
'512': [],
}
# Extra Feature Layers 的输入通道数,为vgg网络的输出通道数,即conv7的输出通道数:1024
i = 1024
def add_extras(cfg, i, batch_norm=False):
# Extra layers added to VGG for feature scaling
layers = []
in_channels = i
flag = False
for k, v in enumerate(cfg):
if in_channels != 'S':
if v == 'S':
layers += [nn.Conv2d(in_channels, cfg[k + 1],
kernel_size=(1, 3)[flag], stride=2, padding=1)]
else:
layers += [nn.Conv2d(in_channels, v, kernel_size=(1, 3)[flag])]
flag = not flag
in_channels = v
return layers
输出:
Sequential(
(0): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
(1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
(2): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1))
(3): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
(4): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1))
(5): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1))
(6): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1))
(7): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1))
)
2、使用卷积预测器产生预测边界框(相比于YOLO的全连接层)
【论文内容】
YOLO中使用全连接层来进行分类和回归,而SSD将其改为了卷积层。SSD并不是对所有的卷积层都使用 “卷积预测器”,而是选择了其中的6层(conv4_3、conv7、conv8_2、conv9_2、conv10_2、conv11_2)进行特征提取。
对于 的特征图,用 2个
- 一个输出分类用的置信度损失(confidence loss),每个默认边界框生成21个类别置信度损失
代码中主要使用 Pascal VOC 数据集进行训练、验证、测试。有20种分类;另外还有1种分类,为背景;所以总的类别数为21。 - 一个输出回归用的定位损失(localization loss),每个默认边界框生成4个坐标值。
代码
# 函数调用:
multibox(vgg(base[str(size)], 3),
add_extras(extras[str(size)], 1024),
mbox[str(size)], num_classes)
# 卷积层 conv4_3、conv7、conv8_2、conv9_2、conv10_2、conv11_2 每层每个 cell 生成 default boxes 的个数依次为:
mbox = {
'300': [4, 6, 6, 6, 4, 4], # number of boxes per feature map location
'512': [],
}
# SSD300,原图像大小为 300×300
size=300
num_classes=21
def multibox(vgg, extra_layers, cfg, num_classes):
loc_layers = []
conf_layers = []
# 骨干网vgg16共有34个元素(每个conv层后面都有一个relu)
# 所以第21、-2个元素分别为:可以参考“一、骨干网络 3、代码”的输出
# (21): Conv2d(512, 512, kernel_ size=(3, 3),stride=(1, 1),padding=(1, 1))
# (-2)即(33): Conv2d(1024, 1024,kernel_ size=(1, 1), stride=(1, 1))
vgg_source = [21, -2]
for k, v in enumerate(vgg_source):
loc_layers += [nn.Conv2d(vgg[v].out_channels,
cfg[k] * 4, kernel_size=3, padding=1)]
conf_layers += [nn.Conv2d(vgg[v].out_channels,
cfg[k] * num_classes, kernel_size=3, padding=1)]
for k, v in enumerate(extra_layers[1::2], 2):
loc_layers += [nn.Conv2d(v.out_channels, cfg[k]
* 4, kernel_size=3, padding=1)]
conf_layers += [nn.Conv2d(v.out_channels, cfg[k]
* num_classes, kernel_size=3, padding=1)]
return vgg, extra_layers, (loc_layers, conf_layers)
3、每个特征图生成一组固定的预测边界框
【论文内容】
SSD 对多尺度特征图中的 每个 产生固定数量的 default bounding boxes:
- 每个特征图生成 default boxes 的个数:,总数量为8732个
- 每个 default box 的输出个数:有 ,共25个输出
:特征图大小; :每个特征图产生 default box 的个数,取值如下表所示:(“三、SSD网络结构” 中的结构图,每一条 conv 层与 detections 的连线上标明了 的取值。如:Classifier : Conv: 3x3x(4
x(Classes+4)))还请路过的小伙伴指教,Classifier : Conv: 3x3x(4x(Classes+4)))中的 3x3 是什么?
每个特征图产生 default bounding boxes 的具体数量如下表所示:
层 | |
conv4_3 | 4×38×38 |
conv7_2 | 6×19×19 |
conv8_2 | 6×10×10 |
conv9_2 | 4×5×5 |
conv10_2 | 4×3×3 |
conv11_2 | 4×1×1 |
代码
这部分的代码分两部分实现:
- 每个特征图生成 default boxes 的个数:由于这部分涉及到论文中【训练】部分,因此在下一篇文章中介绍具体实现。
- 每个 default box 的输出个数:在
ssd.py/multibox()
函数中实现
loc_layers += [nn.Conv2d(vgg[v].out_channels, cfg[k] * 4, kernel_size=3, padding=1)]
conf_layers += [nn.Conv2d(vgg[v].out_channels, cfg[k] * num_classes, kernel_size=3, padding=1)]
三、SSD网络结构
输入:
网络结构:如上图
输出:满足条件的目标检测框、目标的label、分数,具体格式为:[image_ id, label, confidence, xmin, ymin, xmax, ymax]
模型详细层结构:
红色方框:骨干网络、Extra Feature Layers层
黄色条形:SSD对VGG16的修改
粉色条形:生成 default box 的层
代码
def forward(self, x):
"""Applies network layers and ops on input image(s) x.
Args:
x: input image or batch of images. Shape: [batch,3,300,300].
Return:
Depending on phase:
test:
Variable(tensor) of output class label predictions,
confidence score, and corresponding location predictions for
each object detected. Shape: [batch,topk,7]
train:
list of concat outputs from:
1: confidence layers, Shape: [batch*num_priors,num_classes]
2: localization layers, Shape: [batch,num_priors*4]
3: priorbox layers, Shape: [2,num_priors*4]
"""
sources = list()
loc = list()
conf = list()
# apply vgg up to conv4_3 relu
for k in range(23):
x = self.vgg[k](x)
s = self.L2Norm(x)
sources.append(s)
# apply vgg up to fc7
for k in range(23, len(self.vgg)):
x = self.vgg[k](x)
sources.append(x)
# apply extra layers and cache source layer outputs
for k, v in enumerate(self.extras):
x = F.relu(v(x), inplace=True)
if k % 2 == 1:
sources.append(x)
# apply multibox head to source layers
for (x, l, c) in zip(sources, self.loc, self.conf):
loc.append(l(x).permute(0, 2, 3, 1).contiguous())
conf.append(c(x).permute(0, 2, 3, 1).contiguous())
loc = torch.cat([o.view(o.size(0), -1) for o in loc], 1)
conf = torch.cat([o.view(o.size(0), -1) for o in conf], 1)
if self.phase == "test":
output = self.detect(
loc.view(loc.size(0), -1, 4), # loc preds
self.softmax(conf.view(conf.size(0), -1,
self.num_classes)), # conf preds
self.priors.type(type(x.data)) # default boxes
)
else:
output = (
loc.view(loc.size(0), -1, 4),
conf.view(conf.size(0), -1, self.num_classes),
self.priors
)
return output