发布于2026-07-21 阅读(0)
扫一扫,手机访问
先聊聊一个很有意思的话题:写代码的时候,你是喜欢逐行执行、随时能打印中间结果那种“所见即所得”的感觉,还是更倾向于先把整个计算流程搭好,最后统一“发射”一次?这两种风格,分别对应着命令式编程(imperative style)和符号式编程(symbolic style)。它们在深度学习框架里各有拥趸,也各有长短。下面我们掰开揉碎聊聊。
用过 Python 或 C++ 的人对命令式编程都不陌生——程序在运行时逐条计算,绝大部分 Python 代码都是这个路子。比如下面这段:
import numpy as np
a = np.ones(10)
b = np.ones(10) * 2
c = b * a
d = c + 1
当执行到 c = b * a 时,真正的数值计算立刻就发生了。简单直接。
符号式编程的玩法就不太一样了。你得先“定义”一个函数——这个定义本身并不做实际计算,而是用占位符描述运算逻辑,等真正给定了输入数据再统一编译执行。上面的例子用符号式重写会是这个样子:
A = Variable('A')
B = Variable('B')
C = B * A
D = C + Constant(1)
# compiles the function
f = compile(D)
d = f(A=np.ones(10), B=np.ones(10)*2)
看到没?C = B * A 这一句并没有触发数值计算,它只是构建了一张计算图(也叫符号图),描述出“计算链路长什么样”。下图就是 D 对应的计算图:

大多数符号式程序都会显式或隐式地包含一个编译步骤,把计算图转换成可以调用的函数。上面例子中,真正的数值计算只发生在最后一行。符号式编程的一个核心特征就是:明确把“构建计算图”和“生成可执行代码”分为两个阶段。对于神经网络来说,通常就是用一张计算图来描述整个模型。
在流行深度学习框架里,Torch、Chainer、Minerva 走的是命令式路线;Theano、CGT、TensorFlow 走的则是符号式路线。CXXNet、Caffe 这类依赖配置文件的框架,其实也可以看作是符号式的——配置文件就是计算图的定义。下面我们来对比一下两边的优劣势。
命令式编程就是一步步改变程序状态,明确输入变量,按逻辑顺序逐步运算。
优势在于:好理解——在 Python 里写命令式代码,大部分逻辑都很直观;好调试——可以单步跟踪,随时取到所有中间变量,Python 的调试工具全都能用。
但代价也很明显:运行可能偏慢,会重复调用函数,长时间保存中间变量值,内存占用也比较高。
用 Python 调用命令式风格的库非常自然,写法跟普通 Python 代码一样,只是在合适的位置调用库的功能来加速。但如果用 Python 调用符号式风格的库,代码结构就会发生一些变化——比如有些循环可能就很难直接写。试着把下面这个命令式片段转换成符号式看看:
a = 2
b = a + 1
d = np.zeros(10)
for i in range(d):
d += np.zeros(10)
如果符号式 API 不支持 for 循环,这个转换就没法直接完成。你不能用写 Python 的思路去调用符号式库,而是需要利用符号 API 定义的领域特定语言(DSL)。深度学习框架通常都会提供功能强大的 DSL,让用户把神经网络转成可调用的计算图。
说到底,命令式编程更符合人的直觉,用起来更简单——你可以在任何位置打印变量值,可以随便用流程控制和循环。但事情总有两面。
符号式编程通常在计算流程完全定义好之后才会真正执行。
优势在于:更高效——编译时系统能拿到整个计算图,有更多空间做优化;更容易移植——符号式程序可以脱离 Python 环境,以避开解释器的性能瓶颈。
符号式编程一般需要三个步骤:
由于编译时系统能“看到”完整程序,所以可以大幅优化计算——不仅减少函数调用,还能节省内存。
TensorFlow 和 Theano 就是符号式编程的代表。
既然命令式编程更灵活、更贴近原生语言,为什么那么多深度学习框架要选择符号式?效率是最主要的原因——内存效率和计算效率都更高。我们来看一个具体的例子:
import numpy as np
a = np.ones(10)
b = np.ones(10) * 2
c = b * a
d = c + 1
...

假设每个数组元素占 8 字节,这段代码在命令式编程下需要多少内存?每一行都要分配空间,一共 4 个数组,每个 10 个元素,总共 4 × 10 × 8 = 320 字节。
但如果我们事先知道最后只需要 d 这个结果,构建计算图的时候就可以重复利用中间变量的空间。比如用原址计算——把 b 的内存借给 c 用,c 的内存再借给 d 用——这样就能省下一半内存,只需要 2 × 10 × 8 = 160 字节。
符号式编程的限制更多——因为只需要 d,中间量比如 c 的值就无法访问了。通过原址计算,符号式可以安全复用内存,但牺牲了对中间结果的访问。而命令式编程允许你随时访问任何中间变量——这就是灵活性的代价。
符号式编程还能通过 算子融合(operation folding) 来优化计算。还是上面那个例子,乘法和加法可以合并成一个操作,如下图所示:

如果在 GPU 上跑,计算图只需要一个 kernel,省掉了一个 kernel 的开销。很多优化库(比如 Caffe / CXXNet)都会人工编码来做这种融合,从而提升计算效率。
命令式编程没法自动做算子融合,因为你不知道中间变量以后会不会被访问到。符号式编程能拿到完整的计算图,明确哪些量以后会被访问、哪些不会,所以可以放心地做融合。
符号式编程把计算过程抽象成一张计算图(符号图),用来描述整个计算流程。
大多数符号式程序都会显式或隐式地包含编译步骤,把计算图转换成可调用的函数,在代码的最后一行才真正进行运算。也就是说,符号式程序清晰地把“定义运算图”和“编译运算”这两个步骤分割开来。
简而言之:命令式编程容易理解和调试,命令语句基本没有额外优化,按原有逻辑执行。符号式编程涉及较多嵌入和优化,不容易理解和调试,但运行速度有明显提升。
那么问题来了——有没有可能既享受命令式的好处,又能拥有符号式的优势?
开发者们的思路是:用户先用纯命令式编程进行开发和调试,等到需要产品级别的计算性能和部署时,再把大部分命令式程序转换成符号式程序来运行。深度学习框架 Caffe 和 MXNet 就采用了这种两种模式混合的方法,给足了选择的余地。
上一篇:我的C语言编程编程之路
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8