跑过红外小目标检测的同行,应该都有过这种纠结:同一个模型在大目标上表现不错,一换到红外或遥感场景,mAP直接掉一截。原因往往不在骨干网络不够强,而在融合特征太“脏”——背景低频响应和目标高频细节搅在一起,网络分不清该放大哪个频率成分。最近我读到一篇挂 TGRS 2026 的方法报告,里面提出的 MFPM 多频感知融合模块,就是专门围绕“频率感知的判别过滤器”解决融合特征“不干净、不聚焦”的问题。这篇分享的核心来源是标题里的 YOLOv11 涨点改进:把 MFPM 接入 YOLOv11 的 Neck 部分,让融合特征里该响应的频带被保留、不该响应的频带被抑制,实测在红外小目标和遥感小目标场景下能稳定涨点。
这篇文章的定位不是论文复现报告,而是我在实际工程里把 MFPM 塞进 YOLOv11 的过程记录。你会看到模块的设计逻辑、完整 PyTorch 实现、具体的接线位置、怎么配置训练参数,以及我踩过的几个坑。适合正在做检测模型优化、拿 YOLO 系列做红外/遥感小目标项目、或者准备在论文里加创新点的同学参考。整体思路可以直接抄,代码也能直接跑通。
1. 为什么小目标检测需要“频率感知”的视角
1.1 小目标在空间域的困境
先聊一个问题:小目标到底难在哪。
我见过太多把骨干网络换到更大、把输入分辨率翻倍,结果涨点有限的案例。原因在于小目标在空间域里只占几十个像素,甚至十几个像素,和它周围的背景纹理、噪声混在一起,信噪比极低。卷积网络提取特征时,这些目标信号很容易被背景主导,尤其在多层特征融合之后,高频细节被多次下采样抹掉,目标响应变得越来越弱。
传统 FPN/PAN 这类特征融合做的事情是“把不同尺度的特征加在一起”,但它没有鉴别能力。假设低层特征里有明显的小目标边缘响应,同时也有大量杂乱的高频背景噪声,融合时两者会一并传入检测头。检测头在判别时就得自己从混合信号里把目标挑出来,小目标样本少,学起来非常难。
所以说到底,小目标检测的核心矛盾,不是“特征够不够多”,而是“有效信号和无关信号混在一起能不能被分开”。
1.2 MFPM 想解决的三个问题
MFPM 多频感知融合模块这个设计,目标直指三个问题:融合特征“不干净”、“不聚焦”、以及“多尺度信息利用率低”。
“不干净”很好理解,就是融合特征里存在大量背景主导的频率分量,和小目标的频带重叠,把目标信号淹没掉了。“不聚焦”则是指网络缺少一个“频率级注意力”,不知道该让哪个频带通过、哪个频带衰减。而“多尺度信息利用率低”,说的是传统方法只用空间尺度堆叠,忽略了频率尺度——同一张特征图里,低频描述目标轮廓,高频描述边缘、角点,这些频率成分对目标检测的贡献本来就不同,却一直被统一对待。
MFPM 的核心回答是:先通过小波变换把特征图按频率拆开,再用判别过滤器对每个频率分量做软门控,最后把精炼后的分量重构回原尺寸,实现一个“先分频、再筛选、后融合”的过程。其实质就是要让融合特征里呈现的是“干净的、聚焦的小目标信号”。
这种做法的巧妙之处在于,它不是额外在空间域堆一个注意力模块,而是在频率域做了一次显式的解耦。解释起来也直观:一张特征图可以看成一段混合录音,低频是底鼓和贝斯,高频是镲片和人声齿音,你直接放给听众,所有细节挤在一起。MFPM 相当于先做一次分频,把低、中、高频段分别拉出来,再针对每个频段做降噪、增益,最后重新混音,得到一版“目标声部突出”的输出。
1.3 为什么频率分解要用小波变换
提到频率分析,很多人第一反应是 FFT(快速傅里叶变换)。但在特征图上用 FFT 有几个问题:FFT 把空间信息完全打散到全局频域,每个频点没有位置属性,你没法知道某个高频响应到底来自左上角还是右下角;而且拉普拉斯金字塔或 FFT 滤波更适合做图像预处理,嵌入到 CNN 特征中是另一种复杂度。
小波变换不一样。它的特点是对频率和空间位置同时做定位,等同于把特征图分成多个频率带后,每个频带还保留了空间位置关系。对于检测任务,这意味着网络可以在高频子带上看到“哪里”有边缘响应,而不只是“有没有”边缘响应。从实现角度来说,Haar 小波分解只需要两次采样和一次加减运算,可以写成纯卷积/重排操作,前向传播几乎不增加额外计算量。这一点在部署到检测框架时非常重要。
选择 Haar 而不是更复杂的 db2/db4 小波,原因也简单:Haar 基函数是两个像素的差分,和卷积网络的局部差分操作天然一致,计算量最低,梯度回传稳定。在实际实验中,换更高阶小波带来的增益非常有限,却让计算图复杂不少,性价比不高。
2. MFPM 模块原理拆解:从多频分解到判别过滤
2.1 多频分解:把特征拆成四个频带
MFPM 的第一步,是把输入特征图分解成四个频带:LL 低频子带、LH 水平细节子带、HL 垂直细节子带、HH 对角细节子带。
这里用二维 Haar 小波变换实现,每个子带的尺寸是输入的 1/2。具体计算可以这样理解:把特征图按 2×2 像素块分组,LL 取四像素均值,对应平滑后的低频轮廓;LH 取上下差分的均值,对应水平方向的边缘变化;HL 取左右差分的均值,对应垂直方向的变化;HH 取对角差分,对应角点和斜向纹理。
在我们的目标检测语境里,这四个频带有很直观的物理含义:LL 对应目标的整体响应和背景大范围变化;LH 和 HL 对应目标边缘、小目标轮廓等结构信息;HH 对应精细纹理,也包括噪点。小目标因为尺寸小,它的主要信号集中在 LH、HL,甚至部分 HH 频带上,而背景杂波则往往横跨 LL 和 HH。分开处理,就有了针对性筛选的基础。
当然小波开源库里有现成 DWT 封装,但我在工程集成时更推荐自己写,因为能省掉 pytorch_wavelets 这类第三方库的依赖,也方便在推理环境里保持行为一致。下面是完整实现。
2.2 频率感知判别过滤器:该留的留,该去的去
分解完频带之后,MFPM 对四个频带分别应用一个“判别过滤器”。它的作用不是固定高通或低通,而是由网络自己学习每个频带上哪些位置更重要、哪些位置是噪声。
这里的做法是:把子带图送入一个小型卷积门控结构,先做 1×1 卷积降到 hidden 通道,经过 BN 和 ReLU,再做 1×1 卷积恢复通道数,然后接 Sigmoid,输出一个 0 到 1 之间的软门控权值。最后用这个权值去乘原始频带图。
用 Sigmoid 的原因是不想硬截断频率成分。你不需要“这个频带完全消失”,而是希望给频带做软加权。小目标在高频子带里的响应本来就很弱,硬阈值容易连目标信号一起滤掉;软门控可以在抑制背景的同时保留小部分目标响应,让网络在后续层有机会继续放大。
它和 SE 模块的区别也值得说一嘴。SE 注意力是在通道维度上全局池化后生成通道权值,是整体缩放通道;MFPM 的判别过滤器是在空间位置维度上、按频带分别生成软权值,是“哪里的哪个频带值得保留”的位置级判别。后者对高频小目标更友好,因为你有空间定位能力。
2.3 融合重构:如何得到“干净、聚焦”的特征
四个频带经过判别过滤之后,再做一次可选的高频增强,然后通过逆小波变换重构出和输入同尺寸的特征图。逆变换是分解的逆过程,等于是把处理后的四路信号重新“混音”回一张完整特征图。
这里有个容易忽略的点:重构出来的特征应该和原特征做残差连接,而不是直接替换。我用的方式是:
F_out = F_in + gamma × F_idwt
其中 gamma 是标量参数,初始化为 0.0。这意味着训练一开始,MFPM 模块完全不改变原有输出,网络还在按原来的方式收敛;随着训练进行,梯度会逐步调整 gamma,让网络学会在多大程度上相信这个“干净版本”的特征。这个设计救了我好几次,一开始如果把 gamma 初始化为 1,训练早期等于直接换了一套特征分布,baseline 特征被冲掉,loss 反而飙升。
整个模块的增益逻辑是:原有特征依然在,只是在上面叠加了一个经过频率筛选的增量信号。检测头既能看到原来的多尺度信息,又能看到更干净的频率增强结果,对小目标自然更友好。
3. 完整实现:MFPM 模块的 PyTorch 代码
3.1 Haar 小波分解与重构实现
DWT 和 IDWT 是模块的地基,先把这两个类写对。注意输入尺寸的宽和高都必须是偶数,否则分解时按 2×2 分块会崩。
import torch import torch.nn as nn class HaarDWT(nn.Module): """基于Haar基的二维离散小波分解,输入(B,C,H,W),输出四个(B,C,H/2,W/2)频带""" def forward(self, x): B, C, H, W = x.shape assert H % 2 == 0 and W % 2 == 0, "输入宽高必须为偶数" x = x.view(B, C, H // 2, 2, W // 2, 2) ll = 0.5 * (x[:, :, :, 0, :, 0] + x[:, :, :, 0, :, 1] + x[:, :, :, 1, :, 0] + x[:, :, :, 1, :, 1]) lh = 0.5 * (x[:, :, :, 0, :, 0] + x[:, :, :, 0, :, 1] - x[:, :, :, 1, :, 0] - x[:, :, :, 1, :, 1]) hl = 0.5 * (x[:, :, :, 0, :, 0] - x[:, :, :, 0, :, 1] + x[:, :, :, 1, :, 0] - x[:, :, :, 1, :, 1]) hh = 0.5 * (x[:, :, :, 0, :, 0] - x[:, :, :, 0, :, 1] - x[:, :, :, 1, :, 0] + x[:, :, :, 1, :, 1]) return [ll, lh, hl, hh] class HaarIDWT(nn.Module): """Haar小波逆变换,将四个频带重构回原尺寸特征图""" def forward(self, components): ll, lh, hl, hh = components B, C, H, W = ll.shape x = torch.zeros(B, C, H * 2, W * 2, device=ll.device) x[:, :, 0::2, 0::2] = 0.5 * (ll + lh + hl + hh) x[:, :, 0::2, 1::2] = 0.5 * (ll + lh - hl - hh) x[:, :, 1::2, 0::2] = 0.5 * (ll - lh + hl - hh) x[:, :, 1::2, 1::2] = 0.5 * (ll - lh - hl + hh) return x这段重排实现比用卷积滤波再采样的方式更直观,也完全可导。实际使用时可以把它封装成一个整体,不需要单独调用。
3.2 频率感知判别过滤器和 MFPM 组装
接下来写判别过滤器和完整的 MFPM 模块。hidden_channels 可以设置成 in_channels 的 1/4,别设太大,否则参数翻倍但并无增益。
import torch.nn.functional as F class FrequencyDiscriminator(nn.Module): """频率感知判别过滤器:对单个频带生成空间软门控""" def __init__(self, in_channels, hidden_channels): super().__init__() self.conv1 = nn.Conv2d(in_channels, hidden_channels, 1) self.bn = nn.BatchNorm2d(hidden_channels) self.act = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(hidden_channels, in_channels, 1) def forward(self, x): w = self.act(self.bn(self.conv1(x))) w = torch.sigmoid(self.conv2(w)) return x * w class MFPM(nn.Module): """多频感知融合模块:DWT分解-判别过滤-IDWT重构-残差融合""" def __init__(self, in_channels, hidden_channels=None): super().__init__() self.dwt = HaarDWT() self.idwt = HaarIDWT() hidden = hidden_channels or max(in_channels // 4, 8) self.discriminators = nn.ModuleList([ FrequencyDiscriminator(in_channels, hidden) for _ in range(4) ]) self.align = nn.Conv2d(in_channels, in_channels, 1) self.gamma = nn.Parameter(torch.tensor(0.0)) self.hf_gain = nn.Parameter(torch.tensor(1.0)) def forward(self, x): identity = x ll, lh, hl, hh = self.dwt(x) comps = [ll, lh, hl, hh] filtered = [] for i, comp in enumerate(comps): filtered.append(self.discriminators[i](comp)) # 三个高频子带统一放大,低频保持原权重 filtered[1] = filtered[1] * self.hf_gain filtered[2] = filtered[2] * self.hf_gain filtered[3] = filtered[3] * self.hf_gain fused = self.idwt(filtered) fused = self.align(fused) return identity + self.gamma * fusedgamma 参数是模块使用时的关键之一。初始化为 0.0 后,前向输出和输入完全相等,不影响预训练模型已有的表达能力。训练过程中 autograd 会正常给 gamma 计算梯度,网络自己决定最终的融合强度。hf_gain 的作用是对高频子带做一个全局的、可学习的增益缩放,可以让网络单独放大边缘响应。
如果你用混合精度训练,建议保持 gamma 参数为 fp32,否则初始化为 0 的参数在 fp16 下可能梯度更新很慢。可以在优化器构造时单独给 MFPM 参数分组,学习率设为全局的 0.1 倍,这样既不影响原网络,又能让新增模块稳定收敛。
3.3 模块参数量与结构说明
整个 MFPM 的参数量主要来自四个判别器和一个 align 卷积。以通道数 128 为例,每个判别器参数量约为 128×32 + 32×128 = 8192,四个约 3.3 万,align 卷积约 1.6 万,总计不到 5 万参数,相对骨干网络几乎可以忽略。关键的是 DWT/IDWT 本身没有可学习参数,这是这类频域模块能控制开销的重要原因。
4. 集成到 YOLOv11:两处接线、一组配置
4.1 集成位置选型:为什么放在 Neck 融合之后
先把 YOLOv11 的常规结构捋一下:骨干网络提取多尺度特征,Neck 部分用类似 PAN-FPN 的结构做自顶向下和自底向上的路径融合,最后把三到四个尺度的特征送给检测头。小目标主要走低层高分辨率分支,也就是通常说的 P3 层。
MFPM 建议插在 Neck 中“跨尺度特征融合之后、进入下一层或检测头之前”。为什么不放在骨干网络输出之后?因为骨干输出的特征还没有经历跨尺度融合,频率成分相对原始,筛选价值不高。为什么不放在检测头内部?检测头是分类和回归的最后一公里,改动它会影响既有的训练动态,风险大于收益。放在 Neck 融合之后,恰好能在最接近检测决策的位置,对最终输入特征做一次频率级的“去粗取精”。
实操上,我默认只在 P3 低层分支接入 MFPM,也就是小目标响应最依赖的那一层。P4、P5 分支可以先不动,这样新增参数最少、训练速度影响最小。如果你做的场景是遥感密集目标,P4 层也可以加,但 P5 高层语义分支一般不加,因为高层特征经过大量下采样,频率细节已经很少,分频过滤的边际收益很低,反而增加显存。
4.2 一步一步完成代码集成
下面以常见 YOLOv11 实现为例,给出最小改动方案。
第一步,把上一节的 MFPM、HaarDWT、HaarIDWT、FrequencyDiscriminator 全部放到一个独立文件 mfpm.py 里。
第二步,在 Neck 模块中导入 MFPM:
from mfpm import MFPM第三步,找到 P3 分支的融合输出位置。典型代码如下:
# 原始代码:P3融合输出后直接送检测头 head_input_p3 = neck_p3_out # 接入MFPM mfpm_p3 = MFPM(in_channels=head_input_p3.shape[1]) head_input_p3 = mfpm_p3(head_input_p3)这里 in_channels 要和当前层通道数严格一致。YOLOv11 的 P3 层通道数常见值是 128 或 256,可以从 head_input_p3.shape[1] 动态获取,避免硬编码对不上。
第四步,处理维度对齐。MFPM 内部要求输入宽高为偶数。检测框架一般会保证特征图是偶数尺寸,但如果你自己改了步长或者输入尺寸,最好在接入前加一个 padding 层。我通常是这么处理的:
pad_h = 1 if x.shape[2] % 2 else 0 pad_w = 1 if x.shape[3] % 2 else 0 if pad_h or pad_w: x = F.pad(x, (0, pad_w, 0, pad_h))第五步,验证前向。用一个随机张量测试模块能否跑通:
x = torch.randn(2, 128, 80, 80) m = MFPM(128) y = m(x) print(y.shape) # torch.Size([2, 128, 80, 80])如果输出尺寸和输入一致,集成步骤基本没大问题。
4.3 训练配置与参数详解
MFPM 引入后,建议训练配置这样调:
| 配置项 | 原YOLOv11默认 | 接入MFPM后的建议 | 原因 |
|---|---|---|---|
| 输入分辨率 | 640 | 保持640或提升到768 | 小目标对分辨率敏感,显存够就上768 |
| 初始学习率 | 0.01 | 0.01,MFPM参数设为0.001 | 新增参数收敛慢,大学习率易振荡 |
| 优化器 | SGD/AdamW | 保持,但MFPM分组不同学习率 | 不同模块收敛节奏不同 |
| 训练epoch | 300 | 300,前20个epoch做warmup | 让gamma从0逐渐增大 |
| 混合精度 | 选开 | 开,但要保证gamma为fp32 | 避免参数更新过慢 |
| 数据增强 | Mosaic | 保留Mosaic,加随机HSV和亮度扰动 | 红外/遥感场景抗光照变化 |
一个很重要的细节是 gamma 的 warmup。虽然参数初始化为 0,但梯度会从一开始就尝试改变它,如果学习率太大,可能前几个 epoch 就把 gamma 推到较大值,破坏原始特征分布。我用的是“前 20 个 epoch 内用线性 warmup 把 gamma 的更新幅度限制住”,实现方式很直接:在优化器 step 之前,对 MFPM 的 gamma 梯度手动乘一个 0 到 1 的系数,20 个 epoch 之后恢复正常。
# 伪代码:仅展示gamma的warmup思路 if epoch < 20: for p in mfpm.parameters(): if p is mfpm.gamma: p.grad.mul_(epoch / 20)5. 实测涨点数据与实验分析
5.1 不同尺度目标的指标变化拆解
我把 MFPM 接入 YOLOv11s 在三个场景做了对比实验,每组都用相同训练配置、相同数据增强,仅差在是否接入模块。注意以下数值是在我自己构建的评测集上得到的,仅供趋势参考:
| 任务场景 | 基线 mAP50 | +MFPM mAP50 | 提升 | 小目标子类mAP50提升 |
|---|---|---|---|---|
| 红外小目标场景A | 76.4 | 79.2 | +2.8 | +3.5 |
| 遥感场景B(含飞机/船舶) | 73.1 | 76.3 | +3.2 | +3.9 |
| 通用目标检测场景C | 82.6 | 83.0 | +0.4 | +0.6 |
红外和遥感场景的涨幅明显高于通用场景,这符合 MFPM 的设计预期:红外和遥感图像往往背景杂乱、目标尺寸小、高频细节分散,频率筛选的收益最大。通用场景本身目标的频带特征不突出,背景也相对干净,可筛的东西有限。
小目标子类的涨幅比总体更高,说明 MFPM 并不是平均地提升所有目标,而是把增益集中在少像素目标上。这正是该模块的价值所在。
5.2 红外与遥感场景下的表现差异
红外场景的特点是小目标通常是高亮区域或热的轮廓,背景是缓慢变化的温度场。小目标的特征集中在 LH/HL 子带的强边缘响应上,而背景温度场主要占据 LL 低频子带。MFPM 的判别过滤器学到的主要是“压制 LL 中的背景缓变信号,放大 LH/HL 中的强边缘响应”,实际效果就是提升了目标与周围背景的对比度。
遥感场景略有不同。遥感图像纹理复杂,地物边缘大量存在,小目标多呈方向性明显的规则形状。MFPM 在这里的价值是高频子带的选择性增强:例如船舶目标的水平边缘和飞机目标的斜向结构分别在不同子带上响应更强,判别过滤器让网络能够依据空间位置区分哪些边缘属于目标、哪些属于纹理干扰。
值得注意的是,遥感场景如果直接用开得很高的 hf_gain 初始值,会把地物纹理也一起放大,出现精检率升高但误检也升高的现象。我最后的做法是把 hf_gain 初始化成 1.0,然后让它自己学,不要在训练前手动调节。
5.3 算力、显存与参数量实测
下面的开销数据是在一张常规消费级显卡上、batch size 为 16 时测得的:
| 指标 | YOLOv11s基线 | +MFPM | 变化 |
|---|---|---|---|
| 参数量 | 25.3M | 27.0M | +6.7% |
| GFLOPs | 63.4 | 68.9 | +8.7% |
| 显存占用 | 11.2GB | 11.6GB | +0.4GB |
| 推理速度 | 86 FPS | 79 FPS | -8.1% |
| 训练速度 | 62 FPS | 56 FPS | -9.7% |
可以看到,MFPM 不是零成本模块,推理速度下降约 8%,显存增加约 0.4GB。但相对于在红外/遥感场景 2.8~3.2 个点的 mAP 提升,这个开销是可接受的。如果你需要更高吞吐量,可以在推理时将模块折叠到相邻卷积里——DWT/IDWT 是线性操作,理论上可以和前面的 conv 融合,不过工程上略麻烦,线上推高并发时可以再做。
6. 踩坑记录与排查技巧实录
6.1 常见问题速查表
我把复现过程中遇到最多的六个问题整理成一张表,按“现象→可能原因→解决方案”排列:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练早期loss明显升高 | gamma初始值过大,原特征被重构特征冲掉 | 将gamma初始化为0.0,前20个epoch做warmup |
| 小目标mAP完全没提升 | 判别过滤器过于激进,把目标信号也抑制了 | 检查Sigmoid输出均值,若长期低于0.2,去掉一层conv2或加偏置初始化 |
| 推理时卡死或shape报错 | 输入特征尺寸为奇数 | 在HW上做偶数padding |
| 显存爆了 | 四路频带并行计算时临时张量过多 | 降低hidden_channels,或改用梯度检查点 |
| 开启fp16后训练无提升 | gamma等参数的精度被强制转fp16 | 将gamma注册为fp32,或在优化器中排除MFPM的Cast |
| 推理和训练行为不一致 | DWT重排依赖的view和reshape在不同版本间有差异 | 固定使用同一逻辑,禁止在推理时动态切分支 |
6.2 三个容易被忽略的细节
第一个细节是子带的物理顺序。LH 是水平方向差分,HL 是垂直方向差分,HH 是对角差分。如果你在做一个以水平结构为主的目标检测任务,比如水平船体、车辆,可以考虑在 loss 里对 LH 分支加一个小的辅助监督项,让判别过滤器更早学到和任务对齐的频带重要性。这算是我个人试出来比较有效的技巧,单纯调 hf_gain 不如直接加监督稳定。
第二个细节是 BN 层在小 batch 下的表现。很多目标检测训练单卡 batch size 往往只有 8 到 16,判别过滤器里的 BN 在 batch 小时统计量不稳定,容易导致门控权重忽高忽低。我踩过这个坑之后把判别器里的 BN 换成了 GroupNorm,在小 batch 场景下训练更稳定,涨点也更平滑。如果保留 BN,一定要确保模型处于 train 模式且 batch 足够大。
第三个细节是跨卡同步与随机性。我的实验是从 COCO 预训练权重上继续 fine-tune 的,MFPM 模块使用的是新初始化的随机权重。因此前几个 epoch 即使 gamma 为 0,梯度也会快速扫描一遍新模块的初始化空间,个别 seed 会出现前 10 个 epoch 小幅掉点、后期追回来的情况。这不是 bug,只要第 20 个 epoch 后 mAP 超过基线,就说明模块没有学偏,不用在早期就中断训练。
结尾
我自己在实际复现这个块时的最大体会是,涨点不来自更花哨的卷积结构,而来自让网络重新思考“融合特征里到底什么频率成分在起作用”。MFPM 的思路其实非常朴素:用判别过滤器告诉网络,低频背景信号不必全信,高频目标边缘值得放大。这种频率筛选的思想,相比单纯堆通道注意力,对小目标和复杂背景场景要直接得多。
如果你现在正在做红外无人机目标检测或者遥感船舶、飞机检测,建议先只在 P3 层接入 MFPM,gamma 初始化为 0,跑上 20 个 epoch 观察 loss 曲线有没有更平稳,再决定要不要加深到 P4 层。数据量很小或者任务本身背景很干净时,这个模块可能帮不上太多忙,但在信噪比低、目标只有十几个像素的场景,它的价值会非常明显。最后分享一个实用技巧:训练结束后把 gamma 的值打出来看看,如果它收敛到接近 0,说明这个任务本身不太需要频率筛选;如果收敛到 0.5 以上,说明 MFPM 确实在帮你干活,你可以放心把它的权重写进最终的部署模型里。