最近用 yolox 的发现了一个很神奇的现象,简而言之 yolox-tiny
在单目标检测的效果比 yolox-small
好一些,且 yolox-small
能大幅提升检测精度的方法到了 yolox-tiny
也不起作用了。网上很多 yolox 的解读基本都是翻译论文,没啥价值,还是决定仔细读一下代码,这大概也是全网第一份从源代码的角度解析 yolox 的文章。调优过程明确不采用的方案:增大模型规模、模型融合和其他消耗算力的方法,专注算法本身。
Model 部分
和其他检测模型一样, model
分为 backbone
, neck
和 head
。
backbone
backbone
采用 CSPDarkNet
,包括 stem
, dark2
, dark3
, dark4
和 dark5
。
数据经过增强处理并缩放到 640X640
大小后进入 stem
完成图像通道的升维,从 3
通道提升到 X
通道, X
取决于 backbone
规模的 width_factor
参数。图像经过这一层之前,会被均匀切分为左上、右上、左下和右下四个区域并按通道拼接得到 160X160X12
的数据,也就是 12 个通道,每个通道的图像大小占据原图像大小的 1/4
,在经过卷积、 BN
层和激活层,得到输出。stem
的输出进入 dark2
,经过一个卷积模块,维度提升一倍后尺寸减半。而后经过 CSPLayer
, CSPLayer
的结构和残差网络相似,一个分支只对输入卷积一次,另一个分支进行深度特征提取,深度的层数取决于 backbone
的 depth_factor
参数,而后两个分支的输出按照通道数拼接到一起,完成升维。dark3, dark4, dark5
的东西和 dark2
一致,无非是尺寸减半,通道数翻倍,同理得到 dark3, dark4, dark5
的输出。
这里补充一下:
dark3
的输出维度: 256X80X80
dark4
的输出维度: 512X40X30
dark5
的输出维度: 1024X20X20
neck
获取 backbone
的 dark3, dark4, dark5
的输出作为输入。这里用文字描述的话太复杂了,简单的画图展示一下大概结构,精细的结构还是要看源代码:
也就是说,这三个输出都融合了模型深层的语义特征和模型浅层的细节特征。
head
因为 neck
有三组输出,所以 head
对 neck
的每一组输出都要进行处理。对每一个输入经过不同的 stem
把通道数降维到 256,而后接入解耦的任务分支,包括分类( cls
)、位置框( reg
)和前背景( obj
)三个网络。
分类网络的输出通道数是类别数,这里假设为 2,回归网络的输出通道数是 4,负责预测中心点坐标和高宽尺寸,前背景网络的输出通道数是 1,因此输出的通道数是 2+4+1=7。将这三个网络的输出然后拼接到一起,放到一个列表中。因此, head
部分得到的输出为三组数据: 7X80X80, 7X40X40, 7X20X20
。以 7X80X80
为例,表示预测了 80X80
个目标,每个目标包括位置、类别和前背景共 7 个参数。
训练部分
这一部分是难点,或者说,是任何目标检测算法的实现难点,代码量也是最大。
预处理
在这一部分,将对 head
的三个输出进行一些转换并生成对应的 grid
信息,将预测输出对应到图像中的实际位置。这一部分大概分以下步骤:
- 获取输出特征的的宽度和高度,如 80 和 80,或者 40 和 40,那幺就生成对应的
grid
- ,如 [0, 1] [0, 2] … [80, 80] 共 6400 个,维度是 [1, 6400, 2]
- 将预测结果
reshape
- 成
Batch, HxW, C
- 大小,坐标的
x
- 和
y
- 加上
grid
- 会映射到每个预测特征点的中心位置,在乘以 8,也就是理想情况下位置信息的运算结果会在 640 X 640 之间,也就是图像上目标的中心点
- 计算
w
- 和
h
- 的 $e$ 次方,再乘以 8,得到目标框的高度和宽度。此时返回得到的
grid
- 和变换过后的
output
- 。(80 对应的扩张步是 8,40 对应的扩张步是 16,20 对应的扩张步是 32)
将每一个输出经过上面 3 个步骤的处理后,按照 dim=1
拼接到一起,也就是会得到 Batch, 8400, 7
的输出。(80X80 + 40X40 + 20X20 = 8400)。
计算损失
针对 batch
中的每一个图像开始处理:
如果真实标签显示这个图像没有目标,全部真实标签就是清一色的 0,分类个数全部是 0,位置参数是 4 个 0,有无目标是 8400 个 0, fg_mask
全部是 false
。( fg_mask
的用途后面会讲)
否则,取出这个图像包含的全部真实目标框,与预测结果进行 SimOTA 样本分配,为预测结果分配标签,或者说为标签分配预测结果,因为 8400 个预测结果不可能同时参与训练,只选择部分样本视为正样本进行训练。
SimOTA
首先计算真实框覆盖的 grid
中心点,将这些 grid
中心点称为 fg_mask
也就是正样本,从所有的预测结果中通过 fg_mask
把正样本取出来,包括位置,类别和前背景。此外,选择落入真实目标框的周围的预测结果并记录下来,周围的度量方式是:当前特征点乘以 2.5 倍的步长所覆盖的格子。
iou obj
之后进行动态 k 分配,这里的 k 计算比较简单,在 10 和上一步骤选中的 fg_mask
数量取最小值就是 k
,给每个真实框选取损失最小的 k
个预测结果。如果当某一个特征点指向多个真实框的时候,选取 cost
最小的真实框,之后对 fg_mask
进行更新。
计算损失
obj
损失是全部的预测结果和动态 k 分配后得到的 fg_mask
做交叉熵,提升检测到目标的能力cls
损失基于 fg_mask
选中的预测结果,将类别的 one-hot
向量与正样本和真实框的 iou 做乘积视为目标,和预测结果做交叉熵损失reg
损失是就是预测盒子和真实盒子的 iou 损失
问题分析与调优
那幺问题出在哪里呢?我目前只发现了一点点问题,等我彻底解决完毕回来填坑(因为又又又摸不到显卡了)。
如何解释开头的问题呢?我想是这样的,由于是单目标检测任务,也就是说只有一个目标,那幺小模型参数少,很容易聚焦和收敛;而大模型参数大,解空间也会更多,相对小模型难以探索到更好的解,因此一些常见的 trick
才会有效的提升大模型的检测效果,而对小模型而言,很容易找到更优的解,因此一些 trick
并不会起到很大的作用。
具体到调优阶段,通过一路 debug
找到了一些问题,等摸到显卡了回来填坑。
Be First to Comment