038、GFNet-Filter频域滤波注意力在YOLOv12中的复现——全局频域建模与目标检测增强
兄弟们,今天聊个有意思的东西。前阵子在调YOLOv12的时候,遇到个特别头疼的问题——小目标检测精度死活上不去,尤其是那种密集排列的遥感场景,模型对全局上下文的理解明显不够。我试过加SE、加CBAM、加SimAM,效果都一般,感觉就像是在局部特征上打补丁,治标不治本。后来翻到GFNet那篇论文,突然意识到一个问题:我们一直在空间域做注意力,但有没有想过在频域里做全局建模?这思路一下子就把我点醒了。
GFNet的核心思想其实特别朴素——把图像特征变换到频域,在频域里做可学习的滤波操作,再变换回空间域。这玩意儿的好处在于,频域里的一个点对应的是全局的空间频率信息,天然就具备全局感受野,不像空间注意力那样要堆好几层卷积才能看到全图。我当时就在想,要是把这个思路塞进YOLOv12的骨干网络里,是不是能解决那个全局上下文缺失的问题?
先说说GFNet的论文核心。它提出用2D FFT把特征图从空间域变换到频域,然后在频域里用一个可学习的滤波器(其实就是个复数权重矩阵)去调制频谱,最后用逆FFT变换回空间域。这个操作可以插在Transformer的每个block里,替代原来的自注意力机制。关键点在于,频域滤波的计算复杂度是O(HW log HW),比自注意力的O(H²W²)低太多了,而且还能保持全局交互。
那放到YOLOv12里,插入位置就很有讲究了。我试了好几个地方,最后发现最有效的是插在骨干网络的C3k2模块之后、SPPF之前。为什么选这儿?因为这时候特征图分辨率还比较大(一般是80×80或40×40),频域变换能保留更多的空间细节信息,而且这个位置正好是网络准备做多尺度融合的节点,全局上下文信息在这里注入,能更好地指导后续的检测头。你要是插在太浅的位置,频域滤波会干扰底层纹理特征;插在太深的位置,特征图太小,频域变换的优势就体现不出来了。
代码实现上,我直接贴核心部分。这里踩过一个坑——PyTorch的FFT默认是沿着最后一维做的,但图像特征图是BCHW格式,你得先permute成BHWC再变换,不然维度对不上。别这样写:
# 错误示范,别这样写x_fft=torch.fft.fft2(x,dim=(-2,-1))正确做法是:
classGFNetFilter(nn.Module):def__init__(self,dim,h=14,w=8):super().__init__()# 这里h和w是特征图尺寸的一半,因为FFT后是共轭对称的# 我们只需要保留一半的频谱分量self.complex_weight=nn.Parameter(torch.randn(h,w,dim,2,dtype=torch.float32)*0.02)# 注意这里用randn初始化,别用ones,不然训练初期梯度爆炸defforward(self,x):B,C,H,W=x.shape# 先permute成BHWC,方便FFTx=x.permute(0,2,3,1).contiguous()# 做2D FFT,只保留前一半频率x_fft=torch.fft.rfft2(x,dim=(1,2))# 取实部和虚部,拼成复数weight=torch.view_as_complex(self.complex_weight)# 频域滤波,这里用复数乘法x_fft=x_fft*weight# 逆变换回空间域x=torch.fft.irfft2(x_fft,s=(H,W),dim=(1,2))# 再permute回BCHWx=x.permute(0,3,1,2).contiguous()returnx这里有个细节要注意——rfft2返回的频谱尺寸是(H, W//2+1),所以complex_weight的尺寸要对应好。我上面写的h和w就是H和W//2+1。你要是直接照搬论文里的参数,很容易维度对不上报错。
插入到YOLOv12里,我是在C3k2后面加了个GFNetFilter,然后接一个残差连接。这样设计的好处是,如果频域滤波学不到有用信息,残差连接能保证梯度直接回传,不会影响原有特征。实际训练的时候,我建议把学习率调低一点,因为频域参数对学习率比较敏感,我一开始用默认学习率,loss直接飞了。
实验对比这块,我在VisDrone数据集上跑了50个epoch,baseline是原版YOLOv12n。加了GFNet之后,mAP50从34.2%涨到了36.8%,mAP50:95从18.7%涨到了20.3%。最明显的是小目标那一栏,AP_s从12.1%涨到了14.5%,提升幅度接近20%。参数量增加了大概0.3M,推理速度从62FPS降到58FPS,这个代价完全可以接受。
消融实验我做了三组:第一组只加GFNet不加残差,mAP50是35.1%,说明残差连接确实有用;第二组把GFNet插在SPPF后面,mAP50只有33.9%,比baseline还低,说明位置选错了反而有害;第三组把滤波器换成固定的高斯滤波(不学习),mAP50是34.5%,说明可学习的滤波器才是关键。
可视化分析的时候,我做了个有趣的实验——把训练好的滤波器权重可视化出来。发现网络学到的滤波器在低频区域有较大的幅值,在高频区域幅值较小但相位变化剧烈。这说明网络自动学会了保留低频的全局结构信息,同时在高频区域做选择性增强,这正好对应了目标检测里"既要看全局又要抓细节"的需求。
最后给点个人经验。第一,GFNet不是万能的,它更适合那些背景复杂、目标尺度变化大的场景,你要是做简单的工业检测,可能收益不大。第二,插入位置一定要多试,我试了五个位置才找到最优的,别偷懒直接照搬论文里的位置。第三,训练的时候建议用warmup,让频域参数先稳定下来,不然前期震荡很厉害。第四,如果你显存够大,可以试试把GFNet和CBAM串起来用,我试过效果还能再涨0.5个点,但推理速度会再慢一些。
这篇就先写到这儿,代码我放在GitHub上了,需要的兄弟自己去拿。有问题评论区聊,我看到都会回。