ssd 原理及代码实现详解

date
May 30, 2021
Last edited time
May 1, 2022 03:38 PM
status
Published
slug
ssd-note
tags
DL
CV
summary
关于SSD网络的一些笔记
type
Post
origin
Field
Plat
本文由 简悦 SimpRead 转码
参考链接

序言

ssd 由三部分组成:
  • base
  • extra
  • predict base 原论文里用的是 vgg16 去掉全连接层. base + extra 完成特征提取的功能. 得到不同 size 的 feature map, 基于这些 feature maps, 我们再用不同的卷积核去卷积, 分别完成类别预测和坐标预测.
    • notion image

基础特征提取网络

特征提取网络由两部分组成
  • vgg16
  • extra layer

vgg16 变种

vgg16 结构:
notion image
将 vgg16 的全连接层用卷积层换掉.
ssd.py 中
定义了每一层的卷积核的数量. 其中’M’,’C’均代表 maxpool 池化层. 只是’C’会使用 ceil instead of floor to compute the output shape. 参见 https://pytorch.org/docs/stable/nn.html#maxpool2d
这样就形成了一个基础的特征提取网络. 前面的部分和 vgg16 一样的, 全连接层换成了 conv6+relu+conv7+relu.

extra layer

在前面得到的输出的基础上, 继续做卷积, 以得到更多不同尺寸的 feature map.
notion image

代码实现

如果是’S’的话, 代表用的卷积核为 3 x 3, 否则为 1 x 1, 卷积核的数量为’S’下一个的数字.
这样的话, 我们就构建出了 extra layers.

多尺度检测 multibox

我们已经得到了很多 layer 的输出 (称其为 feature map).size 有大有小. 那么现在我们就对某些层(conv4_3,conv7,conv8_2,conv9_2,conv10_2,conv11_2) 的 feature map 再做卷积, 得到类别和位置信息. 分别用 2 组 3 x 3 的卷积核去做卷积, 一个负责预测类别, 一个负责预测位置. 卷积核的个数分别为 boxnum x clasess_num, boxnum x 4(坐标由 4 个参数, 中心坐标, box 宽高即可确定).
即在 m x m 的 feature map 上做卷积我们会得到一个 m x m x (boxnum x clasess_num) 和一个 m x m x (boxnum x 4) 的 tensor. 分别用以计算概率和框的位置.
classes_num 的大小等于分类数量+1, 因为多了一个背景分类

代码实现

其中每一个 feature map 预测几个 box 由下面变量给出.
在哪些 layer 的 feature map 上做预测, 根据论文里是固定的, 参见开头的 ssd 结构图. 反映到代码里则为
notion image
中的 conv4_3,conv7,conv8_2,conv9_2,conv10_2,conv11_2 这六个 layer 的 feature map.

网络输出分析

location 的维度为 , 其中每个预测框输出包含
先验框(Anchor Box) 的位置表示为 , 那么预测出来的真实边界框为
上述获得边界框的值的过程, 我们称之为编码. 而解码就是将其倒过来操作, 从预测值 计算出边界框的真实位置
在SSD源码当中, 还有trick, 那就是使用variance超参数来调整检测值. 这时的解码方式为
与yolo网络所不同的是,SSD网络生成的Anchor Box已经包含了坐标信息。因此,在计算真实预测框位置的时候,不需要额外生成偏移量 。而在yolo网络当中,需要给计算出的grid内相对位置加上一个grid左上角格点的坐标进行偏移。
 

先验框生成

你可以称之为 priorbox/default box/anchor box 都是一个意思. 我们先来讲先验框的原理. 这个其实类似 yolov3 中的 anchor box, 我们基于这些 shape 的 box 去做预测.
priorbox 和不同的 feature map 上做预测都是为了解决对不同尺寸的物体的检测问题. 不同的 feature map 负责不同尺寸的目标. 同时每一个 feature map cell 又负责该尺寸的不同宽高比的目标.
notion image
首先, 不同的 feature_map 负责不同的尺寸.
表示比例的最小值与最大值, 论文当中选择的是
表示特征图的个数-1, 因为第一个特征图要单独设置
对于第一个特征图,其先验框的尺度比例一般设置为 ,那么尺度为
带入可得
对于长宽比, 一般选择
即可计算先验框的宽度与高度
 
从而我们得到了这一个 feature map 负责预测的不同形状的 box
如图:
notion image
那么对于 conv4_3 这个 layer 而言的话, 我们设定的 deafault box 数量是 4, 于是我们最终就有 38 x 38 x 4 个 box. 我们在这些 box 的基础上去预测我们的 box.
我们对不同层的 default box 的数量设定是 (4, 6, 6, 6, 4, 4), 所以我们最终总共预测出 个 box.
实际调参的重点也就是这些 default box 的调整, 要尽量使其贴合你自己要检测的目标., 类似于 yolov3 中调参调整 anchor 的大小.

代码实现

prior_box.py 中定义了 PriorBox 类, forward 函数实现 default box 的计算.
其中
用以计算每一个 feature map 的 default box. 这里配置文件的定义让人稍微有点糊涂. min_size/max_size 都是用于宽高比为 1 的 box 的预测.[2] 用于预测宽高比为 2:1 和 1:2 的 box.
比如对 38 x 38 这个 feature map 的第一个 cell, 共计算出 4 个 default box. 前两个参数是 box 中点, 后面是宽, 高. 都是相对原图的比例.

预测框生成

feature_map 卷积后的 tensor 含义

每一个 feature_map 卷积后可得一个 m x m x 4tensor. 其中 4 是 , 这时候我们需要用这些数在 default box 的基础上去得到我们的预测框的坐标. 可以认为神经网络预测得到的是相对参考框的偏移. 这也是所谓的把坐标预测当做回归问题的含义.
其中 p_* 代表的是 default box. b_* 才是我们最终预测的 box 的坐标.
这时候我们得到了很多很多 (8732) 个 box. 我们要从这些 box 中筛选出我们最终给出的 box. 伪代码为

代码实现

detection.py
具体的核心逻辑在 box_utils.py
  • decode 用于根据卷积结果计算 box 坐标
这里做了一个 center_x,center_y, w, h -> xmin, ymin, xmax, ymax 的转换.
返回的已经是 (xmin, ymin, xmax, ymax) 的形式来表示 box 了.
  • nms 如果两个框的 overlap 超过 0.5, 则认为框的是同一个物体, 只保留概率更高的框
以上就是有关 ssd 网络结构以及每一层的输出的含义. 这些已经足够我们了解推理过程了. 即给定一个图, 模型如何预测出 box 的位置. 后面我们将继续关注训练的过程.

loss 计算

第一个要解决的问题就是 box 匹配的问题. 即每一次训练, 怎样的预测框算是预测对了? 我们需要计算这些模型认为的预测对了的 box 和真正的 ground truth box 之间的差异.
notion image
如上所示, 猫的 ground truth box 匹配了 2 个 default box, 狗的 ground truth box 匹配了 1 个 default box.

匹配策略

notion image
匹配策略是
  • 把 gt box 朝着 prior box 做匹配, 和 gt box 的 IOU 最高的 prior box 被选为正样本
  • 任意和 gt box 的 IOU 大于 0.5 的也被选为正样本
    • 有一个问题困扰了我好久,第二步不是包含第一步吗, 直到某天豁然开朗,可能所有的 prior box 与 gt box 的 iou 都 < 阈值,第一步就是为了保证至少有一个 prior box 与 gt box 对应
box_utils.py
这里的逻辑实际上是有点绕的. 给个具体的例子会更好滴帮助你理解. 我们假设一张图片里有 2 个 object. 那就有 2 个 gt box, 假设计算出 3 个 (实际是 8732 个)prior box. 计算每个 gt box 和每个 prior box 的 iou 即得到一个两行三列的 overlaps.
notion image
至此, 我们得到了 matches, 即对每一个 prior box 都找到了其对应的 gt box. 也得到了 conf. 即 prior box 归属的类别. 如果 iou 过低的, 类别就被标记为 background.
接下来
我们比较 prior box 和其对应的 gt box 的差异. 注意这里的 matched 的格式是 (lefttop_x,lefttop_y,rightbottom_x,rightbottom_y). 所以这里得到的其实是 gt box 和 prior box 之间的偏移.

计算 loss

notion image
 
损失函数为位置误差与置信度误差的加权和
  1. 对于位置误差
    1. 是先验框的正样本数量, 时代表第 个先验框与第 个ground truth 匹配, 且其类别为
  1. 对于置信度误差

Hard negative mining

在匹配 default box 和 gt box 以后, 必然是有大量的 default box 是没有匹配上的. 即只有少量正样本, 有大量负样本. 对每个 default box, 我们按照 confidence loss 从高到低排序. 我们只取排在前列的一些 default box 去计算 loss, 使得负样本:正样本在 3:1. 这样可以使得模型更加快地优化, 训练更稳定.
关于目标检测中的样本不平衡可以参考 https://zhuanlan.zhihu.com/p/60612064
简单滴说就是, 负样本使得我们学到背景信息, 正样本使得我们学到目标信息. 所以二者都需要, 并且保持一个合适比例. 论文里用的是 3:1. 对应代码即 MultiBoxLoss.negpos_ratio
这时候的 loss 还不是网络的 conf loss, 并不是论文里的 l_conf.
这里用到了一个 trick. 参考 https://github.com/amdegroot/ssd.pytorch/issues/203https://stackoverflow.com/questions/42599498/numercially-stable-softmax 为了避免 e 的 n 次幂太大或者太小而无法计算, 常常在计算 softmax 时使用这个 trick.
这个函数严重影响了我对 loss_c 的理解, 实际上, 你可以把上述函数中的 x_max 移除. 那这个函数 那么 loss_c 就变为了
就好理解多了.
conf_t 的列方向是相应的 label 的 index. batch_conf.gather(1, conf_t.view(-1, 1))得到一个 [batch*8732,1] 的 tensor, 即只保留 prior box 对应的 label 的概率预测信息.
那总体的 loss 即为所有类别的 loss 之和减去这个 prior box 应该负责的 label 的 loss.
得到 loss_c 以后, 我们去得到正样本 / 负样本的 index
至此, 我们就得到了正负样本的下标. 接下来就可以计算预测值与真值的差异了.
用交叉熵衡量 loss. 最后除以正样本的数量, 做归一化处理.
notion image
在计算 loss 前, 不需要手动 softmax 转换成概率值了.

训练

前面已经实现了网络结构创建, loss 计算. 接下来就可以实现训练了. 实现在 train.py 精简后的主要逻辑如下:
  • 定义网络结构
  • 定义损失函数及反向传播求梯度方法
  • 加载训练集
  • 前向传播得到预测值
  • 计算 loss
  • 反向传播, 更新网络权重参数
涉及到的部分 torch 中函数用法参考:https://www.cnblogs.com/sdu20112013/p/11731741.html

© Lazurite 2021 - 2024