SSD(Single Shot Multibox Detecor)算法借鉴了Faster RCNN与YOLO的思想,在一阶网络的基础上使用了固定框进行
区域生成,并利用了多层的特征信息,在速度与检测精度上都有了一定的提升。
5.1.1 SSD的算法流程
SSD算法的算法流程如图5.1所示,输入图像首先经过了VGGNet的基础网络,在此之上又增加了几个卷积层,然后利用3×3的卷积核在6个大小与深浅不同的特征层上进行预测,得到预选框的分类与回归预测值,最后直接预测出结果,或者求得网络损失。
SSD的算法思想,主要可以分为4个方面:
由整个过程可以看出,SSD只进行了一次框的预测与损失计算,属于一阶网络。由于利用了多个特征图,SSD实现了较好的检测精度。接下来将会详细介绍SSD的以上4个方面。
5.2.2 数据增强
SSD做了丰富的数据增强策略,这部分为模型的mAP带来了8.8%的提升,尤其是对于小物体和遮挡物体等难点,数据增强起到了非常重要的作用。
SSD的数据增强整体流程如图5.2所示,总体上包括光学变换与几何变换两个过程。光学变换包括亮度和对比度等随机调整,可以调整图像像素值的大小,并不会改变图像尺寸;几何变换包括扩展、裁剪和镜像等操作,主要负责进行尺度上的变化,最后再进行去均值操作。大部分操作都是随机的过程,尽可能保证数据的丰富性。
SSD使用VGGNet作为基础Backbone,然后为了提取更高语义的特征,在VGGNet后又增加了多个卷积层,最后利用多个特征图进行边框的特征提取。得到深层网络后。
首先,利用人工设置的一系列PriorBox与标签里的边框进行匹配,并根据重叠程度筛选出正、负样本,得到分类与偏移的真值,这一步类似于Faster RCNN中的匹配过程。筛选出正、负样本后,从深层网络中拿出对应的样本的分类预测值与偏移预测值,与真值计算分类和偏移的损失。
5.3.1 基础VGG结构
SSD采用了VGG 16作为基础网络,并在之上进行了一些改善,如图5.5所示。输入图像经过预处理后大小固定为300×300,首先经过VGG16网络的前13个卷积层,然后利用两个卷积Conv 6与Conv 7取代了原来的全连接网络,进一步提取特征。
针对SSD的基础网络,有以下两点需要注意:
SSD的基础网络代码主要在ssd.py中。
5.3.2 深度卷积层
在VGG 16的基础上,SSD进一步增加了4个深度卷积层,用于更高语义信息的提取,如图5.6所示。可以看出,Conv 8的通道数为512,而Conv 9、Conv 10与Conv 11的通道数都为256。从Conv 7到Conv 11,这5个卷积后输出特征图的尺寸依次为19×19、10×10、5×5、3×3和1×1。
为了降低参数量,在此使用了1×1卷积先降低通道数为该层输出通道数的一半,再利用3×3卷积进行特征提取。
利用PyTorch可以很方便地实现该深度卷积层,源代码文件为ssd.py中的def add_extras.
5.3.3 PriorBox与边框特征提取网络
与Faster RCNN的Anchor类似,SSD采用了PriorBox来进行区域生成。不同的是,Faster RCNN首先在第一个阶段对固定的Anchor进行了位置修正与筛选,得到感兴趣区域后,在第二个阶段再对该区域进行分类与回归,而SSD直接将固定大小宽高的PriorBox作为先验的感兴趣区域,利用一个阶段完成了分类与回归。
PriorBox本质上是在原图上的一系列矩形框,如图5.7所示。某个特征图上的一个点根据下采样率可以得到在原图的坐标,SSD先验性地提供了以该坐标为中心的4个或6个不同大小的PriorBox,然后利用特征图的特征去预测这4个PriorBox的类别与位置偏移量。
在Faster RCNN中,所有Anchors对应的特征都来源于同一个特征图,而该层特征的感受野相同,很难处理被检测物体的尺度变化较大的情况,多个大小宽高的Anchors能起到的作用也有限。
从前几章的讲解可以得出,在深度卷积网络中,浅层的特征图拥有较小的感受野,深层的特征图拥有较大的感受野,因此SSD充分利用了这个特性,使用了多层特征图来做物体检测,浅层的特征图检测小物体,深层的特征图检测大物体。
从图5.8中可以看出,SSD使用了第4、7、8、9、10和11这6个卷积层得到的特征图,这6个特征图尺寸越来越小,而其对应的感受野越来越大。6个特征图上的每一个点分别对应4、6、6、6、4、4个PriorBox。
接下来分别利用3×3的卷积,即可得到每一个PriorBox对应的类别与位置预测量。
举个例子,第8个卷积层得到的特征图大小为10×10×512,每个点对应6个PriorBox,一共有600个PriorBox。由于采用的PASCAL VOC数据集的物体类别为21类,因此3×3卷积后得到的类别特征维度为6×21=126,位置特征维度为6×4=24。
如何确定每一个特征图PriorBox的具体大小呢?由于越深的特征图拥有的感受野越大,因此其对应的PriorBox也应越来越大,SSD采用了公式(5-1)来计算每一个特征图对应的PriorBox的尺度。
公式中K的取值为1、2、3、4、5、6,分别对应着SSD中的第4、7、8、9、10、11个卷积层。Sk代表这一层对应的尺度,Smin为0.2,Smax为0.9,分别表示最浅层与最深层对应的尺度与原图大小的比例,即第4个卷积层得到的特征图对应的尺度为0.2,第11个卷积层得到的特征图对应的尺度为0.9。
算法:
综上所述,这一小节一方面生成了共计8732个PriorBox的位置信息,同时也利用卷积网络提取了这8732个PriorBox的特征。
5.3.4 总体网络计算过程
上一节讲解了SSD的PriorBox与特征提取网络。为了更好地梳理网络的前向过程,本节将从代码角度讲述SSD网络的整个前向过程。
def forward(self, x):
# sources保存特征图,loc与conf保存所有PriorBox的位置与类别预测特征
sources = list()
loc = list()
conf = list()
# 对输入图像卷积到conv4_3,将特征添加到sources中
for k in range(23):
x = self.vgg[k](x)
s = self.L2Norm(x)
sources.append(s)
# 继续卷积到Conv 7,将特征添加到sources中
for k in range(23, len(self.vgg)):
x = self.vgg[k](x)
sources.append(x)
# 继续利用额外的卷积层计算,并将特征添加到sources中
for k, v in enumerate(self.extras):
x = F.relu(v(x), inplace=True)
if k % 2 == 1:
sources.append(x)
# 对sources中的特征图利用类别与位置网络进行卷积计算,并保存到loc与conf中
for (x, l, c) in zip(sources, self.loc, self.conf):
loc.append(l(x).permute(0, 2, 3, 1).contiguous())
conf.append(c(x).permute(0, 2, 3, 1).contiguous())
loc = torch.cat([o.view(o.size(0), -1) for o in loc], 1)
conf = torch.cat([o.view(o.size(0), -1) for o in conf], 1)
# 对于训练来说,output包括了loc与conf的预测值及PriorBox的信息
output = (
loc.view(loc.size(0), -1, 4),
conf.view(conf.size(0), -1, self.num_classes),
self.priors
)
return output
上一节的卷积网络得到了所有PriorBox的预测值与边框位置,为了得到最终的结果,还需要进行边框的匹配及损失计算。
SSD的这部分网络后处理可以分为4步:
5.4.1 预选框与真实框的匹配
在求得8732个PriorBox坐标及对应的类别、位置预测后,首先要做的就是为每一个PriorBox贴标签,筛选出符合条件的正样本与负样本,以便进行后续的损失计算。判断依据与Faster RCNN相同,都是通过预测与真值的IoU值来判断。
SSD处理匹配过程时遵循以下4个原则:
# 输入包括IoU阈值、真实边框位置、预选框、方差、真实边框类别
# 输出为每一个预选框的类别,保存在conf_t中,对应的真实边框位置保存在loc_t中
def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
# 注意,这里的truth是最大或最小值的形式,而prior是中心点与宽高形式
# 求取真实框与预选框的IoU
overlaps = jaccard(truths, point_form(priors))
......
# 将每一个真实框对应的最佳PriorBox的IoU设置为2,确保是最优的PriorBox
best_truth_overlap.index_fill_(0, best_prior_idx, 2)
# 对于每一个真实框,其拥有最大IoU的PriorBox要对应到该真实框上,即使在这个PriorBox
# 中该真实框不是最大的IoU,这是为了保证Recall
for j in range(best_prior_idx.size(0)):
best_truth_idx[best_prior_idx[j]] = j
# 每一个PriorBox对应的真实框的位置
matches = truths[best_truth_idx]
# 每一个PriorBox对应的真实类别
conf = labels[best_truth_idx] + 1
# 如果一个PriorBox对应的最大IoU小于0.5,则视为负样本
conf[best_truth_overlap < threshold] = 0
# 进一步计算定位的偏移真值
loc = encode(matches, priors, variances)
loc_t[idx] = loc
conf_t[idx] = conf
5.4.2 定位损失的计算
在完成匹配后,由于有了正、负样本及每一个样本对应的真实框,因此可以进行定位的损失计算。与Faster RCNN相同,SSD使用了smooth L1函数作为定位损失函数,并且只对正样本计算。
5.4.3 难样本挖掘
在完成正、负样本匹配后,由于一般情况下一张图片的物体数量不会超过100,因此会存在大量的负样本。如果这些负样本都考虑则在损失反传时,正样本能起到的作用就微乎其微了,因此需要进行难样本的挖掘。这里的难样本是针对负样本而言的。
Faster RCNN通过限制正负样本的数量来保持正、负样本均衡,而在SSD中,则是保证正、负样本的比例来实现样本均衡。具体做法是在计算出所有负样本的损失后进行排序,选取损失较大的那一部分进行计算,舍弃剩下的负样本,数量为正样本的3倍。
具体实现如代码所示。在计算完所有边框的类别交叉熵损失后,难样本挖掘过程主要分为5步:
# 对于类别损失,进行难样本挖掘,控制比例为1:3
# 所有PriorBox的类别预测量
batch_conf = conf_data.view(-1, self.num_classes)
# 利用交叉熵函数,计算所有PriorBox的类别损失
loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
# 接下来进行难样本挖掘,分为5步
loss_c = loss_c.view(pos.size()[0], pos.size()[1])
# 1:首先过滤掉正样本
loss_c[pos] = 0 # filter out pos boxes for now
loss_c = loss_c.view(num, -1)
# 2:将所有负样本的类别损失排序
_, loss_idx = loss_c.sort(1, descending=True)
# idx_rank为排序后每个PriorBox的排名
_, idx_rank = loss_idx.sort(1)
# 3:计算正样本的数量
num_pos = pos.long().sum(1, keepdim=True)
# 4:控制正、负样本的比例为1∶3
num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
# 5:选择每个batch中负样本的索引
neg = idx_rank < num_neg.expand_as(idx_rank)
5.4.4 类别损失计算
在得到筛选后的正、负样本后,即可进行类别的损失计算。SSD在此使用了交叉熵损失函数,并且正、负样本全部参与计算。
# 计算正、负样本的类别损失
# 将正、负样本的索引扩展为[32, 8732, 21]格式
pos_idx = pos.unsqueeze(2).expand_as(conf_data)
neg_idx = neg.unsqueeze(2).expand_as(conf_data)
# 把类别的预测值从所有的预测中提取出来
conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
# 把类别的真值从所有真值中提取出来
targets_weighted = conf_t[(pos+neg).gt(0)]
loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)
5.5.1 审视SSD
SSD实现了一个较为优雅、简洁的物体检测框架,使用了一阶网络即完成了物体检测任务,达到了同时期物体检测的较高水平。总体上,
SSD主要有以下3个优点:
与此同时,追求更高检测性能的脚步永不会停止,SSD算法也有以下3点限制:
针对SSD的这些问题,后续的学者从多个角度探讨了提升SSD性能的策略,在此介绍4个较为经典的改进算法,分别是DSSD、RSSD、RefineDet及RFBNet算法。
ssd是典型的多尺度输出⽅式,其在多个尺度上进⾏bbox预测,ssd⽹络也分为backbone和head部分。
(1) backbone
⾻架⽹络是标准的vgg16。原始论⽂没有采⽤BN,后⾯有很多新的复现加上了BN。输⼊图⽚是300x300和512x512两种。vgg16是标准的直筒结构。
采⽤VGG16做基础模型,⾸先VGG16是在ILSVRC CLS-LOC数据集预训练,由于vgg后⾯⼏层是分类⽹络,故需要修改, 分别将VGG16的全连接层fc6和fc7转换成3x3的conv6和1x1的conv7,同时将池化层pool5由原来的stride=2的2x2修改为stride=1的3x3。为了配合这种变化,采⽤了⼀种Atrous Algorithm,其实就是conv6采⽤空洞率为6的空洞卷积,在不增加参数与模型复杂度的条件下指数级扩⼤卷积的视野,然后移除dropout层和fc8层。
(2) extra
ssd在vgg16的后⾯扩展了⼏层卷积⽤于进⾏多尺度预测。
(3) head
在vgg16⾻架⽹络的Conv4_3和新增特征图的Conv7,Conv8_2,Conv9_2,Conv10_2,Conv11_2⼀共6个特征图进⾏输出预测,其输出特征图⼤⼩为(38, 38), (19, 19), (10, 10),(5, 5),(3, 3), (1, 1),每个预测输出的通道为(batch, h, w, anchor*(class+4))。包括类别和bbox回归值,其中类别包含背景类。
采⽤多尺度预测的⽬的是希望⼤输出特征图检测⼩物体,⼩特征图检测⼤物体。ssd的预测输出形式和yolo类似,也是学习基于当前anchor的偏移量((lx, ly, lw, lh)),注意此时的anchor就有中⼼坐标的概念(yolo没有),⽽没有yolo中的⽹格概念。具体为:对于任何⼀个特征图的任何⼀个位置,设其预测值为l=(lx, ly, lw, lh),预测框bbox为b=(bx, by, bw, bh),anchor为d=(dx, dy, dw, dh),则有:
可以看出预测的xy值是相对于当前anchor的xy⽅向偏移,⽽wh值是和yolo⼀样,也是真实宽⾼除以anchor宽⾼,然后取log。
上图中蓝⾊的猫是⼩物体,对应的检测特征图是在⽐较⼤的8x8上⾯,虚线框为anchor,结合上⾯公式,很容易知道预测输出本质上学习的是gt相对于anchor的变换(包括平移和缩放,⽽yolo学习的是gt相对于⽹格的平移和相对于anchor的缩放)。因此可以得知,假设训练好后,还原到原图⽐例为如下公式:
注意以上公式上⾯的数值都是需要基于原图⽐例和特征图⽐例进⾏适当缩放,和yolo系列⼀样,所有的原图数值都需要缩放到对应的特征图尺度再进⾏计算的。
(1) anchor⽣成
ssd的loss计算完全依靠anchor,⽽前⾯说过anchor的先验设置⾮常关键,故⾸先需要讲清楚
anchor到底如何设置的。其在不同的尺度上采⽤了数量不同的anchor。具体为:
从上表可以看出,anchor数⽬不全部⼀样,应该是为了考虑速度和精度的平衡。总共的anchor数⽬为38 x 38 x 4 + 19 x 19 x 6 + 10 x 10 x 6 + 5 x 5 x 6 + 3 x 3 x 4 + 1 x 1 x 4=8732。这个anchor数⽬是⾮常多的,所以可以减低训练难度。为了⽅便设置anchor,作者设计了⼀个公式来⽣成anchor,具体为:
k为特征图索引,m为5,⽽不是6,因为第⼀层输出特征图Conv4_3⽐较特殊,是单独设置的,s_k表⽰anchor⼤⼩相对于图⽚的⽐例,s_min和s_max是⽐例的最⼩和最⼤值,论⽂中设置min=0.2,max=0.9,但是实际上代码不是这样写的。
实际上是:对于第⼀个特征图Conv4_3,其先验框的尺度⽐例⼀般设置为s_min/2=0.1,故第⼀层的s_k=0.1,输⼊是300,故conv4_3的min_size=30。
对于从第⼆层开始的特征图,则利⽤上述公式进⾏线性增加,然后再乘以图⽚⼤⼩,可以得到各个特征图上ancho尺度最⼤值分别为60, 111, 162, 213, 264。
最后⼀个特征图conv9_2的size是直接计算的,300*105/100=315。以上计算可得每个特征的min_size和max_size,如下:
计算得到min_size和max_size后,需要再使⽤宽⾼⽐例因⼦来⽣成更多⽐例的anchor,⼀般选取a_r ∈{1, 2, 3, 1/2, 1/3},但是对于⽐例为1的先验框,作者⼜单独多设置了⼀种⽐例为1,s_k=sqrt (s_k *s_k+1)的尺度,所以⼀共是6种尺度。但是在实现时,Conv4_3,Conv8_2和Conv9_2层仅使⽤4个先验框,它们不使⽤⻓宽⽐为3,1/3的先验框,每个单元的先验框的中⼼点分布在各个单元的中⼼。
具体细节如下:
⽬的是保存在该⽐例下,⾯积不变。
以fc7为例,前⾯知道其min_size=60, max_size=111,由于其需要6种⽐例,故⽣成过程是:
(1) 第⼀种⽐例:(min_size, min_size)=(60, 60);
(2) 第⼆种⽐例:(60 x sqrt(2), 60 / sqrt(2)),(60 / sqrt(2), 60 x sqrt(2));
(3) 第三种⽐例:(60 x sqrt(3), 60 / sqrt(3)),(60 / sqrt(3), 60 x sqrt(3));
(4) 第四种⽐例:(sqrt(60 x 110), sqrt(60 x 110))
⼀共6种⽐例,注意,这些anchor的尺⼨都是相对于原图的,其余特征图也是同样⽣成。可以看出这些anchor⽣成规则是⽐较⿇烦的,对于我们⾃⼰的项⽬,可以采⽤默认的anchor,也可以采⽤简单的kmeans算法直接得到。
(2) loss函数
ssd的loss函数⽐较简单,如下所⽰:
分别为分类和bbox回归loss,分类采⽤交叉熵,回归采⽤smooth L1。
xijk是指⽰函数(对应程序就是掩码矩阵),⽤于确定哪些位置的预测值需要计算回归loss。这就需要分析anchor和gt的匹配规则。ssd的anchor匹配机制和yolo相似,但是不完全相同。思想也是要确定训练图⽚中的ground truth与哪个anchor进⾏匹配,ssd的匹配规则和faster rcnn⼀致,具体如下:
确定了匹配规则,上述的xijk其实就固定了。尽管⼀个ground truth可以与多个先验框匹配,但是ground truth相对先验框还是太少了,所以负样本相对正样本会很多。为了保证正负样本尽量平衡,SSD采⽤了hard negative mining,就是对负样本进⾏抽样,抽样时按照置信度误差(预测背景的置信度越⼩,误差越⼤)进⾏降序排列,选取误差的较⼤的top-k作为训练的负样本,以保证正负样本⽐例接近1:3 (代码层⾯的操作其实就是对于负样本,按照输出背景置信度的概率从⼩到⼤进⾏排序,然后取top-k个)。
还有⼀个细节:前⾯说过设置相同预测层的anchor⼤⼩不⼀样,并且不同预测尺度的anchor也不⼀样,本质是希望⼤输出特征图检测⼩物体,⼩特征图检测⼤物体,故实际操作时候,是不分尺度的。
6个尺度的anchor和gt值同时匹配,也即将6个特征层的所有anchor经过相应的变换后到原图,⽐如第3个特征层的相对于原图是变⼩了8倍,那就anchor的⾼和宽都乘以8转换到相对于原图⼤⼩,第5个特征图的anchor相对于原图变⼩了16倍,也是同样的操作。
具体为:不分6个尺度,⽽是匹配时认为就⼀个尺度。简单来说就是⼀共8732个anchor,和某⼀张图⽚中的所有gt应⽤匹配规则(1)和(2),⽽不是对每个尺度的anchor分别应⽤匹配规则。这样得到的结果是:
应⽤匹配规则(1)后,对于任何⼀个gt,⼀定只有⼀个anchor进⾏匹配,这个anchor可能来⾃第⼀个特征图,也可能来⾃第4个特征图,不会出现某⼀个gt和第⼀个特征图中某个anchor匹配,同时和另⼀个特征图中某个anchor匹配;
再应⽤匹配规则(2)后,就会出现某⼀个gt和好⼏个anchor进⾏匹配了。所以在算ssd的loss时候,是不分多尺度,⽽是当做就⼀个尺度的。
说明上⾯情况后,对于yolov3的多尺度预测有⼀个细节需要说明:在ssd的多尺度预测中,假设只考虑匹配规则(1)时候,那么对于任何⼀个gt,⼀定只会和某⼀个输出特征图的某⼀个anchor匹配。但是在yolov3中,就有些不同了,其存在两个版本:
和ssd⼀样,但是只应⽤匹配规则(1),对于某个gt值,⼀定只会和某⼀个输出特征图的某⼀个anchor匹配
对于每个gt值,分别在多个预测尺度上单独匹配,此时就会出现某⼀个gt值就⼀定会和每个输出特征图的某⼀个anchor匹配。
对于(1)的场景,就是现在⻜哥使⽤的版本loss计算⽅法;对于(2)的场景,现在新版的pytorch框架代码采⽤了这种⽅式。我暂时不知道哪⼀种⽅式更好,个⼈简单分析是:
对于(1)的场景,正样本实在是太⼩了,对于每个尺度来说,本⾝正样本就少,这么⼀弄就更⼩了,可能训练不好;
对于(2)的场景,虽然正样本增加了,但是引⼊了很⼤噪声,因为⼩特征图是⽤于检测⼤物体的,但是你现在是强⾏要⼩特征图也检测⼩物体,难度太⼤,会造成训练难度加⼤。
ssd的⼀个细节就是数据增强⾮常关键。
数据增⼴,即每⼀张训练图像,随机的进⾏如下⼏种选择:
采样的patch是原始图像⼤⼩⽐例在[0.1,1]之间,aspect ratio在1/2与2之间。当GT的中⼼(center)在采样的patch中时,保留重叠部分。在这些采样步骤之后,每⼀个采样的patch被resize到固定的⼤⼩,并且以0.5的概率随机的⽔平翻转(horizontally flipped)。主要⽬的就是为了防⽌⽆脑增加,⽽是希望在指定的iou⽐例下产⽣数据,这个增强做法是很不错的。
其实Matching strategy,Hard negative mining,Data augmentation,都是为了加快⽹络收敛⽽设计的。尤其是Data augmentation,翻来覆去的randomly crop,保证每⼀个prior box都获得充分训练⽽已。不过当数据达到⼀定量的时候,不建议再进⾏Data augmentation,毕竟“真”的数据⽐“假”数据还是要好很多。
VGG16中的Conv4_3层作为⽤于检测的第⼀个特征图,由于该层⽐较靠前,其norm较⼤,和后⾯其他检测层数值上差距⽐较⼤,为了平衡数值和梯度,所以在其后⾯增加了⼀个L2Normalization层以保证和后⾯的检测层差异不是很⼤。
使⽤了image expansion data augmentation(通过zoom out来创造⼩的训练样本)技巧来提升SSD在⼩⽬标上的检测效果,所以性能会有所提升。
SSD与Faster RCNN有同样的准确度,并且与Yolo具有同样较快地检测速度。可以发现,ssd是⽐yolov1快的,但是没有yolov2快。另外注意的是:⾕歌基于mobilenet+ssd的⽅案达到了100+的帧率,已经成为了⾕歌⽬标检测标准api,可以说速度上两者差距不算很⼤,精度上ssd和yolov2持平,但是yolov3精度是远⾼于ssd。
⽂章还对SSD的各个trick做了更为细致的分析,下表为不同的trick组合对SSD的性能影响,从表中可以得出如下结论:
此外作者还对多尺度进⾏对⽐实验,认为采⽤多尺度的特征图⽤于检测也是⾄关重要的。
✔️ 根据SSD的论文描述,作者采用了vgg16的部分网络作为基础网络,在5层网络后,丢弃全连接,改为两个卷积网络,分别为:1024x3x3、1024x1x1。
✏️ 值得注意: 1. conv4-1前面一层的maxpooling的ceil_mode=True,使得输出为 38x38; 2. Conv4-3网络是需要输出多尺度的网络层; 3. Conv5-3后面的一层maxpooling参数为(kernel_size=3, stride=1, padding=1),不进行下采样。
输出网络结构:
Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=True)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace)
(30): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
(31): Conv2d(512, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(6, 6), dilation=(6, 6))
(32): ReLU(inplace)
(33): Conv2d(1024, 1024, kernel_size=(1, 1), stride=(1, 1))
(34): ReLU(inplace)
)
作者为了后续的多尺度提取,在VGG Backbone后面添加了卷积网络。
网络层次:
PS: 红框的网络需要进行多尺度分析,输入到multi-box网络。
输出:
Sequential(
(0): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
(1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
(2): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1))
(3): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
(4): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1))
(5): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1))
(6): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1))
(7): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1))
)
SSD一共有6层多尺度提取的网络,每层分别对 loc 和 conf 进行卷积,得到相应的输出。
网络层次:
输出:
'''
loc layers:
'''
Sequential(
(0): Conv2d(512, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): Conv2d(1024, 24, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(2): Conv2d(512, 24, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): Conv2d(256, 24, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(4): Conv2d(256, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(5): Conv2d(256, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
---------------------------
'''
conf layers:
'''
Sequential(
(0): Conv2d(512, 84, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): Conv2d(1024, 126, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(2): Conv2d(512, 126, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): Conv2d(256, 126, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(4): Conv2d(256, 84, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(5): Conv2d(256, 84, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
输出:
Loc shape: torch.Size([1, 8732, 4])
Conf shape: torch.Size([1, 8732, 21])
Priors shape: torch.Size([8732, 4])
✔️ SSD从Conv4_3开始,一共提取了6个特征图,其大小分别为 (38,38),(19,19),(10,10),(5,5),(3,3),(1,1)
,但是每个特征图上设置的先验框数量不同。
✔️ 先验框的设置,包括尺度(或者说大小)和长宽比两个方面。对于先验框的尺度,其遵守一个线性递增规则:随着特征图大小降低,先验框尺度线性增加:
其中:m 指特征图个数,但是为5,因为第一层(Conv4_3)是单独设置的;
表示先验框大小相对于图片的比例;
和
表示比例的最小值与最大值,paper里面取 0.2 和 0.9。
1、对于第一个特征图,它的先验框尺度比例设置为
,则其尺度为
;
2、对于后面的特征图,先验框尺度按照上面公式线性增加,但是为了方便计算,先将尺度比例先扩大100倍,此时增长步长为:
3、根据上面的公式,则有:
4、将上面的值除以100,然后再乘回原图的大小300,再综合第一个特征图的先验框尺寸,则可得各个特征图的先验框尺寸为:
5、先验框的长宽比一般设置为:
6、根据面积和长宽比可得先验框的宽度和高度:
7、默认情况下,每个特征图会有一个
且尺度为
的先验框,除此之外,还会设置一个尺度为
且
的先验框,这样每个特征图都设置了两个长宽比为1但大小不同的正方形先验框;
8、最后一个特征图需要参考一个虚拟
来计算
9、因此,每个特征图一共有 6 个先验框
,但是在实现时,Conv4_3,Conv10_2和Conv11_2层仅使用4个先验框,它们不使用长宽比为
的先验框;
10、每个单元的先验框的中心点分布在各个单元的中心,即:
其中
为特征图的大小。
因此,SSD 先验框共个数:
num_priors = 38x38x4+19x19x6+10x10x6+5x5x6+3x3x4+1x1x4=8732
✔️ SSD的损失函数包括两部分的加权:
整个损失函数为:
其中:
1. 对于位置损失函数:
针对所有的正样本,采用 Smooth L1 Loss, 位置信息都是 encode
之后的位置信息。
2. 对于置信度损失函数:
首先需要使用 hard negative mining 将正负样本按照 1:3
的比例把负样本抽样出来,抽样的方法是:
思想: 针对所有batch的confidence,按照置信度误差进行降序排列,取出前top_k
个负样本。
编程:
batch_conf = conf_data.view(-1, self.num_classes)
|logsoftmax|
越大,降序排列-logsoftmax
,取前 top_k
的负样本。log_softmax与softmax的区别在哪里?https://www.zhihu.com/question/358069078
详细分析:
这里借用logsoftmax的思想:
为了防止数值溢出,可以把问题转化为:
上述变换的关键在于,我们引入了一个不牵涉log或exp函数的常数项c。
现在我们只需为 c 选择一个在所有情形下有效的良好的值,结果发现,$max(x_1…x_n)$
很不错。
由此我们可以构建对数softmax的新表达式:
因此,可以把排序的函数定义为:
python代码:
logSumExp的表示为:
def log_sum_exp(x):
x_max = x.detach().max()
return torch.log(torch.sum(torch.exp(x-x_max), 1, keepdim=True))+x_max
conf_logP
表示为:
conf_logP = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
排除正样本
conf_logP.view(batch, -1) # shape[b, M]
conf_logP[pos] = 0 # 把正样本排除,剩下的就全是负样本,可以进行抽样
两次sort,能够得到每个元素在降序排列中的位置idx_rank
_, index = conf_logP.sort(1, descending=True)
_, idx_rank = index.sort(1)
可以参考如下表:
两次sort后续,就可以筛选出所需的负样本,配合正样本求出conf的cross entropy。
✔️ VGG网络的conv4_3特征图大小38x38,网络层靠前,norm较大,需要加一个L2 Normalization,以保证和后面的检测层差异不是很大。
L2 norm 的公式如下:
其中:
注意,如果我们不按比例缩小学习范围,简单地对一个层的每个输入进行标准化就会改变该层的规模,并且会减慢速度学习,因此需要引入一个scaling paraneter
,对于每一个通道,l2 norm 变为:
通常,scale 值设为10或20,效果比较好。
✔️ Bounding Box的位置表示方式有两种:
A:
B:
代码:
# B --> A
def point_form(boxes):
'''
把 prior_box (cx, cy, w, h)转化为(xmin, ymin, xmax, ymax)
'''
return torch.cat((boxes[:, :2] - boxes[:, 2:]/2, # xmin, ymin
boxes[:, :2] + boxes[:, 2:]/2,), 1) # xmax, ymax
# A --> B
def center_size(boxes):
'''
把 prior_box (xmin, ymin, xmax, ymax) 转化为 (cx, cy, w, h)
'''
return torch.cat((boxes[:, :2] + boxes[:, 2:])/2, # cx, cy
(boxes[:, 2:] - boxes[:, :2],), 1) # w, h
✔️ 根据论文的描述,预测和真实的边界框是有一个转换关系的,具体如下:
用于调整检测值
编码: 得到预测框相对于default box的偏移量 l。
解码: 从预测值 l 中得到边界框的真实值。
✔️ 在训练过程中,首先需要确定训练图片中的 ground truth 与哪一个先验框来进行匹配,与之匹配的先验框所对应的边界框将负责预测它。
✔️ SSD的先验框和ground truth匹配原则主要两点: 1. 对于图片中的每个gt,找到与其IOU最大的先验框,该先验框与其匹配,这样可以保证每个gt一定与某个prior匹配。 2. 对于剩余未匹配的priors,若某个gt的IOU大于某个阈值(一般0.5),那么该prior与这个gt匹配。
注意点:
2. 某个gt可以和多个prior匹配,而每个prior只能和一个gt进行匹配。
3. 如果多个gt和某一个prior的IOU均大于阈值,那么prior只与IOU最大的那个进行匹配。
✔️ 非极大值抑制(Non-maximum suppression,NMS)是一种去除非极大值的算法,常用于计算机视觉中的边缘检测、物体识别等。
算法流程:
✔️ 给出一张图片和上面许多物体检测的候选框(即每个框可能都代表某种物体),但是这些框很可能有互相重叠的部分,我们要做的就是只保留最优的框。假设有N个框,每个框被分类器计算得到的分数为
代码:
✔️ NMS算法一般是为了去掉模型预测后的多余框,其一般设有一个nms_threshold=0.5,具体的实现思路如下:
boxes[i]
与其余的 boxes
的 IOU
值;IOU>0.5
了,那么就舍弃这个box(由于可能这两个box表示同一目标,所以保留分数高的哪一个)✔️ 模型进行测试的时候,需要把预测出的loc和conf输入到detect函数进行nms,最后给出相应的结果。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。