
做分割和检测的朋友应该都有过这种体会——你在 backbone 最后一两层把普通卷积替换成膨胀卷积想用它扩大感受野、补上下文结果调参调到头秃膨胀率设小了大目标的全局信息永远差一口气膨胀率设大了小目标的边缘和细节直接糊成一片。真正让人难受的是一张图里往往大小目标混在一起你选哪个膨胀率都注定要牺牲一部分目标只能赌哪个占多数。这篇要聊的 FADCFrequency-Adaptive Dilated Convolution频率自适应卷积就是冲着这个问题来的思路很直接既然高频区和低频区需要的信息粒度不一样那干脆把特征拆开看——高频区小膨胀精细采样保细节低频区大膨胀大步扩视野抓全局。等于给卷积配了一个会看菜下饭的动态采样器。我这次会从膨胀卷积为什么两头难讲起把 FADC 的三个核心组件拆开讲明白再给出一套可以复现的即插即用改造步骤、实测效果和训练中遇到的坑。适合正在做语义分割、目标检测、实例分割想给现成 backbone 加一点多尺度能力的朋友。1. 固定膨胀率的两难小目标细节和大目标上下文为什么顾此失彼1.1 膨胀卷积给网络带来的远近视力要理解 FADC 的价值得先回到膨胀卷积本身。普通卷积核在输入特征图上逐像素滑动每个输出位置看到的范围取决于卷积核大小3x3 就是 3x3。膨胀卷积在卷积核的相邻采样点之间插入空洞不增加参数却能成倍扩大感受野。一个常用的理解方式是普通卷积就像你站着不动看面前的桌子膨胀卷积更像是让你退后几步再看同一张桌子。膨胀率 r 越大每个采样点之间的间距越大你每只看眼睛能看到的外围信息就越广但代价是中间那些原本相邻的细节会被跳过。在密集预测任务里这个特性几乎是刚需。分割要判断一个大楼整体属于什么类别检测要抓到远处的小目标这些都需要网络在某一层把局部特征和上下文特征揉在一起。所以如今大多数 backbone 的 stage3、stage4 位置都有膨胀卷积的身影配合 ASPP 这类多分支结构把几种尺度的膨胀并联起来再融合输出。1.2 高频和低频对应的是细节和全局两类信息如果把一张特征图或图像做频域变换傅里叶、DCT 都行事情会变得清晰很多高频分量对应像素值变化剧烈的地方也就是边缘、纹理、小目标的边界、零碎细节低频分量对应变化平缓的区域比如大物体的内部、天空、路面也就是全局结构和上下文。生活里也有类似体验看一张远处的人群照片人脸是看不清的因为五官属于高频细节但你能看出人群聚集在哪片区域、大概多大范围这是低频信息在起作用。反过来你凑近屏幕盯着一张脸的轮廓时轮廓线很清楚但整张脸在画面中的位置关系反而变模糊了。卷积网络也面临同样的取舍。膨胀率小的时候采样点挨得近高频细节保留得好膨胀率大的时候采样点拉得开低频上下文覆盖得广。坏消息是这两个需求在同一个空间位置上是冲突的——你又想看清边缘又想把整个大目标包进感受野一个固定膨胀率做不到。1.3 多分支并联为什么只是治标有人会说这不是有标准答案吗——像 ASPP 那样搞几个平行分支膨胀率分别取 2、4、8最后 concat 起来多尺度不就全覆盖了确实ASPP 是现在最普遍的方案但它本质上还是预先把几个固定档位的眼镜都戴上再看哪副合适。问题是每个空间位置不论处于高频区还是低频区都会用同样的几组膨胀率去采样计算量被均匀摊给所有位置。真正需要大量上下文的平坦区域和需要精细采样的边缘区域在特征融合前始终是各管各的没有做到按需分配。另一个不那么明显的问题是多分支并联之后膨胀率档位是离散的中间尺度只能靠后续卷积去隐式插值。而真实场景里目标尺度分布是连续的小目标的位置也不固定离散档位往往只能覆盖大致范围到具体每个像素时依然是一个位置一套固定配置。所以你会发现固定膨胀率的矛盾本质上是单一策略套全图的矛盾。FADC 换了个思路——先判断每个位置是偏高频还是偏低频再让膨胀率真正跟着频率走一个位置一个采样策略。这就是标题里高频区小膨胀 低频区大膨胀这半句话的来源。2. FADC 的工作原理拆解把特征拆成高低频再让膨胀率跟着频率走2.1 三个组件一条流水线FD、FDP、FADCFADC 不是一个孤零零的新卷积核而是由三个模块组成的一条完整流水线。整体流程可以用一句话概括输入特征先做频率分解把高低频信息分开然后由一个频率感知的预测头判断每个空间位置应该用多大膨胀率最后带着这张膨胀率图去执行真正的自适应卷积。拆开看三个组件各管一段Frequency DecompositionFD把输入特征转成频域表示按频段拆出低频分量和高频分量再转回空间域。Frequency-aware Dilated PredictionFDP把低频、高频分量一起喂进去预测出每个空间位置对应的膨胀率实际是按通道组共享。Frequency-Adaptive Dilated ConvolutionFADC利用预测得到的膨胀率图对原特征图做一次非均匀采样卷积输出结果。这个设计最巧妙的地方在于把频率分解这件看起来像预处理的事直接嵌到了卷积的采样策略里。网络不再依赖某个卷积核暗地里学到多尺度而是显式地告诉每一层你在这个点该看近还是看远。2.2 FD为什么用 DCT 而不是直接教网络去滤波FD 的常规实现是用二维 DCT离散余弦变换把特征从空间域转到频率域。想到频率分解很多人第一反应是 FFT但 DCT 在工程上有两个明显优点它只产生实数系数不需要处理复数运算在 GPU 上的存储和计算都更友好DCT 的能量集中特性更强图像类特征用 DCT 转换后低频系数集中在左上角一个很紧凑的区域方便直接按矩形掩膜切成高低频。实际操作时说白了就是在频域上罩一个掩膜低频系数保留、高频置零逆变换回去得到的就是一个模糊版特征图反过来保留高频逆变换回去得到的就是细节差值图。两个图加起来能近似还原原始特征信息但已经被拆成两个侧重点完全不同的分支。论文里对频域系数做处理时用到了按极坐标构建的加权窗口不是简单一刀切的理想矩形滤波器。理解起来也不难极坐标下半径方向就代表频率高低角度方向代表纹理方向。按半径对系数做加权可以避免硬截断带来的振铃效应让高低频分量的过渡更平滑后面 FDP 预测膨胀率时也不会因为频带边界处的突变而抖动。我自己复现的时候先用简单矩形掩膜跑通流程再逐步替换成极坐标加权发现对训练稳定性的确有帮助。2.3 FDP膨胀率不是逐像素算出来的而是按组分拿到低频和高频分量后最直接的想法是每个像素都预测一个膨胀率。但这样做有两个问题一是完全没有空间平滑性相邻像素的膨胀率可能一个极大一个极小会导致特征出现盐噪点二是计算量爆炸膨胀率图和特征图一样大后续采样还得逐点执行。FDP 的做法是把通道分成若干组每组通道共享一个膨胀率图。输入是低频和高频特征在通道维上的拼接通道数为原来的两倍经过分组卷积处理输出维度是组数的膨胀率图。组数通常取 8 或者 16也就是说这一层网络最终学到的是 8~16 幅膨胀率分布图同一幅图内的位置共享膨胀策略。这里有个值得注意的细节预测膨胀率的卷积核尺寸不大作用域在局部范围内所以预测出的膨胀率天然有空间连续性。相邻像素之间膨胀率不会突变而是渐变的。这在实际采集时很重要——空洞采样最怕的就是采样位置跳变跳跃太大会让特征在空间上不连贯。膨胀率的数值范围在 FDP 中也有约束。论文设置的上限可以到 31也就是说在低频主导区域网络能够把采样范围扩展到很大。但训练初期如果直接放开到 31loss 极易不稳我后面会在踩坑章节细说。2.4 FADC 重采样常规卷积核不常规的采样位置最后一个环节也是命名之源把预测到的膨胀率真正用起来。这里的技术核心是非规则采样——不同空间位置的卷积核采样点间隔不同这已经不能用一个普通 Conv2d 表达通常会用可变形卷积deformable convolution的思路来实现。具体逻辑并不复杂。对于一个 3x3 卷积核每个采样方向可以用一个归一化偏移量表示。如果某个位置的膨胀率为 r那么该位置的卷积核采样点在标准位置的基础上往外推一个与 (r - 1) 成比例的距离。换句话说每个输出位置都有一组独立偏移量偏移量大小由膨胀率图决定。为了控制计算量这里并不对每个通道单独算偏移量而是与 FDP 的通道组设计保持一致同一组内的通道共享同一套偏移量。实现时FDP 输出的 group 张量会先经过一个轻量的映射层生成可变形卷积所需的偏移量场然后喂给可变形卷积完成采样。所以在最终的效果层面FADC 的行为是这样的当一个空间位置周围高频信息丰富比如小目标边缘、草丛纹理网络学到的膨胀率偏小卷积核老老实实贴着当前位置采样细节不丢当一个位置处于低频主导区域比如大片天空、路面、大物体内部网络会倾向于给出大膨胀率卷积核采样点向四周大幅扩展把远处的上下文也装进来。这个行为模式几乎就是把高频区小膨胀 低频区大膨胀这句话直接写进了网络结构里。3. 即插即用实操在现有 backbone 中替换普通卷积的完整步骤3.1 改哪里效果最明显优先选 stage3/stage4 的 3x3 卷积很多朋友拿到新模块第一反应是全部替换但我建议先想清楚替换目标。FADC 的核心收益是自适应感受野这个能力在分辨率高、特征还很浅的早期 stage 用起来帮助不大反而徒增算力开销到了 stage3/stage4特征图分辨率已经降到输入 1/16 或 1/32语义信息更浓缩这才是感受野策略能左右结果的位置。对应到常见结构里ResNet-like backbone 的 stage4 通常有一个 3x3 卷积在检测/分割模型中往往还被改成 dilation2 或更大。把这一层替换成 FADC保留原来的残差连接是比较标准的做法。如果你想更激进一点可以连 stage3 的那个 3x3 一起换但先只换一个位置对比收益再决定要不要扩大范围这个习惯能少走很多弯路。3.2 核心代码骨架频率分解、膨胀率预测、自适应卷积下面给一份便于理解原理的 PyTorch 风格伪代码不是官方完整实现但结构和调用关系足够清晰。真实跑实验时建议对照公开源码做细节修正。import torch import torch.nn as nn import torch.nn.functional as F class FrequencyDecomposition(nn.Module): 将输入特征按频段分解为低频分量和高频分量。 工程化做法二维DCT可以拆成两次一维DCT执行 也可以用预计算的DCT矩阵做矩阵乘法GPU上更友好。 def __init__(self, ratio0.4): super().__init__() self.ratio ratio def forward(self, x): b, c, h, w x.shape coeff dct2d(x) # 占位函数完整实现由两次一维DCT组成 # 构建低频/高频掩膜 lh int(h * self.ratio) lw int(w * self.ratio) mask torch.zeros(h, w, devicex.device) mask[:lh, :lw] 1.0 low_coeff coeff * mask high_coeff coeff * (1 - mask) low idct2d(low_coeff) high idct2d(high_coeff) return low, highclass FrequencyAwareDilatedPrediction(nn.Module): 输入低频高频特征输出每组的膨胀率图。 这里用 sigmoid 将输出约束到 [1, max_dilation]。 论文实现中还会叠加极坐标加权窗口这里为清晰起见省略。 def __init__(self, in_channels, groups8, max_dilation31): super().__init__() self.groups groups self.max_dilation max_dilation self.ratio_conv nn.Conv2d(in_channels * 2, groups, 3, padding1) def forward(self, low, high): feat torch.cat([low, high], dim1) logit self.ratio_conv(feat) # [B, groups, H, W] ratio torch.sigmoid(logit) * (self.max_dilation - 1) 1 return ratioclass FADConv(nn.Module): 自适应膨胀卷积利用膨胀率图生成采样偏移 借助可变形卷积完成非规则采样。 这里保留结构示意完整可变形卷积请参考公开实现。 def __init__(self, in_channels, out_channels, kernel_size3, groups8): super().__init__() self.groups groups self.offset_conv nn.Conv2d(groups, 2 * kernel_size * kernel_size, 3, padding1) self.deform_conv DeformConv2d(in_channels, in_channels, kernel_size, paddingkernel_size // 2) self.point_conv nn.Conv2d(in_channels, out_channels, 1) def forward(self, x, ratio): # ratio: [B, groups, H, W] -- 生成每个组共享的偏移量 offset self.offset_conv(ratio) out self.deform_conv(x, offset) return self.point_conv(out)调用流程就三段class FADCBlock(nn.Module): def __init__(self, channels, groups8): super().__init__() self.fd FrequencyDecomposition(ratio0.4) self.fdp FrequencyAwareDilatedPrediction(channels, groups) self.fadc FADConv(channels, channels, groupsgroups) def forward(self, x): identity x low, high self.fd(x) ratio self.fdp(low, high) out self.fadc(x, ratio) return out identity替换时注意保持输入输出通道数不变否则残差连接对不上。3.3 加载预训练权重的关键点别让新分支把旧权重带跑这是即插即用最容易翻车的一步。FADC 新加的分支FDP、offset 映射在你的模型里属于全新参数而原来那个 3x3 卷积是带预训练权重的。如果你图省事直接load_state_dict系统会报一大堆 missing key你要么硬着头皮从零训要么瞎初始化。我的做法是三步走先把原始 backbone 权重加载进来确认哪些 key 是存在的然后单独提取原 3x3 卷积的权重拷贝给 FADConv 内部对应的 depth_conv让它一上来就有一个合理起点最后对 FDP 和 offset_conv 做小标准差初始化标准差建议取 0.01 或更小让膨胀率预测在训练初期偏向小膨胀率。这样做的好处很明显早期阶段网络主要依赖原有的卷积采样新分支只是在边缘微调膨胀策略不会因为膨胀率一开始就乱跳导致 loss 飞升。等训练几万步之后再让它慢慢放开手脚。3.4 常用超参配置参考下面这张表是我在分割任务上调试时采用的初始配置效果稳定可以作为起步值超参数建议值说明替换位置stage4 首个 3x3 卷积分辨率低语义强性价比最高通道组数 groups8组数越少越省算力但自适应粒度变粗频率分解比例 ratio0.4保留低频比例过高或过低会偏科膨胀率上限31论文默认实际训练初期可先约束到 17新分支初始化标准差0.01保证训练初期膨胀率预测接近 1~2与 ASPP 的关系二选一同时保留会重复建议去掉 ASPP 的大膨胀分支这个配置在开源街景分割数据集上复现时相比原始 baseline 有明显改善尤其是多尺度目标混合的区域。如果你只做单类目标检测可能收益不大后面会更细说。4. 实测效果与开销账精度提升在哪里代价是什么4.1 哪些任务收益明显哪些任务几乎不动先说结论FADC 在语义分割、小目标检测、实例分割这类目标尺度差异大的任务上收益最明显。原因也不难理解这些任务要求网络同时处理密集的细节和广阔的背景上下文而 FADC 恰好提供了一条按需分配感受野的路径。我在自己项目里观察到的具体变化最明显的是两处小目标边缘的分割质量提升。以前小目标常常被糊掉要么和背景粘连要么边界缺一块替换 FADC 后召回明显变好大目标内部的类别一致性也更好以前大物体内部容易出现零碎的错误分类块自适应的大膨胀率让每个位置更易看到全局错误块变少。反过来如果任务本身就是单一尺度的大目标检测或者类别极少、画面极其简单FADC 的收益会缩水。这种场景下一个固定合适的膨胀率本来就够用自适应带来的能力是多余的。纯分类任务也不建议用分类网络最后靠的不是密集感受野而是全局池化替换了基本属于白费算力。4.2 从膨胀率分布看网络学会了什么调试过程中我常做的一个验证把 FDP 输出的膨胀率图画出来叠在原图上观察。正常训练收敛后你会发现几个规律物体边缘、线条密集的地方膨胀率图上的数值明显偏小大片平坦区域天空、墙面、路面数值偏大同一个物体内部膨胀率也会从边缘向中心逐渐变大。这个现象很有说服力——说明 FADC 确实学到了标题里那套行为模式而不是简单把所有位置都调成中间值。如果你的膨胀率图看起来一片均匀基本是频率分解的比例没调对或者 FDP 没有有效接收到高频细节信息这时候去调 ratio 和分组数比瞎改学习率管用。4.3 算力开销到底多大DCT 没有想象中吓人很多人听到频率分解第一反应是 DCT 很重。事实上二维 DCT 可以拆成两次一维变换而且变换矩阵可以在初始化时一次性算好之后用矩阵乘法替代GPU 上跑起来非常快甚至比某些注意力模块更省。我实测过整体开销替换一个 stage4 卷积为 FADC额外耗时主要集中在 offset 生成和可变形卷积上约占整个模型推理时间的 3%~6%DCT 本身占比反而不高。训练阶段因为新分支梯度更新显存占用增加约 5%~10%这在大多数设备上都可以接受。方案感受野策略额外参数额外推理耗时估多尺度目标友好度普通膨胀卷积全局固定00低ASPP 多分支固定档位融合中中中FADC逐位置自适应低3%~6%高如果你预算特别紧张也可以只替换 stage4 一个卷积收益依然可观。我个人习惯是先小范围替换确认有效再决定要不要铺开而不是一开始就全网络更换。5. 训练与调试阶段的坑我复现过程里处理过的几个典型问题5.1 频率分解对输入尺寸敏感直接套用容易崩FD 模块的 DCT 是定义在固定 H、W 上的一旦输入特征图尺寸在训练过程中发生变化比如多尺度训练、随机裁剪原来算好的变换矩阵就失效了。这个问题我是在把 FADC 接到一个带多尺度增强的检测模型时遇到的——训练到一半程序直接报错排查半天发现是输入尺寸从 320 换到 640矩阵维度对不上。解决思路很朴素在 FD 入口处做尺寸统一用反射填充或最近邻插值把特征图对齐到固定长宽序列完成分解后再把结果切回原尺寸。注意填充要在分解前做否则高低频分量会被填充边缘污染。如果你不做多尺度训练只是固定输入尺寸直接一次性对每个目标尺寸预计算 DCT 矩阵缓存起来即可。5.2 训练初期 loss 抖动膨胀率上限要先锁住FDP 刚随机初始化时输出的膨胀率基本是乱的可能某个位置直接给出 20 以上的大膨胀率。采样点瞬间跳到远处背景上梯度方向完全错误于是呈现出来的就是 loss 在训练前几千步疯狂抖动。我试过几种缓解方案最后固定下来的是前 10% 的训练轮次把膨胀率上限锁在 5之后每过一段再逐步放开到 31。锁定的方式也很简单把 sigmoid 输出乘以当前阶段允许的最大值推导出来的缩放系数就行。这个做法相当于给自适应感受野加了一个预热期让主干卷积和新分支先互相熟悉再放大自由度。还有一个辅助手段把 FDP 初始化偏向小膨胀率。前面提到的新分支权重用小标准差初始化配合 sigmoid 的天然下限 1训练初期大多数位置膨胀率会落在 1~3 之间loss 自然稳定。5.3 和 ASPP 同时用效果反而变差FADC 本身已经实现了自适应多尺度采样如果你原来的结构里还留着 ASPP两边的能力会重叠。我第一次做对比实验时没想太多直接替换完发现 mIoU 只涨了一点点甚至某些类别还倒退。后来把 ASPP 拆开观察每一步输出才意识到FADC 在低频区给出的膨胀率已经可以覆盖 ASPP 里大膨胀率分支的采样范围高频区小膨胀率又已经把细节处理得很好ASPP 的大分支几乎只剩重复计算。最终我把 ASPP 简化成一个 1x1 卷积分支再跟 FADC 配合精度反而更高算力也降了。这其实点出一个通用原则即插即用模块虽然方便但你要清楚它和现有结构之间存在什么能力重叠。FADC 这类自适应感受野模块更适合作为膨胀卷积的升级替代而不是ASPP 的补充配件。5.4 可视化的价值用膨胀率直方图判断收敛最后一个建议每次训练完 FADC先别急着看指标把膨胀率图拉出来做一次可视化分析。我通常用一个小工具脚本统计一个 batch 里膨胀率的均值、方差和直方图再随机挑选几张图叠加显示。正常收敛状态应该是整体膨胀率分布比较宽有相当一部分位置集中在 1~5高频细节区也有相当一部分集中在 15 以上低频全局区中间过渡自然。如果直方图几乎都挤在低值区说明 FDP 根本没学会大范围上下文多半是频率分解的比例设置太小高频分量占了主导如果所有位置都飙到高值区说明高频细节被淹没边缘会出现颗粒状噪声。做对了这一步后面调参基本就是按图索骥不会像无头苍蝇一样乱试。6. 一点使用体会什么时候该用 FADC什么时候别用跑了一轮实验之后我最大的感受是频率自适应这个方向比单纯堆多分支并行更能触及问题的本质。像 ASPP 那类方案属于准备几副眼镜挨个试FADC 则是先判断远处还是近处再决定戴哪副眼镜后者显然更省力也更有针对性。按我自己的经验如果你的任务满足这几个条件非常值得试试 FADC图像里目标尺度分布很宽大小目标要同时兼顾边缘细节对结果影响大比如分割小物体、检测远距离目标你手头有现成的预训练 backbone只是想让它对多尺度更友好。反过来如果任务场景单一比如工业缺陷检测目标大小几乎固定或者计算资源极度紧张那 FADC 的收益确实有限。这时候老老实实用固定膨胀率、甚至普通卷积都行没必要为了新而强行上模块。最后分享一个我在多尺度改进上屡试不爽的顺带技巧训练过程中可以定期把 FDP 的膨胀率分布导出看看网络是不是真的按边缘小、区域大的方式学习。一旦看到这个规律说明模块逻辑生效后面微调学习率和数据增强就能顺水推舟如果看不到规律优先排查频率分解和膨胀率初始化别急着堆训练时长。好的结构设计最终都应该能经得起可视化这一关。