目标检测 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']
下载地址
-
2007 trainval (460MB)
-
2012 trainval (2GB)
-
2007 test (451MB)
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 Scale | Aspect Ratios | 每个Position的Prior数 | 该层的Prior总数 |
---|---|---|---|---|---|
conv4_3 | 38, 38 | 0.1 | 1:1, 2:1, 1:2 + an extra prior | 4 | 5776 |
conv7 | 19, 19 | 0.2 | 1:1, 2:1, 1:2, 3:1, 1:3 + an extra prior | 6 | 2166 |
conv8_2 | 10, 10 | 0.375 | 1:1, 2:1, 1:2, 3:1, 1:3 + an extra prior | 6 | 600 |
conv9_2 | 5, 5 | 0.55 | 1:1, 2:1, 1:2, 3:1, 1:3 + an extra prior | 6 | 150 |
conv10_2 | 3, 3 | 0.725 | 1:1, 2:1, 1:2 + an extra prior | 4 | 36 |
conv11_2 | 1, 1 | 0.9 | 1:1, 2:1, 1:2 + an extra prior | 4 | 4 |
总数 | – | – | – | – | 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)
(∣fk∣i+0.5,∣fk∣j+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=skahka=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}}
s′k=sksk+1。所以conv4_3层的每个cell有4个Priox Box
论文的式子是
s
′
k
=
s
k
s
k
+
1
s^{\prime}{k}=\sqrt{s_k s_{k+1}}
s′k=sksk+1
我们这里实际用的公式是要加个括号,
s
′
k
=
s
k
∗
(
s
k
+
1
)
s^{\prime}{k}=\sqrt{s_k * (s_{k+1})}
s′k=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
cx−c^xgcy=h^cy−c^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)−Y∣2=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)−Y∣L1′=±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,∣x∣−0.5,if∣x∣<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的其他衍生版本
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
MDSSD: Multi-scale Deconvolutional Single Shot Detector for small objects