.net解析传过来的xml_YOLO V1 深层解读与代码解析

3c4bcd483c2b95943a6b385107d9a998.png

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处的值就确定了。

下面举个例子(图是上面链接中的图):

fe89f36536e619d582d22c2afca074ce.png

图中有三个物体:这三个物体的中心分别落在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来的,别介意)

409014a424aef0c5f330eae7d30805fd.png

我们可以看出,总共有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各个坐标的含义。

c045457371b25cd6ef218ce521bad970.png
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)中心坐标

还是用上面的图来说明

2b601a6dbc383baf18df355c55dc380d.png

我们标出了response格子对应的offset xoffset 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式子,看看我们还缺少什么没有进行说明。

409014a424aef0c5f330eae7d30805fd.png

上面公式中,置信度损失的

8f04e58d3c90841d2e4fb841cde1d74e.png

e36303376b6f8bf4b506353f48c4efd9.png

我们还没有进行说明。

接下来的一段代码如下:

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,说了这么多,这个就是我们公式中的

e36303376b6f8bf4b506353f48c4efd9.png

那么自然地,公式中的

d46b0f8b36543aeacbbdea10a8ad864e.png

是不满足上述两个条件的框的集合,定义如下:

# calculate no_I tensor [CELL_SIZE, CELL_SIZE, BOXES_PER_CELL]

接下来就来我们最关键LOSS定义了。

参照着LOSS 图(为了方便,这个图已经出来三次了)

409014a424aef0c5f330eae7d30805fd.png

这里有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)?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值