卷积这个词,做信号处理的人天天挂在嘴边,做深度学习的人更是绕不开。但很多人对它的理解停留在“滑动窗口加权求和”这个层面,一旦遇到空洞卷积、多尺度特征金字塔、图卷积这类变形,脑子里就有点乱。其实这些变形背后有一条很统一的主线,就是卷积的尺度特性。你把这个东西悟透了,再去理解为什么空洞卷积能扩大感受野、为什么池化会丢信息、为什么对信号先下采样再滤波和先滤波再下采样结果不一样,就会顺畅很多。这篇内容我打算从数学骨架讲到工程实操,把尺度这个维度彻底摊开讲清楚,适合已经用过卷积但想搞明白底层逻辑的人,也适合刚入门、想少走弯路的同学。文中给的代码都能直接跑,参数计算我会把过程写全,不跳步。
1. 为什么尺度是理解卷积的第一把钥匙
1.1 从一个日常场景说起
你可以把卷积想象成用一把刷子去刷一条长长的画布。刷子有大小,也就是卷积核的尺寸;刷子每次挪多远,也就是步长;刷子上的毛怎么分布,那就是核里的权重。现在的问题是:同样一条画布,你换一把大小不同的刷子,刷出来的效果完全是两回事。小刷子能描出细节纹理,大刷子只能拍出大块色块。这个“刷子大小相对画布纹理大小的关系”,就是尺度问题。
我在实际做音频降噪的时候踩过这个坑。一段语音信号里既有低频的元音共振峰,又有高频的摩擦音。我一开始用了一个很窄的平滑核去做卷积,结果是摩擦音被磨掉了,听起来像含着东西说话;换成大核,低频是稳了,可每一个辅音都被抹平,整句话糊成一团。这不是核选得对不对的问题,而是核的尺度跟你要保留的成分尺度不匹配。所以理解卷积,第一步不是背公式,而是建立“尺度匹配”的直觉。
1.2 尺度特性到底指什么
严格来讲,卷积的尺度特性包含两层意思,一层是等变性,一层是分辨率的变化。等变性说的是,如果输入信号在时间轴上被拉伸了 a 倍,输出也会被拉伸 a 倍,只是幅度要除以一个系数。这个性质说明卷积本身对尺度没法自动适应,它不会因为你把信号放大就自动调整核的大小。第二层是分辨率层面,当你对信号做下采样,或者用带步长的卷积,特征图的空间尺寸变了,同一层的卷积核此时覆盖的原始输入范围就变了,这就是深层网络里“感受野随层数增长”的根本原因。
很多人第一次听到“卷积不具备尺度不变性”会有点懵,因为它明明具备平移不变性。区别在于,平移不变性是卷积天生自带的,你把信号整体挪一段,输出只是跟着挪,形状不变;但你把信号整体缩放,输出形状就变了。想让卷积对尺度也鲁棒,必须额外设计,比如多尺度并行、尺度金字塔、或者在数据端做尺度增广。这也是为什么图像分类网络里普遍要加多尺度训练,光靠堆卷积层是解决不了尺度问题的。
提示:如果你在做的是目标检测或者关键词唤醒这类对尺度敏感的任务,只堆卷积层而不做尺度相关的结构设计,模型上限会被卡得很死。
2. 卷积尺度特性的数学骨架
2.1 连续卷积的尺度变换公式
先把连续卷积的定义摆出来。对于两个连续函数 f 和 g,它们的卷积是:
(f * g)(t) = ∫ f(τ) g(t - τ) dτ
现在定义尺度算子 S_a,它把函数变成 (S_a f)(t) = f(a t),也就是把时间轴压缩了 a 倍(a 大于 1 时是压缩,a 小于 1 时是拉伸)。把这个算子同时作用到 f 和 g 上,再去算卷积:
(S_a f * S_a g)(t) = ∫ f(a τ) g(a(t - τ)) dτ
做一个换元,令 u = a τ,则 dτ = du / a。假设 a 大于 0,积分变成:
= ∫ f(u) g(a t - u) (1/a) du = (1/a) (f * g)(a t)
如果考虑 a 为负数的一般情况,系数要写成 1/|a|。所以完整的尺度变换性质是:
(S_a f * S_a g)(t) = (1/|a|) (f * g)(a t)
这个式子信息量很大。第一,输出的时间轴同样被压缩了 a 倍,形态上是“跟着缩放”的,这叫等变性。第二,幅度要乘 1/|a|,这是因为面积守恒。你可以拿一个矩形脉冲和它自己卷积得到三角波来验算,信源压缩一半,三角波也压缩一半,但峰值高度加倍,正好被 1/|a| 抵消掉一部分。
这里的关键推论是:卷积不会自动对尺度不变。如果输入被缩放而核保持不变,就相当于核的相对尺度变了,结果等价于用了一个完全不同形状的相对核。这就是为什么多尺度任务里,你不能指望同一个卷积核通吃所有尺度的目标。
2.2 离散卷积、下采样与多相分解
离散情况更有意思。设离散信号 x[n],卷积核 h[n],标准卷积是 y[n] = Σ_k h[k] x[n - k]。现在引入下采样算子,记作 ↓M,表示每 M 个点取一个,得到 x_d[n] = x[Mn]。问题来了:先卷积再下采样,和先下采样再卷积,结果一样吗?
答案是一般不一样。先卷积再下采样得到的是 (h * x)[Mn],而下采样后再用同样的核卷积,是 h * (x[Mn])。这两个只有在核满足特殊条件时才等价。要严格分析这个问题,需要多速率信号处理里的多相分解。把滤波器 h 按相位拆成 M 个子滤波器 h_0, h_1, ..., h_{M-1},其中 h_m[n] = h[Mn + m]。下采样后的输出在 z 域可以写成:
Y(z) = Σ_{m=0}^{M-1} z^{-m} H_m(z^M) X(z)
这个分解的价值在于,它把“高采样率下做一次大卷积”等价变换成“低采样率下做 M 次小卷积再合并”,计算量能大幅下降。工程上很多高效卷积实现,比如某些音频重采样库里的高效滤波,用的就是这个思路。你如果做过重采样,会发现输出信号里偶尔混进一些不该有的镜像成分,那多半就是抗混叠滤波和下采样顺序没处理好。
我把这件事总结成一句能记住的话:卷积和尺度变换(下采样)不可随意交换,交换的代价由核的多相结构决定。你在设计带步长的卷积层时,其实就是在做这个交换,所以步长卷积和非重叠池化的行为差异,根源就在这里。
2.3 感受野递推:尺度在深层网络里的传播
深度学习里讨论尺度,绕不开感受野。感受野是某一层输出上的一个像素,对应到原始输入上覆盖的区域大小。随着层数加深,感受野单调增大,网络看到的“尺度”就越来越大。递推公式是这样:
RF_i = RF_{i-1} + (k_i - 1) × ∏_{j=1}^{i-1} s_j
这里 RF_i 是第 i 层感受野,k_i 是第 i 层核大小,s_j 是第 j 层步长,RF_0 取 1。这个公式一定要会手算,因为很多结构选型决策就靠它。
举个具体例子。假设网络前三层都是 3×3 卷积,步长都是 1:
RF_1 = 1 + (3-1) × 1 = 3
RF_2 = 3 + (3-1) × 1 = 5
RF_3 = 5 + (3-1) × 1 = 7
三层 3×3 下来感受野是 7,这正好等于一层 7×7。这说明多个小核对单个大核在感受野上是等效的,但参数量上 3×3×3 = 27 远小于 7×7 = 49,而且多了两次非线性激活,表达能力更强。这是 VGG 那套设计的核心理由。
现在把第二层步长改成 2:
RF_1 = 3
RF_2 = 3 + (3-1) × 1 = 5
RF_3 = 5 + (3-1) × 2 = 13
你看,仅仅把第二层步长从 1 改成 2,第三层感受野就从 7 跳到了 13,几乎翻倍。这就是为什么下采样层放得越靠前,感受野膨胀得越快。但代价是分辨率下降,细节损失。所以实际网络设计就是在“感受野要够大”和“分辨率要保住”之间找平衡,这也是很多现代网络把下采样集中放在浅层、深层保持高分辨率的思路来源。
3. 主流卷积变体的尺度设计逻辑
3.1 空洞卷积:不加参数换大尺度观测窗
空洞卷积(也叫带孔卷积)是我个人最喜欢的一个尺度工具。它的做法是在卷积核的元素之间插入空洞,让核的有效尺寸变大,但不增加参数。设原始核大小 k,膨胀率 r,有效核大小是:
k_eff = k + (k - 1) (r - 1) = (k - 1) r + 1
拿 3×3 核、膨胀率 2 举例,k_eff = (3-1) × 2 + 1 = 5,相当于 5×5 的感受野,但参数还是 9 个。膨胀率 3 时就是 7×7 的感受野。这在语义分割里非常有用,因为你既要保留像素级的分辨率,又需要足够大的上下文。
但空洞卷积有个著名的坑,叫网格效应(gridding)。当你连续堆叠几层膨胀率相同的空洞卷积时,采样点会呈稀疏网格状分布,中间出现大量没被覆盖的空洞,导致局部信息丢失。我实测过一个膨胀率全是 2 的三层叠加,在细粒度分割任务上边缘明显锯齿。解决办法是采用混合膨胀率,比如按 1、2、3 循环排列,或者照 HDC 那套思路,让相邻层的膨胀率满足最大公约数为 1 的约束。具体来说,设第 i 层膨胀率 r_i,要求任意两层之间 M_i = max(M_{i+1} - 2 r_i, M_{i+1} - 2(M_{i+1} - r_i), r_i) 不超过核大小 k,这样能保证感受野被完整覆盖。
注意:膨胀率不是随便堆的,堆之前先用上面这个覆盖约束检查一遍,能省掉很多调试时间。
3.2 池化与步长卷积:显式改变尺度的手段
池化和步长卷积是两种显式降分辨率的手段,但它们改变尺度的方式不同。池化是固定窗口取最大值或平均值,不含可学习参数;步长卷积是带着可学习核、隔着步长采样。从尺度特性角度看,平均池化本质上就是一个核里全是 1/窗口大小 的固定卷积,乘上步长,所以它同样受 2.1 那个尺度公式约束。最大池化则不同,它是非线性操作,尺度行为更复杂,不满足简单线性缩放关系。
我做实验对比过这两者在分类任务上的差异。同样的降采样比例,用步长卷积替代平均池化,通常能带来一两个点的精度提升,因为步长卷积在降采样的同时还在学习特征。但代价是参数量和计算量都上去了。最大池化则在保留显著特征上更强,尤其对纹理突出的任务,比如边缘检测。选择哪种,取决于你对“降采样时要不要顺便学特征”这个问题的答案。
还有一个容易忽略的点,池化的窗口大小和步长如果设计成不整除,会出现边缘信息丢失或者padding后引入伪影。我一般建议降采样用 2×2 窗口、步长 2,或者 3×3、步长 2 加 padding,尽量让输入尺寸是 2 的整数次幂,这样每一层都能整除,避免半像素级别的错位。这个细节在浅层网络里看不出来,但深层里误差会被放大。
3.3 深度可分离卷积与多尺度结构
深度可分离卷积是另一个跟尺度密切相关的结构。它把标准卷积拆成两步:逐通道卷积负责空间尺度上的局部聚合,逐点卷积负责通道之间的信息融合。参数量对比很直接,标准卷积是 k × k × C_in × C_out,深度可分离是 k × k × C_in + C_in × C_out,比值约为 1/C_out + 1/k²。
以 3×3 核、输入通道 128、输出通道 256 为例。标准卷积参数量是 3×3×128×256 = 294912。深度可分离是 3×3×128 + 128×256 = 1152 + 32768 = 33920。参数量降到约 1/8.7。这个压缩幅度意味着你可以用省下来的预算去堆更多层,从而在相同算力下获得更大的有效感受野。MobileNet 系列就是靠这个思路在移动端跑起来的。
但深度可分离卷积有个尺度上的软肋:逐通道卷积用的是独立核,各个通道的空间尺度是独立决定的,容易出现通道间尺度不一致。缓解办法是在逐通道卷积后面接通道注意力或者尺度归一化。我在一个轻量分割项目里试过在深度可分离块后面加一层轻量的通道加权,小目标的分割精度提升了大约两个点,代价只是多了很少的参数。
多尺度结构则是把大中小核并行使用,然后把结果在通道维拼接或相加。Inception 系列是典型代表,用 1×1、3×3、5×5 三路并行,再加一个池化路。它的好处是网络自己学着决定用哪个尺度,不需要人工指定。相关变体里还有一种门控卷积,它用一条支路生成门控权重去调制另一条支路的输出,让网络按输入内容动态选择有效尺度。门控机制在序列任务里尤其常见,因为它能根据上下文抑制不相关的尺度响应。
3.4 图卷积与球面卷积:非欧空间的尺度难题
前面讲的都是在规则网格(时间轴、图像像素阵列)上的卷积,尺度靠核大小和步长来控制。到了图结构和球面上,尺度就没这么好定义了。图卷积里,节点没有固定的邻域顺序,也没有统一的“距离”概念,所以尺度得靠跳数或者谱域上的频率来定义。自适应图卷积的做法是让邻接矩阵本身变成可学习的,让网络自己找出哪些节点在语义上属于同一尺度。这在骨骼动作识别里效果很明显,因为不同动作的关节关联尺度差别很大。
球面卷积更麻烦,因为球面上没有平移不变性可用,你没法直接滑动一个核。常见做法是把球面信号变换到球谐域,在频域上做乘法(对应空间上的卷积),然后用不同阶数的球谐系数控制尺度。低阶系数描述大尺度结构,高阶系数描述细节。这跟傅里叶域里低频高频的对应关系是一样的。我在处理全景图像的时候深有体会,直接在经纬图上做普通卷积,两极附近会严重失真,因为像素间距在两极被压缩了,必须换成球面卷积或者至少做等面积投影。
这三类结构给我的共同启发是:卷积的尺度特性本质由“邻域如何定义”决定。在规则网格上邻域是几何固定的,在图上是拓扑关系,在球面上是球谐阶数。理解了这一层,你就能判断一个新提出的卷积变体到底在解决什么尺度问题,而不是被各种名字绕晕。
4. 动手验证:卷积尺度特性的代码实操
4.1 验证连续尺度变换公式
先来验证 2.1 里那个核心公式。思路很简单:构造一个信号和核,算一遍原始卷积,再把两者都按同一比例压缩,算一遍,看结果是不是符合 (1/|a|)(f*g)(at)。用 numpy 就能做,把时间轴离散化,积分换成求和。
import numpy as np def conv_manual(f, g, dt): # 直接按定义做离散近似卷积 n = len(f) m = len(g) out = np.zeros(n + m - 1) for i in range(n): for j in range(m): out[i + j] += f[i] * g[j] * dt return out # 原始信号:一个矩形脉冲,核:另一个矩形脉冲 dt = 0.01 t = np.arange(0, 2, dt) f = ((t > 0.5) & (t < 1.0)).astype(float) g = ((t > 0.0) & (t < 0.5)).astype(float) y1 = conv_manual(f, g, dt) # 压缩 a 倍:对时间轴重采样 a = 2.0 t2 = np.arange(0, 1, dt) f2 = ((t2 > 0.25) & (t2 < 0.5)).astype(float) g2 = ((t2 > 0.0) & (t2 < 0.25)).astype(float) y2 = conv_manual(f2, g2, dt) # 理论预测:y2 应该等于 (1/a) * y1 在压缩后的时间轴上取值 print("压缩后卷积峰值:", y2.max()) print("理论预测峰值:", y1.max() / a)跑下来你会发现峰值对得上,形状也吻合。这个实验能帮你把抽象的尺度公式变成手上能摸到的东西。第一次看公式记不住 1/|a| 这个系数的人,跑一遍就记住了。
4.2 空洞卷积有效核与感受野计算
第二步,把空洞卷积的有效核和感受野算清楚。我写了一个小函数,输入每层的核大小、步长、膨胀率,输出每层感受野和有效核尺寸,方便你直接抄去用。
def receptive_field(k_list, s_list, r_list): rf = 1 # RF_0 stride_prod = 1 # 累积步长 results = [] for i, (k, s, r) in enumerate(zip(k_list, s_list, r_list)): k_eff = (k - 1) * r + 1 rf = rf + (k_eff - 1) * stride_prod stride_prod *= s results.append((i + 1, k_eff, rf, stride_prod)) return results # 例子:3层3x3,步长1,膨胀率分别为1、2、3 for layer, k_eff, rf, sp in receptive_field( [3, 3, 3], [1, 1, 1], [1, 2, 3]): print(f"第{layer}层 有效核={k_eff} 感受野={rf} 累积步长={sp}")输出会告诉你,三层下来感受野是 1 + 2 + 4 + 6 = 13,而不是普通三层 3×3 的 7。膨胀率翻了感受野,但参数没变。这里要注意膨胀率的选择,像我前面说的网格效应,如果三层都用 2,你会看到有效核都是 5,但覆盖点稀疏。改用 1、2、3 之后覆盖就密实多了。
4.3 下采样与卷积交换的实验对比
第三步,验证 2.2 里说的“先卷积再下采样”和“先下采样再卷积”不一样。这个实验能让你亲眼看到多相分解的必要性。
import numpy as np np.random.seed(0) x = np.random.randn(32) h = np.array([0.25, 0.5, 0.25]) # 一个简单平滑核 # 路径A:先卷积再2倍下采样 y_full = np.convolve(x, h, mode='same') path_a = y_full[::2] # 路径B:先2倍下采样再卷积 x_down = x[::2] path_b = np.convolve(x_down, h, mode='same') print("路径A:", np.round(path_a[:8], 3)) print("路径B:", np.round(path_b[:8], 3)) print("最大差异:", np.abs(path_a - path_b).max())跑一下会发现两条路径的输出差异不小。原因就是先下采样会丢失高频信息,后面的卷积没法把它找回来;而先卷积保住了高频过滤后的结果再抽取,信息保留更完整。这就是抗混叠滤波要放在下采样之前的道理。工程上做音频重采样、图像缩放,几乎都要先低通滤波再降采样,用的就是路径A的结构,不是为了好看,是因为路径B真的会引入混叠失真。
我还试过用多相分解把路径A的计算量降下来。把核按奇数偶数相位拆开,分别在低采样率下卷积再合并,输出和路径A完全一致,但乘法次数少了一半左右。这个技巧在嵌入式音频处理里很常见,值得掌握。
5. 常见问题与排查技巧
5.1 网格效应与空洞卷积的坑
空洞卷积最典型的故障就是网格效应。症状是分割结果的边缘出现规律性锯齿,或者小目标整块消失。排查方法很简单:把中间层的特征图可视化出来,如果看到采样点呈规整的稀疏网格,基本就确诊了。修复手段有三条,一是混合膨胀率,让每层膨胀率不同且最大公约数为 1;二是用可变形卷积,让采样位置自己学偏移;三是在空洞卷积后面接一个普通卷积做信息补全。我一般首选第一条,改动最小,效果稳定。
还有一个坑是膨胀率过大导致远距离采样引入的伪相关。膨胀率设成 6 以上的时候,核覆盖范围很大,但中间隔得太远,两组本来不相关的特征会被强行关联。我在一个长序列任务里把膨胀率设到 8,结果模型把句子头部和尾部的噪声关联起来了。经验法则是膨胀率不要超过当前特征图尺寸的 1/4,超了就考虑先降分辨率再上大膨胀率。
5.2 尺度不匹配导致的典型问题
尺度不匹配的表现五花八门,但有几个高频场景。第一个是小目标检测漏检,多半是浅层感受野太小、深层感受野太大,中间尺度缺档。解决办法是加特征金字塔,把不同层的特征融合起来,让每个尺度都有对应的检测头。第二个是语音识别里的语速变化,同一个人说快说慢,音素时长差一倍,固定尺度的卷积就吃不消。这时候要么在数据端做时间拉伸增广,要么在模型里用多尺度池化。
第三个是图卷积里的过平滑。层数堆多了,不同尺度节点的表示被反复平均,最后全都趋同,小尺度结构信息全丢。缓解办法是加残差连接,或者用自适应图卷积让邻接矩阵自己调,把不相关的边权重压下去。这些问题的共同点都是“尺度的覆盖范围和任务需求对不上”,排查时先问自己一句:我的任务里目标或模式的尺度跨度有多大,我的结构覆盖到了吗。
5.3 常见问题速查表
把上面的问题整理成一张表,方便你遇到时对照。
| 现象 | 可能原因 | 排查手段 | 推荐修法 |
|---|---|---|---|
| 分割边缘锯齿 | 空洞卷积网格效应 | 可视化采样点分布 | 混合膨胀率,最大公约数为 1 |
| 小目标整块丢失 | 中间尺度感受野缺档 | 逐层算感受野 | 加特征金字塔融合 |
| 语音识别语速敏感 | 卷积尺度固定 | 测不同语速下的精度 | 时间拉伸增广 + 多尺度池化 |
| 图卷积层深后失效 | 过平滑 | 看节点表示相似度 | 残差连接 + 自适应邻接 |
| 重采样有混叠杂音 | 先降采样后滤波 | 对比两条路径输出 | 抗混叠滤波前置 |
| 全景图两极失真 | 用普通卷积处理球面 | 看两极区域 | 球面卷积或等面积投影 |
最后分享一个我自己常用的调试习惯:任何涉及尺度改动的新结构,上大模型之前,先在一个只有几层的沙盒网络里把感受野手算一遍,再用随机输入跑一遍看特征图形状。这一步花不了十分钟,但能挡掉绝大多数因为维度或尺度对不上导致的报错和精度异常。踩过几次尺度不匹配的坑之后,我现在设计网络第一件事就是画一张尺度覆盖表,把每一层的感受野和任务目标尺度列出来对照,比事后调参省心太多。