1. 前言
本来的打算是不打算对YOLO v1进行解读的,因为怎么说,虽然YOLO v1是YOLO 的开山之作,但是说实话,现在YOLO v3都烂大街了,谁还用YOLO v1呀?简单尝试了下看了下代码,发现代码中还是存在一些比较精妙之处的,可以让我们更好了解YOLO系列模型的思想。话不多说,开始进行的我们代码的解读。再次我推荐YOLOv1解读
当然代码看这个就好了
https://github.com/TowardsNorth/yolo_v1_tensorflow_guiyu
2. 代码解读
YOLO V1的代码倒是比FasterRCNN代码看起来简单多了呀,仔细观察,一些逻辑顺序和FasterRCNN倒是挺像的呀。
(1) pascal voc.py:对图片数据和XML数据进行解析和预处理
(2) yolo_net.py:搭建yolo v1网络,设置yolo v1的损失函数
(3) train.py 和test.py :一个用来训练模型,一个用来测试模型
多么简单明了呀,这么一看one-stage比two-stage模型看起来真的简单很多呀!
话不多说,进入pascal voc.py脚本中,我们主要看函数load_pascal_annotation中:
def
输出的label是一个(7,7,25)数组(这里假设类别数为20)。(我们YOLO v1网络的最后输出的shape是(7,7,30),这里好像尺寸不匹配耶,先不管,后面说明)
这里label是宽7,高7,深度为25的数组,沿着深度进行解析,根据代码
x_ind
可以看出,深度(axis=2)索引为0(第一个)处的值是根据gt_boxes的中心是否落在这个方格内部的标识(方格是图片7*7后 的每一格),也就是这个方格负不负责对这个物体(框)进行检测(response),如果落在当中,该处的值就是1,否则为0。这样做的意图就是:防止出现多个格子争抢对同一个物体的检测权。
那么这么一处理,共7*7的格子,深度索引为0处的值就确定了。
下面举个例子(图是上面链接中的图):
图中有三个物体:这三个物体的中心分别落在7*7格子中的(1,4),(2,3),(5,1),索引从0开始。
那么这张图片通过pascal_voc.py后生成label的第一层就是一个(7,7,1)的数组,其中(1,4),(2,3),(5,1)处的值为1,其余的都是0。
从代码中的
label[y_ind, x_ind, 1:5] = boxes #设置标记的框,框的形式为 ( x_center, y_center, width, height)
我们可以看出,(7,7,25)数组中深度(aixs=2)为(1,5)的部分存储着gt_boxes的坐标。当然啦,只在responsible的格子才有坐标,不负责检测该框的格子中的都是值0。
还是上面的图,只有label[1,4,1:5]、label[2,3,1:5]、label[5,1,1:5]存放着坐标,其余的label[:,:,1:5]全部为0。
代码中
label
可以看出,(7,7,25)数组中深度(axis=2)为(5,25)的部分存储着gt_boxes的20分类的类别信息。
那么了解完label的形式了,我们就来进入yolo v1的核心代码,他在yolo_net.py脚本中,我们将yolo_net.py脚本中的函数主要分为一下几个:
(1) build_network
(2) calc_iou
(3) loss_layer
第一个build_network函数显然就是搭建yolo v1的网络了,代码如下
def
看完网络搭建的模型后,我们是否有种感觉,yolo v1的网络和vgg16有种类似感,至少从网络形状来说,比较像,先是一堆卷积和池化,后面加入全连接层。最大的差异是最后输出层用线性函数做激活函数,因为需要预测bounding box的位置(数值型),而不仅仅是对象的概率。其实可以看出,模型的输出的shape为(7,7,30)。
函数calc_iou是一个求两个bbox之间的IOU大小的函数。
我们进入loss_layer,我个人认为其实只要把网络搭建和损失函数完成,那么这个程序就完成了一大半了。
损失函数仍然需要两个输入,一个是predict,就是网络前向的结果(和图片分类的logit是类似的),另一个是label,就是从原图中获取的相关信息。
我们根据损失函数的公式,如图(图是copy来的,别介意)
我们可以看出,总共有5个loss,每个loss参与计算损失的参数都不一样。我们需要提取predict和labels中对应的参数来计算每一个loss。
所以首先应该对predict和labels进行处理,使他们中的求解loss的参数能够对应起来。以下代码就是做了一件这样的事
with
这段代码提取了predict中的
(1)7*7个格子对应的坐标信息(7*7*2*4),
(2)类别信息(7*7*20),
(3)框的置信度信息(7*7*2),
也提取了labels中的
(1) response信息(7*7*1)
(2) boxes信息(7*7*4--->7*7*4*2,这里为了和predict中的坐标信息对应,使用了tile函数)
(3) 分类信息(7*7*20)
这里我们注意一点:
boxes
这里使用tile函数将原本一个gt_bbox的坐标复制了两份,然后做了一件事,除以图片的大小(448),这里很重要,如果漏看了,后面根本无法解释清楚!
Gt_box除以图片大小,意思就是将gt_box做了归一化,框的信息限制在了(0,1)中。
我们对predict和labels做了处理后,现在我们思考下,既然我们已经对了labels中的坐标信息做了归一化,那么predict输出框信息应当也需要做归一化,这样在相同的空间中,我们才能求两者之间的IOU了。
接下来的一段代码需要读者们好好进行领悟的。
就是关于偏移offset的设置。
offset
这里出现了个self.offset,我们看看这是一个什么。
self
嗯,这一段代码看似简短,实际上还是有点绕的,我写了另一篇文章,大家可以看一下里面的解释。
其实这段代码的输出,就是
#offset
# array([[[0, 0],
# [1, 1],
# [2, 2],
# [3, 3],
# [4, 4],
# [5, 5],
# [6, 6]],
#
# [[0, 0],
# [1, 1],
# [2, 2],
# [3, 3],
# [4, 4],
# [5, 5],
# [6, 6]],
#
# [[0, 0],
# [1, 1],
# [2, 2],
# [3, 3],
# [4, 4],
# [5, 5],
# [6, 6]],
#
# [[0, 0],
# [1, 1],
# [2, 2],
# [3, 3],
# [4, 4],
# [5, 5],
# [6, 6]],
#
# [[0, 0],
# [1, 1],
# [2, 2],
# [3, 3],
# [4, 4],
# [5, 5],
# [6, 6]],
#
# [[0, 0],
# [1, 1],
# [2, 2],
# [3, 3],
# [4, 4],
# [5, 5],
# [6, 6]],
#
# [[0, 0],
# [1, 1],
# [2, 2],
# [3, 3],
# [4, 4],
# [5, 5],
# [6, 6]]])
以上就是self.offset的值
那么前一段代码,
offset
offset_tran的输出就是
#offset_tran如下,只不过batch_size=1
# [[[[0. 0.]
# [0. 0.]
# [0. 0.]
# [0. 0.]
# [0. 0.]
# [0. 0.]
# [0. 0.]]
#
# [[1. 1.]
# [1. 1.]
# [1. 1.]
# [1. 1.]
# [1. 1.]
# [1. 1.]
# [1. 1.]]
#
# [[2. 2.]
# [2. 2.]
# [2. 2.]
# [2. 2.]
# [2. 2.]
# [2. 2.]
# [2. 2.]]
#
# [[3. 3.]
# [3. 3.]
# [3. 3.]
# [3. 3.]
# [3. 3.]
# [3. 3.]
# [3. 3.]]
#
# [[4. 4.]
# [4. 4.]
# [4. 4.]
# [4. 4.]
# [4. 4.]
# [4. 4.]
# [4. 4.]]
#
# [[5. 5.]
# [5. 5.]
# [5. 5.]
# [5. 5.]
# [5. 5.]
# [5. 5.]
# [5. 5.]]
#
# [[6. 6.]
# [6. 6.]
# [6. 6.]
# [6. 6.]
# [6. 6.]
# [6. 6.]
# [6. 6.]]]]
#
获取了offset和offset_trans后,我们看下一段代码,
predict_boxes_tran
解说这段代码之前,必须要补充点知识,就是关于predict_boxes的输出,我们知道predict_boxes的输出是网络前向传播后预测的候选框。固定思维让我们认为,predict_boxes的值就是类似gt_box坐标那样的(x,y,d,h)坐标。错!保持这个固有的思维,这段代码就无法看懂了,我也是不断推测的,才知道实际上道predict_boxes各个坐标的含义。
其实predict_boxes中的前两位,就是中心点坐标(x,y)代表的含义如上图,是predict_boxes中心坐标离所属格子(response)左上角的坐标。而predict_boxes中的后两位,其实并不是predict_boxes的宽度高度,而是predict_boxes的宽度高度相对于图片的大小(归一化后)的开方。
那么我们所说的输入predict中包含的坐标信息,就不是
(中心横坐标,
中心纵坐标,
宽,
高)
而是
(中心横坐标离所属方格左上角坐标的横向距离(假设每个方格宽度为1),
中心纵坐标离所属方格左上角坐标的纵向距离(假设每个方格高度为1),
宽度(归一化)的开方,
高度(归一化)的开方)
这里理解了,后面理解起来就很easy了。
代码
predict_boxes_tran
中的
(predict_boxes[..., 0] + offset) / self.cell_size,
(predict_boxes[..., 1] + offset_tran) / self.cell_size,
就是将predict_boxes的中心坐标转换为相对于整张图来说的(x,y)中心坐标。
还是用上面的图来说明
我们标出了response格子对应的offset x和offset y,那么结合上面所说,
(predict_boxes[..., 0] + offset) / self.cell_size,
(predict_boxes[..., 1] + offset_tran) / self.cell_size,
就是先将上面图中的x和y变成(x+offset x,y+offset y),然后除以cell_size=7,相当于对中心坐标进行了归一化,
tf.square(predict_boxes[..., 2]),
tf.square(predict_boxes[..., 3])],
就是将原来的宽度(归一化)的开方和高度(归一化)的开方恢复成:(宽度(归一化),高度(归一化)),那么predict_bbox中的坐标信息,全部通过这段代码,恢复成了和labels中坐标相同格式的了,难道不是很妙嘛?
说到这里了,接下来我们就准备分析下loss的代码了,我们仔细分析下,
根据下面的loss式子,看看我们还缺少什么没有进行说明。
上面公式中,置信度损失的
和
我们还没有进行说明。
接下来的一段代码如下:
iou_predict_truth
iou_predict_truth的定义是比较简单的,就是求预测框和实际框的IOU。
输出的结果shape为[batch_size, 7, 7, 2],就是求每个对应位置格子中的对应框的IOU。
接着object_mask对上述的[batch_size, 7, 7, 2]个IOU按照axis=3取最大值,就是在7*7个格子中的2个对象中,找到与实际框IOU最大的那一个,注意这里是去取值,而不是取索引。
后面的代码
object_mask
这段代码的意思就是找到7*7格子中满足两个以下条件的对象
(1) 该对象属于的框是response框,负责检测物体
(2) 该对象是所属框中的,与实际物体IOU比例较大的那个
这样我们获得了object_mask,他的shape为[batch_size, 7, 7, 2],满足以上两个条件的框的位置为1,其余为0,说了这么多,这个就是我们公式中的
。
那么自然地,公式中的
是不满足上述两个条件的框的集合,定义如下:
# calculate no_I tensor [CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]
接下来就来我们最关键LOSS定义了。
参照着LOSS 图(为了方便,这个图已经出来三次了)
这里有5个LOSS,我们将第一个LOSS(边框中心误差)和第二个LOSS(边框的宽度和高度误差)整合为一个LOSS,程序如下:
# 框坐标的损失,只计算有目标的cell中iou最大的那个框的损失,即用这个iou最大的框来负责预测这个框,其它不管,乘以0
这里的predict_boxes和boxes_tran是最后一个维度的信息是归一化的(x,y,
上面LOSS图的第三个LOSS,是置信度损失(框内有对象),代码是:
object_delta
这里有个很妙的应用,就是用iou_predict_truth替代真实的置信度。上面我们提到的iou_predict_truth是真实框和预测框(归一化)的IOU。那么我们使用这个IOU去当作训练的目标,原因在哪儿呢?
我们这样做的目的很明显,我们就是尽可能使得置信度(框内有对象)接近这个IOU,当然了,就是教yolo 如何去学习计算置信度信息,结果就是yolo学会了使用使用IOU当作置信度啦!你说妙不妙(当然这是我的想法,不知是否正确,说错了,记得评论告诉我)
置信度的第一个LOSS解决了,现在我们来看置信度的第二个LOSS,就是图上的第三个LOSS:置信度损失(框内无对象),代码是:
# 没有目标的时候,置信度的损失函数,这里的predict_scales是(predict_scales-0)
这个定义很简单,就不多说。
最后一个损失是分类损失,代码如下
# class_loss, 计算类别的损失
这里使用平方差损失函数就好了,不用交叉熵啦!
说完损失函数,差不多这个yolo的核心就掌握了。关于YOLO v1的模型,其实看起来是比较普通的,各种博客帖子都有很好的说明,我们就不多说了。
写了fasterRCNN和yolo v1的总结,发现网络模型真的仅仅是一部分,如果能把损失函数LOSS设置合理可行,加上模型上的一些优势,真的效果就出来了。
大家下载代码后,可以进行YOLO V1的实战,你们会在实战中发现,还有很多问题(不使用YOLO_small.ckpt),后面如果有空,我会出一个yolo v1的训练,但是这意义不大,能吃牛肉(yolo v3),谁还吃清道夫呢(yolo v1)?