SSD算法理解(2)

article/2025/9/15 12:28:29

作者:叶 虎

编辑:祝鑫泉

前言

目标检测近年来已经取得了很重要的进展,主流的算法主要分为两个类型:(1)two-stage方法,如R-CNN系算法,其主要思路是先通过启发式方法(selective search)或者CNN网络(RPN)产生一系列稀疏的候选框,然后对这些候选框进行分类与回归,two-stage方法的优势是准确度高;(2)one-stage方法,如Yolo和SSD,其主要思路是均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,然后利用CNN提取特征后直接进行分类与回归,整个过程只需要一步,所以其优势是速度快,但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡(参见Focal Loss,https://arxiv.org/abs/1708.02002),导致模型准确度稍低。不同算法的性能如图1所示,可以看到两类方法在准确度和速度上的差异。

图1 不同检测算法的性能对比

本文讲解的是SSD算法,其英文全名是Single Shot MultiBox Detector,名字取得不错,Single shot指明了SSD算法属于one-stage方法,MultiBox指明了SSD是多框预测。在上一篇文章中我们已经讲了Yolo算法,从图1也可以看到,SSD算法在准确度和速度(除了SSD512)上都比Yolo要好很多。图2给出了不同算法的基本框架图,对于Faster R-CNN,其先通过CNN得到候选框,然后再进行分类与回归,而Yolo与SSD可以一步到位完成检测。相比Yolo,SSD采用CNN来直接进行检测,而不是

像Yolo那样在全连接层之后做检测。其实采用卷积直接做检测只是SSD相比Yolo的其中一个不同点,另外还有两个重要的改变,一是SSD提取了不同尺度的特征图来做检测,大尺度特征图(较靠前的特征图)可以用来检测小物体,而小尺度特征图(较靠后的特征图)用来检测大物体;二是SSD采用了不同尺度和长宽比的先验框(Prior boxes, Default boxes,在Faster R-CNN中叫做锚,Anchors)。Yolo算法缺点是难以检测小目标,而且定位不准,但是这几点重要改进使得SSD在一定程度上克服这些缺点。下面我们详细讲解SDD算法的原理,并最后给出如何用TensorFlow实现SSD算法。

图2 不同算法的基本框架图

1

设计理念

SSD和Yolo一样都是采用一个CNN网络来进行检测,但是却采用了多尺度的特征图,其基本架构如图3所示。下面将SSD核心设计理念总结为以下三点:

图3 SSD基本框架

(1)采用多尺度特征图用于检测

所谓多尺度采用大小不同的特征图,CNN网络一般前面的特征图比较大,后面会逐渐采用stride=2的卷积或者pool来降低特征图大小,这正如图3所示,一个比较大的特征图和一个比较小的特征图,它们都用来做检测。这样做的好处是比较大的特征图来用来检测相对较小的目标,而小的特征图负责检测大目标,如图4所示,8x8的特征图可以划分更多的单元,但是其每个单元的先验框尺度比较小。

图4 不同尺度的特征图

(2)采用卷积进行检测

与Yolo最后采用全连接层不同,SSD直接采用卷积对不同的特征图来进行提取检测结果。对于形状为m*n*p,的特征图,只需要采用3*3*3的特征图,只需要采用。

(3)设置先验框

在Yolo中,每个单元预测多个边界框,但是其都是相对这个单元本身(正方块),但是真实目标的形状是多变的,Yolo需要在训练过程中自适应目标的形状。而SSD借鉴了Faster R-CNN中anchor的理念,每个单元设置尺度或者长宽比不同的先验框,预测的边界框(bounding boxes)是以这些先验框为基准的,在一定程度上减少训练难度。一般情况下,每个单元会设置多个先验框,其尺度和长宽比存在差异,如图5所示,可以看到每个单元使用了4个不同的先验框,图片中猫和狗分别采用最适合它们形状的先验框来进行训练,后面会详细讲解训练过程中的先验框匹配原则。

图5 SSD的先验框

SSD的检测值也与Yolo不太一样。对于每个单元的每个先验框,其都输出一套独立的检测值,对应一个边界框,主要分为两个部分。第一部分是各个类别的置信度或者评分,值得注意的是SSD将背景也当做了一个特殊的类别,如果检测目标共有C个类别,SSD其实需要预测C+1个置信度值,其中第一个置信度指的是不含目标或者属于背景的评分。后面当我们说C个个类别置信度时,请记住里面包含背景那个特殊的类别,即真实的检测类别只有C-1个。在预测过程中,置信度最高的那个类别就是边界框所属的类别,特别地,当第一个置信度值最高时,表示边界框中并不包含目标。第二部分就是边界框的location,包含4个值(cx,cy,w,h),分别表示边界框的中心坐标以及宽高。但是真实预测值其实只是边界框相对于先验框的转换值(paper里面说是offset,但是觉得transformation更合适,参见R-CNN,https://arxiv.org/abs/1311.2524)。先验框位置用

,表示,其对应边界框用

表示,那么边界框的预测值l,其实是b相对于d的转换值:

习惯上,我们称上面这个过程为边界框的编码(encode),预测时,你需要反向这个过程,即进行解码(decode),从预测值l中得到边界框的真实位置b:

然而,在SSD的Caffe源码(https://github.com/weiliu89/caffe/tree/ssd)实现中还有trick,那就是设置variance超参数来调整检测值,通过bool参数variance_encoded_in_target来控制两种模式,当其为True时,表示variance被包含在预测值中,就是上面那种情况。但是如果是Fasle(大部分采用这种方式,训练更容易?),就需要手动设置超参数variance,用来对l的4个值进行放缩,此时边界框需要这样解码:

综上所述,对于一个大小m*n的特征图,共有mn个单元,每个单元设置的先验框数目记为k,那么每个单元共需要(c+4)k个预测值,所有的单元共需要(c+4)kmn个预测值,由于SSD采用卷积做检测,所以就需要(c+4)k个卷积核完成这个特征图的检测过程。

2

网络结构

SSD采用VGG16作为基础模型,然后在VGG16的基础上新增了卷积层来获得更多的特征图以用于检测。SSD的网络结构如图6所示。上面是SSD模型,下面是Yolo模型,可以明显看到SSD利用了多尺度的特征图做检测。模型的输入图片大小是300*300(还可以是512*512,其与前者网络结构没有差别,只是最后新增一个卷积层,本文不再讨论)。

图5 SSD网络结构

采用VGG16做基础模型,首先VGG16是在ILSVRC CLS-LOC数据集预训练。然后借鉴了DeepLab-LargeFOV,链接https://export.arxiv.org/pdf/1606.00915,分别将VGG16的全连接层fc6和fc7转换成3*3卷积层conv6和1*1卷积层conv7,同时将池化层pool5由原来的2*2-s2变成3*3-s1(猜想是不想reduce特征图大小),为了配合这种变化,采用了一种Atrous Algorithm,其实就是conv6采用扩展卷积或带孔卷积(Dilation Conv,https://arxiv.org/abs/1511.07122),其在不增加参数与模型复杂度的条件下指数级扩大卷积的视野,其使用扩张率(dilation rate)参数,来表示扩张的大小,如下图6所示,(a)是普通的3*3卷积,其视野就是3*3,,(b)是扩张率为2,此时视野变成7*7,(c)扩张率为4时,视野扩大为15*15,但是视野的特征更稀疏了。Conv6采用3*3大小但dilation rate=6的扩展卷积。

图6 扩展卷积

然后移除dropout层和fc8层,并新增一系列卷积层,在检测数据集上做finetuing。

其中VGG16中的Conv4_3层将作为用于检测的第一个特征图。conv4_3层特征图大小是38*38,但是该层比较靠前,其norm较大,所以在其后面增加了一个L2 Normalization层(参见ParseNet,https://arxiv.org/abs/1506.04579),以保证和后面的检测层差异不是很大,这个和Batch Normalization层不太一样,其仅仅是对每个像素点在channle维度做归一化,而Batch Normalization层是在[batch_size, width, height]三个维度上做归一化。归一化后一般设置一个可训练的放缩变量gamma,使用TF可以这样简单实现:

# l2norm (not bacth norm, spatial normalization)
def l2norm(x, scale, trainable=True, scope="L2Normalization"):n_channels = x.get_shape().as_list()[-1]l2_norm = tf.nn.l2_normalize(x, [3], epsilon=1e-12)with tf.variable_scope(scope):gamma = tf.get_variable("gamma", shape=[n_channels, ], dtype=tf.float32,initializer=tf.constant_initializer(scale),trainable=trainable)return l2_norm * gamma
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

从后面新增的卷积层中提取Conv7,Conv8_2,Conv9_2,Conv10_2,Conv11_2作为检测所用的特征图,加上Conv4_3层,共提取了6个特征图,其大小分别是(38,38),(19,19),(10,10),(5,5),(3,3,),(1,1),,但是不同特征图设置的先验框数目不同(同一个特征图上每个单元设置的先验框是相同的,这里的数目指的是一个单元的先验框数目)。先验框的设置,包括尺度(或者说大小)和长宽比两个方面。对于先验框的尺度,其遵守一个线性递增规则:随着特征图大小降低,先验框尺度线性增加:

其中m指的特征图个数,但却是5,因为第一层(Conv4_3层)是单独设置的,Sk示先验框大小相对于图片的比例,而Smin和Smax表示比例的最小值与最大值,paper里面取0.2和0.9。对于第一个特征图,其先验框的尺度比例一般设置为Smin/2=0.1,那么尺度为300*0.1=30。对于后面的特征图,先验框尺度按照上面公式线性增加,但是先将尺度比例先扩大100倍,此时增长步长为

,这样各个特征图的Sk为20,37,54,71,88,将这些比例除以100,然后再乘以图片大小,可以得到各个特征图的尺度为60,111,162,213,264,这种计算方式是参考SSD的Caffe源码。综上,可以得到各个特征图的先验框尺度30,60,111,162,213,264。对于长宽比,一般选取

,对于特定的长宽比,按如下公式计算先验框的宽度与高度(后面的Sk均指的是先验框实际尺度,而不是尺度比例):

默认情况下,每个特征图会有一个

且尺度为Sk的先验框,除此之外,还会设置一个尺度为

的先验框,这样每个特征图都设置了两个长宽比为1但大小不同的正方形先验框。注意最后一个特征图需要参考一个虚拟

来计算

。因此,每个特征图一共有6个先验框

,但是在实现时,Conv4_3,Conv10_2和Conv11_2层仅使用4个先验框,它们不使用长宽比为3,1/3的先验框。每个单元的先验框的中心点分布在各个单元的中心,即

,其中

为特征图的大小。

得到了特征图之后,需要对特征图进行卷积得到检测结果,图7给出了一个5*5大小的特征图的检测过程。其中Priorbox是得到先验框,前面已经介绍了生成规则。检测值包含两个部分:类别置信度和边界框位置,各采用一次3*3卷积来进行完成。令

为该特征图所采用的先验框数目,那么类别置信度需要的卷积核数量为

,而边界框位置需要的卷积核数量为

。由于每个先验框都会预测一个边界框,所以SSD300一共可以预测

个边界框,这是一个相当庞大的数字,所以说SSD本质上是密集采样。

图7 基于卷积得到检测结果

3

训练过程

(1)先验框匹配

在训练过程中,首先要确定训练图片中的ground truth(真实目标)与哪个先验框来进行匹配,与之匹配的先验框所对应的边界框将负责预测它。在Yolo中,ground truth的中心落在哪个单元格,该单元格中与其IOU最大的边界框负责预测它。但是在SSD中却完全不一样,SSD的先验框与ground truth的匹配原则主要有两点。首先,对于图片中每个ground truth,找到与其IOU最大的先验框,该先验框与其匹配,这样,可以保证每个ground truth一定与某个先验框匹配。通常称与ground truth匹配的先验框为正样本,反之,若一个先验框没有与任何ground truth进行匹配,那么该先验框只能与背景匹配,就是负样本。一个图片中ground truth是非常少的, 而先验框却很多,如果仅按第一个原则匹配,很多先验框会是负样本,正负样本极其不平衡,所以需要第二个原则。第二个原则是:对于剩余的未匹配先验框,若某个ground truth的IOU大于某个阈值(一般是0.5),那么该先验框也与这个ground truth进行匹配。这意味着某个ground truth可能与多个先验框匹配,这是可以的。但是反过来却不可以,因为一个先验框只能匹配一个ground truth,如果多个ground truth与某个先验框IOU大于阈值,那么先验框只与IOU最大的那个先验框进行匹配。第二个原则一定在第一个原则之后进行,仔细考虑一下这种情况,如果某个ground truth所对应最大IOU小于阈值,并且所匹配的先验框却与另外一个ground truth的IOU大于阈值,那么该先验框应该匹配谁,答案应该是前者,首先要确保某个ground truth一定有一个先验框与之匹配。但是,这种情况我觉得基本上是不存在的。由于先验框很多,某个ground truth的最大IOU肯定大于阈值,所以可能只实施第二个原则既可以了,这里的TensorFlow(https://github.com/xiaohu2015/SSD-Tensorflow/blob/master/nets/ssd_common.py)版本就是只实施了第二个原则,但是这里的Pytorch(https://github.com/amdegroot/ssd.pytorch/blob/master/layers/box_utils.py)两个原则都实施了。图8为一个匹配示意图,其中绿色的GT是ground truth,红色为先验框,FP表示负样本,TP表示正样本。

图8 先验框匹配示意图

尽管一个ground truth可以与多个先验框匹配,但是ground truth相对先验框还是太少了,所以负样本相对正样本会很多。为了保证正负样本尽量平衡,SSD采用了hard negative mining,就是对负样本进行抽样,抽样时按照置信度误差(预测背景的置信度越小,误差越大)进行降序排列,选取误差的较大的top-k作为训练的负样本,以保证正负样本比例接近1:3。

(2)损失函数

训练样本确定了,然后就是损失函数了。损失函数定义为位置误差(locatization loss, loc)与置信度误差(confidence loss, conf)的加权和:

其中N是先验框的正样本数量。这里

为一个指示参数,当

时表示第i个先验框与第j个ground truth匹配,并且ground truth的类别为p。c为类别置信度预测值。l为先验框的所对应边界框的位置预测值,而g

是ground truth的位置参数。对于位置误差,其采用Smooth L1 loss,定义如下:

由于的

存在,所以位置误差仅针对正样本进行计算。值得注意的是,要先对ground truth的g进行编码得到

,因为预测值l也是编码值,若设置variance_encoded_in_target=True,编码时要加上variance:

对于置信度误差,其采用softmax loss:

权重系数a通过交叉验证设置为1。

(3)数据扩增

采用数据扩增(Data Augmentation)可以提升SSD的性能,主要采用的技术有水平翻转(horizontal flip),随机裁剪加颜色扭曲(random crop & color distortion),随机采集块域(Randomly sample a patch)(获取小目标训练样本),如下图所示:

图9 数据扩增方案

其它的训练细节如学习速率的选择详见论文,这里不再赘述。

4

预测过程

预测过程比较简单,对于每个预测框,首先根据类别置信度确定其类别(置信度最大者)与置信度值,并过滤掉属于背景的预测框。然后根据置信度阈值(如0.5)过滤掉阈值较低的预测框。对于留下的预测框进行解码,根据先验框得到其真实的位置参数(解码后一般还需要做clip,防止预测框位置超出图片)。解码之后,一般需要根据置信度进行降序排列,然后仅保留top-k(如400)个预测框。最后就是进行NMS算法,过滤掉那些重叠度较大的预测框。最后剩余的预测框就是检测结果了。

5

性能评估

首先整体看一下SSD在VOC2007,VOC2012及COCO数据集上的性能,如表1所示。相比之下,SSD512的性能会更好一些。加*的表示使用了image expansion data augmentation(通过zoom out来创造小的训练样本)技巧来提升SSD在小目标上的检测效果,所以性能会有所提升。

表1 SSD在不同数据集上的性能

SSD与其它检测算法的对比结果(在VOC2007数据集)如表2所示,基本可以看到,SSD与Faster R-CNN有同样的准确度,并且与Yolo具有同样较快地检测速度。

表2 SSD与其它检测算法的对比结果(在VOC2007数据集)

文章还对SSD的各个trick做了更为细致的分析,表3为不同的trick组合对SSD的性能影响,从表中可以得出如下结论:

  • 数据扩增技术很重要,对于mAP的提升很大;
  • 使用不同长宽比的先验框可以得到更好的结果;

表3 不同的trick组合对SSD的性能影响

同样的,采用多尺度的特征图用于检测也是至关重要的,这可以从表4中看出:

表4 多尺度特征图对SSD的影响

6

TensorFlow上的实现

SSD在很多框架上都有了开源的实现,这里基于balancap的TensorFlow版本(https://github.com/balancap/SSD-Tensorflow)来实现SSD的Inference过程。这里实现的是SSD300,与paper里面不同的是,这里采用

。首先定义SSD的参数:

self.ssd_params = SSDParams(img_shape=(300, 300),   # 输入图片大小num_classes=21,     # 类别数+背景no_annotation_label=21,feat_layers=["block4", "block7", "block8", "block9", "block10", "block11"],   # 要进行检测的特征图namefeat_shapes=[(38, 38), (19, 19), (10, 10), (5, 5), (3, 3), (1, 1)],  # 特征图大小anchor_size_bounds=[0.15, 0.90],  # 特征图尺度范围anchor_sizes=[(21., 45.),(45., 99.),(99., 153.),(153., 207.),(207., 261.),(261., 315.)],  # 不同特征图的先验框尺度(第一个值是s_k,第2个值是s_k+1)anchor_ratios=[[2, .5],[2, .5, 3, 1. / 3],[2, .5, 3, 1. / 3],[2, .5, 3, 1. / 3],[2, .5],[2, .5]], # 特征图先验框所采用的长宽比(每个特征图都有2个正方形先验框)anchor_steps=[8, 16, 32, 64, 100, 300],  # 特征图的单元大小anchor_offset=0.5,                       # 偏移值,确定先验框中心normalizations=[20, -1, -1, -1, -1, -1],  # l2 normprior_scaling=[0.1, 0.1, 0.2, 0.2]       # variance)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

然后构建整个网络,注意对于stride=2的conv不要使用TF自带的padding="same",而是手动pad,这是为了与Caffe一致:

def _built_net(self):"""Construct the SSD net"""self.end_points = {}  # record the detection layers outputself._images = tf.placeholder(tf.float32, shape=[None, self.ssd_params.img_shape[0],self.ssd_params.img_shape[1], 3])with tf.variable_scope("ssd_300_vgg"):# original vgg layers# block 1net = conv2d(self._images, 64, 3, scope="conv1_1")net = conv2d(net, 64, 3, scope="conv1_2")self.end_points["block1"] = netnet = max_pool2d(net, 2, scope="pool1")# block 2net = conv2d(net, 128, 3, scope="conv2_1")net = conv2d(net, 128, 3, scope="conv2_2")self.end_points["block2"] = netnet = max_pool2d(net, 2, scope="pool2")# block 3net = conv2d(net, 256, 3, scope="conv3_1")net = conv2d(net, 256, 3, scope="conv3_2")net = conv2d(net, 256, 3, scope="conv3_3")self.end_points["block3"] = netnet = max_pool2d(net, 2, scope="pool3")# block 4net = conv2d(net, 512, 3, scope="conv4_1")net = conv2d(net, 512, 3, scope="conv4_2")net = conv2d(net, 512, 3, scope="conv4_3")self.end_points["block4"] = netnet = max_pool2d(net, 2, scope="pool4")# block 5net = conv2d(net, 512, 3, scope="conv5_1")net = conv2d(net, 512, 3, scope="conv5_2")net = conv2d(net, 512, 3, scope="conv5_3")self.end_points["block5"] = netprint(net)net = max_pool2d(net, 3, stride=1, scope="pool5")print(net)
    # additional SSD layers# block <span class="token number">6</span><span class="token punctuation">:</span> use dilate convnet <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">1024</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> dilation_rate<span class="token operator">=</span><span class="token number">6</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv6"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>end_points<span class="token punctuation">[</span><span class="token string">"block6"</span><span class="token punctuation">]</span> <span class="token operator">=</span> net#net <span class="token operator">=</span> <span class="token function">dropout</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> is_training<span class="token operator">=</span>self<span class="token punctuation">.</span>is_training<span class="token punctuation">)</span># block <span class="token number">7</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">1024</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv7"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>end_points<span class="token punctuation">[</span><span class="token string">"block7"</span><span class="token punctuation">]</span> <span class="token operator">=</span> net# block <span class="token number">8</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">256</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv8_1x1"</span><span class="token punctuation">)</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span><span class="token function">pad2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">512</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> stride<span class="token operator">=</span><span class="token number">2</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv8_3x3"</span><span class="token punctuation">,</span>padding<span class="token operator">=</span><span class="token string">"valid"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>end_points<span class="token punctuation">[</span><span class="token string">"block8"</span><span class="token punctuation">]</span> <span class="token operator">=</span> net# block <span class="token number">9</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">128</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv9_1x1"</span><span class="token punctuation">)</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span><span class="token function">pad2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">256</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> stride<span class="token operator">=</span><span class="token number">2</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv9_3x3"</span><span class="token punctuation">,</span>padding<span class="token operator">=</span><span class="token string">"valid"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>end_points<span class="token punctuation">[</span><span class="token string">"block9"</span><span class="token punctuation">]</span> <span class="token operator">=</span> net# block <span class="token number">10</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">128</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv10_1x1"</span><span class="token punctuation">)</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">256</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv10_3x3"</span><span class="token punctuation">,</span> padding<span class="token operator">=</span><span class="token string">"valid"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>end_points<span class="token punctuation">[</span><span class="token string">"block10"</span><span class="token punctuation">]</span> <span class="token operator">=</span> net# block <span class="token number">11</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">128</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv11_1x1"</span><span class="token punctuation">)</span>net <span class="token operator">=</span> <span class="token function">conv2d</span><span class="token punctuation">(</span>net<span class="token punctuation">,</span> <span class="token number">256</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> scope<span class="token operator">=</span><span class="token string">"conv11_3x3"</span><span class="token punctuation">,</span> padding<span class="token operator">=</span><span class="token string">"valid"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>end_points<span class="token punctuation">[</span><span class="token string">"block11"</span><span class="token punctuation">]</span> <span class="token operator">=</span> net
# &lt;span class="token keyword"&gt;class&lt;/span&gt; &lt;span class="token class-name"&gt;and&lt;/span&gt; location predictions
predictions &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
logits &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
locations &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
&lt;span class="token keyword"&gt;for&lt;/span&gt; i&lt;span class="token punctuation"&gt;,&lt;/span&gt; layer &lt;span class="token keyword"&gt;in&lt;/span&gt; &lt;span class="token function"&gt;enumerate&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;self&lt;span class="token punctuation"&gt;.&lt;/span&gt;ssd_params&lt;span class="token punctuation"&gt;.&lt;/span&gt;feat_layers&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;cls&lt;span class="token punctuation"&gt;,&lt;/span&gt; loc &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;ssd_multibox_layer&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;self&lt;span class="token punctuation"&gt;.&lt;/span&gt;end_points&lt;span class="token punctuation"&gt;[&lt;/span&gt;layer&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; self&lt;span class="token punctuation"&gt;.&lt;/span&gt;ssd_params&lt;span class="token punctuation"&gt;.&lt;/span&gt;num_classes&lt;span class="token punctuation"&gt;,&lt;/span&gt;self&lt;span class="token punctuation"&gt;.&lt;/span&gt;ssd_params&lt;span class="token punctuation"&gt;.&lt;/span&gt;anchor_sizes&lt;span class="token punctuation"&gt;[&lt;/span&gt;i&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;self&lt;span class="token punctuation"&gt;.&lt;/span&gt;ssd_params&lt;span class="token punctuation"&gt;.&lt;/span&gt;anchor_ratios&lt;span class="token punctuation"&gt;[&lt;/span&gt;i&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;self&lt;span class="token punctuation"&gt;.&lt;/span&gt;ssd_params&lt;span class="token punctuation"&gt;.&lt;/span&gt;normalizations&lt;span class="token punctuation"&gt;[&lt;/span&gt;i&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; scope&lt;span class="token operator"&gt;=&lt;/span&gt;layer&lt;span class="token operator"&gt;+&lt;/span&gt;&lt;span class="token string"&gt;"_box"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;predictions&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;append&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;tf&lt;span class="token punctuation"&gt;.&lt;/span&gt;nn&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;softmax&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cls&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;logits&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;append&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cls&lt;span class="token punctuation"&gt;)&lt;/span&gt;locations&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;append&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;loc&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token keyword"&gt;return&lt;/span&gt; predictions&lt;span class="token punctuation"&gt;,&lt;/span&gt; logits&lt;span class="token punctuation"&gt;,&lt;/span&gt; locations&lt;/pre&gt;&lt;p&gt;对于特征图的检测,这里单独定义了一个组合层ssd_multibox_layer,其主要是对特征图进行两次卷积,分别得到类别置信度与边界框位置:&lt;/p&gt;&lt;pre class="prism-token token  language-javascript"&gt;# multibox layer&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token keyword"&gt;get&lt;/span&gt; &lt;span class="token keyword"&gt;class&lt;/span&gt; &lt;span class="token class-name"&gt;and&lt;/span&gt; location predicitions &lt;span class="token keyword"&gt;from&lt;/span&gt; detection layer

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

def ssd_multibox_layer(x, num_classes, sizes, ratios, normalization=-1, scope=“multibox”):
pre_shape = x.get_shape().as_list()[1:-1]
pre_shape = [-1] + pre_shape
with tf.variable_scope(scope):
# l2 norm
if normalization > 0:
x = l2norm(x, normalization)
print(x)
# numbers of anchors
n_anchors = len(sizes) + len(ratios)
# location predictions
loc_pred = conv2d(x, n_anchors4, 3, activation=None, scope=“conv_loc”)
loc_pred = tf.reshape(loc_pred, pre_shape + [n_anchors, 4])
# class prediction
cls_pred = conv2d(x, n_anchorsnum_classes, 3, activation=None, scope=“conv_cls”)
cls_pred = tf.reshape(cls_pred, pre_shape + [n_anchors, num_classes])
return cls_pred, loc_pred

对于先验框,可以基于numpy生成,定义在ssd_anchors.py文件中,链接为https://github.com/xiaohu2015/DeepLearning_tutorials/blob/master/ObjectDetections/SSD/ssd_anchors.py。结合先验框与检测值,对边界框进行过滤与解码:

classes, scores, bboxes = self._bboxes_select(predictions, locations)

    这里将得到过滤得到的边界框,其中classes, scores, bboxes分别表示类别,置信度值以及边界框位置。

    基于训练好的权重文件在这里下载https://pan.baidu.com/s/1snhuTsT,这里对SSD进行测试:

    ssd_net = SSD()
    classes, scores, bboxes = ssd_net.detections()
    images = ssd_net.images()
    • 1
    • 2
    • 3

    sess = tf.Session()

    Restore SSD model.

    ckpt_filename = ‘./ssd_checkpoints/ssd_vgg_300_weights.ckpt’
    sess.run(tf.global_variables_initializer())
    saver = tf.train.Saver()
    saver.restore(sess, ckpt_filename)

    img = cv2.imread(’./demo/dog.jpg’)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img_prepocessed = preprocess_image(img) # 预处理图片,主要是归一化和resize
    rclasses, rscores, rbboxes = sess.run([classes, scores, bboxes],
    feed_dict={images: img_prepocessed})
    rclasses, rscores, rbboxes = process_bboxes(rclasses, rscores, rbboxes) # 处理预测框,包括clip,sort,nms

    plt_bboxes(img, rclasses, rscores, rbboxes) # 绘制检测结果

    详细的代码放在GitHub上了,链接为https://github.com/xiaohu2015/DeepLearning_tutorials/tree/master/ObjectDetections/SSD,然后看一下一个自然图片的检测效果:

    如果你想实现SSD的train过程,你可以参考附录里面的Caffe,TensorFlow以及Pytorch实现。

    小结

    SSD在Yolo的基础上主要改进了三点:多尺度特征图,利用卷积进行检测,设置先验框。这使得SSD在准确度上比Yolo更好,而且对于小目标检测效果也相对好一点。由于很多实现细节都包含在源码里面,文中有描述不准或者错误的地方在所难免,欢迎交流指正。

    参考文献

    1. SSD: Single Shot MultiBox Detector 链接:https://arxiv.org/pdf/1611.10012.pdf
    2. SSD Slide 链接:http://www.cs.unc.edu/~wliu/papers/ssd_eccv2016_slide.pdf
    3. SSD Caffe 链接:https://github.com/weiliu89/caffe/tree/ssd
    4. SSD TensorFlow 链接:https://github.com/balancap/SSD-Tensorflow
    5. SSD Pytorch 链接:https://github.com/amdegroot/ssd.pytorch
    6. leonardoaraujosantos Artificial Inteligence online book 链接:https://leonardoaraujosantos.gitbooks.io/artificial-inteligence/content/single-shot-detectors/ssd.html
            </div><link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-2787195726.css" rel="stylesheet"></div>
    

    http://chatgpt.dhexx.cn/article/I6mIZJME.shtml

    相关文章

    SSD算法详解default box

    本文提出的SSD算法是一种直接预测目标类别和bounding box的多目标检测算法。 与faster rcnn相比&#xff0c;该算法没有生成 proposal 的过程&#xff0c;这就极大提高了检测速度。针对不同大小的目标检测&#xff0c;传统的做法是先将图像转换成不同大小&#xff08;图像金字塔…

    【个人整理】一文看尽目标检测算法SSD的核心架构与设计思想

    前言&#xff1a;SSD&#xff08;Single Shot MultiBox Detector&#xff09;是大神Wei Liu在 ECCV 2016上发表的一种的目标检测算法。对于输入图像大小300x300的版本在VOC2007数据集上达到了72.1%mAP的准确率并且检测速度达到了惊人的58FPS&#xff08; Faster RCNN&#xff1…

    目标检测:SSD算法原理综述

    SSD&#xff08;Single Shot Detection&#xff09;是一个流行且强大的目标检测网络&#xff0c;网络结构包含了基础网络&#xff08;Base Network&#xff09;&#xff0c;辅助卷积层&#xff08;Auxiliary Convolutions&#xff09;和预测卷积层&#xff08;Predicton Convol…

    SSD算法阅读记录

    SSD: Single Shot MultiBox Detector 一、网络结构二、模型设计1、多尺度特征映射2、使用卷积进行检测3、使用default boxes 三、模型训练1、匹配策略2、损失函数3、default boxes的确定4、难分样本挖掘 四、参考链接 一、网络结构 SSD网络是在VGG16的基础上修改得到的&#xf…

    2.1 SSD算法理论

    前言 随着人工智能的不断发展&#xff0c;机器学习这门技术也越来越重要&#xff0c;很多人都开启了学习机器学习&#xff0c;本文就介绍了机器学习的基础内容。来源于哔哩哔哩博主“霹雳吧啦Wz”&#xff0c;博主学习作为笔记记录&#xff0c;欢迎大家一起讨论学习交流。 一…

    SSD检测算法理解

    SSD检测算法理解 简介1 SSD模型2 SSD算法的核心设计思想3 多尺度特征图检测思想4 单尺度特征图检测4.1 先验框的设置4.2 先验框的比例和宽高比4.3 Loss计算 5 性能评估 简介 在作者的原论文中提到&#xff0c;SSD算法要比当时优秀的Faster RCNN算法和YOLO算法识别效果更好&…

    SSD算法通俗详解

    算法简介 算法原理 样本构造 损失函数 使用细节 # ssd算法&#xff1a; ##简介 刘伟在2016年提出&#xff0c;发表在ECCV&#xff1b;是一种通过直接回归的方式去获取目标类别和位置的one-stage算法&#xff0c;不需要proposal&#xff1b;作用在卷积网络的输出特征图上进行预…

    详细解读目标检测经典算法-SSD

    学习目标&#xff1a; 知道SSD的多尺度特征图的网络知道SSD中先验框的生成方式知道SSD的损失函数的设计 目标检测算法主要分为两类&#xff1a; Two-stage方法&#xff1a;如R-CNN系列算法&#xff0c;主要思路就是通过Selective Search或者CNN网络产生一系列的稀疏矩阵的候…

    SSD目标检测算法——通俗易懂解析

    目录 前言SSD网络 前言 前面几篇文章我们讲解了YOLO系类的论文&#xff0c;今天我们来看下SSD算法。对YOLO有兴趣的小伙伴们可以一步到我前面的几篇博文&#xff1a; YOLOv1目标检测算法——通俗易懂的解析YOLOv2目标检测算法——通俗易懂的解析YOLOv3目标检测算法——通俗易懂…

    SSD算法理解(1)

    版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 https://blog.csdn.net/u010167269/article/details/52563573 Preface 这是今年 ECCV 2016 的一篇文章&#xff0c;是 UNC Chapel Hill&#xff08;北卡罗来纳大学教堂山分校&#xff09; 的 Wei Li…

    SSD算法分析

    SSD算法分析 1 SSD算法概述2 SSD整体流程3 SSD中的重要概念3.1 多尺度Feature Map检测3.2 Default Box3.2.1 设计思路3.2.2 参数计算 4 SSD网络架构与网络预测4.1 网络架构4.2 网络预测4.3 预测值解码 5 网络训练5.1 Ground Truth编码5.2 匹配策略5.3 损失函数5.3.1 定位损失5.…

    SSD算法简单解析

    前言 今天学习SSD目标检测算法&#xff0c;SSD&#xff0c;全称Single Shot MultiBox Detector&#xff0c;是2016年提出的算法&#xff0c;今天我们还是老规矩&#xff0c;最简单的做算法解析&#xff0c;力求让像我一样的小白也可以看得懂。 算法初识 1》算法能干什么&…

    深度学习算法之-SSD(一)

    版权声明&#xff1a;本文为博主原创文章&#xff0c;转载需注明出处。 https://blog.csdn.net/qianqing13579/article/details/82106664 </div><link rel"stylesheet" href"https://csdnimg.cn/release/phoenix/template/css/ck_html…

    SSD系列算法原理讲解----(1)SSD系列算法介绍(主干网络、多尺度Feature Map预测)(笔记)

    SSD系列算法原理介绍 SSD算法介绍&#xff1a; Single Shot MultiBox Detector&#xff08;One-stage方法&#xff09; - Wei Liu在ECCV 2016提出 - 直接回归目标类别和位置 - 不同尺度的特征图上进行检测 - 端到端的训练 - 图像的分辨率比较低&#xff0c;也能保证检测的精度 …

    目标检测算法——SSD详解

    目录 一、 背景&#xff08;基本介绍&#xff09; 二、 网络结构 三、 具体过程 1. default box 匹配 2. 损失函数 3. 数据增广 4. Atrous Algothrim 5. NMS&#xff08;非极大值抑制&#xff09; 五、 性能评估 优点&#xff1a; 缺点&#xff1a; SSD 算法的改进…

    SSD算法原理与代码(三)

    说明&#xff1a;这几篇文章是讲解SSD&#xff0c;从算法原理、代码到部署到rk3588芯片上的过程。环境均是TF2.2&#xff0c;具体的安装过程请参考网上其他的文章。 一、SSD简介 SSD算法是一个优秀的one-stage目标检测算法。能够一次就完成目标的检测和分类过程。主要是的思路…

    目标检测算法之SSD

    码字不易&#xff0c;欢迎给个赞&#xff01; 欢迎交流与转载&#xff0c;文章会同步发布在公众号&#xff1a;机器学习算法全栈工程师(Jeemy110) 目录 目录前言设计理念网络结构训练过程预测过程性能评估TensorFlow上的实现小结参考文献 前言 目标检测近年来已经取得了很重…

    深度学习 -- SSD 算法流程详解

    SSD同样是经典论文&#xff0c;后续很多论文以此为基础&#xff0c;所以搞懂流程比较重要&#xff0c;中间如果 有写的不对、有问题或者有看不懂的地方&#xff0c;还望指正。如果有了新的理解&#xff0c;我会持续更新。 作为经典论文&#xff0c;SSD算法也同样产生了很多后续…

    SSD原理解读-从入门到精通

    前言 当初写这篇博客的初衷只是记录自己学习SSD的一些心得体会&#xff0c;纯属学习笔记&#xff0c;后来由于工作上的需要&#xff0c;需要对小伙伴进行目标检测方面的培训&#xff0c;后来就基于这篇博客进行了扩展&#xff0c;逐渐演变成了现在的样子&#xff0c;本文力求从…

    SSD算法理论

    SSD算法 SSD(Single Shot MultiBox Detector)是one-stage目标检测方法&#xff0c;one-stage算法就是目标检测和分类是同时完成的&#xff0c;其主要思路是利用CNN提取特征后&#xff0c;均匀地在图片的不同位置进行密集抽样&#xff0c;抽样时可以采用不同尺度和长宽比&#…