首先是yolo1,英文全称就是You Only Look Once,是典型的end-to-end结构,输入一张图像,回归出位置和置信率。其网络结构如下:
24个卷积层加两个全连接层,其相应结构部分的代码实现如下:
def build_network(self,
images,
num_outputs,
alpha,
keep_prob=0.5,
is_training=True,
scope='yolo'):
with tf.variable_scope(scope):
with slim.arg_scope(
[slim.conv2d, slim.fully_connected],
activation_fn=leaky_relu(alpha),
weights_regularizer=slim.l2_regularizer(0.0005),
weights_initializer=tf.truncated_normal_initializer(0.0, 0.01)
):
net = tf.pad(
images, np.array([[0, 0], [3, 3], [3, 3], [0, 0]]),
name='pad_1')#最开始是batchsizex448x448x3,填充成batchsizex454x454x3
net = slim.conv2d(
net, 64, 7, 2, padding='VALID', scope='conv_2')#(454-7+1)/2=224
net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_3')#112
net = slim.conv2d(net, 192, 3, scope='conv_4')
net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_5')#56
net = slim.conv2d(net, 128, 1, scope='conv_6')
net = slim.conv2d(net, 256, 3, scope='conv_7')
net = slim.conv2d(net, 256, 1, scope='conv_8')
net = slim.conv2d(net, 512, 3, scope='conv_9')
net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_10')#28
net = slim.conv2d(net, 256, 1, scope='conv_11')
net = slim.conv2d(net, 512, 3, scope='conv_12')
net = slim.conv2d(net, 256, 1, scope='conv_13')
net = slim.conv2d(net, 512, 3, scope='conv_14')
net = slim.conv2d(net, 256, 1, scope='conv_15')
net = slim.conv2d(net, 512, 3, scope='conv_16')
net = slim.conv2d(net, 256, 1, scope='conv_17')
net = slim.conv2d(net, 512, 3, scope='conv_18')
net = slim.conv2d(net, 512, 1, scope='conv_19')
net = slim.conv2d(net, 1024, 3, scope='conv_20')
net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_21')#14
net = slim.conv2d(net, 512, 1, scope='conv_22')
net = slim.conv2d(net, 1024, 3, scope='conv_23')
net = slim.conv2d(net, 512, 1, scope='conv_24')
net = slim.conv2d(net, 1024, 3, scope='conv_25')
net = slim.conv2d(net, 1024, 3, scope='conv_26')
net = tf.pad(
net, np.array([[0, 0], [1, 1], [1, 1], [0, 0]]),
name='pad_27')#填充成16
net = slim.conv2d(
net, 1024, 3, 2, padding='VALID', scope='conv_28')#(16-3+1)/2=7
net = slim.conv2d(net, 1024, 3, scope='conv_29')
net = slim.conv2d(net, 1024, 3, scope='conv_30')
net = tf.transpose(net, [0, 3, 1, 2], name='trans_31')
net = slim.flatten(net, scope='flat_32')
net = slim.fully_connected(net, 512, scope='fc_33')
net = slim.fully_connected(net, 4096, scope='fc_34')
net = slim.dropout(
net, keep_prob=keep_prob, is_training=is_training,
scope='dropout_35') #4096
net = slim.fully_connected( #1470
net, num_outputs, activation_fn=None, scope='fc_36')
return net
图片分成7x7大小,论文中采取的方式如下:
将图片分成SxS小块(这里S=7),如果目标的中心落在某一小块上,则这一小块负责识别这个目标。每一小块预测B个bounding boxes(论文中B=2)和它们的置信度,这就包括5个预测值(x,y,w,h,conf),也就是box中心点横纵坐标、宽度和高度以及置信度,其中置信度用预测框和ground truth box的IOU表示。另外,每个小块还要预测C种类别的概率(论文中C=20)。这些一起就组成了网络的输出张量长度SxSx(B*5+C)=7*7*30=1470。
另外,关于损失函数。它由5部分组成:
第一、二项为位置回归损失,中心点坐标和归一化后的宽度和高度,第三项是小格内有目标的置信度误差(预测框与真实框的IOU),第四项是小格内没目标(可以看做就是预测框,预测框大小越接近0,预测越准确),第五项是预测类别误差。其代码实现如下:
def loss_layer(self, predicts, labels, scope='loss_layer'):#Predicts中是归一化的,labels中没归一化
with tf.variable_scope(scope):
predict_classes = tf.reshape(#45x7x7x20
predicts[:, :self.boundary1],
[self.batch_size, self.cell_size, self.cell_size, self.num_class])
predict_scales = tf.reshape(#45x7x7x2
predicts[:, self.boundary1:self.boundary2],
[self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell])
predict_boxes = tf.reshape(#45x7x7x2x4
predicts[:, self.boundary2:],
[self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell, 4])
response = tf.reshape(
labels[..., 0],#置信度,0/1是否有目标
[self.batch_size, self.cell_size, self.cell_size, 1])
boxes = tf.reshape(
labels[..., 1:5],#x,y,w,h
[self.batch_size, self.cell_size, self.cell_size, 1, 4])
boxes = tf.tile(
boxes, [1, 1, 1, self.boxes_per_cell, 1]) / self.image_size
classes = labels[..., 5:]#20个类别
offset = tf.reshape(
tf.constant(self.offset, dtype=tf.float32),
[1, self.cell_size, self.cell_size, self.boxes_per_cell])
offset = tf.tile(offset, [self.batch_size, 1, 1, 1])
offset_tran = tf.transpose(offset, (0, 2, 1, 3))
predict_boxes_tran = tf.stack(
[(predict_boxes[..., 0] + offset) / self.cell_size,
(predict_boxes[..., 1] + offset_tran) / self.cell_size,
tf.square(predict_boxes[..., 2]),
tf.square(predict_boxes[..., 3])], axis=-1)
iou_predict_truth = self.calc_iou(predict_boxes_tran, boxes)
# calculate I tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]
object_mask = tf.reduce_max(iou_predict_truth, 3, keep_dims=True)
object_mask = tf.cast(
(iou_predict_truth >= object_mask), tf.float32) * response
# calculate no_I tensor [CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]
noobject_mask = tf.ones_like(
object_mask, dtype=tf.float32) - object_mask
boxes_tran = tf.stack( #归一化
[boxes[..., 0] * self.cell_size - offset,
boxes[..., 1] * self.cell_size - offset_tran,
tf.sqrt(boxes[..., 2]),
tf.sqrt(boxes[..., 3])], axis=-1)
# class_loss#误差第5项
class_delta = response * (predict_classes - classes)
class_loss = tf.reduce_mean(
tf.reduce_sum(tf.square(class_delta), axis=[1, 2, 3]),
name='class_loss') * self.class_scale
# object_loss#误差第3项
object_delta = object_mask * (predict_scales - iou_predict_truth)
object_loss = tf.reduce_mean(
tf.reduce_sum(tf.square(object_delta), axis=[1, 2, 3]),
name='object_loss') * self.object_scale
# noobject_loss#误差第4项
noobject_delta = noobject_mask * predict_scales
noobject_loss = tf.reduce_mean(
tf.reduce_sum(tf.square(noobject_delta), axis=[1, 2, 3]),
name='noobject_loss') * self.noobject_scale
# coord_loss#误差第1,2项
coord_mask = tf.expand_dims(object_mask, 4)
boxes_delta = coord_mask * (predict_boxes - boxes_tran)
coord_loss = tf.reduce_mean(
tf.reduce_sum(tf.square(boxes_delta), axis=[1, 2, 3, 4]),
name='coord_loss') * self.coord_scale
tf.losses.add_loss(class_loss)
tf.losses.add_loss(object_loss)
tf.losses.add_loss(noobject_loss)
tf.losses.add_loss(coord_loss)
tf.summary.scalar('class_loss', class_loss)
tf.summary.scalar('object_loss', object_loss)
tf.summary.scalar('noobject_loss', noobject_loss)
tf.summary.scalar('coord_loss', coord_loss)
tf.summary.histogram('boxes_delta_x', boxes_delta[..., 0])
tf.summary.histogram('boxes_delta_y', boxes_delta[..., 1])
tf.summary.histogram('boxes_delta_w', boxes_delta[..., 2])
tf.summary.histogram('boxes_delta_h', boxes_delta[..., 3])
tf.summary.histogram('iou', iou_predict_truth)
yolo2的部分
yolo2是论文介绍的主要网络结构,在此基础上,提出的yolo9000是实时检测框架,也就是说能检测超过9000种目标类别。yolo2主要是针对yolo1在其基础上进行了更好、更快、更强的改进。
better:
1.batch normalization:在每一个卷积层后面添加batch normlization,使其得到2%的MAP提升
2.high resolution classifier:大部分网络分类器的输入图像分辨率小于256X256,yolo1中的网络在224×224分辨率上训练分类器网络,并将分辨率增加到448以用于检测。这就要求网络应该有所调整,而yolo2分辨率改成448 * 448,在ImageNet数据集上训练了10轮,最后得到4%的MAP提升
3.convolutional with anchor boxes: 借鉴anchor思想,去掉了全连接层,然后再去掉后面的一个池化层以确保输出的卷积特征图有更高的分辨率,通过缩减网络,让图片输入分辨率为416 * 416,这一步的目的是为了让后面产生的卷积特征图宽高都为奇数,这样就可以产生一个center cell,使得输入卷积网络的416 * 416图片最终得到13 * 13的卷积特征图(416/32=13)最后具体数据为:没有anchor boxes,模型recall为81%,mAP为69.5%;加入anchor boxes,模型recall为88%,mAP为69.2%
4.dimension clusters:作者在使用anchor box时遇到两个问题,第一个就是box是手动挑选的,网络可以学习适当地调整框,但如果我们能够选择更好的网络先验框,我们就能够更容易预测更好检测。论文中使用了K均值聚类来查找更好的先验box,为了让此方法与box的尺寸无关,选择的距离量度为d(box,centroid)=1-IOU(box,centroid)。
文中选择k=5作为模型复杂度和高召回率的一个平衡。这将更有益于更瘦高的box。下表是box产生的方式、不同质心数据产生的不同平均IOU
5.在使用anchor box中的第二个问题就是模型的不稳定性,特别是在早期迭代的过程中。
预测公式如下:
这个公式没有任何限制,使得无论在什么位置进行预测,任何anchor boxes可以在图像中任意一点结束,模型随机初始化后,需要花很长一段时间才能稳定预测敏感的物体位置。
使用预测相对于grid cell的坐标位置的办法,又把ground truth限制在了0到1之间,利用logistic回归函数来进行这一限制。
6.fine-grained features:上述网络上的修改使YOLO最终在13 * 13的特征图上进行预测,虽然这足以胜任大尺度物体的检测,但是用上细粒度特征的话可能对小尺度的物体检测有帮助, 简单添加了一个转移层,这一层要把浅层特征图连接到深层特征图。这个方法把26 * 26 * 512的特征图连接到了13 * 13 * 2048的特征图,这个特征图与原来的特征相连接。YOLO的检测器使用的就是经过扩张的特征图,它可以拥有更好的细粒度特征,使得模型的性能获得了1%的提升。
7.multi-scale training
YOLO网络使用固定的448 * 448的图片作为输入,现在加入anchor boxes后,输入变成了416 * 416。目前的网络只用到了卷积层和池化层,那么就可以进行动态调整,不同于固定输入网络的图片尺寸的方法,作者在几次迭代后就会微调网络。YOLO网络使用的降采样参数为32,那么就使用32的倍数进行尺度池化{320,352,…,608}。最终最小的尺寸为320 * 320,最大的尺寸为608 * 608。接着按照输入尺寸调整网络进行训练。这种机制使得网络可以更好地预测不同尺寸的图片,意味着同一个网络可以进行不同分辨率的检测任务,在小尺寸图片上YOLOv2运行更快,在速度和精度上达到了平衡。
faster
yolo1使用的是基于Googlenet的定制网络,速度比VGG-16快,但精度不如。yolo2采用的是Darknet-19,有19个卷积层和5个最大池化层,其结构如下:
stronger:
提出了一个关于分类和检测数据的联合训练机制。我们的方法使用标记为检测的图像来学习检测特定信息,如边界框坐标预测和目标以及如何分类常见目标。它使用只有类标签的图像来扩展它可以检测的类别数。当我们的网络看到标记为检测的图像时,我们可以基于完整的yolo2损失函数反向传播。当它看到一个分类图像,我们只反向传播从结构的分类特定部分的损失。
但这种方法提出了一些挑战,检测数据集只有常用目标和常规标签,分类数据集具有更宽和更深的标签范围。如果我们想训练两个数据集,我们需要一种合适的方式来合并这些标签。大多数分类方法在所有可能的类别中使用softmax层来计算最终的概率分布。使用softmax前提是假定类是互斥的,但很多情况下并非如此,很多情况下类别是相互包涵的。于是我们使用多标签模型来组合不承担互斥的数据集。这种方法忽略了我们所知道的关于数据的所有结构
分层分类:计算概率是用连续相乘的方法,如:
采用的数据结构worldTree形式如下:
更多细节有待去代码里面发现。