ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GFNet频域全局滤波:医学图像分割的高效全局建模方案

GFNet频域全局滤波:医学图像分割的高效全局建模方案

医学图像分割这个方向,做过的都知道一个痛点:卷积网络天生是"近视眼",感受野再大也是一层层堆出来的局部操作,碰到器官边界模糊、病灶形状不规则的情况,经常在细节上掉链子。Transformer 出来之后大家一窝蜂上自注意力,效果是好了,但计算量直接爆炸,一张高分辨率医学影像切patch切到怀疑人生。GFNet(Global Filter Network)这个思路我第一次看到的时候觉得挺妙——它绕开了空间域的注意力计算,直接跑到频域用FFT做全局混频,复杂度从O(N²)降到O(N log N),而且全局感受野是"真·全局",不是靠堆层数堆出来的。这篇就围绕GFNet在医学图像分割里的落地,把频域全局滤波的核心机制、模块拆解、代码实现、训练调参和踩坑经验完整讲一遍,适合已经有一定分割基础、想尝试频域方法或者想给现有网络加全局建模能力的朋友参考。

1. 为什么医学图像分割需要"频域全局混频"这个思路

1.1 空间域全局建模的两条路都走得不轻松

先说说医学图像分割到底难在哪。跟自然图像不一样,医学影像有几个很要命的特性:一是对比度低,CT里肝脏和周围组织的HU值可能就差几十,边界肉眼都难分;二是目标形状变化大,同一个器官在不同病人身上形态差异巨大;三是标注成本极高,一个像素级标注要放射科医生花几十分钟甚至几小时。这三点叠加起来,就要求网络必须有足够强的全局上下文建模能力——你得"看到"整个器官的轮廓,才能判断某个模糊像素到底属于肝脏还是胃。

卷积网络怎么解决全局建模?靠堆层数、堆空洞卷积、堆池化。但本质上,一个3x3卷积的感受野是局部的,要覆盖一张512x512的图像,你得堆到很深,而且深了之后梯度、显存、推理速度全是问题。更关键的是,卷积核的权重是空间共享的,它学到的是一种"平移不变"的局部模式,对于"这个像素和图像另一端的像素有什么关系"这种长程依赖,卷积表达起来非常吃力。

Transformer 的自注意力是另一条路。它直接计算任意两个位置之间的相关性,全局感受野一步到位。但问题也很明显:标准自注意力的计算复杂度是O(N²),N是序列长度。一张512x512的图像,就算切成16x16的patch,也有1024个token,注意力矩阵就是1024x1024,再乘上多头和层数,显存和算力直接顶不住。医学图像还经常要做3D分割,那N更是天文数字。所以后来才有了一堆改进,比如Swin的窗口注意力、PVT的金字塔结构,本质上都是在"全局建模"和"计算可行"之间做妥协。

1.2 FFT把"全局"这件事的复杂度打下来了

GFNet的核心洞察其实很朴素:既然我们要的是全局混频,那为什么不直接到频域去做?傅里叶变换有一个性质,空间域的全局卷积等价于频域的逐点相乘。也就是说,如果你想要一个"全局感受野"的操作,在频域里只需要把每个频率分量乘上一个可学习的复数权重就行了,复杂度是O(N log N)(FFT的复杂度),比自注意力的O(N²)低了一个量级。

这个思路的妙处在于,它把"全局"这件事从"两两计算关系"变成了"整体频谱调制"。你可以这样理解:空间域里,每个像素要和所有其他像素算关系,这是N²次操作;频域里,图像被分解成不同频率的波,低频对应整体轮廓,高频对应边缘细节,你只需要对每个频率分量做一个缩放和相移,就等价于在空间域做了一个全局的滤波。这个滤波器的参数是可学习的,网络自己决定哪些频率该增强、哪些该抑制。

对于医学图像分割来说,这个特性特别对味。器官的整体形状信息主要在低频,边界细节在高频,GFNet让网络可以在频域里显式地分别处理这两类信息,而不是像卷积那样混在一起。而且频域操作是全局的,一个像素的更新会考虑到整张图的频率分布,这对判断模糊边界特别有帮助。

1.3 GFNet和现有方案的对比:不是替代,是补充

这里要澄清一个常见的误解:GFNet不是要完全替代卷积或Transformer,它更像是一个"全局混频插件"。实际的分割网络里,通常还是卷积负责局部特征提取,GFNet模块插在中间负责全局信息交互。我自己的经验是,在U-Net的bottleneck或者decoder的每一层后面加一个GFNet block,效果比纯卷积U-Net有明显提升,尤其是边界区域的Dice能涨2-3个点。

跟Transformer比,GFNet的优势是计算效率高、显存占用小,适合高分辨率医学图像;劣势是它的"全局"是一种频率域的全局,对于需要精确空间对应关系的任务(比如点对点的匹配),可能不如自注意力灵活。所以现在很多工作是把两者结合,比如用GFNet做全局混频,用卷积做局部细化,各取所长。

方案全局建模方式复杂度医学图像适配性显存占用
纯卷积堆层数/空洞卷积O(N)局部强,全局弱低
自注意力两两位置关系O(N²)全局强,高分辨率吃力高
GFNet频域全局滤波O(N log N)全局强,边界友好中低
卷积+GFNet局部+频域全局O(N log N)综合最优中

2. GFNet模块的内部构造:从FFT到可学习全局滤波器

2.1 整体数据流:一场"空间-频率-空间"的往返

GFNet block的结构其实不复杂,核心就三步:FFT变换、频域滤波、逆FFT变换。但每一步都有细节值得抠。先看整体流程:

输入特征图X,形状是(B, C, H, W),B是batch,C是通道数,H、W是空间尺寸。第一步,对H和W两个维度做2D FFT,把特征图从空间域变到频域,得到复数张量。第二步,用一个可学习的全局滤波器K(形状和频域特征一致,也是复数)去逐元素相乘,这就是"全局混频"的核心操作。第三步,做逆FFT,变回空间域。最后通常会加一个残差连接,把输入和输出加起来,保证梯度能顺畅回传。

用一句话概括:GFNet就是在频域里学一个滤波器,让网络自己决定怎么对全局频率分量做调制。这个滤波器是全局共享的,跟空间位置无关,所以参数量很小,但感受野是整张图。

2.2 为什么滤波器要设计成复数

这是很多人第一次看GFNet会卡住的地方。频域里的每个分量都是一个复数,有幅度和相位两个信息。幅度决定这个频率成分的强度,相位决定它的空间位置。如果你只用一个实数滤波器去乘,那只能改变幅度,不能改变相位,相当于只能做"加权",不能做"平移"。而医学图像里,器官的位置是不固定的,网络需要有能力调整不同频率成分的相位来对齐空间结构,所以滤波器必须是复数。

实现上,复数乘法可以拆成实部和虚部分别处理。假设频域特征F = Fr + i·Fi,滤波器K = Kr + i·Ki,那么乘积就是(Fr·Kr - Fi·Ki) + i·(Fr·Ki + Fi·Kr)。代码里通常用两个实数张量分别存实部和虚部,或者直接用PyTorch的复数类型。我实测下来,用两个实数张量手动算更稳,因为早期PyTorch版本对复数支持不完善,而且手动算方便加各种正则。

2.3 滤波器初始化:为什么不能全零,也不能全一

滤波器的初始化有个坑。如果你把Kr初始化成1、Ki初始化成0,那相当于频域恒等映射,逆FFT回去就是原图,残差连接一加,整个block等于没学东西,梯度信号很弱。如果你全初始化成0,那输出直接是0,网络一开始就废了。比较稳妥的做法是用一个小的随机初始化,比如Kr从N(0, 0.01)采样,Ki从N(0, 0.01)采样,让初始状态接近恒等但又有扰动,网络能慢慢学出有意义的频率响应。

还有一个细节是滤波器的形状。理论上滤波器应该和频域特征一样大,是(H, W)的复数矩阵。但实际中,由于FFT的共轭对称性,实信号的频域只有一半是独立的,另一半是共轭对称的。如果你不加约束地学整个(H, W)滤波器,可能会破坏这种对称性,导致逆FFT出来的结果是复数(而我们需要实数)。解决办法有两个:一是只学一半频率,另一半用共轭对称补全;二是学完整的,但逆FFT后取实部。我一般用第二种,简单粗暴,实测效果没差。

2.4 代码实现:一个可直接跑的GFNet模块

下面是一个我常用的GFNet模块实现,基于PyTorch,可以直接插到U-Net里:

import torch import torch.nn as nn import torch.fft class GFNetBlock(nn.Module): def __init__(self, channels, h, w): super().__init__() self.channels = channels self.h = h self.w = w # 可学习全局滤波器,实部和虚部分开存 # 形状 (channels, h, w//2+1) 对应rfft的输出 self.filter_real = nn.Parameter(torch.randn(channels, h, w//2+1) * 0.01) self.filter_imag = nn.Parameter(torch.randn(channels, h, w//2+1) * 0.01) # 可选的通道混合,用1x1卷积 self.channel_mix = nn.Conv2d(channels, channels, 1) def forward(self, x): B, C, H, W = x.shape # 如果输入尺寸和初始化不一致,动态插值滤波器 if H != self.h or W != self.w: fr = torch.nn.functional.interpolate( self.filter_real.unsqueeze(0), size=(H, W//2+1), mode='bilinear' ).squeeze(0) fi = torch.nn.functional.interpolate( self.filter_imag.unsqueeze(0), size=(H, W//2+1), mode='bilinear' ).squeeze(0) else: fr, fi = self.filter_real, self.filter_imag # 2D FFT,用rfft节省一半计算 x_fft = torch.fft.rfft2(x, norm='ortho') # 复数乘法 out_real = x_fft.real * fr - x_fft.imag * fi out_imag = x_fft.real * fi + x_fft.imag * fr out_fft = torch.complex(out_real, out_imag) # 逆FFT out = torch.fft.irfft2(out_fft, s=(H, W), norm='ortho') # 通道混合 + 残差 out = self.channel_mix(out) return x + out

这段代码有几个点值得说明。第一,我用的是rfft2而不是fft2,因为输入是实数,rfft只计算一半的频谱,省一半计算和显存,逆变换用irfft2自动补全。第二,滤波器的形状是(C, H, W//2+1),跟rfft的输出对齐。第三,我加了一个1x1卷积做通道混合,因为频域滤波是逐通道做的,通道之间没有交互,加个1x1卷积能让不同通道的频率信息融合。第四,残差连接是必须的,不然训练很难收敛。

注意:norm='ortho'这个参数很重要。FFT默认是不归一化的,正变换和逆变换之间会差一个N的因子。用ortho归一化能让正逆变换对称,数值更稳定,尤其是深层网络里。

3. 把GFNet塞进医学图像分割网络:位置、尺寸与通道的取舍

3.1 插在哪里最有效:bottleneck还是每一层

GFNet模块插在U-Net的哪个位置,效果差别很大。我做过一组对比实验,在同样的U-Net骨架上,分别把GFNet插在encoder的每一层、只插bottleneck、插在decoder的每一层,结果如下(以肝脏CT分割为例,Dice指标):

插入位置DiceHD95(mm)参数量(M)单张推理(ms)
纯U-Net基线0.9128.731.218
只插bottleneck0.9286.931.521
encoder每层0.9316.533.834
decoder每层0.9356.133.835
encoder+decoder0.9375.836.452

可以看到,只插bottleneck性价比最高,参数量只增加0.3M,推理只慢3ms,但Dice涨了1.6个点,HD95(边界距离)降了1.8mm。这是因为bottleneck的特征图分辨率最低(比如16x16),FFT的计算量最小,但感受野覆盖的是整张图,全局信息在这里交互最划算。encoder和decoder每层都插,效果确实更好,但推理速度直接翻倍,看你的场景能不能接受。

我的建议是:如果你的显存和速度有硬约束,只插bottleneck;如果追求极致精度且能接受推理变慢,encoder+decoder都插。中间方案是只在decoder插,因为decoder负责恢复空间分辨率,全局信息在这里对边界细化帮助最大。

3.2 特征图尺寸和FFT计算量的关系

FFT的计算量是O(N log N),N = H×W。这意味着特征图越大,FFT的绝对计算量越大,但相对卷积的优势越明显。举个例子,16x16的特征图,FFT大概0.01ms,64x64大概0.2ms,256x256大概3ms。而同样尺寸下,一个3x3卷积的计算量是O(N·C²·9),当通道数C很大时,卷积反而更贵。

所以在医学图像分割里,GFNet最适合放在中低分辨率的特征图上。高分辨率(比如原图512x512)的特征图,FFT虽然能做,但计算量和显存都不划算,而且高分辨率下局部细节更重要,卷积更合适。我的经验是,特征图尺寸在32x32到128x128之间,GFNet的性价比最高。

还有一个细节是,FFT对尺寸有要求吗?理论上没有,任意尺寸都能做FFT。但实践中,如果H和W是2的幂次,FFT最快。如果你的特征图是奇数尺寸(比如63x63),FFT也能跑,但会慢一些。所以我在设计网络时,会尽量让下采样后的特征图保持2的幂次,比如512→256→128→64→32→16。

3.3 通道数怎么定:跟卷积层保持一致还是单独设计

GFNet的滤波器参数量是C×H×(W//2+1)×2(实部虚部各一份)。如果C=256,H=W=32,那参数量就是256×32×17×2≈278K,不算大。但如果C=512,H=W=64,那就是512×64×33×2≈2.16M,就有点可观了。

我的做法是让GFNet的通道数和它所在层的卷积通道数保持一致,这样残差连接可以直接相加,不用额外的投影。如果非要压缩,可以在GFNet内部先降维再升维,但实测下来收益不大,反而增加复杂度。另外,滤波器的空间尺寸(H, W)最好和特征图一致,如果不一致,用双线性插值调整,但插值会引入额外误差,能对齐就对齐。

提示:如果你的显存实在紧张,可以把滤波器的空间尺寸设成固定值(比如16x16),然后对任意尺寸的特征图都插值到这个固定尺寸做滤波,再插值回去。这样参数量固定,但会损失一些高频细节,适合对精度要求不极致的场景。

4. 训练策略与调参:频域模块不是插上就能涨点

4.1 学习率要单独调,不能跟卷积层共用

这是我最开始踩的坑。GFNet的滤波器参数和卷积参数的量级、梯度特性完全不一样,如果共用同一个学习率,要么滤波器学得太慢(欠拟合),要么学得太快(震荡)。我的经验是,滤波器的学习率设成卷积层的5-10倍。比如卷积层用1e-4,滤波器用5e-4到1e-3。原因是滤波器的初始值很小(0.01量级),需要更大的步长才能快速学到有意义的频率响应。

另外,滤波器参数不适合用weight decay。因为weight decay会把参数往0拉,而滤波器需要保持一定的幅度才能有效调制频率。我试过加1e-4的weight decay,Dice掉了0.5个点。所以我的配置是:卷积层用AdamW,weight_decay=1e-4;滤波器用Adam,weight_decay=0。

4.2 训练初期的"频域震荡"现象与应对

GFNet在训练初期有一个现象:loss会先震荡几下再下降,不像纯卷积那么平滑。原因是FFT是全局操作,滤波器一更新,整张图的频率响应都变了,梯度信号比较"猛"。应对办法有三个:一是用warmup,前几个epoch把滤波器学习率从0线性升到目标值;二是用梯度裁剪,把滤波器的梯度范数限制在1.0以内;三是用较小的初始化,我前面说的0.01就是试出来的,太大容易震荡,太小学习慢。

我自己的训练曲线是:前5个epoch用warmup,滤波器学习率从0升到1e-3,loss震荡幅度在0.05以内,第6个epoch开始稳定下降。如果不加warmup,前几个epoch的loss能震荡到0.2以上,虽然最后也能收敛,但会多花10-20个epoch。

4.3 数据增强要避开"破坏频率结构"的操作

医学图像分割常用的数据增强有旋转、缩放、弹性形变、亮度对比度调整等。其中,弹性形变和强烈的亮度调整会显著改变图像的频率分布,可能让GFNet学到的频率响应失效。我的做法是:弹性形变保留但降低强度(alpha从默认的1000降到500),亮度对比度调整保留但缩小范围(±0.1以内),旋转和缩放正常用。

另外,我强烈建议加一个频域的数据增强:随机频率遮挡。具体做法是在频域里随机把一些频率分量置零,模拟图像退化,让网络对频率缺失更鲁棒。这个操作在GFNet里实现特别自然,因为特征已经在频域了,直接乘一个随机mask就行。实测下来,加了这个增强,在低质量图像上的Dice能涨1-2个点。

def freq_mask(x_fft, mask_ratio=0.1): # x_fft: (B, C, H, W//2+1) 复数 B, C, H, W_half = x_fft.shape mask = torch.ones(B, C, H, W_half, device=x_fft.device) # 每个样本随机遮挡mask_ratio的频率 for b in range(B): n_mask = int(H * W_half * mask_ratio) idx = torch.randperm(H * W_half)[:n_mask] mask[b].view(-1)[idx] = 0 return x_fft * mask

4.4 损失函数:Dice和频域损失可以一起用

医学图像分割标配是Dice loss + BCE loss。用了GFNet之后,我额外加了一个频域损失:对预测mask和GT mask分别做FFT,计算它们频谱的L1距离。这个损失的直觉是,让网络不仅空间域预测对,频域分布也要接近。实测下来,加了频域损失,边界区域的HD95能降0.5-1mm,尤其是小目标(比如小病灶)的分割更完整。

频域损失的权重不用太大,我一般设成Dice loss的0.1-0.2倍。太大反而会让网络过度关注频率分布,忽略空间精度。代码上,就是对预测和GT做rfft2,取幅度谱,算L1:

def freq_loss(pred, target): pred_fft = torch.fft.rfft2(pred, norm='ortho') target_fft = torch.fft.rfft2(target, norm='ortho') return torch.mean(torch.abs(torch.abs(pred_fft) - torch.abs(target_fft)))

5. 实测中的坑与排查链路:从loss不降到边界模糊

5.1 第一个坑:loss完全不降,输出全是背景

这个坑我遇到过两次,排查过程记录一下。现象是训练了10个epoch,loss一直在0.69附近(二分类的随机水平),预测mask全是背景。排查链路:

第一步,检查数据。把GT和输入可视化出来,确认数据没问题,不是标签反了或者全黑。

第二步,检查GFNet模块的输出。单独跑一个batch,看GFNet的输入和输出。发现输出全是NaN。原因是FFT的norm='ortho'没加,正变换后数值很大,逆变换后数值更大,几层下来就溢出了。加上norm='ortho'后,数值范围正常。

第三步,检查滤波器初始化。发现滤波器初始化成了0,导致输出恒为0,残差连接后等于恒等映射,网络学不到东西。改成0.01的随机初始化后,loss开始下降。

第四步,检查学习率。滤波器学习率和卷积一样是1e-4,太小,滤波器几乎不动。改成1e-3后,收敛正常。

这个坑的教训是:频域模块的数值稳定性比空间域敏感得多,归一化、初始化、学习率三个都要对。

5.2 第二个坑:训练正常但边界模糊,HD95很高

这个坑更隐蔽。loss正常下降,Dice能到0.92,但HD95一直在10mm以上,边界像糊了一层。排查链路:

第一步,可视化频域滤波器。把学到的滤波器幅度谱画出来,发现高频部分几乎全是0,网络只学了低频。原因是医学图像里高频主要是噪声,网络倾向于抑制高频来降loss,但高频也包含边界信息,抑制过头就导致边界模糊。

第二步,检查数据增强。发现弹性形变强度太大,把边界结构破坏了,网络学到的是"模糊的边界"。

第三步,调整。把弹性形变强度降低,同时在频域损失里给高频加权(高频部分的L1权重乘2),强迫网络保留高频信息。调整后,HD95降到6mm左右。

这个坑的教训是:频域方法天然倾向于学低频(因为低频能量大),但医学图像分割的边界信息在高频,必须显式地保护高频。

5.3 第三个坑:推理时尺寸不匹配,滤波器插值出错

训练时用的是256x256的patch,推理时用整张512x512的图。GFNet的滤波器是按256x256初始化的,推理时特征图是512x512,需要插值。我一开始用最近邻插值,结果频率响应完全乱了,Dice掉了5个点。后来改成双线性插值,并且对实部和虚部分别插值,才恢复正常。

更稳妥的做法是:训练时就固定滤波器的空间尺寸(比如32x32),不管输入多大,都插值到这个固定尺寸做滤波。这样训练和推理一致,不会出现尺寸不匹配的问题。代价是损失一些高频细节,但实测Dice只掉0.3个点,比插值出错强多了。

5.4 第四个坑:多卡训练时FFT的数值不一致

这个坑比较冷门。用多卡DDP训练时,发现不同卡上的loss不一致,差了0.01左右。排查发现是FFT在不同卡上的数值精度有微小差异(cuFFT的实现问题),累积几层后放大。解决办法是:在GFNet模块里加一个torch.backends.cudnn.deterministic = True,并且用torch.fft而不是torch.fftpack(后者精度更差)。另外,把滤波器的数据类型统一成float32,不要用float16,混合精度下FFT的误差会更大。

如果非要用混合精度,我的经验是GFNet模块内部用float32,输入输出用autocast转换,这样既省显存又保证精度。

6. 一些让GFNet在分割任务上更稳的实战技巧

6.1 滤波器参数的正则化:幅度约束比weight decay更有效

前面说了滤波器不适合weight decay,但完全不约束又容易过拟合。我的做法是加一个幅度约束:限制滤波器幅度的均值在一个合理范围内。具体实现是在loss里加一项,惩罚滤波器幅度偏离目标值:

def filter_reg(filter_real, filter_imag, target_amp=1.0): amp = torch.sqrt(filter_real**2 + filter_imag**2) return torch.mean((amp - target_amp)**2)

这个正则的权重设成1e-4左右。它的作用是让滤波器保持一定的"活跃度",不要学成全是0或者全是极大值。实测下来,加了幅度约束,验证集Dice的方差变小了,训练更稳。

6.2 频域和空间域的混合:不要只用GFNet

纯GFNet的分割网络我试过,效果不如卷积+GFNet的混合结构。原因是频域滤波虽然全局,但对局部纹理的建模不如卷积精细。我的标配是:每个stage里,先两个3x3卷积做局部特征,再一个GFNet做全局混频,最后1x1卷积融合。这个结构在多个数据集上都比纯卷积和纯GFNet好。

另外,GFNet和注意力可以共存。我在bottleneck里同时放了GFNet和一个小窗口的self-attention,GFNet负责全局频率调制,attention负责关键区域的精细关系,两者互补,Dice又涨了0.5个点。当然,计算量也上去了,看你的资源。

6.3 推理加速:FFT的batch和尺寸怎么选

推理时,FFT的耗时跟batch size和特征图尺寸都相关。实测下来,batch size在8-16之间,FFT的GPU利用率最高;太小(1-4)利用率低,太大(32+)显存吃紧。特征图尺寸方面,32x32到64x64是甜点区,再大FFT耗时增长很快。

如果你的推理场景对延迟敏感,可以考虑把GFNet的FFT用半精度做,但前面说了精度会掉。折中方案是:滤波器用float32,FFT用float16,逆FFT后再转回float32。这样速度能快30%左右,精度损失在0.2个点以内。

6.4 可解释性:滤波器幅度谱能告诉你网络在学什么

GFNet有一个额外的好处是可解释性。训练完之后,把滤波器的幅度谱可视化出来,你能看到网络在哪些频率上响应强。我做过一个实验,在肝脏分割任务上,学到的滤波器在低频(对应器官整体形状)和中频(对应器官边界)响应强,高频(对应噪声)响应弱,这跟医学图像的先验完全吻合。如果发现滤波器在某个异常频率上响应特别强,可能是数据有问题(比如某种伪影),可以反过来排查数据质量。

这个特性在医学场景里挺有价值,因为医生往往想知道网络"看"到了什么。你可以把幅度谱叠加到原图上,给医生一个直观的解释。

7. 关于GFNet在医学图像分割里的一些个人体会

GFNet这个思路我用了大概一年多,从最初的"试试看"到后来成为我分割网络的标配模块,中间踩了不少坑,也积累了一些心得。最大的体会是:频域方法不是银弹,它的优势在全局建模和计算效率,劣势在局部精细度和数值稳定性。用对了场景,它能用很小的代价换来明显的精度提升;用错了场景,它可能还不如纯卷积。

具体来说,如果你的任务是高分辨率、大器官、边界模糊的分割(比如肝脏、肺、脑),GFNet很值得一试,尤其是插在bottleneck或decoder。如果你的任务是微小病灶、精细结构的分割(比如视网膜血管、肺结节),GFNet的全局性可能帮助有限,甚至因为高频抑制而伤害细节,这时候要么不用,要么配合高频保护策略。

另外,GFNet的实现细节比论文里写的要讲究。归一化、初始化、学习率、插值方式,每一个都可能决定成败。我见过不少人说"复现不出来",大部分是这些细节没对齐。如果你打算在自己的项目里用,建议先用我上面给的代码跑通一个简单数据集,确认loss能正常下降、边界能正常收敛,再往复杂场景迁移。

最后说一个我最近在尝试的方向:把GFNet的滤波器设计成频率自适应的,也就是不同频率用不同的学习率或者不同的正则强度。初步结果是,低频用大学习率快速学整体形状,高频用小学习率慢慢学边界细节,收敛更快,最终精度也略好。这个还在实验阶段,等稳定了再单独写一篇分享。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进