此SSD非彼SSD,不过都有一个特点快,我之前读过了这篇,这次算是重温,而且前面介绍了很多检测网络,尤其是FPN时更是对SSD有一个很根本的解读,所以这篇博客算是一个SSD精华介绍,哈哈。
SSD最大的贡献,就是在多个feature map上进行预测,这点我在上一篇FPN也说过它的好处,可以适应更多的scale。第二个是用小的卷积进行分类回归,区别于YOLO及其faster rcnn的fc,大大降低参数和提速。
最大的特点就是不同于region-based的方法如faster rcnn等,ssd是没有region proposal这个过程的,follow了YOLO的工作,把检测的任务转化为了一个回归任务,直接对default box进行暴力回归。
论文中对不同scale的feature map进行预测,主要是conv4_3(之前在问过作者为什么选用conv4_3,这是作者在另一篇parsing的文章ParseNet得到的经验,这一层和特别,具体是什么就是实验证明这个层好)以及多加了一些更强语义的feature。注意 SSD的分类和坐标回归都是全卷积的,不需要roipooling,因为faster rcnn是两步先有rpn对anchor进行2分类和回归然后修正anchor后的proposal需要重新映射到feature上取feature进行后面的细分类和回归,而SSD是一步到位的,直接对default box进行细分类和一次暴力回归。以以C=21类,Conv4_3,分类为例,用3*3的卷积对feature上每个点进行分类,得到84*h*w的结果,类似分割正常应该是21channel的,由于Conv4_3feature上每个点是对应4个default(prior) box的,所以是84channel
# conv4_3上设置的4个不同的比例,feature上每个点对应4个default(prior) box
layer {
name: "conv4_3_norm_mbox_conf"
type: "Convolution"
bottom: "conv4_3_norm"
top: "conv4_3_norm_mbox_conf"
convolution_param {
num_output: 84 # 21*4 feature上每个点对应4个default(prior) box, 21个类别
pad: 1
kernel_size: 3
stride: 1
}
}
layer {
name: "conv4_3_norm_mbox_loc"
type: "Convolution"
bottom: "conv4_3_norm"
top: "conv4_3_norm_mbox_loc"
convolution_param {
num_output: 16 # 4*4 feature上每个点对应4个default(prior) box, 4个坐标
pad: 1
kernel_size: 3
stride: 1
}
}
这里引用一张别人解读的文章Single Shot MultiBox Detector(SSD)的图片,我们刚刚也说了得到feature上每个点对应boxes(4/6)的回归值16*h*w,然后作者把这个拉平成一个向量(红线处flat),然后和gt做loss。就这样简单用一个3*3的卷积得到回归值然后做loss 佩服 网络这能都学会(其实faster rcnn也差不多是用1*1做的,任务简单些)。另一部分就正常softmax分类。
之前说fpn(发表晚于ssd)时也讲到,解决不同scale物体的检测,通常的做法就是放缩图片送到网络然后融合结果,但是费时,而ssd就是在不同scale的feature上预测不同大小的物体来解决这个问题,不同scale的feature视野域不同,刚好大物体在后面的feature预测,小物体在前面预测。说到底这个default box是十分类似于faster rcnn的anchor的,只是这个box是在不同scale的feature上取的。
不同层default box的大小公式如下,注意每个feature上的点虽然对应4或6个box,但是box并不是整个点对回去的原图,看公式最后的层最大也就0.9。feature上每个点的中心即是其对应box的中心,scale为1时再附带一个S’k的scale。
# SSD300 6个预测的层,他们的步长和比例设置 最后一个feature大小1*1大小步长为300
# 比例为1的其实两个,每个层都有忽略不写了 2是2和1/2 3是3和1/3
mbox_source_layers = ['conv4_3', 'fc7', 'conv6_2', 'conv7_2', 'conv8_2', 'conv9_2']
steps = [8, 16, 32, 64, 100, 300]
aspect_ratios = [[2], [2, 3], [2, 3], [2, 3], [2], [2]]
mbox_source_layers = ['conv4_3', 'fc7', 'conv6_2', 'conv7_2', 'conv8_2', 'conv9_2']
# in percent %
min_ratio = 20
max_ratio = 90
step = int(math.floor((max_ratio - min_ratio) / (len(mbox_source_layers) - 2)))
min_sizes = []
max_sizes = []
for ratio in xrange(min_ratio, max_ratio + 1, step):
min_sizes.append(min_dim * ratio / 100.)
max_sizes.append(min_dim * (ratio + step) / 100.)
min_sizes = [min_dim * 10 / 100.] + min_sizes
max_sizes = [min_dim * 20 / 100.] + max_sizes
print min_sizes
print max_sizes
'''
conv4_3特殊对待了
[30.0, 60.0, 111.0, 162.0, 213.0, 264.0]
[60.0, 111.0, 162.0, 213.0, 264.0, 315.0]
'''
'''
default box的生成
'feature_maps' : [38, 19, 10, 5, 3, 1],
'min_dim' : 300,
'steps' : [8, 16, 32, 64, 100, 300],
'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]],
'''
for k, f in enumerate(self.feature_maps):
for i, j in product(range(f), repeat=2):
f_k = self.image_size / self.steps[k]
# unit center x,y
cx = (j + 0.5) / f_k
cy = (i + 0.5) / f_k
# aspect_ratio: 1
# rel size: min_size
s_k = self.min_sizes[k] / self.image_size
mean += [cx, cy, s_k, s_k]
# aspect_ratio: 1
# rel size: sqrt(s_k * s_(k+1))
s_k_prime = sqrt(s_k * (self.max_sizes[k] / self.image_size))
mean += [cx, cy, s_k_prime, s_k_prime]
# rest of aspect ratios
for ar in self.aspect_ratios[k]:
mean += [cx, cy, s_k * sqrt(ar), s_k / sqrt(ar)]
mean += [cx, cy, s_k / sqrt(ar), s_k * sqrt(ar)]
Conv4_3 38*38的feature map通过3*3的卷积层预测4个不同宽高比的box,Conv7 19*19预测6个,Conv8_2, Conv9_2预测6个不同长宽比的box,Conv 10_2,Conv11_2预测4个。所以38*38*4+19*19*6+10*10*6+5*5*6+3*3*4+4 = 8732,所有不同scale feature map上的点对应8732个box。可能大物体和小物体不占大多数,所以Conv4_3 Conv 10_2 Conv11_2都只有4个scale,而且Conv4_3太大了,scale太多box太多了。
还有一个就是给这些box指定gt的策略,基本和faster rcnn类似
1. 对于每个gt,将其指定给与它iou最大的box,保证每个gt都有box去回归
2. 对于剩下的box,只要与任一gt iou>0.5(faster rcnn是0.7),都将这个gt指定给它
3. 貌似剩下的都是负样本了(faster rcnn是与任一gt的iou<0.3才为负样本,其他非正非负忽略掉),这点文中没有直接说,等我看下代码吧
按照 default boxes 的 confidence 的大小。 选择最高的几个,保持最后的正负样本为1:3,文中训练时没说用nms哎,只有在inference时说了用nms,具体还要我再刷一遍代码。
By using a confidence threshold of 0.01, we can filter out most boxes. We then apply nms with jaccard overlap of 0.45 per class and keep the top 200 detections per image.
数据增强:
1. 使用原始的图像
2. 从原图随机采样一个 patch,但是采样后的patch中与原图所有gt boxes iou的最小值要满足 >0.1 0.3…0.9
3. 从原图随机采样 patch没有任何限制
采样从以上3种形式中随机选取,在这些采样步骤之后,每一个采样的patch被resize到固定的大小,并且以0.5的概率随机的 水平翻转(horizontally flipped)。除此之外,SSD还有一些提前的数据增强S光照啊旋转等等,这也是它最被argue的一点了。
def _crop(image, boxes, labels):
height, width, _ = image.shape
if len(boxes)== 0:
return image, boxes, labels
while True:
'''
1. 使用原始的图像
对应mode: None
if mode is None:
return image, boxes, labels
2. 从原图随机采样一个 patch,但是采样后的patch中与原图所有gt boxes iou的最小值要满足 >0.1 0.3...0.9
对应mode: (0.1, None),(0.3, None),等
iou = matrix_iou(boxes, roi[np.newaxis])
if not (min_iou <= iou.min() and iou.max() <= max_iou):
continue
3. 从原图随机采样 patch没有任何限制
对应mode: (None, None)
if min_iou is None:
min_iou = float('-inf')
if max_iou is None:
max_iou = float('inf')
'''
mode = random.choice((
None,
(0.1, None),
(0.3, None),
(0.5, None),
(0.7, None),
(0.9, None),
(None, None),
))
if mode is None:
return image, boxes, labels
min_iou, max_iou = mode
if min_iou is None:
min_iou = float('-inf')
if max_iou is None:
max_iou = float('inf')
for _ in range(50):
scale = random.uniform(0.3,1.)
min_ratio = max(0.5, scale*scale)
max_ratio = min(2, 1. / scale / scale)
ratio = math.sqrt(random.uniform(min_ratio, max_ratio))
w = int(scale * ratio * width)
h = int((scale / ratio) * height)
l = random.randrange(width - w)
t = random.randrange(height - h)
roi = np.array((l, t, l + w, t + h))
iou = matrix_iou(boxes, roi[np.newaxis])
if not (min_iou <= iou.min() and iou.max() <= max_iou):
continue
image_t = image[roi[1]:roi[3], roi[0]:roi[2]]
centers = (boxes[:, :2] + boxes[:, 2:]) / 2
mask = np.logical_and(roi[:2] < centers, centers < roi[2:]) \
.all(axis=1)
boxes_t = boxes[mask].copy()
labels_t = labels[mask].copy()
if len(boxes_t) == 0:
continue
boxes_t[:, :2] = np.maximum(boxes_t[:, :2], roi[:2])
boxes_t[:, :2] -= roi[:2]
boxes_t[:, 2:] = np.minimum(boxes_t[:, 2:], roi[2:])
boxes_t[:, 2:] -= roi[:2]
return image_t, boxes_t,labels_t
效果真的很赞,又快又好(和作者强大的码力也有很大关系的,膜拜中),一些消融实验就不一一介绍了。
参考了Single Shot MultiBox Detector(SSD)
faster rcnn和ssd我都参看了知乎专栏:机器学习随笔,收益匪浅,ps这个作者之前在csdn现在所有csdn文章都删掉挂到知乎了,特别感谢。