深度学习算子基础:卷积、池化、全连接、激活函数、归一化
各位同学,今天我们来聊聊深度学习里最基础的几个算子。说实话,这些算子看着简单,但真正在芯片上跑起来,坑可不少。我做了这么多年AI芯片优化,每次回头看这些基础算子,都能发现新的门道。
先给大家看一张整体结构图,方便你们建立全局认知:
一、卷积算子:深度学习的主力军
卷积,说白了就是用一个小的窗口在输入上滑动,每次做一次点积。我刚开始接触深度学习时,总觉得这跟图像处理里的卷积差不多。但实际在芯片上实现时,你会发现事情没那么简单。
数学定义其实很直观:
输出特征图[y][x][c_out] = Σ(输入[y+i][x+j][c_in] × 权重[i][j][c_in][c_out]) + 偏置[c_out]
其中 i、j 遍历卷积核的尺寸,c_in 遍历输入通道。嗯,这里要注意,卷积的计算量跟输入尺寸、通道数、卷积核大小都是平方关系。我在项目中遇到过,一个 3×3 卷积如果通道数从 64 翻到 128,计算量直接翻 4 倍——很多人第一次算这个都会吓一跳。
关键参数一览:
- 卷积核大小:常见 1×1、3×3、5×5、7×7。我个人习惯优先用 3×3,因为硬件加速器对 3×3 的支持最成熟
- 步长 (Stride):控制输出尺寸的缩小速度。步长 2 会让输出尺寸减半
- 填充 (Padding):保持边界信息。same padding 让输出尺寸等于输入尺寸
- 分组卷积:将通道分组独立计算。MobileNet 里就用这个来降低计算量
在芯片上实现卷积,最常用的方法是 im2col + GEMM。就是把卷积操作转化成矩阵乘法,然后调用高度优化的矩阵乘库。我早期做的一个项目里,用这个方法直接把卷积速度提升了 3 倍。但代价是内存占用会变大——你想想看,本来是一块一块算的,现在要先把数据铺开成矩阵。
我的经验:如果芯片有专用的脉动阵列(Systolic Array),比如 TPU 那种,直接用硬件实现卷积的滑动窗口,比 im2col 更省内存。但如果是通用 GPU,im2col 还是最稳的选择。
二、池化算子:降采样的艺术
池化,说白了就是在一个小区域内取一个代表值。最常见的两种:
- 最大池化 (Max Pooling):取区域内的最大值。保留最显著的特征
- 平均池化 (Average Pooling):取区域内的平均值。平滑特征响应
数学定义很简单,比如 2×2 最大池化,步长为 2:
输出[y][x] = max(输入[2y][2x], 输入[2y][2x+1], 输入[2y+1][2x], 输入[2y+1][2x+1])
池化本身计算量很小,但它决定了后续层的输入尺寸。我曾经在优化一个检测网络时,发现池化层的步长设置不合理,导致特征图尺寸变化太剧烈,后面的卷积层感受野覆盖不全。后来调整了池化的位置和步长,mAP 直接涨了 2 个点。
避坑指南:我曾经在移植模型到嵌入式芯片时,发现硬件不支持非整数倍的下采样。比如输入 7×7,池化核 2×2 步长 2,边界处理不好就会丢信息。所以设计网络时,尽量让输入尺寸是池化步长的整数倍。
三、全连接算子:参数大户
全连接层,就是每个输出神经元跟所有输入神经元相连。数学上就是矩阵乘法加偏置:
输出[j] = Σ(输入[i] × 权重[i][j]) + 偏置[j]
全连接层的参数量巨大。举个例子,输入 1024 维,输出 512 维,权重矩阵就是 1024×512 = 524288 个参数。如果输入是图像特征图展平后的结果,比如 7×7×512 = 25088 维,那参数量就更恐怖了。
在芯片上,全连接层的瓶颈往往不在计算,而在内存带宽。因为权重矩阵太大,需要频繁从 DDR 搬运数据。我优化过一个全连接层占主导的模型,把权重做了 8bit 量化,带宽需求直接减半,推理速度提升了 1.8 倍。
优化技巧:
- 尽量用全局平均池化替代全连接层(比如 ResNet 的做法)
- 如果必须用,考虑低秩分解或剪枝
- 量化到 INT8 甚至 INT4,精度损失通常可控
四、激活函数:非线性的灵魂
没有激活函数,神经网络就退化成线性变换了。常见的激活函数:
| 函数 | 公式 | 特点 | 硬件友好度 |
|---|---|---|---|
| ReLU | max(0, x) | 简单、稀疏、解决梯度消失 | ★★★★★ |
| Sigmoid | 1 / (1 + e^(-x)) | 输出范围 (0,1),梯度饱和 | ★★☆☆☆ |
| Tanh | (e^x - e^(-x)) / (e^x + e^(-x)) | 输出范围 (-1,1),零中心 | ★★☆☆☆ |
| GELU | x × Φ(x) | Transformer 常用,平滑 | ★★★☆☆ |
ReLU 在硬件上实现起来最简单——就是判断正负,负数直接置零。我做过一个芯片,ReLU 只需要一个比较器加一个多路选择器,几乎不占面积。但 Sigmoid 和 Tanh 就麻烦了,需要查表或者用分段线性近似。
我的建议:在芯片设计时,优先支持 ReLU 和它的变体(Leaky ReLU、PReLU)。如果模型必须用 GELU,可以用多项式近似,比如 GELU ≈ 0.5x(1 + tanh(√(2/π)(x + 0.044715x³))),这样只需要乘加和一次 Tanh 近似。
五、归一化算子:训练的稳定器
归一化层的作用,说白了就是让每一层的输入分布保持稳定,防止梯度爆炸或消失。最常用的两种:
- Batch Normalization (BN):对每个通道,在一个 batch 内做归一化。训练时统计均值和方差,推理时用滑动平均
- Layer Normalization (LN):对每个样本,在所有通道上做归一化。Transformer 里标配
BN 的数学定义:
y = γ × (x - μ) / √(σ² + ε) + β
其中 μ 和 σ² 是 batch 内的均值和方差,γ 和 β 是可学习的缩放和平移参数。
在推理时,BN 可以融合到前面的卷积层中。我做过一个优化,把 BN 的参数吸收到卷积权重里,这样推理时就不需要单独计算 BN 了,省掉了一次额外的访存和计算。具体做法是:
W' = W × γ / √(σ² + ε)
b' = (b - μ) × γ / √(σ² + ε) + β
这样卷积层直接输出归一化后的结果。嗯,这个技巧在部署时几乎是必做的。
避坑指南:我曾经在把 PyTorch 模型转到自家芯片时,发现 BN 的 ε 参数在训练和推理时不一致。训练时 ε=1e-5,推理时框架默认用了 1e-3,结果精度掉了 0.5%。后来统一了 ε 值才解决。所以一定要检查推理框架的 BN 实现细节。
六、算子之间的协同与融合
在实际的 AI 芯片上,这些算子很少单独出现。它们通常组合成一个 block,比如 Conv + BN + ReLU 就是一个经典的三件套。我建议在芯片设计时,尽量把相邻的算子融合成一个计算核,减少中间结果的访存。
举个例子,Conv + BN + ReLU 融合后,计算流程变成:
- 从内存读取输入特征图和权重
- 计算卷积结果
- 立即做 BN 的缩放和平移
- 立即做 ReLU 截断
- 写回内存
这样中间结果(卷积输出、BN 输出)都不需要写回 DDR,直接在片上缓存里流转。我做过一个实验,融合后访存量减少了 40%,整体性能提升了 30%。
总结一下:卷积是计算大户,池化是降采样利器,全连接是参数黑洞,激活函数提供非线性,归一化保证训练稳定。在芯片上,理解每个算子的计算模式和访存特征,是做好优化的第一步。
好了,这一章的内容就到这里。这些基础算子虽然简单,但它们是所有高级优化的基石。下一章我们会深入卷积的硬件实现细节,到时候再聊。