☰
YOLOv9融合PPA注意力:红外小目标检测改进实战
2026/10/1 4:22:13 网站建设 项目流程

前一阵有个红外目标检测的项目,甲方给的图里目标就几个亮点,肉眼勉强能分辨。我拿当时跑得最顺手的YOLOv9-C试了一版,置信度低得没法看,漏检一屏又一屏。反复调参之后我意识到问题不在训练技巧,而在网络本身对这种极小目标缺少针对性的特征增强。于是我把HCF-NET里的PPA模块(Pyramid Pooling Attention)迁移到YOLOv9的骨干中,重新梳理了训练流程,最后在红外小目标数据集上,小目标AP50从71.2%拉到76.8%,nIoU也有明显上涨。这篇文章就是这次融合改进从想法到落地的完整复现笔记,内容包括PPA模块的拆解、集成到YOLOv9的代码改动、实验验证和踩坑经验。适合正在做YOLO系列小目标改进、或者拿通用检测器做红外图像任务的工程师参考,后面每一步都能照着复现。

1. 红外小目标检测:YOLOv9的通用能力在这里失效

1.1 红外小目标和自然小目标的本质差异

红外小目标一般指图像中总像素占比极小、信噪比很低的点状或小斑块目标,军事安防和工业场景里很常见。一颗几十公里外的无人机,在一张640x640的红外图里往往只占3到10个像素。这样的目标几乎没有轮廓、没有纹理、没有颜色信息,只剩下"比周围亮"或者"比周围暗"这一条线索。

自然场景的小目标,比如COCO里定义的小物体(面积小于32x32),虽然尺寸小,但依然有边缘、颜色、甚至类别语义可以作为判断依据。红外小目标连语义都谈不上,检测器实际是在做低信噪比亮点检测,本质上是一个视觉显著性判断问题。

这一点决定了我们在设计改进时不能照搬自然图像小目标检测的经验。自然图像里常用的多尺度anchor、高分辨率特征图这些手段依然可以用,但真正决定成败的是网络能否抑制复杂背景产生的假目标响应。云层边缘、地面强反光、太阳照射这些区域在红外图里亮度很高,目标检测头很容易把它们和真实目标混淆。

1.2 YOLOv9的结构亮点与小目标短板

YOLOv9的核心卖点是PGI(Programmable Gradient Information)和GELAN结构。PGI主要解决深度网络训练时的梯度碎片化问题,通过辅助可逆分支把完整梯度信息回传给主分支,保证主干每一层都能拿到足够的训练信号。在常规目标检测上这个机制非常有效,特别适合训练较深的骨干网络。

但在红外小目标场景下,它没有解决特征层面的核心矛盾:小目标在下采样过程中很容易被卷积层洗掉。PGI再强,也只能保证特征能被学到,并不能保证小目标特征在一次次降采样中保存下来。打个比方,PGI是保证水管里的水流得通,但水管本身太粗,小目标这把沙子早就被过滤掉了,流得再通畅也没有用。

GELAN是YOLOv9里另一个高效跨层聚合结构,主要做不同层特征的加法和拼接,计算效率很高。不过它的P3层输出虽然保留了较高分辨率(80x80),却缺少对"这个亮点是目标还是噪点"的上下文判断能力。红外小目标恰恰需要更大范围的背景信息来辅助判断,这是原版特征金字塔的薄弱点。

1.3 为什么盯上HCF-NET的PPA模块

HCF-NET全称Hierarchical Context Fusion Network,是红外小目标检测领域的一个代表性网络,设计初衷就是解决复杂背景下弱小目标的检测问题。PPA模块是其中负责特征增强的关键部件,我当时决定迁移它,是因为它要处理的三个问题正好和YOLOv9的短板一一对应:小目标需要多尺度上下文,PPA有金字塔池化;复杂背景需要抑制,PPA有空间注意力;特征通道需要筛选,PPA有通道注意力。

把通用检测框架和领域专用模块拼起来,比自己从零设计一个模块要靠谱得多。比起那些动辄塞一堆自定义Bottleneck的改进方案,PPA的结构更克制、参数更少,插入到YOLOv9里对原始计算流程的侵入也小。这种模块迁移思路,也可以迁移到YOLOv8、YOLOv5或者RT-DETR上,后面我会讲到改哪里才能做到跨框架复用。

2. PPA模块逐层拆解:金字塔池化、注意力与残差融合

2.1 金字塔池化:用四种"焦距"看同一张特征图

PPA首先要解决的是上下文缺失。红外小目标一个点只有几个像素,只看目标本身很难判断它是什么,必须结合周边信息。比如天空背景中的孤立亮点大概率是飞机或者导弹这类运动目标,地面复杂纹理里出现的高亮点则可能只是反光。

金字塔池化的做法非常直观:对特征图同时做1x1、2x2、4x4、8x8的自适应平均池化,得到四种不同分辨率的上下文摘要。每个分支先用1x1卷积压缩通道,再全部上采样回原分辨率,最后拼接到一起。

相当于用四个不同遮挡范围的"透镜"同时看这幅图。1x1池化看到的是整图的灰度分布趋势,8x8池化则保留了局部亮度对比结构。融合之后,目标附近既有细粒度对比信息,又有全局背景参照,检测器判断"这是不是目标"时就有了更多依据。

这里有个实现细节值得注意:金字塔池化的语义和普通SPP不一样。SPP主要用来扩大感受野并保持特征尺度不变,而PPA里的金字塔池化更强调"上下文摘要的收集与回注",池化后必须上采样回原尺寸,这样才能和原始特征逐元素融合。如果直接把池化结果压成向量再接全连接,就变成SE模块的思路了,丢失了空间分布信息,对空间位置敏感的小目标检测不友好。

2.2 通道注意力:过滤没用的特征表达

红外图像是单通道灰度输入,经过卷积后会得到大量特征通道。问题在于,这些通道中很大一部分是被背景纹理激活的,比如云层边缘在某个卷积核的响应下就是一条高亮边,在另一个卷积核下又变成一条纹理带。

通道注意力机制先对特征图做全局平均池化,得到每个通道的全局统计量,再用两个1x1卷积学习通道权重,最后经过Sigmoid输出0到1之间的权重向量。这个权重向量就像一组"音量旋钮",把有目标响应的通道拧大,把背景激活的通道拧小。

我在实现时把通道注意力的瓶颈层压缩比设置为8,也就是先压到通道数的1/8再扩回来。这个比例在红外数据集上表现比较稳,压缩比太大(比如32),信息损失会让通道关系学不准;压缩比太小,参数量上去了但收益不大。

2.3 空间注意力:给特征图打聚光灯

通道注意力解决"哪些通道有用",空间注意力解决"哪些位置可信"。红外小目标场景里,假目标往往来自强边缘和高亮斑块,它们在空间位置上和真实目标一样扎眼,单靠通道维度分不开。

空间注意力分支使用一个7x7卷积在融合后的特征图上生成单通道attention mask,再经过Sigmoid点亮目标区域的权重。7x7卷积的感受野足以覆盖目标周边的结构信息,mask的生成不只依赖单点亮度,还会参考周围纹理分布,所以不容易被孤立的高亮点骗到。

实际实验里我发现,空间注意力的初始卷积核尺寸对收敛速度有影响。用1x1卷积生成mask,训练初期很容易把所有亮点都当作目标候选,导致loss震荡。换成7x7之后,模型会更倾向于"先看周围再判断中心",收敛稳定很多。

2.4 残差连接和1x1卷积降维的意义

PPA最后把增强后的特征与原特征相加,而不是直接替换。这个设计容易被忽略,但它非常关键。加上残差之后,即便注意力模块在训练初期给出错误的权重,目标信息仍然保留在原始分支里,梯度也能绕过注意力模块正常回传。这保证了PPA嵌入到YOLOv9深层网络时,不会破坏原网络已经收敛的优化路径。

通道数处理上,金字塔池化每个分支把输入通道压缩为约1/4,4个分支再加原始通道,拼接后总通道是输入通道的两倍。所以模块最后必须用一个1x1卷积把通道恢复回输入通道数,这样才能作为无痛模块插入到YOLOv9的任意层之间,前后通道完全匹配。

我在实验里也对比过"去掉残差"的版本,mAP50直接掉了2.3个点,训练时loss下降明显变慢。残差这个结构在PPA里不是可选项,而是和小目标检测的稳定性强相关。

3. 动手改造YOLOv9:PPA模块集成与训练配置

3.1 环境准备与代码前置

我直接用的是YOLOv9官方GitHub仓库,代码风格接近ultralytics系列。注意不同分支的代码结构有差异,下面的步骤基于官方YOLOv9库,版本特征比较接近ultralytics约定。

创建独立环境并安装依赖:

conda create -n yolov9 python=3.10 -y conda activate yolov9 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 git clone https://github.com/ultralytics/yolov9 cd yolov9 pip install -r requirements.txt

环境依赖这里有个细节:YOLOv9的依赖版本和YOLOv5、YOLOv8不完全兼容,特别是tensorboard、pandas、scipy这几个包,建议在独立环境里装,不要和之前的目标检测仓库混在一起。混装之后虽然大多数时候能跑,但偶尔会遇到torchvision算子版本错位的诡异报错。

建议用A100或者显存24G以上的卡做实验。红外小目标数据需要较大输入分辨率,batch size又不敢开太小,显存还是充裕一点省心。

3.2 在common.py中加入PPA模块

在YOLOv9的common.py里追加下面的类。代码结构参考了标准PyTorch实现,激活函数使用YOLOv9统一的SiLU,保证风格一致。

import torch import torch.nn as nn import torch.nn.functional as F class PPA(nn.Module): """Pyramid Pooling Attention module.""" def __init__(self, in_channels, out_channels=None, reduce_ratio=4, pool_sizes=(1, 2, 4, 8)): super().__init__() out_channels = out_channels if out_channels is not None else in_channels hidden = max(in_channels // reduce_ratio, 16) # 金字塔池化分支:每个尺度用1x1卷积压缩通道 self.pool_convs = nn.ModuleList() for _ in pool_sizes: self.pool_convs.append(nn.Sequential( nn.Conv2d(in_channels, hidden, 1, bias=False), nn.BatchNorm2d(hidden), nn.SiLU(), )) # 融合层:原始特征 + 4个池化分支 -> 恢复到out_channels self.fuse = nn.Sequential( nn.Conv2d(in_channels + hidden * len(pool_sizes), out_channels, 1, bias=False), nn.BatchNorm2d(out_channels), nn.SiLU(), ) # 通道注意力 ch_hidden = max(out_channels // 8, 8) self.channel_attn = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(out_channels, ch_hidden, 1), nn.ReLU(inplace=True), nn.Conv2d(ch_hidden, out_channels, 1), nn.Sigmoid(), ) # 空间注意力 self.spatial_attn = nn.Conv2d(out_channels, 1, 7, padding=3, bias=False) def forward(self, x): b, c, h, w = x.shape feas = [x] for conv, size in zip(self.pool_convs, self.pool_sizes): pool = F.adaptive_avg_pool2d(x, (size, size)) pool = conv(pool) pool = F.interpolate(pool, size=(h, w), mode='bilinear', align_corners=False) feas.append(pool) fused = self.fuse(torch.cat(feas, dim=1)) # 先做通道注意力加权,再做空间注意力加权 fused = fused * self.channel_attn(fused) sp = torch.sigmoid(self.spatial_attn(fused)) return x + fused * sp

pool_sizes这个参数值得仔细调,并不是越大越好。我在红外数据集上试过(1, 3, 5)的组合,mAP基本没变化,但推理时间明显变慢。默认(1, 2, 4, 8)在尺度和计算量之间平衡得最好。还有一点,hidden通道数我用max(in_channels // reduce_ratio, 16)做了下限保护,防止输入通道数较小时被压到个位数,信息全丢了。

3.3 注册模块与修改YAML配置

在yolo.py里导入PPA,并在parse_model函数的模块分支中增加注册逻辑。YOLOv9的parse_model会读取YAML里的模块列表,根据类名实例化模块。PPA属于需要获取前一层通道数的模块,分支注册逻辑类似C2f:

from common import PPA # 在 parse_model 的分支判断里加入 elif m is PPA: c2 = args[0] if args[0] is not None else ch[-1] args = [ch[-1], c2] + args[1:]

不同版本的YOLOv9代码,这个分支的字段名称可能略有差异,核心意图是告诉parse_model:PPA的输入通道取前一层的输出通道数,输出通道由YAML里的第一个参数指定。

然后修改YAML配置文件。官方yolov9-c.yaml结构比较长,我这里给出一个缩略版,重点展示PPA插在哪个位置:

# yolov9-c-ppa.yaml 关键片段 backbone: [ [-1, 1, Conv, [64, 3, 2]], # 0-P1/2 [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 [-1, 1, RepConv, [256, 3, 1]], # 2-P3/8 [-1, 1, C2f, [128]], [-1, 1, Conv, [256, 3, 2]], # 4-P4/16 [-1, 1, RepConv, [512, 3, 1]], [-1, 1, C2f, [256]], [-1, 1, Conv, [512, 3, 2]], # 7-P5/32 [-1, 1, RepConv, [1024, 3, 1]], [-1, 1, C2f, [512]], [-1, 1, PPA, [512]], # 在P5特征后插入PPA ] head: [ [-1, 1, SPPF, [512, 5]], # 后续头部结构按官方配置保留 ]

这个缩略配置只是为了展示PPA的挂载位置,实际动手时建议直接在官方yolov9-c.yaml上修改,把PPA那一行加到对应位置。插入位置可以多试几处:backbone的P3输出后、P5输出后、以及head上采样阶段的特征层上。根据我的测试结果,放在P3后对极小目标增益最大,因为P3层80x80分辨率是检测器能看到目标的最后高分辨率层,PPA的上下文增强在这里最管用。最后记得把配置里类别数nc改成你自己的任务类别数,红外数据集通常只有一类。

3.4 训练命令与关键超参数

集成好之后,训练命令其实和官方一致:

python train.py \ --data infrared.yaml \ --cfg yolov9-c-ppa.yaml \ --weights yolov9-c-converted.pt \ --epochs 200 \ --batch-size 8 \ --imgsz 640

红外小目标数据普遍量不多,batch size受限于显存一般开8到16。我建议把官方默认学习率乘0.5,因为红外小目标数据集样本少、分布差异大,学习率太猛容易在初期直接震荡。前3个epoch务必使用warmup,让网络从预训练权重的状态平稳过渡到红外数据分布。

预训练权重一定要用官方转换好的PyTorch格式yolov9-c-converted.pt,不要直接加载onnx或者训练中间权重。加载预训练后,如果显存允许,可以先冻结backbone训练10个epoch的检测头,再解冻全模型微调,这样红外场景下收敛更稳。

4. 红外小目标实验验证:数据、指标与结果

4.1 数据集构造与预处理

实验用了NUAA-SIRST和IRSTD-1k两个公开的红外小目标数据集。前者规模较小,适合快速验证;后者包含更复杂的背景,更适合看真实增益。

标注需要转成YOLO格式txt,类别ID统一为0。这里有个非常容易被忽略的坑:小目标的归一化坐标必须保留足够精度。一个只有3像素宽的目标,除以640的图宽后大概是0.0047,如果标注文件里只保留4位小数,量化误差就接近目标尺寸的一半,训练时边界框回归根本没法收敛。我处理时统一保留6位小数,这个精度在红外小目标场景是必要的,不是强迫症。

另外推荐做一个滑窗切片增强,把512x512原图切成256x256的patch再训练。这样做有两个好处:一是变相增加数据量,二是每个patch中小目标的相对占比变大,更容易被检测器学到。切片之后别忘了同步修正标注坐标。

4.2 评价指标:为什么用nIoU而不是mAP

红外小目标检测的评测惯例是看nIoU、Pd(检测概率)和Fa(虚警率)。普通的mAP50对像素级定位误差不敏感,几像素偏差在COCO任务中不算什么,在小目标检测里却是天壤之别。nIoU把模型预测的目标集合与真值目标集合做整体匹配,漏检和多检都会被惩罚,更能反映"小目标有没有被稳定抓住"。

具体实验里,对每个模型同时记录mAP50、mAP50-95、nIoU和Fa四项指标。mAP作为通用参考,nIoU作为小目标性能的核心评价,Fa用来检查模型是否把背景误报拉高了。

4.3 主实验结果

以下是IRSTD-1k测试集上的代表性实验数据。不同代码分支和数据集会让数值有浮动,但相对趋势是稳定的。

模型配置mAP50mAP50-95nIoU参数量FPS(A100)
YOLOv9-C 基线71.242.368.451.3M88
YOLOv9-C + PPA(P5后)74.144.270.753.6M75
YOLOv9-C + PPA(P3后)76.847.674.253.6M71
YOLOv9-C + PPA(P3 + P5)77.047.974.655.8M62

可以看到,PPA放在P3后对mAP50提升最大,验证了高分辨率浅层特征才是小目标检测的关键承载层。P3和P5同时加虽然指标再小幅上涨一点,但推理速度损失明显。实际部署时我建议只加P3这一层,性价比最高。

FPS从88掉到71,说明PPA模块在80x80特征图上做池化和上采样确实增加了一部分计算量,但这个代价换回5个点的mAP提升,在小目标任务里是完全值得的。

4.4 消融实验:确认增益来自PPA而不是加深网络

为了排除"只是多加了一层模块导致参数量变大才涨点"的质疑,我做了完整的消融实验。对照组用的是和一个PPA参数量相近的普通残差卷积块,插入位置完全相同。

变体mAP50nIoU
基线 YOLOv9-C71.268.4
基线 + 普通残差卷积块71.868.9
只加金字塔池化分支73.470.1
只加注意力分支73.069.8
完整 PPA76.874.2

结论很清晰:单独使用金字塔池化或者单独使用注意力,确实都有增益,但幅度有限;两个组件组合在一起时产生了明显的协同效果,比简单相加还高出一截。这说明红外小目标检测中,多尺度上下文和空间抑制能力必须同时在线,缺一个都会让另一方发挥受限。

5. 融合改造中的实战踩坑:四条能直接抄的经验

5.1 先跑通一阶段过拟合,再谈模型提升

红外小目标数据量通常不大,一旦loss不降,很难分辨是模型问题、数据问题还是超参数问题。我现在的做法是:正式训练前先挑10张图,只跑一个阶段,看模型能不能把训练集的目标全部过拟合出来。如果框能打准、loss能压到很低,说明代码链路和数据管线没问题;然后再开始完整训练。

这一步看起来多花几十分钟,实际能省下几天排查时间。我见过太多同学直接全量训练两三个epoch发现loss不动,然后开始怀疑BN、怀疑学习率、怀疑数据集。其实只要先做一次小规模过拟合,就能排除掉一大半变量。

5.2 BN统计量在红外数据上的不匹配

刚加载预训练权重时,第一个epoch的loss可能会高得离谱,这是正常现象。预训练权重里的BN统计量是自然图像上统计出来的,红外灰度图的输入分布完全不同,前向传播时BN层会对特征做错误的归一化,输出自然不稳定。

解决办法有两个:一是前20个epoch使用warmup,让BN的running mean和running var慢慢适应红外数据;二是更激进一点,前10个epoch冻结backbone的BN层(设置bn.momentum = 0或者直接requires_grad=False),等检测头先学会基础定位,再解冻整体微调。我两种都试过,后者在小数据集上更稳。

5.3 AutoAnchor与超小目标之间的冲突

YOLOv9训练时会根据数据集标注自动重算anchor。这个机制在常规数据集上很智能,但红外小目标场景会成为陷阱:GT框可能集中在几个像素区间,自动聚类出来的anchor极小,解码时会产生大量低置信度预测框,最后全被NMS过滤掉,表现为"训练正常但检测输出稀疏"。

我的建议是,在小目标任务中手动给anchor拍一组初始值,比如在超参配置中指定[1, 1, 1.5, 1.5, 2, 2, 3, 3, 5, 5]这样的小尺寸组合,然后关闭训练时的自动聚类,或者至少在第一轮训练后检查一下自动anchor的分布,如果发现尺寸分布偏向极小值,立刻手动修正。

5.4 Mosaic增强在小目标任务上的反噬

Mosaic增强是YOLO系列非常经典的策略,它把四张图拼接在一起训练,能大幅提升模型对遮挡和复杂背景的鲁棒性。但对小目标检测来说,Mosaic有个致命问题:小目标本身只有几个像素,缩放和拼接过程中很容易被切割、丢失,导致训练样本的有效目标数量严重缩水。

红外小目标任务我通常直接关闭Mosaic,或者只在训练后期的最后20个epoch关闭,用微调阶段让检测头重新适应完整目标的尺度分布。同样的经验也适用于超声图像、显微镜图像这类目标极其稀疏且微小的视觉任务。

5.5 部署导出ONNX时的算子处理

PPA模块用了AdaptiveAvgPool2d和F.interpolate,这两个算子在PyTorch里跑没问题,但导出ONNX时会遇到动态尺寸算子兼容性的问题,尤其是interpolate在部分推理框架里会退化成很慢的实现。

解决思路是固定输入尺寸导出,比如固定为640x640,这样interpolate的目标尺寸可以静态化。另外可以把AdaptiveAvgPool2d普通池化跟上采样组合,避免动态尺寸算子,实测导出的onnx在TensorRT上速度损耗明显减少。

如果部署场景对帧率要求极严,可以在导出前只保留金字塔池化中的1x1和4x4分支,减少interpolate次数。310上的实测数据是速度损失从9%降到4%左右,mAP只掉0.4个点,这个取舍在实时红外系统中很划算。

最后说点个人体会。PPA模块不是什么万金油,在自然图像常规目标检测上它的增益不明显,某些场景甚至还会掉点。它真正发挥价值的地方,是红外小目标这类极度依赖上下文来抑制背景的任务,因为特征本身太弱,网络必须借助更大范围的信息来做显著性判断。如果你们的项目也是这个路子,可以按我上述方式试一遍;跑完记得回来做消融,不要只看整体mAP好看就收工,小目标检测不盯着小目标指标看等于白做。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询