源码地址:https://github.com/weiliu89/caffe/tree/ssd
本文详细版本
YOLO V3 学习笔记
一. 算法概述
本文提出的SSD算法是一种直接预测目标类别和 bounding box 的多目标检测算法。与 faster rcnn 相比,该算法没有生成 proposal 的过程,这就极大提高了检测速度。针对不同大小的目标检测,传统的做法是先将图像转换成不同大小(图像金字塔),然后分别检测,最后将结果综合起来(NMS)。而SSD算法则利用不同卷积层的 feature map 进行综合也能达到同样的效果。算法的主网络结构是VGG16,将最后两个全连接层改成卷积层,并随后增加了4个卷积层来构造网络结构。对其中 5 种不同尺度的feature map 输出分别用两个不同的 3×3 的卷积核进行卷积,一个输出分类用的 confidence,每个 default box 生成21个类别confidence;一个输出回归用的 localization,每个 default box 生成4个坐标值(x, y, w, h)。此外,这 5 个 feature map 还经过 PriorBox 层生成 prior box(相当于 faster rcnn 的 Anchor)。上述5个 feature map 中每一层的 default box的数量是给定的(个)。最后将前面三个计算结果分别合并然后传给loss层。
二. Default box
文章的核心之一是作者同时采用 lower 和 upper 的 feature map 做检测。如图Fig 1 所示,这里假定有 8×8 和 4×4 两种不同尺度的 feature map。第一个概念是feature map cell,feature map cell 是指 feature map 中每一个小格子,如图中分别有 64 和 16 个 cell。另外有一个概念:default box,是指在feature map的每个小格(cell)上都有一系列固定大小的box,如下图有4个(下图中的虚线框,仔细看格子的中间有比格子还小的一个box)。假设每个 feature map cell 有 k 个 default box,那么对于每个default box都需要预测 c 个类别 score 和 4 个 offset,那么如果一个 feature map的大小是 m×n,也就是有 m*n 个 feature map cell,那么这个 feature map就一共有(c+4)*k * m*n 个输出。这些输出个数的含义是:采用3×3的卷积核对该层的feature map卷积时卷积核的个数,包含两部分:数量 c*k*m*n 是 confidence 输出,表示每个 default box 的 是某一类别的 confidence;数量4*k*m*n是 localization 输出,表示每个default box 回归后的坐标)。训练中还有一个东西:prior box,是指实际中选择的 default box(你可以认为 default box 是抽象类,而 prior box 是具体实例)。训练中对于每个图像样本,需要先将 prior box 与 ground truth box 做匹配,匹配成功说明这个 prior box 所包含的是个目标,但离完整目标的 ground truth box 还有段距离,训练的目的是保证default box 的分类 confidence 的同时将 prior box 尽可能回归到 ground truth box。 举个列子:假设一个训练样本中有2个ground truth box,所有的feature map中获取的prior box一共有8732个。那个可能分别有10、20个prior box能分别与这2个ground truth box匹配上,匹配成功的将会作为正样本参与分类和回归训练,而未能匹配的则只会参加分类(负样本)训练。训练的损失包含定位损失和回归损失两部分。
作者的实验表明default box的shape数量越多,效果越好。
这里用到的 default box 和 Faster RCNN 中的 Anchor 很像,在 Faster RCNN 中 anchor 只用在最后一个卷积层,但是在本文中,default box 是应用在多个不同层的 feature map 上,这一点倒是很像 FPN 了。
那么default box的 scale(大小)和aspect ratio(横纵比)要怎么定呢?假设我们用 m 个 feature maps 做预测,那么对于每个featuer map而言其 default box 的 scale 是按以下公式计算的:
$\vee$
$S_k=S_{min} + \frac{S_{max} - S_{min}}{m-1}(k-1), k\in{[1, m]}$
这里smin是0.2,表示最底层的scale是0.2;smax是0.9,表示最高层的scale是0.9。
至于aspect ratio,用$a_r$表示为下式:注意这里一共有5种aspect ratio
$a_r = \{1, 2, 3, 1/2, 1/3\}$
因此每个default box的宽的计算公式为:
$w_k^a=s_k\sqrt{a_r}$
高的计算公式为:(很容易理解宽和高的乘积是 scale 的平方)
$h_k^a=s_k/\sqrt{a_r}$
另外当 aspect ratio 为1时,作者还增加一种 scale 的 default box:
$s_k^{'}=\sqrt{s_{k}s_{k+1}}$
因此,对于每个 feature map cell 而言,一共有 6 种 default box。
可以看出这种 default box 在不同的 feature 层有不同的 scale,在同一个 feature 层又有不同的 aspect ratio,因此基本上可以覆盖输入图像中的各种形状和大小的 object!
(训练自己的样本的时候可以在 FindMatch() 之后检查是否能覆盖了所有的 ground truth box,实际上是全覆盖了,因为会至少找一个最大匹配)
源代码中的 ssd_pascal.py 设计了上面几个参数值,caffe 源码 prior_box_layer.cpp 中Forward_cpu()实现。
最后会得到(38*38*4 + 19*19*6 + 10*10*6 + 5*5*6 + 3*3*4 + 1*1*4)= 8732 个 prior box。
Fig.2 SSD 框架
三. 正负样本
将 prior box 和 grount truth box 按照 IOU(JaccardOverlap)进行匹配,匹配成功则这个 prior box 就是 positive example(正样本),如果匹配不上,就是 negative example(负样本),显然这样产生的负样本的数量要远远多于正样本。这里默认做了难例挖掘:简单描述起来就是,将所有的匹配不上的 negative prior box 按照前向 loss 进行排序,选择最高的 num_sel 个 prior box 序号集合作为最终的负样本集。这里就可以利用 num_sel 来控制最后正、负样本的比例在 1 : 3 左右。
Fig.3 positive and negtive sample VS ground_truth box
1.正样本获得
我们已经在图上画出了prior box,同时也有了 ground truth,那么下一步就是将 prior box 匹配到 ground truth上,这是在 src/caffe/utlis/bbox_util.cpp
的 FindMatches 以及子函数MatchBBox函数里完成的。具体的:先从 groudtruth box 出发,为每个 groudtruth box 找到最匹配的一个 prior box 放入候选正样本集;然后再尝试从剩下的每个 prior box 出发,寻找与 groundtruth box 满足 $IoU \ge 0.5$ 的最大匹配,如果找到了这样的一个匹配结果就放入候选正样本集。这个做的目的是保证每个 groundtruth box 都至少有一个匹配正样本。
2.负样本获
在生成一系列的 prior boxes 之后,会产生很多个符合 ground truth box 的 positive boxes(候选正样本集),但同时,不符合 ground truth boxes 也很多,而且这个 negative boxes(候选负样本集),远多于 positive boxes。这会造成 negative boxes、positive boxes 之间的不均衡。训练时难以收敛。
因此,本文采取,先将每一个物体位置上对应 predictions(prior boxes)loss 进行排序。 对于候选正样本集:选择 loss 最高的几个prior box集合与候选正样本集进行匹配(box索引同时存在于这两个集合里则匹配成功),匹配不成功则删除这个正样本(因为这个正样本不在难例里已经很接近ground truth box了,不需要再训练了);对于候选负样本集:选择 loss 最高的几个 prior box 与候选负样本集匹配,匹配成功则作为负样本。这就是一个难例挖掘的过程,举个例子,假设在这8732个prior box里,经过 FindMatches 后得到候选正样本 $P$ 个,候选负样本那就有 $8732-P$ 个。将 prior box 的 prediction loss 按照从大到小顺序排列后选择最高的 $M$ 个prior box。如果这 $P$ 个候选正样本里有 $a$ 个box不在这 $M$ 个prior box里,将这 $a$ 个box从候选正样本集中踢出去。如果这 $8732-P$ 个候选负样本集中包含的$ 8732-P$ 有 $b$ 个在这 $M$ 个prior box,则将这 $b$ 个候选负样本作为最终的负样本。总归一句话就是:选择 loss 值高的难例作为最后的负样本参与 loss 反传计算。SSD算法中通过这种方式来保证 positives、negatives 的比例。实际代码中有三种负样本挖掘方式:
如果选择HARD_EXAMPLE方式(源于论文Training Region-based Object Detectors with Online Hard Example Mining),则默认$M = 64$,由于无法控制正样本数量,这种方式就有点类似于分类、回归按比重不同交替训练了。
如果选择MAX_NEGATIVE方式,则$M = P*neg\_pos\_ratio$,这里当$neg\_pos\_ratio = 3$的时候,就是论文中的正负样本比例1:3了。
enum MultiBoxLossParameter_MiningType {
MultiBoxLossParameter_MiningType_NONE = 0,
MultiBoxLossParameter_MiningType_MAX_NEGATIVE = 1,
MultiBoxLossParameter_MiningType_HARD_EXAMPLE = 2
};
3.Data augmentation
本文同时对训练数据做了 data augmentation,数据增广。
每一张训练图像,随机的进行如下几种选择:
- 使用原始的图像
- 随机采样多个 patch(CropImage),与物体之间最小的 jaccard overlap 为:0.1,0.3,0.5,0.7 与 0.9
采样的 patch 是原始图像大小比例是 [0.3,1.0],aspect ratio 在 0.5 或 2。
当 groundtruth box 的 中心(center)在采样的 patch 中且在采样的 patch中 groundtruth box面积大于0时,我们保留CropImage。
在这些采样步骤之后,每一个采样的 patch 被 resize 到固定的大小,并且以 0.5 的概率随机的 水平翻转(horizontally flipped,翻转不翻转看prototxt,默认不翻转)
这样一个样本被诸多batch_sampler采样器采样后会生成多个候选样本,然后从中随机选一个样本送人网络训练。
四. 网络结构
SSD的结构在VGG16网络的基础上进行修改,训练时同样为conv1_1,conv1_2,conv2_1,conv2_2,conv3_1,conv3_2,conv3_3,conv4_1,conv4_2,conv4_3,conv5_1,conv5_2,conv5_3(512),fc6经过3*3*1024的卷积(原来VGG16中的fc6是全连接层,这里变成卷积层,下面的fc7层同理),fc7经过1*1*1024的卷积,conv6_1,conv6_2(对应上图的conv8_2),conv7_1,conv7_2,conv,8_1,conv8_2,conv9_1,conv9_2,loss。然后一方面:针对conv4_3(4),fc7(6),conv6_2(6),conv7_2(6),conv8_2(4),conv9_2(4)(括号里数字是每一层选取的default box种类)中的每一个再分别采用两个3*3大小的卷积核进行卷积,这两个卷积核是并列的(括号里的数字代表prior box的数量,可以参考Caffe代码,所以上图中SSD结构的倒数第二列的数字8732表示的是所有prior box的数量,是这么来的38*38*+19*19*+10*10*+5*5*+3*3*+1*1*=8732),这两个3*3的卷积核一个是用来做localization的(回归用,如果prior box是6个,那么就有6*4=24个这样的卷积核,卷积后map的大小和卷积前一样,因为pad=1,下同),另一个是用来做confidence的(分类用,如果prior box是6个,VOC的object类别有20个,那么就有6*(20+1)=126个这样的卷积核)。如下图是conv6_2的localizaiton的3*3卷积核操作,卷积核个数是24(6*4=24,由于pad=1,所以卷积结果的map大小不变,下同):这里的permute层就是交换的作用,比如你卷积后的维度是32×24×19×19,那么经过交换层后就变成32×19×19×24,顺序变了而已。而flatten层的作用就是将32×19×19×24变成32*8664,32是batchsize的大小。另一方面结合conv4_3(4),fc7(6),conv6_2(6),conv7_2(6),conv8_2(4),conv9_2(4)中的每一个和数据层(ground truth boxes)经过priorBox层生成prior box。
经过上述两个操作后,对每一层feature的处理就结束了。对前面所列的5个卷积层输出都执行上述的操作后,就将得到的结果合并:采用Concat,类似googleNet的Inception操作,是通道合并而不是数值相加。
Fig.5 SSD 流程
损失函数方面:和Faster RCNN的基本一样,由分类和回归两部分组成,可以参考Faster RCNN,这里不细讲。总之,回归部分的loss是希望预测的box和prior box的差距尽可能跟ground truth和prior box的差距接近,这样预测的box就能尽量和ground truth一样。
上面得到的8732个目标框经过Jaccard Overlap筛选剩下几个了;其中不满足的框标记为负数,其余留下的标为正数框。紧随其后:
训练过程中的 prior boxes 和 ground truth boxes 的匹配,基本思路是:让每一个 prior box 回归并且到 ground truth box,这个过程的调控我们需要损失层的帮助,他会计算真实值和预测值之间的误差,从而指导学习的走向。
SSD 训练的目标函数(training objective)源自于 MultiBox 的目标函数,但是本文将其拓展,使其可以处理多个目标类别。具体过程是我们会让每一个 prior box 经过Jaccard系数计算和真实框的相似度,阈值只有大于 0.5 的才可以列为候选名单;假设选择出来的是N个匹配度高于百分之五十的框吧,我们令 i 表示第 i 个默认框,j 表示第 j 个真实框,p表示第p个类。那么$x_{ij}^p$表示 第 i 个 prior box 与 类别 p 的 第 j 个 ground truth box 相匹配的Jaccard系数,若不匹配的话,则$x_{ij}^p=0$。总的目标损失函数(objective loss function)就由 localization loss(loc) 与 confidence loss(conf) 的加权求和:
- N 是与 ground truth box 相匹配的 prior boxes 个数
- localization loss(loc) 是 Fast R-CNN 中 Smooth L1 Loss,用在 predict box(l) 与 ground truth box(g) 参数(即中心坐标位置,width、height)中,回归 bounding boxes 的中心位置,
以及 width、height
- confidence loss(conf) 是 Softmax Loss,输入为每一类的置信度 c
- 权重项 α,可在protxt中设置 loc_weight,默认设置为 1