论文名称

SSD:Single Shot MultiBox Detector

pytorch code

A PyTorch Implementation of Single Shot MultiBox Detector

作者及单位

liu wei & google

    SSDnet是2016年提出的一种单目标检测网络,属于one stage 的目标检测,个人认为SSDnet与yolov3属于相爱相杀,由于在yolov3 之前提出,发现在yolov3中有很多地方与SSDnet相似的地方,本解读系列主要是从代码的角度出发进行ssdnet的深层解析,且与论文结合加深理解。

                                               基础篇  网络架构的简单介绍

  paper中的backbone 是采用vgg16的网络来提取基础特征,以论文中首选的SSDnet-300为例,下图的虚线框是vgg16网络中的卷积层结构,剔除了conv5_3后续的所有层,并单独提出vgg16中的conv4_3卷积层,作为后续得到38*38尺寸feature map的待卷积层。


SSD网络架构 ssdnet_论文解析

图1.ssdnet 网络结构图

   先放一段代码,进行简单说明,便于深度理解,ssd.py 126-146行。

126 def vgg(cfg, i, batch_norm=False):
127    layers = []
128    in_channels = i
129    for v in cfg:
130        if v == 'M':
131            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
132        elif v == 'C':
133            layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
134        else:
135            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
136            if batch_norm:
137                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
138            else:
139                layers += [conv2d, nn.ReLU(inplace=True)]
140            in_channels = v
141    pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
142    conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)
143    conv7 = nn.Conv2d(1024, 1024, kernel_size=1)
144    layers += [pool5, conv6,
145               nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]
146    return layers

vgg的实参给定的为:  

[64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',512,512,512]

这在ssd.py代码的183-185行中有说明。

下图是VGG 原论文中table1 vgg16的结构图,为便于核对,我也搬过来了,这里的‘C’是池化中的天花板模式,300/4=75,由于75/2=37.5,向上约为38*38。在conv5_3后面增加了pool5,conv6,conv7.这里的conv6、conv7分别替代了原vgg16中的fc6、fc7.

 


SSD网络架构 ssdnet_2d_02

图2,vgg16 网络参数

上面应该理清了vgg16的基础层,现在来说额外增加层,是跟在conv7后面的,参照图1看到,跟在conv7后续的conv8_2、conv9_2,conv10_2,就是额外增加层,代码来了! 来自ssd.py 第149-163行。

149 def add_extras(cfg, i, batch_norm=False):
150    # Extra layers added to VGG for feature scaling
151    layers = []
152    in_channels = i
153    flag = False
154    for k, v in enumerate(cfg):
155        if in_channels != 'S':
156            if v == 'S':
157                layers += [nn.Conv2d(in_channels, cfg[k + 1],
158                           kernel_size=(1, 3)[flag], stride=2, padding=1)]
159            else:
160                layers += [nn.Conv2d(in_channels, v, kernel_size=(1, 3)[flag])]
161            flag = not flag
162        in_channels = v
163     return layers

还是继续将cfg 参数摆出来,这里来自第192-193行。

      [256, 'S', 512, 128, 'S', 256, 128, 256, 128, 256]

补充语句,i=1024,是conv7卷积层的层数。我想在这里稍微解释一下代码的运行规律,v遍历cfg参数的所有参数,当k=0,v=256时, 执行第157行代码,此时flag=False,进行卷积操作,conv:1*1*256, 当k=1,v='S'时,此时flag更新为True,in_channels=256,执行157-158行代码,,进行卷积操作,conv:3*3*cfg[2]==>conv:3*3*512,且stride=2.    上一部分完成后得到了conv8_2,后续操作一样近似,分别得到了conv9_2、conv10_2、conv11_2.将所有操作层依旧放在add_extras 的layers中,供后续forward调用。

上述就弄清楚了额外增加层,现在就得说道比较最后的多尺度目标框层(可以看做我自己取得名字,可能不大准) ,为什么叫多尺度目标框层呢,这一部分,我们要像yolov3一样得到输出层13*13*255,26*26*255,52*52*255,这个是yolov3的输出,不要混淆,而ssdnet的尺度有6种,比yolov3多一倍,这也是ssdnet3在小目标物体检测性能好的原因之一,如图3是原paper中的图,


SSD网络架构 ssdnet_SSD网络架构_03

图3多尺度目标框下的先验框

图(b)、图(c)是两种尺寸下的先验框,论文设置每个特征图中的单元格产生几个预选框,这里和yolov3是很像的,还记得吗?255=85*3,如何产生在下一篇具体阐述,为产生对于输出,需要进行一次卷积操作,而ssdnet 每组不同尺度的特征图共产生两种不同数量num_prior的预选框,分别是4、6,根据图1,6组不同尺度的特征图分别为38*38、19*19、10*10、5*5、3*3、1*1,为产生与物体检测相匹配的特征(需要预测类别与坐标信息),分别通过一次卷积操作生成维度为num_prior*classes*4的特征图。代码来了...来自 ssd.py 的第166-180行。

166 def multibox(vgg, extra_layers, cfg, num_classes):
167    loc_layers = []
168    conf_layers = []
169    vgg_source = [21, -2]
170    for k, v in enumerate(vgg_source):
171        loc_layers += [nn.Conv2d(vgg[v].out_channels,
172                                 cfg[k] * 4, kernel_size=3, padding=1)]
173        conf_layers += [nn.Conv2d(vgg[v].out_channels,
174                        cfg[k] * num_classes, kernel_size=3, padding=1)]
175    for k, v in enumerate(extra_layers[1::2], 2):
176        loc_layers += [nn.Conv2d(v.out_channels, cfg[k]
177                                 * 4, kernel_size=3, padding=1)]
178        conf_layers += [nn.Conv2d(v.out_channels, cfg[k]
179                                  * num_classes, kernel_size=3, padding=1)]
180    return vgg, extra_layers, (loc_layers, conf_layers)

cfg 的参数为

[4, 6, 6, 6, 4, 4]

这里的代码解释一下,有点难以理解。首先,此函数是输出预测检测结果loc_layer和conf_layers ,它将置信度(类别)预测和坐标预测分开了。从170-173代码开始解释,这部分是由vgg基础层产生预测信息的,当k=0,v=21时,vgg[21]是conv4_3层输出的卷积层网络,通过卷积操作 3*3*(4*4)、3*3*(4*classes)分两部分预测得到loc_layers,conf_layers,k=1,v=-2是conv7层输出的卷积层网络,通过一样的卷积操作得到,需要注意的是此部分每个特征图单元生成6个预测框。从175-179行是额外增加层产生的预测信息,回顾一下,extra_layers中的layers结构组成部分[conv8_1,conv8_2,conv9_1,conv9_2,conv10_1,conv10_2,conv11_1,conv11_2],因为额外增加层的每一组要经过1*1,3*3两次卷积操作完成,这里放一个代码解释175部分,注意,真实的extra_layers中的元素不是str,完全是方便理解。下图一目了然,k=2,3,4,5,cfg 的参数也就是6,6,4,4,分别对conv8_2、conv9_2、conv_10_2、conv11_2进行卷积。


SSD网络架构 ssdnet_SSD网络架构_04

图4,辅助代码

    上述的所有都是Pytorch 层的定义,从理论网络的介绍角度来说吗,网络细节都已经说的很清楚了,现在就是需要对所有层forward进行串起来,ssd.py 代码中的第10行-121行,sources中是x经过VGG4_3,VGG7,Conv8_2,Conv9_2,Conv10_2,Conv11_2得到的输出,再通过一个zip 将每组送入对应的loc,和conf层得到最后的loc和conf预测值,loc的维度(batch, 8732,4),conf的维度(batch,8732,num_classes),priors的维度(8732,4)。

   下一篇我会继续讲解默认框的生成,也就是priors,先验框的匹配策略。