YOLOv1 模型原理及代码解析
【摘要】 YOLOv1 出自 2016 CVPR 论文 You Only Look Once:Unified, Real-Time Object Detection.
YOLO 系列算法的核心思想是将输入的图像经过 backbone 提取特征后,将得到特征图划分为 S x S 的网格,物体的中心落在哪一个网格内,这个网格就负责预测该物体的置信度、类别以及坐标位置。
YOLOv1
YOLOv1 出自 2016 CVPR 论文 You Only Look Once:Unified, Real-Time Object Detection.
YOLO 系列算法的核心思想是将输入的图像经过 backbone 提取特征后,将得到特征图划分为 S x S 的网格,物体的中心落在哪一个网格内,这个网格就负责预测该物体的置信度、类别以及坐标位置。
Abstract
作者提出了一种新的目标检测方法 YOLO
,之前的目标检测工作都是重新利用分类器来执行检测。作者的神经网络模型是端到端的检测,一次运行即可同时得到所有目标的边界框和类别概率。
YOLO
架构的速度是非常快的,base
版本实时帧率为 45
帧,smaller
版本能达到每秒 155
帧,性能由于 DPM
和 R-CNN
等检测方法。
1. Introduction
之前的目标检测器是重用分类器来执行检测,为了检测目标,这些系统在图像上不断遍历一个框,并利用分类器去判断这个框是不是目标。像可变形部件模型(DPM
)使用互动窗口方法,其分类器在整个图像的均匀间隔的位置上运行。
作者将目标检测看作是单一的回归问题,直接从图像像素得到边界框坐标和类别概率。
YOLO 检测系统如图 1 所示。单个检测卷积网络可以同时预测多个目标的边界框和类别概率。YOLO
和传统的目标检测方法相比有诸多优点。
首先,YOLO
速度非常快,我们将检测视为回归问题,所以检测流程也简单。其次,YOLO
在进行预测时,会对图像进行全面地推理。第三,YOLO
模型具有泛化能力,其比 DPM
和R-CNN
更好。最后,虽然 YOLO
模型在精度上依然落后于最先进(state-of-the-art)的检测系统,但是其速度更快。
2. Unified Detectron
YOLO
系统将输入图像划分成
的网格(grid
),然后让每个gird
负责检测那些中心点落在 grid
内的目标。
检测任务:每个网络都会预测
个边界框及边界框的置信度分数,所谓置信度分数其实包含两个方面:一个是边界框含有目标的可能性,二是边界框的准确度。前者记为
,当边界框包含目标时,
值为 1
,否则为 0
;后者记为
,即预测框与真实框的 IOU
。因此形式上,我们将置信度定义为
。如果 grid
不存在目标,则置信度分数置为 0
,否则,置信度分数等于预测框和真实框之间的交集(IoU
)。
每个边界框(bounding box
)包含 5
个预测变量:
,
,
,
和 confidence
。
坐标不是边界框中心的实际坐标,而是相对于网格单元左上角坐标的偏移(需要看代码才能懂,论文只描述了出“相对”的概念)。而边界框的宽度和高度是相对于整个图片的宽与高的比例,因此理论上以上 4
预测量都应该在
范围之内。最后,置信度预测表示预测框与实际边界框之间的 IOU
。
值得注意的是,中心坐标的预测值 是相对于每个单元格左上角坐标点的偏移值,偏移量 = 目标位置 - grid的位置。
分类任务:每个网格单元(grid
)还会预测
个类别的概率
。grid
包含目标时才会预测
,且只预测一组类别概率,而不管边界框
的数量是多少。
在推理时,我们乘以条件概率和单个 box
的置信度。
它为我们提供了每个框特定类别的置信度分数。这些分数编码了该类出现在框中的概率以及预测框拟合目标的程度。
在 Pscal VOC
数据集上评测 YOLO
模型时,我们设置
,
(即每个 grid
会生成 2
个边界框)。Pscal VOC
数据集有 20
个类别,所以
。所以,模型最后预测的张量维度是
。
总结:YOLO
系统将检测建模为回归问题。它将图像分成
的 gird
,每个 grid
都会预测
个边界框,同时也包含
个类别的概率,这些预测对应的就是
。
这里其实就是在描述 YOLOv1
检测头如何设计:回归网络的设计 + 训练集标签如何构建(即 yoloDataset
类的构建),下面给出一份针对 voc
数据集编码为 yolo
模型的输入标签数据的函数,读懂了这个代码,就能理解前面部分的描述。
代码来源这里。
def encoder(self, boxes, labels):
'''
boxes (tensor) [[x1,y1,x2,y2],[]] 目标的边界框坐标信息
labels (tensor) [...] 目标的类别信息
return 7x7x30
'''
grid_num = 7 # 论文中设为7
target = torch.zeros((grid_num, grid_num, 30)) # 和模型输出张量维尺寸一样都是 14*14*30
cell_size = 1./grid_num # 之前已经把目标框的坐标进行了归一化(这里与原论文有区别),故这里用1.作为除数
# 计算目标框中心点坐标和宽高
wh = boxes[:, 2:]-boxes[:, :2]
cxcy = (boxes[:, 2:]+boxes[:, :2])/2
# 1,遍历各个目标框;
for i in range(cxcy.size()[0]): # 对应于数据集中的每个框 这里cxcy.size()[0] == num_samples
# 2,计算第 i 个目标中心点落在哪个 `grid` 上,`target` 相应位置的两个框的置信度值设为 `1`,同时对应类别值也置为 `1`;
cxcy_sample = cxcy[i]
ij = (cxcy_sample/cell_size).ceil()-1 # ij 是一个list, 表示目标中心点cxcy在归一化后的图片中所处的x y 方向的第几个网格
# [0,1,2,3,4,5,6,7,8,9, 10-19] 对应索引
# [x,y,w,h,c,x,y,w,h,c, 20 个类别的 one-hot编码] 与原论文输出张量维度各个索引对应目标有所区别
target[int(ij[1]), int(ij[0]), 4] = 1 # 第一个框的置信度
target[int(ij[1]), int(ij[0]), 9] = 1 # 第二个框的置信度
target[int(ij[1]), int(ij[0]), int(labels[i])+9] = 1 # 第 int(labels[i])+9 个类别为 1
# 3,计算目标中心所在 `grid`(网格)的左上角相对坐标:`ij*cell_size`,然后目标中心坐标相对于子网格左上角的偏移比例 `delta_xy`;
xy = ij*cell_size
delta_xy = (cxcy_sample -xy)/cell_size
# 4,最后将 `target` 对应网格位置的 (x, y, w, h) 分别赋相应 `wh`、`delta_xy` 值。
target[int(ij[1]), int(ij[0]), 2:4] = wh[i] # 范围为(0,1)
target[int(ij[1]), int(ij[0]), :2] = delta_xy
target[int(ij[1]), int(ij[0]), 7:9] = wh[i]
target[int(ij[1]), int(ij[0]), 5:7] = delta_xy
return target
代码分析,一张图片对应的标签张量 target
的维度是
。然后分别对各个目标框的 boxes
:
和 labels
:(0,0,...,1,0)
(one-hot
编码的目标类别信息)进行处理,符合检测系统要求的输入形式。算法步骤如下:
- 计算目标框中心点坐标和宽高,并遍历各个目标框;
- 计算目标中心点落在哪个
grid
上,target
相应位置的两个框的置信度值设为1
,同时对应类别值也置为1
; - 计算目标中心所在
grid
(网格)的左上角相对坐标:ij*cell_size
,然后目标中心坐标相对于子网格左上角的偏移比例delta_xy
; - 最后将
target
对应网格位置的 分别赋相应wh
、delta_xy
值。
2.1. Network Design
YOLO
模型使用卷积神经网络来实现,卷积层负责从图像中提取特征,全连接层预测输出类别概率和坐标。
YOLO
的网络架构受 GooLeNet
图像分类模型的启发。网络有 24
个卷积层,最后面是 2
个全连接层。整个网络的卷积只有
和
卷积层,其中
卷积负责降维 ,而不是 GoogLeNet
的 Inception
模块。
图3:网络架构。作者在 ImageNet
分类任务上以一半的分辨率(输入图像大小
)训练卷积层,但预测时分辨率加倍。
Fast YOLO
版本使用了更少的卷积,其他所有训练参数及测试参数都和 base YOLO
版本是一样的。
网络的最终输出是
的张量。这个张量所代表的具体含义如下图所示。对于每一个单元格,前 20
个元素是类别概率值,然后 2
个元素是边界框置信度,两者相乘可以得到类别置信度,最后 8
个元素是边界框的
。之所以把置信度
和
都分开排列,而不是按照
这样排列,存粹是为了后续计算时方便。
划分 网格,共
98
个边界框,2
个框对应一个类别,所以YOLOv1
只能在一个网格中检测出一个目标、单张图片最多预测49
个目标。
2.2 Training
模型训练最重要的无非就是超参数的调整和损失函数的设计。
因为 YOLO
算法将检测问题看作是回归问题,所以自然地采用了比较容易优化的均方误差作为损失函数,但是面临定位误差和分类误差权重一样的问题;同时,在每张图像中,许多网格单元并不包含对象,即负样本(不包含物体的网格)远多于正样本(包含物体的网格),这通常会压倒了正样本的梯度,导致训练早期模型发散。
为了改善这点,引入了两个参数:
和
。对于边界框坐标预测损失(定位误差),采用较大的权重
,然后区分不包含目标的边界框和含有目标的边界框,前者采用较小权重
。其他权重则均设为 0
。
对于大小不同的边界框,因为较小边界框的坐标误差比较大边界框要更敏感,所以为了部分解决这个问题,将网络的边界框的宽高预测改为对其平方根的预测,即预测值变为 。
YOLOv1
每个网格单元预测多个边界框。在训练时,每个目标我们只需要一个边界框预测器来负责。我们指定一个预测器“负责”根据哪个预测与真实值之间具有当前最高的 IOU
来预测目标。这导致边界框预测器之间的专业化。每个预测器可以更好地预测特定大小,方向角,或目标的类别,从而改善整体召回率。
YOLO
由于每个网格仅能预测2
个边界框且仅可以包含一个类别,因此是对于一个单元格存在多个目标的问题,YOLO
只能选择一个来预测。这使得它在预测临近物体的数量上存在不足,如钢筋、人脸和鸟群检测等。
最终网络总的损失函数计算公式如下:
指的是第 个单元格存在目标,且该单元格中的第 个边界框负责预测该目标。 指的是第 个单元格存在目标。
- 前 2 行计算前景的
geo_loss
(定位loss
)。 - 第 3 行计算前景的
confidence_loss
(包含目标的边界框的置信度误差项)。 - 第 4 行计算背景的
confidence_loss
。 - 第 5 行计算分类损失
class_loss
。
值得注意的是,对于不存在对应目标的边界框,其误差项就是只有置信度,坐标项误差是没法计算的。而只有当一个单元格内确实存在目标时,才计算分类误差项,否则该项也是无法计算的。
2.4. Inferences
同样采用了 NMS
算法来抑制多重检测,对应的模型推理结果解码代码如下,这里要和前面的 encoder
函数结合起来看。
# 对于网络输出预测 改为再图片上画出框及score
def decoder(pred):
"""
pred (tensor) torch.Size([1, 7, 7, 30])
return (tensor) box[[x1,y1,x2,y2]] label[...]
"""
grid_num = 7
boxes = []
cls_indexs = []
probs = []
cell_size = 1./grid_num
pred = pred.data # torch.Size([1, 14, 14, 30])
pred = pred.squeeze(0) # torch.Size([14, 14, 30])
# 0 1 2 3 4 5 6 7 8 9
# [中心坐标,长宽,置信度,中心坐标,长宽,置信度, 20个类别] x 7x7
contain1 = pred[:, :, 4].unsqueeze(2) # torch.Size([14, 14, 1])
contain2 = pred[:, :, 9].unsqueeze(2) # torch.Size([14, 14, 1])
contain = torch.cat((contain1, contain2), 2) # torch.Size([14, 14, 2])
mask1 = contain > 0.1 # 大于阈值, torch.Size([14, 14, 2]) content: tensor([False, False])
mask2 = (contain == contain.max()) # we always select the best contain_prob what ever it>0.9
mask = (mask1+mask2).gt(0)
# min_score,min_index = torch.min(contain, 2) # 每个 cell 只选最大概率的那个预测框
for i in range(grid_num):
for j in range(grid_num):
for b in range(2):
# index = min_index[i,j]
# mask[i,j,index] = 0
if mask[i, j, b] == 1:
box = pred[i, j, b*5:b*5+4]
contain_prob = torch.FloatTensor([pred[i, j, b*5+4]])
xy = torch.FloatTensor([j, i])*cell_size # cell左上角 up left of cell
box[:2] = box[:2]*cell_size + xy # return cxcy relative to image
box_xy = torch.FloatTensor(box.size()) # 转换成xy形式 convert[cx,cy,w,h] to [x1,y1,x2,y2]
box_xy[:2] = box[:2] - 0.5*box[2:]
box_xy[2:] = box[:2] + 0.5*box[2:]
max_prob, cls_index = torch.max(pred[i, j, 10:], 0)
if float((contain_prob*max_prob)[0]) > 0.1:
boxes.append(box_xy.view(1, 4))
cls_indexs.append(cls_index.item())
probs.append(contain_prob*max_prob)
if len(boxes) == 0:
boxes = torch.zeros((1, 4))
probs = torch.zeros(1)
cls_indexs = torch.zeros(1)
else:
boxes = torch.cat(boxes, 0) # (n,4)
# print(type(probs))
# print(len(probs))
# print(probs)
probs = torch.cat(probs, 0) # (n,)
# print(probs)
# print(type(cls_indexs))
# print(len(cls_indexs))
# print(cls_indexs)
cls_indexs = torch.IntTensor(cls_indexs) # (n,)
# 去除冗余的候选框,得到最佳检测框(bbox)
keep = nms(boxes, probs)
# print("keep:", keep)
a = boxes[keep]
b = cls_indexs[keep]
c = probs[keep]
return a, b, c
4 Comparison to Other Real-Time Systems
基于 GPU Titan X 硬件环境下,与他检测算法的性能比较如下。
5,代码实现思考
一些思考:快速的阅读了网上的一些 YOLOv1
代码实现,发现整个 YOLOv1
检测系统的代码可以分为以下几个部分:
- 模型结构定义:特征提器模块 + 检测头模块(两个全连接层)。
- 数据预处理,最难写的代码,需要对原有的
VOC
数据做预处理,编码成YOLOv1
要求的格式输入,训练集的label
的shape
为(bach_size, 7, 7, 30)
。 - 模型训练,主要由损失函数的构建组成,损失函数包括
5
个部分。 - 模型预测,主要在于模型输出的解析,即解码成可方便显示的形式。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)