深度学习模型基础:神经网络基本概念、常见算子与计算图

各位同学,今天我们聊聊深度学习模型的基础。说实话,这部分内容看起来简单,但却是整个AI芯片编译器的地基。我在做编译器优化时,发现很多性能瓶颈都源于对模型底层理解不够深。你想想看,连算子都认不全,怎么去优化它?

神经网络基本概念

神经网络,说白了就是一个多层函数组合器。每一层都在做一件事:把输入数据做某种变换,然后传给下一层。我习惯把神经网络想象成一个流水线工厂——原材料进来,经过一道道工序,最后产出成品。

这里有几个核心概念,我建议你记牢:

  • 神经元:最基本的计算单元,做加权求和加激活
  • :多个神经元组成一层,常见的有卷积层、池化层、全连接层
  • 权重与偏置:网络要学习的参数,说白了就是决定变换方式的系数
  • 前向传播:数据从输入到输出的计算过程
  • 反向传播:根据损失函数梯度更新参数的过程

我个人经验:刚开始做编译器时,我总以为只要把前向传播优化好就行。后来发现,反向传播的算子(比如卷积的梯度计算)才是真正的性能杀手。很多芯片在推理时跑得飞快,一训练就卡住,问题往往出在这里。

常见算子详解

算子(Operator)是神经网络的基本操作单元。作为编译器开发者,你必须对每个算子的计算特征了如指掌。下面我挑几个最常用的来讲。

卷积(Convolution)

卷积是CNN的绝对核心。它做的事情很简单:用一个滑动窗口(卷积核)在输入特征图上扫一遍,每个位置做一次点积运算。

我在项目中遇到过一个问题:同样的卷积,在不同硬件上性能差异巨大。原因在于卷积的计算模式——它本质上是数据复用内存访问的博弈。

// 卷积的伪代码示意
for (int oh = 0; oh < output_height; oh++) {
  for (int ow = 0; ow < output_width; ow++) {
    for (int ic = 0; ic < input_channels; ic++) {
      for (int kh = 0; kh < kernel_height; kh++) {
        for (int kw = 0; kw < kernel_width; kw++) {
          output[oh][ow][oc] += 
            input[oh+kh][ow+kw][ic] * weight[kh][kw][ic][oc];
        }
      }
    }
  }
}

嗯,这里要注意:卷积的循环嵌套顺序直接影响缓存命中率。我见过不少团队把循环顺序搞反,导致性能直接腰斩。

池化(Pooling)

池化算子,说白了就是降采样。常见的有最大池化和平均池化。它的计算量不大,但内存访问模式很特殊——非连续、跨步长访问。

避坑指南:我曾经在优化池化算子时,想当然地用了连续内存访问策略,结果发现池化的输入输出尺寸不匹配,导致大量边界判断开销。后来改用分块处理,性能提升了30%。

全连接(Fully Connected)

全连接层就是矩阵乘法。每个输出神经元和所有输入神经元相连。它的计算密度很高,但参数也最多。

你想想看,一个1024x1024的全连接层,光权重就有100万个参数。这对芯片的存储带宽是个巨大考验。我建议在做编译器调度时,把全连接层的权重尽可能放在片上缓存里。

激活函数(Activation)

激活函数给网络引入非线性。常见的包括ReLU、Sigmoid、Tanh等。从编译器角度看,激活函数是逐元素操作,计算量小,但访存开销占比高。

我个人习惯把激活函数和前面的卷积或全连接做算子融合。这样能减少一次数据搬运,性能提升很明显。

算子类型 计算特征 访存特征 优化重点
卷积 计算密集,数据复用 滑动窗口,局部性较好 循环分块、数据重排
池化 计算简单 非连续访问 边界处理、向量化
全连接 计算密集,参数多 连续访问 矩阵分块、权重缓存
激活函数 逐元素,计算轻量 访存开销占比高 算子融合

计算图与数据流图

计算图是深度学习框架的核心数据结构。它把网络表示成一个有向无环图(DAG),节点是算子,边是数据依赖关系。

为什么会用图来表示?因为图能清晰地展示算子之间的依赖关系和并行机会。我在做编译器时,第一步就是把模型解析成计算图,然后做各种图优化。

计算图 vs 数据流图

这两个概念容易混淆,我简单区分一下:

  • 计算图:描述计算逻辑,节点是算子,边是张量数据
  • 数据流图:描述数据流动,节点是数据操作,边是数据依赖

其实在大多数框架里,两者是等价的。但编译器更关注数据流图,因为它能告诉我们:哪些数据可以复用?哪些计算可以并行?

注意:计算图中的控制流(如if-else、循环)处理起来很麻烦。我曾经在一个动态网络模型上栽过跟头——图结构在运行时变化,导致编译器的静态优化全部失效。后来我们引入了动态形状处理机制才解决。

计算图的核心作用

从编译器角度看,计算图有三大作用:

  1. 依赖分析:确定算子执行顺序,找出可并行部分
  2. 内存规划:分析张量生命周期,优化内存复用
  3. 图优化:做算子融合、常量折叠、死代码消除等

下面我用一张图来展示计算图的核心逻辑:

计算图与数据流图核心结构 输入 Conv ReLU Pool FC 输出 图优化(编译器视角) • 算子融合:Conv + ReLU → ConvReLU • 常量折叠:提前计算不变的表达式 • 死代码消除:删除无用的计算节点 • 内存规划:分析张量生命周期,复用内存 输入/输出 计算算子 激活函数

这张图展示了一个典型的CNN子图。数据从输入开始,经过卷积、激活、池化,再到全连接,最后输出。编译器要做的就是分析这些节点之间的依赖关系,然后生成高效的执行计划。

小结

今天的内容就到这里。神经网络的基本概念、常见算子、计算图与数据流图,这些都是AI芯片编译器的基础。我个人建议你多动手画计算图,把每个算子的输入输出形状、计算量、访存量都标出来。这样做久了,你自然就能看出哪些地方可以优化。

记住一句话:理解模型,才能优化模型。编译器不是魔法,它只是把人对模型的理解转化为高效的机器指令。