机器学习以及编程语言(简体中文)

2017年12月25日 | 作者:Mike Innes (JuliaHub),David Barber (UCL),Tim Besard (UGent),James Bradbury (Salesforce Research),Valentin Churavy (MIT),Simon Danisch (MIT),Alan Edelman (MIT),Stefan Karpinski (JuliaHub),Jon Malmaud (MIT),Jarrett Revels (MIT),Viral Shah (JuliaHub),Pontus Stenetorp (UCL) 和 Deniz Yuret (Koç University)

任何足够复杂的机器学习系统都包含一个特别设置、不规范、充满 bug 又缓慢实现的编程语言半成品。[1]

作为一个设计编程语言(PL)的人,我们抱持莫大的兴趣看着机器学习(ML)迅速窜升 - 而且有了它,人们用来建立更复杂的 ML 模型与框架。极致(State-of-the-art)的模型正不断增加,有了程序的构造元素像是循环及递归,这为我们的建造工具带来很多有趣的议题 - 那也就是,编程语言。

然而机器学习还没有一个可靠的语言,许多人正在努力有效地创造隐藏在 Python API 底下的新语言(像是 TensorFlow),也有其他则是再利用 Python 当作一个建模语言(像是 PyTorch)。于是我们想问 —— 一个为机器学习量身定制的新语言是否有其必要的?如果是,为什么?还有更重要的是,一个理想中未来的机器学习语言会是什么样子?

  1. 儿童暗语(Pig Latin),及其他隐藏语言
  2. 为什么要创造新语言?
  3. 我们只能用 Python 吗?
  4. 那么量身定制的 ML 语言是什么样子?
  5. 结论:An Inference about Machine Learning

儿童暗语(Pig Latin),及其他隐藏语言

译者注:Pig Latin 是指在英语上加上一点规则使发音改变的暗语。由在德国的英国战俘发明来瞒混德军守卫的,多半被儿童用来自瞒大人秘密沟通,有时则只是说着好玩。

TensorFlow (TF) 家族[2]俨然成为编程语言,尽管有些限制。这对那些用Python 来写 TF 的人来说会有点震惊。不过,想想 TF 需要你用 Python 语法来构建表达式,并且在他的内部语言运算求值。

事实上,你可以用任何语言写出 TensorFlow 的“惰性(lazy)”风格。参考底下的 JavaScript 程序,用这种风格实现了简单的加法:

function add(a,b) {
  return `${a}+${b}`;
}
x = 1; y = 2
z = add('x', 'y') // 'x+y'
eval(z) // 3
x = 4
eval(z) // 6

在这里我们用了 元编程 —— 用代码生成代码。在这种情况下模板语言与目标语言都是相同的(JavaScript),但它们也可以是不同的(就像是 C 的前置处理器(preprocessor) 对应到 C),或者我们也可以用一个数据结构(一个语法树(AST))来代替字符串 —— 原则是相同的。在 TensorFlow 中,Python 作为一种模板语言用来编写 TF 的 graph-based laguage[3]。如果你不相信,思考看看 TensorFlow 的 graph 甚至支持像是变量作用域控制流程的构造元素 —— 而不是利用 Python 本身的语法,你正在通过 API 操作这些构造元素。

TensorFlow 以及其他相似的工具表示它们只是套件而已,但是它们是非常不寻常的一类,大多数的套件只提供一个简单的函数及数据结构的集合而不是整个程序系统以及 runtime。 为什么会需要这么一个复杂的方式?

为什么要创造新语言?

这个理由的核心是非常简单的:机器学习研究员有着非常高的计算需求,而简化建模语言让他们比较容易加入对不同领域的优化以及功能。训练模型需要极好的硬件支持、好的数值系统、较低的解释器 overhead 以及许多类型的并行。而对于像是 Python 这样一个通用型编程语言很难去提供这样的功能,但 TensorFlow 就可以无缝地处理这些需求。

尽管这样还是有个障碍,这些令人印象深刻的优化建立在简化的假设上面(ML 模型不会有递归或是需要特别计算梯度, 对吧?),而这些假设让他比较容易去加入优化或是部署到小型设备上面。不幸的是,对于工程师来说模型的复杂度越来越高,而研究员又喜欢去违反这些假设。现在的模型需要条件分支(OK,很简单可以修改)、重复的循环(不简单但仍然可能),或甚至递归整个树(理论上不可能)。在很多机器学习的领域,包含神经网络概率性编程,模型变得越来越像程序,包括一类会去推论其他程序的程序(也就是程序生成器解释器),以及不可微分的部分像是蒙特卡洛树搜索。这是一个巨大的挑战,来构建一个 runtime 具有完全的弹性同时达成最佳性能,然而不断增加地多数强大的模型,以及不断突破的结果需要两者。

使用 ML 在复杂的树状结构数据上(像是 Stanford Sentiment Treebank)需要可微分的递归算法。

这种做法的另一个实际缺点,至少是目前的典型,是需要上面讨论的这种元编程。构建并执行树会施加可观的重担在程序设计者以及编译器上。因为现在程序有两个执行期,它变得非常难以理解,每个都有各自语言的语义,而像是逐步调试就变得更加困难。这个问题可以通过创造一个有句法结构的语言给新的 runtime, 但这件事不亚于创造一个全新完整的编程语言。当我们已经有一个受欢迎的数值语言,这件事会是值得的吗?

我们只能用 Python 吗?

当 ML 模型开始需要一个编程语言的完整能力, Chainer 以及其他开辟了一种 "define-by-run" 的新方式,而在其中一个 Python 程序本身就是一个模型,通过执行期间的自动微分来计算导数。从一个可用性的观点来看这是非常棒的:如果你想要一个模型来处理树,单纯写下来就好,然后看 AD 变魔法! 感觉上这样的差别是毋庸置疑的,毕竟能有个轻松的方式去玩转新想法对研究来说是无价的。

然而,要让 Python 达到 ML 的沉重计算需求是超乎你想象的困难。重复不断的优化是个巨大的工作量,对于一个高性能的编程语言却可以很容易的达到,然而在 PL 的墓园中充斥着漂亮的评测未能成功让 Python 变快的项目。Python 的语义让他从本质上就很难去提供模型级别的并行化或是将模型编译给小型设备使用。

MXNet 的 Gluon 就是在努力找到一个方式对两边都最好,至少到一定程度上。这个想法是通过结合基本的动态自动微分及代码追踪方式,这个方式会制造可优化的"static sub-graphs"。不幸的是,这是某种完全不同的实现以及 API 的混合体。它也同样有局限性。MXNet 使用他的 graph 不只在核心层级的优化还有高阶的 graph 调度,像是将一个模型分散到多个 GPU 上。这个混合体要如何处理这些并不清楚,除了加入其他新的 API 让 graph 容器可以被运算节点动态计算。

那么量身定制的 ML 语言是什么样子?

除了 ML 外,还有其他领域也碰到需要语言层级的设计问题,但这并非是史无前例的。在像是形式化推理及验证或是集群运算的领域上,新量身定制的语言已经证实了是有效的解决方案。同样地,我们期望看到一个新的或是现存的语言对 ML 的数值系统,可微分,可并行且甚至是概率计算的需求做定制化。

对于 ML 语言,一个明显的挑战是同时达到通用性以及高性能,而提早混合的方式将需要更大量的开发工作。我们希望未来的 ML runtimes 将需要支持随心所欲地混合各种方法(静态的 computational graph 中嵌入动态的当中又嵌入静态…)以及在部署环境让编译动态代码做的更好。理想上,只会剩下单一种弹性的"graph format"(或是 AST)。这个 AST 需要有能够静态描述动态行为的语法(比如:一个 for loop) ― 换句话说,它应该看起来更像是个标准的编程语言。

可编程语义将开启弹性的新阶段,而且可以提供一个类似 macros 的功能。 这将允许类似多 GPU 训练功能可以被建立在核心系统上,并且指定纯数据流语意的代码(相对的是标准命令式语义,虽然较为灵活但是容易有副作用,对于优化来说较为不安全)。它同样可以用来自动操作概率性编程 ,或是自然语言处理模型常手动实现的向量化(批量处理)传递。

同样地在 PL 社群里, ML 工程师应该要花更多时间注意传统的自动微分(AD)社群。 ML 语言可以从顶尖的编程语言设计上得到支持微分成为真正的第一公民(first-class)的灵感。这样的语言可以简单地混合符号和 runtime 技术(这可以帮忙以上提到的取舍)、混合前向及反向模式的 AD (以增进效能及内存使用),以及区别不同的 GPU kernels ― 以上都没有任何性能流失。

ML 研究将会不断地需要更多强大的类型系统,用户定义类型及更多扩展意义。满足于 NVIDIA GPUs 上写死的 strided array 的日子已经过去了;尖端技术相关的稀疏机器学习,新硬件像是 TPUNervanaFPGA,以及多样的部署目标像是 ARM 芯片或是 iPhone 的 CoreML 芯片都渴望更高层级的灵活度。为每个新的开发项目大规模重构 C++ 的核心代码是不会有帮助的。

想象一个世界,在其中加入新硬件资源(或是新的数据表示法)可以简单地被用户通过高阶语言达成,不需要改变原本的系统。这里我们期望 ML 系统可以从现存已经可以轻松处理这些问题的数值计算语言借鉴。

类型系统也可以有安全性好处,但目前的不太适合在重度使用数组的代码,而其中数组的维度是有意义的(举例来说,图像中的空间维度 vs RGB 维度 vs 批次维度)。这些差别只被视为纯粹的使用惯例,粗鲁的维度排列代码并没有去避免错误,给关注数组的类型系统留下很大的空间。我们期待动态类型的趋势继续[4],主要是由于从业人员偏好交互性以及编写脚本,但希望看到更多创新像是 CNTK 的可选动态维度

[4] 虽然我们知道现存的系统内部将整个系统从完全的动态(PyTorch 以及他的 ATen 后端)延伸到极度的静态(在 TensorFlow 的 XLA 及 MXNet 中,在运算 graph 之前所有维度都是已知的)
ML 工程师对传统软件工程的问题(像是可维护性以及产品系统的扩展性)的兴趣正在提升。 ML 程序模型让他很难去创造抽象化以及组件之间的接口,而重新训练模型可能轻易地打破向后兼容性。 ML 语言可能可以整合正常语言对这些问题的解决方案,不过这依然是个设计上的开放问题。

软件工程 2.0? (出自 XKCD)

任何的新语言的缺点就是需要一个新的套件生态系统,因为只有为新 runtime 编写的程序会受益于新语言。举例来说,TensorFlow 开发者需要在 graph language 中为了像是图像处理以及文件读写这类事情重写函数库,而不是去再利用 Python 的生态,不考虑类似 SciPy 项目背后巨大的努力。这可能是前进的唯一道路,但是 ML 的从业者不应该跟广大的数值运算跟高性能运算(HPC)社群分离。一个理想的 ML 生态系是一个理想的数值运算生态系,反之亦然,并且与这些社群携手合作将会让大家的努力成果有加成效应。

我们期待看到这些开发者们从各种观点而来。 Graph 的 IR 以及格式(像是 XLA, ONNX 以及 NNVM)的发展变得更加复杂且将很可能会从传统语言设计[chris]上借鉴更多,也许甚至是加入表面的语法变成一个完整的编程语言。 TensorFlow 的 XLA 已经开始朝向一个特殊目的的编译器堆包含 TVM, DLVM, myelin 以及其他的进行中的项目。 同时,像是 PyTorch JIT, Gluon 以及 Tangent 的项目正在努力让 Python 本身变成一个更好的建模语言,尽管有这些巨大的挑战。我们已经讨论过 ML 是一个数值编程语言问题,所以对 Julia 社群的我们来说感觉这是一个绝佳的素材让我们来实验这些语言层级的议题,并且会继续在像是 Knet, Flux, Cassette, CUDAnative, DataFlow.jl,…… 等这些项目中挑战极限。

结论:An Inference about Machine Learning

机器学习模型已经成为了极度广义的信息处理系统,它构建了高阶而更为复杂的抽象;循环、递归、高维模型,甚至 stack machines语言解释器,所有的实现都是这些基本组件的组合。ML 是一个新的程序范式,虽然比较奇怪的是它是重度数值的、微分的跟并行化的。就如同任何工程领域,可用的工具会深深地影响着未来工作的眼界及品质。

所有这些暗示了 ML 系统的设计师有巨大的挑战在他们面前。但是虽然这些都是千真万确,还是有好消息的: 这些同样的问题,如果还没有被解决,已经被人文学家在过去数十年间深入地探索过!要将这个新领域达到他的真正潜能,机器学习跟编程语言社群需要结合他们力量,而真正的挑战是要结合这两群迥异领域专家们。

我们能不能建立一个把数值、微分、以及并行当作是首要目标的系统,且这个系统不牺牲掉传统编程语言的想法以及智慧?这是一个在接下来十年编程语言需要去回答的更根本之问题。

[1] 出自 Philip Greenspun
[2] 在这里我们使用 TensorFlow 当作例子, 但也可以用其他"执行前定义(define-before-run)"的框架替换,像是 CNTK 还有 MXNet。
[3] TensorFlow 的 graph 实际上是一个基于数据流的语法树(AST)。