在faster—RCNN的基础上,最近又开始着手了解YOLO系列的目标检测网络,YOLO网络改变了faster-rcnn的将目标检测分为候选框提取与分类两部分的模式,直接采用一个较深的卷积神经网络,同时完成边界框的回归与框内物体识别两个任务。
一、预测原理
整个YOLO网络的工作模式如下所示:
可以看到,直到展开成4096前,都是在疯狂卷积,之后,最后一层卷积后,得到了一个7x7x30的张量,这也是最终的预测结果。
那么这个张量代表什么呢,参考YOLO原理可以明白过来,与faster-rcnn相似,7X7即选取49个中心点,针对每一个中心点输出一个n维向量(此处n为30),该向量由什么构成呢:
其中classes就是待检测物体种类个数,此处采用类似softmax的方式输出预测结果。
4就是描述物体大小的窗口坐标,1是该窗口的可信度
k是指对于一个中心点,所预测的窗口数,参考faster-rcnn的anchor机制,就是对一个中心点预测9个窗口,可以这样来理解。
下面放出github代码:
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_initializer=tf.truncated_normal_initializer(0.0, 0.01),
weights_regularizer=slim.l2_regularizer(0.0005)):
net = tf.pad(images, np.array([[0, 0], [3, 3], [3, 3], [0, 0]]), name='pad_1')
net = slim.conv2d(net, 64, 7, 2, padding='VALID', scope='conv_2')
net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_3')
net = slim.conv2d(net, 192, 3, scope='conv_4')
net = slim.max_pool2d(net, 2, padding='SAME', scope='pool_5')
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')
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')
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')
net = slim.conv2d(net, 1024, 3, 2, padding='VALID', scope='conv_28')
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')
#此处输出num_outputs = 7X7X30, 后面采用reshape变为7X7X30
net = slim.fully_connected(net, num_outputs,
activation_fn=None, scope='fc_36')
return net
代码很标准,slim中定义卷积层,全连接层以及采用leaky_relu取代relu作为激活函数。
输出7X7X30维预测向量net。
然后对net进行分解即可得到7X7个分类信息,7X7X4个框坐标以及7X7X2个预测得分。
二、loss定义
完整LOSS由4部分组成,如下:
1、目标框坐标尺寸的回归误差,这里只计算含有object的目标框与ground truth的回归误差,忽视不含object的目标框。
2、含object的目标框与不含object的目标框的confidence误差。
3、分类问题的交叉熵误差。
下面的是代码中的误差层定义:
def loss_layer(self, predicts, labels, scope='loss_layer'):
with tf.variable_scope(scope):
#预测类别
predict_classes = tf.reshape(predicts[:, :self.boundary1], [self.batch_size, self.cell_size, self.cell_size, self.num_class])
#预测窗口置信度
predict_scales = tf.reshape(predicts[:, self.boundary1:self.boundary2], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell])
#预测窗口坐标位置
predict_boxes = tf.reshape(predicts[:, self.boundary2:], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell, 4])
#这个response不知道是什么,但是在label生成的步骤里response直接赋值了1
response = tf.reshape(labels[:, :, :, 0], [self.batch_size, self.cell_size, self.cell_size, 1])
#ground truth
boxes = tf.reshape(labels[:, :, :, 1:5], [self.batch_size, self.cell_size, self.cell_size, 1, 4])
#因为针对每一个点预测了两个框,要把gt复制一下,然后归一化至0-1间
boxes = tf.tile(boxes, [1, 1, 1, self.boxes_per_cell, 1]) / self.image_size
#类别真值
classes = labels[:, :, :, 5:]
offset = tf.constant(self.offset, dtype=tf.float32)
offset = tf.reshape(offset, [1, self.cell_size, self.cell_size, self.boxes_per_cell])
offset = tf.tile(offset, [self.batch_size, 1, 1, 1])
predict_boxes_tran = tf.stack([(predict_boxes[:, :, :, :, 0] + offset) / self.cell_size,
#offset关于中心点转置
(predict_boxes[:, :, :, :, 1] + tf.transpose(offset, (0, 2, 1, 3))) / self.cell_size,
tf.square(predict_boxes[:, :, :, :, 2]),
tf.square(predict_boxes[:, :, :, :, 3])])
predict_boxes_tran = tf.transpose(predict_boxes_tran, [1, 2, 3, 4, 0])
#iou计算,[BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]
iou_predict_truth = self.calc_iou(predict_boxes_tran, boxes)
# calculate I tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]
# 选两个候选框中iou大者负责该目标
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]
# 选两个候选框中iou小者负责该目标
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 - tf.transpose(offset, (0, 2, 1, 3)),
tf.sqrt(boxes[:, :, :, :, 2]),
tf.sqrt(boxes[:, :, :, :, 3])])
boxes_tran = tf.transpose(boxes_tran, [1, 2, 3, 4, 0])
# 分别计算四个loss,并且add_losses
# class_loss
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
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
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
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)
要注意的是,关于boxes的定位误差,这里坐标(x,y) 代表了 bounding box 的中心与 grid cell 边界的相对值。width、height 则是相对于整幅图像的预测值。
三、训练
关于源代码中的train.py文件,只摘取较重要的部分来看:
#优化器,优化net.total_loss,之前的loss都add_loss进来了
self.optimizer = tf.train.GradientDescentOptimizer(
learning_rate=self.learning_rate).minimize(
self.net.total_loss, global_step=self.global_step)
#定义run train_op前会优化一下
with tf.control_dependencies([self.optimizer]):
self.train_op = tf.group(self.averages_op)
# train函数
# 每几步测一下总loss,run优化器
# self.data里存放样本,可以放自己的数据集
def train(self):
train_timer = Timer()
load_timer = Timer()
# 一共15000步
for step in range(1, self.max_iter + 1):
# 加载样本数据
load_timer.tic()
images, labels = self.data.get()
load_timer.toc()
# placehold赋值
feed_dict = {self.net.images: images, self.net.labels: labels}
# 每10步存一下log
if step % self.summary_iter == 0:
# 每100步打印一下样本集遍历情况,当前loss
if step % (self.summary_iter * 10) == 0:
train_timer.tic()
summary_str, loss, _ = self.sess.run(
[self.summary_op, self.net.total_loss, self.train_op],
feed_dict=feed_dict)
train_timer.toc()
log_str = ('{} Epoch: {}, Step: {}, Learning rate: {},'
' Loss: {:5.3f}\nSpeed: {:.3f}s/iter,'
' Load: {:.3f}s/iter, Remain: {}').format(
datetime.datetime.now().strftime('%m/%d %H:%M:%S'),
self.data.epoch,
int(step),
round(self.learning_rate.eval(session=self.sess), 6),
loss,
train_timer.average_time,
load_timer.average_time,
train_timer.remain(step, self.max_iter))
print(log_str)
else:
train_timer.tic()
summary_str, _ = self.sess.run(
[self.summary_op, self.train_op],
feed_dict=feed_dict)
train_timer.toc()
self.writer.add_summary(summary_str, step)
# 其余时间正常训练
else:
train_timer.tic()
self.sess.run(self.train_op, feed_dict=feed_dict)
train_timer.toc()
# 每1000步存一下ckpt
if step % self.save_iter == 0:
print('{} Saving checkpoint file to: {}'.format(
datetime.datetime.now().strftime('%m/%d %H:%M:%S'),
self.output_dir))
self.saver.save(self.sess, self.ckpt_file,
global_step=self.global_step)
如果想用自己的样本还需要看一下pascal_voc.py里面的读样本后的准备工作,才能应用。