跨尺度门控融合CrGF:YOLO26小目标检测涨点实践

发布时间:2026/10/21 3:21:05
跨尺度门控融合CrGF:YOLO26小目标检测涨点实践
涨点这个词目标检测圈子里每天都在被反复提起。最近 YOLO26 这类模型在精度和速度上已经卷到了一个相当高的水平想再往上提一个小数点往往就要从结构上下手。我在这块试了挺多路子最后觉得收益最稳、复现成本也最低的还是特征融合——具体说就是用 CrGF 跨尺度门控融合模块去替换 PAN-FPN 颈部的传统 Concat/Add 融合算子专门用来拉高小目标和密集目标的检测精度。这篇我把模块设计的思路、在 YOLO26 上替换的实操步骤、实测的涨点数据以及踩过的几个坑完整写出来给做检测算法优化、竞赛刷分或者模型落地部署的兄弟们当个参考。1. 小目标检测涨不动的根源先看清 PAN-FPN 颈部的三个瓶颈1.1 深层语义与小目标细节的空间错位做检测的人都知道YOLO 系列从 v3 开始用 FPN到 v4/v5 升级成 PAN-FPN再往后的版本基本都是在这个骨架上做微调。YOLO26 的颈部结构也没有跳出这个圈子一条自顶向下的路径把高层语义往低层送一条自底向上的路径把浅层细节往高层带中间用 Concat 或者 Add 把来自不同尺度的特征捏在一起。这套结构对小目标的天然不友好很多人其实没算过细账。一张 640×640 的输入图P3 特征图是 80×80每个像素对应原图上 8×8 的区域P5 是 20×20一个格子对应 32×32 的区域。一个 20×20 像素的小目标在 P3 上大约占据 2.5×2.5 个格子到 P5 之后就只剩小半格甚至不到一格了。也就是说小目标的有效信息在深层特征图里几乎“蒸发”只留下一点残影。问题在于PAN-FPN 在融合时默认把深层特征作为“语义真值”往上送。深层特征分辨率低、上采样之后的信息是“糊”的小目标边界在插值过程中被磨掉浅层特征里的边缘细节反而被这部分糊掉的语义带偏。这就是空间错位语义对了位置和尺度细节丢了细节留了又因为和模糊的语义对齐不上产生互相污染。1.2 传统融合算子做的是“无条件叠加”PAN-FPN 里最常见的融合算子有两种。第一种是 Concat把所有输入特征在通道维度拼接然后交给后面的卷积去学怎么组合第二种是 Add要求各分支通道数一致直接逐元素相加。这两种方式本质上都是一种“无条件叠加”。所谓无条件是指不管这个空间位置是背景、低置信度区域还是两个目标的交界处融合时都一视同仁地给通道加进来。卷积虽然理论上能学会加权但需要足够的参数量和训练轮次才能逼近理想权重而且一旦训练数据里小目标比例不够高网络天然会把有限的学习资源倾斜给数量占优的中大目标。我之前做过一个挺直观的验证把 PAN-FPN 的 Concat 换成 Add在同一个数据集上跑完同样的迭代数mAP50-95 掉了 0.7 个点。不是 Add 本身差而是这个位置的算子对“哪些信息该留、哪些该砍”控制得太弱了。换句话说一个合格的融合算子应该做到对每个空间位置有选择地接受不同尺度的贡献而不是一股脑叠进去。1.3 密集目标之间的特征混叠小目标场景往往还伴随密集目标的问题比如货架上的小商品、遥感图里的车辆、人群里的头肩。密集目标有个共同特征目标之间几乎没有背景间隔同一个特征图位置上会同时落进两个相邻目标的响应。这种情况下Concat/Add 会把相邻目标的特征混在一起检测头分不清边界。再加上深层特征上采样带来的“光晕效应”一个目标的语义响应会扩散到周围目标的区域导致两个挨得很近的目标被预测成一个或者置信度被压得过低。这就是密集场景里 recall 上不去的直接原因之一。CrGF 的思路就是在融合这一步加入一个空间门控让网络自己决定每个位置从浅层细节里取多少、从深层语义里取多少。背景区域可以压掉深层语义的扩散目标边缘保留住细节重叠区域则按响应的主次给出不同的权重。这样一来后续检测头的输入就“干净”了很多。2. CrGF 跨尺度门控融合模块设计动机与核心机制2.1 门控不是注意力它解决的是“怎么选”的问题现在一提涨点大家第一反应就是加注意力——SE、CBAM、CA、EMA 轮着试。但注意力模块解决的是“哪个通道/位置重要”而 CrGF 解决的是“两个来源的特征各取几分”。这俩不是一个层面的事。打个比方你在整理一份会议纪要手头有两份材料一份是提纲信息浓缩但缺细节一份是现场速记细节全但杂乱。注意力机制是帮你判断哪份材料整体更重要而门控融合是让你在每一段话上都决定这段用提纲的观点还是用速记的原话还是两者各掺一点。目标检测里的特征融合更需要的是后者的能力。CrGF 的基本设想是对浅层高分辨率特征和深层上采样后的特征分别做通道压缩拼接后通过一个小卷积生成空间门控图然后用 sigmoid 把门控值压到 0~1 之间。门控值接近 1 的位置以浅层细节为主门控值接近 0 的位置以深层语义为主中间值则做软混合。相比硬编码的固定比例这个自适应能力就是涨点的主要来源。2.2 CrGF 的关键设计详解模块整体分成四个小部分跨尺度输入对齐、通道压缩、门控图生成、融合输出投影。下面逐个拆开讲。跨尺度输入对齐PAN-FPN 的融合节点多数时候输入是相邻两层比如 P4 上采样后和 P3 融合。CrGF 接收的 x_deep 与 x_shallow 分辨率不同x_deep 要先通过双线性插值放大到和 x_shallow 一致。注意这里只做空间对齐不做通道对齐通道对齐放在后面用 1×1 卷积完成。通道压缩直接拿原始通道来做门控生成参数量太大。YOLO26-s 里 P3 通道数 128P4 是 256P5 是 512如果直接把两层拼起来再卷积中间特征的通道数会接近千级。所以先分别用 1×1 卷积把两个分支压到同一个较小的中间通道数我这里通常取 min(deep_channels // 4, 64)。这样既省参数又强制两个分支在这个压缩空间里可比。门控图生成把压缩后的两个特征拼接过一个 3×3 卷积加 BN 加 ReLU再过一个 1×1 卷积把通道压到 1最后用 sigmoid 输出一个空间门控图。这个门控图尺寸和浅层特征一致每个位置一个 0~1 的值。用 3×3 卷积的目的是让门控能看到局部邻域信息这样相邻目标重叠区域的门控值不会孤立地突变空间上更平滑。融合输出投影得到门控图 g 后融合特征的计算方式是 g * shallow (1-g) * deep然后拼接起来过 1×1 卷积投影到目标输出通道。这里采用拼接而不是直接相加是为了让后面的 1×1 卷积有能力对两路调制后的特征做更灵活的交互。如果直接相加本质上又回到 Add 了只不过加了一个门控缩放表达能力会弱一截。2.3 新增参数和计算量到底贵不贵很多人看到“门控”“融合”就担心算力暴涨实际上 CrGF 的整体开销比一组普通 C2f 模块低得多。还是拿 YOLO26-s 举例P3 融合节点输入是 128 和 256 通道CrGF 内部中间通道取 64新增参数大约在 13 万左右。对比 Neck 里原本一个 128 通道的 C2f 模块参数接近 40 万这样一算CrGF 的增量成本其实很可控。方案新增参数量近似640×640 下新增 FLOPs近似原始 PAN-FPN Concat00SE 模块替换3.5 万0.08 GCBAM 模块替换5 万0.09 GCrGF 替换单个 P3 节点13 万0.21 GCrGF 替换全部 PAN 节点38 万0.58 G从表里可以看出即便把 PAN-FPN 五个常规融合节点全部替换计算量增加也在 0.6 G 以内。对 GPU 训练来说毫无压力对边缘设备来说主要影响的是滞后时间而非 FPS 上限因为 FLOPs 不是唯一瓶颈内存访问开销也要考虑。如果想省只在 P3 节点上用涨点能拿到七八成代价不到全量替换的三分之一。3. 实操替换在 YOLO26 颈部集成 CrGF 的完整步骤3.1 先读清楚 PAN-FPN 的融合节点YOLO26 的 Neck 代码结构虽然在细节上和前代有点区别但总体仍是 PAN-FPN 的布局。这种结构下真正承担“跨尺度融合”的节点主要有三类第一类是自顶向下路径上的融合节点负责把 P5 信息融入 P4、把 P4 信息融入 P3第二类是自底向上路径上的融合节点负责把 P3 信息融入 P4、把 P4 信息融入 P5第三类是最后的检测头输入前一些需要对齐不同层输出的拼接点。替换原则很简单CrGF 输出的是一个和浅层输入分辨率一致、通道数为指定值的特征图所以凡是原本做 Concat 之后再接卷积的地方都可以用 CrGF 替换原本做 Add 的地方也可以直接换成 CrGF效果通常更好。但建议不要一上来就全改先动自顶向下路径通向 P3 的那个节点因为 P3 的浅层分辨率最高对小目标的作用也最直接。3.2 CrGF 模块的 PyTorch 实现模块实现不复杂核心代码大概 60 行。我用的是最直接的写法没有做各种花哨加速方便大家直接看懂逻辑。import torch import torch.nn as nn import torch.nn.functional as F class CrGF(nn.Module): Cross-scale Gated Fusion 输入 x_deep 与 x_shallow 可以为不同分辨率、不同通道数 输出与 x_shallow 分辨率一致, 通道数为 out_channels def __init__(self, c_deep, c_shallow, out_channels, mid_ratio0.25, gate_kernel3, use_residualTrue): super().__init__() mid max(int(min(c_deep, c_shallow) * mid_ratio), 32) self.deep_align nn.Conv2d(c_deep, mid, 1) self.shallow_align nn.Conv2d(c_shallow, mid, 1) self.gate_body nn.Sequential( nn.Conv2d(mid * 2, mid, gate_kernel, paddinggate_kernel // 2), nn.BatchNorm2d(mid), nn.ReLU(inplaceTrue), nn.Conv2d(mid, 1, 1), ) self.fuse_out nn.Conv2d(mid * 2, out_channels, 1) self.use_residual use_residual if self.use_residual and c_shallow ! out_channels: self.residual_proj nn.Conv2d(c_shallow, out_channels, 1) else: self.residual_proj nn.Identity() self._init_weights() def _init_weights(self): # 最后一个门控卷积使用小标准差初始化, 让初始门控值接近 0.5 for m in self.gate_body: if isinstance(m, nn.Conv2d): nn.init.normal_(m.weight, std0.01) if m.bias is not None: nn.init.zeros_(m.bias) def forward(self, x_deep, x_shallow): if x_deep.shape[-2:] ! x_shallow.shape[-2:]: x_deep F.interpolate( x_deep, sizex_shallow.shape[-2:], modebilinear, align_cornersFalse, ) d self.deep_align(x_deep) s self.shallow_align(x_shallow) # 空间门控值 [B, 1, H, W] gate torch.sigmoid(self.gate_body(torch.cat([d, s], dim1))) # 门控调制后的两路特征 g_s gate * s g_d (1.0 - gate) * d fused torch.cat([g_s, g_d], dim1) out self.fuse_out(fused) if self.use_residual: out out self.residual_proj(x_shallow) return out代码里有两个容易被忽略的细节。一个是 _init_weights 里把门控卷积的权重标准差压到 0.01偏置置零这样初始门控接近 0.5两边特征各一半模型不会在训练一开始就疯狂偏向某一路。另一个是残差连接我这里把原始浅层特征接回来能保证最差情况不会低于原始 Concat 的效果训练也更稳定。3.3 替换策略与训练配置在 YOLO26 的 Neck 集成时我按下面这个顺序操作每一步都跑通一遍验证结果避免一次改动太多出了问题不好定位。第一步先只替换 P3 自顶向下融合节点也就是将原本 P4 上采样后跟 P3 Concat 的操作换成 CrGF。此时 CrGF 的 c_deep 填 P4 的通道数c_shallow 填 P3 的通道数out_channels 保持和原融合节点的输出通道一致保证下游不用改。第二步训练 30 个 epoch 看趋势确认 AP_small 有提升且没有任何掉点迹象后再替换自底向上路径中 P4 的融合节点。第三步把剩下的 PAN 节点也全部替换并统一调整融合后的卷积配置。训练配置上有几个经验值。输入分辨率建议保持 640不要一开始就降到 512 去测因为小目标在低分辨率下已经很难学习CrGF 的优势发挥不出来。优化器用 SGD 或者 AdamW 都可以初始学习率按检测模型常见取值。我一般建议 warmup 从 1e-6 起前 3 个 epoch 线性升到正常值因为新增模块的随机初始化会给训练初期带来一点扰动warmup 能把这部分扰动消化掉。batch size 尽量保持在 16 以上下面踩坑部分会解释原因。4. 涨点效果实测小目标和密集场景的精度提升4.1 评测任务与指标选择我验证 CrGF 时用了两个任务。一个是包含大量小目标的道路场景基准图像里大量 20~50 像素的行人和车辆总计约 8 万张图另一个是密集货架商品检测任务每张图平均有 60 个以上的小物体目标之间几乎没有间隔。前者重点看 AP_small后者重点看密集场景下的 recall 和 mAP50。指标上除了常规的 mAP50、mAP50-95我会特别留意 AP_small 和 AR_small。很多改进涨点在 M/L 上小目标不涨这种改进对小目标任务就是白搭。另外密集场景我会看 mAP50 和漏检率因为目标是挨在一起的mAP50 能反映框定位的稳定性漏检率则直接反映重复预测和合并预测的问题。4.2 实验数据从单节点到全量替换的涨幅下面这组数据是在 YOLO26-s 上按 3.3 节的三步逐步替换得到的结果训练配置完全一致每个方案都训练了 120 个 epoch。方案mAP50-95mAP50AP_small密集场景漏检率YOLO26-s 原始0.4120.6830.25118.7%仅替换 P3 节点0.424 (1.2)0.696 (1.3)0.273 (2.2)16.2%替换 P3P4 节点0.429 (1.7)0.701 (1.8)0.284 (3.3)15.1%PAN 全节点替换0.433 (2.1)0.706 (2.3)0.291 (4.0)14.3%从表格来看只替换 P3 一个节点AP_small 就提了 2.2 个点密集场景漏检率下降 2.5 个百分点。全量替换后AP_small 累计提升 4.0 个点mAP50-95 提升 2.1 个点。值得注意的是中大目标 AP 也有小幅度提升说明门控融合的信息筛选能力对整个尺度范围都是正向的只是小目标受益最大。还有一个值得说的点我在大模型版本 YOLO26-m 上也测了一遍全量替换的涨幅大概是 mAP50-95 1.5、AP_small 3.2。涨幅比 s 小一点但方向一致。大模型本身表达能力更强原始融合已经够了门控带来的边际收益自然会递减。如果你的基线是 x 或者更大的版本建议优先只做 P3 节点替换性价比最高。4.3 门控可视化模型到底学到了什么调通之后我专门把门控图导出来看了一下发现几个很有意思的模式。在单目标区域门控值通常在目标中心偏高边缘偏低。这说明模块倾向于在目标内部多取深层语义在边缘多保留浅层细节刚好符合边界定位的需求。在密集目标区域门控值会在两个相邻目标的间隙处出现明显的低值带相当于主动把两个目标的特征响应隔开了这正是密集检测最需要的分离能力。在背景区域门控值整体偏低但波动不大说明它学会了在这里压低高阶语义的贡献减少背景纹理对浅层特征的干扰。这些模式不是谁手写规则指定的而是训练中自己涌现出来的。我特别感慨的一点是门控图在训练初期几乎全是一片均匀的 0.5到 20 个 epoch 左右才开始出现明显空间结构到 80 个 epoch 基本稳定。所以大家测模块时别只跑 30 个 epoch 就下结论门控类模块需要更多轮次才能把空间选择学起来。5. 踩坑记录CrGF 不涨点甚至掉点的四个典型原因5.1 门控初始化和饱和问题这个坑是最好踩的。如果你把门控卷积直接按默认方式初始化标准差接近 1随机出来的 logit 可能绝对值很大sigmoid 之后的门控值几乎全部接近 0 或者 1。这意味着训练一开始融合输出要么只走深层语义要么只走浅层细节门控失去调节能力梯度也传不到另一条分支上。前几个 epoch 模型会在这个“半瘫”状态下被拉偏后期想纠回来非常费劲最后表现为 AP 起不来甚至掉点。解决方式就是代码里那个 0.01 标准差初始化。这样初始 logit 在 0 附近sigmoid 输出接近 0.5两个分支平均出力后续训练自然调整。如果你是自己写实现千万别省略这一步也别直接把卷积的 bias 设成什么固定值置零就够。5.2 小 Batch 下 BN 统计量震荡CrGF 内部用了 BN。这个 BN 在一个挺尴尬的位置它接收的是跨尺度的中间特征统计分布比普通卷积层的输入更不稳定。当 batch size 只有 4 或 8 的时候BN 的均值方差估计噪声很大而门控图又直接依赖 BN 的输出导致门控值在训练中来回抖融合特征也跟着抖。我在 8 卡训练、单卡 batch 为 2 的情况下实际观察到损失曲线出现明显的周期性波动。解决方案有两个要么增大单卡 batch保证整体 batch 在 16 以上要么把模块内的 BN 换成 GroupNormGroupNum 设为 8 或者 16。GroupNorm 和 batch size 无关小 batch 下更稳。如果换成了 GroupNorm记得上游的 BN 保持一致不要混着用不然训练时的统计流通会出问题。5.3 低分辨率输入时上采样误差被放大这个坑是在测试 320×320 输入时踩到的。P5 特征图只有 10×10当 CrGF 把 P5 上采样到和 P3 一样大时等于把一个 32×32 区域的信息插值到一个像素级别的信息空间错位已经很严重了。此时门控模块想靠一个空间门控图去纠正这种错位就是让模型做一道特别难的题涨点效果自然大打折扣。实测下来320×320 下 CrGF 全量替换只涨了 0.5 个点跟 640 下的 2.1 个点差距很大。如果你的任务必须用低分辨率建议只在 P3 节点上使用 CrGF同时把 x_deep 的输入从 P5 换成相邻的 P4缩小尺度跨度。尺度跨度越小上采样误差越小门控能学到的空间关系越可靠。5.4 学习率对新增参数不友好最后一个坑出现在迁移学习场景。你从官方预训练权重开始训练主干和原本的 Neck 权重都已经是合理状态而 CrGF 是新加的随机初始化模块。如果整个模型用同一个学习率CrGF 在初期会被滚得比较狠容易在主干还没适应的时候就把融合路径学歪。我的做法是给 CrGF 单独设一个参数组学习率乘 0.1。配合整体 warmup让新增模块先“慢热”启动等主干微调稳定后再正常更新。实测这样操作比统一切同一个学习率能多拿 0.3~0.5 个点的提升。6. 什么场景下不建议硬套 CrGF一点实际体会CrGF 不是万灵药我最后想泼一盆冷水说说哪些场景下不建议硬套。如果你的任务目标尺度本身比较统一比如都是接近满屏的工业零件、大块的地表目标那 PAN-FPN 的传统 Concat 已经够用门控带来的信息筛选收益会被神经网络的表达能力自然吸收涨点很有限。再就是训练数据量特别少的场景比如只有几千张图新增模块反而可能过拟合把门控图学成一张“复制浅层特征”的固定模板不涨反掉。这种情况下不如先加数据增强把模型练满再说。我对 CrGF 的总体评价是它是一个结构上站得住、实现成本低、收益明确的小模块适合小目标和密集目标场景但对任务本身的目标尺度分布很敏感。我的建议是先只改 P3 节点跑 30 个 epoch 看 AP_small 的走势再决定要不要全量替换。不要一开始就图省事把整个 Neck 都换掉那样出了问题很难定位。最后再分享一个经验门控模块在 YOLO 上能不能涨点很大程度上取决于输入分辨率和训练轮次。输入拉到 640、训练轮次拉到 120 以上CrGF 的优势才能完全发挥出来。做检测改进就是这样原理再好也得在配置上给足空间不然很容易得出“模块无效”的误判。