0
点赞
收藏
分享

微信扫一扫

w~视觉~YOLO~合集1

一、其他yolo


1.1 YOLO-MS

使用相当数量的参数和 FLOP 时,YOLO-MS 优于最新最先进的实时目标检测器,包括 YOLO-v7 和 RTMDet。以YOLO-MS的XS版本为例,仅4.5M可学习参数和8.7G FLOPs,在MS COCO上即可达到43%+的AP分数。南开大学提出即插即用YOLO-MS:超越 YOLOv8 与 RTMDet

论文链接:https://arxiv.org/pdf/2308.05480.pdf

代码链接:https://github.com/FishAndWasabi/YOLO-MS

为了验证作者策略的有效性,作者构建了一个名为YOLO-MS的网络架构。作者在MS COCO数据集上从头开始训练YOLO-MS,而不依赖于任何其他大规模数据集,如ImageNet,或预训练权重。作者的YOLO-MS在使用相同数量的参数和FLOPs的情况下,优于最近的最先进的实时目标检测器,包括YOLO-v7和RTMDet。以YOLO-MS的XS版本为例,仅具有450万个可学习参数和8.7亿个FLOPs,它可以在MS COCO上达到43%+的AP得分,比相同模型尺寸的RTMDet高出约2%+。

此外,作者的工作还可以作为其他YOLO模型的即插即用模块。通常情况下,作者的方法可以将YOLOv8的AP从37%+显著提高到40%+,甚至还可以使用更少的参数和FLOPs。

实时目标检测,以YOLO系列为例,已在工业领域中找到重要应用,特别是在边缘设备(如无人机和机器人)中。与之前的目标检测器不同,实时目标检测器旨在在速度和准确性之间追求最佳平衡。为了实现这一目标,提出了大量的工作:从第一代DarkNet到CSPNet,再到最近的扩展ELAN,随着性能的快速增长,实时目标检测器的架构经历了巨大的变化。

尽管性能令人印象深刻,但在不同尺度上识别对象仍然是实时目标检测器面临的基本挑战。这促使作者设计了一个强大的编码器架构,用于学习具有表现力的多尺度特征表示。

具体而言,作者从两个新的角度考虑为实时目标检测编码多尺度特征:

  • 从局部视角出发,作者设计了一个具有简单而有效的分层特征融合策略的MS-Block。受到Res2Net的启发,作者在MS-Block中引入了多个分支来进行特征提取,但不同的是,作者使用了一个带有深度卷积的 Inverted Bottleneck Block块,以实现对大Kernel的高效利用。
  • 从全局视角出发,作者提出随着网络加深逐渐增加卷积的Kernel-Size。作者在浅层使用小Kernel卷积来更高效地处理高分辨率特征。另一方面,在深层中,作者采用大Kernel卷积来捕捉广泛的信息。

基于以上设计原则,作者呈现了作者的实时目标检测器,称为YOLO-MS。为了评估作者的YOLO-MS的性能,作者在MS COCO数据集上进行了全面的实验。还提供了与其他最先进方法的定量比较,以展示作者方法的强大性能。如图1所示,YOLO-MS在计算性能平衡方面优于其他近期的实时目标检测器。

w~视觉~YOLO~合集1_视觉

具体而言,YOLO-MS-XS在MS COCO上获得了43%+的AP得分,仅具有450万个可学习参数和8.7亿个FLOPs。YOLO-MS-S和YOLO-MS分别获得了46%+和51%+的AP,可学习参数分别为810万和2220万。

此外,作者的工作还可以作为其他YOLO模型的即插即用模块。通常情况下,作者的方法可以将YOLOv8的AP从37%+显著提高到40%+,甚至还可以使用更少的参数和FLOPs。

本文方法

作为现代目标检测中的关键主题,多尺度特征表示对检测性能产生重要影响。在本节中,作者从两个角度分析如何设计一个强大的编码器架构,能够有效地从两个角度学习表达力强的多尺度特征表示。

多尺度构建块设计

CSP块是一个基于阶段级梯度路径的网络,平衡了梯度组合和计算成本。它是广泛应用于YOLO系列的基本构建块。已经提出了几种变体,包括YOLOv4和YOLOv5中的原始版本,Scaled YOLOv4中的CSPVoVNet,YOLOv7中的ELAN,以及RTMDet中提出的大Kernel单元。作者在图2(a)和图2(b)中分别展示了原始CSP块和ELAN的结构。

w~视觉~YOLO~合集1_yolo_02

上述实时检测器中被忽视的一个关键方面是如何在基本构建块中编码多尺度特征。其中一个强大的设计原则是Res2Net,它聚合了来自不同层次的特征以增强多尺度表示。然而,这一原则并没有充分探索大Kernel卷积的作用,而大Kernel卷积已经在基于CNN的视觉识别任务模型中证明有效。将大Kernel卷积纳入Res2Net的主要障碍在于它们引入的计算开销,因为构建块采用了标准卷积。

在作者的方法中,作者提出用 Inverted Bottleneck Block替代标准的3 × 3卷积,以享受大Kernel卷积的好处。

MS-Block

基于前面的分析,作者提出了一个带有分层特征融合策略的全新Block,称为MS-Block,以增强实时目标检测器在提取多尺度特征时的能力,同时保持快速的推理速度。

w~视觉~YOLO~合集1_yolo_03

根据这个公式,作者不将 Inverted Bottleneck Block层连接到,使其作为跨阶段连接,并保留来自前面层的信息。最后,作者将所有分割连接在一起,并应用1×1卷积来在所有分割之间进行交互,每个分割都编码不同尺度的特征。当网络加深时,这个1×1卷积也用于调整通道数。

异构Kernel选择协议

除了构建块的设计外,作者还从宏观角度探讨了卷积的使用。之前的实时目标检测器在不同的编码器阶段采用了同质卷积(即具有相同Kernel-Size的卷积),但作者认为这不是提取多尺度语义信息的最佳选项。

在金字塔结构中,从检测器的浅阶段提取的高分辨率特征通常用于捕捉细粒度语义,将用于检测小目标。相反,来自网络较深阶段的低分辨率特征用于捕捉高级语义,将用于检测大目标。如果作者在所有阶段都采用统一的小Kernel卷积,深阶段的有效感受野(ERF)将受到限制,影响大目标的性能。在每个阶段中引入大Kernel卷积可以帮助解决这个问题。然而,具有大的ERF的大Kernel可以编码更广泛的区域,这增加了在小目标外部包含噪声信息的概率,并且降低了推理速度。

在这项工作中,作者建议在不同阶段中采用异构卷积,以帮助捕获更丰富的多尺度特征。具体来说,在编码器的第一个阶段中,作者采用最小Kernel卷积,而最大Kernel卷积位于最后一个阶段。随后,作者逐步增加中间阶段的Kernel-Size,使其与特征分辨率的增加保持一致。这种策略允许提取细粒度和粗粒度的语义信息,增强了编码器的多尺度特征表示能力。

w~视觉~YOLO~合集1_视觉_04

正如图3所示,作者将k的值分别分配给编码器中的浅阶段到深阶段,取值为3、5、7和9。作者将其称为异构Kernel选择(HKS)协议。

w~视觉~YOLO~合集1_视觉_05

如表1所示,将大Kernel卷积应用于高分辨率特征会产生较高的计算开销。然而,作者的HKS协议在低分辨率特征上采用大Kernel卷积,从而与仅使用大Kernel卷积相比,大大降低了计算成本。

在实践中,作者经验性地发现,采用HKS协议的YOLO-MS的推理速度几乎与仅使用深度可分离的3 × 3卷积相同。

架构

如图3所示,作者模型的Backbone由4个阶段组成,每个阶段后面跟随1个步长为2的3 × 3卷积进行下采样。在第3个阶段后,作者添加了1个SPP块,与RTMDet中一样。在作者的编码器上,作者使用PAFPN作为Neck来构建特征金字塔[31, 35]。它融合了从Backbone不同阶段提取的多尺度特征。Neck中使用的基本构建块也是作者的MS-Block,在其中使用3 × 3深度可分离卷积进行快速推理。

此外,为了在速度和准确性之间取得更好的平衡,作者将Backbone中多级特征的通道深度减半。作者提供了3个不同尺度的YOLO-MS变体,即YOLO-MS-XS、YOLO-MS-S和YOLO-MS。不同尺度的YOLO-MS的详细配置列在表2中。对于YOLO-MS的其他部分,作者将其保持与RTMDet相同。

w~视觉~YOLO~合集1_yolo_06

实验

Analysis of MS-Block

在本小节中,作者对作者的MS-Block进行了一系列消融分析。默认情况下,作者对所有实验都使用YOLO-MS-XS模型。

Inverted Bottleneck Block

w~视觉~YOLO~合集1_视觉_07

w~视觉~YOLO~合集1_视觉_08

特征融合策略通常情况下,MS-Block通过加法逐步融合相邻分支之间的特征。作者进行了消融研究,以评估特征融合策略的有效性。

w~视觉~YOLO~合集1_yolo_09

结果如表4所示,表明分支之间的特征融合对于提高模型性能至关重要。特别地,它使YOLO-MS的AP得分显著提高了+1.2%。

MS-Layers的数量

w~视觉~YOLO~合集1_视觉_10

可以看出,MS-Block中的MS-Layers数量显著影响了YOLO-MS的速度。例如,在YOLO-MS-XS的情况下,随着从1增加到2,然后增加到3,参数数量分别增加了25.8%和51.5%。

此外,FLOPs分别增加了18.1%和36.2%。当= 2和 = 3时,推理过程的FPS也分别下降了9.2%和16.6%。因此,作者在所有后续实验中将= 1作为默认设置。

注意机制

w~视觉~YOLO~合集1_yolo_11

与RTMDet一致,作者在最后的1 × 1卷积之后使用SE注意力来捕捉通道间的相关性。作者进行实验研究以研究通道注意力的影响。计算分析见表6,性能见表12。有趣的是,注意力机制只能略微提高性能,但会降低推理时间。因此,用户可以根据自己的条件选择性地使用通道注意力。分支数量

w~视觉~YOLO~合集1_yolo_12

w~视觉~YOLO~合集1_视觉_13

PAFPN模块消融分析 

w~视觉~YOLO~合集1_视觉_14

实验结果表明,作者提出的方法在几乎不增加计算成本的情况下,可以产生与不使用预训练权重的PAFPN相近的性能。

此外,作者提出的方法还优于没有PAFPN的Baseline模型。此外,作者的方法与FPN模块是正交的。作者将原始PAFPN与PAFPN-MS(带有MS-Block的PAFPN)进行了比较。如实验结果所示,带有PAFPN-MS的检测器在仅有约60%的参数和约80%的FLOPs的情况下,获得了更好的性能(+0.2% AP)。

图像分辨率分析

在这里,作者进行了一个实验,以研究图像分辨率与多尺度构建块设计之间的关系。作者在推理过程中应用了测试时间增强(Test Time Augmentation),对图像进行多尺度变换(320 × 320、640 × 640和1280 × 1280)。

另外,作者还分别使用这些分辨率进行了测试。需要注意的是,作者在训练中使用的图像分辨率为640 × 640。

w~视觉~YOLO~合集1_视觉_15

结果见表10。实验结果表明了一个一致的趋势:随着图像分辨率的增加,AP也会增加。然而,低分辨率图像可以实现更高的APl。这也验证了作者的HKS协议的有效性。

应用于其他YOLO模型

w~视觉~YOLO~合集1_yolo_16

结果如表11所示,YOLOv6和YOLOv8的AP得分可以分别提高到43.5% (+2.5%)和40.3% (+3.1%),并且参数和FLOPs都更少。HKS协议分析

w~视觉~YOLO~合集1_视觉_17

w~视觉~YOLO~合集1_视觉_18

如表8中所示,作者实验的结果揭示了有趣的见解。作者观察到,简单地增加卷积Kernel-Size并不总是会显著提高性能。然而,当作者使用HKS时,作者在性能上取得了显著的提升(43.4%的AP),这优于所有其他均匀卷积Kernel-Size设置。

此外,卷积核在阶段内的排列顺序起着关键作用。具体而言,在浅阶段使用大Kernel,在深阶段使用小Kernel时,性能与HKS相比下降了0.9%的AP。这表明,与浅阶段相比,深阶段需要更大的感受野来有效捕捉粗粒度信息。

考虑到计算成本,作者的HKS因其计算开销最小而脱颖而出。这表明,通过在合适的位置上策略性地放置具有不同卷积Kernel-Size的卷积,作者可以最大程度地高效利用这些卷积。

有效感受野分析

先前的研究引入了有效感受野(ERF)的概念,作为理解深度卷积神经网络(CNN)行为的度量。ERF测量了受特征表示影响的输入空间中的有效区域。在这里,作者进一步利用ERF的概念来研究HKS的有效性。

具体而言,作者测量了编码器的第2、3和4阶段中高贡献像素包含的ERF的边长。

w~视觉~YOLO~合集1_视觉_19

视觉比较如图4所示。如图4(a)所示,随着卷积Kernel-Size的增加,所有阶段的ERF区域也变大,这支持卷积Kernel-Size与感受野之间的正相关性。

此外,在浅阶段,ERF区域小于大多数其他设置,而在深阶段则相反。这一观察表明,该协议在扩大深阶段的感受野的同时,不会损害浅阶段。在图4(b)中,作者可以观察到作者的HKS在深阶段实现了最大的ERF,从而更好地检测大目标。

Comparison with State-of-the-Arts

与CAM的可视化比较

为了评估检测器注意力集中在图像的哪个部分,作者使用Grad-CAM生成类响应图。作者从YOLOv6-tiny、RTMDet-tiny RTMDet、YOLOV7-tiny和YOLO-MS-XS的neck部分生成了类响应图,并从MS COCO数据集中选择了不同大小的典型图像,包括小、中和大的目标。

w~视觉~YOLO~合集1_视觉_20

可视化结果如图5所示。YOLOv6-tiny、RTMDet-tiny和YOLOV7-tiny都无法检测到密集的小目标,如人群,而且会忽略目标的某些部分。相反,YOLO-MS-XS在类响应图中对所有目标都展现出强烈的响应,表明其出色的多尺度特征表示能力。此外,它突显了作者的检测器在不同尺寸的目标和包含不同密度目标的图像中都能够实现出色的检测性能。

定量比较

此外,YOLO-MS表现出51.0%的AP,优于具有相似参数和计算复杂性的最先进的目标检测器,甚至是大型模型,如YOLOv6-M和YOLOv6-L。

总之,YOLO-MS能够作为实时目标检测的有希望的Baseline,提供强大的多尺度特征表示能力。

w~视觉~YOLO~合集1_yolo_21

 

1.2YOLO5~OneTeacher

在这里提出了一种名为 OneTeacher 的新型师生学习方法,通过对 COCO 和 Pascal VOC 进行的大量实验,充分验证了所提方法不仅可以实现优于比较方法的性能(相对于 Unbiased Teacher 提高 15.0% 的 AP),而且可以很好地处理单阶段 SSOD 中的关键问题。

Paper: https://arxiv.org/pdf/2302.11299.pdf

Code: https://github.com/luogen1996/OneTeacher

大家从中也可以看到一个趋势,便是现在监督学习领域已经是非常饱和了,如果说都到 2320 年了,你还在想着如何一昧的涨点和刷榜,那可真是要好好的反思下自己了。以今天介绍的目标检测任务为例,常规的方法其实已经很难大规模提升性能了,通用的做法来来去去无非就是这些:

整一些有的没的数据增强;

考虑一下样本分赃的策略;

换一个给力点的骨干大力出奇迹;

加多几个像注意力这种闷骚点的结构;

接一些解耦装置拆拆补补;

改进一下损失函数加速收敛;

还是搞点类似于 Rep 或者 提前多少个迭代关闭 Mosaic 的 tricks;

再不济我们直接加大输入分辨率硬 train 一发行不行?

这种吃力不讨好的事情还是得留给大公司或大型科研机构去做吧,毕竟不是每个人都有那么多时间、精力和资源去做实验。此外,大家如果能从标签和资源高效的角度出发,考虑如何设计和创新已有的成熟框架,这对学术界(换个新颖点的故事包装下?)和工业界来说何尝不是一个双赢的局面。

要知道,在大多数实际应用场景中,由于涉及到任务本身数据资源的稀缺性,又或者出于隐私保护等方面的制约因素,很多情况下无法获取到大量数据的。再者说,诸如医疗影像这类数据,哪怕真能给你搞到大量样本,费时费力不说,你去哪找那么多砖家去帮你打标对吧?所以说如何在有限的资源条件下利用好已有的成熟框架去解决实际的问题这才是大多数人值得投入的方向。

动机

本文同样是围绕半监督目标检测(Semi-Supervised Object Detection, SSOD)进行展开。作者首先表明了现有的 SSOD 方法大都是基于 Faster R-CNN 等两阶段算法所设计的,这类方法通常都不够高效。而一种直白的首先替代方案必定是改换单阶段的目标检测方法,这类方法中最具代表性的网络必定是 YOLO,速度与精度的完美权衡者。

但单阶段算法应用到 SSOD 通常会引发两个关键问题,即

低质量伪标签的局限性

涉及多任务优化的冲突

针对以上两点,本文提出了一种名为 OneTeacher 的新型师生学习方法,它具备两种创新设计,即

多视图伪标签优化 (Multi-view Pseudo-label Refinement, MPR)

解耦半监督优化 (Decoupled Semi-supervised Optimization, DSO)

具体地,MPR 通过增强视图细化和全局视图过滤提高了伪标签的质量,而 DSO 则通过结构调整和特定于任务的伪标签来处理联合优化冲突。此外,作者们还仔细修改了 YOLOv5 的实现,以最大限度地发挥 SSOD 的优势。最后,通过对 COCO 和 Pascal VOC 进行的大量实验,充分验证了所提方法不仅可以实现优于比较方法的性能(相对于 Unbiased Teacher 提高 15.0% 的 AP),而且可以很好地处理单阶段 SSOD 中的关键问题。下面让我们就这两个方法具体展开讲讲。

方法Framework

w~视觉~YOLO~合集1_视觉_22

上图为 OneTeacher 的整体框架图。可以看到,它是由两个具有相同配置的检测网络组成,即教师网络和学生网络。其中,教师网络负责生成伪标签,而学生网络则根据伪标签与真实标签一起进行训练。为此,我们可以将学生网络的优化定义为如下形式: 

w~视觉~YOLO~合集1_yolo_23

注意,EMA 的使用是为了让教师网络在训练过程中生成稳定的伪标签,从而减轻伪标签偏差的影响。在实践中,教师网络可以看作是处于不同训练状态的学生网络的集合,也可以作为训练后的目标模型。

SSL for One-stage Object Detection

众所周知,单阶段目标检测相比于两阶段目标检测方法的一个特点便是能够进行端到端优化,一步到位,一次性回归出对应的边框和类别信息。以 YOLOv5 为例,我们先来看下它的损失函数定义:

w~视觉~YOLO~合集1_视觉_24

 

这一点我们在阿里的那篇 Efficient Teacher 中也提到过,作者采用的解决方案是通过设计一种伪标签选择策略即 PLA 方法,它可以依据 score 将伪标签自动地划分为可靠伪标签和不确定伪标签。

Decoupled semi-supervised optimization

为了缓解多任务优化冲突,本文为 OneTeacher 提出了一种新颖的解耦半监督优化方案,该方案通过简单的分支结构和任务特定的伪标记策略来解耦联合优化问题。以下是示意图:

w~视觉~YOLO~合集1_视觉_25

如上图所示,对于每幅图像,我们将预测分支分解为两个独立的分支,然后得到用于分类和回归任务的预测张量。之后,我们便可以针对无监督损失执行任务特定的伪标记。具体来说,给定教师模型对未标记图像的预测,我们使用置信概率与最大分类 score 的乘积作为指标。基于这个指标,我们进一步设置了两个不同的阈值来选择用于回归和分类的伪标签。

在这里,作者按照 Unbiased Teacher 中的设置来丢弃边界框回归的无监督优化。这种任务特定的伪标签策略可以灵活调整不同任务的噪声程度,从而提高师生学习的效率。最后,在部署期间,我们还向模型添加了一个多标签分类任务,以更好的细化伪标签。

Multi-view Pseudo-label Refinement

对于像基于 Fater-RCNN 这类的两阶段 SSOD 方法来说,教师模型的预测会依次经过 RPN 和 ROI 的筛选,得到最终的伪标签集。这种 multi-stage 的选择在一定程度上可以保证伪标签的质量,但由于预测范式不同,这并不适用于单阶段模型。简单点理解就是,我们不能直接把它套上去。另一方面,单阶段模型的折衷方案是直接采用它们的置信度分数来确定伪标签,但这并不足以评估伪标签的质量。

为了解决这个问题,本文提出了一种新颖的多视图伪标签细化方案,该方案由两个主要过程组成,即

增强视图细化

全局视图过滤

MPR 的处理流程如下所示:

w~视觉~YOLO~合集1_yolo_26

具体来说,给定一个未标记的图像,我们首先应用增强视图细化来调整其伪标签信息。如图所示,教师网络将预测伪标签的边界框及其翻转的增强视图。之后,通过 IoU 值对两个不同的边界框进行比较,并从两个视图中选择匹配的边界框。下面给出相应的示意图: 

w~视觉~YOLO~合集1_视觉_27

此外,为进一步从全局视图过滤中增强 MPR,本文还引入了一个额外的多标签分类,即利用教师网络输出图像级别的多类概率分布。如果特定类别的全局概率低于设定阈值,我们将过滤此类的伪边界框。此处假设的是局部伪边界框的类别识别应该与全局一致,否则这些伪边界框往往质量较差。

总的来说,所提出的 MPR 可以过滤大量低质量的边界框,也可以大大提高伪标签的质量。MPR 之后,我们应用特定于任务的阈值来选择最终的伪标签集进行后续的分类和回归任务。

Implementation on YOLOv5

为了验证所提出的 OneTeacher 方法,作者进一步将其应用于 YOLOv5。前面我们提到,YOLOV5 和 现有的 SSOD 框架中使用的训练方法是冲突的,不像 Faster-RCNN 这样的两阶段检测网络的默认配置相对实现起来比较简单。下面我们简单分析下为什么会出现这种现象。

首先,YOLOv5 在训练时默认都会开启 Mosaic 和 Mixup 等增强,大家都知道这些均属于 strong 级别的增强,如果去掉的掉点会很严重,特别是针对从头训练一个预训练权重来说。既然如此,一个直接的想法就是保持原有的数据增强策略不变,并为学生网络也配置同等的数据扩充。那么问题来了,当我们把具备强扰动增强后的图像硬塞给教师模型时,不可避免地会产生较低质量的标签,换句话说便是不利于一致性学习。

因此,为了解决这个问题,作者将数据增强分为两组,即与边框相关和与边框无关的增强,如下表所示。

w~视觉~YOLO~合集1_yolo_28

其中,box-relevant 的增强例如 Flip-lr\Mosaic\Scale Jitter 均可以有效增强边界框信息,同时对图像表示的影响较小。相反,box-inrelevant 的方法则不会影响到 GT,但会强烈扰乱图像内容,例如颜色变换和高斯模糊。

综合上述考虑,我们可以将保留框相关的方法作为教师的弱数据增强,同时使用框相关和框无关方法作为学生的强数据增强。这种策略可以最大限度地减少对教师网络的扰动,同时保留 YOLOv5 的原始设置。

与此同时,作者还为 YOLOv5 调整了一些师生学习中常见的超参数。比如,将伪标记的阈值降低到 0.4,这在两阶段 SSOD 方法中通常设置为高分值,例如达到 0.7。这种变化归因于单阶段检测中的噪声伪标签问题,其中模型通常无法在初始阶段提供高置信度的伪标签。

w~视觉~YOLO~合集1_视觉_29

上图显示了对应的伪标签分布,结果表明 0.4 的阈值可以在伪标签的质量和数量之间实现良好的权衡(看红色部分)。同时,其他超参数如 Focal Loss 的权重 也将根据一阶段 SSOD 的训练状态设置。 

实验

w~视觉~YOLO~合集1_yolo_30

可以看到,对于 COCO 数据集来说,在仅用 1% 标注数据的前提下,有监督学习直接降低到 8.4% mAP,直白点就是基本学不到有用的知识。反观半监督学习的范式,本文方法比经典方法 UbTeacher 高出了几个点。另一方面,从 VOC 数据上的表现也不难看出,哪怕是应用全量数据,这一结论同样成立。

w~视觉~YOLO~合集1_yolo_31

此表展示了不同容量下的模型表现。可以看出,增大网络规模对于本文所提方法同样适用,性能也会随之增长,尤其是规模越大,增益越显著。

w~视觉~YOLO~合集1_视觉_32

表6展示了关于本文所提出的两个方法的相关消融实验,可以看到,MPR 和 DSO 并不是互斥的,可以有机的结合到现有的框架,帮助网络提升性能。

w~视觉~YOLO~合集1_yolo_33

表7主要对控制分类和回归分支的两个超参数做了相应的消融。通过实验结果我们发现让分类分支占比较大整体的性能会更好。

w~视觉~YOLO~合集1_yolo_34

表8为大家展示了 YOLOv5 的两种不同的数据增强设置。可以发现,与 UbTeacher 等双阶段 SSOD 方法中使用的默认增强方案相比,本文针对 YOLOv5 的新策略大大提高了模型性能,竟然提升了将近 13.5 mAP!

 

w~视觉~YOLO~合集1_yolo_35

上图可视化了 OneTeacher 使用和不使用 MPR 生成的伪标签。从这些示例中,我们首先观察到 MPR 可以有效地过滤不正确的检测,例如 Exp.(b) 中的“餐桌”。同时,类别预测错误的伪边框也可以通过 MPR 进行细化,例如 Exp.(a) 中“时钟”的错误预测。总的来说,这些结果均很好地证实了 MPR 对低质量伪标签问题的有效性。

总结

针对双阶段 SSOD 方法效率过低的问题,本文提出了一种用于单阶段检测网络的新型师生学习范例——OneTeacher。同时,考虑到单阶段 SSOD 方法中存在的两个关键挑战,即低质量伪标记和多任务优化冲突,本文提出了相应的解决方案。  

首先,针对第一个问题,OneTeacher 应用了一种称为多视图伪标签优化的新颖设计,以提高从微观到宏观视图的伪标签质量。其次,OneTeacher 采用解耦半监督优化方案,通过分支结构和特定于任务的伪标记来解决多任务优化冲突。最后,通过对 YOLOv5 的一系列改进策略,充分发挥了 OneTeacher 的性能。实验结果表明,OneTeacher 在不同设置下大大优于传统监督和半监督方法,也证实了其对上述单阶段 SSOD 关键问题的有效性。

 


1.3YOLOX改进

如何设计在 mAP 和延迟方面表现良好的单级轻量级检测器?新型的单阶段轻量检测器和各种操作的准确性和延迟。此基础上分别提出了GPU和CPU的最佳操作和架构。一份YOLOX改进的实验报告:如何设计性能优异的单阶段轻量级目标检测器

论文链接:https://arxiv.org/abs/2210.17151

这项工作是为了设计在mAP和延迟方面表现良好的单阶段轻量级检测器。对于分别以GPU和CPU为目标的基线模型,应用各种操作来代替基线模型主干网络中的主要操作。除了主干网络和操作的实验之外,还研究了几种特征金字塔网络(FPN)架构。在作为目标检测基准数据集的MS COCO数据集上,从参数数量、Gflop、GPU延迟、CPU延迟和mAP等方面分析了基准和建议的检测器。考虑到准确性和延迟之间的权衡,这项工作提出了类似或更好的网络架构。例如,提出的GPU目标骨干网络的性能优于YOLOX tiny,后者在NVIDIA GeForce RTX 2080 Ti GPU上以1.43倍的速度和0.5 mAP的精度被选为基准。

目标检测是对场景中的目标进行定位和分类的各种视觉任务之一。近年来,目标检测被应用于许多领域,如无人商店和基于人脸识别的安全系统。早期关于目标检测的研究基于两阶段检测器,它们显示出高性能但低硬件效率。如今,目标检测在现实生活中渗透得越多,对轻量化检测器的需求就越高。例如,在监视系统领域可能需要实时目标检测,或者边缘设备中可能存在诸如电池限制和计算能力等限制。然而,检测器的效率不仅受到网络中的操作的影响,还受到检测器在其上执行的硬件架构的影响。例如,MobileNetv2中提出的倒置残差瓶颈设计旨在提高效率,同时几乎不牺牲精度。同时,谷歌的TPU是为执行DNN而优化的最出色的硬件之一。不幸的是,倒置残差瓶颈瓶颈在TPU上表现不佳,因为其架构优势不适合开发TPU。因此,有必要在综合考虑操作特性和硬件架构特性的同时设计网络。

在这项工作中检查了新型的单阶段轻量检测器和各种现代操作的准确性和延迟。在此基础上分别提出了GPU和CPU的最佳操作和架构。在GPU实验中,建议的目标检测器基于YOLOX,它在前面采用融合的倒置残差瓶颈,在后面采用倒置的残差瓶颈。它在速度上优于YOLOX微型1.43倍,在精度上优于0.5mAP。在CPU实验中,尽管YOLOX tiny在mAP方面是最好的,但建议的基于PP PicoDet的实验仅显示了74%的参数数量和1.12倍的速度,同时牺牲了1.3 mAP的精度。

Lightweight detector designMicro architectures for backbone network

在GPU实验中,CSPDarknet中的CSP层被几个瓶颈架构所取代,例如MBConv、融合倒置残差瓶颈、RegNet瓶颈和沙漏瓶颈,以验证CSP层是否是YOLOX骨干网络中的最佳架构。EfficientNetv2提出,在网络前端使用融合的倒置残差瓶颈,在其余部分使用倒置残差瓶颈对准确性和效率都有好处。在本文中,讨论了仅包含倒置残差瓶颈、仅融合倒置残差瓶颈并且同时使用这两种操作的每个网络。在单个网络中使用倒置残差瓶颈和融合倒置残差瓶颈的策略称为混合倒置残差瓶颈。

在CPU实验中,使用PP-PicoDet检查深度可分离卷积运算和深度可分离卷积运算。为了与YOLOX基线进行公平比较,每个区块的通道设置与YOLOX相同。此外,PP-PicoDet中的FPN架构和检测头被YOLOX取代。

Feature pyramid network

YOLOX的PAFPN的主要操作分别是CSP层和LCPAN的深度可分离卷积。此外,这两个FPN之间的主要区别在于,在FPN操作之前,输入特征的通道是否均衡。在PAFPN中,输入特征的通道不均衡。相反,FPN输出的通道在被馈送到检测头之前被均衡。它在精度方面带来了更好的性能,但对延迟不利,因为FPN中的信道很大。相反,在LCPAN中,输入特征的信道在FPN之前被均衡。然后,输出特征的通道相同,而FPN中的通道减少。SepFPN基于YOLOX的PAFPN。也就是说,SepFPN的主要操作是CSP层,并且输入特征的通道不均衡。在这项工作中,提出了一种改进的PAFPN架构,该架构将FPN中的拼接操作替换为和。通过这样做,可以减少FPN中的通道,同时期望保留特征图中的丰富语义。该技术应用于YOLOX的PAFPN和PP-PicoDet的LCPAN,并进行了测试。

实验Experimental settings

为了公平比较,在两个基线中都使用了YOLOX的检测头。也就是说,这项工作的目标是几个操作、主干和FPN架构。在GPU实验中,除沙漏瓶颈之外的瓶颈架构的扩展比被设置为1,沙漏瓶颈的扩展比设置为0.5。与网络设计相关的超参数(例如,块的数量、每个块的通道等)被设置为与YOLOX tiny相同。在训练网络时,除网络架构之外的任何其他超参数都遵循YOLOX的默认设置。在GPU实验中,NVIDIA GeForce RTX 2080 Ti用于测量GPU延迟。在CPU实验中,Intel(R)Core(TM)i9-9900K CPU@3.60GHz用于测量CPU延迟。在测量延迟时,将小批量大小和线程数设置为1。

Baseline latency breakdown

图1显示了基线模型的GPU和CPU延迟。主干网络在GPU上占总延迟的40%,在CPU上占53%。因此,减少主干延迟以减轻检测器的重量至关重要。FPN也是这项工作中需要改进的目标,占GPU总延迟的27%,CPU总延迟的18%。与FPN相比,检测头占用更多空间。然而,因为它与探测器的损失函数高度相关,所以在所有实验中都固定了检测头,以便进行公平的比较。

w~视觉~YOLO~合集1_视觉_36

GPU-target detector

图2显示了主干网络和mAP的GPU延迟,具体取决于主干网络的主要操作。如图2a所示,就mAP而言,YOLOX tiny是最好的,但其GPU延迟是最差的。由融合的倒置残差瓶颈组成的主干网络具有最大的参数数目。然而,它的GPU延迟是所有设置中最快的。融合倒置残差瓶颈是唯一使用3×3卷积运算而不是3×3深度卷积运算的瓶颈。3×3卷积运算是最基本的卷积运算,在GPU上进行了高度优化。这就是为什么采用融合倒置残差瓶颈作为主干的主要操作的检测器在GPU上是最快的,尽管参数的数量是最大的。

这项工作的重点是Efficientnetv2中提出的策略;在网络前端使用融合的倒置残差瓶颈,其余部分使用倒置残差瓶颈。在图2a中,混合倒置残差瓶颈很明显,因为它速度快、重量轻,并且在设计方面有很多多样性。混合倒置残差瓶颈可以利用并行计算,通过使用融合倒置残差瓶颈获得更好的mAP,同时通过使用倒置残差瓶颈来追求轻量化。

此外,它具有很大的潜力,因为融合倒置残差瓶颈操作的数量是一项重要的设计策略。CSPDarknet是YOLOX的主干网络,有4个区块。因此,研究了使用1或2个融合的倒置残差瓶颈的网络。此外,由于混合倒置残差瓶颈中的参数数量小于YOLOX基线的参数数量,因此还研究了将扩展比设置为1.5的网络。

图2b显示了消融研究的结果。在等待时间和mAP方面,使用2个融合的倒置残差瓶颈和2个倒置残差瓶颈优于1个融合的倒置残差瓶颈和3个倒置残差瓶颈。

此外,使用更大扩张比率的策略仍比YOLOX基线更快。因此,图2b中的紫色点被选为GPU实验中的最佳检测器。

w~视觉~YOLO~合集1_视觉_37

CPU-target detector

在本节中,作为YOLOX骨干网络的CSPDarknet被PP-PicoDet中的PP-LCNet取代。此外,深度可分离卷积运算被应用于PP-LCNet,作为深度可分离卷积的替代,这是PP-LCNet的主要操作。PP-LCNet的通道设置与CSPDarknet的通道相同,以便公平比较。然而,由于PP-LCNet比CSPDarknet小得多,因此也会检查使用两倍于默认网络的检测器。请注意,这些检测器的FPN架构与其他检测器不同,以便使用相同的检测头进行公平比较。

图3a显示了主干网络的CPU延迟和上述检测器的mAP。使用PP-LCNet作为主干,可以生成轻量化检测器。与采用深度可分离卷积运算的检测器相比,采用蓝图可分离卷积运算作为主要运算的检测器速度较慢,并且显示出更高的mAP。不幸的是,与YOLOX基线相比,采用PP-LCNet作为主干网络的检测器显示出低mAP,其参数数量少。

为了改进mAP,还分析了具有较大通道的骨干网络。它们在牺牲延迟的同时显示了mAP的许多改进。尽管每个主干中的参数数量与YOLOX相似,但延迟或mAP都比YOLOX差。当主干网络中的通道数改变时,FPN中的通道也应改变。然而,由于检测头在所有实验中都是固定的,所以具有较大通道的PP-LCNet的输出通道是均衡的,因此其FPN架构比其他的更小。

图3b显示了图3a中相同检测器的整个检测器和mAP的CPU延迟,以考虑其FPN架构。与YOLOX基线相比,采用DSConv作为主要操作并具有较大通道的检测器显示出更快的延迟和更少的参数。

w~视觉~YOLO~合集1_yolo_38

w~视觉~YOLO~合集1_yolo_39

 

FPN architecture analysis

在本节中,使用第4.3节中搜索的主干网络来研究FPN架构的性能。PAFPN是YOLOX的默认FPN架构,在mAP方面是最好的。LCPAN是PP-PicoDet的默认FPN架构,在参数数量和延迟方面是最好的。建议的FPN架构(用和替换级联操作)与基线相比性能不佳。所提出的FPN架构的预期效果是保留语义,同时通过对不同块的特征求和来减少FPN中的通道。然而,它并没有像预期的那样工作。特别是,基于PAFPN的FPN架构在GPU上表现不佳。这意味着减少FPN中的信道并不能减少延迟,因为GPU具有巨大的并行计算能力。

结论

这项工作分析了一种新型单阶段检测器的设计空间。目标检测器的最佳结构设计取决于目标硬件和用途。图5显示了第4节中搜索的网络以及YOLOX基线。图5a表示在GPU上执行的基线、GPU目标检测器和CPU目标检测器的mAP和延迟。GPU目标检测器 用橙色圆点标记的表示最佳mAP,同时实现最佳GPU延迟,即使CPU目标检测器的参数数量较少。

因此,本文搜索的GPU目标检测器在这两方面都是最好的。图5a表示在CPU上执行的与图5a相同的检测器的mAP和延迟。用蓝点标记的CPU目标检测器显示最差的mAP,但它比其他CPU检测器快得多。

此外,与其他参数相比,它的参数数量要少得多。因此,如果没有足够的计算能力和能量预算,CPU目标检测器可能是一个很好的解决方案。图5没有绘制FPN结果。然而,FPN也是检测器中的一个重要因素,应根据用途仔细设计。

w~视觉~YOLO~合集1_视觉_40

 参考:Tech Report:One-stage Lightweight Object Detectors


1.4YOLOX部署优化训练

用给一个小不知名小板卡部署一下 不过这里用python 就当个玩具吧

YOLOX将近两年来目标检测领域的各个角度的优秀进展与YOLO进行了巧妙地集成组合并且重回Anchor Free的怀抱。本文详细的介绍将FCOS+ATSS模型换成YOLOX模型的全过程并附带相关代码。

YOLOX的Anchor Free(Anchor Based针对数据集聚类分析得到Anchor Box的方式,怕对泛化会有影响,尤其前期缺乏现场数据时)以及更有效的Label Assignment(SimOTA),使我下决心将目前所用的FCOS+ATSS模型换成YOLOX模型。

这次改动将YOLOX添加到了Yolov5上,在Yolov5的框架下,训练150个epoch的yolox-s模型的mAP也达到了39.7(且未使用mixup数据增强和random resize)。

实验机器:

1台PC:CPU: AMD Ryzen 7 1700X Eight-Core Processor, 内存: 32G, 显卡: 2张GeForce GTX 1080 Ti 11G

1台PC:CPU: AMD Ryzen 5 2600 Six-Core Processor, 内存: 32G, 显卡: 2张GeForce GTX 1080 Ti 11G

目标部署硬件:A311D开发板(带8位整型5TOPS算力的NPU)

软件版本:Python版本为3.7.7,Pytorch版本为1.7.1,Cuda版本为10.1

官方YoloX版本:https://github.com/Megvii-BaseDetection/YOLOX.git

Commits: 29df1fb9bc456fcd5c35653312d7c22c9f66b9f8 (Aug 2, 2021)

官方Yolov5版本:https://github.com/ultralytics/yolov5.git

Commits: f409d8e54f9391ce21436d33334beff3a2fd4042 (Aug 4, 2021)

选择适合NPU的架构试验:1、速度实验:

注:a、 模型在NPU上的速度实验,并不需要把模型完整地训练一遍,那样太耗时,只需要将模型导出(初始化后导出或者少量图片train一个epoch),再量化转换为NPU的模型即可。

b、 另一方面,NPU对有些层不支持、层与层之间的搭配、或层的实现完整性差异(参见:【原创】A311D模型转换问题 https://www.yuque.com/yerunyuan/npu/gwq7ak),会导致模型转换成NPU模型时失败,这样花大力气训练出来模型用不上,白白浪费时间,尤其对于小公司,训练机器资源有限,训练一个模型一两天时间就过去了,因为模型转换失败或者模型性能不达标,又要重来一遍,会推迟项目进度。

c、 对于面向产品快速部署落地而言,在开始训练模型之前,需要先确保模型能成功转换成目标硬件上,以及能在目标硬件上达到所需的性能要求。

1) YOLOX_S模型在NPU上640x640分辨率下纯推理速度(不包括前处理和后处理)每帧需要62.3ms;

2) 原来部署在NPU上的FCOS ATSS模型在同等分辨率下NPU纯推理速度每帧只需要45.58ms;

我们的FCOS对解耦头做过简化设计,但为了融合多数据集以及自有数据集训练(如,coco,wider face等),进行多种不同任务检测(如,人体检测,人脸检测等),采用更多的解耦头分支来规避数据集之间相互缺少的类别标签问题。最终更换成YOLOX_S模型,也是需要实现同时检测多个任务的功能,如果原始的YOLOX_S模型在NPU上就比FCOS在速度上差这么多,较难应用;

3) 将YOLOX_S模型的SiLU激活函数替换成ReLU激活函数在同等分辨率下NPU纯推理速度每帧只需要42.61ms;

我们所用的NPU可以将Conv+ReLU融合成一个层(注意多看NPU手册,了解哪些层的组合以及什么样的层的参数配置对性能优化更友好),而SiLU激活函数是不会做融合的,这意味着更多的运算量以及内存访问(在32位DDR4甚至DDR3的内存的NPU开发板上,内存访问对性能的影响是不容忽视的),因此,只是更换了一下激活函数推理速度便提升为原来的1.46倍了;

我很想知道SiLU比ReLU到底能提升多少的AP值(但没找到,唯一能找到的是对ImageNet数据集的分类模型来说的),如果AP提升不多,1.46倍的性能差别,觉得不值,从其他地方补回来可能更划算;

4) 将YOLOX_S模型的SiLU激活函数替换成LeakyReLU激活函数在同等分辨率下NPU纯推理速度每帧需要54.36ms;

Conv+LeakyReLU不会融合成一个层,LeakyReLU也多一点运算,性能相比ReLU也慢不少;

5) YOLOX_S模型使用ReLU+SiLU激活函数,即大部分使用ReLU激活函数小部分使用SiLU激活函数(所有stride为2的Conv、SPP、所有C3中的最后一个Conv都使用SiLU)在同等分辨率下NPU纯推理速度每帧需要44.22ms;

SiLU激活函数可以增加非线性, ReLU激活函数的非线性感觉还是比较有限:

w~视觉~YOLO~合集1_yolo_41

w~视觉~YOLO~合集1_yolo_42

而ReLU激活函数大于0部分是一条直线,只靠小于0时截断为0实现非线性,其对非线性的表达能力相对于SiLU激活函数感觉是不如的。非线性表达能力的欠缺,感觉会让模型收敛更慢,更难以训练。

全部使用SiLU激活函数推理速度较低,因此,打算使用ReLU激活函数+SiLU激活函数的方式。

注:由于YOLOX代码的模型训练速度太慢(补充:Aug 19, 2021后的版本对训练速度做了优化),下面使用YOLOv5的YOLOv5-s模型做实验。

下面是不同激活函数的YOLOX_S模型的前5个epoch的mAP值,其中ReLU+Kaiming初始化,表示卷积层的初始化使用的是针对ReLU的Kaiming初始化;

表格中的每个单元第一个数值为mAP@.5:.95,第二个的数值为mAP@.5

w~视觉~YOLO~合集1_视觉_43

选择ReLU与SiLU的组合时,只使用了第一个epoch的mAP作为参考,一般第一个epoch的mAP越高,后面的收敛速度也越快,最终的mAP一般也会更高一些。像ReLU+Kaiming初始化与ReLU两个网络一样,只是Conv参数的初始化方法不同,ReLU+Kaiming初始化开始的收敛要慢些(从mAP值的角度来看),但最终训练完150个epoch其mAP@.5:.95相差不大(一个为0.339,一个为0.338),训练迭代次数多了之后参数初始化的影响变得很小。

训练完150epoch,各模型的mAP对比(移植了YOLOX的评估方法):

w~视觉~YOLO~合集1_视觉_44

可以看到,ReLU+SiLU相比SiLU低1.1个点左右,比ReLU高1个点,ReLU+SiLU是推理速度与mAP值较好的折中方案。注:虽然Yolov5中没有使用解耦头和SimOTA,但测试模型速度时是带了解耦头的,这里也大概反映出了SiLU和ReLU对mAP值的影响。

w~视觉~YOLO~合集1_yolo_45

发现kaiming的Relu卷积初始化的两种标准差生成的参数的绝对值之和的均值,都是比默认的卷积初始化要大,分别是默认初始化的2.247倍和1.587倍(统计随机生成的100000个参数,受随机数影响,每次运行这个倍率会有少量变化,差别不大);

总得来说,kaiming的Relu卷积初始化比默认的卷积初始化的参数要大,如果最优化参数偏小,初始化为较大参数,在模型训练前期收敛确实也会慢些。

2、一个想法:

统计训练好的模型的参数,对均匀分布或正态分布做参数估计,看模型以什么方式初始化参数更合适?能否从统计角度得到更好的参数初始化方法?这对于从0开始训练模型也许会有帮助,也许对提升AP影响不大,不过可能可以减少收敛所用的epoch数,相当于提升训练速度。

合并YoloX到Yolov5中:

由于YoloX训练速度相比Yolov5慢很多 ,而我手上只有2张1080ti显卡的机器,YoloX训练yolox-s配置且只训练150epoch也要5天17小时(54.98分钟/epoch),而Yolov5训练yolov5-s配置且只训练150epoch只要1天19小时(17.48分钟/epoch),虽然yolox-s加了解耦头和SimOTA,也不至于差那么多。因此,打算将YoloX合并到Yolov5中。注:YoloX后来Aug 19, 2021的版本对训练速度做了优化,据说速度提升2倍。

另外,将YoloX合并到Yolov5后,也好对比YoloX相对于Yolov5哪些改进比较有效。解耦头可能有效,但也增加了不少计算量,Yolov5总结的计算量来看,Yolov5-s是17.1 GFLOPs,而YoloX-s是26.8 GFLOPs。实际在NPU上测试的推理速度,在640x640分辨率下,YoloX-s需要62.3ms,而Yolov5-s只需要52.00ms,慢了1.198倍,10.3ms差别还是不小的。如果像YoloX论文所说(下表)只相差1.1个AP也许并不值得,或者将解耦头从2层减少到1层。

w~视觉~YOLO~合集1_视觉_46

1、问题解决:

Yolov5的混合精度训练使用的是torch自带的torch.cuda.amp,而YoloX使用的是apex的apex.amp,使用binary_cross_entropy会引起报错:

pair_wise_cls_loss = F.binary_cross_entropy(cls_preds_, gt_cls_per_image, reductinotallow="none").sum(-1)  # [num_gt, fg_count]

报错如下:

Traceback (most recent call last):
  File "/rootfs/media/kasim/DataSet/git/yolov5/models/yolo.py", line 425, in get_losses
    obj_preds,
  File "/media/kasim/Data1/pytorch1.7_python3.7_venv/lib/python3.7/site-packages/torch/autograd/grad_mode.py", line 26, in decorate_context
    return func(*args, **kwargs)
  File "/rootfs/media/kasim/DataSet/git/yolov5/models/yolo.py", line 624, in get_assignments
    pair_wise_cls_loss = F.binary_cross_entropy(cls_preds_, gt_cls_per_image, reduction="none").sum(-1)  # [num_gt, fg_count]
  File "/media/kasim/Data1/pytorch1.7_python3.7_venv/lib/python3.7/site-packages/torch/nn/functional.py", line 2526, in binary_cross_entropy
    input, target, weight, reduction_enum)
RuntimeError: torch.nn.functional.binary_cross_entropy and torch.nn.BCELoss are unsafe to autocast.
Many models use a sigmoid layer right before the binary cross entropy layer.
In this case, combine the two layers using torch.nn.functional.binary_cross_entropy_with_logits
or torch.nn.BCEWithLogitsLoss.  binary_cross_entropy_with_logits and BCEWithLogits are
safe to autocast.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/media/kasim/DataSet/git/yolov5/train.py", line 642, in <module>
    main(opt)
  File "/media/kasim/DataSet/git/yolov5/train.py", line 540, in main
    train(opt.hyp, opt, device)
  File "/media/kasim/DataSet/git/yolov5/train.py", line 341, in train
    loss, loss_items = compute_loss(pred, targets.to(device))  # loss scaled by batch_size
  File "/rootfs/media/kasim/DataSet/git/yolov5/utils/loss.py", line 322, in __call__
    dtype=bbox_preds[0].dtype,
  File "/rootfs/media/kasim/DataSet/git/yolov5/models/yolo.py", line 465, in get_losses
    "cpu",
  File "/media/kasim/Data1/pytorch1.7_python3.7_venv/lib/python3.7/site-packages/torch/autograd/grad_mode.py", line 26, in decorate_context
    return func(*args, **kwargs)
  File "/rootfs/media/kasim/DataSet/git/yolov5/models/yolo.py", line 624, in get_assignments
    pair_wise_cls_loss = F.binary_cross_entropy(cls_preds_, gt_cls_per_image, reduction="none").sum(-1)  # [num_gt, fg_count]
  File "/media/kasim/Data1/pytorch1.7_python3.7_venv/lib/python3.7/site-packages/torch/nn/functional.py", line 2526, in binary_cross_entropy
    input, target, weight, reduction_enum)
RuntimeError: torch.nn.functional.binary_cross_entropy and torch.nn.BCELoss are unsafe to autocast.
Many models use a sigmoid layer right before the binary cross entropy layer.
In this case, combine the two layers using torch.nn.functional.binary_cross_entropy_with_logits
or torch.nn.BCEWithLogitsLoss.  binary_cross_entropy_with_logits and BCEWithLogits are
safe to autocast.

大意是torch.nn.functional.binary_cross_entropy和torch.nn.BCELoss不能进行安全的自动转换(16位浮点与32浮点之间的互转),让你使用torch.nn.functional.binary_cross_entropy_with_logits或torch.nn.BCEWithLogitsLoss 。

不过,YoloX标签分配(label assign)函数get_assignments,使用的分类loss所输入的预测类别置信度(概率)是预测类别置信度(概率)乘上预测目标置信度(概率)再开根号,伪码如下:

cls_preds_ = (cls_preds_.sigmoid_() * obj_preds_.sigmoid_()).sqrt_()

这不是标准的logits函数,无法使用binary_cross_entropy_with_logits或BCEWithLogitsLoss来代替。其实传递给F.binary_cross_entropy的cls_preds_和gt_cls_per_image已经是32位浮点了(从代码易知),因此,不需要自动转换,通过torch.cuda.amp.autocast关闭F.binary_cross_entropy的自动转换即可,伪码如下:

from torch.cuda.amp import autocast

with autocast(enabled=False):
    pair_wise_cls_loss = F.binary_cross_entropy(cls_preds_, gt_cls_per_image, reduction="none").sum(-1)  # [num_gt, fg_count]

当然也可以分别计算分类loss和目标loss再相加,毕竟最终训练的所用的loss也是这样。而,标签分配(label assign)所用的loss将预测类别置信度与预测目标置信度相乘后再求loss,可能考虑到最终NMS所用的置信度也是通过他们两者相乘得到,可能更能反应给anchor所分配的标签的可信度(loss越小越可能分配对)。开根号可能是为了统一单位?另外,两个小于1的值相乘也会变得更小,训练一开始置信度可能都比较低,再乘一起值可能会更小。

2、两个想法:

A、 如果NMS也使用的置信度在预测类别置信度与预测目标置信度相乘后再开根号对AP值会有什么影响?可能也没什么影响,毕竟NMS比的只是置信度大小,开根号后并不影响单调性(大的还是大,小的还是小),不过对置信度阈值的选择可能会有影响,置信度阈值的选择可能变得更为平滑?毕竟原来的置信度有点二次关系的意味;

B、 如果训练的总loss也加上这个loss进行辅助训练能否提升AP值?毕竟模型推理时NMS所用的置信度便是预测类别置信度与预测目标置信度乘积;

3、优化,再快一点:

将YoloX移植到Yolov5上后,训练1个epoch的时间从54.979分钟降低到了27.766分钟,训练速度提升了1.98倍,从原来训练150个epoch要5天17小时,降低到了2天21小时。不过,YoloX实现代码还有优化空间,尤其是标签分配(label assign)函数get_assignments,除了解耦头,YoloX与Yolov5的训练速度差距主要就在于SimOTA标签分配函数get_assignments,对其进行优化后(在将YoloX移植到Yolov5后,在Yolov5框架上测试),get_assignments的速度从原来的4852524ns(纳秒一帧,单GPU上测试,4.852ms)下降到2658222ns(2.658ms),forward+get_losses的速度从11661420ns(11.661ms)下降到9198305ns(9.198ms),训练1个epoch的时间降低到了23.533分钟,训练速度再提升1.1798倍,总共提升到原始YoloX的2.336倍。

注:这里的测试数据只针对我使用的机器,不同机器差别也许不一样。

SimOTA标签分配函数get_assignments各主要优化速度对比:

w~视觉~YOLO~合集1_视觉_47

1)一些小修改:

A、注意顺序:

cls_preds_.float().unsqueeze(0).repeat(num_gt, 1, 1).sigmoid_()

repeat之后数量就增多了,sigmoid_放后面无疑也增加了运算量,修改如下:

cls_preds_.float().sigmoid_().unsqueeze(0).repeat(num_gt, 1, 1)

B、不要频繁地在gpu和cpu之间切换数据:

for gt_idx in range(num_gt):
    _, pos_idx = torch.topk(cost[gt_idx], k=dynamic_ks[gt_idx].item(), largest=False)

item()函数会将gpu的数据转换为python的数据,但不要每个数据都去调用一次,如果每个数据都要转,调用tolist()函数对整个tensor做转换即可。

ks = dynamic_ks.tolist()
for gt_idx in range(num_gt):
    _, pos_idx = torch.topk(cost[gt_idx], k=ks[gt_idx], largest=False)

C、注意Tensor的创建:

expanded_strides.append(    torch.zeros(1, grid.shape[1])    .fill_(stride_this_level)    .type_as(xin[0]))

这个Tensor的创建会先对创建的Tensor清0,再填充为stride_this_level,然后做类型转换,其实这个可以一步做完:

expanded_strides.append(    torch.full((1, grid.shape[1]), stride_this_level, dtype=xin[0].dtype, device=xin[0].device))

D、等等,每一小点修改不一定能带来多少性能提升,不过积少成多、养成良好的编码习惯。

2)理解pytorch的几个函数及差别:

A、expand和repeat:它们可以完成类似的功能,但repeat会分配内存和拷贝数据,而expand不会,它只是创建新视图,因此,如果要节省内存使用量,可以使用expand;

B、view和permute:它们使用的还是原来的内存,修改这两个操作返回的数据,也会修改到这两个操作输入的数据,即它们不会分配新的内存,只是改变视图;permute会让tensor的内存变得不连续(is_contiguous函数返回False),我的理解是,对permute转置后的维度,按0,1,2,3顺序索引来读写转置后tensor,其访问到的内存不是连续的。不过,如果转置的两个维度其中一个维度为1,那么转置后还是连续的。permute转置后接view,view也不会让tensor变得连续,可以使用contiguous函数使得tensor内存变得连续,不过它会分配新的内存并拷贝数据(如果这个tensor的内存是不连续时)。reshape可以完成view相似的功能,区别是reshape相当于在view后再调用了contiguous,即reshape可能会重新分配内存和拷贝数据。

C、cat:会分配新的内存和拷贝数据,非必要不去cat,尤其是那种为了方便参数传递,cat在一起,后面又要去拆cat来处理的情况;

D、通过切片方式获取子tensor不会分配内存,通过list或tensor作为索引获取子tensor会分配内存,切片方式可以节省内存但获取的子tensor的内存是不连续的,连续的内存有时可以加速运算:

cc0 = cost[:, :2, :]  # 不会分配新内存cc1 = cost[:, 0::2, :]  # 不会分配新内存idx = torch.tensor([0, 1], dtype=torch.long)cc2 = cost[:, idx, :]  # 会分配新内存cc3 = cost[:, [0, 1], :]  # 会分配新内存print(cc1.is_contiguous(), cc2.is_contiguous(), cc3.is_contiguous())

3)get_in_boxes_info优化:

原代码:

def get_in_boxes_info(    self,    gt_bboxes_per_image,    expanded_strides,    x_shifts,    y_shifts,    total_num_anchors,    num_gt,):    expanded_strides_per_image = expanded_strides[0]    x_shifts_per_image = x_shifts[0] * expanded_strides_per_image    y_shifts_per_image = y_shifts[0] * expanded_strides_per_image    x_centers_per_image = (        (x_shifts_per_image + 0.5 * expanded_strides_per_image)        .unsqueeze(0)        .repeat(num_gt, 1)    )  # [n_anchor] -> [n_gt, n_anchor]    y_centers_per_image = (        (y_shifts_per_image + 0.5 * expanded_strides_per_image)        .unsqueeze(0)        .repeat(num_gt, 1)    )    gt_bboxes_per_image_l = (        (gt_bboxes_per_image[:, 0] - 0.5 * gt_bboxes_per_image[:, 2])        .unsqueeze(1)        .repeat(1, total_num_anchors)    )    gt_bboxes_per_image_r = (        (gt_bboxes_per_image[:, 0] + 0.5 * gt_bboxes_per_image[:, 2])        .unsqueeze(1)        .repeat(1, total_num_anchors)    )    gt_bboxes_per_image_t = (        (gt_bboxes_per_image[:, 1] - 0.5 * gt_bboxes_per_image[:, 3])        .unsqueeze(1)        .repeat(1, total_num_anchors)    )    gt_bboxes_per_image_b = (        (gt_bboxes_per_image[:, 1] + 0.5 * gt_bboxes_per_image[:, 3])        .unsqueeze(1)        .repeat(1, total_num_anchors)    )    b_l = x_centers_per_image - gt_bboxes_per_image_l    b_r = gt_bboxes_per_image_r - x_centers_per_image    b_t = y_centers_per_image - gt_bboxes_per_image_t    b_b = gt_bboxes_per_image_b - y_centers_per_image    bbox_deltas = torch.stack([b_l, b_t, b_r, b_b], 2)    is_in_boxes = bbox_deltas.min(dim=-1).values > 0.0    is_in_boxes_all = is_in_boxes.sum(dim=0) > 0    # in fixed center    center_radius = 2.5    gt_bboxes_per_image_l = (gt_bboxes_per_image[:, 0]).unsqueeze(1).repeat(        1, total_num_anchors    ) - center_radius * expanded_strides_per_image.unsqueeze(0)    gt_bboxes_per_image_r = (gt_bboxes_per_image[:, 0]).unsqueeze(1).repeat(        1, total_num_anchors    ) + center_radius * expanded_strides_per_image.unsqueeze(0)    gt_bboxes_per_image_t = (gt_bboxes_per_image[:, 1]).unsqueeze(1).repeat(        1, total_num_anchors    ) - center_radius * expanded_strides_per_image.unsqueeze(0)    gt_bboxes_per_image_b = (gt_bboxes_per_image[:, 1]).unsqueeze(1).repeat(        1, total_num_anchors    ) + center_radius * expanded_strides_per_image.unsqueeze(0)    c_l = x_centers_per_image - gt_bboxes_per_image_l    c_r = gt_bboxes_per_image_r - x_centers_per_image    c_t = y_centers_per_image - gt_bboxes_per_image_t    c_b = gt_bboxes_per_image_b - y_centers_per_image    center_deltas = torch.stack([c_l, c_t, c_r, c_b], 2)    is_in_centers = center_deltas.min(dim=-1).values > 0.0    is_in_centers_all = is_in_centers.sum(dim=0) > 0    # in boxes and in centers    is_in_boxes_anchor = is_in_boxes_all | is_in_centers_all    is_in_boxes_and_center = (        is_in_boxes[:, is_in_boxes_anchor] & is_in_centers[:, is_in_boxes_anchor]    )    return is_in_boxes_anchor, is_in_boxes_and_center

其主要计算网格中心是否在gt bboxes框中,以及网格中心是否在以gt bboxes框的中心为中心,2.5为半径(需乘上网格的stride,相当于5个网格大小的矩形框)的矩形框(中心框)中,只要满足其中一个即为前景anchor(fg_mask)记为is_in_boxes_anchor,两个都满足的anchor记为is_in_boxes_and_center(既在gt bboxes框中又在中心框中,这种框的cost要比其他前景anchor的cost要低很多,其他前景anchor的cost要加上10000)。

优化做法:

A、gt bboxes框的计算要将xywh模式(中心坐标+宽高)的框转换为xyxy的模式(左上角坐标+右下角坐标),xyxy的模式框在IOUloss和bboxes_iou里也都会再计算一遍,觉得没有必要,因此,把它统一到get_output_and_grid函数中算一遍就好了;

B、x_centers_per_image和y_centers_per_image在输入图像分辨率不变的情况下,并不需要每处理一张图片都去计算,在一个batch里输入图像的分辨率都一样的,而yolov5训练默认是没有带--multi-scale选项(多尺寸图像缩放,如,对输入的图像尺寸进行0.5到1.5倍的随机缩放),即输入图像的分辨率都是统一为640x640(默认没带--multi-scale选项,可能yolov5考虑到random_perspective中也有进行图片随机缩放?),而yolox官方代码是每10个迭代随机改变一次图像的输入尺寸(random_resize),即使64的batch size,两个GPU,相当于1个GPU的batch size为32,10个迭代相当于每处理320张图片才需要计算一次;

另外,x_centers_per_image和y_centers_per_image可以合并为xy_centers_per_image,b_l、b_t、b_r、b_b可以合并为b_lt、b_rb;

C、判断是否在以gt bboxes框的中心为中心,2.5为半径的中心框内时,计算c_l、c_t、c_r、c_b可以用下面的伪码表示:

gt_bboxes_per_image_l = gt_bboxes_per_image_x - center_radius * expanded_strides_per_imagegt_bboxes_per_image_t = gt_bboxes_per_image_y - center_radius * expanded_strides_per_imagegt_bboxes_per_image_r = gt_bboxes_per_image_x + center_radius * expanded_strides_per_imagegt_bboxes_per_image_b = gt_bboxes_per_image_y + center_radius * expanded_strides_per_imagec_l = x_centers_per_image - gt_bboxes_per_image_lc_t = y_centers_per_image - gt_bboxes_per_image_tc_r = gt_bboxes_per_image_r - x_centers_per_imagec_b = gt_bboxes_per_image_b - y_centers_per_imagecenter_deltas = cat(c_l, c_t, c_r, c_b)

center_radius * expanded_strides_per_image的计算是固定的,可以通过将gt_bboxes_per_image_l、gt_bboxes_per_image_t、gt_bboxes_per_image_r、gt_bboxes_per_image_b代入c_l、c_t、c_r、c_b公式将center_radius * expanded_strides_per_image的计算与x_centers_per_image、y_centers_per_image合并成固定项,在分辨率不变的情况下只需计算一遍:

c_l = x_centers_per_image - (gt_bboxes_per_image_x - center_radius * expanded_strides_per_image)c_t = y_centers_per_image - (gt_bboxes_per_image_y - center_radius * expanded_strides_per_image)c_r = (gt_bboxes_per_image_x + center_radius * expanded_strides_per_image) - x_centers_per_imagec_b = (gt_bboxes_per_image_y + center_radius * expanded_strides_per_image) - y_centers_per_imagecenter_deltas = cat(c_l, c_t, c_r, c_b)

交换整理:

c_l = -gt_bboxes_per_image_x + (x_centers_per_image + center_radius * expanded_strides_per_image)c_t = -gt_bboxes_per_image_y + (y_centers_per_image + center_radius * expanded_strides_per_image)c_r = gt_bboxes_per_image_x + (center_radius * expanded_strides_per_image - x_centers_per_image)c_b = gt_bboxes_per_image_y + (center_radius * expanded_strides_per_image - y_centers_per_image)center_deltas = cat(c_l, c_t, c_r, c_b)

c_l、c_t、c_r、c_b公式的括号项为固定值(分辨率不变情况下)可提取在get_output_and_grid函数中计算好,另外将x、y合并为1项计算,即:

center_lt =  xy_centers_per_image + center_radius * expanded_strides_per_image  # 固定项center_rb =  center_radius * expanded_strides_per_image - xy_centers_per_image  # 固定项center_ltrb = cat(center_lt, center_rb)  # 固定项gt_xy_center = cat(-gt_bboxes_per_image_xy,gt_bboxes_per_image_xy)center_deltas = gt_xy_center + center_ltrb

D、最终get_in_boxes_info函数优化为:

def get_in_boxes_info(    org_gt_bboxes_per_image,    gt_bboxes_per_image,    center_ltrbes,    xy_shifts,    total_num_anchors,    num_gt,):    xy_centers_per_image = xy_shifts.expand(num_gt, total_num_anchors, 2)    gt_bboxes_per_image = gt_bboxes_per_image[:, None, :].expand(num_gt, total_num_anchors, 4)    b_lt = xy_centers_per_image - gt_bboxes_per_image[..., :2]    b_rb = gt_bboxes_per_image[..., 2:] - xy_centers_per_image    bbox_deltas = torch.cat([b_lt, b_rb], 2)  # [n_gt, n_anchor, 4]    is_in_boxes = bbox_deltas.min(dim=-1).values > 0.0  # [_n_gt, _n_anchor]    is_in_boxes_all = is_in_boxes.sum(dim=0) > 0    center_ltrbes = center_ltrbes.expand(num_gt, total_num_anchors, 4)    org_gt_xy_center = org_gt_bboxes_per_image[:, 0:2]    org_gt_xy_center = torch.cat([-org_gt_xy_center, org_gt_xy_center], dim=-1)    org_gt_xy_center = org_gt_xy_center[:, None, :].expand(num_gt, total_num_anchors, 4)    center_deltas = org_gt_xy_center + center_ltrbes    is_in_centers = center_deltas.min(dim=-1).values > 0.0  # [_n_gt, _n_anchor]    is_in_centers_all = is_in_centers.sum(dim=0) > 0    # in boxes and in centers    is_in_boxes_anchor = is_in_boxes_all | is_in_centers_all  # fg_mask    is_in_boxes_and_center = (        is_in_boxes[:, is_in_boxes_anchor] & is_in_centers[:, is_in_boxes_anchor]    )    return is_in_boxes_anchor, is_in_boxes_and_center

4)bboxes_iou优化:

如上所说,已经在get_output_and_grid函数中将xywh模式(中心坐标+宽高)的框转换为xyxy的模式(左上角坐标+右下角坐标)的框,原来代码如下,可以看到xyxy为True时要比xyxy为False时计算量要少,因此,使用时可将xyxy设为True:

def bboxes_iou(bboxes_a, bboxes_b, xyxy=True):    if bboxes_a.shape[1] != 4 or bboxes_b.shape[1] != 4:        raise IndexError    if xyxy:        tl = torch.max(bboxes_a[:, None, :2], bboxes_b[:, :2])        br = torch.min(bboxes_a[:, None, 2:], bboxes_b[:, 2:])        area_a = torch.prod(bboxes_a[:, 2:] - bboxes_a[:, :2], 1)        area_b = torch.prod(bboxes_b[:, 2:] - bboxes_b[:, :2], 1)    else:        tl = torch.max(            (bboxes_a[:, None, :2] - bboxes_a[:, None, 2:] / 2),            (bboxes_b[:, :2] - bboxes_b[:, 2:] / 2),        )        br = torch.min(            (bboxes_a[:, None, :2] + bboxes_a[:, None, 2:] / 2),            (bboxes_b[:, :2] + bboxes_b[:, 2:] / 2),        )        area_a = torch.prod(bboxes_a[:, 2:], 1)        area_b = torch.prod(bboxes_b[:, 2:], 1)    en = (tl < br).type(tl.type()).prod(dim=2)    area_i = torch.prod(br - tl, 2) * en  # * ((tl < br).all())    return area_i / (area_a[:, None] + area_b - area_i)

另外,再化简一下计算,并增加inplace模式减少内存使用,修改如下:

def bboxes_iou(bboxes_a, bboxes_b, xyxy=True, inplace=False):    if bboxes_a.shape[1] != 4 or bboxes_b.shape[1] != 4:        raise IndexError    if inplace:        if xyxy:            tl = torch.max(bboxes_a[:, None, :2], bboxes_b[:, :2])            br_hw = torch.min(bboxes_a[:, None, 2:], bboxes_b[:, 2:])            br_hw.sub_(tl)  # hw            br_hw.clamp_min_(0)  # [rows, 2]            del tl            area_ious = torch.prod(br_hw, 2)  # area            del br_hw            area_a = torch.prod(bboxes_a[:, 2:] - bboxes_a[:, :2], 1)            area_b = torch.prod(bboxes_b[:, 2:] - bboxes_b[:, :2], 1)        else:            tl = torch.max(                (bboxes_a[:, None, :2] - bboxes_a[:, None, 2:] / 2),                (bboxes_b[:, :2] - bboxes_b[:, 2:] / 2),            )            br_hw = torch.min(                (bboxes_a[:, None, :2] + bboxes_a[:, None, 2:] / 2),                (bboxes_b[:, :2] + bboxes_b[:, 2:] / 2),            )            br_hw.sub_(tl)  # hw            br_hw.clamp_min_(0)  # [rows, 2]            del tl            area_ious = torch.prod(br_hw, 2)  # area            del br_hw            area_a = torch.prod(bboxes_a[:, 2:], 1)            area_b = torch.prod(bboxes_b[:, 2:], 1)        union = (area_a[:, None] + area_b - area_ious)        area_ious.div_(union)  # ious        return area_ious    else:        if xyxy:            tl = torch.max(bboxes_a[:, None, :2], bboxes_b[:, :2])            br = torch.min(bboxes_a[:, None, 2:], bboxes_b[:, 2:])            area_a = torch.prod(bboxes_a[:, 2:] - bboxes_a[:, :2], 1)            area_b = torch.prod(bboxes_b[:, 2:] - bboxes_b[:, :2], 1)        else:            tl = torch.max(                (bboxes_a[:, None, :2] - bboxes_a[:, None, 2:] / 2),                (bboxes_b[:, :2] - bboxes_b[:, 2:] / 2),            )            br = torch.min(                (bboxes_a[:, None, :2] + bboxes_a[:, None, 2:] / 2),                (bboxes_b[:, :2] + bboxes_b[:, 2:] / 2),            )            area_a = torch.prod(bboxes_a[:, 2:], 1)            area_b = torch.prod(bboxes_b[:, 2:], 1)        hw = (br - tl).clamp(min=0)  # [rows, 2]        area_i = torch.prod(hw, 2)        ious = area_i / (area_a[:, None] + area_b - area_i)        return ious

5)dynamic_k_matching优化:

原来代码:

def dynamic_k_matching(self, cost, pair_wise_ious, gt_classes, num_gt, fg_mask):    # Dynamic K    # ---------------------------------------------------------------    # gt box与前景anchor的匹配矩阵,维度为[num_gt, fg_count]    matching_matrix = torch.zeros_like(cost)    # gt_bboxes与前景anchor(fg_mask)的预测框通过bboxes_iou函数计算的两两之间的IOU,维度为[num_gt, fg_count]    ious_in_boxes_matrix = pair_wise_ious    # 每个gt bbox的候选前景anchor预测框数量,ious_in_boxes_matrix.size(1)为fg_count所有前景anchor预测框数量    n_candidate_k = min(10, ious_in_boxes_matrix.size(1))    # 计算每个gt bbox前k个最大的前景anchor预测框的IOU(gt bbox与预测框的IOU)    topk_ious, _ = torch.topk(ious_in_boxes_matrix, n_candidate_k, dim=1)    # 每个gt bbox以它的前k个最大前景anchor预测框的IOU之和作为动态候选数dynamic_k(至少为1)    dynamic_ks = torch.clamp(topk_ious.sum(1).int(), min=1)    # 为每个gt bbox选出dynamic_k个cost最小的前景anchor预测(正例索引pos_idx),    # 并将匹配矩阵matching_matrix对应位置设为1,表示为此gt bbox匹配到的候选前景anchor,    # 也可知道前景anchor匹配给了哪些gt bbox    for gt_idx in range(num_gt):        _, pos_idx = torch.topk(            cost[gt_idx], k=dynamic_ks[gt_idx].item(), largest=False        ) # dynamic_ks[gt_idx].item()多次在gpu和cpu中转化数值        matching_matrix[gt_idx][pos_idx] = 1.0    del topk_ious, dynamic_ks, pos_idx    # 计算每个前景anchor匹配到的gt bbox的数量    anchor_matching_gt = matching_matrix.sum(0)    # 每个前景anchor只能匹配一个gt bbox,如果前景anchor匹配到的gt bbox的数量多于1个,    # 只保留cost最小的那个gt bbox作为此前景anchor匹配的gt    if (anchor_matching_gt > 1).sum() > 0: # anchor_matching_gt > 1算了4次,可以给它定义一个变量        _, cost_argmin = torch.min(cost[:, anchor_matching_gt > 1], dim=0)        matching_matrix[:, anchor_matching_gt > 1] *= 0.0 # 乘以0,可以直接赋值为0        matching_matrix[cost_argmin, anchor_matching_gt > 1] = 1.0    # 计算前景anchor匹配到的gt bbox的数量,若此数量大于0,则表示此前景anchor有匹配到gt bbox,    # 从而得到匹配到gt box的前景anchor的mask(fg_mask_inboxes)    fg_mask_inboxes = matching_matrix.sum(0) > 0.0    # 计算有匹配到gt box的前景anchor的数量    num_fg = fg_mask_inboxes.sum().item()    # 前景anchor(fg_mask)更新为有匹配到gt box的哪些前景anchor,没匹配到gt box的哪些前景anchor不再作为前景anchor    fg_mask[fg_mask.clone()] = fg_mask_inboxes    # 求出有配对到gt box的前景anchor所匹配到的gt box的索引(前景anchor所匹配到的gt box索引)    # 由于每个前景anchor只能匹配一个gt,因此只有此gt位置为1(最大值),其他位置0,因此可以使用argmax得到此最大值的位置    matched_gt_inds = matching_matrix[:, fg_mask_inboxes].argmax(0)    # 获取每个前景anchor所匹配到的gt的类别    gt_matched_classes = gt_classes[matched_gt_inds]    # 求出每个前景Anchor的预测框与所匹配到的gt box的IOU    pred_ious_this_matching = (matching_matrix * pair_wise_ious).sum(0)[        fg_mask_inboxes    ]    return num_fg, gt_matched_classes, pred_ious_this_matching, matched_gt_inds

函数的功能从上面的注释应该也清楚了,为每个gt选择Dynamic k个前景anchor(正样本),k的估计为prediction aware ,先计算与每个gt最接近的10个预测(不大于前景anchor数,可能实际小于10个),再将这10个预测与gt的IOU之和作为这个gt最终的k(小于1时设为1),然后求出每个gt前k个最小cost的前景anchor预测,可以认为得到了每个gt的k个候选的前景anchor(正样本)。

这样一个前景anchor(正样本)可能会被分配给了多个gt做候选,但实际上一个前景anchor(正样本)只能匹配一个gt(如果一个预测框要预测两个gt框,到底要预测成哪个?因此,只能预测1个),因此,需要选出与此前景anchor(正样本)cost最小的gt,作为它最终匹配(分配)到的gt。

优化方法:A、matching_matrix在代码中只有0和1两种值,其实并不需要分配成cost的类型(32位浮点),定义为torch.bool或torch.uint8。cost的维度[num_gt, fg_count],num_gt为一张图片的gt bboxes的数量,fg_count为前景anchor的数量(fg_mask为True时的总项数);

B、fg_mask在代码的很多地方都使用到,其维度为[n_anchors_all],n_anchors_all表示所有的anchor数量,对于640x640分辨率为8400,通过它来取值,显然要对8400个fg_mask值都要做判断。可通过torch.nonzero函数将mask转换为索引(fg_mask_inds = torch.nonzero(fg_mask)[..., 0]),那么就可以通过索引直接访存,且只需访问作为前景的anchor;

C、对求每个gt最小cost的前k个前景anchor(正样本)的优化,原始代码:

for gt_idx in range(num_gt):    _, pos_idx = torch.topk(cost[gt_idx], k=dynamic_ks[gt_idx].item(), largest=False)    matching_matrix[gt_idx][pos_idx] = 1.0

dynamic_ks[gt_idx].item()多次在gpu和cpu中转化数值会降低速度,可以在循环外统一通过tolist()转换,如:

ks = dynamic_ks.tolist()for gt_idx in range(num_gt):    _, pos_idx = torch.topk(cost[gt_idx], k=ks[gt_idx], largest=False)    matching_matrix[gt_idx][pos_idx] = 1

GPU有众多的cuda核,每次循环执行一次torch.topk,可能很多cuda核都处于空闲状态,没有利用起来。这里不能并行使用torch.topk的原因是每个gt的k值可能不一样,为此,可以用它们中最大的k值作为k。这样,有些gt的topk会多计算,但也没关系,由于cuda核很多反而更快:

max_k = dynamic_ks.max().item()_, pos_idxes = torch.topk(cost, k=max_k, dim=1, largest=False)

注:当前k个最小的cost中有两个及以上相同的cost时,torch.topk返回的索引,大的索引会排在前面,小的索引会排在后面。不过,如果第k个只能包含到多个相同cost中的一个时,torch.topk返回的索引却又是最小的那个,如,k=1,索引100和索引200处的cost同时为最小,那么此时torch.topk返回的是100,而k=2时,torch.topk返回的是200,100的顺序。不知pytorch为何如此实现,没有去深究。所以,在前max_k有相同的cost时,原来循环方式的代码与修改后的并行代码产生的topk结果可能会有一点差别。不过都是相同cost,选哪个索引可能影响也没那么大。

现在问题是怎么给matching_matrix赋值。pos_idxes得到的是前max_k个索引,而不是每个gt各自的k个索引,因此,需要通过下面第3、4行代码,选出每个gt各自的k个索引组成的pos_idxes,不过torch.masked_select会将其转换为1维的tensor(因为k值不一样也无法作为通常的2维tensor,每行的长度不一)。

但原来的pos_idxes的每一行索引都是从0开始计数的,因此,需要在之前加上每一行的偏移offsets,最后将matching_matrix转为1维视图,直接通过index_fill_函数将索引为pos_idxes的matching_matrix赋值为1:

offsets = torch.arange(0, matching_matrix.shape[0]*matching_matrix.shape[1], step=matching_matrix.shape[1])[:, None]pos_idxes.add_(offsets)masks = (torch.arange(0, max_k)[None, :].expand(num_gt, max_k) < dynamic_ks[:, None])pos_idxes = torch.masked_select(pos_idxes, masks)matching_matrix.view(-1).index_fill_(0, pos_idxes, 1)

由于给赋值多个几行代码,当num_gt不多于3个时,速度是没有循环的方法快,当num_gt多于3个时,会更快,且执行速度和num_gt没有太大关系(只测到num_gt=13)。另外,如果每个gt的k值都一样(min_k==max_k时,有不少这种情况),可以更优化,最终代码改为:

if num_gt > 3:    min_k, max_k = torch._aminmax(dynamic_ks)    min_k, max_k = min_k.item(), max_k.item()    if min_k != max_k:        offsets = torch.arange(0, matching_matrix.shape[0] * matching_matrix.shape[1], step=matching_matrix.shape[1], dtype=torch.int, device=device)[:, None]        masks = (torch.arange(0, max_k, dtype=dynamic_ks.dtype, device=device)[None, :].expand(num_gt, max_k) < dynamic_ks[:, None])        _, pos_idxes = torch.topk(cost, k=max_k, dim=1, largest=False)        pos_idxes.add_(offsets)        pos_idxes = torch.masked_select(pos_idxes, masks)        matching_matrix.view(-1).index_fill_(0, pos_idxes, 1)    else:        _, pos_idxes = torch.topk(cost, k=max_k, dim=1, largest=False)        matching_matrix.scatter_(1, pos_idxes, 1)else:    ks = dynamic_ks.tolist()    for gt_idx in range(num_gt):        _, pos_idx = torch.topk(cost[gt_idx], k=ks[gt_idx], largest=False)        matching_matrix[gt_idx][pos_idx] = 1

D、求出每个前景Anchor的预测框与所匹配到的gt box的IOU,原代码:

pred_ious_this_matching = (matching_matrix * pair_wise_ious).sum(0)[    fg_mask_inboxes]

matching_matrix由于只有0和1两个值,且每个前景Anchor只匹配一个gt,因此,无需相乘求和的计算,可以直接索引,如下:

# pred_ious_this_matching = pair_wise_ious[:, fg_mask_inboxes_inds][matched_gt_inds, torch.arange(0, matched_gt_inds.shape[0])]  # [matched_gt_inds_count]pred_ious_this_matching = pair_wise_ious.index_select(1, fg_mask_inboxes_inds).gather(dim=0, index=matched_gt_inds[None, :])  # [1, matched_gt_inds_count]

E、由于每个前景anchor只对应一个gt,不需要求和来判断这个前景anchor有没有匹配gt(matching_matrix.sum(0) > 0),只需要判断其中是否有任何一项为1(matching_matrix.any(dim=0))。

另外,index_select、index_fill_等函数调用会比直接使用中括号带索引的速度快1.x~2倍,不过中括号带索引的执行速度也都很快,为了代码可读性也可以保持使用中括号带索引的方式。

下面也不再一一说了,最终修改代码:

def dynamic_k_matching(cost, pair_wise_ious, gt_classes, num_gt, fg_mask_inds):    # Dynamic K    # ---------------------------------------------------------------    device = cost.device    matching_matrix = torch.zeros(cost.shape, dtype=torch.uint8, device=device)  # [num_gt, fg_count]    ious_in_boxes_matrix = pair_wise_ious  # [num_gt, fg_count]    n_candidate_k = min(10, ious_in_boxes_matrix.size(1))    topk_ious, _ = torch.topk(ious_in_boxes_matrix, n_candidate_k, dim=1)    dynamic_ks = topk_ious.sum(1).int().clamp_min_(1)    if num_gt > 3:        min_k, max_k = torch._aminmax(dynamic_ks)        min_k, max_k = min_k.item(), max_k.item()        if min_k != max_k:            offsets = torch.arange(0, matching_matrix.shape[0] * matching_matrix.shape[1], step=matching_matrix.shape[1], dtype=torch.int, device=device)[:, None]            masks = (torch.arange(0, max_k, dtype=dynamic_ks.dtype, device=device)[None, :].expand(num_gt, max_k) < dynamic_ks[:, None])            _, pos_idxes = torch.topk(cost, k=max_k, dim=1, largest=False)            pos_idxes.add_(offsets)            pos_idxes = torch.masked_select(pos_idxes, masks)            matching_matrix.view(-1).index_fill_(0, pos_idxes, 1)            del topk_ious, dynamic_ks, pos_idxes, offsets, masks        else:            _, pos_idxes = torch.topk(cost, k=max_k, dim=1, largest=False)            matching_matrix.scatter_(1, pos_idxes, 1)            del topk_ious, dynamic_ks    else:        ks = dynamic_ks.tolist()        for gt_idx in range(num_gt):            _, pos_idx = torch.topk(cost[gt_idx], k=ks[gt_idx], largest=False)            matching_matrix[gt_idx][pos_idx] = 1        del topk_ious, dynamic_ks, pos_idx    anchor_matching_gt = matching_matrix.sum(0)    anchor_matching_one_more_gt_mask = anchor_matching_gt > 1    anchor_matching_one_more_gt_inds = torch.nonzero(anchor_matching_one_more_gt_mask)    if anchor_matching_one_more_gt_inds.shape[0] > 0:        anchor_matching_one_more_gt_inds = anchor_matching_one_more_gt_inds[..., 0]        # _, cost_argmin = torch.min(cost[:, anchor_matching_one_more_gt_inds], dim=0)        _, cost_argmin = torch.min(cost.index_select(1, anchor_matching_one_more_gt_inds), dim=0)        # matching_matrix[:, anchor_matching_one_more_gt_inds] = 0        matching_matrix.index_fill_(1, anchor_matching_one_more_gt_inds, 0)        matching_matrix[cost_argmin, anchor_matching_one_more_gt_inds] = 1        # fg_mask_inboxes = matching_matrix.sum(0) > 0        fg_mask_inboxes = matching_matrix.any(dim=0)        fg_mask_inboxes_inds = torch.nonzero(fg_mask_inboxes)[..., 0]    else:        fg_mask_inboxes_inds = torch.nonzero(anchor_matching_gt)[..., 0]    num_fg = fg_mask_inboxes_inds.shape[0]    matched_gt_inds = matching_matrix.index_select(1, fg_mask_inboxes_inds).argmax(0)    fg_mask_inds = fg_mask_inds[fg_mask_inboxes_inds]    gt_matched_classes = gt_classes[matched_gt_inds]    # pred_ious_this_matching = pair_wise_ious[:, fg_mask_inboxes_inds][matched_gt_inds, torch.arange(0, matched_gt_inds.shape[0])]  # [matched_gt_inds_count]    pred_ious_this_matching = pair_wise_ious.index_select(1, fg_mask_inboxes_inds).gather(dim=0, index=matched_gt_inds[None, :])  # [1, matched_gt_inds_count]    return num_fg, gt_matched_classes, pred_ious_this_matching, matched_gt_inds, fg_mask_inds

Yolov5下的YoloX训练结果:

这里为两个配置的训练结果,每种配置都只训练150个epoch,在最后15个epoch停止数据增强。

1、配置一:

YoloX模型,但使用Yolov5的训练超参配置(数据增强,学习率控制等)

1)训练命令:

python -m torch.distributed.launch --nproc_per_node 2 train.py --noautoanchor --img-size 640 --data coco.yaml --cfg models/yoloxs.yaml --hyp data/hyps/hyp.scratch.yolox.yaml --weights '' --batch-size 64 --epochs 150 --device 0,1

2)超参配置:

data/hyps/hyp.scratch.yolox.yaml,可以看到在数据增强方面相对于官方的yolox没有使用mixup,也没有使用旋转和shear,另外,也没有使用random resize:

lr0: 0.01  # initial learning rate (SGD=1E-2, Adam=1E-3)lrf: 0.2  # final OneCycleLR learning rate (lr0 * lrf)momentum: 0.937  # SGD momentum/Adam beta1weight_decay: 0.0005  # optimizer weight decay 5e-4warmup_epochs: 3.0  # warmup epochs (fractions ok)warmup_momentum: 0.8  # warmup initial momentumwarmup_bias_lr: 0.1  # warmup initial bias lrbox: 0.05  # box loss gain, not usedcls: 0.5  # cls loss gain, not usedcls_pw: 1.0  # cls BCELoss positive_weight, not usedobj: 1.0  # obj loss gain (scale with pixels), not usedobj_pw: 1.0  # obj BCELoss positive_weight, not usedhsv_h: 0.015  # image HSV-Hue augmentation (fraction)hsv_s: 0.7  # image HSV-Saturation augmentation (fraction)hsv_v: 0.4  # image HSV-Value augmentation (fraction)degrees: 0.0  # image rotation (+/- deg)translate: 0.1  # image translation (+/- fraction)scale: 0.5  # image scale (+/- gain)shear: 0.0  # image shear (+/- deg)perspective: 0.0  # image perspective (+/- fraction), range 0-0.001flipud: 0.0  # image flip up-down (probability)fliplr: 0.5  # image flip left-right (probability)mosaic: 1.0  # image mosaic (probability)mixup: 0.0  # image mixup (probability)mixup_mode: "yolov5"  # image mixup mode: "yolox" is yolox mixup, else yolov5 mixupmixup_scale: [0.5, 1.5]  # image mixup scale, used by yolox mixup modemixup_ratio: 0.5  # image mixup ratiocopy_paste: 0.0  # segment copy-paste (probability)no_aug_epochs: 15

3)模型配置:

models/yoloxs.yaml,使用yolox-s配置:

# Parametersnc: 80  # number of classesdepth_multiple: 0.33  # model depth multiplewidth_multiple: 0.50  # layer channel multipleanchors: 1  # number of anchorsloss: ComputeXLoss# YOLOv5 backbonebackbone:  # [from, number, module, args]  [[-1, 1, Focus, [64, 3]],  # 0-P1/2   [-1, 1, Conv, [128, 3, 2]],  # 1-P2/4   [-1, 3, C3, [128]],   [-1, 1, Conv, [256, 3, 2]],  # 3-P3/8   [-1, 9, C3, [256]],   [-1, 1, Conv, [512, 3, 2]],  # 5-P4/16   [-1, 9, C3, [512]],   [-1, 1, Conv, [1024, 3, 2]],  # 7-P5/32   [-1, 1, SPP, [1024, [5, 9, 13]]],   [-1, 3, C3, [1024, False]],  # 9  ]# YOLOv5 headhead:  [[-1, 1, Conv, [512, 1, 1]],   [-1, 1, nn.Upsample, [None, 2, 'nearest']],   [[-1, 6], 1, Concat, [1]],  # cat backbone P4   [-1, 3, C3, [512, False]],  # 13   [-1, 1, Conv, [256, 1, 1]],   [-1, 1, nn.Upsample, [None, 2, 'nearest']],   [[-1, 4], 1, Concat, [1]],  # cat backbone P3   [-1, 3, C3, [256, False]],  # 17 (P3/8-small)   [-1, 1, Conv, [256, 3, 2]],   [[-1, 14], 1, Concat, [1]],  # cat head P4   [-1, 3, C3, [512, False]],  # 20 (P4/16-medium)   [-1, 1, Conv, [512, 3, 2]],   [[-1, 10], 1, Concat, [1]],  # cat head P5   [-1, 3, C3, [1024, False]],  # 23 (P5/32-large)# yolox head   [17, 1, Conv, [256, 1, 1]],  # 24 lateral0 (P3/8-small)   [20, 1, Conv, [256, 1, 1]],  # 25 lateral1 (P4/16-medium)   [23, 1, Conv, [256, 1, 1]],  # 26 lateral2 (P5/32-large)   [24, 2, Conv, [256, 3, 1]],  # 27 cls0 (P3/8-small)   [24, 2, Conv, [256, 3, 1]],  # 28 reg0 (P3/8-small)   [25, 2, Conv, [256, 3, 1]],  # 29 cls1 (P4/16-medium)   [25, 2, Conv, [256, 3, 1]],  # 30 reg1 (P4/16-medium)   [26, 2, Conv, [256, 3, 1]],  # 31 cls2 (P5/32-large)   [26, 2, Conv, [256, 3, 1]],  # 32 reg2 (P5/32-large)    [[27, 28, 29, 30, 31, 32], 1, DetectX, [nc, anchors]],  # Detect(P3, P4, P5)  ]

4)COCO验证集的结果:

对last.pt的验证结果,150个epoch达到了39.7,比官方yolox的300个epoch的yolox-s的39.6差不多(注:官方yolox目前最新版本yolox-s的提升到了40.5,有机器资源的同学也可以train够300个epoch看与最新官方yolox-s的mAP值差多少,后面会放增加yolox的yolov5代码的git)。

验证命令:

python val.py --data data/coco.yaml --weights runs/train/exp/weights/last.pt --batch-size 64 --device 0,1 --save-json --classwiseAverage Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.397 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.589 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.427 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.223 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.443 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.516 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.324 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.541 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.585 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.409 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.642 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.728+---------------+-------+--------------+-------+----------------+-------+| category      | AP    | category     | AP    | category       | AP    |+---------------+-------+--------------+-------+----------------+-------+| person        | 0.535 | bicycle      | 0.280 | car            | 0.399 || motorcycle    | 0.435 | airplane     | 0.649 | bus            | 0.633 || train         | 0.637 | truck        | 0.350 | boat           | 0.249 || traffic light | 0.261 | fire hydrant | 0.647 | stop sign      | 0.647 || parking meter | 0.468 | bench        | 0.228 | bird           | 0.328 || cat           | 0.625 | dog          | 0.583 | horse          | 0.579 || sheep         | 0.488 | cow          | 0.532 | elephant       | 0.639 || bear          | 0.663 | zebra        | 0.658 | giraffe        | 0.668 || backpack      | 0.125 | umbrella     | 0.395 | handbag        | 0.131 || tie           | 0.292 | suitcase     | 0.370 | frisbee        | 0.645 || skis          | 0.202 | snowboard    | 0.279 | sports ball    | 0.416 || kite          | 0.433 | baseball bat | 0.270 | baseball glove | 0.349 || skateboard    | 0.484 | surfboard    | 0.359 | tennis racket  | 0.441 || bottle        | 0.357 | wine glass   | 0.315 | cup            | 0.387 || fork          | 0.294 | knife        | 0.160 | spoon          | 0.158 || bowl          | 0.422 | banana       | 0.261 | apple          | 0.165 || sandwich      | 0.322 | orange       | 0.281 | broccoli       | 0.236 || carrot        | 0.223 | hot dog      | 0.349 | pizza          | 0.508 || donut         | 0.462 | cake         | 0.362 | chair          | 0.288 || couch         | 0.436 | potted plant | 0.248 | bed            | 0.423 || dining table  | 0.293 | toilet       | 0.622 | tv             | 0.572 || laptop        | 0.599 | mouse        | 0.574 | remote         | 0.246 || keyboard      | 0.474 | cell phone   | 0.326 | microwave      | 0.560 || oven          | 0.365 | toaster      | 0.334 | sink           | 0.384 || refrigerator  | 0.549 | book         | 0.140 | clock          | 0.480 || vase          | 0.350 | scissors     | 0.231 | teddy bear     | 0.451 || hair drier    | 0.014 | toothbrush   | 0.189 | None           | None  |+---------------+-------+--------------+-------+----------------+-------+

2、配置二:

YoloX模型(激活函数使用relu+silu),但使用Yolov5的训练超参配置(数据增强,学习率控制等)

1)训练命令:

python -m torch.distributed.launch --nproc_per_node 2 train.py --noautoanchor --img-size 640 --data coco.yaml --cfg models/yoloxs_rslu.yaml --hyp data/hyps/hyp.scratch.yolox.yaml --weights '' --batch-size 64 --epochs 150 --device 0,1

2)超参配置:

data/hyps/hyp.scratch.yolox.yaml,使用“配置一”相同的超参配置;

3)模型配置:

models/yoloxs_rslu.yaml:

# Parametersnc: 80  # number of classesdepth_multiple: 0.33  # model depth multiplewidth_multiple: 0.50  # layer channel multipleanchors: 1  # number of anchorsloss: ComputeXLoss# YOLOv5 backbonebackbone:  # [from, number, module, args]  [[-1, 1, Focus, [64, 3, 1, None, 1, 'relu']],  # 0-P1/2   [-1, 1, Conv, [128, 3, 2, None, 1, 'silu']],  # 1-P2/4   [-1, 3, C3, [128, True, 1, 0.5, 'relu']],   [-1, 1, Conv, [256, 3, 2, None, 1, 'silu']],  # 3-P3/8   [-1, 9, C3, [256, True, 1, 0.5, 'relu_silu']],   [-1, 1, Conv, [512, 3, 2, None, 1, 'silu']],  # 5-P4/16   [-1, 9, C3, [512, True, 1, 0.5, 'relu_silu']],   [-1, 1, Conv, [1024, 3, 2, None, 1, 'silu']],  # 7-P5/32   [-1, 1, SPP, [1024, [5, 9, 13], 'silu']],   [-1, 3, C3, [1024, False, 1, 0.5, 'relu_silu']],  # 9  ]# YOLOv5 headhead:  [[-1, 1, Conv, [512, 1, 1, None, 1, 'silu']],   [-1, 1, nn.Upsample, [None, 2, 'nearest']],   [[-1, 6], 1, Concat, [1]],  # cat backbone P4   [-1, 3, C3, [512, False, 1, 0.5, 'relu_silu']],  # 13   [-1, 1, Conv, [256, 1, 1, None, 1, 'silu']],   [-1, 1, nn.Upsample, [None, 2, 'nearest']],   [[-1, 4], 1, Concat, [1]],  # cat backbone P3   [-1, 3, C3, [256, False, 1, 0.5, 'relu_silu']],  # 17 (P3/8-small)   [-1, 1, Conv, [256, 3, 2, None, 1, 'silu']],   [[-1, 14], 1, Concat, [1]],  # cat head P4   [-1, 3, C3, [512, False, 1, 0.5, 'relu_silu']],  # 20 (P4/16-medium)   [-1, 1, Conv, [512, 3, 2, None, 1, 'silu']],   [[-1, 10], 1, Concat, [1]],  # cat head P5   [-1, 3, C3, [1024, False, 1, 0.5, 'relu_silu']],  # 23 (P5/32-large)# yolox head   [17, 1, Conv, [256, 1, 1, None, 1, 'silu']],  # 24 lateral0 (P3/8-small)   [20, 1, Conv, [256, 1, 1, None, 1, 'silu']],  # 25 lateral1 (P4/16-medium)   [23, 1, Conv, [256, 1, 1, None, 1, 'silu']],  # 26 lateral2 (P5/32-large)   [24, 2, Conv, [256, 3, 1]],  # 27 cls0 (P3/8-small)   [24, 2, Conv, [256, 3, 1]],  # 28 reg0 (P3/8-small)   [25, 2, Conv, [256, 3, 1]],  # 29 cls1 (P4/16-medium)   [25, 2, Conv, [256, 3, 1]],  # 30 reg1 (P4/16-medium)   [26, 2, Conv, [256, 3, 1]],  # 31 cls2 (P5/32-large)   [26, 2, Conv, [256, 3, 1]],  # 32 reg2 (P5/32-large)    [[27, 28, 29, 30, 31, 32], 1, DetectX, [nc, anchors]],  # Detect(P3, P4, P5)  ]

4)COCO验证集的结果:

对last.pt的验证结果,150个epoch达到了38.9,比"配置一"的39.7低0.8个点,不过在NPU上的推理速度可提升1.4倍。验证命令:

python val.py --data data/coco.yaml --weights runs/train/exp/weights/last.pt --batch-size 64 --device 0,1 --save-json --classwise
Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.389
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.581
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.419
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.223
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.434
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.503
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.322
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.533
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.577
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.391
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.633
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.715

+---------------+-------+--------------+-------+----------------+-------+
| category      | AP    | category     | AP    | category       | AP    |
+---------------+-------+--------------+-------+----------------+-------+
| person        | 0.529 | bicycle      | 0.279 | car            | 0.395 |
| motorcycle    | 0.417 | airplane     | 0.629 | bus            | 0.624 |
| train         | 0.627 | truck        | 0.319 | boat           | 0.253 |
| traffic light | 0.254 | fire hydrant | 0.621 | stop sign      | 0.651 |
| parking meter | 0.477 | bench        | 0.219 | bird           | 0.310 |
| cat           | 0.612 | dog          | 0.570 | horse          | 0.565 |
| sheep         | 0.465 | cow          | 0.523 | elephant       | 0.621 |
| bear          | 0.643 | zebra        | 0.644 | giraffe        | 0.666 |
| backpack      | 0.141 | umbrella     | 0.376 | handbag        | 0.118 |
| tie           | 0.282 | suitcase     | 0.383 | frisbee        | 0.618 |
| skis          | 0.212 | snowboard    | 0.306 | sports ball    | 0.416 |
| kite          | 0.441 | baseball bat | 0.255 | baseball glove | 0.342 |
| skateboard    | 0.462 | surfboard    | 0.354 | tennis racket  | 0.425 |
| bottle        | 0.351 | wine glass   | 0.297 | cup            | 0.370 |
| fork          | 0.277 | knife        | 0.143 | spoon          | 0.144 |
| bowl          | 0.423 | banana       | 0.242 | apple          | 0.171 |
| sandwich      | 0.307 | orange       | 0.280 | broccoli       | 0.210 |
| carrot        | 0.208 | hot dog      | 0.339 | pizza          | 0.498 |
| donut         | 0.447 | cake         | 0.352 | chair          | 0.283 |
| couch         | 0.427 | potted plant | 0.243 | bed            | 0.401 |
| dining table  | 0.298 | toilet       | 0.598 | tv             | 0.570 |
| laptop        | 0.581 | mouse        | 0.574 | remote         | 0.230 |
| keyboard      | 0.469 | cell phone   | 0.304 | microwave      | 0.568 |
| oven          | 0.344 | toaster      | 0.342 | sink           | 0.372 |
| refrigerator  | 0.520 | book         | 0.140 | clock          | 0.474 |
| vase          | 0.344 | scissors     | 0.280 | teddy bear     | 0.437 |
| hair drier    | 0.001 | toothbrush   | 0.207 | None           | None  |
+---------------+-------+--------------+-------+----------------+-------+

3、增加Yolox的Yolov5代码与模型:

1)代码:

基于2021年8月31日的Commits:de534e922120b2da876e8214b976af1f82019e28的yolov5修改的代码(保持写文档时与最新的yolov5版本同步,与实验时所用版本有所不同)已提交,通过下面命令下载:

git clone https://gitee.com/SearchSource/yolov5_yolox.git

环境安装:除了yolov5原来的环境安装之外,还需安装从yolox移植过来的评估工具:yoloxtools,进入yolov5_yolox目录执行下面命令:

pip install -e .

2)模型:

A、yolox-s:

百度网盘: https://pan.baidu.com/s/1i7Si3oCv3QMGYBngJUEkvg

提取码: j4co

验证命令:

python val.py --data data/coco.yaml --weights yolox-s.pt --batch-size 64 --device 0,1 --save-json --classwise

B、yolox-s(relu+silu):

百度网盘: https://pan.baidu.com/s/1oCHzeO6w4G9PXXLtKkVhbA 

提取码: spcp

验证命令:

python val.py --data data/coco.yaml --weights yolox-s_rslu.pt --batch-size 64 --device 0,1 --save-json --classwise

关于旋转的数据增强:

官方的YoloX代码使用了-10度到10度之间的随机角度旋转的数据增强,对于检测模型里使用随机旋转的数据增强,个人是持保留意见的,因为旋转之后的gt bbox是不准的。下面为旋转数据增强实验的代码(扣取YoloX的random_perspective函数的旋转部分的代码):

def rotation(img, targets, degree=5):
    # Rotation and Scale
    M = np.eye(3)
    M[:2] = cv2.getRotationMatrix2D(angle=degree, center=(0, 0), scale=1.0)

    height, width = img.shape[:2]
    new_img = cv2.warpAffine(img, M[:2], dsize=(width, height), borderValue=(114, 114, 114))

    # Transform label coordinates
    n = len(targets)
    # warp points
    xy = np.ones((n * 4, 3))
    xy[:, :2] = targets[:, [0, 1, 2, 3, 0, 3, 2, 1]].reshape(
        n * 4, 2
    )  # x1y1, x2y2, x1y2, x2y1
    xy = xy @ M.T  # transform
    xy = xy[:, :2].reshape(n, 8)

    # create new boxes
    x = xy[:, [0, 2, 4, 6]]
    y = xy[:, [1, 3, 5, 7]]
    xy = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T

    # clip boxes
    xy[:, [0, 2]] = xy[:, [0, 2]].clip(0, width)
    xy[:, [1, 3]] = xy[:, [1, 3]].clip(0, height)
    new_targets = xy
    return new_img, new_targets

原图(红框为gt bbox):     

w~视觉~YOLO~合集1_视觉_48

旋转5度(蓝框为gt框旋转后的框,红框为手工重画的gt框,其上的绿色数值为蓝框与红框的IOU):

w~视觉~YOLO~合集1_视觉_49

旋转10度(蓝框为gt框旋转后的框,红框为手工重画的gt框,其上的绿色数值为蓝框与红框的IOU):

w~视觉~YOLO~合集1_yolo_50

 可以看到旋转后的物体框(蓝框)与真实的物体框(红框)差别还是很大的(这个也取决于旋转的角度与旋转的中心点、以及物体在图像中的位置等)。这可能可以提高低IOU的AP值,不过可能也会降低高IOU的AP值,降低预测的检测框框住物体的精准度。如果希望检测框框的很准,可能不该使用旋转的数据增强。如果对检测框框的准度要求不高,能框出来就好的话,也许旋转的数据增强可以使原来无法框出来的物体变得可以框出来。

yolov5默认的hyp.scratch.yaml配置没有使用旋转的数据增强(degrees为0),唯一使用了旋转的数据增强的yolov5配置hyp.finetune.yaml,也只用了-0.373度到0.373度(degrees为0.373)的旋转数据增强,相当于只有一个微小扰动。不知yolov5作者是否也出于相同的考虑?另外,也不知yolox作者实验将degrees设为10对AP值是否有提升?

不过如果有物体的mask标注,旋转后再计算mask标注的外接矩形作为gt bbox,觉得可以使用这种增强。

部署:

通过下面的命令将模型转换为onnx格式的模型:

python export.py --weights ./yolox_rslu.pt --include onnx --opset 10 --simplify --deploy

将转换为onnx格式的模型再使用NPU的工具链转换格式量化等操作,从而得到能在NPU上跑的模型,不同的NPU及其工具链做法会有所不同,这里就不详述NPU相关部分的转换了。





二、YOLOv1-7~~

YOLOv1-v7全系

YOLOv1-v7不同版本各有特色,在不同场景,不同上下游环境,不同资源支持的情况下,如何从容选择使用哪个版本,甚至使用哪个特定部分,都需要我们对YOLOv1-v7有一个全面的认识。故Rocky将YOLO系列每个版本都表示成下图中的五个部分,逐一进行解析,并将每个部分带入业务侧,竞赛侧,研究侧进行延伸思考,探索更多可能性。

参考资料

[1] YOLOv1-Darkent: https://github.com/pjreddie/darknet

[2] YOLOv2-Darkent: https://github.com/pjreddie/darknet

[3] YOLOv3: An Incremental Improvement: https://arxiv.org/pdf/1804.02767.pdf

[4] YOLOv3-PyTorch: https://github.com/ultralytics/yolov3

[5] YOLOv4: Optimal Speed and Accuracy of Object Detection: https://arxiv.org/pdf/2004.10934.pdf

[6] YOLOv4-Darkent: https://github.com/AlexeyAB/darknet

[7] YOLOv5-PyTorch: https://github.com/ultralytics/yolov5

[8] YOLOX: Exceeding YOLO Series in 2021: https://arxiv.org/pdf/2107.08430.pdf

[9] YOLOx-PyTorch: https://github.com/Megvii-BaseDetection/YOLOX

[10] YOLOv6:又快又准的目标检测框架开源啦: https://tech.meituan.com/2022/06/23/yolov6-a-fast-and-accurate-target-detection-framework-is-opening-source.html

[11] YOLOv6-PyTorch: https://github.com/meituan/YOLOv6

[12] Official YOLOv7-PyTorch: https://github.com/WongKinYiu/yolov7

[13] Feature Pyramid Networks for Object Detection: https://arxiv.org/pdf/1612.03144.pdf

[14] Path Aggregation Network for Instance Segmentation: https://arxiv.org/pdf/1803.01534.pdf

[15] RepVGG: Making VGG-style ConvNets Great Again: https://arxiv.org/pdf/2101.03697.pdf

这是从v1到v7 整体说一遍 有的也不是官方的应该..

 ---【目录】----

YOLO系列中Neck结构的由来以及作用

YOLOv1-v3 Neck侧解析

YOLOv4 Neck侧解析

YOLOv5 Neck侧解析

YOLOx Neck侧解析

YOLOv6 Neck侧解析

YOLOv7 Neck侧解析

w~视觉~YOLO~合集1_视觉_51

【一】YOLO系列中Neck结构的由来以及作用

YOLO从v3版本开始设计Neck结构,其中的特征融合思想最初在FPN(feature pyramid networks)网络中提出,在YOLOv3中进行结构的微调,最终成为YOLO后续系列不可或缺的部分。

FPN的思路剑指小目标,原来很多目标检测算法都是只采用高层特征进行预测,高层的特征语义信息比较丰富,但是分辨率较低,目标位置比较粗略。假设在深层网络中,最后的高层特征图中一个像素可能对应着输出图像 的像素区域,那么小于 像素的小物体的特征大概率已经丢失。与此同时,低层的特征语义信息比较少,但是目标位置准确,这是对小目标检测有帮助的。FPN将高层特征与底层特征进行融合,从而同时利用低层特征的高分辨率和高层特征的丰富语义信息,并进行了多尺度特征的独立预测,对小物体的检测效果有明显的提升。

w~视觉~YOLO~合集1_yolo_52

FPN结构

FPN论文地址:Feature Pyramid Networks for Object Detection[1]【Rocky的延伸思考】

业务侧:FPN具备业务模块沉淀价值,但还是要分场景来使用,主要在小目标场景可以尝试,但同时要兼顾上游数据侧与下游部署侧的适配。

竞赛侧:FPN的思想可谓是竞赛侧的一个利器,在分类,分割,检测等任务中都能大展拳脚,进行迁移应用。

研究侧:FPN具备作为baseline的价值,不管是进行拓展研究还是单纯学习思想,这个算法都是不错的选择。

【二】YOLOv1-v3 Neck侧解析

YOLOv1和YOLOv2都是不含Neck结构的,Rocky将在本系列的Backbone篇中对这两个模型进行详细介绍,本文中便不做过多赘述。

w~视觉~YOLO~合集1_视觉_53

YOLOv3则是较好的引入了FPN的思想,以支持后面的Head侧采用多尺度来对不同size的目标进行检测,越精细的grid cell就可以检测出越精细的目标物体。YOLOv3设置了三个不同的尺寸,分别是 , 和 ,他们之间的比例为 。YOLOv3采用全卷积的思路,在Neck侧也不例外(YOLOv1-v2中采用池化层做特征图的下采样, v3中采用卷积层来实现)。【Rocky的延伸思考】

业务侧:YOLOv3 Neck侧经过时间的考验与沉淀,非常适合作为业务侧的入场baseline部分模块进行搭建。

竞赛侧:YOLOv3 Neck架构在竞赛侧有迁移应用的价值。

研究侧:YOLOv3 Neck架构具备作为baseline的价值,不管是进行拓展研究还是单纯学习思想。

【三】YOLOv4 Neck侧解析

YOLOv4的Neck侧主要包含了SPP模块和PAN模块。

w~视觉~YOLO~合集1_视觉_54

w~视觉~YOLO~合集1_yolo_55

SPP模块在YOLOv3_SPP.cfg中率先展现,但是在YOLOv4中则成为了一个标配。SPP模块包含3个最大池化层,其滑动核(sliding kernel)尺寸分别是 和,并通过Padding操作,使每个最大池化层的输出特征图不变,用于Concat融合操作。SPP模块代替了卷积层后的常规池化层,可以增加感受野,更能获取多尺度特征,训练速度也让人满意。Yolov4论文中使用 的图像在COCO目标检测任务进行实验,SPP模块能以0.5%的额外计算代价将AP50提升2.7%。

w~视觉~YOLO~合集1_yolo_56

w~视觉~YOLO~合集1_视觉_57

SPP模块

SPP模块论文:Spatial Pyramid Pooling in Deep Convolutional Networks for Visual RecognitionPAN模块对不同层次的特征进行疯狂融合,其在FPN模块的基础上增加了自底向上的特征金字塔结构,保留了更多的浅层位置特征,将整体特征提取能力进一步提升。在引入YOLOv4时,特征图最后的融合操作相比于原论文发生了变化,从add操作改为concat操作,增加了特征图的通道数:

w~视觉~YOLO~合集1_视觉_58

PAN模块论文:Path Aggregation Network for Instance Segmentation[2]【Rocky的延伸思考】

业务侧:可以作为baseline模型的一个备选,实际效果还需通过实验来反馈。

竞赛侧:YOLOv4 Neck侧可以作为竞赛侧的提分策略。

研究侧:YOLOv4 Neck架构具备作为baseline的价值,不管是进行拓展研究还是单纯学习思想。

【四】YOLOv5 Neck侧解析

由于YOLOv5在YOLOv4发布之后没多久就开源了,且并没有论文的发表,创新性部分一直受到热议。

w~视觉~YOLO~合集1_yolo_59

YOLOv5的Neck侧也使用了SPP模块和PAN模块,但是在PAN模块进行融合后,将YOLOv4中使用的常规CBL模块替换成借鉴CSPnet设计的CSP_v5结构,加强网络特征融合的能力。【Rocky的延伸思考】

业务侧:YOLOv5 Neck侧在工程中非常稳定,且其github库更新频率让人敬佩,可以作为业务baseline模型的首选。

竞赛侧:YOLOv5 Neck侧可以作为检测和分割竞赛入场的模块。

【五】YOLOx Neck侧解析

YOLOx的Neck侧依然使用了YOLOv3的结构,并且使用了SPP模块。

w~视觉~YOLO~合集1_视觉_60

【六】YOLOv6 Neck侧解析

YOLOv6的Neck侧受到硬件感知神经网络设计思想的启发,基于RepVGG style设计了可重参数化、更高效的Rep-PAN。

w~视觉~YOLO~合集1_视觉_61

YOLOv6 Neck结构

硬件感知神经网络设计的思想基于硬件的特性、推理框架/编译框架的特点,以硬件和编译友好的结构作为设计原则,在网络构建时,综合考虑硬件计算能力、内存带宽、编译优化特性、网络表征能力等,进而获得又快又好的网络结构。Rep-PAN在PAN模块基础上,引入RepVGG style的RepBlock替换了YOLOv5中使用的CSP-Block,同时对整体Neck中的算子进行了调整,目的是在硬件上达到高效推理的同时,保持较好的多尺度特征融合能力。RepVGG论文:RepVGG: Making VGG-style ConvNets Great Again[3]【Rocky的延伸思考】

业务侧:YOLOv6的Neck侧使用了端侧友好的设计策略,我也在实际中测试过,发现其效果会因任务类型与场景的改变而变化,是否使用还需要通过实验来反馈。

竞赛侧:相信YOLOv6的Neck侧在一些限定算力资源的竞赛中会大放异彩。

【七】YOLOv7 Neck侧解析

YOLOv7的Neck侧主要包含了SPPSCP模块和优化的PAN模块。

w~视觉~YOLO~合集1_yolo_62

SPPCSP模块在SPP模块基础上在最后增加concat操作,与SPP模块之前的特征图进行融合,更加丰富了特征信息。PAN模块引入E-ELAN结构,使用expand、shuffle、merge cardinality等策略实现在不破坏原始梯度路径的情况下,提高网络的学习能力。论文原文:(E-ELAN uses expand, shuffle, merge cardinality to achieve the ability to continuously enhance the learning ability of the network without destroying the original gradient path.)

w~视觉~YOLO~合集1_视觉_63

E-ELAN模块

【延伸思考】

业务侧:YOLOv7 Neck侧作为YOLO系列最新的一版,其值得我们在业务侧进行实验验证其效果。

研究侧:YOLOv7 Ncek是刚发表的,具备很强的研究侧价值。





举报

相关推荐

0 条评论