【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网络结构图如下图:

深度学习提取特征需要正常图像吗_ssd


VGG16网络结构图

2、SSD对VGG16的改进

【论文内容】

深度学习提取特征需要正常图像吗_深度学习提取特征需要正常图像吗_02


深度学习提取特征需要正常图像吗_算法_03

  • 为了能够与在骨干网络之后增加特征提取层,将全连接层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算法】史上最全代码解析-核心篇

深度学习提取特征需要正常图像吗_深度学习提取特征需要正常图像吗_04

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_05


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的全连接层)

【论文内容】

深度学习提取特征需要正常图像吗_ssd_06

YOLO中使用全连接层来进行分类和回归,而SSD将其改为了卷积层。SSD并不是对所有的卷积层都使用 “卷积预测器”,而是选择了其中的6层(conv4_3、conv7、conv8_2、conv9_2、conv10_2、conv11_2)进行特征提取。

对于 深度学习提取特征需要正常图像吗_ide_07 的特征图,用 2个 深度学习提取特征需要正常图像吗_2d_08

  • 一个输出分类用的置信度损失(confidence loss),每个默认边界框生成21个类别置信度损失
    代码中主要使用 Pascal VOC 数据集进行训练、验证、测试。有20种分类;另外还有1种分类,为背景;所以总的类别数为21。
  • 一个输出回归用的定位损失(localization loss),每个默认边界框生成4个坐标值深度学习提取特征需要正常图像吗_ide_09
代码
# 函数调用:
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、每个特征图生成一组固定的预测边界框

【论文内容】

深度学习提取特征需要正常图像吗_深度学习提取特征需要正常图像吗_10


SSD 对多尺度特征图中的 每个 深度学习提取特征需要正常图像吗_2d_11 产生固定数量的 default bounding boxes:

  • 每个特征图生成 default boxes 的个数:深度学习提取特征需要正常图像吗_深度学习提取特征需要正常图像吗_12总数量为8732个
  • 每个 default box 的输出个数:有 深度学习提取特征需要正常图像吗_ide_13共25个输出

深度学习提取特征需要正常图像吗_ide_14:特征图大小;深度学习提取特征需要正常图像吗_ssd_15 :每个特征图产生 default box 的个数,取值如下表所示:(“三、SSD网络结构” 中的结构图,每一条 conv 层与 detections 的连线上标明了 深度学习提取特征需要正常图像吗_ssd_15 的取值。如:Classifier : Conv: 3x3x(4x(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

代码

这部分的代码分两部分实现:

  1. 每个特征图生成 default boxes 的个数:由于这部分涉及到论文中【训练】部分,因此在下一篇文章中介绍具体实现。
  2. 每个 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网络结构

深度学习提取特征需要正常图像吗_ide_18

输入深度学习提取特征需要正常图像吗_算法_19
网络结构:如上图
输出:满足条件的目标检测框、目标的label、分数,具体格式为:[image_ id, label, confidence, xmin, ymin, xmax, ymax]


模型详细层结构:
红色方框:骨干网络、Extra Feature Layers层
黄色条形:SSD对VGG16的修改
粉色条形:生成 default box 的层

深度学习提取特征需要正常图像吗_算法_20

代码
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