Press "Enter" to skip to content

YOLOX 源码解析与调优

最近用 yolox 的发现了一个很神奇的现象,简而言之 yolox-tiny 在单目标检测的效果比 yolox-small 好一些,且 yolox-small 能大幅提升检测精度的方法到了 yolox-tiny 也不起作用了。网上很多 yolox 的解读基本都是翻译论文,没啥价值,还是决定仔细读一下代码,这大概也是全网第一份从源代码的角度解析 yolox 的文章。调优过程明确不采用的方案:增大模型规模、模型融合和其他消耗算力的方法,专注算法本身。

 

Model 部分

 

和其他检测模型一样, model 分为 backboneneckhead

 

backbone

 

backbone 采用 CSPDarkNet ,包括 stemdark2dark3dark4dark5

数据经过增强处理并缩放到 640X640 大小后进入 stem 完成图像通道的升维,从 3 通道提升到 X 通道, X 取决于 backbone 规模的 width_factor 参数。图像经过这一层之前,会被均匀切分为左上、右上、左下和右下四个区域并按通道拼接得到 160X160X12 的数据,也就是 12 个通道,每个通道的图像大小占据原图像大小的 1/4 ,在经过卷积、 BN 层和激活层,得到输出。
stem 的输出进入 dark2 ,经过一个卷积模块,维度提升一倍后尺寸减半。而后经过 CSPLayerCSPLayer 的结构和残差网络相似,一个分支只对输入卷积一次,另一个分支进行深度特征提取,深度的层数取决于 backbonedepth_factor 参数,而后两个分支的输出按照通道数拼接到一起,完成升维。
dark3, dark4, dark5 的东西和 dark2 一致,无非是尺寸减半,通道数翻倍,同理得到 dark3, dark4, dark5 的输出。

 

这里补充一下:

dark3 的输出维度: 256X80X80
dark4 的输出维度: 512X40X30
dark5 的输出维度: 1024X20X20

neck

 

获取 backbonedark3, dark4, dark5 的输出作为输入。这里用文字描述的话太复杂了,简单的画图展示一下大概结构,精细的结构还是要看源代码:

 

 

也就是说,这三个输出都融合了模型深层的语义特征和模型浅层的细节特征。

 

head

 

因为 neck 有三组输出,所以 headneck 的每一组输出都要进行处理。对每一个输入经过不同的 stem 把通道数降维到 256,而后接入解耦的任务分支,包括分类( cls )、位置框( reg )和前背景( obj )三个网络。

 

分类网络的输出通道数是类别数,这里假设为 2,回归网络的输出通道数是 4,负责预测中心点坐标和高宽尺寸,前背景网络的输出通道数是 1,因此输出的通道数是 2+4+1=7。将这三个网络的输出然后拼接到一起,放到一个列表中。因此, head 部分得到的输出为三组数据: 7X80X80, 7X40X40, 7X20X20 。以 7X80X80 为例,表示预测了 80X80 个目标,每个目标包括位置、类别和前背景共 7 个参数。

 

 

训练部分

 

这一部分是难点,或者说,是任何目标检测算法的实现难点,代码量也是最大。

 

预处理

 

在这一部分,将对 head 的三个输出进行一些转换并生成对应的 grid 信息,将预测输出对应到图像中的实际位置。这一部分大概分以下步骤:

 

 

    1. 获取输出特征的的宽度和高度,如 80 和 80,或者 40 和 40,那幺就生成对应的

grid

    1. ,如 [0, 1] [0, 2] … [80, 80] 共 6400 个,维度是 [1, 6400, 2]

 

    1. 将预测结果

reshape

Batch, HxW, C

    1. 大小,坐标的

x

y

    1. 加上

grid

    1. 会映射到每个预测特征点的中心位置,在乘以 8,也就是理想情况下位置信息的运算结果会在 640 X 640 之间,也就是图像上目标的中心点

 

    1. 计算

w

h

    1. 的 $e$ 次方,再乘以 8,得到目标框的高度和宽度。此时返回得到的

grid

    1. 和变换过后的

output

    1. 。(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

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注