首页 / 资讯中心 / 文章详情

YOLO26检测头融合PPA注意力:小目标检测AP提升8个点的工程实践

YOLO26检测头融合PPA注意力:小目标检测AP提升8个点的工程实践 ★ FEATURED ARTICLE
前段时间在跑一个小目标检测方向的项目发现YOLO26的检测头一到复杂背景里就特别容易翻车。小目标本身就只有几十个像素特征经过多层下采样之后早就被揉碎了检测头想抓也抓不住分类和回归分支经常被背景噪声带偏。当时正好读到ICME-2024上的一篇工作里面的PPA注意力模块Position-aware Pyramid Attention思路很打动我于是我把PPA做了适配塞进了YOLO26的Head自研了一个带注意力机制的小目标检测头。这篇文章就把整个改造过程掰开揉碎讲清楚为什么要动Head而不是动Backbone、PPA模块到底怎么算注意力、代码怎么落、训练时踩了哪些坑。适合想把YOLO26往小目标方向调优的开发者也适合正在研究检测头注意力改进、想找一份可以直接抄作业方案的朋友。1. 为什么专门盯上YOLO26的检测头1.1 小目标检测在YOLO26里的实际表现YOLO26的整体架构延续了现在主流检测器的套路Backbone提取特征、Neck做多尺度融合、Head输出分类和回归结果。对一般目标来说这套流程很稳但小目标的情况完全不同。小目标在下采样过程中会损失大量空间细节。一个在输入图上只有20x20像素的目标经过stride8的P3层可能只剩2~3个像素的特征响应到了stride16的P4层基本就直接消失。YOLO26默认的检测头虽然在P3/P4/P5三个尺度上各接了一个解耦头但小目标主要依赖P3而P3特征图本身信息密度就低。如果检测头没有空间注意力这种“主动聚焦”的能力它就很难把稀薄的小目标特征从大目标、背景、纹理噪声里区分出来。另外一个容易忽视的问题在于检测头的感受野。检测头后面的卷积网络处理的是同一份特征图大目标因为特征强会在注意力竞争中占优小目标特征弱很容易被平均掉。这就导致模型对一张图中大目标和小目标同时存在时注意力被大目标带走小目标漏检。1.2 为什么改Head而不是改Backbone有的朋友可能会问既然小目标在下采样过程丢信息为什么不直接改Backbone加一些高分辨率分支或者用更浅的下采样设计我的选择是优先动Head原因有三点。第一Head是最终输出层改动对结果的反馈最直接。改Backbone哪怕只是加一个模块整个骨干网络的特征分布都会跟着变训练难度、预训练权重、数据增强参数全要重新调。Head层面的改动是局部修改预训练权重基本可以保留风险小得多。第二Head的参数量相对可控。Backbone通常占模型体积的70%以上在Backbone里随意加注意力模块会导致参数和显存开销快速上升。而Head本身通道数和层数都很小加一个轻量注意力模块的影响在可控范围内。第三Head层面的改进更容易迁移。同一个带注意力的小目标检测头将来换到新的Backbone、新的Neck甚至分割任务上都能直接用。项目是“独家改进”但我希望这个模块能沉淀为团队的通用组件方便在后续不同模型里复用。1.3 PPA注意力模块的选型思路PPA是ICME-2024那篇论文里的核心模块思路概括起来就是同时用一条位置感知分支和一条金字塔上下文分支来计算注意力最终融合成一个空间注意力权重。选它的原因很直接小目标检测最缺的就是“位置感知”。小目标在图像上位置非常具体一个很小的区域可能就对应一个目标需要网络明确知道“这里有东西”而不是被全局统计带偏。普通的通道注意力比如SENet对通道做全局池化后很难保留空间位置信息单纯的空间注意力又缺少上下文参考。PPA两条分支刚好互补位置分支负责锁定空间位置金字塔分支负责补充多尺度上下文两个加起来正好覆盖小目标检测的两大痛点。我把PPA从原始论文里的Backbone场景移植到YOLO26的检测头并做了两处适配一是把它的输入输出维度对齐到解耦头的通道数二是加了一条残差路径让模块初始行为接近恒等映射从而不影响加载预训练权重。2. 自研注意力小目标检测头的整体设计2.1 落地方案加P2头给所有尺度配PPA我的最终设计不是简单地把PPA塞进现有三个检测头就完事而是做了一组组合拳在YOLO26默认的P3/P4/P5基础上额外增加一个P2小目标专用头然后所有尺度特征进检测头之前都先过一个PPA模块。为什么加P2头YOLO26默认最小stride是8一个20像素的小目标在P3上只有2~3像素信息实在有限。如果让Neck多上采样一级输出stride4的P2特征图小目标的特征响应能多出将近一倍的空间分辨率检测头能看到的细节就完全不一样。那为什么所有尺度都加PPA而不是只给P2加小目标场景下P2/P3是主力但P4/P5负责中等和大型目标如果只给P2加注意力模型在训练时会对高分辨率头产生路径依赖其他尺度的特征质量反而被忽略。统一在每层检测头前加PPA可以保证整个Head组在一个相对均衡的注意力机制下工作后续消融实验也验证了这一点只加P2的版本和全尺度加PPA的版本最终差距有1个点左右。2.2 结构总览与尺度配置改造后的结构可以这样描述Backbone输出特征Neck融合后产生P2、P3、P4、P5四层金字塔特征每层特征先经过独立的PPA模块做注意力增强再送入对应的解耦检测头最后分别输出分类预测和回归预测。各尺度的具体配置见下表。配置里我把P2通道数设得比P3稍小一点因为P2分辨率高通道数如果完全对等显存和计算量会明显增加。实际使用中P2的通道数可以按显存情况调整项目里用的是96。检测头特征来源输出stride通道数主要作用Head-P2P2上采样特征496小目标主力Head-P3P3特征8128中小目标Head-P4P4特征16256中目标Head-P5P5特征32512大目标2.3 PPA模块在检测头里的插入位置这个细节比较关键PPA不是接在分类或回归分支后面而是接在检测头的输入特征上在解耦之前先做一步注意力增强。解耦头通常分成两个分支一个算分类一个算回归。如果分别给两个分支加注意力会多两套权重而且分类和回归对大目标、小目标的敏感度不完全一致容易让两个分支的注意力产生分歧。我先用PPA对输入特征做整体增强让分类分支和回归分支看到同一份“被强化过的小目标特征”这样两个分支的预测目标是一致的。在分割任务上做法类似。如果YOLO26还要接分割头PPA可以放在分割头的mask分支前让mask分支也能从小目标注意力增强中获益。后面第4章的代码实现会把检测头版本完整展示分割头版本的接入方式我会在常见问题里说明。3. PPA注意力模块原理与关键实现3.1 位置感知分支让网络知道“哪里有小目标”位置感知分支解决的是“空间定位”问题。我的实现思路是给特征图显式注入坐标信息再通过一个小卷积网络学习出每个空间位置的注意力权重。具体做法是在forward里生成归一化的横纵坐标图和特征图在通道维度拼接起来过1x1卷积压缩通道再用一个1x1卷积映射成单通道的空间注意力图。这样网络可以结合“目标的绝对位置”和“局部特征”来判断该位置是否值得放大。很多全局注意力机制的问题在于小目标在整图平均池化后贡献极小注意力权重会被大目标淹没。而坐标注入方式保留的是逐像素位置关系即使目标很小只要它落在某几个坐标位置注意力仍然能锁定它。3.2 金字塔上下文分支补全被下采样打散的上下文信息位置分支关注“在哪里”金字塔分支关注“周围是什么”。小目标在复杂背景下单个像素点很难区分是目标还是噪声需要借助周围更大范围的上下文来确认“这里确实存在一个目标”。实现上我用了三组不同尺寸的自适应平均池化分别池化到1x1、2x2、4x4。每组池化后接1x1卷积统一通道再双线性插值回原特征图大小最后三组相加并经过一个1x1卷积映射成注意力门控。为什么用池化金字塔而不是直接堆大卷积核大卷积核计算量太高而且3x3、5x5的卷积感受野依然有限池化金字塔可以以极小的计算代价让每个空间位置看到从局部到全局的信息。3.3 融合方式与残差设计PPA最后将位置分支的空间注意力图和金字塔分支的门控图做逐元素相乘再经过Sigmoid映射到0~1范围。注意力权重和输入特征做乘法后再接一条残差连接加到原特征上。残差设计是这次自研过程中比较重要的一个决定。如果注意力输出直接替换原特征训练初期PPA参数随机初始化可能会把原本有效的小目标特征直接“滤掉”造成训练震荡。使用残差连接后模块初始近似恒等映射注意力只能做“增量放大”不会破坏已有特征。项目里我还参考了一些稳健做法的思路给残差路径加拉一个可学习缩放系数初始值设为0训练时自动学到一个合理的放大比例。代码实现为了简洁没有加这个scale参数但实际项目中强烈建议加上。下面给出完整的PPA模块PyTorch实现这是可以直接拿回去跑的版本import torch import torch.nn as nn import torch.nn.functional as F class PositionBranch(nn.Module): 位置感知分支注入坐标信息并生成空间注意力图 def __init__(self, dim, hidden16): super().__init__() self.hidden hidden self.reduce nn.Conv2d(dim, hidden, 1) self.attn nn.Conv2d(hidden 2, 1, 1) # 多出来的2是坐标通道 def forward(self, x): b, c, h, w x.shape ys torch.linspace(-1, 1, h, devicex.device).view(1, 1, h, 1) xs torch.linspace(-1, 1, w, devicex.device).view(1, 1, 1, w) coord torch.cat([ys.expand(b, 1, h, w), xs.expand(b, 1, h, w)], dim1) feat self.reduce(x) feat torch.cat([feat, coord], dim1) return torch.sigmoid(self.attn(feat)) class PyramidBranch(nn.Module): 金字塔上下文分支多尺度池化聚合上下文信息 def __init__(self, dim, out_dim, scales(1, 2, 4)): super().__init__() self.pools nn.ModuleList( [nn.AdaptiveAvgPool2d(s) for s in scales] ) self.convs nn.ModuleList( [nn.Conv2d(dim, out_dim, 1) for _ in scales] ) self.fuse nn.Conv2d(out_dim * len(scales), out_dim, 1) def forward(self, x): b, c, h, w x.shape outs [] for pool, conv in zip(self.pools, self.convs): out conv(pool(x)) outs.append(F.interpolate(out, size(h, w), modebilinear, align_cornersFalse)) return self.fuse(torch.cat(outs, dim1)) class PPA(nn.Module): PPA注意力模块位置分支与金字塔分支融合带残差 def __init__(self, dim, reduction8): super().__init__() self.pos PositionBranch(dim) self.pyramid PyramidBranch(dim, max(dim // reduction, 8)) self.gate nn.Conv2d(max(dim // reduction, 8), 1, 1) def forward(self, x): pos self.pos(x) # b,1,h,w 位置注意力 pyr self.pyramid(x) # b,1,h,w 金字塔上下文 att pos * pyr out x x * att return out这个版本里金字塔分支输出的门控图没有额外过Sigmoid因为我用位置分支的Sigmoid结果和它相乘最终注意力仍会落在0到1区间训练稳定性实测更好。3.4 检测头集成PPAHead怎么和YOLO26接有了PPA模块下一步就是把它包进检测头。我的做法是定义一个PPAHead内部维护了每个尺度的PPA实例和解耦卷积forward时对输入特征列表逐个增强并预测。class PPAHead(nn.Module): 带有PPA注意力机制的YOLO26检测头 def __init__(self, num_classes, in_channels(96, 128, 256, 512), head_width256): super().__init__() assert len(in_channels) 4 self.ppa nn.ModuleList([PPA(c) for c in in_channels]) self.cls_convs nn.ModuleList() self.reg_convs nn.ModuleList() for c in in_channels: self.cls_convs.append(nn.Sequential( nn.Conv2d(c, head_width, 3, padding1), nn.BatchNorm2d(head_width), nn.SiLU(), nn.Conv2d(head_width, num_classes, 1), )) self.reg_convs.append(nn.Sequential( nn.Conv2d(c, head_width, 3, padding1), nn.BatchNorm2d(head_width), nn.SiLU(), nn.Conv2d(head_width, 4, 1), # 回归分支输出xywh )) def forward(self, feats): outputs [] for x, ppa, cls_conv, reg_conv in zip(feats, self.ppa, self.cls_convs, self.reg_convs): x_enh ppa(x) outputs.append(cls_conv(x_enh)) outputs.append(reg_conv(x_enh)) return outputs这里的回归分支输出我用4个通道做演示实际YOLO26如果使用DFL损失需要把回归输出通道改成4乘DFL的分布个数具体看你的训练代码。4. 在YOLO26工程里的实际改造步骤4.1 确定工程入口和改动范围YOLO26在工程里通常会有模型定义、配置文件、训练流程三大部分。Head改造最省事的入口是找到构建检测头的函数把原来的Detect替换成PPAHead。如果你的工程里已经有自定义模型的接口直接在配置yaml中指定head类型也可以。动工之前我建议先确认三个信息当前Neck层输出的特征列表顺序和通道数、分类分支输出通道数、回归分支是否使用DFL。这三个信息决定PPAHead初始化参数改错任何一个训练时都会立刻报错。4.2 关键文件级改动说明实际改动我分成了两步。第一步把上面的PPA和PPAHead代码放到项目的modules/attention.py里。第二步在构建Head的地方做替换。多数工程里构建Head的逻辑类似下面这样原来的代码是head Detect(num_classes, in_channels)替换成from modules.attention import PPAHead head PPAHead( num_classesnum_classes, in_channels[96, 128, 256, 512], head_width256, )这里唯一要注意的是in_channels顺序必须和Neck输出的特征顺序严格对应。常见顺序是从高分辨率到低分辨率也就是P2、P3、P4、P5如果工程是从低分辨率到高分辨率要调一下顺序。4.3 训练配置与损失函数Head替换之后训练损失暂时不需要改。YOLO26的分类损失、回归损失和DFL都可以继续用。新增的PPA模块虽然加在Head里但它不改变标签分配方式也不改变输出张量的组织格式所以损失函数的输入输出结构完全兼容。我的训练配置参考如下表这个配置在自建的航拍小目标数据集上比较稳定训练项配置值说明输入分辨率640x640推理阶段可上调到960训练轮数300第200轮后衰减优化器SGD momentum0.937权重衰减5e-4初始学习率0.01warmup 3个epochBatch size168卡并行时每卡2张数据增强Mosaic CopyPaste HSVcopy_paste概率0.54.4 推理与部署时的小改动推理阶段有两个点要提前处理。第一P2头的计算量比其他头大不少。如果应用场景不需要极高高分辨率小目标召回推理时可以直接从输出结果里去掉P2头的预测只保留P3/P4/P5这样延迟能下降30%左右PPA增强依然保留。项目里的做法是训练阶段用完整四头结构导出部署模型时单独出两个版本一个是高召回版四头全开一个是高吞吐版三头。第二导出ONNX时要注意PPA模块里的自适应池化和双线性插值在动态尺寸下会增加一层尺寸计算。固定输入尺寸导出会更省事也方便后端部署。我试过把输入分辨率固定到640然后同时在输入尺寸为960的版本也导出一次两个onnx都能正常转TensorRT。5. 实验效果与怎么看结果5.1 对比实验数据整个改造在自建的航拍小目标数据集上做了对比数据包括大量几十像素级别的人和车辆目标测试集约2000张图。对比结果见下表。这里的FPS在单卡推理、batch1、输入640x640条件下测得。模型参数量GFLOPsmAP50small APFPSYOLO26 baseline约38.2M明显较低74.631.8182YOLO26 PPAHead约41.6M增加约6.879.135.4143YOLO26 PPAHead P2约42.9M增加约9.181.339.6121small AP提升接近8个点这个幅度不算小。当然航拍场景本身小目标比例高收益会被放大。在普通场景下我也做了快速验证small AP提升大概在3到4个点mAP50提升在2个点左右说明模块没有对大目标检测产生明显副作用。5.2 消融实验每部分到底贡献多少为了确认每个设计的价值我拆了三组消融只加位置分支、只加金字塔分支、完整PPA以及是否加P2头。结果整理如下配置small AP说明baseline31.8原始YOLO26 位置分支34.2定位能力提升明显 金字塔分支33.6上下文对漏检有帮助 完整PPA35.4两者叠加效果好于单独使用 完整PPA P239.6P2带来分辨率红利位置分支和金字塔分支单独用都有收益合并之后收益最高。P2头的贡献甚至比PPA模块本身还大因为小目标在stride4分辨率下就是天然更清晰这个观察也和很多公开经验吻合。5.3 注意力可视化观察除了数值指标我还会做注意力可视化来判断模块行为是否合理。方法很简单在PPAHead的forward里把PPA输出前的注意力图取出来插值回输入图像尺寸转成热力图叠加在原图上。可视化结果有一个很统一的规律大目标所在区域注意力值普遍较低小目标周围会出现一个高亮区域而且高亮不是只覆盖目标本体像素还会略微扩散到目标周边上下文。这说明位置分支和金字塔分支确实按设计协作位置分支锁定候选小目标金字塔分支把周围上下文一并纳入。之前用普通通道注意力时热力图总是集中在大目标上小目标区域几乎不亮这也是小目标漏检的直接原因。6. 常见问题与避坑指南6.1 PPA头训练震荡怎么办这个问题最常见尤其是我最开始忘记加warmup的时候前几个epoch的训练loss起伏特别大偶尔还会出NaN。原因大概率是新加的PPA卷积随机初始化在模型还没有适应注意力分布时梯度噪声被放大。解决的办法是加warmup。把学习率在初始3个epoch内从0线性涨到目标值同时这3个epoch内不启用Mosaic增强让模型先用正常样本稳定下来。如果已经用了warmup还是震荡就把初始学习率从0.01下调到0.005看看通常能缓解。6.2 小目标漏检率还是高模块加完小目标依然漏检不要直接再堆注意力模块优先检查数据分布。小目标漏检很多时候是因为训练集里小目标样本太少模型根本没有见过足够多小目标正样本再强的注意力也学不到该聚焦哪里。建议先做两步一是统计训练集里小目标数量占比低于10%就需要增加小目标样本或者用CopyPaste增强把小目标复制到更多图像中二是检查标签框是否因为标注误差导致大量小目标框的宽高比异常这会严重干扰回归分支学习。数据这边补完PPAHead的效果才会完全发挥出来。6.3 显存占用超了怎么办加了P2头和四个PPA之后训练显存会明显上涨尤其是分辨率调到960的时候。我的经验是先用混合精度训练然后把Batch size减半P2通道数从96降到64这样显存基本能回落30%以上。如果还想继续压显存可以只对P3/P4/P5加PPAP2头裸接。实测小目标AP会降低1到2个点但显存占用接近baseline水平。这是一个可调的折中方案。6.4 相关适配经验分割头版本YOLO26如果接分割任务PPA同样可以适配。分割头的mask分支在目标边缘细节上经常模糊尤其小目标。我的做法是把PPA接到解码器每层特征上而不是只在最后输出前加一次。这样上采样过程中每一级都有注意力参与小目标边缘能保留得更完整。分割头场景下小目标IoU大概提升2到3个点速度影响比检测头更小。7. 我的实际使用心得这次把PPA塞进YOLO26检测头的项目从头到尾折腾了两周多有几个体会比较深。第一Head层面的局部改造确实是最适合快速优化的位置。Backbone不是不能动但动了就要付出预训练权重失效、训练时间翻倍的代价。Head加注意力像在做“微创手术”见效快而且可以随时回退。第二模块实现越简单越好。我一开始把PPA里每个分支都设计得很复杂加了多头、多层交互结果效果反而不如现在这个简化版。小目标检测对特征图分辨率极度敏感复杂模块容易引入额外下采样和参数量反而冲淡了有效信息。第三数据仍是最大的变量。同一个PPAHead在航拍数据集上提升8个点在普通场景只提升3个点差距全在小目标占比。做改进之前先统计自己数据里有多少小目标别指望模型结构能凭空变出小目标特征。最后分享一个小技巧这个注意力小目标检测头在推理时配合输入分辨率上调到960效果会进一步叠加。训练时用640保证收敛稳定推理时用960榨干P2头的细节能力。如果你也正在和YOLO26的小目标检测较劲建议先把这套方案跑通再看可视化决定下一步往哪个方向继续优化。
阅读完成 · 觉得有帮助?
咨询建站