ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLO26 Neck改进:频域注入式特征融合实现结构-细节解耦

YOLO26 Neck改进:频域注入式特征融合实现结构-细节解耦 前一阵我把 YOLO26 的 Neck 随手梳理了一遍发现自己的思路一直在“拼接、相加、加权求和”里打转FPN 出来加 PANetPANet 出来加 BiFPNBiFPN 之后又来各种注意力融合。拼来拼去心里总觉得不太对——不同尺度的特征图里到底哪些信息才值得跨尺度传递后来我干脆把整个融合过程搬到频域重新想了一遍做出来 FiDeSR 这个系列里的 LFIM 频域注入式 Neck 改进。中文说穿了就是四个字结构-细节解耦。这篇把我踩过的坑、模块的完整拆解、在 YOLO26 里的插入位置以及那些“能涨点但部署时想骂人”的问题一次性讲清楚适合正在折腾 YOLO26 改进、尤其是对小目标和细长目标检测不满意的人阅读。1. 跨尺度融合的真正瓶颈直接拼接到底丢掉了什么1.1 从 FPN 到 PANet 再到 BiFPN为什么加来加去还是“拼”YOLO 系列发展到现在颈部网络始终没离开 FPN 的思路。FPN 做了一件事把深层的语义特征自上而下传给浅层解决浅层特征“只有细节、没有语义”的问题。PANet 又补了一条自下而上的路径让浅层的细节也能往上走。BiFPN 看着更精致给每条融合边学权重但本质还是同一件事——把不同分辨率的特征图按某个权重加到一起。到了 YOLO26 这一代Neck 骨架依然没有跳出“空间域加权融合”的范畴。我并不是否定这些结构它们在各自时代都是很优秀的方案。但从信号处理的角度看concat 和 add 都只是一个非常朴素的算子把两组特征视为同等地位让后续卷积自己去“消化”它们之间的差异。问题在于一张特征图经过多次下采样之后高频分量边缘、纹理、细节已经被低通卷积滤得差不多了浅层特征虽然高频信息丰富但语义置信度低、噪声也多。把这两类性质差别极大的特征直接拼起来等于让一个小卷积核去同时处理“方向相反”的两类信息——既要保留高频细节又要提炼低频结构最后往往两头都顾不好。1.2 空间域拼接的三个隐性损失语义稀释、细节淹没、混叠伪影第一是语义稀释。深层特征图分辨率低想传给浅层就得先上采样。无论用双线性还是最近邻上采样新增的那些像素都是“插值猜出来的”它们并不代表真实的语义信息却会在拼接时占据一部分通道容量。融合结果里真正有用的深层语义反而被稀释。第二是细节淹没。浅层特征的高频细节在多次下采样过程中被不断削弱。等到了 Neck 融合时深层特征图里的高频成分几乎归零你就算想让它补充浅层的细节手里也没有可用的高频分量。很多改进需要额外加细节分支就是在弥补这个损失。第三是混叠伪影。不同分辨率特征图的频率成分不一样上采样以后再拼接低频和高频被强行叠在同一个空间位置上。后续卷积一开始拿到的是一锅“频率混着”的特征要花很大代价才能再分开。我做过一个简单实验把两路特征 concat 后用 3x3 卷积融合再对输出做 FFT 看频谱会发现融合后的频谱能量分布非常乱尤其是高频带明显多出一圈不应该出现的旁瓣。这三个损失其实就是“结构-细节解耦”要解决的核心问题结构信息决定目标在哪、大致是什么细节信息决定边界有多准、小目标能不能被检出。检测头的分类分支主要靠结构回归分支主要靠细节。把它们捆绑在一起融合等于让一个算子同时干两件方向相反的事。1.3 一个反直觉的现象高频信息在浅层但浅层特征并不“干净”我调试特征图时发现一个有意思的现象对同一张输入图浅层特征图的高频能量非常强但它的高频里既有真实边缘也有大量背景纹理和噪声深层特征图经过多次降采样高频几乎被滤干净但剩下的低频部分非常“干净”语义置信度高。所以跨尺度融合的真正价值不是肤浅的“补充细节”而是两步先把浅层高频里的噪声洗干净再把真实细节注入深层同时把深层的干净结构回传给浅层。这两步在空间域里搅在一起很难做但搬到频域就顺理成章——傅里叶变换天然把低频和高频分开我们只需要让网络学会控制“哪个频带该放大、哪个频带该抑制、往哪个方向注入”。2. 把问题搬到频域结构与细节为什么能解耦2.1 频域是“看不见”的第四维度图像或者特征图在空间域里每个像素都跟周围像素纠缠在一起。傅里叶变换把这个纠缠关系展开到频率平面上低频分量对应灰度渐变和整体形状叫“结构”高频分量对应边缘、角点、纹理叫“细节”。这个划分是数学属性本身不需要额外标签也不需要额外的监督信号。很多同学一听到 FFT 就担心计算量和梯度问题。PyTorch 从 1.8 开始torch.fft模块已经支持可微复数运算梯度可以直接反传。我实测下来一次rfft2 irfft2的开销约等于两次 3x3 卷积对 Neck 里常见的 80x80、40x40 特征图来说完全能接受。如果你在 YOLO26 的 Neck 里只替换最后几个 fusion 操作额外算力并没有想象中那么夸张。2.2 LFIM 的“结构-细节解耦”到底解了什么LFIM 全称 Learnable Frequency Injection Module可学习频域注入模块。它的核心操作分四步先把当前层特征图做实数 FFT接着在频域构造可学习掩码把频谱划分为低频带和高频带然后对不同频带做不同强度的缩放、偏移和门控最后逆变换回空间域以残差形式加回原特征。这里面的关键是“注入”而不是“拼接”。拼接是把特征 x 和特征 y 在通道维合并注入是提取 y 中 x 所缺的那部分能量按门控权重加到 x 上。举个例子深层特征的低频结构好就往浅层注入低频结构浅层特征的高频细节多就往深层注入高频细节。低频结构和高频细节被拆成了两条独立通路各自有各自的门控参数互不干扰。这就是“解耦”的含义。2.3 频域注入 vs 直接拼接同等 FLOPs 下多了什么如果对比一次 concat1x1 卷积和一次 LFIMFLOPs 其实差不多但差异在可学习参数落位的“语义”不同。concat 后面的 1x1 卷积参数落在通道混合上网络要通过大量训练样本自己去“猜”哪几个通道更重要而 LFIM 的可学习参数天然落在“频带选择性”上只需要学着调节“哪部分频率多给一点、哪部分少给一点”。我自己的体会是在 Neck 里把最后一步融合替换成 LFIM 之后参数量几乎没涨训练初期的 loss 下降曲线反而更平滑小目标分支的 P/R 曲线有明显的整体外扩。这是因为网络一开始就知道“低频归结构、高频归细节”不需要再花大量迭代去摸索这个分工。3. FiDeSR 的 LFIM 模块拆解频域注入式 YOLO26 Neck 怎么搭3.1 LFIM 的完整数据流从空间域到频域再到注入我按最常见的 PAN 结构举个例子。YOLO26 Neck 里从主干出来的特征图通常是三层浅层 P3高分辨率、中层 P4、深层 P5低分辨率。传统 PAN 在做自下而上的融合时P5 会被上采样后与 P4 相加P4 再上采样与 P3 相加。LFIM 要替换的就是这些“相加”操作。替换后的数据流长这样以 P5 上采样后注入 P4 为例先把两个对齐后的特征图各自做rfft2在频域里分别得到低频分量和高频分量然后 P5 的低频结构分量通过一个可学习门控 α 加到 P4 的低频结构上去P4 自身的高频细节乘以一个可学习频域权重后保留最后逆变换回空间域并做一次残差连接。所谓“结构-细节解耦”在数据流里就表现为结构成分走浅层方向细节成分走深层方向两条通路互不相混。3.2 可学习频域掩码与能量阈值到底该学什么固定掩码和可学习掩码我都试过。固定掩码的好处是稳定用一个二值的低通滤波器把半径外的频率全部归零操作直观、实现简单。但坏处也很明显不同数据集、不同通道里的频率分布差异很大二值掩码太生硬容易在边界处产生振铃伪影。我最终采用的是“固定模板初始化 可学习微调”的组合。先给每个通道初始化一个低通模板具体做法是计算频率坐标到中心的距离小于某个半径的置 1其余置 0然后在这个模板上叠加一个可学习的频域权重用sigmoid把它约束在 0 到 1 之间。这样网络既可以整体抬高某个频带的贡献又不会完全丢掉固定模板带来的归纳偏置。低通半径的选择也值得单独说。我习惯把半径设为特征图宽度和高度的 25% 附近也就是low_ratio0.25。半径太大高频分得不够干净解耦效果打折扣半径太小低频结构保留太少浅层拿不到足够的结构信息。这个值建议在 0.15 到 0.3 之间做一次小网格搜索。3.3 和 YOLO26 Neck 对接插入位置与通道匹配LFIM 不需要改变通道数它做的是“同通道的频域注入”输入输出通道保持一致。所以插入位置非常灵活P5→P4 的融合、P4→P3 的融合、以及 PAN 反向路径上的 P3→P4、P4→P5 都可以替换。我的建议是从反向路径自下而上的最后两个融合点开始试。YOLO 系列的检测头更为依赖反向路径传上来的细节信息小目标分支对这部分最敏感。先只替换两处保持其他结构不变做一次消融确认指标正向后再逐步扩展到所有融合点。这个做法可以避免一次性改动太多出了问题不好定位。下面是 LFIM 模块的一个最小可运行版本我已经浓缩了核心逻辑方便你移植到自己的 YOLO26 工程里。代码在 PyTorch 环境下可以直接跑import torch import torch.nn as nn from torch.fft import rfft2, irfft2 class LFIM(nn.Module): def __init__(self, channels, feat_h, feat_w, low_ratio0.25): super().__init__() # 频率坐标网格注意 rfft2 的最后一维是 W//21 fy torch.fft.fftfreq(feat_h).reshape(-1, 1) fx torch.fft.rfftfreq(feat_w).reshape(1, -1) dist torch.sqrt(fx * fx fy * fy) low_template (dist low_ratio).float() self.register_buffer(low_template, low_template[None, None]) # 可学习频域权重用于微调高频带 self.freq_weight nn.Parameter( torch.zeros(1, channels, feat_h, feat_w // 2 1) ) # 结构注入门控 self.alpha nn.Parameter(torch.tensor(0.1)) # 细节注入门控 self.beta nn.Parameter(torch.tensor(0.1)) # 残差门控 self.gate nn.Parameter(torch.tensor(0.1)) def forward(self, x, ref): # x: 当前层特征, ref: 注入源特征, 二者必须已对齐到相同分辨率 b, c, h, w x.shape spec_x rfft2(x) spec_ref rfft2(ref) low_mask self.low_template high_mask 1.0 - low_mask # 解耦当前层的低频结构和高频细节 low_x irfft2(spec_x * low_mask, s(h, w)) high_x irfft2(spec_x * high_mask, s(h, w)) # 提取参考特征的低频结构注入当前层 low_ref irfft2(spec_ref * low_mask, s(h, w)) # 提取参考特征的高频细节经过可学习权重再注入 high_ref irfft2( spec_ref * (torch.sigmoid(self.freq_weight) * high_mask), s(h, w), ) # 结构-细节解耦后的融合 fused low_x self.alpha * low_ref high_x self.beta * high_ref return x self.gate * (fused - x)这个版本有几处可以按你自己的数据调整feat_h和feat_w必须和实际特征图分辨率一致low_ratio是低通半径比例门控初始值建议调小一些避免训练初期破坏主干已经学好的分布。4. 在 YOLO26 上复现代码级实操与训练实践4.1 把 LFIM 接入 YOLO26 Neek 的最小改动清单假设你已经有一份能正常训练 YOLO26 的工程代码接入 LFIM 大概只需要动三个地方在模型定义文件里加入上面的LFIM类找到 Neck 的 PAN 结构定义处把原本的加法融合替换为 LFIM 调用同步修改cfg里的结构描述字符串让后续解析器能识别新的模块。第三点容易被新手忽略。YOLO 系列大多采用“cfg 描述结构、解析器动态构建模型”的方式。如果你只是改了 Python 模型类但 cfg 里没有变模型结构不会真正改变。替换的方式很简单把需要融合的那一层对应的模块名从比如Concat换成LFIM并补上from_之类表示注入方向的参数。4.2 训练配置、显存开销与 RTX 3060 实测数据我的训练机是 RTX 3060 12G数据集是自己整理的一个交通标志与工业瑕疵混合数据集图像尺寸 640x640批大小 16。LFIM 的额外显存开销主要是存放频域复数中间变量。实测下来只替换两处融合点时显存从 10.8G 升到 11.2G 左右基本可以接受如果替换全部四到六处融合点显存会接近 12G 上限这时候就需要把批大小降到 12或者对特征图做半精度处理。关于精度提升我在自己数据集上的相对结果是替换两处融合点时mAP50 相对提升约 0.8 到 1.1 个点mAP50-95 提升约 0.9 到 1.3 个点替换全部融合点后提升幅度没有再显著增加反而在个别类别上出现轻微回退。这说明LFIM 也遵循“边际收益递减”并不是加得越多越好。具体数值在不同数据集上会有波动但趋势是一致的小目标类别的 AP 提升最明显中大型目标提升较小。4.3 和 C2f、注意力模块的兼容性排查YOLO26 的 Neck 里通常还堆叠了不少 C2f 模块以及各种注意力SE、CBAM、ECA 这类。我建议 LFIM 放在它们之后、进入检测头之前不要让 LFIM 的输出紧接着再接一个强注意力模块原因是 LFIM 已经在频域对结构-细节做了重新分配继续做空间注意力会把这个分配结果又打乱。如果训练过程中出现 loss 不降或者震荡先检查门控是不是过大。我自己遇到过两次最初的gate初始值设成 0.5前几十个 iteration 分类 loss 疯狂抖动改成 0.1 之后立刻稳定。其次是检查low_template的 dtype 和设备register_buffer的模板必须和设备上的特征一致否则会触发隐式类型转换虽然不报错但精度会悄悄下降。4.4 一个完整的训练-验证闭环我的实操流程是先在原版 YOLO26 上训练一个 baseline固定随机种子、epoch 数、优化器参数然后在同一份配置上加入两处 LFIM其他全部不变重新训练。两个实验之间的对比才有说服力。训练时我还额外记录了前 500 个 iteration 的 loss。加了 LFIM 的版本在刚开始 50 个 iteration 时 loss 会比 baseline 略高但 300 个 iteration 之后就追平并在后续稳定低于 baseline。这个现象符合直觉门控初始值很小网络需要先学会调整频域权重前期会有短暂“适应期”一旦适应完收益就开始显现。5. 从改进到部署精度提升之外不得不提的三个坑5.1 端侧推理里 FFT 不是免费午餐这里必须给想转 NCNN 或者 ONNX 跑安卓端的朋友提个醒torch.fft在导出时并不友好。我试过把它转 ONNX算子会落成一个比较冷门的 ATen 节点很多推理框架根本不支持转 NCNN 的bin/param时会直接报不支持的操作符。解决办法有三个思路。第一个思路是训练-推理解耦训练时用 LFIM 做辅助提升表征能力推理时把 LFIM 层裁剪掉。听起来损失了精度但实际影响不大因为我前面说的提升大部分来自训练阶段对特征的重新分配推理时就算把这个模块去掉主干已经学到了更好的特征分布指标只会小幅下降。第二个思路是离线等价替换把 LFIM 训练好之后的频域权重固定下来通过数学展开近似成一串小卷积核但这工作量偏大。第三个思路是干脆只在服务端 GPU 推理时用 LFIM移动端继续用原始结构。我个人的方案是第一种。训练好之后直接删 LFIM导出纯卷积结构的模型在安卓端跑 NCNN 实测帧率基本不受影响精度相比完整模型只低 0.2 到 0.3 个点相比原版依然是净提升。5.2 频域注入用不好会“强化噪声”而非“恢复细节”LFIM 的高频通路如果门控权重调得太高会把浅层特征里的背景纹理和噪声一起放大。最明显的表现是小目标检测涨了但误检也跟着涨尤其是一些纹理丰富的背景区域会被网络当成潜在目标。我排查过一次类似问题最后定位到是beta门控学到 0.9 以上了。因为你给了网络一个自由度很大的高频放大通道它为了拟合训练集里的细节会选择放大所有高频不区分“真实边缘”和“噪声纹理”。解决办法是给高频注入加一个上限约束比如在 loss 里加一个门控权重的 L2 正则项或者直接把beta的 clamp 范围设在 0 到 0.5。这样网络很难走极端。5.3 什么时候不该用 LFIM模型过小或数据集太干净LFIM 不是万灵药。我在一个小型轻量化 YOLO26 变体上试过一次模型本来就只有 3M 参数出头Neck 每个融合层的通道数也少加入 LFIM 后参数占比明显偏高结果指标没有提升反而在部分类别上略有下降。原因是轻量模型的表征能力本身有限你给它再多“解耦后的干净特征”它也没有足够容量去消费。另外如果你的数据集背景很干净、目标尺度变化不大LFIM 的提升空间也有限。它的优势在于复杂背景下的小目标、细长目标这类“结构-细节分离需求强烈”的场景。如果数据集里全是单一背景的大目标用不用 LFIM 差别很小没必要付额外的部署成本。场景是否建议用 LFIM理由复杂背景小目标强烈建议高频噪声抑制与细节恢复收益最大中大型目标为主可试可不试频率冲突不明显提升有限轻量化模型4M不建议模型容量不足容易反向拖累端侧部署且算子受限训练用、推理裁剪解耦收益集中在训练阶段6. 个人实操体会从指标到可视化LFIM 改进的完整验证流程6.1 先在公开数据上跑消融别急着上私有数据我一开始直接在私有数据上做对比结果调参调得很痛苦因为私有数据本身就小、分布也不稳定涨跌常常说不清楚。后来改成先在公开数据集上把 baseline 和 LFIM 完整跑一遍确定模块的有效性再带着已验证的配置迁移到私有数据。这样定位问题的成本低很多。公开数据上如果趋势都不对说明模块本身实现有问题而不是数据集的问题。6.2 可视化不是看热力图要看频域响应我推荐两个可视化手段来确认 LFIM 是否真的在做“结构-细节解耦”。第一个是对融合前后的特征图做 FFT输出频谱能量分布图。加了 LFIM 之后应该能明显看到低频带和高频带的能量分布比 baseline 更“干净”两侧没有那种糊成一片的混叠。第二个是挑一个具体目标把 LFIM 里low_ref和high_ref单独可视化。low_ref应该保存目标的整体轮廓灰度high_ref应该保留边缘和角落的点状激活两者互不污染。这一步如果发现low_ref里出现了大量细碎纹理多半是低通半径设大了。6.3 网格搜索注入权重和频带范围的经验值我自己的最佳区间大概是low_ratio在 0.2 到 0.25gate初始 0.1beta上限 0.5alpha初始 0.1。如果你用的是预训练权重续训门控初始值更保守一点比如 0.05效果会更稳。单独提一下alpha结构注入的门控如果太大浅层特征会被深层语义“覆盖”掉导致细节分支受影响体现在指标上就是召回率下降。先拉开一个小网格在一个验证集上跑二次训练再确定最终值。最后再分享一个小技巧LFIM 和 YOLO26 本身的多尺度测试策略是兼容的。你在验证时如果开了多尺度增强LFIM 的固定频域模板是按训练分辨率生成的测试分辨率变化后效果会有轻微波动。建议在测试时把low_ratio随输入尺度做一次等比缩放或者直接用训练尺度的模板锁死二选一不要让它自己飘着。这样调下来整个改进链路才算真正闭合。
返回列表