目标检测 SSD: Single Shot MultiBox Detector - 综述

目标检测 SSD: Single Shot MultiBox Detector - 综述

flyfish

论文的效果
对于图片大小为300×300的输入,SSD在VOC2007测试中以59FPS的速度在Nvidia Titan X上达到74.3%的mAP,对于图片大小为512×512的输入,SSD达到了76.9%的mAP

数据集(VOC)

VOC数据集的类别个数是21,还有一个没在列表里的背景
VOC(Pascal Visual Object Classes)包括2007和2012
我们使用2007和2012的trainval作为训练集,2007的test作为测试集

VOC_CLASS = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle',
                   'bus', 'car', 'cat', 'chair', 'cow',
                   'diningTable', 'dog', 'horse', 'motorbike', 'person',
                   'pottedPlant', 'sheep', 'sofa', 'train', 'TV']
下载地址

SSD300的意思让我们输入的图像是RGB通道,大小为300 *300的图片
假设 使用的是PyTorch实现,则使用的顺序是NCHW ,一张图片那就是 [1,3,300,300],N张图片就是[N,3,300,300],N就是batch size

如何看懂下面这张图
在这里插入图片描述

feature map 是什么

简单理解
假设有个函数,我们叫它卷积函数f,x为输入。
f(x)=y,输入x,经过卷积函数f之后得到y,我们可以称为这个y就是feature map
f(y)=z, 输入y,经过卷积函数f之后得到z,我们可以称为这个z就是feature map
如果f(x)的结果y有一个通道,可以说一个feature map或者一张feature map
如果f(x)的结果y有三个通道,可以说三个feature map或者三张feature map

SSD 有6张feature map
feature map,因为map翻译成地图,地图就是真是地理的映射,所以有的地方翻译成特征图,有的地方翻译成特征映射

feature map 要解决什么问题

1 what(recognition),也就是feature,我们根据feature判断出来它是什么object
2 where(localization),也就是map,在什么地方,用一个矩形的边框画出来,表明object在哪

从哪里取feature layer

一部分在basenet即backbone中的feature layer
一部分是自行添加的feature layer

feature map 在哪一层是我们人为指定的,我们认为它是,他就是,并不是每个经过卷积层的输出我们都认为它是feature map。

例如上图,划分成了8 × 8 feature map 和 4 × 4 feature map
图b, 8 × 8 feature map 有64个 feature map cell
这个就像一个Excel表格 中的 单元格的概念一样
SSD 的6张feature map 划分的是 39 × 39,19 × 19, 10 × 10, 5 × 5, 3 × 1, 1 × 1

程序中通常以配置展现 38, 19, 10, 5, 3, 1

后面用38, 19, 10, 5, 3, 1 指代每层的feature map
例如说38 feature map,19 feature map
等会下面还会说到 feature map

GT(Ground Truth) Box 是什么

在这里插入图片描述
绿色的GT是ground truth
就像我们人为操作画出猫狗的边框,也就是正确的标注数据,也可以把标注数据都叫ground truth
这里我们给图片正确打标记,画上边框就是ground truth,实际ground truth在程序是一行文本表示的

Default Box是什么

在这里插入图片描述
如果在SSD中出现anchor box、prior box、default box先验框都是一个意思。
对比ground truth 和 default boxes

绿色的GT是ground truth,红色为default box
default box是怎么产生的
每个feature map cell都产生
产生根据不同的比例(different scale) 和 aspect ratio(纵横比),
scale有的地方翻译成尺度,aspect ratio有地方翻译成高宽比

同一个feature map上每个feature map cell的default box的个数是相同的,
不同的feature map上的feature map cell的default box的个数是不同的

关于每个feature map cell的default box个数 按层个数如下
4, 6, 6, 6, 6, 4, 4

也就是38 feature map 对应 4
19 feature map  对应 6,依次向后

图中loc是default box的位置,default box 中心点坐标,宽度,高度,4个值
conf 是预测C个类别的分数,使用VOC数据集,因为是是21个类别,所以这里有21个值

ssd 预测的边框数
38 × 38 × 4 + 19 × 19 × 6 + 10 × 10 × 6 + 5 × 5 × 6 + 3 × 3 × 4 + 1 × 1 × 4 = 8732 38 \times 38 \times 4+19 \times 19 \times 6+10 \times 10 \times 6+5 \times 5 \times 6+3 \times 3 \times 4+1 \times 1 \times 4=8732 38×38×4+19×19×6+10×10×6+5×5×6+3×3×4+1×1×4=8732

不同形式的代码,表达的方式是不一样的
参数解释

'feature_maps': [38, 19, 10, 5, 3, 1],
'min_sizes': [30, 60, 111, 162, 213, 264],
'max_sizes': [60, 111, 162, 213, 264, 315],
'aspect_ratios': [[2], [2, 3], [2, 3], [2, 3], [2], [2]],

feature_maps 已经知道了

min_sizes和max_sizes的解释
s m i n s_min smin is 0.2 and s m a x s_max smax is 0.9, meaning the lowest layer has a scale of 0.2 and
the highest layer has a scale of 0.9

为了计算将0.2和0.9扩大100倍
min_ratio = 20
max_ratio = 90
step = int(math.floor((90 - 20) / (6 - 2)))
70/4=17.5 ,因为math.floor所以最后是17
从20-90之间以step=17为间隔产生一组数结果是[20,37,54,71,88]

输入是300 * 300
38 feature map 的 min_size=( 300 * 10 / 100 )=30,max_size =( 300 * 20 / 100) = 60

简化下计算 0.2和0.9扩大100倍,300 缩小100倍,间隔都是17
38 feature map 的 min_size= 3 * 10 = 30, max_size = 3 * 20 = 60
19 feature map 的 min_size= 3 * 20 = 60, max_size = 3 * 37 = 111
10 feature map 的 min_size= 3 * 37 = 111,max_size = 3 * 54 = 162
5 feature map 的 min_size= 3 * 54 = 162,max_size = 3 * 71 = 213
3 feature map 的 min_size= 3 * 71 = 213,max_size = 3 * 88 = 264
1 feature map 的 min_size= 3 * 88 = 264,max_size = 3 * 105 = 315

这也就是下面的数的由来
‘min_sizes’: [30, 60, 111, 162, 213, 264],
‘max_sizes’: [60, 111, 162, 213, 264, 315],

aspect_ratios的解释下面还有更清楚的一种表达方式,所见即所得的方式
‘aspect_ratios’: [[2], [2, 3], [2, 3], [2, 3], [2], [2]]

实际表达方式如下
正方形的default box,共两个,其边长计算公式为:
小正方形的边长=min_size
大正方形的边长=sqrt(min_size * max_size)

矩形
height=1 / sqrt(aspect_ratio) * min_size,
width=sqrt(aspect_ratio) * min_size

[2]是

1:1 小正方形
1:2
2:1
1:1 大正方形

4种纵横比

[2,3]

1:1 小正方形
2:1
1:2
3:1
1:3
1:1 大正方形

6种纵横比
这也是4、6、6、6、4、4的由来

The model loss is a weighted sum between localization loss ( Smooth L1 ) and confidence loss ( Softmax )

什么是感受野

感受野,Receptive Field,简称RF
在这里插入图片描述
输入图像经过1次 k × k 卷积后,输入图像的 k × k 像素会变成输出图像的 1个 像素,
所以输出图像的每一个像素与输入图像的 k × k 有关.这里 k × k卷积 相当于标准卷积 stride=1,dilation=1 ,无padding,groups=1.

A(大小 7 × 7) -》经过卷积(3 × 3)-》B(大小 5 × 5) 感受野 大小是3 × 3
输出图像B的一个像素 和 A图像的 3×3像素 有关

假设B再做1次 3 × 3 卷积后,那么输出图像C的一个像素 又和 B图像的 3×3 有关.

B(大小 5 × 5) -》经过卷积(3 × 3)-》 C(大小 3 × 3)

C的每一个像素与A图像 的 (3 × 2 - 1)×( 3 × 2 - 1)=5 × 5像素有关

简单理解感受野就是 输出图像的每一个像素与输入图像的 N×N 有关
,那么输出图像的感受野就是 N×N
A是输入
B的感受野大小是3 × 3
C的感受野大小是5 × 5
如果还有D,那感受野就是(3 × 3 - 2)×( 3 × 3 - 2)=7 × 7
第一个卷积输出图像的感受野的大小等于卷积核的大小

下图只是把 7 × 7 变成了 7
在这里插入图片描述

Hard Negative Mining

其他名字:难例挖掘 难分样本挖掘

样本分为正样本和负样本
正样本
GT box

负样本
检测每一个default box,如果与GT box的IoU 都小于0.5(自定义),认定其为负样本

default box 和 GT box 匹配的时候,有大量的default box 不合格。
将负样本的default box按照置信度从大到小排序,选择那些置信度分数大的作为负样本,调整到正负样本数量的比值为1:3。

具体情况看这里

feature map 概览

特征图的层名特征图的维度Prior ScaleAspect Ratios每个Position的Prior数该层的Prior总数
conv4_338, 380.11:1, 2:1, 1:2 + an extra prior45776
conv719, 190.21:1, 2:1, 1:2, 3:1, 1:3 + an extra prior62166
conv8_210, 100.3751:1, 2:1, 1:2, 3:1, 1:3 + an extra prior6600
conv9_25, 50.551:1, 2:1, 1:2, 3:1, 1:3 + an extra prior6150
conv10_23, 30.7251:1, 2:1, 1:2 + an extra prior436
conv11_21, 10.91:1, 2:1, 1:2 + an extra prior44
总数8732 个prior

Prior Box或者说Prior、Default Box是如何设计的

以conv9_2层为例,看下图红色的框框就是Prior Box
问题就是这些红色的框框怎么来的
feature map cell 是指 feature map 中每一个小格子,图中有25个cell。
prior 是feature map的每个cell上都有一系列固定大小的边框,图中有6个不同大小,不同高宽比的矩形框就是prior
每个cell都是这样6个,如果超过了图片大小就剪裁掉
这么多框也就是Prior Box的目的就是有个框能够框住包含检测目标
在这里插入图片描述在这里插入图片描述

设计该Prior需要三个参数,一个是feature map的大小
一个是aspect ratio,还有一个是scale

feature map大小分别是38,19,10,5,3,1
在这里插入图片描述
scale
conv4_3设计为0.1,其余的Prior 的scale从0.2线性增加到0.9
也就是中间间隔是0.175
‘conv7’: 0.2,
‘conv8_2’: 0.375,
‘conv9_2’: 0.55,
‘conv10_2’: 0.725,
‘conv11_2’: 0.9}

the lowest layer has a scale of 0.2 and
the highest layer has a scale of 0.9

aspect ratio

1表示 1:1
2表示 2:1
3表示 3:1
0.5 表示1:2
0.333表示 1:3
aspect ratio翻译是纵横比,实际宽度和高度的比
在这里插入图片描述

例如
3表示 3:1,w=3,h=1

以conv9_2为例
feature map cell 是指 feature map 中每一个小格子,图中有25个cell。
prior 是feature map的每个cell上都有一系列固定大小的边框,图中有6个不同大小,不同高宽比的矩形框就是prior
每个cell都是这样6个,剪裁掉超过了图片大小的部分
‘conv9_2’: [1., 2., 3., 0.5, .333] 当aspect ratio是1:1的时候,需要附加一个,也就5+1=6

以conv4_3
中心位置
( i + 0.5 ∣ f k ∣ , j + 0.5 ∣ f k ∣ ) \left( \frac{i+0.5}{\vert f_k \vert}, \frac{j+0.5}{\vert f_k \vert} \right) (fki+0.5,fkj+0.5)
∣ f k ∣ {\vert f_k \vert} fk是第 k 个 feature map 的大小
对应代码是(完整代码看下面)

cx = (j + 0.5) / fmap_dims[fmap]
cy = (i + 0.5) / fmap_dims[fmap]

换成conv4_3层就是

cx = (j + 0.5) / 38
cy = (i + 0.5) / 38

因为都要归一化 所以中心点都除以 feature map的大小
w和h的计算,根据上图已知的,得到下图结果
在这里插入图片描述
这个也就是公式
w k a = s k a h k a = s k / a w^a_k=s_k \sqrt{a} \\ h^a_k=s_k/\sqrt{a} wka=ska hka=sk/a
这里的 s k s_k sk就是

obj_scales = {'conv4_3': 0.1,
              'conv7': 0.2,
              'conv8_2': 0.375,
              'conv9_2': 0.55,
              'conv10_2': 0.725,
              'conv11_2': 0.9}

k=1时 s k s_k sk=0.1
k=2时 s k s_k sk=0.2
k=3时 s k s_k sk=0.375
也就是代码中的
对于 aspect ratio 为 1 时,还增加了一个 priox box,这个 box 的 scale 是 s ′ k = s k s k + 1 s^{\prime}{k}=\sqrt{s_k s_{k+1}} sk=sksk+1 。所以conv4_3层的每个cell有4个Priox Box

论文的式子是 s ′ k = s k s k + 1 s^{\prime}{k}=\sqrt{s_k s_{k+1}} sk=sksk+1
我们这里实际用的公式是要加个括号, s ′ k = s k ∗ ( s k + 1 ) s^{\prime}{k}=\sqrt{s_k * (s_{k+1})} sk=sk(sk+1)

w=obj_scales[fmap] * sqrt(ratio)
h=obj_scales[fmap] / sqrt(ratio)

Prior Box的设计采用PyTorch实现

def create_prior_boxes(self):
    """
    Create the 8732 prior (default) boxes for the SSD300, as defined in the paper.

    :return: prior boxes in center-size coordinates, a tensor of dimensions (8732, 4)
    """
    fmap_dims = {'conv4_3': 38,
                 'conv7': 19,
                 'conv8_2': 10,
                 'conv9_2': 5,
                 'conv10_2': 3,
                 'conv11_2': 1}

    obj_scales = {'conv4_3': 0.1,
                  'conv7': 0.2,
                  'conv8_2': 0.375,
                  'conv9_2': 0.55,
                  'conv10_2': 0.725,
                  'conv11_2': 0.9}

    aspect_ratios = {'conv4_3': [1., 2., 0.5],
                     'conv7': [1., 2., 3., 0.5, .333],
                     'conv8_2': [1., 2., 3., 0.5, .333],
                     'conv9_2': [1., 2., 3., 0.5, .333],
                     'conv10_2': [1., 2., 0.5],
                     'conv11_2': [1., 2., 0.5]}

    fmaps = list(fmap_dims.keys())

    prior_boxes = []

    for k, fmap in enumerate(fmaps):
        for i in range(fmap_dims[fmap]):
            for j in range(fmap_dims[fmap]):
                cx = (j + 0.5) / fmap_dims[fmap]
                cy = (i + 0.5) / fmap_dims[fmap]

                for ratio in aspect_ratios[fmap]:
                    prior_boxes.append([cx, cy, obj_scales[fmap] * sqrt(ratio), obj_scales[fmap] / sqrt(ratio)])

                    # For an aspect ratio of 1, use an additional prior whose scale is the geometric mean of the
                    # scale of the current feature map and the scale of the next feature map
                    if ratio == 1.:
                        try:
                            additional_scale = sqrt(obj_scales[fmap] * obj_scales[fmaps[k + 1]])
                        # For the last feature map, there is no "next" feature map
                        except IndexError:
                            additional_scale = 1.
                        prior_boxes.append([cx, cy, additional_scale, additional_scale])

    prior_boxes = torch.FloatTensor(prior_boxes).to(device)  # (8732, 4)
    prior_boxes.clamp_(0, 1)  # (8732, 4)

    return prior_boxes

以conv4_3的prior为例第一个cell的结果如下

[0.0131, 0.0131, 0.1, 0.1]
[0.0131, 0.0131, 0.234, 0.234] #aspect_ratio为1的时候附加
[0.0131, 0.0131, 0.141, 0.070]
[0.0131, 0.0131, 0.070, 0.141]

另外一种配置的方法(与上面的配置稍微有些不同)

from itertools import product
from itertools import islice
import torch
from math import sqrt


class PriorBox:
    def __init__(self):
        self.image_size = 300
        # number of priors for feature map location (either 4 or 6)
        self.num_priors = [[2], [2, 3], [2, 3], [2, 3], [2], [2]]
        self.variance =  [0.1]
        self.feature_maps = [38, 19, 10, 5, 3, 1]
        self.min_sizes =[30, 60, 111, 162, 213, 264]
        self.max_sizes =  [60, 111, 162, 213, 264, 315]
        self.strides = [8, 16, 32, 64, 100, 300]
        self.aspect_ratios =[[2], [2, 3], [2, 3], [2, 3], [2], [2]]
        self.clip = True

    def __call__(self):

        priors = []
        for k, f in enumerate(self.feature_maps):
            scale = self.image_size / self.strides[k]
            for i, j in product(range(f), repeat=2):
                # unit center x,y
                cx = (j + 0.5) / scale
                cy = (i + 0.5) / scale

                # small sized square box
                size = self.min_sizes[k]
                h = w = size / self.image_size
                priors.append([cx, cy, w, h])

                # big sized square box
                size = sqrt(self.min_sizes[k] * self.max_sizes[k])
                h = w = size / self.image_size
                priors.append([cx, cy, w, h])

                # change h/w ratio of the small sized box
                size = self.min_sizes[k]
                h = w = size / self.image_size
                for ratio in self.aspect_ratios[k]:
                    ratio = sqrt(ratio)
                    priors.append([cx, cy, w * ratio, h / ratio])
                    priors.append([cx, cy, w / ratio, h * ratio])

        priors = torch.tensor(priors)
        if self.clip:
            priors.clamp_(max=1, min=0)
        return priors
    
    
l=PriorBox()()
#输出前4行
iterator = islice(l, 4)
for item in iterator:
    print(item)

目标检测SSD中回归任务是回归的什么?

目标检测SSD中回归4个偏移量是什么样的
坐标表示都是使用 center-size coordinate
一个是先验框Prior,一个是边框(bounding box)其实这里的边框相当于ground true box,手工标注的这只猫在哪里
标注的边框与先验框之间有些误差偏移,我们要回归的就是这些偏移量,回归是差多少就可以和ground true box一样了
在这里插入图片描述在这里插入图片描述 g c x = c x − c ^ x w ^ g c y = c y − c ^ y h ^ g w = log ⁡ ( w w ^ ) g h = log ⁡ ( h h ^ ) \begin{aligned} &g_{c_{x}}=\frac{c_{x}-\hat{c}_{x}}{\widehat{w}}\\ &g_{c_{y}}=\frac{c_{y}-\hat{c}_{y}}{\hat{h}}\\ &g_{w}=\log \left(\frac{w}{\widehat{w}}\right)\\ &g_{h}=\log \left(\frac{h}{\widehat{h}}\right) \end{aligned} gcx=w cxc^xgcy=h^cyc^ygw=log(w w)gh=log(h h)
这个偏移量 g c x , g c y , g w , g ) g_{c_x}, g_{c_y}, g_w, g_) gcx,gcy,gw,g)就是我们将回归边框坐标的样子

Feature Map是如何输出目标的位置和分数的

# Number of prior-boxes we are considering per position in each feature map
n_boxes = {'conv4_3': 4,
           'conv7': 6,
           'conv8_2': 6,
           'conv9_2': 6,
           'conv10_2': 4,
           'conv11_2': 4}
# 4 prior-boxes implies we use 4 different aspect ratios, etc.

# Localization prediction convolutions (predict offsets w.r.t prior-boxes)
self.loc_conv4_3 = nn.Conv2d(512, n_boxes['conv4_3'] * 4, kernel_size=3, padding=1)
self.loc_conv7 = nn.Conv2d(1024, n_boxes['conv7'] * 4, kernel_size=3, padding=1)
self.loc_conv8_2 = nn.Conv2d(512, n_boxes['conv8_2'] * 4, kernel_size=3, padding=1)
self.loc_conv9_2 = nn.Conv2d(256, n_boxes['conv9_2'] * 4, kernel_size=3, padding=1)
self.loc_conv10_2 = nn.Conv2d(256, n_boxes['conv10_2'] * 4, kernel_size=3, padding=1)
self.loc_conv11_2 = nn.Conv2d(256, n_boxes['conv11_2'] * 4, kernel_size=3, padding=1)

# Class prediction convolutions (predict classes in localization boxes)
self.cl_conv4_3 = nn.Conv2d(512, n_boxes['conv4_3'] * n_classes, kernel_size=3, padding=1)
self.cl_conv7 = nn.Conv2d(1024, n_boxes['conv7'] * n_classes, kernel_size=3, padding=1)
self.cl_conv8_2 = nn.Conv2d(512, n_boxes['conv8_2'] * n_classes, kernel_size=3, padding=1)
self.cl_conv9_2 = nn.Conv2d(256, n_boxes['conv9_2'] * n_classes, kernel_size=3, padding=1)
self.cl_conv10_2 = nn.Conv2d(256, n_boxes['conv10_2'] * n_classes, kernel_size=3, padding=1)
self.cl_conv11_2 = nn.Conv2d(256, n_boxes['conv11_2'] * n_classes, kernel_size=3, padding=1)

在这里插入图片描述

我们将整个SSD分为3快
Base convolutions
Auxiliary convolutions
Prediction convolutions
feature map的输出实际就是Prediction convolutions这一部分的输出
以conv9_2为例详细说明
蓝色的表示输出的位置
黄色输出表示分数
在这里插入图片描述因为这一个层有6个prior
表示位置的偏移量有4个分量 6 * 4 =24
表示分数 要看有多少个类别,每个类别是多少分
假设有3个类别 就是6*3 =18,背景也算一个类别
24个通道和18个通道,而通道数就相当于卷积函数的输出
在这里插入图片描述在这里插入图片描述整理输出
在这里插入图片描述
在这里插入图片描述

预测的prior与N个Ground Truth 目标之间是如何匹配的

prior有8732个,一个图片标注了N个目标,Ground Truth就有N个
匹配上了,表示找到目标的位置,根据分数就知道了是什么

交并比(Intersection-over-Union,IoU)、Jaccard overlap

步骤1 、计算8732个prior与N个Ground Truth目标之间的交并比(Intersection-over-Union,IoU)也叫 Jaccard overlap
看图一下就明白了
在这里插入图片描述
下面是PyTorch的实现,C++的实现看这里

def find_intersection(set_1, set_2):
    """
    Find the intersection of every box combination between two sets of boxes that are in boundary coordinates.

    :param set_1: set 1, a tensor of dimensions (n1, 4)
    :param set_2: set 2, a tensor of dimensions (n2, 4)
    :return: intersection of each of the boxes in set 1 with respect to each of the boxes in set 2, a tensor of dimensions (n1, n2)
    """

    # PyTorch auto-broadcasts singleton dimensions
    lower_bounds = torch.max(set_1[:, :2].unsqueeze(1), set_2[:, :2].unsqueeze(0))  # (n1, n2, 2)
    upper_bounds = torch.min(set_1[:, 2:].unsqueeze(1), set_2[:, 2:].unsqueeze(0))  # (n1, n2, 2)
    intersection_dims = torch.clamp(upper_bounds - lower_bounds, min=0)  # (n1, n2, 2)
    return intersection_dims[:, :, 0] * intersection_dims[:, :, 1]  # (n1, n2)


def find_jaccard_overlap(set_1, set_2):
    """
    Find the Jaccard Overlap (IoU) of every box combination between two sets of boxes that are in boundary coordinates.

    :param set_1: set 1, a tensor of dimensions (n1, 4)
    :param set_2: set 2, a tensor of dimensions (n2, 4)
    :return: Jaccard Overlap of each of the boxes in set 1 with respect to each of the boxes in set 2, a tensor of dimensions (n1, n2)
    """

    # Find intersections
    intersection = find_intersection(set_1, set_2)  # (n1, n2)

    # Find areas of each box in both sets
    areas_set_1 = (set_1[:, 2] - set_1[:, 0]) * (set_1[:, 3] - set_1[:, 1])  # (n1)
    areas_set_2 = (set_2[:, 2] - set_2[:, 0]) * (set_2[:, 3] - set_2[:, 1])  # (n2)

    # Find the union
    # PyTorch auto-broadcasts singleton dimensions
    union = areas_set_1.unsqueeze(1) + areas_set_2.unsqueeze(0) - intersection  # (n1, n2)

    return intersection / union  # (n1, n2)

步骤2、设置一个阈值,假设是0.5
如果在目标匹配中一个prior与IoU小于0.5,则不能说该prior“包含”该目标,因此是Negative匹配,如果大于等于0.5,则表示该prior包含该目标,因此是positive匹配。我们可是有8732个prior,那么大多数prior对一个物体的检测都是阴性的(Negative)。
阴性(negative)和阳性(positive)是借用医学的名词

如下图所示
8732个 prior都画出来,就看不出谁是谁了,为了方便起见这里仅仅有7个prior用红色表示,标志的ground truth用黄色表示,图片中有三个目标。
在这里插入图片描述在这里插入图片描述每一个prior都有一个匹配,结果不是positive就是negative
预测中positive匹配,一个目标是有ground truth坐标的,可用作回归任务,negative匹配不需要。
ground truth是有标签(label)的,可以用做分类任务,如果是negative匹配,类别是背景类。
定位损失(Localization loss)是根据我们将positive匹配的预测边框框回归到相应的ground truth坐标的精确度来计算。

由于我们以偏移量 g c x , g c y , g w , g ) g_{c_x}, g_{c_y}, g_w, g_) gcx,gcy,gw,g)的形式预测定位边框的,因此在我们计算损失之前,我们还需要相应地编码ground truth真实坐标。

定位损失( localization loss)是positive匹配的定位边框的编码偏移量与它们的ground truth之间的平均平滑L1损失(averaged Smooth L1)。

localization loss(loc)

在这里插入图片描述经过是L2-》L1-》Smooth L1

L2
L 2 = ∣ f ( x ) − Y ∣ 2 L 2 ′ = 2 f ′ ( x ) ( f ( x ) − Y ) \begin{aligned} L_{2} &=|f(x)-Y|^{2} \\ L_{2}^{\prime} &=2 f^{\prime}(x)(f(x)-Y) \end{aligned} L2L2=f(x)Y2=2f(x)(f(x)Y)

L1
L 1 = ∣ f ( x ) − Y ∣ L 1 ′ = ± f ′ ( x ) \begin{array}{l}{L_{1}=|f(x)-Y|} \\ {L_{1}^{\prime}=\pm f^{\prime}(x)}\end{array} L1=f(x)YL1=±f(x)

Smooth L1
Smooth ⁡ L 1 ( x ) = { 0.5 x 2 , i f ∣ x ∣ < 1 ∣ x ∣ − 0.5 ,  otherwise  \operatorname{Smooth}_{L 1}(x)=\left\{\begin{array}{ll}{0.5 x^{2},} & {i f|x|<1} \\ {|x|-0.5,} & {\text { otherwise }}\end{array}\right. SmoothL1(x)={0.5x2,x0.5,ifx<1 otherwise 

otherwise的情况是
x < − 1  or  x > 1 x<-1 \text { or } x>1 x<1 or x>1
它的导数是
d smooth ⁡ L 1 d x = { x  if  ∣ x ∣ < 1 ± 1  otherwise  \frac{\mathrm{d} \operatorname{smooth}_{L_{1}}}{\mathrm{d} x}=\left\{\begin{array}{ll}{x} & {\text { if }|x|<1} \\ { \pm 1} & {\text { otherwise }}\end{array}\right. dxdsmoothL1={x±1 if x<1 otherwise 

confidence loss(conf)

Cross Entropy API的使用看这里

Cross Entropy 背后的知识看这里

在这里插入图片描述

总的目标损失函数(Total loss)

总的目标损失函数(Total loss)等于 localization loss(loc) 与 confidence loss(conf) 的加权求和
在这里插入图片描述按照论文的说法是:总的目标损失函数,论文里的描述如下,就是综合了上面三个公式
L ( x , c , l , g ) = 1 N ( L c o n f ( x , c ) + α L l o c ( x , l , g ) ) L \left( x,c,l,g \right) = \frac{1}{N} \left( L_{conf}(x,c) + \alpha L_{loc}(x,l,g) \right) L(x,c,l,g)=N1(Lconf(x,c)+αLloc(x,l,g))
解释
1、N是与 ground truth 边框 相匹配的 prior 个数
2、localization loss(loc) 是 Smooth L1 Loss,用在 predict box(l) 与 ground truth box(g) 参数(即中心坐标位置,w、h)中,回归 bounding boxes 的中心位置,以及 width、height
3、confidence loss(conf) 是 Softmax Loss,输入为每一类的置信度 c

Softmax

σ ( z ) i = e z i ∑ j = 1 K e z j  for  i = 1 , … , K  and  z = ( z 1 , … , z K ) ∈ R K \sigma(\mathbf{z})_{i}=\frac{e^{z_{i}}}{\sum_{j=1}^{K} e^{z_{j}}} \text { for } i=1, \ldots, K \text { and } \mathbf{z}=\left(z_{1}, \ldots, z_{K}\right) \in \mathbb{R}^{K} σ(z)i=j=1Kezjezi for i=1,,K and z=(z1,,zK)RK
4、权重项 α= 1

预测

在模型训练完后,我们输入一张图片,模型输出的两个tensor包括8732个prior的定位偏移量和类别分数,这时候还得继续解析,解析成 人可以读的
一个边框对应一个目标, 如果一个目标有多个边框,这些边框都是候选边框,就要去掉多余的,留下哪个,非极大值抑制(Non-Maximum Suppression,NMS就登场了
下面这张图清楚的展示了3个目标,但是模型输出了5个目标
在这里插入图片描述我们把每个类别按照分数从大到小排列起来
在这里插入图片描述我们已经有一个工具来计算两个边框的相同程度,那就是上面说的交并比(IoU)
在给定类别中计算所有候选边框间的交并比,如果我们计算每对的候选框有大的重叠,仅保留分数高的那个。

关于狗的这一部分,关于需要计算的两两边框,这个叫pair
如果两只狗的边框IoU大于0.5,他们有更大的可能是相同的狗
猫B与得分更高的猫A是同一只猫
因为当发现多个候选边框彼此之间有明显重叠时,它们可能指向同一个目标,因此我们将除得分最高的那一个以外的所有候选边框都抑制掉
在这里插入图片描述计算结果
在这里插入图片描述

SSD的整体结构

SSD300(
  (base): VGGBase(
    (conv1_1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv1_2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (conv2_1): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv2_2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (conv3_1): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv3_2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv3_3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=True)
    (conv4_1): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv4_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv4_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (pool4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (conv5_1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv5_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (conv5_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (pool5): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
    (conv6): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(6, 6), dilation=(6, 6))
    (conv7): Conv2d(1024, 1024, kernel_size=(1, 1), stride=(1, 1))
  )
  (aux_convs): AuxiliaryConvolutions(
    (conv8_1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
    (conv8_2): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
    (conv9_1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1))
    (conv9_2): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
    (conv10_1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1))
    (conv10_2): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1))
    (conv11_1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1))
    (conv11_2): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1))
  )
  (pred_convs): PredictionConvolutions(
    (loc_conv4_3): Conv2d(512, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (loc_conv7): Conv2d(1024, 24, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (loc_conv8_2): Conv2d(512, 24, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (loc_conv9_2): Conv2d(256, 24, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (loc_conv10_2): Conv2d(256, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (loc_conv11_2): Conv2d(256, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (cl_conv4_3): Conv2d(512, 84, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (cl_conv7): Conv2d(1024, 126, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (cl_conv8_2): Conv2d(512, 126, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (cl_conv9_2): Conv2d(256, 126, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (cl_conv10_2): Conv2d(256, 84, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (cl_conv11_2): Conv2d(256, 84, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  )
)

看整体结果
(pred_convs): PredictionConvolutions中的每一层都是接在对应层之后,例如

conv8_2除了继续向下一层 (conv9_1)执行,还要向loc_conv8_2和cl_conv8_2分别输出
相当于这样

1(conv8_2): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
    (conv9_1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1))
2(conv8_2): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
  (loc_conv8_2): Conv2d(512, 24, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
3(conv8_2): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
 (cl_conv8_2): Conv2d(512, 126, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

参考

SSD 论文

SSD 官方Caffe实现

SSD Tensorflow版的实现(很多星星)

SSD PyTorh版的实现 (很多星星)

SSD PyTorch (一文弄懂)

MobileNet版 Caffe 实现

MobileNet v3版 PyTorch 实现

SSD的其他衍生版本

DSSD : Deconvolutional Single Shot Detector

Enhancement of SSD by concatenating feature maps for object detection

Context-aware Single-Shot Detector

Feature-Fused SSD: Fast Detection for Small Objects

FSSD: Feature Fusion Single Shot Multibox Detector

Weaving Multi-scale Context for Single Shot Detector

Extend the shallow part of Single Shot MultiBox Detector via Convolutional Neural Network

Tiny SSD: A Tiny Single-shot Detection Deep Convolutional Neural Network for Real-time Embedded Object Detection

MDSSD: Multi-scale Deconvolutional Single Shot Detector for small objects

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西笑生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值