0%

YOLOX 源码解析与小目标检测调优

最近用 yolox 的发现了一个很神奇的现象,简而言之 yolox-tiny 在单目标检测的效果比 yolox-small 好上很多(3.2mAP%),且 yolox-small 能大幅提升检测精度的方法到了 yolox-tiny 也不起作用了。网上很多 yolox 的解读基本都是翻译论文,没啥价值,还是决定仔细读一下代码,这大概也是全网第一份从源代码的角度解析 yolox 的文章。

阅读源码后发现 yolox 的 SimOTA 机制存在一些漏洞,并使用对应的方法调优。调优过程明确不采用的方案:增大模型规模、模型融合和其他消耗算力的方法,专注算法本身。

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 的输出作为输入。这里用文字描述的话太复杂了,简单的画图展示一下大概结构,精细的结构还是要看源代码:

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

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

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

训练部分

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

预处理

在这一部分,将对 head 的三个输出进行一些转换并生成对应的 grid 信息,将预测输出对应到图像中的实际位置。grid 可以理解为特征点的位置吧,是固定的,如下图所示黑色的那一个个格子(其他颜色不用看,我实在找不到类似的图了):

这一部分大概分以下步骤:

  1. 获取输出特征的的宽度和高度,如 80 和 80,或者 40 和 40,那么就生成对应的 grid,如 [0, 1] [0, 2] … [80, 80] 共 6400 个,维度是 [1, 6400, 2]
  2. 将预测结果 reshapeBatch, HxW, C 大小,坐标的 xy 加上 grid 会映射到每个预测特征点的中心位置,在乘以 8,也就是理想情况下位置信息的运算结果会在 640 X 640 之间,也就是图像上目标的中心点
  3. 计算 wh 的 $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 的激活输出相乘得到类别得分,以此得到类别损失;
  • 将没有被选中的预测结果视为负样本,也就是上面没有落入真实目标框及周围的预测结果视为预测失败,计算预测失败的损失,有一个预测结果不在,损失就是1,有 100 个不在,就是 100,然后计算这三个损失的和。

之后进行动态 k 分配,这里的 k 计算比较简单,在 10 和上一步骤选中的 fg_mask 数量取最小值就是 k,给每个真实框选取损失最小的 k 个预测结果。如果当某一个特征点指向多个真实框的时候,选取 cost 最小的真实框,之后对 fg_mask 进行更新。

计算损失

  • obj 损失是全部的预测结果和动态 k 分配后得到的 fg_mask 做交叉熵,提升检测到目标的能力
  • cls 损失基于 fg_mask 选中的预测结果,将类别的 one-hot 向量与正样本和真实框的 iou 做乘积视为目标。比如预测框和真实框的 iou 是 0.4,那么对应的类别得分就是 0.4,毕竟相交面积小。预测结果和目标做交叉熵损失
  • reg 损失是就是预测盒子和真实盒子的 iou 损失

问题分析与调优

如何解释开头的问题以及如何调优呢?通过一路 debug 找到了一些问题,我目前只发现了一点点问题,等我彻底解决完毕回来填坑(因为又又又摸不到显卡了)。

  • 第一点,由于是单目标检测任务,也就是说只有一个目标,那么小模型参数少,很容易聚焦和收敛;而大模型参数大,解空间也会更多,相对小模型难以探索到更好的解,因此一些常见的 trick (比如预训练 backbone)才会有效的提升大模型的检测效果,而对小模型而言,参数少,搜索空间小,很容易找到更优的解,因此一些 trick 并不会起到很大的作用。
  • 第二点,由于检测任务绝大多数目标是小目标,而 yolox-tiny 模型尺寸小,输出的通道数也少,底层的特征信息的保留程度好于大模型,因为模型越深,对图像细节的保留程度就越低。
  • 第三点,也是最重要的一点,由于 YOLOX 选取正样本的机制是:预测结果落入真实框,或者落入真实框的周围,这些落入真实框周围的正样本在模型初期会侥幸存活下来并通过 SimOTA ,但是,由于大部分都是小目标,会导致预测结果和真实目标毫不相交的场景,这就 reg 分支的 IoU Loss 面临难以优化的场景,我们换成 CIoU Loss 就可以了。具体可以参考这里:IoU Loss 系列

如图所示,红色是真实框,绿色是落入真实框周围的预测结果(正样本),灰色表示不参与训练。可以看到,由于目标较小,预测结果和真实标签毫不相交,IoU Loss 的损失恒定是 1,这显然是不合理的。应该根据距离预测结果与真实目标的远近而定,而 CIoU Loss 能很好的解决这一点。

如果你要从发论文的角度调优 yolox,那么建议改动它的 SimOTA 机制,但是这也只能是为了毕业而发的一篇普通的论文,还远远达不到 yolov6 问世的高度。如果是工程的角度,那么 CIoU Loss 会很适合你。结果也显示,使用 CIoU Loss 的 mAP 要远远高于不使用和 yolox-tiny,提升 4.1 的 mAP。

感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章