卷 IV · 相乘CH 13深度 13/20

模糊、回声、平均、掷骰子是同一件事

前三卷讲的都是「频域怎么看」和「看要付多少钱」。这一卷回答那个一直没问的问题:那我们为什么还要跑这一趟。答案是一条定理,而这一章先把定理的主角请出来——一个你每天都在用、但多半没意识到它有名字的运算。

1 3 5 3四件事是一件事LTI 的唯一形状

▷ 先猜一下

掷两个骰子,求和的概率分布——你大概记得它是个三角形,7 最容易出(6/36)。

问:下面四件事里,哪几件和「求两个骰子的和的分布」用的是同一个运算?

① 把一张照片做高斯模糊 ② 给一段干声加混响 ③ 把两个多项式相乘 ④ 对一条时间序列做 5 点滑动平均

A 只有 ④。滑动平均和概率求和都是「加权求和」,其他三个不是 B ① 和 ④。图像模糊也是加权求和,另外两个是别的东西 C ①②④。混响也算,但多项式相乘是纯代数,和信号处理没关系 D 四件全是。而且是字面意义上的同一个运算,同一段代码

最短的入口:两个多项式相乘

小学的乘法竖式,中学的多项式展开:

(1 + 2x + 3x²) · (1 + x)

展开:  1·1 = 1                                  → x⁰ 的系数 1
       1·x + 2x·1 = 3x                          → x¹ 的系数 3
       2x·x + 3x²·1 = 5x²                       → x² 的系数 5
       3x²·x = 3x³                              → x³ 的系数 3

结果: 1 + 3x + 5x² + 3x³

只看系数:   [1, 2, 3]  ✻  [1, 1]  =  [1, 3, 5, 3]

「x² 的系数怎么来的」这个问题,答案是:把所有「下标加起来等于 2」的组合,乘起来再加起来。写成公式:

(a ✻ b)[k]  =  Σ  a[i] · b[k − i]
                i

念出来:**所有加起来等于 k 的组合,各自相乘,然后求和。**

这就是卷积。整章剩下的内容,都是这一句话在不同场合的样子。

四件事,同一句话

掷两个骰子

「和等于 7」有多少种?所有「a + b = 7」的组合。这不就是上面那句话吗——把两个 1/6 的均匀分布卷起来:

[1,1,1,1,1,1]/6  ✻  [1,1,1,1,1,1]/6

  和:    2    3    4    5    6    7    8    9   10   11   12
  次数:  1    2    3    4    5    6    5    4    3    2    1     (/36)

  P(和=7) = 6/36 = 0.166667

三角形不是巧合:方波卷方波等于三角波。三个骰子再卷一次,出来的形状已经明显像钟形了(最高点 27/216)——第 19 章会说明为什么这一定会发生。

滑动平均

「每个点换成它和前四个点的平均」,就是卷一个 [1,1,1,1,1]/5 的核。它的作用是压噪声,压多少?本机拿 65536 个高斯白噪声样本量了一遍:

  核长 L      原始 σ        平均后 σ        理论值 1/√L
  ────────────────────────────────────────────────────
     4        1.0009        0.5015          0.5000
     9        1.0009        0.3344          0.3333
    16        1.0009        0.2495          0.2500
   100        1.0009        0.1007          0.1000

噪声按 √L 下降,这是所有「多测几次取平均」的理论依据。代价是把信号也抹平了——第 15 章会说清这个代价在频域里长什么样。

照片模糊

高斯模糊就是让每个像素变成「它自己和邻居的加权平均」,权重是一个高斯形状的小方块(叫卷积核)。二维卷积和一维完全一样,只是求和变成两重。

顺带一个实用的事实:高斯核是可分离的——先对每一行卷一个一维高斯,再对每一列卷一次,结果和二维卷积完全相同,但运算量从 O(k²) 降到 O(2k)。这是图形学里最常用的一个优化。

混响

在一个房间里拍一下手,录下来的那一长串衰减的回响,叫这个房间的脉冲响应。把任何一段干声和这个脉冲响应卷起来,听起来就像是在那个房间里录的。

这就是「卷积混响」这个产品类别的全部原理,而且它是字面意义上的卷积——插件的名字就叫这个。有人专门跑去悉尼歌剧院、维也纳金色大厅、废弃的水塔里拍一下手,把脉冲响应卖钱。

◆ 主线

为什么这么多毫不相干的事情是同一个运算?因为它们都满足两条性质,而满足这两条的运算只有卷积这一种

  • 线性:两个输入叠加,输出也叠加(两个人同时说话,录到的是两段声音的和)。
  • 时不变(或平移不变):输入晚三秒,输出也晚三秒,形状不变(房间的混响不会因为你早上拍手还是晚上拍手而改变)。

这两条合起来叫 LTI(线性时不变)。定理是:任何 LTI 系统,都可以完全由它的脉冲响应描述,而它对任意输入的作用,就是和脉冲响应做卷积。

证明的思路一句话:任何输入都可以拆成一串加权的冲激(这是平凡的——第 n 个采样值就是第 n 个冲激的权重);线性说明可以逐个处理再叠加;时不变说明每个冲激的响应都是同一个形状,只是位置不同。把这些平移过的形状按权重加起来——那就是卷积的定义。

三条路,同一个答案

卷积可以用完全不同的方式算,本机核对了一遍:

a = [3, 1, 4, 1, 5]     b = [2, 7, 1]

  按定义式直接算:   6  23  18  31  21  36  5
  当作多项式相乘:   6  23  18  31  21  36  5
  绕道频域算:       6  23  18  31  21  36  5

  ✓ 直接算与频域路线的最大差:1.8 × 10⁻¹⁵

最后那条路就是下一章的全部内容,也是这本书真正的收款处。

✎ 术语正名

卷积神经网络里的「卷积」,不是这一章的卷积。这一点值得单独说,因为它坑过很多人。

数学定义里,(a ✻ b)[k] = Σ a[i]·b[k−i],注意 b 的下标是倒着走的——这叫「翻转」。而 PyTorch、TensorFlow 里的 Conv2d 实现的是

Σ a[i] · b[k + i]        ← 没有翻转

这在数学上叫互相关(cross-correlation),是第 16 章的主角。

为什么框架敢这么干?因为卷积核的权重是学出来的——网络完全可以学出一个翻转过的核,两种定义训出来的模型能力完全等价。所以这只是个命名上的历史遗留,不影响正确性。

但它在两个地方会咬人:一是你手写一个已知的核(比如 Sobel 边缘检测算子)扔进 Conv2d 时,方向会反;二是你想用第 14 章的 FFT 加速去替换框架的卷积时,必须记得翻转,否则结果整个错位。判据是:核是学出来的就无所谓,核是你自己写死的就必须确认方向。

⌨ 自己跑一遍

卷积的定义式只有四行,值得亲手写一次:

def convolve(a, b):
    out = [0.0] * (len(a) + len(b) - 1)
    for i, x in enumerate(a):
        for j, y in enumerate(b):
            out[i + j] += x * y            # 下标相加 —— 这就是全部
    return out

print(convolve([1, 2, 3], [1, 1]))                    # [1, 3, 5, 3]

die = [1/6] * 6
two = convolve(die, die)
print([round(v * 36) for v in two])                   # [1,2,3,4,5,6,5,4,3,2,1]
print('P(和=7) = %.6f' % two[5])                       # 0.166667

three = convolve(two, die)
print('三个骰子最高点 = %d/216' % round(max(three) * 216))   # 27/216

注意那一行 out[i + j] += x * y整个卷积就是这一行。「下标相加」这四个字,同时解释了多项式相乘、概率求和、图像模糊和混响。

再看噪声压制那张表:

import numpy as np
rng = np.random.default_rng(20260814)
noise = rng.standard_normal(65536)
for L in (4, 9, 16, 100):
    y = np.convolve(noise, np.ones(L)/L, 'valid')
    print(f'L={L:4d}  σ {noise.std():.4f} → {y.std():.4f}   1/√L = {1/np.sqrt(L):.4f}')

python3 -c " def c(a,b): o=[0]*(len(a)+len(b)-1) for i,x in enumerate(a): for j,y in enumerate(b): o[i+j]+=x*y return o print(c([1,2,3],[1,1]))"

在线跑:python.org/shell,第一段纯标准库。想听卷积混响:Audacity 的「效果 → 混响」里有一项是卷积,随便找一个 impulse response 的 wav 文件试试。

▸ 在现实里
  • CNN 的名字。卷积神经网络之所以在图像上有效,核心就是这一章那两条性质:一只猫出现在图片左上角还是右下角,都应该被认出来(平移不变)。卷积是「平移不变的线性运算」的唯一形状,所以它不是一个可选的设计,是这个需求的逻辑后果
  • 大整数乘法。两个大数相乘,本质上是它们的数位序列做卷积(然后处理进位)。这就是为什么下一章那个「用 FFT 加速卷积」的技巧,能直接用来加速大整数乘法——Python 的 int 在数字很大时、GMP 库、以及所有做密码学的地方,都在用这条路。你算 RSA 时 CPU 里跑的是傅里叶变换。
  • 概率论。「两个独立随机变量之和的分布,是它们各自分布的卷积」是一条基本定理。这条路一直通到第 19 章的中心极限定理。
  • 物理测量。任何仪器都有它的「响应函数」——望远镜的点扩散函数、质谱仪的峰形、光谱仪的狭缝函数。你测到的永远是「真实信号 ✻ 仪器响应」。做反卷积(把仪器响应除掉)是整个实验科学的一项日常工作,而它之所以难,是因为除法在有噪声时极不稳定。
  • 移动平均线。股票图上的 MA20 就是卷一个长度 20 的矩形核。它「滞后」的原因在第 15 章会看得很清楚:那不是实现问题,是这个核的相位特性。
✗ 这个直觉是错的

「卷积是信号处理里的一个技巧,用来做滤波。跟概率、跟多项式那些是不同的东西,只是长得像。」

不是长得像,是同一个。上面那段四行的 convolve 函数,喂给它两个概率分布就得到和的分布,喂给它两个多项式系数就得到乘积的系数,喂给它音频和脉冲响应就得到混响。一段代码,一个运算。

这种「看起来不相干的东西其实是同一个」在数学里通常意味着有一条更深的原因,这里的原因就是那条 LTI 定理:只要你要的操作是「线性 + 平移不变」,那么它必然是某个核的卷积,没有别的可能。所以凡是满足这两条的场合,卷积就会自动出现,无论那个场合叫概率、叫代数还是叫音频。

把这个认识倒过来用,是它真正值钱的地方:下次你遇到一个「线性 + 平移不变」的问题,直接去找它的核;找到了,第 14 章那条定理就会替你把它变成乘法。

◇ 揭晓

正确答案是 D:四件全是,而且是同一段代码。

ABC 这三个错法是同一种:按「学科」分类,而不是按「运算」分类。骰子被归到概率,模糊被归到图形学,多项式被归到代数,混响被归到音频——这是四门课,于是看起来像四件事。而它们的共同点不在学科里,在那一行 out[i+j] += x*y 值得多说一句 C 里那个「多项式相乘是纯代数」。恰恰相反,多项式这条线是四条里最有用的一条:它给了卷积一个代数结构(卷积就是多项式环里的乘法),从而立刻送来三条性质——交换律(a ✻ b = b ✻ a)、结合律(连着模糊两次等于模糊一次,核是两个核的卷积)、分配律。这三条在信号处理里天天用,而它们的来源是代数。 还有一条更实际的:因为卷积就是多项式乘法,所以「快速多项式乘法」和「快速卷积」是同一个问题——这正是下一章的定理,以及第 17 章那个算法所要解决的事。
⟳ 换个域看
时域里的卷积是一个二重循环:翻转、平移、逐点相乘、求和,对每一个输出点做一遍。N 个点对 M 个点,要 N × M 次乘法。这是全书目前为止最贵的运算。 频域里……(下一章揭晓,但你多半已经猜到了)。

先把线索摆出来:卷积说到底是「把核平移到每个位置,按权重叠加」。而第 4 章测过,平移在频域里就是给每根谱线乘一个单位长度的复数;叠加在频域里还是叠加。「乘一个数再相加」——那不就是把一堆复数加权求和吗?

而当你把那堆权重和那堆复指数拼在一起看,会发现每一根谱线上发生的事情,恰好就是「核在那个频率上的值」乘「信号在那个频率上的值」。一个二重循环,塌缩成了逐点相乘。

这一章的一句话

卷积不是一个技巧,是「线性 + 平移不变」这六个字的唯一形状;所以模糊、混响、平均、掷骰子、多项式相乘会是同一个运算,而这不是巧合,是定理。

下一章是这本书的收款处:那条让前面十三章所有账单都变得值得的定理。先给一个数——两条 16384 长的序列做卷积,老老实实按定义算要 402 毫秒,绕道频域走一趟 2.1 毫秒,而两条路的结果相对误差是 8.4 × 10⁻¹³