商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 命令式编程VS符号式编程

命令式编程VS符号式编程

  发布于2026-07-21 阅读(0)

扫一扫,手机访问

先聊聊一个很有意思的话题:写代码的时候,你是喜欢逐行执行、随时能打印中间结果那种“所见即所得”的感觉,还是更倾向于先把整个计算流程搭好,最后统一“发射”一次?这两种风格,分别对应着命令式编程(imperative style)和符号式编程(symbolic style)。它们在深度学习框架里各有拥趸,也各有长短。下面我们掰开揉碎聊聊。

什么是 symbolic / imperative 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 这类依赖配置文件的框架,其实也可以看作是符号式的——配置文件就是计算图的定义。下面我们来对比一下两边的优劣势。

命令式编程(imperative style programs)

命令式编程就是一步步改变程序状态,明确输入变量,按逻辑顺序逐步运算。

优势在于:好理解——在 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,让用户把神经网络转成可调用的计算图。

说到底,命令式编程更符合人的直觉,用起来更简单——你可以在任何位置打印变量值,可以随便用流程控制和循环。但事情总有两面。

符号式编程更加有效

符号式编程(symbolic style programs)

符号式编程通常在计算流程完全定义好之后才会真正执行。

优势在于:更高效——编译时系统能拿到整个计算图,有更多空间做优化;更容易移植——符号式程序可以脱离 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)都会人工编码来做这种融合,从而提升计算效率。

命令式编程没法自动做算子融合,因为你不知道中间变量以后会不会被访问到。符号式编程能拿到完整的计算图,明确哪些量以后会被访问、哪些不会,所以可以放心地做融合。

计算图 / 符号图(computation graph / symbolic graph)

符号式编程把计算过程抽象成一张计算图(符号图),用来描述整个计算流程。

  • 所有输入节点、运算节点、输出节点都符号化处理。
  • 通过建立输入节点到输出节点的传递闭包,从输入节点出发,沿着传递闭包完成数值计算和数据流动,直到到达输出节点。
  • 经过计算图优化,以数据流方式完成计算,节省内存,计算速度快,但不太适合调试,通常不用于通用编程语言。

大多数符号式程序都会显式或隐式地包含编译步骤,把计算图转换成可调用的函数,在代码的最后一行才真正进行运算。也就是说,符号式程序清晰地把“定义运算图”和“编译运算”这两个步骤分割开来。

混合式编程

简而言之:命令式编程容易理解和调试,命令语句基本没有额外优化,按原有逻辑执行。符号式编程涉及较多嵌入和优化,不容易理解和调试,但运行速度有明显提升。

那么问题来了——有没有可能既享受命令式的好处,又能拥有符号式的优势?

开发者们的思路是:用户先用纯命令式编程进行开发和调试,等到需要产品级别的计算性能和部署时,再把大部分命令式程序转换成符号式程序来运行。深度学习框架 Caffe 和 MXNet 就采用了这种两种模式混合的方法,给足了选择的余地。

本文转载于:https://blog.csdn.net/qq_31290747/article/details/96110596 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注