0%

YOLO目标检测从 V1 开始,细读代码

忙里偷闲,写几篇长文,从 YOLO 的 v1 到 v5。没想到时隔多年会回来重新看 YOLO 系列的东西,相比两阶段检测,YOLO 真的太快了,加上一些训练的 trick,mAP 也不会很低。网上看了好多教程不明所以,索性还是直接去读原论文了,读了原论文有些东西还是不理解,索性又去读了源程序。不过为了便于理解,有的地方不会按照论文顺序进行整理。少问问题,读论文产生的疑问在代码里都有解答,不看代码永远不能被称为学会了。

如果对本文有疑问或者想找男朋友,可以联系我,点击此处有我联系方式

YOLO v1

YOLO v1 将目标检测定义为回归问题,直接读入全部图像,回归出边界框和分类概率。与同时期的 Faster RCNN 对比,算法快了不少,也没有 RPN 以及后处理,也避免里滑动窗口这样的暴力检测。所以能用到一些实时系统中,也是我花精力看这一些列论文的原因。

但是对于 v1 的 YOLO 存在一些缺陷,作者在论文中也进行了阐述:准确率低、定位不准尤其是小目标的定位。

算法流程

首先将图片分为 $S\times S$ 个网格,论文中 $S=7$,如果一个物体的中心落入这个格子中,那么这个格子负责预测这个目标。设每个格子负责预测 $B$ 个物体的盒子参数和置信度得分,盒子参数指明物体的位置,置信度表示盒子含有目标且预测准确的可信程度。即对于图片的每个格子,会输出 $B$ 个 $(x,y,w,h,c)$,论文中 $B=2$。

既然有了输出,那么就需要 label 进行损失计算。$(x,y,w,h,c)$ 是人工标注的数据,$c$ 初始化为 1。论文定义网络输出的置信度标签是一个分段函数,如果格子没有目标,置信度是 0;如果有目标,置信度是预测框和真实框的 IOU 值,公式描述为 $\text{Pr(Object)} * \text{IOU}_{\text{pred}}^\text{truth}$。

目标检测和分类是分不开的,为了达到分类的目的,每个格子也会输出 $C$ 个类别的概率,公式表述为 $\text{Pr(Class}_i|\text{Object})$,即格子里面得是个目标,才能计算分类的概率和损失。而每个格子输出一组预测,即使输出了 $B$ 组数据,这就限制了网络的表达。

在测试阶段,类别置信度的分数就是分类概率和置信度相乘,即盒子中「有这个类别的概率」和「网络预测这个类别的概率」的乘积:

\begin{equation}
\text{Pr(Class}_i|\text{Object})*\text{Pr(Object)}*\text{IOU}_{\text{pred}}^\text{truth}=\text{Pr(Class}_i) * \text{IOU}_{\text{pred}}^\text{truth}
\end{equation}

网络结构

这种东西还是代码清楚,只放了最关键的检测输出:

1
2
3
nn.Linear(4096, S * S * (5 * B + C))
nn.Sigmoid()
x.view(-1, S, S, 5 * B + C)

损失函数

\begin{aligned}
{ } & \lambda_{coord} \sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{I}_{ij}^{\text{obj}} [(x_i-\hat{x}_i)^2 + (y_i-\hat{y}_i)^2] \\
{ } &+ \lambda_{coord} \sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{I}_{ij}^{\text{obj}} [(\sqrt{w_i}-\sqrt{\hat{w}_i})^2+(\sqrt{h_i}-\sqrt{\hat{h}_i})^2] \\
{ } &+ \sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{I}_{ij}^{\text{obj}} (C_i - \hat{C}_i)^2 \\
{ } &+ \lambda_{noobj} \sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{I}_{ij}^{\text{noobj}} (C_i - \hat{C}_i)^2 \\
{ } &+ \sum_{i=0}^{S^2} \mathbb{I}_{ij}^{\text{obj}} \sum_{c\in classes} (p_i(c)-\hat{p}_i(c))^2
\end{aligned}

  • $\lambda_{coord}$ 是前景的权重,$\mathbb{I}_{ij}^{\text{obj}}$ 是指示函数,取值只有 0 和 1
  • 前两行表示 bound box 的损失
  • 第三行是前景置信度的损失
  • 第四行是背景置信度的损失
  • 第五行是分类的损失

缺陷

  1. 每个网格只能检测一个类别和两个目标,类间竞争严重,网络表达受限,对于密集群体的检测性能会下降;
  2. 定位不准确,因为网络直接预测 bounding box 的坐标,一开始的偏移可能会很大,导致定位不准确,读完代码能深刻理解这里的缺陷;
  3. 对于检测问题而言,大多情况背景居多,前景居少,也就是样本不均衡。YOLO v1 的损失中,并没有计算背景的 bound box 损失,只计算了前景的,YOLO v1 回避了样本不均衡的问题,这会影响网络的稳定性与背景的识别。

程序解析

「如果对算法有疑问,就去读代码吧」这一经验帮助我理解了很多算法的困惑之处,不仅仅是 YOLO。如果要看懂一个深度学习的算法,核心有三要素,首先是网络模型,理解输入、输出和结构;其次是数据与损失,理解加载什么格式的数据,理解网络预测数据和加载的数据如何计算损失,所以这俩常常放在一起;最后是细枝末节,即数据增强、学习率策略、整体训练流程等。所以接下来整理网络模型和损失。训练策略那些不是 YOLO 的重点。

网络模型

我们知道网络的输出是 x.view(-1, S, S, 5 * B + C) 这种类型的格式,这是预测数据,即 $S \times S $ 组 $5 \times B + C$ 这样的数据,$C$ 是类别数量。那么可想而知,在训练阶段,同样需要提供同等尺寸大小的标签数据。

数据与损失

YOLO 处理目标时,使用的是目标中心点的坐标相对图像大小的占比。如果一张图像的大小是 224 X 224,目标中心点位于 112 X 112,那么中心点的坐标是 $(0.5,0.5)$。这有两点好处:

  1. 如果一个图像的尺寸是 1920 X 1080,目标中心点的坐标是 1000 X 1000,直接输出 1000 对于网络来说难以把控,会造成梯度爆炸的现象。而占比只需要输出 [0,1] 之间的小数,不会导致梯度爆炸。
  2. 方便图像的标准化处理。网络常常使用多个 batch 进行训练,每个 batch 的数据要求大小统一,对于不同尺寸的图像应选择 resize。如果直接用坐标,resize 后会导致坐标错位,而如果用占比,位于之前图像 (0.5, 0.5) 处的点在 resize 后的坐标仍然是 (0.5, 0.5)。

由于 YOLO 最初设计的方案是:物体中心落到哪个格子,就由这个格子预测这个目标,这一观点需要仔细阅读代码才能理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 和网络输出同等大小的标签
target = torch.zeros(S, S, N)
# S=7,表示每个格子的占比
cell_size = 1.0 / float(S)
# 计算宽度和高度
boxes_wh = boxes[:, 2:] - boxes[:, :2]
# x,y 的中心点坐标,此时已经除以图像大小
boxes_xy = (boxes[:, 2:] + boxes[:, :2]) / 2.0

# 对于 b 和 batch 的盒子进行处理
for b in range(boxes.size(0)):
xy, wh, label = boxes_xy[b], boxes_wh[b], int(labels[b])

# 计算中心点位于哪个格子
ij = (xy / cell_size).ceil() - 1.0
# 取出 i j,用于 SXS 的填充
i, j = int(ij[0]), int(ij[1])
# 格子左上角的坐标
x0y0 = ij * cell_size
# 相对格子左上角的坐标
# 除以 cell_size 没啥用,这人代码写的有问题,后面损失计算的时候又乘了回来
xy_normalized = (xy - x0y0) / cell_size

# 开始填充
for k in range(B):
s = 5 * k
target[j, i, s :s+2] = xy_normalized # 坐标
target[j, i, s+2:s+4] = wh # 大小
target[j, i, s+4 ] = 1.0 # 置信度
target[j, i, 5*B + label] = 1.0 # 类别标签

return target

在损失计算阶段,代码真的太长了不便展示,这里只记录核心要素:

  1. 对于有目标计算损失,无目标忽略这一点,是通过对真实标签进行掩码处理实现的,只取出真实标签中置信度为 1 的标签记录维度,并在 predict 中取出同维度的数据就算损失,其余数据忽略。假设这一步保留了 $X$ 个盒子。
  2. 对于 $X$ 个盒子中的 $B$ 组数据继续处理,对于每组数据而言,选择和真实标签 IOU 最大的盒子计算损失,其余盒子忽略,也就是没有正负样本的概念。这里需要注意,如果网络初期计算到 IOU 为 0,那么默认第一个盒子和真实标签进行损失计算。

结语

我已经正负样本划分、格子、bounding box、anchor box 的概念已经搞混了,论文里不会写这太细节的东西,不然我也不会来读代码。毕竟整理理论知识太简单了,也容易自欺欺人,并不清楚网络的流程。所以 YOLO v2, v3, v4, v5 和 x 的内容等下几篇博客了。

参考

  1. 你一定从未看过如此通俗易懂的YOLO系列(从v1到v5)模型解读
  2. YOLO v1/v2/v3/v4
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章