
前一阵子在折腾YOLOv11的目标检测主要想把小目标召回拉上来。试了不少常规套路SPPF改ASPP、加注意力模块多少有点效果但总感觉在细节保留上差一口气。后来在一个遥感场景的项目里做了个高频引导信道增强模块HFG-CEB插到特征融合路径里AP_s直接涨了三个多点而且推理速度没有明显变慢。这篇就把设计思路、完整实现和几个容易踩的坑写清楚想在YOLOv11上做特征融合改进的朋友可以直接参考。1. 整体思路为什么选“特征融合”和“高频引导”作为涨点方向1.1 特征融合路径是最值得先动刀的地方YOLOv11的结构从主干到颈部再到检测头在公开数据集上已经很强了。但实际业务数据往往不是那种“主角居中、目标大而清晰”的分布。尤其是遥感场景、无人机航拍、工业质检这类任务小目标多、边界模糊、背景纹理复杂模型跑出来的结果经常是漏检多、定位不准。很多人上来就换backbone比如把主干换成分类精度更高的网络这当然有效但换backbone对训练成本和推理速度的影响都很大有时候还会破坏原本已经调好的预训练权重。相比之下特征融合路径是性价比最高的手术点。原因很简单颈部结构连接着主干的语义信息和细节信息这一段改动一小块收益会传导到所有尺度的检测分支上。标准结构里深层特征分辨率低、语义强浅层特征分辨率高、细节多。融合时虽然有多层金字塔和自顶向下、自底向上的路径但本质上还是“拼接/相加 卷积自己去学”。问题在于如果融合前特征的通道重要程度是“一视同仁”的那细节信息很容易被语义信息带偏。HFG-CEB做的事情就是让高频响应先说话告诉融合模块哪些通道带着清晰边界和小目标纹理给这些通道更高的权重。1.2 高频信息是小目标的“救命稻草”小目标在图像上通常只有几十甚至十几个像素。这个尺度下颜色统计、形状语义都不太可靠最能区分目标和背景的往往是局部边缘和灰度突变也就是高频分量。打个比方在低分辨率图上一栋楼和一辆车可能都有类似的色块但它们的边界陡峭程度完全不同。高频算子对灰度突变极其敏感能一下子把突变位置标出来。所以把小目标检测问题部分转化为“找高频响应集中区”是一条合理路径。有人可能试过直接给输入图像做锐化或者加高通滤波器但那只在RGB层面做对网络特征图的影响非常有限。HFG-CEB是在特征图内部做这更接近网络的语义层次卷积网络的每个通道已经编码了不同的模式我们不是单纯增强某个固定频率而是利用高频能量分布重新分配通道权重。1.3 HFG-CEB模块的整体架构HFG-CEB的全称是High-Frequency Guided Channel Enhancement Block名字里的“信道”其实就是特征图的通道叫法不同而已。模块整体拆成三个部分高频提取分支先过一个可学习的深度卷积再用固定的拉普拉斯核逐通道提取高频响应信道权重生成把高频响应图的平均池化和最大池化结果拼起来通过一个1x1卷积瓶颈结构生成0到1之间的通道权重残差加权输出原始特征图乘以通道权重经过1x1卷积后再与输入做残差相加。重点在于“高频引导”这四个字。通道权重不是根据原始特征的全局均值算的而是根据高频能量算的这样对细小目标的敏感度会明显更高。残差结构则保证模块在训练初期接近恒等映射不会一上来就把网络梯度搅乱。2. 核心细节解析模块里的每个设计都不是多余的2.1 高频提取分支为什么先用可学习卷积再用固定拉普拉斯核很多人看到拉普拉斯算子第一反应是直接用OpenCV里的高通滤波不就行了但在特征图内部做情况不太一样。特征图是网络学出来的抽象表示它的“边缘”不一定对应RGB空间里的边缘可能是一种更复杂的局部模式。所以我多放了一个深度卷积在拉普拉斯前面。这个深度卷积是3x3、groups等于通道数的每个通道独立学习一组权重。它可以从数据里自适应地学出“哪种局部模式应该被当成高频来关注”然后再用拉普拉斯做固定提取。两步合在一起既保持了拉普拉斯零和、响应集中的优点又不至于被固定核的限制框死。实现上有三个坑要特别注意第一拉普拉斯核必须注册成buffer不要放进nn.Parameter。一旦放进可学习参数里训练过程中核的中心值很容易被梯度带偏最后学出来的东西就不是高频提取了模块就名存实亡。第二用F.conv2d做逐通道拉普拉斯时要把核expand成与通道数一致的形状并且设置groupsc。这样才能保证每个通道独立卷积不会跨通道混叠。普通卷积会把不同通道的高频信息“抹匀”失去了逐通道的独立性。第三拉普拉斯输出是有正有负的不是标准的ReLU友好的分布。后面接Sigmoid的MLP能起到一定的数值压缩作用但如果上游BatchNorm没有收敛好拉普拉斯响应可能会在训练初期出现较大的数值波动这时候需要在代码里对响应做一次缩放或裁剪防止梯度异常。2.2 信道权重生成平均池化和最大池化缺一不可高频响应图拿到之后最常见的做法是全局平均池化成c维向量然后接全连接层。但我实际调试中发现只用平均池化效果并不好。原因是平均池化代表的是“这个通道所有位置的高频能量均值”它稳定但容易被大量弱响应稀释而小目标只在图中一小块区域产生强边缘平均完之后它的贡献变得很小。所以我把最大池化也加进来了。最大池化代表“最强的一处边缘响应”对小目标非常敏感但只取一个点又容易被噪点干扰。平均池化负责整体稳定性最大池化负责捕捉峰值信号两个拼在一起正好互补。权重生成部分用的是1x1卷积构成的瓶颈结构不是全连接层。其实在1x1输入上1x1卷积和全连接是等价的但用卷积写起来更自然通道数变化也更直观。hidden维度我取的是max(c // 16, 8)也就是说如果通道特别小不会压到低于8个维度避免信息损失太厉害。这里的reduction是个可调参数。我的经验是如果你的模块放在P5这种低分辨率层建议把reduction从16调到8因为高层通道数大压到c/16信息损失有点多。放在P3/P4层时reduction16就够用。2.3 残差结构模块不听话的时候怎么稳住训练模块输出如果直接替换掉原特征图网络在最开始几轮非常容易震荡。所以我加了一条残差路径输出 输入 1x1卷积(输入 * 通道权重)。这里有两个细节值得说。第一1x1卷积的权重初始化成零。这样训练刚开始时模块输出等于原输入不会干扰已经初始化的主干和检测头训练几十轮后网络再慢慢“放开”这个模块的增益。第二如果输入输出通道数不一致残差路径上需要一个1x1卷积把输入对齐到输出通道。目标检测的neck里一般通道数保持不变但写成兼容版本总没错。我自己实际测试过不做零初始化直接硬训loss曲线在最初200轮会明显抖动做了零初始化后曲线平滑很多。对于想快速验证模块效果的人来说这个初始化技巧能省下不少排查时间。2.4 模块放置位置不同效果差别很大HFG-CEB不是放在哪里都能涨点的。我在自己的数据集上试了四个位置结论差异非常大位置AP3层C2f之后这个位置分辨率高、小目标信息最丰富位置BP4层Concat之后这里是语义信息和浅层细节融合的第一个交汇点位置CP5层Concat之后分辨率低高频信息已经被压缩得差不多了位置D每层Concat后都加计算量上涨最明显。最终保留的是P3和P4各加一个模块收益最大。P5那层加上几乎不影响小目标AP只有大目标指标微涨。原因不难理解P5特征图空间分辨率太低高频细节在多次下采样里丢得差不多了模块有点“无米下锅”的感觉。如果你的数据集以大目标为主模块加在P5也许还有一点用但小目标是主力场景时优先放在P3/P4。3. 实操过程完整代码、集成方法和训练配置3.1 HFG-CEB模块的PyTorch完整实现下面的代码是我实际在项目里用的版本PyTorch 2.0以上都能直接跑。代码里注释写得比较细照着敲就行。import torch import torch.nn as nn import torch.nn.functional as F class HFG_CEB(nn.Module): High-Frequency Guided Channel Enhancement Block 输入输出通道数可以不同残差路径会自动对齐。 构造函数第一个参数是输出通道数c2reduction是通道缩减比例。 实际插入时一般保持输出通道数和输入一致。 def __init__(self, c2, reduction16, k3): super().__init__() hidden max(c2 // reduction, 8) # 逐通道高频模式学习3x3深度卷积不跨通道混合 self.pre_conv nn.Conv2d( c2, c2, kernel_sizek, paddingk // 2, groupsc2, biasFalse ) # 固定拉普拉斯核注册为buffer不允许训练改变 laplacian torch.tensor( [[0., 1., 0.], [1., -4., 1.], [0., 1., 0.]], dtypetorch.float32 ).reshape(1, 1, 3, 3) self.register_buffer(laplacian, laplacian) # 信道权重生成2c - hidden - c self.fc nn.Sequential( nn.Conv2d(c2 * 2, hidden, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(hidden, c2, 1, biasFalse), nn.Sigmoid(), ) # 输出变换和残差对齐 self.out_conv nn.Conv2d(c2, c2, 1, biasFalse) # 如果放在通道数变化的位置这里改成对应的对齐卷积即可 self.align nn.Identity() # 关键初始化让新增路径初始时几乎不改变输出 nn.init.zeros_(self.out_conv.weight) def forward(self, x): identity self.align(x) # 1) 深度卷积增强局部模式 x_hf self.pre_conv(x) # 2) 拉普拉斯逐通道提高频 b, c, _, _ x_hf.shape lk self.laplacian.expand(c, -1, -1, -1) x_hf F.conv2d(x_hf, lk, padding1, groupsc) # 3) 高频响应统计量均值 最大值 hf_avg F.adaptive_avg_pool2d(x_hf, 1) hf_max F.adaptive_max_pool2d(x_hf, 1) z torch.cat([hf_avg, hf_max], dim1) # 4) 生成高频引导的通道权重 w self.fc(z) # 5) 加权输出 残差 out self.out_conv(x * w) return identity out启动初始化里把out_conv权重全部置零配合残差结构训练初期模块完全不对特征图做改变等网络自己学起来。这个细节是我踩了几次坑之后加上的效果很稳。3.2 把模块接入YOLOv11模型配置实际接入YOLOv11有两条路取决于你的代码是自己魔改过的还是官方开源的原始结构。两种我都说。第一种直接改模型配置文件。先把HFG_CEB这个类放到模型模块的目录下比如新建一个hf_ceb.py然后在模型解析的注册表里把HFG_CEB加进去。不同代码的注册方式不一样最核心的是让解析器在遇到yaml里的HFG_CEB字符串时能找到对应的类。在head部分插入一行示意如下head: - [-1, 1, Conv, [256, 3, 2]] - [-1, 1, HFG_CEB, [256, 16]] # P3 高频引导增强 - [-1, 1, Concat, [-1, 3]] - [-1, 1, C2f, [256, 3]]这里第一个参数256是模块的输出通道数第二个是reduction。模块内部的输入通道数是从特征图的shape动态拿的所以在通道不变的neck里插入不会出问题。如果你的代码版本解析参数的习惯是把第一个参数当输入通道传那写的时候把类签名调整一下即可原理一样。第二种不动yaml直接在模型forward里调用。如果你用的是一个深度魔改过的代码解析机制比较复杂那就在neck的特征融合处手动调用模块from hf_ceb import HFG_CEB hf_module HFG_CEB(c2256, reduction16) x hf_module(x) # 放在Concat之后、C2f之前这种方法最稳妥坏处是每次改网络结构都要动Python代码而不是配置文件但胜在逻辑清晰适合调试阶段。3.3 训练配置与推荐的实验顺序我在模拟项目X的数据集上用的配置是输入尺寸640x640优化器SGD初始学习率0.01、momentum 0.937、weight_decay 5e-4epochs设置150batch size根据显存调节。8G显存跑n模型可以到batch 16跑l模型就只能放4到8。如果目标是更大的小目标检测建议把输入尺寸提到960或1024小目标在特征图上的可学习信息会多不少。代价是训练时间变长GPU显存需求量也上来了可以先在640下完成消融对比最后上大分辨率。做对比实验时我强烈建议先用n模型跑不要在l或者x模型上直接验证。n模型参数少、单轮速度快能快速看出模块到底有没有正收益。等确认方向对了再往s/ml上迁移。另外一定要固定随机种子每个配置至少跑两次取平均不然差0.1个点很难判断是模块的功劳还是随机性。3.4 我实际用到的消融实验与结果下面这张表是我在自建的一个无人机航拍小目标数据集上做的对比结果负样本和正样本比例接近真实作业场景共3000多张图主要目标是车辆和船舶两类。训练细节保持完全一致唯一的变量是是否加入HFG-CEB模块配置mAP50mAP50-95AP_s小目标推理耗时单张YOLOv11n baseline0.7860.5280.3146.8msYOLOv11n HFG-CEBP3/P40.8050.5510.3517.0msYOLOv11s baseline0.8120.5560.34211.5msYOLOv11s HFG-CEBP3/P40.8290.5790.38111.8ms可以看到mAP50的涨幅在一个点左右但小目标AP_s涨幅能到3.7个点正好印证了模块对细节保留和微小目标的作用。推理耗时增加在3%以内对工程落地是可以接受的。如果你手里的数据是大中目标为主这个模块带来的收益通常只在0.2到0.5个点之间不用抱太高期待。它更像一个“细节放大器”增益大小取决于数据本身有多少细碎信息可以放大。4. 常见问题与排查技巧实录4.1 加了模块之后loss变成NaN这个问题我遇到过一次触发条件是在混合精度训练下拉普拉斯响应值的数值范围偏大反向传播时梯度溢出。排查思路很直接先看loss是第几百轮开始出问题的把AMP关掉试一轮如果正常基本就是数值溢出。解决办法有两个。一是对拉普拉斯输出做完归一化最简单的做法是乘以一个固定缩放系数0.1让高频响应回到相对温和的数值区间。二是在out_conv上保持零初始化训练前期模块不参与输出epoch开到一定数量后再放开。两个方法可以同时用。这个坑不是模块特有的很多带固定卷积核的特征增强模块在AMP下都会撞上大家遇到可以先往数值范围这个方向想。4.2 加了模块之后不掉点也不涨点同样硬件、同样epoch加了模块前后指标几乎一样最常见原因不是模块没用而是数据集里小目标占比太低了。我统计过小目标面积小于32x32像素占比低于5%时模块的收益会被大目标指标稀释在噪声里根本看不出来。想让AP_s有可感知的提升小目标占比最好在15%以上。还有一个容易被忽略的点如果数据增强里mosaic、copy-paste的力度太狠目标被切碎、缩放后高频信息本身已经被破坏了模块自然无从发挥。这种情况下先关闭部分数据增强看看模块收益是不是就出来了。4.3 显存和推理速度涨太多怎么办显存方面在P3和P4各加一个模块batch8、输入640的前提下显存增加大概300到400M属于正常范围。推理耗时大约增加2%到5%一般都能接受。如果显存实在紧张最简单的做法是把pre_conv去掉只保留拉普拉斯提取和MLP权重生成显存还能再降一半。特别提醒不要在P2这种超浅层特征图上加模块。P2分辨率太高深度卷积加拉普拉斯在这么大的特征图上的计算量会明显上升而收益非常小性价比极低。4.4 一个验证模块有没有起作用的小技巧训练之前先用一个batch的输入数据跑一次前向把每个HFG_CEB输出的拉普拉斯响应统计量打印出来看看是不是零均值、有正负尖峰的分布。如果统计量全是0说明前置深度卷积的初始化或上游的BatchNorm把特征“压平”了模块实际上没有在做事。这种情况下可以先把它放到网络更浅的位置再测或者把pre_conv换成独立初始化的卷积不要让它继承一个可能已经饱和的分布。这个小习惯能帮你快速排除“模块存在但没起作用”的状态省下整轮训练的时间。我个人实际操作中的体会是HFG-CEB真正的价值不是“多加了一个模块”而是把高频细节从直觉层面变成了模型可学习的通道权重。这个思路在遥感航拍、工业缺陷检测这类高频信息丰富的任务上很吃香但通用大目标场景收益没那么夸张。后续想扩展的话把它用在语义分割或者超分辨率的特征融合里理论上也成立。最后再多说一句新模块第一次试验时先在小模型、小数据集上跑通把数值曲线看明白再全量训练能省非常多时间。