卷积基础回顾:离散卷积定义、二维卷积操作、卷积的复杂度分析
好,咱们正式开始。在动手优化卷积之前,得先把基础夯实。说实话,我见过不少同学一上来就啃FFT加速,结果连最基础的卷积定义都模棱两可。这不行。今天我们就从根上捋一遍。
离散卷积定义
先说说离散卷积。数学上,一维离散卷积的定义长这样:
(f * g)[n] = Σ f[k] · g[n - k]
其中 k 从负无穷到正无穷求和。当然,实际计算时我们只关心非零区域。
这个公式看着有点抽象,我换个说法。你想想看,卷积其实就是「加权滑动平均」。一个信号滑过另一个信号,每一步都做点乘求和。嗯,就这么简单。
我个人习惯把卷积理解为「翻转 + 滑动 + 点积」。为什么要有翻转?因为数学上要保证交换律成立。不过在实际的深度学习框架里,我们通常省略了翻转这一步——这叫互相关,但大家还是习惯叫它卷积。
二维卷积操作
图像处理里用的是二维卷积。输入是一张图,核是一个小矩阵。核在图上滑动,每个位置做逐元素相乘再求和。
公式长这样:
S(i, j) = Σ Σ I(i+m, j+n) · K(m, n)
m 和 n 遍历整个核。说白了,就是拿一个小窗口去扫大图。
我在项目中遇到过一个问题:输入是 224×224 的 RGB 图像,卷积核是 3×3,步长为1,padding为1。输出尺寸是多少?
答案是 224×224。因为 padding 补了一圈,尺寸不变。这个计算其实很简单:
输出尺寸 = (输入尺寸 - 核尺寸 + 2×padding) / 步长 + 1
代入就是 (224 - 3 + 2) / 1 + 1 = 224。嗯,没毛病。
卷积的复杂度分析
好,重头戏来了。为什么我们要费劲心思去加速卷积?因为它的计算量实在太大了。
考虑一个典型的卷积层:
- 输入特征图:H × W × C_in
- 卷积核:K × K × C_in × C_out
- 输出特征图:H × W × C_out
每个输出像素需要做 K×K×C_in 次乘加运算。总共有 H×W×C_out 个输出像素。所以总计算量是:
O(H × W × C_in × C_out × K × K)
举个例子。VGG16 里有个卷积层,输入是 112×112×128,输出是 112×112×128,核大小是 3×3。算一下:
112 × 112 × 128 × 128 × 3 × 3 = 1.85 × 10^9 次乘加
18.5亿次操作。就这一层。一个VGG16有十几个这样的层。你想想看,一张图片就要算几十亿次。训练的时候要迭代几十万张图片。这计算量,啧啧。
为什么卷积这么慢?说白了,就是「计算密度」太低。每个输出像素的计算只涉及一个很小的局部区域,数据复用率不高。CPU和GPU的缓存很难完全利用起来。
我刚开始做深度学习时,总觉得GPU能搞定一切。直到有一次要部署一个实时检测模型到嵌入式设备上,才发现卷积的计算量是个大问题。嗯,从那以后我就开始认真研究各种加速方法了。
复杂度对比表
为了让你有个直观感受,我整理了一个对比表:
| 卷积层类型 | 输入尺寸 | 核大小 | 输出通道 | 计算量(乘加次数) |
|---|---|---|---|---|
| 浅层卷积 | 224×224×3 | 3×3 | 64 | 8.7×10^7 |
| 中层卷积 | 56×56×256 | 3×3 | 256 | 1.85×10^9 |
| 深层卷积 | 28×28×512 | 3×3 | 512 | 1.85×10^9 |
| 全连接层(对比) | 1×1×4096 | — | 4096 | 1.68×10^7 |
看到没?中层和深层的卷积层计算量是浅层的20倍以上。而全连接层的计算量反而没那么大。所以加速的重点应该放在卷积层上,尤其是那些通道数多、特征图尺寸适中的层。
好了,卷积的基础就回顾到这里。下一节我们会正式进入FFT加速卷积的世界。说实话,我第一次看到FFT加速卷积的实现时,觉得这简直是个魔法。但理解了背后的原理后,你会发现它其实很自然。我们下节见。