You Only Look Once: Unified, Real-Time Object Detection

前言

在R-CNN 系列的论文中,目标检测被分成了候选区域提取和候选区域分类及精校两个阶段。不同于这些方法,YOLO将整个目标检测任务整合到一个回归网络中。对比Fast R-CNN 提出的两步走的端到端方案,YOLO 的单阶段的使其是一个更彻底的端到端的算法(图1)。YOLO的检测过程分为三步:

  1. 图像Resize到448*448;

  2. 将图片输入卷积网络;

  3. NMS得到最终候选框。

图1:YOLO算法框架

虽然在一些数据集上的表现不如Fast R-CNN及其后续算法,但是YOLO带来的最大提升便是检测速度的提升。在YOLO算法中,检测速度达到了45帧/秒,而一个更快速的Fast Yolo版本则达到了155帧/秒。另外在YOLO的背景检测错误率要低于Fast R-CNN。最后,YOLO算法具有更好的通用性,通过Pascal数据集训练得到的模型在艺术品问检测中得到了比Fast R-CNN更好的效果。

YOLO是可以用在Fast R-CNN中的,结合YOLO和Fast R-CNN两个算法,得到的效果比单Fast R-CNN要更好。

YOLO源码是使用DarkNet框架实现的,由于本人对DarkNet并不熟悉,所以这里我使用YOLO的TensorFlow源码详细解析YOLO算法的每个技术细节和算发动机。

YOL算法详解

YOLO检测速度远远超过R-CNN系列的重要原因是YOLO将整个物体检测统一成了一个回归问题。YOLO的输入是整张待检测图片,输出则是得到的检测结果,整个过程只经过一次卷积网络。Faster R-CNN 虽然使用全卷积的思想实现了候选区域的权值共享,但是每个候选区域的特征向量任然要单独的计算分类概率和bounding box。

1. YOLO输出层详解

图2:窗格

CELL_SIZE = 7

1.2 Bounding Box

BOXES_PER_CELL = 2

代码片段1:bounding box预处理

boxes = tf.tile(boxes, [1, 1, 1, self.boxes_per_cell, 1]) / self.image_size
classes = labels[..., 5:]
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))
...
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)

labels需要往前追溯到Pascal voc文件的解析代码中,位于文件./utils/pascal_voc.py的139和145行

boxes = [(x2 + x1) / 2.0, (y2 + y1) / 2.0, x2 - x1, y2 - y1]
...
label[y_ind, x_ind, 1:5] = boxes

代码片段2:bounding box的标签值处理

predict_boxes = tf.reshape(
    predicts[:, self.boundary2:],
    [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell, 4])

response = tf.reshape(labels[..., 0],[self.batch_size, self.cell_size, self.cell_size, 1])
...
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
...
coord_mask = tf.expand_dims(object_mask, 4)

该部分代码在./test.py文件中:

for i in range(self.boxes_per_cell):
    for j in range(self.num_class):
        probs[:, :, i, j] = np.multiply(class_probs[:, :, j], scales[:, :, i])

1.3 类别C

不同于Faster R-CNN添加了背景类,YOLO仅使用了数据集提供的物体类别,在Pascal VOC中,待检测物体有20类,具体类别内容了列在了配置文件./yolo/config.py

CLASSES = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus',
           'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse',
           'motorbike', 'person', 'pottedplant', 'sheep', 'sofa',
           'train', 'tvmonitor']

图3:YOLO的输出层

2. 输入层

YOLO作为一个统计检测算法,整幅图是直接输入网络的。因为检测需要更细粒度的图像特征,YOLO将图像Resize到了448*448而不是物体分类中常用的224*224的尺寸。resize在./utils/pascal_voc.py中,需要注意的是YOLO并没有采用VGG中先将图像等比例缩放再裁剪的形式,而是直接将图片非等比例resize。所以YOLO的输出图片的尺寸并不是标准比例的。

image = cv2.resize(image, (self.image_size, self.image_size))

3. 骨干架构

YOLO使用了GoogLeNet作为骨干架构,但是使用了更少的参数,同时YOLO也不像GoogLeNet有3个输出层,图4。为了提高模型的精度,作者也使用了在ImageNet进行预训练的迁移学习策略。

图4:YOLO的骨干架构

研究发现,在AlexNet中提出的ReLU存在Dead ReLU的问题,所谓Dead ReLU是指由于ReLU的x负数部分的导数永远为0,会导致一部分神经元永远不会被激活,从而一些参数永远不会被更新。

def leaky_relu(alpha):
    def op(inputs):
        return tf.nn.leaky_relu(inputs, alpha=alpha, name='leaky_relu')
    return op

然而现在的一些文章指出leaky ReLU并不是那么理想,现在尝试网络超参数时ReLU依旧是首选。

4. 损失函数

4.1 noobj

需要注意的是TF的源码(./yolo/config.py)使用的并不是论文和DarkNet源码中给出的超参数。对于损失函数的四个任务,坐标预测,前景预测,背景预测和分类预测的权值使用的权值分别是1,1,2,5。该值并不是非常重要,通常需要根据模型在验证集上的表现调整。

OBJECT_SCALE = 1.0
NOOBJECT_SCALE = 1.0
CLASS_SCALE = 2.0
COORD_SCALE = 5.0

最后还有一个小知识点,为了平衡短边和长边对损失函数的影响,YOLO使用了边长的平方根来减小长边的影响。

YOLO的损失函数见代码片段3

代码片段3:YOLO的损失函数

# 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

5. 后处理

测试样本时,有些物体会被多个单元检测到,NMS用于解决这个问题。

YOLO的优点

YOLO的快速我们已经一再重复,其性能的提升是因为YOLO统一检测框架的提出。同时YOLO在检测背景和通用性上表现也比Fast R-CNN要好。关于为什么YOLO比Fast R-CNN更擅长检测背景我们在4.1节进行了说明。从图5中我们可以看出YOLO的主要问题在于bounding box的精确检测。

  • Correct: 正确分类且IoU>0.5;

  • Localization:正确分类且0.1<IoU<0.5

  • Similar:类别近似且IoU>0.1

  • Other:分类错误且IoU>0.1

  • Background:IoU<0.1的所有样本

图5:Fast R-CNN vs YOLO

YOLO的论文中也指出YOLO的通用性更强,例如在人类画作的数据集上YOLO的表现要优于Fast R-CNN。但是为什么通用性更好至今我尚未想通,等待大神补充。

YOLO的缺点

YOLO的缺点也非常明显,首先其精确检测的能力比不上Fast R-CNN更不要提和其更早提出的Faster R-CNN了。

YOLO的另外一个重要的问题是对小物体的检测,其为了提升速度而粗粒度划分单元而且每个单元的bounding box的功能过度重合导致模型的拟合能力有限,尤其是其很难覆盖到的小物体。YOLO检测小尺寸问题效果不好的另外一个原因是因为其只使用顶层的Feature Map,而顶层的Feature Map已经不会包含很多小尺寸的物体的特征了。

Faster R-CNN之后的算法均趋向于使用全卷积代替全连接层,但是YOLO依旧笨拙的使用了全连接不仅会使特征向量失去对于物体检测非常重要的位置信息,而且会产生大量的参数,影响算法的速度。

不过暂时不用着急,YOLO也在不断进化,YOLOv2,YOLOv3将会在速度和精度上进行优化,我们会在后续的文章中介绍。

最后更新于