📗 第7章 深度学习工具
📄 一句话概括

这一节先做一个对照实验:同一件事(一层隐层的前向),用手写 numpy、符号图、 静态图会话三种方式各写一遍。 结果差 0.00e+00,一个 bit 都不差。 500 次耗时分别是 11.89、13.70、16.91 毫秒。 所以这三套东西不是三种算法,是三种「写算法的方式」。 多写的那些代码换来的是别的东西:自动求导、全局优化、能脱离 Python 部署。 七个工具里五个已停止维护,唯一还在更新的那个也改掉了自己的核心设计。 但它们的范式全都活下来了。

📝 本节和前面几节的关系

这一章讲的框架基本都用不上了,所以小结不能停在「复习知识点」上。 这一节要回答的是:既然都过时了,这一章到底带走了什么。

一、三种写法,一件事

网络就一层隐层:X 乘 W1 加 b1,tanh,再乘 W2 加 b2。 三种写法各自的样子:

# 甲:手写 numpy
h = np.tanh(X @ W1 + b1)
out = h @ W2 + b2

# 乙:符号图
h = th(ad(mm(X, W1), b1))
o = ad(mm(h, W2), b2)

# 丙:静态图加会话
iM1 = put('mm', [iX, put('const', [W1v])])
iA1 = put('ad', [iM1, put('const', [b1v])])
iH = put('th', [iA1])

甲两行,乙两行但要多写一个求值器,丙六行但要多写一个执行器。 所以「代码更短」不是这些框架的卖点——恰恰相反,它们都更长。

二、结果与代价

写法 与甲的最大差 500 次耗时
甲 手写 numpy 基准 11.89 毫秒
乙 符号图 0.00e+00 13.70 毫秒
丙 静态图加会话 0.00e+00 16.91 毫秒
三个结果一模一样,差 0.00e+00。
这说明它们算的是同一件事——它们不是三种算法,是三种「写算法的方式」。
代价也一目了然:甲最快也最短,乙和丙都要多写一套求值机制,慢 1.2 到 1.4 倍。
那为什么还要用它们?
因为它们换来了别的东西:乙能自动求导,丙能把整张图拿出来做全局优化、能脱离 Python 部署。
这也是选框架时该问的问题:你缺的是那 1.2 倍的速度,还是自动求导和可部署?

三、七个工具各管一段

节 工具 它解决什么 今天在哪
7.1 开发环境 装什么、跑多快 还在,换成 conda 和容器
7.2 Theano 符号图加编译 ONNX 和 TorchScript 里
7.3 Pylearn2 配置驱动训练 hydra 和 wandb 里
7.4 Caffe 模型定义文件 ONNX 和部署格式里
7.5 DIGITS 网页管训练任务 云训练平台里
7.6 Chainer 先运行后建图 被 PyTorch 完整吸收
7.7 TensorFlow 静态图加会话 2.0 起自己也改动态图
七个里五个已经停止维护,唯一还在更新的 TensorFlow,
也在 2.0 把自己最大的卖点(静态图)改掉了。
但注意右边那一列:没有一个是「消失了」,全都是「换个地方活着」。
这就是技术演进里最常见的形态:框架会死,范式会活。

四、带走一张地图

回头看不难发现,这一章的七个工具,其实是在三条轴上做选择:

轴 一端 另一端
图什么时候画 先定义后运行(Caffe、TF 1.x) 边运行边定义(Chainer、PyTorch)
参数写在哪 配置文件(Pylearn2) 代码里(手写 numpy)
要不要界面 命令行(Caffe、Theano) 网页(DIGITS)
今天的所有框架,都是在这三条轴上取了某个组合。
PyTorch 取的是「边运行边定义加代码里加命令行」,
TensorFlow 2.x 取的是「边运行边定义加配合同名,加一套可视化」。
所以这一章真正该带走的不是某个 API,是这张地图。
下次遇到一个新框架,先问它在这三条轴上站在哪,比读它的文档快得多。

五、关于「学一个过时的东西」

这一章是个不太常见的样本:整整一章讲的东西,基本都用不上了。 但恰恰因为这样,它比讲一个当红框架更有价值。 当红框架你只需要学 API,过时框架你才看得见「哪些东西是不变的」。 Theano 的符号图、Caffe 的模型文件、Chainer 的动态图——这三件事今天全都在,只是换了名字。 下一章是最后一章,讲深度学习的应用和未来。 那一章没有 Demo,只有一张更长的地图。

六、动手 Demo:同一件事的三种写法,结果与耗时对照

三种写法都在这一个脚本里,用的是同一份权重、同一份输入。 所以结果能不能对上,是硬指标——对不上就是有 bug。 最后还有一张七个工具的定位表,那是手写的,不是算出来的。

# 写法甲:手写 numpy(第 2 章的路子)
h = np.tanh(X @ W1 + b1)
out = h @ W2 + b2

# 写法乙:符号图(Theano 的路子)
h = th(ad(mm(X, W1), b1))
o = ad(mm(h, W2), b2)

# 写法丙:静态图加会话(TensorFlow 1.x 的路子)
iX = put('ph', ['X'])
iM1 = put('mm', [iX, put('const', [W1v])])
iA1 = put('ad', [iM1, put('const', [b1v])])
iH = put('th', [iA1])
iM2 = put('mm', [iH, put('const', [W2v])])
iO = put('ad', [iM2, put('const', [b2v])])

# 三者算的是同一件事,结果差 0.00e+00
print(np.abs(outA - outB).max(), np.abs(outA - outC).max())

实跑输出:

--------------------------------------------------------------------
小结:同一件事的三种写法,以及七个工具的定位
--------------------------------------------------------------------
[1] 先做一个对照实验:同一件事,三种写法
  网络就一层隐层:X 乘 W1 加 b1,tanh,再乘 W2 加 b2。
  用三种方式各写一遍,看结果一不一样、代码有多长、跑得多快。

  写法甲:手写 numpy(第 2 章的路子)
    h = np.tanh(X @ W1 + b1)
    out = h @ W2 + b2
    代码 2 行

  写法乙:符号图(Theano 的路子)
    h = th(ad(mm(X, W1), b1))
    o = ad(mm(h, W2), b2)
    代码 6 行(还得先写一个 20 行的求值器)

  写法丙:静态图加会话(TensorFlow 1.x 的路子)
    9 个节点:1 个占位符、4 个常量、2 个乘、2 个加、1 个 tanh
    代码 9 行(还得先写一个 20 行的执行器)

[2] 三份结果一对,完全相同
  写法                  与甲的最大差      500 次耗时
  甲 手写 numpy         基准              12.04 毫秒
  乙 符号图             0.00e+00          13.33 毫秒
  丙 静态图加会话       0.00e+00          17.63 毫秒

  三个结果一模一样,差是 0.00e+00——一个 bit 都不差。
  这说明这三套东西算的是同一件事——它们不是三种算法,是三种「写算法的方式」。
  代价也一目了然:甲最快也最短,乙和丙都要多写一套求值机制,慢 1.1 到 1.5 倍。
  那为什么还要用乙和丙?因为它们换来了别的东西——
  乙能自动求导,丙能把整张图拿出来做全局优化、能脱离 Python 部署。

[3] 七个工具各管一段
  节    工具          它解决什么                    今天在哪
  7.1   开发环境      装什么、跑多快                还在,但换成 conda 和容器
  7.2   Theano        符号图加编译                  思路在 ONNX 和 TorchScript 里
  7.3   Pylearn2      配置驱动训练                  思路在 hydra 和 wandb 里
  7.4   Caffe         模型定义文件                  思路在 ONNX 和部署格式里
  7.5   DIGITS        网页管训练任务                思路在云训练平台里
  7.6   Chainer       先运行后建图                  被 PyTorch 完整吸收
  7.7   TensorFlow    静态图加会话                  2.0 起自己也改动态图了

  七节里五个框架已经停止维护,唯一还在更新的 TensorFlow
  也把自己最大的卖点(静态图)改掉了。

[4] 那这一章还读它干什么
  因为「范式」比「框架」长寿。
  今天你写的每一行 PyTorch,背后都还是这几件事:
  今天的东西            当年的源头
  torch.autograd        Theano 的符号求导
  ONNX 导出             Caffe 的 prototxt
  TorchScript / tracing TensorFlow 1.x 的建图
  PyTorch 默认动态图    Chainer 的 define-by-run
  hydra 管超参数        Pylearn2 的 YAML
  TensorBoard 和云平台  DIGITS 的网页界面

  所以这一章真正该带走的不是某个 API,是一张地图:
  你想解决的问题,落在「图什么时候画」「配置写在哪」「界面要不要」这三条轴上,
  每一条轴上的选择,都有人替你试过错。

写法乙和丙的求值器各自只有二十来行,这里没全贴出来。 但它们的结构是一样的:一个按节点类型分派的递归函数,加一个 cache。 这说明「框架」和「二十行代码」的差距,不在核心机制,在工程完备度。 耗时那一列要谨慎解读。 11.89 对 16.91,看着差四成,但这是 Python 里跑出来的数字。 换成真实框架、真实网络,这层开销会被矩阵乘淹没——这也是为什么框架能流行: 它们的额外开销在真实规模下可以忽略。 结果差是 0.00e+00,不是 1e-16。 因为三种写法最终调用的都是同一个 BLAS 矩阵乘,浮点运算顺序完全一致。 如果中间有算子融合或不同的累加顺序,才会出现末位差异。

30 秒实验

  1. 把三种写法的输入改成 float32 再跑。 耗时都会明显下降,而且三者之间的差距会缩小——这印证了「开销主要在矩阵乘」。
  2. 给写法乙和丙各加一个 tanh 层。 乙只要多一行 th(...),丙要多三个节点——这就是两种范式的手感差异。
  3. 把这一章七个 Demo 的脚本行数统计一下。 你会发现越往「框架」方向走,代码越长;但它们换来的能力也越多。

常见误区

  1. 「框架就是帮你少写代码。」 这一节的对照实验说明相反:三种写法的代码量是 2 行、6 行、9 行,框架反而更长。 框架换来的是自动求导、全局优化、可部署,不是更短的代码。
  2. 「这一章讲的东西都过时了,白读。」 七个工具里五个停了,但它们定下的范式全都在。 今天你在 PyTorch 里写的每一行,都能在这一章里找到源头。
  3. 「选框架就要选最流行的那个。」 流行会变。2015 年是 Theano,2017 年是 TensorFlow,2019 年之后是 PyTorch。 选框架要看你需要哪条轴上的能力:能部署、能调试、还是要生态。
自测三问(点开对答案) 1. **为什么三种写法的结果能差 0.00e+00?** —— **因为它们最终调用的都是同一个底层矩阵乘库,浮点运算的执行顺序也一样。** **框架只是换了一种「描述」的方式,没有换「计算」的方式。** 2. **框架比手写慢 1.2 到 1.4 倍,为什么还值得用?** —— **因为这点开销在真实规模下会被矩阵乘淹没。** **而框架带来的是:自动求导不用手推、能在 GPU 上跑、能保存和加载模型、能部署。** **这些收益远超那点调度开销。** 3. **这一章的七个工具里,哪一个的范式最完整地活到了今天?** —— **Chainer 的 define-by-run。** **它提出的「边运行边建图」现在是 PyTorch 和 TensorFlow 2.x 的默认做法。** **有意思的是它自己停了,因为这套思想已经被完整吸收,不需要第二份实现。**

最后一章只有三篇,没有 Demo。 它讲深度学习已经用在了哪些地方、未来会往哪走——以及一个更根本的问题:这条路还能走多远。

相关笔记