深度学习算子基础:卷积、池化、全连接、激活函数、归一化

各位同学,今天我们来聊聊深度学习里最基础的几个算子。说实话,这些算子看着简单,但真正在芯片上跑起来,坑可不少。我做了这么多年AI芯片优化,每次回头看这些基础算子,都能发现新的门道。

先给大家看一张整体结构图,方便你们建立全局认知:

深度学习算子核心体系 输入特征图 (Input Feature Map) 卷积 (Convolution) 计算密集型 池化 (Pooling) 降采样/特征压缩 全连接 (FC) 矩阵向量乘 激活/归一化 非线性/数据调整 滑动窗口 + 乘累加 im2col + GEMM Winograd / FFT 区域统计 最大值 / 平均值 步长控制降采样 矩阵乘法 权重 × 输入 + 偏置 内存带宽瓶颈 逐元素操作 ReLU / Sigmoid / BN LayerNorm / Softmax 数据复用 / 并行度 计算量小 / 访存优化 带宽瓶颈 / 量化 融合 / 近似计算 核心目标:减少访存 + 提高计算密度 + 算子融合

一、卷积算子:深度学习的主力军

卷积,说白了就是用一个小的窗口在输入上滑动,每次做一次点积。我刚开始接触深度学习时,总觉得这跟图像处理里的卷积差不多。但实际在芯片上实现时,你会发现事情没那么简单。

数学定义其实很直观:

输出特征图[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 融合后,计算流程变成:

  1. 从内存读取输入特征图和权重
  2. 计算卷积结果
  3. 立即做 BN 的缩放和平移
  4. 立即做 ReLU 截断
  5. 写回内存

这样中间结果(卷积输出、BN 输出)都不需要写回 DDR,直接在片上缓存里流转。我做过一个实验,融合后访存量减少了 40%,整体性能提升了 30%。

总结一下:卷积是计算大户,池化是降采样利器,全连接是参数黑洞,激活函数提供非线性,归一化保证训练稳定。在芯片上,理解每个算子的计算模式和访存特征,是做好优化的第一步。

好了,这一章的内容就到这里。这些基础算子虽然简单,但它们是所有高级优化的基石。下一章我们会深入卷积的硬件实现细节,到时候再聊。

可可精品资料库