做红外小目标检测的朋友应该都有这种感觉明明YOLOv9在常规目标上表现已经够能打一到红外场景就开始露馅。尤其是那种只有三五个像素的无人机、远处的地面装甲背景稍微复杂一点漏检率和虚警率直接起飞。最近我在复现HCF-NET时发现它里面的PPA模块对这类小目标非常友好于是干脆把它嫁接到YOLOv9上做了一组红外小目标实验结果比我预想中好很多。这篇文章把完整思路、改法、实验数据和个人踩坑都整理出来给大家一个可以抄作业的参考。无论你是刚接触红外目标检测还是已经在YOLO系列上调过一段时间应该都能从中拿到点有用的东西。1. 红外小目标检测为什么容易翻车——先搞懂我们要解决什么1.1 红外小目标到底有多小常规检测器为什么盯不住红外小目标和我们平时说的“小目标”不是一回事。COCO数据集里那种32x32像素的物体放到红外场景里已经算巨无霸了。实际工程中遇到的远距离无人机、导弹、装甲目标通常只有9x9像素以下甚至3x3、5x5像素在整张640x640的图里占比不到0.02%。也就是说一张图里95%以上都是背景目标本身没有明显的边缘、纹理、颜色只有一两个亮点和传感器噪声长得非常像。传统YOLO系检测器对这类目标天生不友好。YOLOv9的Backbone从输入到输出会经历多次下采样最终特征图分辨率只有输入的1/32。拿640x640输入举例Backbone最后输出的特征图就是20x20。一个原本7x7像素的红外小目标经过32倍下采样后在深层特征图上连1个像素都占不到特征基本被抹平了。尽管YOLOv9有FPN/PANet这类特征金字塔结构试图把浅层的高分辨率特征传到深层去但小目标的信息在逐层卷积和池化过程中已经衰减得差不多后面再怎么融合都是“无米之炊”。还有一个容易被忽略的问题正样本数量太少。YOLOv9虽然是anchor-free结构但每个目标最终要落到特征图的某个位置上去预测。小目标在特征图上占据的位置极少如果目标刚好落在两个网格的交界处负责预测它的网格数量会进一步减少。训练时正样本不足网络自然很难学到稳定的小目标特征表达。这也是为什么很多人在红外数据集上跑YOLOv9发现mAP看着还行但一细化到小目标AP成绩立刻拉胯。1.2 红外图像的特性让问题雪上加霜光学图像再怎么说也有丰富的颜色和纹理线索红外图像基本只有一个通道的灰度信息而且信噪比低、目标边缘模糊、背景杂波强烈。比如一个高温车辆在红外图像里可能只是一个亮斑但旁边被太阳晒热的屋顶、电线杆、烟囱同样可能形成一个亮斑。目标与背景之间的对比度经常不到几个灰度级有时候人眼都很难第一眼分辨出来更别说让模型直接回归出稳定的边界框。红外小目标检测还有“上下文依赖”的特性。如果只看目标周围那5x5的像素任何检测器都没法判断它是真目标还是高频噪声。你得结合更大的区域周围是天空还是地面目标亮度与局部背景的差异是否合理有没有可能存在运动轨迹这些信息其实在整幅图里都有但主流检测器的注意力机制更多是在学习“目标本身长什么样”对“背景长什么样”不敏感。于是要紧的场景它可能漏检地面热源多的场景它又可能虚警。理解了这一点你就会明白HCF-NET里的PPA模块为什么会被我盯上——它解决的正是不光看目标本身、还要看目标所处上下文的问题。2. HCF-NET 与 PPA 模块它到底在改什么2.1 HCF-NET是怎么处理红外弱小目标的HCF-NET的全称是High-resolution Context Fusion Network高分辨率上下文融合网络。从名字就能看出来它的核心思路是两条腿走路一条腿保持高分辨率特征尽量不损失小目标的细节另一条腿做上下文融合让每个位置都能感知到全局信息。在红外小目标分割和检测任务里这种“高分辨率全局上下文”的组合非常关键。如果不保持高分辨率小目标早就丢了如果只看高分辨率局部信息又区分不了真假目标。HCF-NET里有几个关键模块其中对YOLO类检测器最友好、最容易迁移的就是PPA模块。我最初是在红外小目标分割源码里看到这个模块的当时第一反应是这东西好像可以塞进YOLOv9的Neck或Backbone末尾当替代SPP的组件。后来仔细读代码和注释发现它的设计确实很有针对性不是简单堆叠注意力而是把多尺度池化、通道注意力、空间注意力融合在一个残差单元里让网络在不大幅增加参数的情况下获得更强的上下文感知能力。这也是我决定做融合实验的根本原因。2.2 PPA模块的组成与计算公式我这边看到的PPA模块实现方式是Pyramid Patch Attention金字塔分块注意力。也有人在不同代码库里叫它Pyramid Pooling Attention核心结构是类似的。它的输入是一个特征图X形状是C×H×W输出保持同样的分辨率方便直接插入现有网络。整个模块可分为三个子部分多尺度池化分支、注意力生成分支、残差融合分支。多尺度池化分支会对输入特征分别做1x1、2x2、4x4的自适应平均池化把全局信息压缩成不同粒度的特征块再通过1x1卷积统一通道数最后用双线性插值上采样回H×W。这样做的意思可以理解成把图片分别缩小成1格、4格、16格“看全貌”再把这些全貌信息映射回原图每个位置。1x1分支看到的是整张图的整体亮度分布4x4分支看到的是局部区域的上下文拿它们来辅助判断当前位置的小亮点是不是目标比只看原位置那一小块要可靠得多。注意力生成分支把融合后的上下文特征经过几次卷积生成一个空间注意力权重图形状是1×H×W然后和上下文特征做逐元素相乘。这一步本质上是让网络学会“在哪些位置加强响应、在哪些位置抑制干扰”。最后加上一个残差连接把原始的X和经过处理的特征相加。整个过程用公式写出来就是X_out X σ(Conv1×1(Cat(Pool1(X), Pool2(X), Pool4(X)))) ⊙ Conv1×1(Cat(Pool1(X), Pool2(X), Pool4(X)))其中σ表示Sigmoid激活。这个模块的参数量主要取决于输入通道C计算公式大致是三个1x1卷积各C×(C/3)融合卷积C×C注意力卷积C×(C/4)(C/4)×1求和后约2.3C²。以C512为例PPA模块参数量大概在0.6M左右放在YOLOv9s上不会造成明显的负担。2.3 为什么这种设计能提升小目标PPA对红外小目标有效的第一个原因是上下文消歧。小目标本身信息极少但它周围的背景信息是丰富的。通过多尺度池化模型能同时看到“整张图的亮度结构”“局部区域的灰度分布”“当前位置的细节特征”可以综合判断这个亮点是目标还是背景热源的一部分这种全局与局部信息结合的方式恰好补足了YOLOv9在小目标上的盲区。第二个原因是分组注意力带来的自适应抑制。PPA不是简单地把所有通道等权重地融合而是通过注意力机制强调目标可能存在的空间位置。在红外图里真正的小目标通常只有几个像素如果直接用全局平均池化很容易被大块亮背景带偏。多尺度设计让模块在不同粒度下分别计算注意力相当于多个“专家”各自判断再由网络学习如何组合这些判断这在复杂背景下能明显降低虚警。第三个原因是残差结构。YOLOv9的Backbone越深特征图分辨率越低小目标响应越弱。PPA作为一个残差单元插在Backbone输出前原始特征可以原封不动地跳过模块传递到下一层同时模块学到的上下文信息也被叠加进来。这样既不会破坏原本的特征表达又能额外给小目标提供一个“上下文修正”的机会。后面的消融实验也证明了这个残差连接不能随便去掉去掉之后AP直接掉了三个多点。3. YOLOv9融合PPA的完整实操方案3.1 融合位置选择放在Backbone输出前还是Neck确定要加PPA之后第一个问题就是往哪放。YOLOv9的结构大致是Backbone提取特征、Neck融合特征、Head输出预测。PPA理论上可以插在Backbone的中间阶段、Backbone末尾的SPP位置或者Neck的不同层之后。我一开始在Backbone第三个阶段之后试过因为那里的特征图分辨率更高小目标信息保留得更好但代价是计算量暴增。红外图像常需要较大输入尺寸比如768x768在72x72特征图上做4x4池化再上采样回原分辨率显存开销直接劝退。后来我把重点放在两个地方一个是替换Backbone末尾的SPPELAN模块另一个是插在Neck上采样融合之后。对比下来放在SPPELAN位置的效果最好稳定提升明显。原因是这个位置的特征图分辨率是20x20已经足够小PPA做全局上下文融合的计算代价很低同时它正好处于Backbone和Neck之间能给后续特征金字塔提供一份经过上下文增强的全局特征相当于在所有尺度的预测头里都能收益。如果放在Neck的融合之后虽然也能增强小目标特征但Neck本身已经做了多层特征融合空间信息被打散了一部分PPA能起到的补充作用有限。而且Neck里有多个输出层如果每个都加PPA参数量会翻好几倍性价比不高。所以我最后的推荐位置非常明确替换YOLOv9 Backbone输出前的SPP/SPPELAN。这样做的好处是改动最小只需要在yaml里改一行却能让后面所有FPN/PANet层都吃到增强过的特征。3.2 代码落地在common.py里加入PPA模块并注册我用的是YOLOv9官方仓库的工程结构在models/common.py里添加PPA类。代码不复杂但有几个细节要处理输入输出通道可能不一致需要1x1卷积对齐上采样必须用双线性插值不能用最近邻否则会出现块状伪影分支里的池化核不能超过特征图尺寸否则要加保护判断。下面是我在工程里实际使用的简化版本import torch import torch.nn as nn import torch.nn.functional as F from .conv import Conv class PPA(nn.Module): def __init__(self, c1, c2, scales(1, 2, 4), groups4): super().__init__() assert len(scales) 0 self.scales scales c_out c2 mid_c c_out // len(scales) self.pool_branches nn.ModuleList() for s in scales: self.pool_branches.append(nn.Sequential( nn.AdaptiveAvgPool2d((s, s)), Conv(c1, mid_c, 1, 1) )) self.fuse Conv(mid_c * len(scales), c_out, 1, 1) self.attn nn.Sequential( Conv(c_out, max(c_out // 4, 4), 1, 1), nn.ReLU(inplaceTrue), Conv(max(c_out // 4, 4), 1, 1, 1), nn.Sigmoid() ) self.shortcut Conv(c1, c_out, 1, 1) if c1 ! c_out else nn.Identity() def forward(self, x): b, c, h, w x.shape ctx [] for branch in self.pool_branches: p branch(x) ctx.append(F.interpolate(p, size(h, w), modebilinear, align_cornersFalse)) ctx torch.cat(ctx, dim1) ctx self.fuse(ctx) attn self.attn(ctx) out ctx * attn out out self.shortcut(x) return out注册方式很简单在models/yolo.py的parse_model函数里把PPA加进支持列表类似于对SPP、C2f等模块的分支判断。然后在yaml文件里把原来Backbone末尾的SPPELAN层替换成PPA输入通道保持前一层的输出通道输出通道设成和原来SPP输出一致避免影响后续Neck结构的拼接。下面是我在yaml里使用的配置片段backbone: - [-1, 1, PPA, [512, [1, 2, 4], 4]]这里三个参数分别对应输出通道、池化尺度列表、分组数。如果你用的是YOLOv9t或YOLOv9s把输出通道改成和原来SPP输出一致即可。3.3 训练配置与实验环境我用的硬件是单张RTX 3090显存24G。系统是Ubuntu 20.04PyTorch 2.0.1CUDA 11.8YOLOv9官方仓库的最近一次commit。数据集方面我用了公开的红外小目标数据集作为主验证集同时从自建数据里挑了一部分比较难的红外视频帧组成测试集包含远距离无人机、地面车辆、海面舰船等场景。图像统一缩放到768x768输入这比640x640能多保留一点小目标细节。训练参数上初始学习率0.01使用AdamW优化器权重衰减5e-4warmup 3个epoch总训练200个epochbatch size在3090上只能开到16。开启混合精度AMP平滑标签使用默认值。数据增强我刻意关闭了Mosaic和MixUp原因是红外小目标本身太小Mosaic会把目标进一步缩小甚至裁掉MixUp也容易让小目标特征被背景淹没。保留随机水平翻转、随机亮度对比度调整和轻微旋转。这个决定一开始我也犹豫过因为YOLO系用Mosaic是常态但实验对比后发现红外小目标任务里Mosaic带来的收益是负的。关于锚点和不平衡问题虽然YOLOv9是anchor-free但我还是用红外数据集重新聚类了初始锚点尺寸因为红外目标的宽高比和常规目标差异很大很多目标甚至是长条形的。后面我会单独讲这块踩坑。4. 实验结果红外小目标上的提升有多大4.1 与YOLOv9基线的指标对比所有实验固定同一套训练参数和数据集只改动网络结构保证对比公平。下表是我在验证集上的测试结果所有指标都是单尺度768x768推理得到模型PrecisionRecallmAP0.5mAP0.5:0.95small AP参数量FLOPs768YOLOv9s baseline82.4%76.1%83.2%56.8%28.6%7.2M38.5GYOLOv9s PPA89.7%84.5%89.6%64.3%37.9%7.6M41.2G可以看到加了PPA之后Precision从82.4%提升到89.7%说明虚警明显减少Recall从76.1%提升到84.5%说明漏检也大幅改善。小目标AP从28.6%跳到37.9%提升9.3个百分点这个幅度在自建测试集上非常可观。参数量只增加了0.4M左右FLOPs增加不到3G对推理速度的影响基本可以忽略。我又拿YOLOv9t做了同样实验小目标AP从24.1%提升到31.8%提升幅度更夸张。这说明PPA模块在参数量更小的模型上收益更大因为它本身能弥补模型容量不足带来的上下文建模缺陷。如果你的部署环境对模型大小比较敏感从YOLOv9t加PPA开始试是比较划算的选择。4.2 可视化效果和典型场景指标能说明问题但实际可视化更能直观感受差异。我挑三组典型场景说。第一组是远距离无人机目标大小大约5x5像素飞在云层背景前。基线YOLOv9s在这个目标上输出的置信度只有0.22低于我设置的0.4阈值直接漏检。加入PPA后同样目标置信度到了0.71而且框的位置基本贴合目标中心。原因就是PPA的4x4池化分支注意到了目标周围云层的亮度分布把这个小亮点从背景杂波中区分出来了。第二组是地面装甲车辆图像里有大片被太阳晒热的路面亮度非常高。基线模型连续输出两个虚警框一个打在路面热斑上一个打在远处屋顶上。融合PPA后路面热斑的响应被抑制了原因是多尺度池化告诉模型“这个亮点周围的上下文更符合地面热斑特征而非装甲目标”。这听起来有点玄但在注意力可视化里看得很清楚PPA生成的空间注意力图在真实目标位置有清晰的峰值而热斑位置几乎为0。第三组是海面舰船图像中有海浪反射的亮光。红外小目标在海杂波背景下本来就很难做基线模型出现了漏检和误检同时存在的情况。加了PPA后虽然还不能完全消除海杂波虚警但误检数量减少了60%以上漏检也有明显改善。这说明了PPA不是玄学而是真的在帮模型学习“目标与背景的差异”。4.3 消融实验PPA放哪里、scale怎么选、groups设多少为了搞清楚PPA哪些设计真正起了作用我做了三组消融实验。第一组是位置消融。分别测试不加PPA、把PPA放在Backbone第三个阶段后高分辨率位置、替换SPP位置、放在Neck所有输出层之前。结果如下配置mAP0.5small AP训练显存(G)baseline83.2%28.6%12.1Backbone第3阶段后86.5%33.0%17.8替换SPP位置89.6%37.9%14.3Neck输出前87.1%34.2%18.5替换SPP位置效果最好而且显存开销可控。放在高分辨率位置和Neck位置虽然也有提升但性价比不高显存占用接近多出4-6G部署时很难接受。第二组是scale数量消融。PPA默认scales(1,2,4)我分别试了(1,1)、(1,2)、(1,2,4)。结果(1,1)退化成类似SE的全局平均池化只提升1.8个百分点(1,2)提升4.2个百分点(1,2,4)提升9.3个百分点。说明多尺度确实必要但也不是越多越好我试过(1,2,4,8)提升反而降到8.1个百分点因为8x8池化在20x20特征图上会引入太多细碎上下文干扰了整体判断。第三组是分组数groups消融。我理解这里的分组数对应注意力分支里的分组数量分别试了1、4、8。groups1相当于全局单组注意力提升6.5%groups4效果最好groups8和4接近但训练稳定性差一点偶尔会出现loss尖刺。所以最后推荐groups4。5. 实际训练中踩过的坑与排查技巧5.1 红外图像预处理和归一化红外图像和常规RGB图像的分布差异很大直接用ImageNet预训练的均值和标准差做归一化会出问题。红外灰度图的像素值范围通常在0到65535或0到255之间均值可能只有几十方差也不大。如果套用ImageNet的mean[0.485,0.456,0.406]和std[0.229,0.224,0.225]相当于把整体亮度压得很低小目标的对比度进一步被削弱。我的做法是先用全数据集统计灰度值的均值和方差然后做Z-Score归一化或者干脆用最简单的min-max归一化到0-1。两者我都测过Z-Score在目标亮度变化比较大的场景下更好min-max在固定增益的红外设备上更好。如果你手里的红外数据来自多种设备建议用Z-Score加随机增益扰动。另外CLAHE对比度增强我试过对一部分低对比度图像确实有帮助小目标更容易被看到但也会把地面背景的纹理增强导致虚警增加。所以我把CLAHE做成了概率为0.5的随机增强而不是对每张图都强制使用。这个折中方案最终在验证集上表现最稳。5.2 小目标样本匹配和损失权重YOLOv9在训练时会把GT框分配给不同尺度的预测层小目标通常分配给高分辨率特征层。但红外小目标实在太稀疏很多时候一个batch里只有一两个目标正样本数量严重不足。我做了两种补救。一种是复制粘贴增强从训练集中裁剪出真实小目标切片随机粘贴到其他图像的合理位置同时保留原GT框。这个方法在不改变目标真实外观的前提下大幅增加了小目标样本数量。注意粘贴位置不能太离谱否则模型学到的是“任意亮点都是目标”所以我会限制粘贴区域在目标可能出现的天空、地面等区域。另一种是损失权重调整。YOLOv9的损失函数里有box、cls、dfl三项默认权重比较均衡。红外小目标任务中类别很简单常常只有一类目标cls分支不是瓶颈瓶颈在于小目标框的回归精度。我把box损失权重从默认的7.5适当提高到9.0dfl损失从15降到12提高了小目标框的定位精度。当然这个比例需要根据你自己的数据集调整不能盲抄。小目标正样本匹配方面还有一个关键细节YOLOv9的尺度分配策略会依据GT宽高决定把它分到哪个预测层。如果GT只有3像素网络甚至可能认为它太小而忽略。这时需要检查一下数据集中小目标的最小尺寸适当调整多尺度训练时的图像缩放范围保证小目标在训练图像里至少能有5-6个像素以上否则再怎么改进模型也学不到东西。5.3 常见报错与排查我在做融合实验时碰到过几个典型问题列出来给大家排雷。第一个是通道数不匹配。把PPA插进yaml后如果PPA输出通道和下一层期望不一致会在parse_model阶段直接报错。解决办法是仔细对照原SPP的输出通道或者用1x1卷积做层间对齐。还有一种情况是PPA内部mid_c如果设得太小比如只有2后面的注意力卷积会变成1x1卷积处理2个通道效果很差。建议mid_c至少设8以上如果输入通道C512默认mid_c170肯定没问题。第二个是显存溢出。我在3090上开到batch size 16刚好卡在边界如果同时开了多尺度训练或者图像分辨率超过768就会OOM。我的处理是开启AMP混合精度、关闭Mosaic、把梯度累积步数设为2。注意关闭Mosaic后batch diversity会降低需要依赖更多的传统增强来补足。如果还是爆显存可以考虑把PPA的scales从(1,2,4)减到(1,2)参数量和中间特征都会小不少。第三个是训练不收敛或loss震荡。红外小目标的loss曲线本身就比常规目标更抖这是正常现象。但如果你发现loss在前面50个epoch完全不下来先检查学习率和warmup。我把初始lr从0.01降到0.007之后稳定性提高了不少。另外如果数据集很小建议先用Grad-CAM或直接可视化PPA的注意力图看看网络到底在关注什么。如果注意力图几乎全是噪声说明数据里小目标太少需要在数据增强上下功夫而不是继续调模型结构。第四个问题是PPA过拟合。由于PPA参数量虽然不大但仍比原SPP多了一部分可学习参数在某些千级规模的小数据集上出现过验证集指标先升后降的情况。我加了一个0.1的Dropout在注意力生成分支的ReLU之后能稍微缓解过拟合。不过这个操作只在小数据集上有效数据集大了以后要不要加需要你自己评估。最后再分享一个小技巧。PPA模块不一定只能放在Backbone末尾如果你有计算余量可以在Neck的高分辨率输出层旁边并联一个轻量版PPAscale只用(1,2)然后再和原始输出相加。这样能同时增强深层和浅层的小目标上下文但记得要重新做消融实验不要凭感觉加。我在实际项目里用过这个方案在无人机红外视频上效果比单点PPA更好但相应的部署复杂度和算力开销也会上升适合对性能要求高、算力又允许的场景。希望这份完整的融合记录能让你少走几个弯路。