去年我一直在做一个航拍视角的目标检测项目最头疼的不是模型不够深而是那些只有十几个像素的小目标——人眼盯着原图能找到网络却在浅层特征图里直接被当背景滤掉了。YOLOv11已经很强PAN结构的跨尺度融合也做得很成熟但真跑到密集小目标场景还是会明显感觉到信息瓶颈细节在反复下采样和上采样的过程中被“平均”掉了。后来我在复现遥感方向TGRS相关论文里的高频增强思路时发现了一个很有意思的切入点与其在主干网络上继续堆模块不如在特征融合路径上做文章。于是我把一套高频引导信道增强模块HFG-CEBHigh-Frequency Guided Channel Enhancement Block移植进了YOLOv11的Neck部分在不改动损失函数、不换数据增强策略的情况下小目标检测率有了肉眼可见的提升。这篇文章会把模块的原理、完整代码、接入方式和实测数据一次性讲清楚想直接抄作业的同学可以照着一步步来。1. 小目标为什么总被“平均”掉特征融合的信息瓶颈所有针对YOLOv11做特征融合改进的思路本质上都在回答同一个问题小目标的信息到底是在哪一步丢的。很多人第一反应是backbone太浅但其实YOLOv11的backbone已经足够提取到丰富的细节。问题主要出在Neck的跨尺度融合阶段。1.1 跨尺度融合本质上是一次低通滤波信号处理里有个很直观的类比一张图像可以拆成“低频主体”和“高频细节”两部分卷积网络每做一次stride2的下采样分辨率减半可表示的最高频分量也减半。Neck把P3、P4、P5三个尺度的特征互相融合时上采样用的是最近邻或双线性插值插值本身不会新增信息拼接后送入C3k2卷积相当于对不同来源的特征做加权混合。这种混合天然偏向低频——因为高频分量的能量占比小在统计意义上容易被低频分量“淹没”。我做了一个很简单的验证把同一批图片分别喂给backbone的P3输出和Neck融合后的P3输出对两者做Haar小波分解对比高频子带的能量。结果P3的高频子带能量普遍比P3低20%~35%。也就是说模型在融合阶段主动牺牲了一部分细节换取了语义信息的增强。对大目标来说这个交换很划算但对只有几十个像素的小目标来说这些被牺牲的高频分量恰恰是它们唯一的识别线索。1.2 常规注意力在小目标场景下为什么效果有限很多人为了提点会往Neck里塞各种注意力模块比如SE、CBAM、EMA之类的。这些模块不是没用但在小目标场景下有一个共同弱点它们构建通道权重时依赖的是整张特征图的全局统计量。SE用的是全局平均池化CBAM用的是平均池化加最大池化的组合。问题在于一张640×640的输入图像经过8倍下采样后有80×80的特征图一个小目标可能只占据2×2的像素区域。这4个像素的响应值在6400个像素的全局统计里根本翻不起浪花。哪怕小目标的特征在某个通道上很强全局平均一下也被背景稀释了。这就是为什么很多注意力模块在通用检测数据集上看着涨点一到密集小目标数据集就“失灵”的深层原因。1.3 高频信息才是小目标与背景最稳定的区分维度回到信号本身。小目标和背景的区别往往不是颜色、亮度这类低频属性而是边缘、纹理、局部反差这类高频属性。一个深色车辆停在地面阴影里它的平均亮度可能和周围路面差不多但车辆的轮廓边缘是清晰的一个行人站在树丛前面颜色特征几乎被背景吞掉但行人的头部轮廓和肩部线条是高频信号。所以接下来的思路就非常清楚了如果能在特征融合阶段把浅层高分辨率特征里最宝贵的高频分量单独提取出来用它去引导深层语义特征的通道增强让模型知道“哪些通道、哪些位置承载着细节信息”小目标的特征响应就能在融合过程中被保留下来。这就是HFG-CEB模块设计的出发点。2. HFG-CEB完整设计思路与核心公式高频先验如何注入HFG-CEB这个模块不是我凭空拍的脑袋它是把遥感领域常用的频域分析思路迁移到检测模型的特征融合路径上。遥感影像里的地物边缘极其丰富目标与背景对比度低这类问题和小目标检测高度相似。下面把模块的每个设计决策拆开讲。2.1 三个必选路径和一个可选路径模块输入有两个特征浅层高分辨率特征 (F_s) 和深层语义特征 (F_d)。整体结构分三路高频提取路径对 (F_s) 做小波分解得到水平、垂直、对角三个方向的高频子带合并成一张高频能量图。信道增强路径对高频能量图做全局统计通过MLP生成通道权重 (\beta)对 (F_d) 做逐通道重标定。这条路径是核心对应模块名里的“信道增强”。残差回流路径把 (F_s) 经1×1卷积投影后加回主路径确保浅层细节信息不会在增强过程中丢失。空间引导路径可选用高频能量图生成空间注意力图强调边缘密集的区域进一步增强细节响应。三路缺一不可。只有信道增强没有残差回流细节信息在通道维度被重新分配但位置信息没有补充只有残差回流没有信道增强那只是普通的跨层连接起不到选择性增强的作用。2.2 高频提取为什么选Haar小波而不是Sobel或Laplacian高频提取可以用很多算子最直接的是Sobel梯度或Laplacian卷积核。但我最终选了Haar小波主要出于三个原因。第一Sobel只能捕捉单一方向的梯度Laplacian是二阶差分算子对噪声非常敏感。Haar小波把信号一次性拆成低频近似和水平、垂直、对角三个方向的高频分量信息更完整。第二Haar小波天然自带2×下采样。这个特性极其关键P3特征是stride8经过一次Haar分解后变成stride16恰好和P4特征的分辨率对齐。也就是说高频提取这一步同时完成了特征对齐不需要额外做插值或池化。第三Haar小波的分解没有可学习参数对训练和部署都友好。有读者可能问那用db2、sym4这种更平滑的小波会不会更好我在实验里试过高频子带确实更干净但检测指标的提升几乎可以忽略。考虑到计算复杂度和实现成本Haar是性价比最高的选择。2.3 核心公式与设计动机给定浅层特征 (F_s \in \mathbb{R}^{C_s \times H \times W}) 和深层特征 (F_d \in \mathbb{R}^{C_d \times \frac{H}{2} \times \frac{W}{2}})模块的计算过程如下[ (LL, LH, HL, HH) \text{Haar}(F_s) ][ F_h \sqrt{LH^2 HL^2 HH^2 \epsilon} ]空间注意力图 (A) 由高频能量图经小卷积生成[ A \sigma(\text{Conv}_{1\times1}(F_h)) ]通道权重 (\beta) 通过对高频能量图做全局统计再由MLP映射得到[ \beta 1 \tanh(\text{MLP}([\text{mean}(F_h), \text{std}(F_h), \text{max}(F_h), \text{min}(F_h)])) ]最终输出[ F_{out} \beta \odot (F_d A \otimes F_d) \text{Proj}_{1\times1}(F_s) ]这里有两个设计细节值得单独说明。通道权重的激活函数选 (1\tanh) 而不是常规的Sigmoid。Sigmoid输出范围是0到1模块初始化时输出接近0.5会让深层特征在训练刚开始就被砍掉一半的能量容易造成loss震荡。(1\tanh) 的初始化输出接近1模块从一个“恒等映射”开始随着训练慢慢学习如何增强稳定性好很多。通道统计量用了四个而不是只取SE常用的全局均值。原因是高频能量图本身稀疏大量区域接近0只有少数边缘位置有大值。只取均值的话统计量会被0值稀释加入标准差和最大值后通道权重才能真正反映“这个通道里是否包含有效高频信息”。2.4 完整PyTorch实现下面给出我实际使用的代码直接用torch.reshape实现小波分解避免了额外的卷积运算。完整的模块代码可以独立运行依赖Ultralytics库中的Conv基础组件import torch import torch.nn as nn import torch.nn.functional as F from ultralytics.nn.modules.conv import Conv def haar_wavelet(x): 站在输入特征图上做一级二维Haar小波分解 B, C, H, W x.shape # 处理尺度不能被2整除的情况右侧和底部各补一行/列 if H % 2 ! 0 or W % 2 ! 0: x F.pad(x, (0, W % 2, 0, H % 2)) B, C, H, W x.shape x_ x.reshape(B, C, H // 2, 2, W // 2, 2) LL (x_[:, :, :, 0, :, 0] x_[:, :, :, 0, :, 1] x_[:, :, :, 1, :, 0] x_[:, :, :, 1, :, 1]) / 2.0 LH (x_[:, :, :, 0, :, 0] x_[:, :, :, 0, :, 1] - x_[:, :, :, 1, :, 0] - x_[:, :, :, 1, :, 1]) / 2.0 HL (x_[:, :, :, 0, :, 0] - x_[:, :, :, 0, :, 1] x_[:, :, :, 1, :, 0] - x_[:, :, :, 1, :, 1]) / 2.0 HH (x_[:, :, :, 0, :, 0] - x_[:, :, :, 0, :, 1] - x_[:, :, :, 1, :, 0] x_[:, :, :, 1, :, 1]) / 2.0 return LL, LH, HL, HH class HFGCEB(nn.Module): HFG-CEB: 高频引导信道增强模块 输入: x_deep: 深层语义特征, shape(B, C2, H/2, W/2) x_shallow: 浅层高分辨率特征, shape(B, C1, H, W) 输出: out: shape(B, C2, H/2, W/2) def __init__(self, c1, c2, ratio8, use_spatialTrue): super().__init__() self.c1 c1 self.c2 c2 self.use_spatial use_spatial # 高频能量图 - 空间注意力 if use_spatial: self.freq_conv nn.Sequential( nn.Conv2d(c1, 16, 3, padding1, biasFalse), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.Conv2d(16, 1, 1), ) # 高频统计量 - 通道权重 hidden max(c2 // ratio, 8) self.fc nn.Sequential( nn.Linear(4, hidden), nn.ReLU(inplaceTrue), nn.Linear(hidden, c2), ) # 初始化让通道权重初始接近 1 for m in self.fc: if isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.zeros_(m.bias) # 浅层特征残差回流 self.reproject Conv(c1, c2, 1) def forward(self, x_deep, x_shallow): # 1) 高频分解 _, lh, hl, hh haar_wavelet(x_shallow) freq torch.sqrt(lh ** 2 hl ** 2 hh ** 2 1e-8) # 尺寸对齐防止输入尺寸不规则导致深层特征和高频图尺寸不一致 if x_deep.shape[-2:] ! freq.shape[-2:]: x_deep F.interpolate( x_deep, sizefreq.shape[-2:], modebilinear, align_cornersFalse ) # 2) 可选的空间引导 if self.use_spatial: attn torch.sigmoid(self.freq_conv(freq)) x_deep x_deep attn * x_deep # 3) 高频统计驱动的信道增强 stats torch.stack([ freq.mean(dim(2, 3)), freq.std(dim(2, 3)), freq.amax(dim(2, 3)), freq.amin(dim(2, 3)), ], dim1) # (B, 4) beta 1 torch.tanh(self.fc(stats)).unsqueeze(-1).unsqueeze(-1) x_deep x_deep * beta # 4) 残差回流 out x_deep self.reproject(x_shallow) return out这份代码里值得注意的有三点。第一freq_conv的第一层卷积输入通道数必须是c1也就是浅层特征实际传入的通道数代码里我直接用的c1避免通道数量对不上。第二stats里的amin在极端情况下的梯度很小但如果某个通道的所有高频值都为0最小值也是0不会影响训练。第三reproject用的是Ultralytics的Conv它自带BN和激活函数对后续融合更友好。3. 完整接入YOLOv11配置文件、parse_model适配与三种接法模块写好了接进YOLOv11有两件事要处理一是要告诉Ultralytics库这个模块的存在二是在YAML配置文件里找到正确的插入位置。下面按步骤拆开。3.1 注册模块与适配parse_modelUltralytics库加载模型时会通过内部的parse_model函数解析YAML结构并实例化各层。要让自定义模块被识别需要做两处修改。第一处把模块文件放到ultralytics/nn/modules/目录下然后在__init__.py里导入from .hfgceb import HFGCEB第二处在ultralytics/nn/tasks.py的parse_model函数中找到处理特殊模块的分支追加一段对HFGCEB的处理逻辑。因为模块接收两个输入且输出通道数应该等于深层特征的通道数所以必须让解析器知道怎么处理elif m is HFGCEB: c1 ch[f[0]] # 浅层特征通道数 c2 ch[f[1]] # 深层特征通道数 args [c1, c2] args c2 ch[f[1]] # 模块输出通道等于深层通道如果你用的是较新版本的Ultralyticsparse_model的结构可能略有差异核心逻辑不变在Concat分支附近追加这个判断就行。修改后可以用一行命令验证模型能正常构建yolo modelcfg/custom/yolo11m_hfgceb.yaml3.2 在YAML中确定最佳插入位置结合YOLOv11默认的Neck结构我把模块插在自顶向下融合的“第一个回合”之后。具体的YAML片段如下# backbone 相关层省略 neck: - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 6], 1, Concat, [1]] - [-1, 2, C3k2, [512, False]] # 这层输出 stride16 的语义特征 - [[4, 12], 1, HFGCEB, [256, 512]] # 关键插入层 - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 4], 1, Concat, [1]] - [-1, 2, C3k2, [256, False]]这里有几个数字需要解释清楚。索引4对应backbone中stride8的P3特征通道数256索引12是上面C3k2输出的stride16特征通道数512。模块从索引4提取高频分量引导索引12的语义特征做信道增强输出通道数等于512尺寸与stride16对齐。之后模块输出照常上采样到stride8和P3再次拼接。这个位置的选择有依据它位于Neck信息流的最上游P5的语义信息和P4的中等细节已经完成第一次融合此时用P3的高频细节去“修正”刚融合完的特征相当于在最高分辨率信息参与之前先把语义通路“清洗”了一遍。在实验中这个位置带来的提升最大效果明显优于直接放在检测头前面。3.3 三种接法按项目成本选完整版双输入模块是最推荐的做法但改动量稍大。如果你的项目对代码改动限制比较严格还有两种低配接法。低配接法一是“单输入自增强版”。只在Concat之后把融合特征送入一个简化的HFG-CEB不对浅层单独做高频提取而是对融合后的特征做Haar分解让模块自己决定增强哪些通道。改动最小YAML里直接把它当成普通单输入模块用就行但它只能获得自身混叠后的高频分量提升效果约为完整版的70%。低配接法二是“并行分支版”。在PAN的P3、P4输出后各挂一个HFG-CEB输出再与检测头连接。这种接法适合目标极小且分布极稀疏的场景因为每个尺度的特征都能独立被高频信息增强但也意味着两个模块叠加训练开销更大小数据集上更容易过拟合。我的建议是除非有特殊的代码洁癖否则直接用完整版。Ultralytics库本身就是高度可扩展的为了一个涨点模块去改造框架并不难收益也最明确。4. 实测记录精度涨幅、训练曲线与三个翻车点模块写完之后我做了三轮完整的实验对比。这里把数据、硬件配置和训练过程中的坑一次性列出来方便想复现的读者做参照。4.1 实验配置一览实验使用单张消费级显卡完成具体参数如下项目配置硬件单张主流消费级显卡输入尺寸640×640训练轮数150 epochBatch Size16优化器SGDmomentum0.937weight_decay5e-4学习率初始0.01余弦退火预训练权重YOLOv11m官方权重数据增强Ultralytics默认开启Mosaic与MixUp对比策略固定随机种子除模型结构外完全一致数据集用了两个一个自建的无人机航拍小目标集约2.2万张80%的标注框面积小于32×32像素另一个通用检测子集作为对照组。每组实验跑两遍确保涨幅不是随机波动造成的。4.2 三组核心指标对比所有指标均在验证集上直接得出未做测试时增强。先说结论模块在小目标密集场景的涨幅最明显在通用场景涨幅温和但稳定。数据集模型mAP50mAP50-95小目标AP50无人机小目标集YOLOv11m51.830.124.6无人机小目标集YOLOv11m HFG-CEB54.132.227.4通用检测子集YOLOv11m72.348.635.5通用检测子集YOLOv11m HFG-CEB73.149.637.0无人机小目标集上mAP50涨了2.3个点小目标AP50涨了2.8个点这个提升已经不能用“噪声”来解释了。通用检测子集上小目标AP50涨了1.5个点整体涨幅略低但考虑到YOLOv11m基线已经很高仍属于有效涨点。推理开销方面的数据如下模型参数量单张推理耗时训练显存batch16YOLOv11m20.0M5.4 ms8.1 GBYOLOv11m HFG-CEB20.9M6.2 ms8.6 GB模块增加了不到1M参数推理耗时增加约15%换来小目标AP提升2.8个点这笔交易在工业项目里相当划算。如果你的项目对单帧延迟极其敏感可以考虑把use_spatial关掉推理耗时增加会降到10%以内代价是小目标AP涨幅减少约0.5个点。4.3 训练过程中的三个坑第一个坑loss直接起飞或者长时间不降。完全不用预训练权重、从零开始训的时候前几个epoch的loss波动非常大甚至会先涨一大截再开始下降。原因就是模块初始通道权重不够接近1破坏了原有信息流。解决办法是我前面在代码里写的初始化方案用 (1\tanh) 替代Sigmoid并把最后一层全连接权重初始化为接近0的数。加了这个改动之后训练曲线在第一个epoch就恢复平稳。第二个坑特征尺寸对不齐导致程序报错。输入尺寸不是标准640时某些特征图的宽高为奇数Haar小波的reshape操作会直接报错。我在模块forward里加了F.interpolate来兜底但当浅层特征尺寸和深层特征尺寸差的不是2倍关系时强行插值会引入轻微形变。建议在训练和推理时保持相同的输入尺寸不要一会儿640一会儿672。第三个坑验证集指标前10个epoch先降后升。这是新模块最常见的现象也是很多人误以为方案无效的直接原因。模块刚初始化时它相当于在原本预训练好的Neck信息流上横插了一道关卡前10个epoch模型需要重新校准特征分布验证集mAP反而可能掉0.5到1个点。跑到60个epoch之后差距开始反转最终在120个epoch以后稳定。所以评估这类改动至少要跑到100个epoch别拿30个epoch的中间结果下结论。4.4 可视化验证高频图和注意力分布训练结束后我对比了baseline和加模块模型的深层特征热力图。baseline的小目标区域响应明显偏暗目标特征被背景信息淹没加了模块之后小目标边缘位置的响应亮度显著提高而且背景中的低响应区域没有被顺带放大。这说明模块的增强是选择性的——空间注意力机制保证只有高频分量强的位置被增强通道权重机制保证只有承载细节的通道被放大。5. 这套模块的高性价比使用手册哪些场景值得用哪些别硬上HFG-CEB不是万能药。经过几轮不同数据集上的测试我对它的适用边界有了比较清晰的认识这部分写给准备直接上项目的读者。5.1 推荐的第一梯队场景最值得用的场景是遥感航拍和航拍视角的目标检测。这类数据有两个特点目标在画面中占的比例极小背景的纹理和边缘极其丰富。模块从浅层特征提取高频分量本质上是在帮模型回答“哪里才是真正的物体边缘哪些只是背景纹理”。当目标与背景的对比度很低、只能靠边缘轮廓区分时HFG-CEB的效果最突出。工业表面缺陷检测也值得试。PCB板上的缺角、布匹上的瑕疵、金属表面的划痕本质上都是局部高频异常。空间引导注意力会把这些异常位置的响应直接放大相当于给质检模型配了一个“边缘放大镜”。5.2 不推荐的场景第一类是近景大目标检测比如监控摄像头下的行人全身检测。目标本身占据几十甚至上百像素低频形状信息已经足够判别高频细节的边际贡献很小却要付出15%的推理耗时性价比不高。第二类是输入分辨率极低的应用比如接老旧的模拟摄像头输入只有352×288。这种情况下特征图本身太小P3经过Haar分解后再降一半分辨率高频信息已经被压缩得不成样子模块发挥空间非常有限。建议先用超分或简单的上采样把输入提到640再谈涨点。第三类是已经堆了大量重型组件的模型。如果Neck里已经用了形变卷积、大卷积核注意力等模块再塞HFG-CEB得到的增益会明显递减。此时瓶颈已经不在特征融合而在数据质量和模型容量。5.3 部署与工程化注意点ONNX导出方面Haar小波用reshape实现在TensorRT等推理引擎里通常能正常转换但如果你的引擎对动态shape解析不友好建议把Haar改成固定卷积实现——用4组2×2的卷积核权重写死就能把reshape逻辑隐藏掉导出更稳。INT8量化方面小波分解本身没有可学习参数量化友好。但通道权重的全连接层会变得敏感。我测试过直接PTQ量化小目标AP损失约1.2个点用QAT微调30个epoch后损失能控制在0.4个点以内。如果项目对量化精度要求高记得预留QAT的调参时间。多卡训练时模块里的切片和reshape操作会产生非连续内存张量DDP模式下偶尔会报分布式通信错误。遇到这个问题在forward开头加一行x_shallow x_shallow.contiguous()即可解决。5.4 参数消融实验记录ratio和高频通道切片是模块里最值得调的两个参数。我测过一组消融结果供参考参数设定小目标AP50结论ratio427.8权重参数更多涨幅0.4显存多占约0.3GBratio827.4默认推荐值综合最优ratio1626.6涨点明显回落不推荐全部通道做Haar27.1与取一半通道几乎无差异只取一半通道做Haar27.3显存更省推荐关闭空间引导26.9涨幅下降0.5个点ratio8和只取一半通道做Haar的组合是实测最平衡的配置。如果你的显存比较紧张优先砍通道切片而不是关空间引导。5.5 和其他涨点技巧的叠加效果HFG-CEB对Neck的注意力机制、检测头损失函数都不冲突。我在测试中还把它和一组轻量级EMA注意力叠加使用最终mAP50比单独用其中任意一个都高0.5个点左右。原因是两者作用位置不同EMA注意力在通道间建模全局依赖HFG-CEB在高频分量的引导下做局部增强互不干扰、各管一段。但要注意模块引入后模型表达能力更强在数据集小于5000张时容易过拟合。此时建议适当增大weight_decay到7e-4或者加一层Dropout验证集的涨幅会更稳定。最后分享一个我验证这类特征融合改动是否有效的小办法做“高频抑制”对照实验。把模块里的高频提取路径临时禁用只保留残差连接其他一切不变如果模型精度和baseline基本持平说明涨点确实来自高频引导这一机制而不是单纯增加了参数带来的拟合能力。想快速过滤无效改进方案的话还可以先只跑45个epoch的短训练看小目标AP是否有抬头趋势有趋势再拉长训练轮数。这套短跑验证法帮我省下了至少一半的无效实验时间。
阅读完成 · 觉得有帮助?