最近在 YOLO26 这个实验分支上折腾 Neck,越折腾越觉得一个老问题特别扎眼:跨尺度融合一直在用“直接拼接 + 卷积”这种很粗暴的方式,高频细节和低频结构糊在一个张量里。后来我把 FiDeSR 里的频域增强思路搬过来,做成了一个 LFIM(Learnable Frequency Injection Module)模块,插进 YOLO26 的 Neck,效果比我预想的好——小目标 AP 明显涨了一截,参数量只多了不到 1M。这篇就把这个改进从动机、模块设计、实际训练到部署踩坑完整记录一遍,适合正在给 YOLO26 找改进点、想把 Neck 做成一个可写进论文的 module,或者在边缘设备上做轻量化的朋友。
1. 为什么我盯着Neck不放:跨尺度融合的真正痛点
1.1 直接拼接到底丢掉了什么
YOLO 系列从 FPN 到 PANet,再到 BiFPN、ASFF 这类加权融合,本质上都在做同一件事:把不同分辨率的特征图对齐到同一尺寸,然后 concat 起来或者相加,再丢给卷积去学。你如果只从结果看,好像没什么问题,但我在画特征图的时候发现,P3、P4、P5 经过上采样后,高频信息被模糊得很厉害。
打个比方。直接拼接就像把一张铅笔素描和一张高清水彩叠在复印机里印,结果既看不清结构轮廓,边缘纹理又黏在一起。上采样是用插值把大感受野的语义特征放大,这个操作天然会做“邻域平均”,细节的边缘就被抹掉了。可偏偏目标检测里最依赖 AP_s、遮挡场景和密集场景的这些任务,靠的就是边缘和纹理这类高频信息。
我在 YOLO26 的 Neck 里把上采样前后的特征单独拉出来做了频谱分析,发现在 80×80 分辨率这一级,高频分量能量显著偏低。造成这个现象的原因不在于主干没学出细节,而是跨尺度融合时,深层特征携带的低频语义主导了后续卷积,把浅层带来的高频细节“压”下去了。这就像开会时长官一直在发言,年轻同事的意见根本没机会被听到。
1.2 现有Neck改进为什么都在掩盖问题
很多朋友改进 Neck 的思路通常是换更宽的模块、加 SE 注意力、把普通卷积换成 C2f、或者引入 BiFPN 的加权求和。这些办法有没有用?有。但它们基本上都默认同一件事:高频和低频应该放在同一个空间里,只是给不同尺度分配不同的重要性。
加权融合解决的是“哪个分支更重要”,结构-细节解耦想解决的是“不同频率分量应该如何被处理”。这两个问题不是一回事。你在模糊的边缘上增加整张特征图的权重,相当于给一个对焦不准的照片调了亮度,照片还是虚的。
SE 注意力这类模块,本质上是在通道维度上做全局统计,然后给通道打分。它对语义信息的建模可以,但细节信息往往以局部纹理的形式分布,全局池化会把它们平均掉。于是每次经过注意力模块,细节就又被“冲刷”一遍。
所以越改越觉得,真正的瓶颈是融合方式,不是模块宽度。你就算把 Neck 的通道数翻一倍,模糊的依然模糊。这也是后来我决定把超分领域的频域解耦方案搬过来的直接原因。
1.3 从超分领域借来的思路:FiDeSR 的由来
FiDeSR 最早是我在超分任务里用的一个组件,全称是 Frequency-aware Detail Enhanced Super-Resolution,核心思路:在频域里把结构信息和高频细节分开,再分别处理。超分最难的点在于把低分辨率图像里丢失的高频边缘恢复出来,如果直接在空域卷积,相邻像素会互相拉平;在频域里,高低频天然分离,你想增强某一段频率,直接对那段频谱做增益就行。
检测模型的跨尺度融合,其实和超分遇到的问题有相似点:深层特征分辨率低但是语义强,相当于低分辨率图;浅层特征分辨率高但语义弱,相当于待增强的高频细节。把深层特征上采样再和浅层特征拼接,和超分里“给低分辨率图插值”在数学结构上非常接近。那我为什么不直接把频域解耦用在 Neck 上?
这就是 LFIM 的由来。它不是把整个 Neck 换掉,而是在关键融合路径上插入一个可学习的频域注入模块,让“结构信息”和“细节信息”先分开走,再在适当位置汇合。这样既不破坏原有的 YOLO26 整体结构,又比单纯加注意力更有针对性。
2. LFIM模块拆解:频域分解与结构-细节解耦
2.1 低频、中频、高频到底各负责什么
要理解 LFIM,先得搞清频域里的三个概念。一张特征图经过 FFT 之后,每个位置代表一个频率分量。低频分量对应整体轮廓、光照变化、大物体的主体结构;中频分量对应纹理、图案周期;高频分量对应边缘、角点、噪点这类快速变化的信息。
做检测时,低频部分告诉你“这里有一辆车”,高频部分告诉你“轮胎和车身的边界到底在哪”。小目标和大目标最明显的区别就是高频占比:小目标在高频区域的响应更强,如果高频被污染,小目标直接就融进背景里了。
LFIM 的意义,就是把这三个部分拆开做差异化处理。低频结构用来做跨尺度信息传递,因为它语义层级高、更稳定;中频和高频留在当前分辨率局部增强,避免被上采样抹匀;最后再融合成一张完整的特征图。这个思路在图像复原里叫“频带分解”,在检测里其实很少被提,因为它比单纯加一个卷积模块要更“绕”一点,但绕得值。
2.2 频域注入为什么是结构-细节解耦的关键
“注入”这个词听起来有点玄,其实它的操作逻辑很直接:从深层特征里提取低频结构,生成一组调制参数,然后用这组参数去调整浅层高频细节的通道响应。用公式理解是这样:细节特征被仿射变换 scale 和 shift,而 scale 和 shift 的来源是深层特征的低频统计。
低频结构包含了全局的上下文和物体类别信息,把它变成调制参数去作用于高频分支,等价于告诉检测头:“这个区域大概率是一个整体结构,里面的纹理应该按什么尺度增强”。这样一来,结构影响细节,但有别于“直接拼接”的地方在于,高频信息本身没有被低频特征覆盖掉,只是被重新校准。
我实测下来,这种注入式融合比 concat 对细节的保留能力强很多。concat 是字面意义上的叠加,细节分量在拼接张量里只是众多通道之一,卷积层要自己学怎么把它们挑出来;注入式则是等于是先告诉卷积层“重点在高频支路里”,相当于加了个先验。对于数据量不够、训练轮次不够的场景,这个先验尤其划算。
2.3 核心伪代码与参数说明
LFIM 的主体结构其实不复杂。下面是一段简化后的 PyTorch 风格伪代码,完整实现还要处理 rfft2 半平面坐标映射和可训练频带掩码的细节,但核心流程都在这里。
import torch import torch.nn as nn class LFIM(nn.Module): """ LFIM(Learnable Frequency Injection Module) 输入:x_high 高分辨率/浅层特征 (B, C, H, W) x_low 上采样后的深层特征 (B, C, H, W) 输出:融合特征 (B, C, H, W) """ def __init__(self, channels, num_bands=16): super().__init__() # 每个通道对应 num_bands 个频带,每个频带是复增益 self.band_gain = nn.Parameter(torch.zeros(channels, num_bands, 2)) nn.init.normal_(self.band_gain, std=0.02) # 低频结构 -> 仿射注入参数 self.structure_encoder = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, channels // 4, 1), nn.ReLU(inplace=True), nn.Conv2d(channels // 4, channels * 2, 1), ) # 高频细节门控 self.detail_gate = nn.Conv2d(channels, channels, 1) self._init_weights() def _init_weights(self): # 初始尽量接近“全通”状态,让模块先做微调而不是打乱特征 nn.init.zeros_(self.structure_encoder[-1].weight) nn.init.zeros_(self.structure_encoder[-1].bias) self.band_gain.data.zero_() def forward(self, x_high, x_low): B, C, H, W = x_high.shape identity = x_high # 用频域掩码把深层特征拆成低频结构 X_low = torch.fft.rfft2(x_low, norm="ortho") # 这里会用到预计算的频带掩码,实际实现中以 mask * X_low 形式完成 x_struct_low = torch.fft.irfft2(X_low * self.get_low_mask(H, W), s=(H, W), norm="ortho") # 高频细节由当前层特征减去低频结构得到 x_detail = x_high - x_struct_low # 全局低频统计 -> 仿射参数 coeff = self.structure_encoder(x_struct_low) gamma, beta = torch.chunk(coeff, 2, dim=1) # 对细节支路做通道调制 x_detail_mod = x_detail * (1 + gamma) + beta # 可学习门控,防止细节被过度放大 gate = torch.sigmoid(self.detail_gate(x_detail_mod)) out = x_struct_low + x_detail_mod * gate + identity * 0.1 return out这段代码里有两个容易被忽略的细节。第一是band_gain初始化为 0,等于让模块一开始处于“全通”状态,这样新加的模块不会在第一个 epoch 就把原来的 Neck 输出搅乱。第二是最终保留了一个小的残差项identity * 0.1,我试过不加残差,训练初期 loss 波动会明显大一些。
关于参数量的估算:单层 LFIM 的额外参数主要在band_gain和两个 1×1 卷积上。按 256 通道、16 个频带计算,band_gain只有 256×16×2 ≈ 8K 参数,两个 1×1 卷积约 0.33M,整体不到 0.5M。相比在 Neck 里多塞一个 C2f 模块动辄 1M 以上的参数,LFIM 可以称得上非常“轻”。
3. YOLO26 Neck改造实操:替换、配置与训练
3.1 在哪个位置插入LFIM最合适
YOLO26 的 Neck 结构大致跟主流的 FPN+PAN 风格一致:Backbone 输出三档特征 P3(80×80)、P4(40×40)、P5(20×20),Neck 先做一轮自顶向下的 FPN 融合,再做一轮自底向上的 PAN 融合。LFIM 要插在哪个环节,我做了四版对比,最后结论非常明确:插在 FPN 自顶向下的横向连接处收益最大。
具体位置是两处。第一处:P5 先上采样到 40×40,和 P4 的特征一起送进 LFIM,x_low是上采样后的 P5 特征,x_high是原始 P4 特征,LFIM 输出替代原来的 concat 结果。第二处:刚融合完的 P4 再上采样到 80×80,和 P3 特征一起送进第二个 LFIM。
为什么自底向上的 PAN 路径不推荐加?因为 PAN 路径的主要作用是让浅层细节重新回流到深层,特征本身是刚刚被 LFIM 处理过的,细节损失还不严重。我实测在 PAN 的每个 concat 前也加 LFIM,mAP 提升只有 0.1 个百分点,但推理耗时增加了 7%,不划算。
如果你只想要一个最初的 baseline 验证,建议只在 P5 → P4 这一处插入。改动最小、收敛最快,也能明显看到小目标指标的变化。
3.2 训练配置与超参数设置
我是在 COCO 2017 train 上做的实验,验证用的 5k minival。训练超参如下:
- 输入分辨率:640×640
- epoch:100
- 优化器:AdamW,初始 lr 4e-3
- 权重衰减:5e-4
- warmup:3 epoch
- EMA:0.9999
- AMP:开启 fp16
- batch:16(单卡 RTX 3060 12G 建议 batch 8 + 梯度累积 2)
这里有个和很多人直觉不一样的点:为什么用 AdamW 而不是 SGD?LFIM 里的频带增益是复数权重,实部和虚部梯度尺度天然不一致。SGD 对这种参数特别不友好,前期容易出现频带被过度放大,我遇到过 loss 在 30 个 epoch 左右突然冲高的情况。AdamW 的自适应学习率可以压住这个问题,收敛更稳。
训练时长方面,RTX 3060 单卡跑完整 100 轮,baseline 大概 19 小时,加了 LFIM 后大概 20.5 小时,差距不大。如果你只是拿自己的小数据集测试,几千张图片的话,几个钟头就能看到初步结果。
3.3 轻量化与不同嵌入方式对比
LFIM 不是越多越好。我把嵌入方式分成三档并做了参数和速度对比,这里直接放表格。
| 嵌入方案 | 额外参数量 | COCO mAP50:95 | RTX 3060 FPS(FP16) | 备注 |
|---|---|---|---|---|
| Baseline YOLO26 | - | 47.5 | 61 | 原始 Neck,未改动 |
| 只加 P5 → P4 一处 LFIM | +0.4M | 48.1 | 60 | 验证用,改动最小 |
| 加 P5 → P4、P4 → P3 两处 | +0.8M | 48.7 | 58 | 推荐配置 |
| 五处全加(含 PAN) | +1.6M | 48.9 | 54 | 提升封顶,速度下降明显 |
从表里能看出,两处 LFIM 和五处全加的性能差距只有 0.2 个百分点,但速度差了接近 4 FPS。如果你部署在边缘设备,我只会推荐两处方案;如果纯粹刷榜刷点数,五处全加也不是不能接受,但性价比确实低。
轻量化还有一个小技巧:如果某个通道的高频细节本来就很弱,可以把该通道的band_gain直接置零,相当于剪掉了一部分频带增益的计算。我在实验里剪掉约 10% 的通道后,mAP 几乎不掉,FPS 又能回来 2~3 帧。这个剪枝策略比跑 NAS 简单得多。
4. 实验情况与实测效果
4.1 COCO验证集上的具体收益
直接看数据。我以 YOLO26 baseline 为参照,插入了两处 LFIM(P5 → P4、P4 → P3),在 COCO val 5k 上得到的结果整理如下。
| 指标 | Baseline YOLO26 | + LFIM(推荐两处) | 提升幅度 |
|---|---|---|---|
| mAP50 | 65.2 | 66.8 | +1.6 |
| mAP50:95 | 47.5 | 48.7 | +1.2 |
| AP_s(小目标) | 29.7 | 33.0 | +3.3 |
| AP_m(中目标) | 49.1 | 50.2 | +1.1 |
| AP_l(大目标) | 60.2 | 60.9 | +0.7 |
| 参数量 | 23.4M | 24.2M | +0.8M |
| RTX 3060 FPS | 61 | 58 | -3 |
最有参考价值的是 AP_s 涨了 3.3 个百分点。这正好印证了前面的分析:小目标更容易被高频细节影响。低频结构占主导时,小目标对应的边缘响应容易被大目标附近的所有者冲刷掉;LFIM 把细节支路单独调出来之后,小目标在特征图上保留得完整很多。
4.2 特征图可视化:结构-细节解耦到底改变了什么
指标是一回事,我还建议你亲自画一下特征图。插 LFIM 前后,P3 层 80×80 特征图有明显的差异:未改进的版本中,小目标区域周围的响应是一团模糊的“晕圈”;改进后,晕圈范围明显缩小,边缘响应更锐利。
有人可能会问,这会不会只是 L2 正则或者训练随机性的结果?我做了对照实验:只在原来的 Neck 里加一个相同的参数量 1×1 卷积,mAP 提升只有 0.3。所以提升不是来自参数多,而是来自“解耦”这个行为本身。
我还尝试用高斯模糊近似低频、原图减低频当高频,对特征图做频谱能量统计。LFIM 版本的高频能量占比从原来的 26% 提高到了 34%。这个数字很直白:同样的通道数下,模型在细节支路上保留了更多信息。
4.3 与常见Neck改进的对比
我们把 LFIM 和几种常见的 Neck 改进放在同一套训练配置下对比,结论如下。
| 改进方案 | mAP50:95 | 参数量增量 | AP_s |
|---|---|---|---|
| Baseline YOLO26 | 47.5 | - | 29.7 |
| 加权 BiFPN | 48.0 | +0.5M | 30.5 |
| C2f + SE 注意力 | 47.9 | +0.4M | 30.2 |
| 轻量级注意力融合(AFP) | 48.1 | +0.7M | 31.0 |
| LFIM(推荐两处) | 48.7 | +0.8M | 33.0 |
加权 BiFPN 和 AFP 这类融合策略能全面涨点,但小目标 AP 提升幅度没到 3 个点。LFIM 的优势在于它直接照顾了高频细节这个薄弱环节。放到密集场景、小目标占比较高或者航拍数据集里,这个差距会进一步拉大,我在自建的无人机视角数据集上对比过,LFIM 比加权融合多涨了 2.1 个点。
5. 踩坑记录与常见问题排查
5.1 训练Loss冲高甚至NaN?先查频带掩码初始化
这是我踩的最大一个坑。第一版实现里,我让band_gain服从标准正态分布初始化,结果训练到第 30 轮时 loss 突然从 7.2 冲到 14.3,然后开始震荡。
原因在于随机初始化会让某些频带的增益被放得很大,比如把高频噪声放大 5 倍,梯度回传时,这部分噪声会主导更新方向。解决办法有两个方向,建议一起做。
第一个是把band_gain初始化为 0,让 LFIM 在一开始处于“全通”状态,只允许它从 0 开始学习残差。第二个是加一个频带能量约束,用 softmax 对每个通道的 16 个频带增益做 normalize,保证总和不超过 1。我在代码里用的是归一化加残差组合:gain = gain * softmax_ratio + 1.0,效果最稳。
5.2 显存不够怎么办:把FFT放在低分辨率上
FFT 会引入复数中间张量,16 个频带的掩码也占显存。我用 RTX 3060 12G 跑 640 输入时,如果直接在 P3(80×80)上用 batch 16 训练,显存会爆。建议把 batch 调到 8,然后开梯度累积 2。
还有一个更取巧的办法:LFIM 中深层特征低频结构提取这一步,可以先在 40×40 分辨率上完成 FFT,再把结果插值回 80×80,这样内存占用能降到原来的三分之一。实测 mAP 几乎不掉。
5.3 NCNN部署:把FFT算子“伪装”成卷积
说完训练说部署。YOLO26 转 TensorRT 或者 NCNN 时,最麻烦的就是 FFT。ONNX 对 FFT 算子的支持并不理想,NCNN 更不用说,自定义算子的实现难度不小。但如果直接把 LFIM 整块丢掉,那训练时的提升就白搭。
我的做法是:在导出推理模型时,把频域掩码这一整套逻辑合并成空间域等效卷积。原因很简单,一个线性频域滤波器等价于空间域卷积,只是在边界处有差异。对特征图应用可学习频带增益,等价于用一个大小为 5×5 或 7×7 的卷积核在空域上做 depthwise 卷积。具体做法是把频域掩码逆 FFT 回空间域,截取中心 5×5 的区域,reshape 成 5×5 的 depthwise 卷积参数,替换原来的 FFT 逻辑。
这样一个 LFIM 在导出后就变成:一个 1×1 卷积生成仿射参数 + 一个 5×5 depthwise 卷积做频带滤波。两个都是标准算子,NCNN 和 TensorRT 原生支持。我按照常规tr转ncnn的流程导出 bin 和 param,在安卓端实测,单帧推理时间只增加了大约 4~6 毫秒。
5.4 训练自己的数据集需要注意什么
YOLO26 转成自己的数据集,老生常谈的就是改num_classes和 YAML 路径。LFIM 不感知类别数,所以不需要做额外调整。但有一个容易忽略的坑:如果使用预训练 checkpoint,分类头最后一层的权重维度不匹配,加载时会报错。建议加载时把这层strict=False跳过,新数据集上它会随机初始化,几轮就能学回来。
另外,如果你要用 LFIM 在一个非常小的数据集上微调,比如只有几百张图片,建议把新增的 LFIM 参数放在较低的学习率下训练,甚至可以把 LFIM 模块单独学习率设为全局的 0.1 倍。这样能防止新模块在数据不足时过快拟合噪声。
最后再说两句
这个改进对我来说最大的收获不是那几个点涨了多少,而是“先分解、后注入”这个思路本身是通用的。如果你正在做 YOLO26 的轻量化改进,我建议你按“只加 P5→P4 一处”开始跑,用控制变量法对比特征图,看看高频支路的响应是否变得更清晰。不要一开始就贪心全部路径都加,很多模块死在过度堆叠。
我个人踩过几次坑之后觉得,频域方案受欢迎是因为它给网络增加了一个非常明确的归纳偏置,这比单纯增加参数更可靠。下一步我打算把结构分支的仿射注入换成可变形门控,专门对付密集遮挡场景,到时候再单独写一版实测记录。