我自己的原文哦~ https://blog.51cto.com/whaosoft/12327538
一、其他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。核心设计基于一系列对不同Kernel-Size卷积如何影响不同尺度上目标检测性能的研究。研究结果是一种可以显著增强实时目标检测器多尺度特征表示的新策略。
为了验证作者策略的有效性,作者构建了一个名为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在计算性能平衡方面优于其他近期的实时目标检测器。
具体而言,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的结构。
上述实时检测器中被忽视的一个关键方面是如何在基本构建块中编码多尺度特征。其中一个强大的设计原则是Res2Net,它聚合了来自不同层次的特征以增强多尺度表示。然而,这一原则并没有充分探索大Kernel卷积的作用,而大Kernel卷积已经在基于CNN的视觉识别任务模型中证明有效。将大Kernel卷积纳入Res2Net的主要障碍在于它们引入的计算开销,因为构建块采用了标准卷积。
在作者的方法中,作者提出用 Inverted Bottleneck Block替代标准的3 × 3卷积,以享受大Kernel卷积的好处。
MS-Block
基于前面的分析,作者提出了一个带有分层特征融合策略的全新Block,称为MS-Block,以增强实时目标检测器在提取多尺度特征时的能力,同时保持快速的推理速度。
根据这个公式,作者不将 Inverted Bottleneck Block层连接到,使其作为跨阶段连接,并保留来自前面层的信息。最后,作者将所有分割连接在一起,并应用1×1卷积来在所有分割之间进行交互,每个分割都编码不同尺度的特征。当网络加深时,这个1×1卷积也用于调整通道数。
异构Kernel选择协议
除了构建块的设计外,作者还从宏观角度探讨了卷积的使用。之前的实时目标检测器在不同的编码器阶段采用了同质卷积(即具有相同Kernel-Size的卷积),但作者认为这不是提取多尺度语义信息的最佳选项。
在金字塔结构中,从检测器的浅阶段提取的高分辨率特征通常用于捕捉细粒度语义,将用于检测小目标。相反,来自网络较深阶段的低分辨率特征用于捕捉高级语义,将用于检测大目标。如果作者在所有阶段都采用统一的小Kernel卷积,深阶段的有效感受野(ERF)将受到限制,影响大目标的性能。在每个阶段中引入大Kernel卷积可以帮助解决这个问题。然而,具有大的ERF的大Kernel可以编码更广泛的区域,这增加了在小目标外部包含噪声信息的概率,并且降低了推理速度。
在这项工作中,作者建议在不同阶段中采用异构卷积,以帮助捕获更丰富的多尺度特征。具体来说,在编码器的第一个阶段中,作者采用最小Kernel卷积,而最大Kernel卷积位于最后一个阶段。随后,作者逐步增加中间阶段的Kernel-Size,使其与特征分辨率的增加保持一致。这种策略允许提取细粒度和粗粒度的语义信息,增强了编码器的多尺度特征表示能力。
正如图3所示,作者将k的值分别分配给编码器中的浅阶段到深阶段,取值为3、5、7和9。作者将其称为异构Kernel选择(HKS)协议。
作者的HKS协议能够在深层中扩大感受野,而不会对浅层产生任何其他影响。第4节的图4支持了作者的分析。此外,HKS不仅有助于编码更丰富的多尺度特征,还确保了高效的推理。
如表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相同。
实验
Analysis of MS-Block
在本小节中,作者对作者的MS-Block进行了一系列消融分析。默认情况下,作者对所有实验都使用YOLO-MS-XS模型。
Inverted Bottleneck Block
作者对MS-Block中 Inverted Bottleneck Block中的通道扩展比例(记作)进行了消融研究,结果如表3所示。
特征融合策略通常情况下,MS-Block通过加法逐步融合相邻分支之间的特征。作者进行了消融研究,以评估特征融合策略的有效性。
结果如表4所示,表明分支之间的特征融合对于提高模型性能至关重要。特别地,它使YOLO-MS的AP得分显著提高了+1.2%。
MS-Layers的数量
作者还分析了不同数量的MS-Layers(用Nl表示)的计算成本和推理速度。结果如表5所示。
可以看出,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作为默认设置。
注意机制
与RTMDet一致,作者在最后的1 × 1卷积之后使用SE注意力来捕捉通道间的相关性。作者进行实验研究以研究通道注意力的影响。计算分析见表6,性能见表12。有趣的是,注意力机制只能略微提高性能,但会降低推理时间。因此,用户可以根据自己的条件选择性地使用通道注意力。分支数量
PAFPN模块消融分析
作者对PAFPN模块进行了消融研究,结果见表9。PAFPN是一种广泛应用于其他YOLO模型的流行结构。作者从YOLO-MS中去除PAFPN模块,以进一步验证作者的方法的有效性。
实验结果表明,作者提出的方法在几乎不增加计算成本的情况下,可以产生与不使用预训练权重的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。
结果见表10。实验结果表明了一个一致的趋势:随着图像分辨率的增加,AP也会增加。然而,低分辨率图像可以实现更高的APl。这也验证了作者的HKS协议的有效性。
应用于其他YOLO模型
作者的方法可以作为其他YOLO模型的即插即用模块使用。为了展示作者方法的泛化能力,作者将该方法应用于其他YOLO模型,并在MS COCO数据集上进行了全面比较。
结果如表11所示,YOLOv6和YOLOv8的AP得分可以分别提高到43.5% (+2.5%)和40.3% (+3.1%),并且参数和FLOPs都更少。HKS协议分析
如表8中所示,作者实验的结果揭示了有趣的见解。作者观察到,简单地增加卷积Kernel-Size并不总是会显著提高性能。然而,当作者使用HKS时,作者在性能上取得了显著的提升(43.4%的AP),这优于所有其他均匀卷积Kernel-Size设置。
此外,卷积核在阶段内的排列顺序起着关键作用。具体而言,在浅阶段使用大Kernel,在深阶段使用小Kernel时,性能与HKS相比下降了0.9%的AP。这表明,与浅阶段相比,深阶段需要更大的感受野来有效捕捉粗粒度信息。
考虑到计算成本,作者的HKS因其计算开销最小而脱颖而出。这表明,通过在合适的位置上策略性地放置具有不同卷积Kernel-Size的卷积,作者可以最大程度地高效利用这些卷积。
作者还使用了两种新的设置,[5, 7, 9, 11]和[3, 7, 11, 15]。结果如下表所示。根据结果,直观地可以看出[3, 5, 7, 9]的设置在较低的计算成本下获得了更好的性能。
有效感受野分析
先前的研究引入了有效感受野(ERF)的概念,作为理解深度卷积神经网络(CNN)行为的度量。ERF测量了受特征表示影响的输入空间中的有效区域。在这里,作者进一步利用ERF的概念来研究HKS的有效性。
具体而言,作者测量了编码器的第2、3和4阶段中高贡献像素包含的ERF的边长。
视觉比较如图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数据集中选择了不同大小的典型图像,包括小、中和大的目标。
可视化结果如图5所示。YOLOv6-tiny、RTMDet-tiny和YOLOV7-tiny都无法检测到密集的小目标,如人群,而且会忽略目标的某些部分。相反,YOLO-MS-XS在类响应图中对所有目标都展现出强烈的响应,表明其出色的多尺度特征表示能力。此外,它突显了作者的检测器在不同尺寸的目标和包含不同密度目标的图像中都能够实现出色的检测性能。
定量比较
作者将YOLO-MS与当前最先进的目标检测器进行了比较。从表12中,作者可以看到YOLO-MS取得了显著的速度-准确性平衡。与第二好的微型检测器,即RTMDet RTMDet相比,YOLO-MS-XS达到了43.4%的AP,比使用ImageNet预训练模型的AP高了2.3%。YOLO-MS-S达到了46.2%的AP,相比YOLOv6减少了一半的参数大小,带来了5.7%的AP改进。
此外,YOLO-MS表现出51.0%的AP,优于具有相似参数和计算复杂性的最先进的目标检测器,甚至是大型模型,如YOLOv6-M和YOLOv6-L。
总之,YOLO-MS能够作为实时目标检测的有希望的Baseline,提供强大的多尺度特征表示能力。
1.2 YOLO5~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
上图为 OneTeacher 的整体框架图。可以看到,它是由两个具有相同配置的检测网络组成,即教师网络和学生网络。其中,教师网络负责生成伪标签,而学生网络则根据伪标签与真实标签一起进行训练。为此,我们可以将学生网络的优化定义为如下形式:
注意,EMA 的使用是为了让教师网络在训练过程中生成稳定的伪标签,从而减轻伪标签偏差的影响。在实践中,教师网络可以看作是处于不同训练状态的学生网络的集合,也可以作为训练后的目标模型。
SSL for One-stage Object Detection
众所周知,单阶段目标检测相比于两阶段目标检测方法的一个特点便是能够进行端到端优化,一步到位,一次性回归出对应的边框和类别信息。以 YOLOv5 为例,我们先来看下它的损失函数定义:
这一点我们在阿里的那篇 Efficient Teacher 中也提到过,作者采用的解决方案是通过设计一种伪标签选择策略即 PLA 方法,它可以依据 score 将伪标签自动地划分为可靠伪标签和不确定伪标签。
Decoupled semi-supervised optimization
为了缓解多任务优化冲突,本文为 OneTeacher 提出了一种新颖的解耦半监督优化方案,该方案通过简单的分支结构和任务特定的伪标记策略来解耦联合优化问题。以下是示意图:
如上图所示,对于每幅图像,我们将预测分支分解为两个独立的分支,然后得到用于分类和回归任务的预测张量。之后,我们便可以针对无监督损失执行任务特定的伪标记。具体来说,给定教师模型对未标记图像的预测,我们使用置信概率与最大分类 score 的乘积作为指标。基于这个指标,我们进一步设置了两个不同的阈值来选择用于回归和分类的伪标签。
在这里,作者按照 Unbiased Teacher 中的设置来丢弃边界框回归的无监督优化。这种任务特定的伪标签策略可以灵活调整不同任务的噪声程度,从而提高师生学习的效率。最后,在部署期间,我们还向模型添加了一个多标签分类任务,以更好的细化伪标签。
Multi-view Pseudo-label Refinement
对于像基于 Fater-RCNN 这类的两阶段 SSOD 方法来说,教师模型的预测会依次经过 RPN 和 ROI 的筛选,得到最终的伪标签集。这种 multi-stage 的选择在一定程度上可以保证伪标签的质量,但由于预测范式不同,这并不适用于单阶段模型。简单点理解就是,我们不能直接把它套上去。另一方面,单阶段模型的折衷方案是直接采用它们的置信度分数来确定伪标签,但这并不足以评估伪标签的质量。
为了解决这个问题,本文提出了一种新颖的多视图伪标签细化方案,该方案由两个主要过程组成,即
增强视图细化
全局视图过滤
MPR 的处理流程如下所示:
具体来说,给定一个未标记的图像,我们首先应用增强视图细化来调整其伪标签信息。如图所示,教师网络将预测伪标签的边界框及其翻转的增强视图。之后,通过 IoU 值对两个不同的边界框进行比较,并从两个视图中选择匹配的边界框。下面给出相应的示意图:
此外,为进一步从全局视图过滤中增强 MPR,本文还引入了一个额外的多标签分类,即利用教师网络输出图像级别的多类概率分布。如果特定类别的全局概率低于设定阈值,我们将过滤此类的伪边界框。此处假设的是局部伪边界框的类别识别应该与全局一致,否则这些伪边界框往往质量较差。
总的来说,所提出的 MPR 可以过滤大量低质量的边界框,也可以大大提高伪标签的质量。MPR 之后,我们应用特定于任务的阈值来选择最终的伪标签集进行后续的分类和回归任务。
Implementation on YOLOv5
为了验证所提出的 OneTeacher 方法,作者进一步将其应用于 YOLOv5。前面我们提到,YOLOV5 和 现有的 SSOD 框架中使用的训练方法是冲突的,不像 Faster-RCNN 这样的两阶段检测网络的默认配置相对实现起来比较简单。下面我们简单分析下为什么会出现这种现象。
首先,YOLOv5 在训练时默认都会开启 Mosaic 和 Mixup 等增强,大家都知道这些均属于 strong 级别的增强,如果去掉的掉点会很严重,特别是针对从头训练一个预训练权重来说。既然如此,一个直接的想法就是保持原有的数据增强策略不变,并为学生网络也配置同等的数据扩充。那么问题来了,当我们把具备强扰动增强后的图像硬塞给教师模型时,不可避免地会产生较低质量的标签,换句话说便是不利于一致性学习。
因此,为了解决这个问题,作者将数据增强分为两组,即与边框相关和与边框无关的增强,如下表所示。
其中,box-relevant 的增强例如 Flip-lr\Mosaic\Scale Jitter 均可以有效增强边界框信息,同时对图像表示的影响较小。相反,box-inrelevant 的方法则不会影响到 GT,但会强烈扰乱图像内容,例如颜色变换和高斯模糊。
综合上述考虑,我们可以将保留框相关的方法作为教师的弱数据增强,同时使用框相关和框无关方法作为学生的强数据增强。这种策略可以最大限度地减少对教师网络的扰动,同时保留 YOLOv5 的原始设置。
与此同时,作者还为 YOLOv5 调整了一些师生学习中常见的超参数。比如,将伪标记的阈值降低到 0.4,这在两阶段 SSOD 方法中通常设置为高分值,例如达到 0.7。这种变化归因于单阶段检测中的噪声伪标签问题,其中模型通常无法在初始阶段提供高置信度的伪标签。
上图显示了对应的伪标签分布,结果表明 0.4 的阈值可以在伪标签的质量和数量之间实现良好的权衡(看红色部分)。同时,其他超参数如 Focal Loss 的权重 也将根据一阶段 SSOD 的训练状态设置。
实验
可以看到,对于 COCO 数据集来说,在仅用 1% 标注数据的前提下,有监督学习直接降低到 8.4% mAP,直白点就是基本学不到有用的知识。反观半监督学习的范式,本文方法比经典方法 UbTeacher 高出了几个点。另一方面,从 VOC 数据上的表现也不难看出,哪怕是应用全量数据,这一结论同样成立。
此表展示了不同容量下的模型表现。可以看出,增大网络规模对于本文所提方法同样适用,性能也会随之增长,尤其是规模越大,增益越显著。
表6展示了关于本文所提出的两个方法的相关消融实验,可以看到,MPR 和 DSO 并不是互斥的,可以有机的结合到现有的框架,帮助网络提升性能。
表7主要对控制分类和回归分支的两个超参数做了相应的消融。通过实验结果我们发现让分类分支占比较大整体的性能会更好。
表8为大家展示了 YOLOv5 的两种不同的数据增强设置。可以发现,与 UbTeacher 等双阶段 SSOD 方法中使用的默认增强方案相比,本文针对 YOLOv5 的新策略大大提高了模型性能,竟然提升了将近 13.5 mAP!
上图可视化了 OneTeacher 使用和不使用 MPR 生成的伪标签。从这些示例中,我们首先观察到 MPR 可以有效地过滤不正确的检测,例如 Exp.(b) 中的“餐桌”。同时,类别预测错误的伪边框也可以通过 MPR 进行细化,例如 Exp.(a) 中“时钟”的错误预测。总的来说,这些结果均很好地证实了 MPR 对低质量伪标签问题的有效性。
总结
针对双阶段 SSOD 方法效率过低的问题,本文提出了一种用于单阶段检测网络的新型师生学习范例——OneTeacher。同时,考虑到单阶段 SSOD 方法中存在的两个关键挑战,即低质量伪标记和多任务优化冲突,本文提出了相应的解决方案。
首先,针对第一个问题,OneTeacher 应用了一种称为多视图伪标签优化的新颖设计,以提高从微观到宏观视图的伪标签质量。其次,OneTeacher 采用解耦半监督优化方案,通过分支结构和特定于任务的伪标记来解决多任务优化冲突。最后,通过对 YOLOv5 的一系列改进策略,充分发挥了 OneTeacher 的性能。实验结果表明,OneTeacher 在不同设置下大大优于传统监督和半监督方法,也证实了其对上述单阶段 SSOD 关键问题的有效性。
1.3 YOLOX改进
如何设计在 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相比,检测头占用更多空间。然而,因为它与探测器的损失函数高度相关,所以在所有实验中都固定了检测头,以便进行公平的比较。
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实验中的最佳检测器。
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作为主要操作并具有较大通道的检测器显示出更快的延迟和更少的参数。
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也是检测器中的一个重要因素,应根据用途仔细设计。
参考:Tech Report:One-stage Lightweight Object Detectors
1.4 YOLOX部署优化训练
用给一个小不知名小板卡部署一下 不过这里用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激活函数的非线性感觉还是比较有限:
而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
选择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的评估方法):
可以看到,ReLU+SiLU相比SiLU低1.1个点左右,比ReLU高1个点,ReLU+SiLU是推理速度与mAP值较好的折中方案。注:虽然Yolov5中没有使用解耦头和SimOTA,但测试模型速度时是带了解耦头的,这里也大概反映出了SiLU和ReLU对mAP值的影响。
发现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层。
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各主要优化速度对比:
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):
旋转5度(蓝框为gt框旋转后的框,红框为手工重画的gt框,其上的绿色数值为蓝框与红框的IOU):
旋转10度(蓝框为gt框旋转后的框,红框为手工重画的gt框,其上的绿色数值为蓝框与红框的IOU):
可以看到旋转后的物体框(蓝框)与真实的物体框(红框)差别还是很大的(这个也取决于旋转的角度与旋转的中心点、以及物体在图像中的位置等)。这可能可以提高低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相关部分的转换了。
1.5 DocLayout-YOLO
Make YOLO Great Again | DocLayout-YOLO:速度精度拉满了
论文地址:https://arxiv.org/pdf/2410.12628
论文源码:https://github.com/opendatalab/DocLayout-YOLO
项目主页:https://huggingface.co/spaces/opendatalab/DocLayout-YOLO
导读
TL;DR: 本文提出了一个名为DocLayout-YOLO
的新方法,旨在通过多样化的合成数据和全局到局部的自适应感知来增强文档布局分析。
在当今数字化的世界里,文档布局分析(DLA)是理解和处理文档的关键步骤。想象一下,你手中有一堆杂乱无章的文件,DocLayout-YOLO就像一个超级助手,能够快速帮你识别出文件中的文本、标题、表格等不同区域。这项技术对于提高文档处理的自动化和准确性至关重要。
在以往的文档解析领域,研究者们一直在速度和准确性之间寻找平衡。其中:
- 多模态方法通过结合视觉和文本信息虽然准确度高,但处理速度慢;
- 而单模态方法仅依赖视觉特征,虽然处理速度快,但准确性较低。
- DocLayout-YOLO的创新之处在于,它试图在保持速度优势的同时提高准确性。
那么,DocLayout-YOLO 是如何做到的呢?文章中披露了两大法宝:Mesh-candidate BestFit算法及全局到局部可控感受野模块(GL-CRM)。
DocLayout-YOLO
DocLayout-YOLO的首个创新是Mesh-candidate BestFit算法,它将文档合成问题视为一个二维装箱问题,通过这种方式生成了一个大规模、多样化的合成数据集DocSynth-300K
。这个数据集的预训练显著提高了在各种文档类型上的微调性能。
第二个创新点则是全局到局部可控感受野模块(GL-CRM),这个模块能够更好地处理文档元素的多尺度变化。从全局的页面尺度到局部的语义信息,GL-CRM使得模型能够有效检测不同尺度的目标。
DocSynth-300K 合成数据集的生成方法
首先,我们先来看下 DocSynth-300K 是如何合成的。如上所述,这是一个由 Mesh-candidate BestFit 的算法生成的,通过将文档合成问题视为一个二维装箱问题,利用丰富的基础组件(文本、图像、表格)来生成一个大规模、多样化的预训练语料库。
如上图所示,算法通过以下步骤来生成数据集:
- 预处理:从一个小的初始数据集中提取和构建一个按细粒度类别分类的元素池。为了保持同一类别内元素的多样性,设计了一个增强管道来扩大数量少于100个元素的稀有类别的数据池。
- 布局生成:在布局生成阶段,算法迭代地执行最佳匹配搜索,寻找候选元素和所有网格(bins)的最佳配对。找到最佳匹配对后,将候选元素插入文档中,并继续迭代搜索,直到元素数量达到阈值N(经验设置为15)。
- 候选采样:对于每个空白页面,基于元素大小从元素池中进行分层抽样,作为候选集。然后从候选集中随机抽取一个元素,并将其放置在页面上的某个位置。
- 网格构建:基于布局构建网格,并过滤掉与已插入元素重叠的无效网格。只有剩余的网格才能参与后续步骤中与候选元素的匹配。
- 最佳匹配对搜索:对于每个候选元素,遍历所有满足大小要求的网格,并寻找具有最大填充率的网格-候选元素对。然后从候选集中移除最优候选元素,并更新布局。
- 迭代布局填充:重复步骤2至3,直到没有有效的网格-候选元素对满足大小要求。最终,对所有填充的元素分别应用随机中心缩放。
最终的合成示例可以参考下图:
GL-CRM 全局到局部可控感受野模块的工作原理
GL-CRM(Global-to-Local Controllable Receptive Module)是为了更好地处理文档中不同元素的多尺度变化而设计的。它包括两个主要组件:可控感受野模块(CRM)和全局到局部设计(GL)。CRM灵活地提取和整合具有多个尺度和粒度的特征,而GL架构具有从全局上下文(整页规模)到子块区域(中等规模)再到局部语义信息的层次感知过程。
CRM:对于每一层的特征X,首先使用权重共享的卷积层w和核大小k提取特征。通过使用一组不同的扩张率d=[d1, d2, ..., dn]来捕获不同粒度的特征。然后,将这些特征融合,并允许网络自主学习如何融合不同的特征组件。
GL设计:全局级别使用较大的核和扩张率来捕获整页元素的更多纹理细节和保存局部模式。在中间阶段,特征图被下采样,纹理特征减少,此时使用较小的核和扩张率来感知中等规模的元素,如文档子块。在深层阶段,语义信息占主导地位,使用基本的瓶颈作为轻量级模块,专注于局部语义信息。
优势
DocLayout-YOLO在文档布局分析中相比其他方法具有以下优势:
- 速度与准确性的平衡:DocLayout-YOLO在保持速度优势的同时提高了准确性,这得益于其在预训练和模型设计中针对文档特定优化的增强。它匹配了单模态方法YOLOv10的速度,并在准确性上超越了所有现有方法,包括单模态和多模态方法。
- 多样化的合成数据集:通过Mesh-candidate BestFit算法生成的DocSynth-300K数据集,提供了更多样化和高质量的合成文档数据,这有助于模型在各种文档类型上进行更好的泛化。
- 全局到局部的感知能力:GL-CRM模块使得DocLayout-YOLO能够有效检测不同尺度的目标,从而提高了检测的准确性。
- 实时布局分析:DocLayout-YOLO在多样化的下游数据集上实现了高达85.5帧每秒(FPS)的推理速度,使得在多种文档上进行实时布局分析成为可能。
实验
论文中使用了两大公共数据集 D4LA 和 DocLayNet,并引入了一个复杂且具有挑战性的基准测试集DocStructBench
,包含学术、教材、市场分析和财务四个类别的文档,用于验证不同文档类型上的性能。其中,精度指标采用 COCO 风格的 mAP,速度指标为每秒处理的图片数(FPS)。
- 表1 显示了 DocSynth-300K 预训练和 GL-CRM 模块对 D4LA、DocLayNet、以及 DocStructBench 数据集的精度提升。实验表明:
- 在没有预训练和 GL-CRM 模块的情况下,模型表现较差(如在 D4LA 上 mAP 为 68.6)。
- 当同时启用 GL-CRM 和 DocSynth-300K 预训练时,mAP 提升至 70.3,表明两者的结合能带来最大的性能改进。
- 表2 主要是 D4LA 和 DocLayNet 上的性能对比
- DocLayout-YOLO 与其他主流单模态和多模态方法进行对比。在 D4LA 数据集上,DocLayout-YOLO 达到 70.3 mAP,超越了多模态模型 VGT 的 68.8 和 DiT-Cascade-B 的 67.7。
- 在 DocLayNet 上,DocLayout-YOLO 的 mAP 为 79.7,也优于 DINO-4scale(77.7)和 DiT-Cascade-B(73.2)。
结果表明 DocLayout-YOLO 无论在单模态还是多模态方法中,都表现最佳。
- 表3:DocStructBench 数据集的性能对比
- 在 DocStructBench 的四个子数据集(学术、教材、市场分析、财务)上,DocLayout-YOLO 在三个子数据集上表现最佳(如学术:mAP 81.8,财务:mAP 90.1),仅在市场分析文档中略逊于 DiT-Cascade-L。
- DocLayout-YOLO 的推理速度(85.5 FPS)远超 DINO-4scale 和 DiT-Cascade-L。
- 表4:不同预训练数据集的下游微调性能
- DocSynth-300K 预训练模型在所有文档类型上表现最好,超越了公共数据集 PubLayNet 和 DocBank。例如,DocSynth-300K 在财务文档上 mAP 为 90.3,而 PubLayNet 为 89.7。
- 这表明 DocSynth-300K 具有更强的通用性,适用于多种下游任务。
通过在下游数据集上的广泛实验,DocLayout-YOLO 的确无论在速度和准确性上都表现出色,其平均精度均值(mAP)和每秒帧数(FPS)的表现都超越了现有的方法。
应用
X-AnyLabeling 是一款基于AI推理引擎和丰富功能特性于一体的强大辅助标注工具,其专注于实际应用,致力于为图像数据工程师提供工业级的一站式解决方案,可自动快速进行各种复杂任务的标定。
作为一款与时俱进的AI标注工具,X-AnyLabeling 最新版本已无缝集成了DocLayout-YOLO模型,提供了更高精度和实时性能的文档布局分析解决方案。无论是复杂的学术文档、市场报告还是财务报表都能轻松胜任,帮助您加速数据标注流程,提高模型的精度和效率。🚀
赶紧体验吧!对自定义数据集进行标注,构建并优化属于您的自定义AI模型,提升文档解析能力!
源码地址:https://github.com/CVHub520/X-AnyLabeling
安装指南:https://github.com/CVHub520/X-AnyLabeling/blob/main/docs/zh_cn/get_started.md
使用教程:https://github.com/CVHub520/X-AnyLabeling/blob/main/examples/optical_character_recognition/document_layout_analysis/README.md
总结
总的来说,DocLayout-YOLO 不仅在速度上与单模态方法YOLOv10相匹配,而且在准确性上超越了所有现有方法,包括单模态和多模态方法。这表明DocLayout-YOLO在文档布局分析领域具有重要的应用潜力。
最后,值得称赞的是,这项工作不仅在技术上取得了突破,作者们还提供了代码、数据和模型的开源链接,为文档布局分析领域的研究和实践提供了宝贵的资源,这使得研究者和开发者能够进一步探索和应用DocLayout-YOLO。
1.6 OpenMMLab和YOLOX实现PCB缺陷检测
PCB 无处不在。几乎所有的电子设备都隐藏了 PCB。在很多情况下,这些 PCB 在设计时或使用后都可能有缺陷。以下是互联网上列出的 PCB 中一些常见的已知缺陷列表,以及来自免费数据集的示例图像。
1. Opens
2. Excessive solder
3. Component shifting
4. Cold joints
5. Solder bridges
6. Webbing and splashes
7. Lifted pads
PCB数据集下载地址:
https://github.com/tangsanli5201/DeepPCB
我们将使用 OpenMMLab ⁸ 的 mmdetection⁶ 来检测 PCB 图像中的缺陷。OpenMMLab⁸ 是一个深度学习库,拥有计算机视觉领域大多数最先进实现的预训练模型。它几乎实现了所有众所周知的视觉问题,如分类、对象检测和分割、姿势估计、图像生成、对象跟踪等等。
YOLOX:2021 年超越 YOLO 系列
本文中我们将使用 YOLOX 进行微调,并使用 mmdetection进行微调。YOLOX 是 2021 年发布的最先进的型号,是 YOLO 系列的改进。作者进行了一些重大改进,如下所列。
- 引入 SimOTA 进行标签分配
- 删除的锚框
- 特别注意数据增强,如具有抖动的 MixUp
- 用于检测和分类的独立头
之前从 v3 到 v5 的 YOLO 系列都有一个预测头,其中包括边界框预测、分类分数预测以及对象性分数预测,如上图的上半部分所示。
这在 YOLOX系列中发生了变化,作者选择对所有预测使用具有单独头的解耦头。如上图 2 的下半部分所示,检测头和分类头位于不同的头中。这有助于缩短训练期间的收敛时间,并略微提高模型的准确性。
因此,模型的速度确实受到了影响,因为随着分离为两个头,参数的数量显着增加。正如我们在图 4 中看到的,YOLOX-L 比 YOLOv5-L 慢一点。它还具有专为参数低得多的边缘设备构建的 Nano 和 Tiny 版本。
与之前最先进的对象检测模型相比,它们在平均精度方面确实有所改进,但 FPS 略有下降。
使用 mmdetection 微调 YOLOX
我们有一个开源的 PCB 缺陷数据集,称为 DeepPCB。该数据集由 1500 个图像对组成,每个图像对都有一个无缺陷的模板图像和一个有缺陷的图像,该图像具有 6 种常见缺陷类型的边界框注释,即开放、鼠咬、短、刺、伪铜和针孔。如果需要,您可以从数据集链接获取有关它的更深入信息。
图像的尺寸为 640×640,在我们的例子中是完美的,因为 YOLOX 是在相同的维度上训练的。
OpenMMLab
OpenMMLab 可以很容易地微调最先进的模型,只需很少的代码更改。它有一个针对特定用例的综合 API。我们将使用 mmdetection 在 DeepPCB 数据集上微调 YOLOX。
我们需要将数据集修改为 COCO 格式或 Pascal VOC 格式以重新训练模型。这是 mmdetection⁶ 加载自定义数据集进行训练所必需的。我们将使用 COCO 格式进行培训。您无需费力地将数据集转换为 COCO 格式,因为它已经为您完成了。您可以从此处直接下载转换后的数据集。整个数据集与 DeepPCB⁵ 中的数据集相同,只是添加了训练和测试 JSON 文件,其中包含 COCO 格式的注释以进行训练。
我不会进行转换为 COCO 格式的过程,因为您可以找到许多文档,就像 mmdetection 文档中提到的那样。请随意浏览脚本,如果您需要任何帮助,请不要介意留下评论。此处共享了将此数据集转换为 COCO 格式的脚本。
import json
import os
TRAIN_PATH = 'PCBData/PCBData/trainval.txt'
TEST_PATH = 'PCBData/PCBData/test.txt'
def create_data(data_path, output_path):
images = []
anns = []
with open(data_path, 'r') as f:
data = f.read().splitlines()
dataset = []
counter = 0
for idx, example in enumerate(data):
image_path, annotations_path = example.split()
image_path = os.path.join('PCBData', 'PCBData', image_path.replace('.jpg', '_test.jpg'))
annotations_path = os.path.join('PCBData', 'PCBData', annotations_path)
with open(annotations_path, 'r') as f:
annotations = f.read().splitlines()
for ann in annotations:
x, y, x2, y2 = ann.split()[:-1]
anns.append({
'image_id': idx,
'iscrowd': 0,
'area': (int(x2)-int(x)) * (int(y2)-int(y)),
'category_id': int(ann.split()[-1])-1,
'bbox': [int(x), int(y), int(x2)-int(x), int(y2)-int(y)],
'id': counter
})
counter += 1
images.append({
'file_name': image_path,
'width': 640,
'height': 640,
'id': idx
})
dataset = {
'images': images,
'annotations': anns,
'categories': [
{'id': 0, 'name': 'open'},
{'id': 1, 'name': 'short'},
{'id': 2, 'name': 'mousebite'},
{'id': 3, 'name': 'spur'},
{'id': 4, 'name': 'copper'},
{'id': 5, 'name': 'pin-hole'},
]
}
with open(output_path, 'w') as f:
json.dump(dataset, f)
create_data(TRAIN_PATH, 'train.json')
create_data(TEST_PATH, 'test.json')
数据集配置
number of classesannotation pathsdataset pathnumber of epochsbase config path 和一些 dataloader 参数。
我们将复制预先编写的 YOLOX-s 配置,并针对我们的数据集对其进行修改。其余配置(如增强、优化器和其他超参数)将相同。
我们不会做太多改变,因为本博客的主要动机是熟悉手头的问题,尝试最先进的 YOLOX 架构,并试验 mmdetection 库。我们将此文件命名为 并将其用于训练。yolox_s_config.py
我们将为我们的预测头添加和更改 。需要更改,因为配置将从根目录而不是目录加载。class namesnumber of classesbase pathconfigs
_base_ = ['configs/_base_/schedules/schedule_1x.py', 'configs/_base_/default_runtime.py']
classes = ('open', 'short', 'mousebite', 'spur', 'copper', 'pin-hole')
bbox_head = dict(type='YOLOXHead', num_classes=6, in_channels=128, feat_channels=128)
classes和annotations路径。
train_dataset = dict(
type='MultiImageMixDataset',
dataset=dict(
type=dataset_type,
classes=classes,
ann_file='train.json',
img_prefix='',
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations', with_bbox=True)
],
filter_empty_gt=False,
),
pipeline=train_pipeline)
我们需要在这里对 validation 和 test set 执行相同的操作。我们在这里不打算使用单独的测试集,而是使用相同的测试集进行验证和测试。
data = dict(
samples_per_gpu=8,
workers_per_gpu=4,
persistent_workers=True,
train=train_dataset,
val=dict(
type=dataset_type,
classes=classes,
ann_file='test.json',
img_prefix='',
pipeline=test_pipeline),
test=dict(
type=dataset_type,
classes=classes,
ann_file='test.json',
img_prefix='',
pipeline=test_pipeline))
我们只会训练模型 20 个 epoch,每 5 个 epoch 获得验证结果。我们不需要再训练它,因为我们只需 20 个 epoch 就可以获得不错的结果。
max_epochs = 20
interval = 5
训 练
接下来我们需要做的是训练模型。mmdetection 库最好的一点是,与训练相关的一切都已经为您完成了。您需要做的就是从目录运行训练脚本,并将路径传递给我们在上面创建的数据集配置。
python3 tools/train.py yolox_s_config.py
推 理
让我们看看我们的模型在一些示例中的表现如何。您一定想知道,看看训练模型是多么容易,必须有一个命令来对图像运行推理吗?有!但是,不要让训练模型的简单过程宠坏了您。让我们编写一些代码进行推理,但令您高兴的是,它不到 10 行代码。
from mmdet.apis import init_detector, inference_detector, show_result_pyplot
config_file = 'yolox_s_config.py'
checkpoint_file = 'best_bbox_mAP_epoch_20.pth'
device = 'cuda:0'
# init a detector
model = init_detector(config_file, checkpoint_file, device=device)
# inference the demo image
image_path = 'demo.jpg'
op = inference_detector(model, image_path)
show_result_pyplot(model, image_path, op, score_thr=0.6)
预训练模型和代码下载:
https://drive.google.com/file/d/19dTqDVHyoRmC3LJdJUypy-zcXb1BhqN0/view
https://colab.research.google.com/drive/1cbXvQm29koR6MnX_f130NIapGhtw4wYR?usp=sharing
1.7 YOLO 和 Webcam 实现实时目标检测
对象检测已成为计算机视觉中越来越受欢迎的领域,YOLO(You Only Look Once)是使用最广泛的算法之一。在这篇博文中,我们将探讨如何使用 YOLO 和网络摄像头来开始使用实时对象检测系统。
与需要对图像进行多次传递的传统对象检测算法不同,YOLO 在一次传递中处理整个图像,使其更快、更高效。
YOLO 已用于各种应用,包括自动驾驶汽车、安全系统以及图像和视频分析。
首先,我们需要设置您的环境。我们需要一个安装了 OpenCV(一种流行的计算机视觉库)和 YOLO 的 Python 环境。
🌎 设置环境
main.py,用于导入初始要求(例如 Opencv),并准备好通过网络摄像头进行检测的基础。
ultralytics,opencv-python,以及我们将在本博客后面介绍的其他依赖项。
pip install opencv-python
📷 从网络摄像头开始捕获
我们将使用OpenCV从网络摄像头捕获帧。这可以使用 OpenCV 中的 VideoCapture 函数来完成。
import cv2
cap = cv2.VideoCapture(0)
cap.set(3, 640)
cap.set(4, 480)
while True:
ret, img= cap.read()
cv2.imshow('Webcam', img)
if cv2.waitKey(1) == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
在上面的代码中,我们创建了一个 VideoCapture 对象,并将其设置为从默认相机 (0) 捕获帧。它将网络摄像头的分辨率设置为 640x480。然后,我们遍历这些帧并将其显示在窗口中,直到用户按 'q' 退出。
使用ultralytics YOLO
我们安装的ultralytics库使使用 YOLO 变得非常简单和轻松。
pip install ultralytics
使用库加载 YOLO 模型,并指定 YOLO 权重文件在ultralyticsyolo-Weights/yolov8n.pt.
from ultralytics import YOLO
model = YOLO("yolo-Weights/yolov8n.pt")
我们实例化一个 classNames 变量,其中包含 YOLO 模型经过训练以检测的对象类列表。
classNames = ["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat",
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella",
"handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat",
"baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
"fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli",
"carrot", "hot dog", "pizza", "donut", "cake", "chair", "sofa", "pottedplant", "bed",
"diningtable", "toilet", "tvmonitor", "laptop", "mouse", "remote", "keyboard", "cell phone",
"microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors",
"teddy bear", "hair drier", "toothbrush"
]
while 循环启动,并使用cap.read()从网络摄像头读取每一帧。然后,它将帧传递给 YOLO 模型进行对象检测。对象检测的结果存储在 'results' 变量中。
import cv2
cap = cv2.VideoCapture(0)
cap.set(3, 640)
cap.set(4, 480)
while True:
ret, img= cap.read()
results = model(img, stream=True)
cv2.imshow('Webcam', frame)
if cv2.waitKey(1) == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
对于每个结果,代码会提取检测到的对象的边界框坐标,并使用cv2.rectangle()它还会在控制台上打印检测到的对象的置信度分数和类名称。
📃 完整代码 — 使用 YOLO 和网络摄像头进行对象检测
from ultralytics import YOLO
import cv2
import math
# start webcam
cap = cv2.VideoCapture(0)
cap.set(3, 640)
cap.set(4, 480)
# model
model = YOLO("yolo-Weights/yolov8n.pt")
# object classes
classNames = ["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat",
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella",
"handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat",
"baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
"fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli",
"carrot", "hot dog", "pizza", "donut", "cake", "chair", "sofa", "pottedplant", "bed",
"diningtable", "toilet", "tvmonitor", "laptop", "mouse", "remote", "keyboard", "cell phone",
"microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors",
"teddy bear", "hair drier", "toothbrush"
]
while True:
success, img = cap.read()
results = model(img, stream=True)
# coordinates
for r in results:
boxes = r.boxes
for box in boxes:
# bounding box
x1, y1, x2, y2 = box.xyxy[0]
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) # convert to int values
# put box in cam
cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 255), 3)
# confidence
confidence = math.ceil((box.conf[0]*100))/100
print("Confidence --->",confidence)
# class name
cls = int(box.cls[0])
print("Class name -->", classNames[cls])
# object details
org = [x1, y1]
font = cv2.FONT_HERSHEY_SIMPLEX
fontScale = 1
color = (255, 0, 0)
thickness = 2
cv2.putText(img, classNames[cls], org, font, fontScale, color, thickness)
cv2.imshow('Webcam', img)
if cv2.waitKey(1) == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
运行结果:
1.8 YOLO和DeepSort实现目标跟踪与计数
DeepSORT 通过添加基于深度学习的外观描述符来扩展 SORT(简单在线实时跟踪)算法,以提高跟踪鲁棒性。其工作原理如下:
- 通过检测进行跟踪:DeepSORT 需要从检测器(如 YOLO)检测到物体。
- ReID 特征:神经网络为每个检测到的物体生成外观特征,这有助于区分具有相似形状或重叠位置的物体。
- 卡尔曼滤波器和匈牙利算法:卡尔曼滤波器预测未来物体的位置,匈牙利算法将检测到的物体与现有轨迹进行匹配,即使在遮挡期间也是如此。
要使用 YOLO 和 DeepSORT 跟踪对象,通常需要设置一个管道,其中 YOLO 负责处理对象检测,而 DeepSORT 负责在视频帧之间保持一致的跟踪。以下是有关如何实现它的分步指南:
安装所需的库:
- YOLOv5(或其他 YOLO 变体,例如 YOLOv8/YOLO11)
- DeepSORT 包或符合您要求的实现
- 常见的 Python 包,例如 OpenCV、NumPy 和 PyTorch
详细步骤
步骤1:设置 YOLO 进行对象检测。
加载针对实时对象检测进行了优化的预训练 YOLO 模型(例如 YOLOv5 或 YOLOv8)。
import torch
model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) # Load YOLOv5 model
步骤2:初始化 DeepSORT。
DeepSORT 需要对其卡尔曼滤波器和重新识别模型进行配置。您可以使用预先训练的重新识别模型来生成特征嵌入,以实现跟踪一致性。
from deep_sort.deep_sort import DeepSort
deep_sort = DeepSort(
model_checkpoint='path_to_reid_model.pt',
max_dist=0.2, # adjust based on camera distance and tracking area
max_iou_distance=0.7,
max_age=30,
n_init=3
)
步骤3:捕获视频帧。
import cv2
cap = cv2.VideoCapture('path_to_video.mp4')
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
步骤4:物体检测。
# YOLO Detection
results = model(frame)
detections = results.pred[0].numpy() # Get YOLO predictions
bbox_xywh = []
confidences = []
class_ids = []
for *xyxy, conf, cls in detections:
x_c, y_c = (xyxy[0] + xyxy[2]) / 2, (xyxy[1] + xyxy[3]) / 2
w, h = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
bbox_xywh.append([x_c, y_c, w, h])
confidences.append(conf)
class_ids.append(int(cls))
第 5 步:使用 DeepSORT 进行过滤和跟踪。
将检测结果和边界框传递给 DeepSORT,它将跨帧跟踪每个对象并分配唯一的 ID。
# Format detections for DeepSORT
bbox_xywh = torch.Tensor(bbox_xywh)
confidences = torch.Tensor(confidences)
# Perform DeepSORT tracking
outputs = deep_sort.update(bbox_xywh, confidences, frame)
# Draw tracking boxes and IDs on the frame
for output in outputs:
x1, y1, x2, y2, track_id = output[:5]
cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 0, 0), 2)
cv2.putText(frame, f"ID: {track_id}", (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
第 6 步:显示结果。
cv2.imshow('YOLO + DeepSORT', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
1.9 使用NVIDIA DeepStream实现多视频流YOLO目标检测
使用多个视频流进行对象检测对于安全摄像头、交通管理、工业自动化、人员监控等任务非常重要。对象检测是一项计算成本高昂的任务,因此优化资源对于应用程序非常重要。NVIDIA DeepStream SDK 功能强大,可用于 AI 任务,并由 NVIDIA 工程师完美优化。
在本文中,我将向您展示如何使用 DeepStream SDK 通过自定义 YOLO 模型对多个流执行检测。根据您的 GPU,您可以同时对 30 多个视频执行检测。
使用 Deepstream SDK 有两种主要方式:
- NVIDIA Jetson 设备
- 带有 Nvidia GPU 的 Ubuntu 操作系统
我有一个 Ubuntu 22.04 操作系统,但不同设备和 ubuntu 版本的步骤是相同的。
设置 DeepStream 开发环境
1. 安装 DeepStream SDK :这是 Nvidia 的官方文档,您可以找到不同设备的指南。
https://docs.nvidia.com/metropolis/deepstream/dev-guide/text/DS_Installation.html
完成第一步后,您可以按照文档运行 demo 模型,但不能直接运行 YOLO 模型。您需要某种桥接来使用 YOLO 模型进行检测。
2. DeepStream-Yolo 存储库 :这个存储库是一个非常流行的存储库,非常易于使用。克隆后,请按照说明进行操作。
https://github.com/marcoslucianops/DeepStream-Yolo
使用 Deepstream 运行 YOLO 模型
我将使用预先训练的 yolov8s 模型,但您可以使用自定义的 YOLO 模型;一切都是一样的。DeepStream-Yolo 存储库支持不同版本的 YOLO。每个支持的模型都有配置文件,不用担心,我将向您展示如何准备这些配置文件。
1. 将 YOLO 模型转换为 onnx
我们不能直接使用 YOLO 模型,我们需要将它们转换为 ONNX 模型。幸运的是,DeepStream-Yolo 存储库中有一个 Python 脚本。进入文件夹并在其中复制您的 YOLO 模型。
如果您想使用预训练的 yolo 模型,可以从此链接下载。utils文件夹内有用于转换的 Python 文件。请看下面的图片。
我将使用export_yoloV8.py。有不同的参数,您可以检查这些参数来调整您的模型,
https://github.com/marcoslucianops/DeepStream-Yolo/blob/master/docs/YOLOv8.md
但它也会像这样工作:
/DeepStream-Yolo/utils$ python3 export_yoloV8.py -w yolov8s.pt — dynamic — simplify
现在将您的 onnx 文件复制到“DeepStream-Yolo”文件夹。
2. 编译 deepstream demo 应用程序
DeepStream 为各种任务提供了不同的演示应用程序。我将使用deepstream-app ,它支持多个流。最好使用演示应用程序一段时间,因为从头开始编写 DeepStream 应用程序是一项复杂的任务。
现在,让我们编译deepstream-app。首先,进入文件夹,然后导出 CUDA 版本,最后编译deepstream-app。请看下面的图片。
3. 调整配置文件
别担心,在此步骤之后,我们将运行我们的模型。我们需要两个不同的配置文件。
deepstream_app_config.txt :它控制整个管道,指定源(如视频或摄像机)、主要推理配置文件
config_infer_primary_yoloV8.txt :它定义了 YOLOv8 推理模型的配置
有数百个参数,我只讨论最重要的参数 (文档)。
https://docs.nvidia.com/metropolis/deepstream/dev-guide/text/DS_ref_app_deepstream.html#application-group
deepstream_app_config.txt内部:
[tiled-display] :配置多个输入流在单个平铺输出视图中的显示方式。设置行数和列数。
[source0] :对于每个源,递增末尾的数字并更改视频的路径。我有 4 个不同的视频,有 4 个 [源] 元素适合我。
[streammux] :将多个视频流合并到一个缓冲区中,将 batch-size 设置为输入流的数量。
[primary-gie] :将配置文件设置为第二个配置文件的名称,在本例中为 config_infer_primary_yoloV8.txt
config_infer_primary_yoloV8.txt内部:
batch-size :将此项设置为等于输入源的数量。
network-mode :性能会根据网络模式而变化。有 3 种不同的类型:0 = FP32 , 1 = int8 , 2=FP16
为了获得更好的 FPS,请使用 int8(步),为了获得更好的准确性,请使用 FP32。
我将使用 FP16。
[class-attrs-all] :模型参数
nms-iou-threshold:交并比 (IoU) 阈值
pre-cluster-threshold:根据置信度分数筛选检测
4. 检测
一切准备就绪,是时候进行对象检测了。第一次创建文件需要一些时间,可能需要 10 到 20 分钟。
导航到 DeepStream-Yolo 文件夹,然后使用以下命令运行 DeepStream 应用程序:
/opt/nvidia/deepstream/deepstream/7.0/sources/apps/sample_apps/deepstream-app/deepstream-app -c deepstream_app_config.txt
deepstream-app(Step 2) 和我们的配置文件 (Step 3)。查看 FPS 值;即使使用我的旧 GPU (GTX 1660 Ti),FPS 值也非常好。
2.0 Hyper-YOLO
超图计算+目标检测,性能新SOTA!清华发布:用超图捕捉高阶视觉关联
Hyper-YOLO是一种新型目标检测方法,通过超图计算增强了特征之间的高阶关联,提升了检测性能,尤其在识别复杂场景下的中小目标时表现更出色。
YOLO(You Only Look Once)系列是目标检测领域中的主流方法,以其高效性和实时性而著称。然而,现有的YOLO模型在处理跨层特征融合和复杂的高阶特征关系时存在局限,无法充分捕捉跨位置和跨尺度的复杂特征关联。
为了解决这一难点,清华大学提出了Hyper-YOLO:一种基于超图计算的目标检测方法。Hyper-YOLO首次将超图计算集成到目标检测网络,对特征图中的复杂高阶关联进行建模,实现了高阶信息的跨层次和跨位置传播。
作者列表:Yifan Feng, Jiangang Huang, Shaoyi Du, Shihui Ying, Junhai Yong, Yipeng Li, Guiguang Ding, Rongrong Ji, Yue Gao.
论文地址:https://arxiv.org/abs/2408.04804
Github仓库:https://github.com/iMoonLab/Hyper-YOLOv1.1
零代码平台:http://hyperyolo.gaoyue.org:28501/#/predict
使用超图计算结合YOLO,性能在各种规模模型中都达到顶尖,在COCO数据集上的表现明显优于其他模型,尤其是对中小目标提升更加显著。其中,相比于最新的YOLOv9-T、YOLO11-S,同规模的Hyper-YOLO分别实现了2%和1%的平均精度提升。
目标检测的困境
近年来,随着深度学习的快速发展,YOLO(You Only Look Once)系列模型凭借其高效的单阶段检测架构,在目标检测领域中脱颖而出。YOLO模型通过将目标检测简化为回归问题,在保持高精度的同时实现了实时检测能力,受到了广泛关注和应用。
然而,随着应用场景的复杂化,现有的YOLO模型在处理跨层次特征融合和高阶特征关系时暴露出了一定的局限性。
下图展示了几个典型的案例 (YOLOv8为例)。在打网球的场景中,现有模型将网球拍误判为了棒球棒;在冲浪的场景中,现有模型将冲浪板误判为了风筝。这种错误正是由于现有的模型难以捕捉视觉对象之间的复杂关系。
因而,不同目标间的高阶语义关联推理限制了目标检测的性能。
高阶关联的建模方法
为了解决这些问题,学术界一直在探索更加先进的模型设计与优化方法。其中,超图计算作为一种能够捕捉多方关系的数学工具,逐渐被应用于包括社交网络、脑网络等复杂数据结构分析中。
超图覆盖了多种关联,是复杂关联计算的有效方法。在Hyper-YOLO中,作者首次将超图计算引入目标检测领域,以实现视觉高阶语义关联的建模与学习。
模型效果
该工作在COCO数据集上进行了丰富的实验。Hyper-YOLOv1.1提供了3种规模的版本(Tiny, Small , Medium),在对比实验中,均明显优于最新的YOLO模型。
其中,Tiny版本的平均精度(mAP)指标相比于YOLOv8、YOLOv9、YOLO11的同规模版本分别提升3.0%、2.0%、0.8%;Small版本的mAP指标相比于YOLOv8、YOLOv9、YOLO11的同规模版本分别提升3.1%、1.2%、1.0%。此外,对于骨干网络、Kernel大小、特征增强策略、超图构建策略的消融实验证明了所提出的方法的先进性。
以下两图为YOLOv8、Hyper-YOLO在目标检测和实例分割任务下的可视化结果。
下图为使用高阶关联学习对特征图增强前后的可视化图(通过HyperC2Net前后的特征图)。
上述实验结果证明,Hyper-YOLO具有目前最先进的检测性能,尤其对场景中不同视觉对象的高阶关系具有更准确的理解能力。
Hyper-YOLO零代码平台
智能媒体与认知实验室还推出了一款基于Hyper-YOLO的零代码训练平台。在该平台上,无需配置环境、修改配置文件等繁琐操作,既可以一键上传图像利用训练好的Hyper-YOLO模型进行推理,也可以上传数据集自定义训练、直观展示训练过程。(推荐使用PC端chrome/Edge浏览器)
项目链接:http://hyperyolo.gaoyue.org:28501/#/predict
项目支持自定义训练。
在训练完成后,可以对验证数据进行推理和评估,并可视化检测结果:
方法概述
超图计算
超图是图的推广形式,是一种高效的特征表示学习方法。在超图中,一条超边可以连接多个顶点,从而表示对象之间的高阶关联。超图神经网络作为超图计算的核心方法,通常包含以下几个步骤:
- 从原始数据构建超边2. 从顶点到超边的消息聚合(超边卷积)
- 从超边到顶点的消息分发(节点卷积)
超图神经网络由于其灵活性和丰富的表达能力,广泛应用于社交网络分析、生物信息学、推荐系统等领域,能够更有效地建模和分析复杂的多层次数据关联。
Hyper-YOLO整体架构
Hyper-YOLO 继承了典型的 YOLO 架构,骨干网络通过引入混合聚合网络(MANet)来增强特征提取能力,从五个特征层中获取信息。
颈部网络(Neck)采用基于超图的跨层次和跨位置表示网络(HyperC2Net),通过超图计算集成多尺度特征,实现高阶信息的跨层次和跨位置传播,从而生成适用于目标检测的语义特征,显著提升模型的检测性能。
基于超图的跨层次和跨位置表示网络
在传统YOLO模型中,颈部为连接骨干网络和预测头之间的部分,通常采用类似PANet的结构,承担多尺度特征提取与融合的功能。虽然这类结构能够进行一定的多尺度特征融合,但直接的信息交互局限在相邻层之间。
而基于超图的跨层次跨位置表示网络(Hypergraph-Based Cross-Level and Cross-Position Representation Network, HyperC2Net)则突破了这一瓶颈,主要过程如下:
1. 超图构建: HyperC2Net将来自不同层次的特征图进行拼接,形成跨层次的视觉特征集合。然后通过计算特征点之间的距离,构建一个超图,其中每个超边连接多个顶点,代表多个特征点之间的高阶关系。超图能够表达跨层次和跨位置的复杂关系,而非简单的相邻层信息融合。
2. 超图卷积: 在构建超图后,HyperC2Net利用超图卷积在特征图上进行消息传播。通过这种方式,不同位置和层次的特征点可以相互传递信息,建模特征点之间的高阶关联,增强了模型对于复杂场景中目标的识别能力。特别是在跨位置特征交互方面,相比于传统的卷积操作,超图卷积能够捕捉到更广泛和复杂的特征关联。
总结
Hyper-YOLO通过引入超图计算方法,突破了传统YOLO模型在多尺度特征融合上的局限。超图的高阶关联建模能力使得HyperC2Net能够在跨层次和跨位置的信息传播中表现出色,不仅在特征点之间实现高效的信息聚合和分发,还通过跨层次的消息传递显著提升了目标检测性能,尤其在处理复杂场景和多目标检测任务中表现优异。
2.1基于YOLO和EasyOCR从视频中识别车牌
先决条件
在开始之前,请确保已安装以下 Python 包:
pip install opencv-python ultralytics easyocr Pillow numpy
实现步骤
步骤 1:初始化库
我们将首先导入必要的库。我们将使用 OpenCV 进行视频处理、使用 YOLO 进行对象检测以及使用 EasyOCR 读取检测到的车牌上的文字。
import cv2
from ultralytics import YOLO
import easyocr
from PIL import Image
import numpy as np
# Initialize EasyOCR reader
reader = easyocr.Reader(['en'], gpu=False)
# Load your YOLO model (replace with your model's path)
model = YOLO('best_float32.tflite', task='detect')
# Open the video file (replace with your video file path)
video_path = 'sample4.mp4'
cap = cv2.VideoCapture(video_path)
# Create a VideoWriter object (optional, if you want to save the output)
output_path = 'output_video.mp4'
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, 30.0, (640, 480)) # Adjust frame size if necessary
步骤2:处理视频帧
我们将读取视频文件中的每一帧,对其进行处理以检测车牌,然后应用 OCR 来识别车牌上的文字。为了提高性能,我们可以跳过每三帧的处理。
# Frame skipping factor (adjust as needed for performance)
frame_skip = 3 # Skip every 3rd frame
frame_count = 0
while cap.isOpened():
ret, frame = cap.read() # Read a frame from the video
if not ret:
break # Exit loop if there are no frames left
# Skip frames
if frame_count % frame_skip != 0:
frame_count += 1
continue # Skip processing this frame
# Resize the frame (optional, adjust size as needed)
frame = cv2.resize(frame, (640, 480)) # Resize to 640x480
# Make predictions on the current frame
results = model.predict(source=frame)
# Iterate over results and draw predictions
for result in results:
boxes = result.boxes # Get the boxes predicted by the model
for box in boxes:
class_id = int(box.cls) # Get the class ID
confidence = box.conf.item() # Get confidence score
coordinates = box.xyxy[0] # Get box coordinates as a tensor
# Extract and convert box coordinates to integers
x1, y1, x2, y2 = map(int, coordinates.tolist()) # Convert tensor to list and then to int
# Draw the box on the frame
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) # Draw rectangle
# Try to apply OCR on detected region
try:
# Ensure coordinates are within frame bounds
r0 = max(0, x1)
r1 = max(0, y1)
r2 = min(frame.shape[1], x2)
r3 = min(frame.shape[0], y2)
# Crop license plate region
plate_region = frame[r1:r3, r0:r2]
# Convert to format compatible with EasyOCR
plate_image = Image.fromarray(cv2.cvtColor(plate_region, cv2.COLOR_BGR2RGB))
plate_array = np.array(plate_image)
# Use EasyOCR to read text from plate
plate_number = reader.readtext(plate_array)
concat_number = ' '.join([number[1] for number in plate_number])
number_conf = np.mean([number[2] for number in plate_number])
# Draw the detected text on the frame
cv2.putText(
img=frame,
text=f"Plate: {concat_number} ({number_conf:.2f})",
org=(r0, r1 - 10),
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.7,
color=(0, 0, 255),
thickness=2
)
except Exception as e:
print(f"OCR Error: {e}")
pass
# Show the frame with detections
cv2.imshow('Detections', frame)
# Write the frame to the output video (optional)
out.write(frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break # Exit loop if 'q' is pressed
frame_count += 1 # Increment frame count
# Release resources
cap.release()
out.release() # Release the VideoWriter object if used
cv2.destroyAllWindows()
代码说明:
初始化 EasyOCR:初始化 EasyOCR 阅读器以进行英文文本识别。
加载 YOLO 模型:YOLO 模型从指定路径加载。请确保将此路径替换为您的模型路径。
读取视频帧:使用 OpenCV 打开视频文件,VideoWriter如果要保存输出,则初始化。
帧处理:读取并调整每一帧的大小。该模型预测车牌位置。
绘制预测:在帧上绘制检测到的边界框。包含车牌的区域被裁剪以进行 OCR 处理。
应用 OCR:EasyOCR 从裁剪的车牌图像中读取文本。检测到的文本和置信度分数显示在框架上。
输出视频:处理后的帧可以显示在窗口中,也可以选择保存到输出视频文件中。
二、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侧解析
【一】YOLO系列中Neck结构的由来以及作用
YOLO从v3版本开始设计Neck结构,其中的特征融合思想最初在FPN(feature pyramid networks)网络中提出,在YOLOv3中进行结构的微调,最终成为YOLO后续系列不可或缺的部分。
FPN的思路剑指小目标,原来很多目标检测算法都是只采用高层特征进行预测,高层的特征语义信息比较丰富,但是分辨率较低,目标位置比较粗略。假设在深层网络中,最后的高层特征图中一个像素可能对应着输出图像 的像素区域,那么小于 像素的小物体的特征大概率已经丢失。与此同时,低层的特征语义信息比较少,但是目标位置准确,这是对小目标检测有帮助的。FPN将高层特征与底层特征进行融合,从而同时利用低层特征的高分辨率和高层特征的丰富语义信息,并进行了多尺度特征的独立预测,对小物体的检测效果有明显的提升。
FPN结构
FPN论文地址:Feature Pyramid Networks for Object Detection[1]【Rocky的延伸思考】
业务侧:FPN具备业务模块沉淀价值,但还是要分场景来使用,主要在小目标场景可以尝试,但同时要兼顾上游数据侧与下游部署侧的适配。
竞赛侧:FPN的思想可谓是竞赛侧的一个利器,在分类,分割,检测等任务中都能大展拳脚,进行迁移应用。
研究侧:FPN具备作为baseline的价值,不管是进行拓展研究还是单纯学习思想,这个算法都是不错的选择。
【二】YOLOv1-v3 Neck侧解析
YOLOv1和YOLOv2都是不含Neck结构的,Rocky将在本系列的Backbone篇中对这两个模型进行详细介绍,本文中便不做过多赘述。
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模块。
SPP模块在YOLOv3_SPP.cfg中率先展现,但是在YOLOv4中则成为了一个标配。SPP模块包含3个最大池化层,其滑动核(sliding kernel)尺寸分别是 和,并通过Padding操作,使每个最大池化层的输出特征图不变,用于Concat融合操作。SPP模块代替了卷积层后的常规池化层,可以增加感受野,更能获取多尺度特征,训练速度也让人满意。Yolov4论文中使用 的图像在COCO目标检测任务进行实验,SPP模块能以0.5%的额外计算代价将AP50提升2.7%。
SPP模块
SPP模块论文:Spatial Pyramid Pooling in Deep Convolutional Networks for Visual RecognitionPAN模块对不同层次的特征进行疯狂融合,其在FPN模块的基础上增加了自底向上的特征金字塔结构,保留了更多的浅层位置特征,将整体特征提取能力进一步提升。在引入YOLOv4时,特征图最后的融合操作相比于原论文发生了变化,从add操作改为concat操作,增加了特征图的通道数:
PAN模块论文:Path Aggregation Network for Instance Segmentation[2]【Rocky的延伸思考】
业务侧:可以作为baseline模型的一个备选,实际效果还需通过实验来反馈。
竞赛侧:YOLOv4 Neck侧可以作为竞赛侧的提分策略。
研究侧:YOLOv4 Neck架构具备作为baseline的价值,不管是进行拓展研究还是单纯学习思想。
【四】YOLOv5 Neck侧解析
由于YOLOv5在YOLOv4发布之后没多久就开源了,且并没有论文的发表,创新性部分一直受到热议。
YOLOv5的Neck侧也使用了SPP模块和PAN模块,但是在PAN模块进行融合后,将YOLOv4中使用的常规CBL模块替换成借鉴CSPnet设计的CSP_v5结构,加强网络特征融合的能力。【Rocky的延伸思考】
业务侧:YOLOv5 Neck侧在工程中非常稳定,且其github库更新频率让人敬佩,可以作为业务baseline模型的首选。
竞赛侧:YOLOv5 Neck侧可以作为检测和分割竞赛入场的模块。
【五】YOLOx Neck侧解析
YOLOx的Neck侧依然使用了YOLOv3的结构,并且使用了SPP模块。
【六】YOLOv6 Neck侧解析
YOLOv6的Neck侧受到硬件感知神经网络设计思想的启发,基于RepVGG style设计了可重参数化、更高效的Rep-PAN。
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模块。
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.)
E-ELAN模块
【延伸思考】
业务侧:YOLOv7 Neck侧作为YOLO系列最新的一版,其值得我们在业务侧进行实验验证其效果。
研究侧:YOLOv7 Ncek是刚发表的,具备很强的研究侧价值。