目标检测之YOLOv2,最详细的代码解析
一、前言
最近一直在研究深度学习在目标检测的应用,看完了YOLOv2的paper和YAD2K的实现源码,来总结一下自己的收获,以便于加深理解。
二、关于目标检测
目标检测可简单划分成两个任务,一个是分类,一个是确定bounding boxes。目前目标检测领域的深度学习方法主要分为两类:two stage的目标检测算法;one stage的目标检测算法。前者是先由算法生成一系列作为样本的候选框,再通过卷积神经网络进行样本分类;后者则不用产生候选框,直接将目标边框定位的问题转化为回归问题处理。正是由于两种方法的差异,在性能上也有不同,前者在检测准确率和定位精度上占优,后者在算法速度上占优。YOLO(You Only Look Once )则是一种one stage的目标检测算法,目前已经迭代发布了三个版本YOLOv1、YOLOv2、YOLOv3。本文着重介绍的是YOLOv2。
三、YOLOv2的改进
作者在论文中主要总结了关于YOLOv2的三个方面改进:Better、Faster、Stronger。这不是本片文章我想分享的主要内容,因为有太多博主已经写的很透彻了,所以这部分我就只是很简单的稍微叙述了作者的思想,公式比较难编辑也基本没写。可以看下我黑体字的概括,如果想要了解更多的细节,可以搜搜别的博客看看。

1、Better
-
(1)batch Normalization
每个卷积层后均使用batch Normalization
采用Batch Normalization可以提升模型收敛速度,而且可以起到一定正则化效果,降低模型的过拟合。在YOLOv2中,每个卷积层后面都添加了Batch Normalization层,并且不再使用droput。使用Batch Normalization后,YOLOv2的mAP提升了2.4%。Bacth_Normalizing -
(2)High ResolutionClassifier
预训练分类模型采用了更高分辨率的图片
YOLOv1先在ImageNet(224x224)分类数据集上预训练模型的主体部分(大部分目标检测算法),获得较好的分类效果,然后再训练网络的时候将网络的输入从224x224增加为448x448。但是直接切换分辨率,检测模型可能难以快速适应高分辨率。所以YOLOv2增加了在ImageNet数据集上使用448x448的输入来finetune分类网络这一中间过程(10 epochs),这可以使得模型在检测数据集上finetune之前已经适用高分辨率输入。使用高分辨率分类器后,YOLOv2的mAP提升了约4%。YOLOv2训练的三个阶段 -
(3)Convolutional With Anchor Boxes
使用了anchor boxes去预测bounding boxes,去掉了最后的全连接层,网络仅采用了卷积层和池化层
在YOLOv1中,输入图片最终被划分为7x7的gird cell,每个单元格预测2个边界框。YOLOv1最后采用的是全连接层直接对边界框进行预测,其中边界框的宽与高是相对整张图片大小的,而由于各个图片中存在不同尺度和长宽比(scales and ratios)的物体,YOLOv1在训练过程中学习适应不同物体的形状是比较困难的,这也导致YOLOv1在精确定位方面表现较差。YOLOv2则引入了一个anchor boxes的概念,这样做的目的就是得到更高的召回率,yolov1只有98个边界框,yolov2可以达到1000多个(论文中的实现是845个)。还去除了全连接层,保留一定空间结构信息,网络仅由卷积层和池化层构成。输入由448x448变为416x416,下采样32倍,输出为13x13x5x25。采用奇数的gird cell 是因为大图像的中心往往位于图像中间,为了避免四个gird cell参与预测,我们更希望用一个gird cell去预测。结果mAP由69.5下降到69.2,下降了0.3,召回率由81%提升到88%,提升7%。尽管mAP下降,但召回率的上升意味着我们的模型有更大的提升空间。 -
(4)Dimension Clusters(关于anchor boxes的第一个问题:如何确定尺寸)
利用Kmeans聚类,解决了anchor boxes的尺寸选择问题
在Faster R-CNN和SSD中,先验框的维度(长和宽)都是手动设定的,带有一定的主观性。如果选取的先验框维度比较合适,那么模型更容易学习,从而做出更好的预测。因此,YOLOv2采用k-means聚类方法对训练集中的边界框做了聚类分析。比较了复杂度和精确度后,选用了K值为5。因为设置先验框的主要目的是为了使得预测框与ground truth的IOU更好,所以聚类分析时选用box与聚类中心box之间的IOU值作为距离指标:距离公式Dimension_Clusters.png -
(5)Direction locationprediction(关于anchor boxes的第二个问题:如何确定位置)
引入Sigmoid函数预测offset,解决了anchor boxes的预测位置问题,采用了新的损失函数
作者借鉴了RPN网络使用的anchor boxes去预测bounding boxes相对于图片分辨率的offset,通过(x,y,w,h)四个维度去确定anchor boxes的位置,但是这样在早期迭代中x,y会非常不稳定,因为RPN是一个区域预测一次,但是YOLO中是169个gird cell一起预测,处于A gird cell 的x,y可能会跑到B gird cell中,到处乱跑,导致不稳定。作者巧妙的引用了sigmoid函数来规约x,y的值在(0,1)轻松解决了这个offset的问题。关于w,h的也改进了YOLOv1中平方差的差的平方的方法,用了RPN中的log函数。 -
(6)Fine-Grained Features
采用了passthrough层,去捕捉更细粒度的特征
YOLOv2提出了一种passthrough层来利用更精细的特征图,Fine-Grained Features之后YOLOv2的性能有1%的提升。 -
(7)Multi-Scale Training
采用不同尺寸的图片训练,提高鲁棒性
由于YOLOv2模型中只有卷积层和池化层,所以YOLOv2的输入可以不限于416x416大小的图片。为了增强模型的鲁棒性,YOLOv2采用了多尺度输入训练策略,具体来说就是在训练过程中每间隔一定的iterations之后改变模型的输入图片大小。由于YOLOv2的下采样总步长为32,输入图片大小选择一系列为32倍数的值:{320,352,384,...,608},输入图片最小为320x320,此时对应的特征图大小为10x10(不是奇数了,确实有点尴尬),而输入图片最大为 608x608,对应的特征图大小为19x19。在训练过程,每隔10个iterations随机选择一种输入图片大小,然后只需要修改对最后检测层的处理就可以重新训练。采用Multi-Scale Training策略,YOLOv2可以适应不同大小的图片,并且预测出很好的结果。
2、Faster
大多数检测框架依赖于VGG-16作为的基本特征提取器。VGG-16是一个强大的,准确的分类网络,但它是不必要的复杂。在单张图像224×224分辨率的情况下VGG-16的卷积层运行一次前馈传播需要306.90亿次浮点运算。YOLO框架使用基于Googlenet架构的自定义网络。这个网络比VGG-16更快,一次前馈传播只有85.2亿次的操作。然而,它的准确性比VGG-16略差。在ImageNet上,对于单张裁剪图像,224×224分辨率下的top-5准确率,YOLO的自定义模型获得了88.0%,而VGG-16则为90.0%。YOLOv2使用Darknet-19网络,有19个卷积层和5个最大池化层。相比YOLOv1的24个卷积层和2个全连接层精简了网络。

3、Stronger
这里作者的想法也很新颖,解决了2个不同数据集相互排斥(mutualy exclusive)的问题。作者提出了WordTree,使用该树形结构成功的解决了不同数据集中的排斥问题。使用该树形结构进行分层的预测分类,在某个阈值处结束或者最终达到叶子节点处结束。下面这副图将有助于WordTree这个概念的理解。

四、YAD2K代码解析
YAD2K用了90%的Keras和10%Tensorflow实现的YOLOv2。下面主要分析一下/yad2k/models/keras_yolo.py
这个文件里的代码。
提示:其实boxes的坐标是[y,x,h,w]而不是[x,y,w,h]。
流程:数据先经过preprocess_true_boxes()函数处理,然后做一些处理输入到模型,损失函数是yolo_loss(),网络最后一个卷积层的输出作为函数yolo_head()的输入,然后再使用函数yolo_eval(),得到结果。
1、preprocess_true_boxes()
这个函数是得到detectors_mask(最佳预测的anchor boxes,每一个true boxes都对应一个anchor boxes),matching_true_boxes(用于后面和pred_boxes做差求loss)代码后都给了比较详细的注释
def preprocess_true_boxes(true_boxes, anchors, image_size): """ 参数 -------------- true_boxes : 实际框的位置和类别,我们的输入。二个维度: 第一个维度:一张图片中有几个实际框 第二个维度: [x, y, w, h, class],x,y 是框中心点坐标,w,h 是框的宽度和高度。x,y,w,h 均是除以图片 分辨率得到的[0,1]范围的比值。 anchors : 实际anchor boxes 的值,论文中使用了五个。[w,h],都是相对于gird cell 的比值。二个维度: 第一个维度:anchor boxes的数量,这里是5 第二个维度:[w,h],w,h,都是相对于gird cell长宽的比值。 [1.08, 1.19], [3.42, 4.41], [6.63, 11.38], [9.42, 5.11], [16.62, 10.52] image_size : 图片的实际尺寸。这里是416x416。 Returns -------------- detectors_mask : 取值是0或者1,这里的shape是[13,13,5,1],四个维度。 第一个维度:true_boxes的中心位于第几行(y方向上属于第几个gird cell) 第二个维度:true_boxes的中心位于第几列(x方向上属于第几个gird cell) 第三个维度:哪个anchor box 第四个维度:0/1。1的就是用于预测改true boxes 的 anchor boxes matching_true_boxes: 这里的shape是[13,13,5,5],四个维度。 第一个维度:true_boxes的中心位于第几行(y方向上属于第几个gird cel) 第二个维度:true_boxes的中心位于第几列(x方向上属于第几个gird cel) 第三个维度:第几个anchor box 第四个维度:[x,y,w,h,class]。这里的x,y表示offset,是相当于gird cell的,w,h是取了log函数的, class是属于第几类。后面的代码会详细看到 """ height, width = image_size num_anchors = len(anchors) assert height % 32 == 0, '输入的图片的高度必须是32的倍数,不然会报错。' assert width % 32 == 0, '输入的图片的宽度必须是32的倍数,不然会报错。' conv_height = height // 32 '进行gird cell划分' conv_width = width // 32 '进行gird cell划分' num_box_params = true_boxes.shape[1] detectors_mask = np.zeros( (conv_height, conv_width, num_anchors, 1), dtype=np.float32) matching_true_boxes = np.zeros( (conv_height, conv_width, num_anchors, num_box_params), dtype=np.float32) '确定detectors_mask和matching_true_boxes的维度,用0填充' for box in true_boxes: '遍历实际框' box_class = box[4:5] '提取类别信息,属于哪类' box = box[0:4] * np.array( [conv_width, conv_height, conv_width, conv_height]) '换算成相对于gird cell的值' i = np.floor(box[1]).astype('int') '(y方向上属于第几个gird cell)' j = np.floor(box[0]).astype('int') '(x方向上属于第几个gird cell)' best_iou = 0 best_anchor = 0 '计算anchor boxes 和 true boxes的iou,找到最佳预测的一个anchor boxes' for k, anchor in enumerate(anchors): # Find IOU between box shifted to origin and anchor box. box_maxes = box[2:4] / 2. box_mins = -box_maxes anchor_maxes = (anchor / 2.) anchor_mins = -anchor_maxes intersect_mins = np.maximum(box_mins, anchor_mins) intersect_maxes = np.minimum(box_maxes, anchor_maxes) intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.) intersect_area = intersect_wh[0] * intersect_wh[1] box_area = box[2] * box[3] anchor_area = anchor[0] * anchor[1] iou = intersect_area / (box_area + anchor_area - intersect_area) if iou > best_iou: best_iou = iou best_anchor = k if best_iou > 0: detectors_mask[i, j, best_anchor] = 1 '找到最佳预测anchor boxes' adjusted_box = np.array( [ box[0] - j, box[1] - i, 'x,y都是相对于gird cell的位置,左上角[0,0],右下角[1,1]' np.log(box[2] / anchors[best_anchor][0]), '对应实际框w,h和anchor boxes w,h的比值取log函数' np.log