Swift与谷歌的可微编程项目 (swift与rapid的区别)

Swift与谷歌的可微编程项目 (swift与rapid的区别)

本文最初发布于 tryolabs 博客,经原作者授权由 InfoQ 中文站翻译并分享。

两年前,谷歌的一个小型团队开始专注 Swift,致力于使其成为第一种 具有一流语言集成可微编程能力 的主流语言。这个项目的进展非常显著,距离公开应用已经不远了。

尽管如此,这个项目在机器学习社区中并没有引起多少人的兴趣,而且大多数从业者对它一无所知。这在一定程度上归因于语言本身,因为 Swift 主要用于构建 iOS 应用程序,在数据科学生态中几乎没有存在感。

很遗憾,即使粗略地看一下谷歌的项目,也能发现这是一个庞大而雄心勃勃的项目,这可能会使 Swift 成为该领域的关键。此外,尽管在 Tryolabs 我们主要使用 Python,但我们还是认为选择 Swift 是个极好的主意,因此,我们决定撰写一篇短文来帮助谷歌做个“宣传”。

在讨论 Swift 和可微编程(differentiable programming)这个术语的实际含义之前,先来看看目前的情况。

Python 出了什么问题

目前 Python 是机器学习中使用最多的语言,谷歌的很多机器学习库和工具都是用它编写的。那么,为什么是 Swift?Python 出了什么问题?

坦率地说,Python 很慢 。而且,Python并不适合并行。

为了解决这些问题,大多数机器学习项目通过用 C/C++/Fortran/CUDA 编写的库来运行计算密集型算法,用 Python 将不同的底层操作粘合在一起。在大多数情况下,这都很有效,但是,与所有的抽象一样,它可能会导致一些问题。下面我们具体探讨。

外部二进制文件

为每个计算密集型操作调用外部二进制文件限制了开发人员,让他们只能处理算法外围的一小部分。例如,编写自定义方法执行卷积就会成为限制,除非开发人员深入理解类似 C 这样的语言。大多数程序员不会这样做,因为他们没有编写底层高性能代码的经验,或者因为在 Python 开发环境和一些低级语言的环境之间来回切换过于繁琐。

这导致了一种令人遗憾的情况,即程序员被鼓励尽可能少地编写复杂代码,并默认调用外部库操作。这与机器学习这个充满活力的领域所期望的情况正好相反,这个领域中还有很多问题仍未解决,非常需要新的想法。

库抽象

让 Python 代码调用底层代码并不像将 Python 函数映射到 C 函数那样容易。现实令人遗憾,机器学习库的创建者不得不为了性能而做出某些妥协,这使问题变得有点复杂。例如,在Tensorflow图模式(这是该库中惟一的性能模式)中,Python 代码通常不会在你认为它会运行的时候运行。实际上,Python 是底层 Tensorflow 图的一种元编程语言。

开发流程如下:开发人员首先使用 Python 定义网络,然后 TensorFlow 后端使用这个定义来构建网络,并将其编译为开发人员无法访问其内部的 blob。编译之后,网络终于可以运行了,开发人员可以开始为训练/推理作业提供数据。这种工作方式使得调试非常困难,因为你不能使用 Python 来深入了解网络运行时的内部情况。你不能使用像 pdb 这样的东西。即使你希望使用以前用起来还不错的打印调试,也必须使用 tf.print 在网络中构建一个打印节点,该节点必须连接到网络中的另一个节点,并在打印之前进行编译。

不过,还有更直接的解决方案。在中,代码会严格按照 Python 的要求运行,唯一不透明的抽象是在 GPU 上异步执行的操作。这通常不是问题,因为 PyTorch 在这方面很聪明,它会在放弃控制权之前等待所有依赖用户交互操作的异步调用完成。尽管如此,这还是需要注意的,特别是基准测试之类的事情。

行业滞后

所有这些可用性问题不仅增加了编写代码的难度,还导致了 行业落后于学术界 。已经有几篇论文对神经网络中使用的低层操作进行了优化,从而在过程中将精度提升了几个百分点,但仍需要很长时间才会被业界采用。

原因之一是,尽管这些算法更改本身非常简单,但是上面提到的工具问题使它们非常难以实现。因为结果往往只能提高 1%的准确性,人们会认为不值得为它付出努力。尤其成问题的是,对于小型机器学习开发团队来说,他们通常无法扩大生产规模来维持他们实现/集成的成本。

因此企业经常忽略这些改进,直到这些算法被添加到像 PyTorch 或 TensorFlow 这样的库中。这为企业节省了实现和集成的成本,但也导致行业落后于学术界 1 到 2 年,因为不能指望库维护者立即实现每一篇新发表的论文的结论。

一个具体的例子是可变形卷积,它似乎可以提高大多数卷积神经网络(CNN)的性能。大约两年前出现了一个开源实现。然而,将该实现集成到 PyTorch/TensorFlow 中非常麻烦,并且该算法没有得到广泛的应用。直到最近,PyTorch 才增加了对它的支持,到目前为止,我还没有听说有官方的 TensorFlow 版本。

现在,让我们做个假设,在这几篇论文中,每一篇论文都提高了 2%的性能;那么这个行业可能会 错过 1.02^n%的巨大精度改进的机遇 ,而原因只是工具不足。考虑到 n 可能相当大,这很令人遗憾。

速度

在某些情况下,使用 Python 和快速库仍然会很慢。是的,对于进行图片分类的 CNN 来说,使用 Python 和 PyTorch/TensorFlow 会非常快。而且,在 CUDA 中编写整个网络代码可能不会获得太多的性能提升,因为大部分推理时间都花在了大型卷积上,而后者是运行在已良好优化的实现中。但情况并不总是如此。

如果不是完全用低级语言实现的话,那些由许多 小型操作 组成的网络通常最容易受到 性能问题 的影响。例如,在一篇博文中,的Jeremy Howard表达了自己对使用 Swift 进行深度学习的热爱。按照他的说法,尽管使用了优秀的 PyTorch JIT 编译器,仍然不能使特定的 RNN 像完全使用 CUDA 实现的版本那样快速运行。

此外,对于很重视 延迟 的情况,或者对于与传感器通信之类的 非常底层的任务 ,Python 都不是一种非常好的语言。一些公司选择绕过这个问题,他们的方法是从一开始就使用 PyTorch/TensorFlow-Python 开发模型。通过这种方式,他们就可以利用 Python 的易用性进行试验并训练新模型。此后,对于生产应用,他们会用C++重写模型。我不确定他们是完全重写,还是只是使用 PyTorch 的跟踪功能或 TensorFlow 的图模式对它进行序列化,然后用 C++重写它外围的 Python 代码。无论采用哪种方式,都需要重写大量的 Python 代码,这对小公司来说成本太高。

这些都是众所周知的问题。深度学习之父曾表示,有必要开发一种新的机器学习语言。在推特主题中,PyTorch 联合创建者 Soumith Chintala 和他讨论了几种可能的候选语言,其中提到了 Julia、Swift,甚至改进 Python。另一方面,Fast AI 的 Jeremy Howard 似乎也明确要登上 Swift 的列车。

谷歌接受了挑战

幸运的是,谷歌的Swift for Tensorflow(S4TF)团队接受了解决这个问题的挑战。更棒的是,整个过程非常透明。在一份极其详尽的文档中,他们详细描述了做出这一决定的过程,阐述了考虑用什么语言来完成这项任务的过程,以及为什么最终选择了 Swift。

以下是其中最值得注意的一些理由:

Swift 酷在哪?

简单来说,Swift 让你可以用一种与 Python 相近的方式,进行高层次编程,同时又非常快。数据科学家可以用几乎与使用 Python 相同的方式使用 Swift,不过,在对使用 Swift 构建的机器学习库进行优化时,需要更加注意管理内存,对于一些特别严格的惯用法,抽象可能会下降到指针级别。

本文就不对这种语言进行详细描述了,官方文档比我做得好多了。作为该语言的新粉,我会介绍一些我认为 Swift 很酷的地方,希望能吸引人们去尝试。在接下来,我会介绍一些 Swift 中各种很酷的东西,没有特定的顺序,也没有特别注意它们的整体意义。下面深入探讨可微编程和谷歌的 Swift 规划。

1. 速度快

Swift 很快。这是我开始使用 Swift 时首先测试的。我写了几个简短的脚本,并将它们与 Python 和 C 进行比较。说实话,这些测试并不很复杂,只是用整数填充一个数组,然后把它们都加起来。我承认,这种方法不足以全面测试 Swift 的真实速度,但更让我好奇的是,就算 Swift 没法在速度上与 C 全面看齐,至少能在某些场景下追上 C 的速度。

在第一组比较中,我对比了 Swift 和 Python。我在 Swift 中对花括号的位置做了一些调整,这样,基本上让两边的代码每一行都做相同的操作。

尽管这个代码片段中 Swift 和 Python 的语法非常相似,但 Swift 脚本比 Python 脚本快 25 倍。Python 脚本中的每个外层循环平均耗时 360μs,Swift 为 14μs。这是一个很大的提升。

还有其他有趣的事情值得注意。+是一个运算符,同时也是一个传递给 reduce 的函数(我将在后面详细说明),CFAbsoluteTimeGetCurrent 展示了 Swift 在遗留 iOS 名称空间方面的古怪之处,…<range 操作符指定范围是否包含边界。

这个测试并不能真正地告诉我们 Swift 的速度到底有多快。为了找到答案,我们需要将它与 C 进行比较。所以,我就这样做了,令人非常失望的是,最初的结果并不好。C语言版本平均需要 1.5μs,比 Swift 代码快了 10 倍,我的天哪!

这不是一个非常公平的比较。Swift 脚本使用了动态数组,随着大小的增加,这个数组会在堆中不断地重新分配。这也意味着它会对每一次追加执行绑定检查。为了证实这一点,我们可以去看看它的定义。像 int、float 和数组这样的 Swift 标准类型并没有硬编码到编译器中,它们是在标准库中定义的结构体。因此,根据数组的追加定义,我们可以看到发生了很多事情。了解了这一点之后,我通过预先分配数组内存并使用指针来填充数组,从而使对比更加公平。结果脚本并没有长太多:

新代码需要 3μs,这个时间是 C 版本的两倍,已经不错了。不过,出于完整性考虑,我继续分析代码,以便找出与 C 版本的区别。原来,我使用的 reduce 方法使用了 nextPartialResult 函数,这个函数提供了不必要的泛化,会执行一些不必要的间接操作。在使用指针对它重写之后,我终于使它达到了 C 版本的速度。这显然违背了使用 Swift 的目的,因为此时我们只是在编写更冗长、更丑陋的 C。如果以后你确实需要提速,可以用这个方法。

总而言之:Swift 无法用与 Python 相同的工作量换得 C 语言的速度,但你至少可以从中权衡利弊。

2. 函数签名巧妙

Swift 的函数签名采取了一种有趣的方式,其最基本的形式相对比较简单:

该函数签名由参数名及其类型组成;没什么太复杂的东西。唯一特别之处在于,Swift 要求在调用函数时提供参数名,因此,在调用 greet 时必须写上 person 和 town,如上述代码片段最后一行所示。

当我们引入参数标签(argument labels)时,事情就变得更有趣了。

参数标签就是它们听起来的样子:它们是函数参数的标签,它们在函数签名中各自的参数之前声明。在上面的例子中,from 是 town 的参数标签,而_是 person 的参数标签。我用_表示后一个标签,因为_是 Swift 中的一个特例,意思是“在调用这个参数时不需要提供任何参数名。”

使用参数标签,每个参数都有两个不同的名称:一个参数标签,用于调用函数,另一个参数名称用于函数体定义。这可能看起来有点随意,但它使代码更易于阅读。

如果你仔细看一下上面的函数签名,就会发现它几乎就像在读英语:“Greet person from town”。这个函数调用是描述性的:“Greet Bill from Cupertino”。如果没有参数标签,事情就会变得比较含糊:“Greet person town”。我们不知道 town 代表什么。是我们现在所在的城镇吗?是我们要去那个城镇见那个人吗?还是这个人来自那个城镇?如果没有参数标签,我们将不得不阅读函数的主体以了解发生了什么,或者采用更长的函数名或参数名使它们更具描述性。如果参数很多,会变得很复杂,在我看来,这会产生很多不必要的长函数名,让代码更丑陋。参数标签更漂亮,扩展性更好,而且它们在 Swift 中被广泛使用。

3. 大量使用闭包

Swift 大量使用了闭包。因此,它有一些快捷方式,使其更加人性化。这个例子来自该语言的文档,着重说明了这些快捷方式如何使 Swift 简洁而富有表现力。

让我们创建一个数组用于反向排序:

不那么符合习惯的做法是使用 Swift 的数组排序方法,并使用一个自定义函数来告诉它如何对数组元素的顺序进行两两比较,如下所示:

backward 函数一次比较两个数据项,如果它们的顺序符合要求,则返回 true;如果不符合要求,则返回 false。数组的 sorted 方法需要这样一个函数作为输入才能知道如何对数组进行排序。顺便说一下,我们还可以看到,这里使用了参数标签,非常简洁。

如果希望采用更地道的 Swift 语法,可以使用闭包:

{}之间的代码是定义一个闭包,同时将其作为一个参数传递。如果你从未听说过闭包,那么我稍微说明下,闭包是捕获其上下文的未命名函数。我们可以把它们看做是加强型的 Python lambdas。闭包中的关键字 in 用于分隔闭包的参数及其主体。更直观的关键字,如:已经被签名类型定义占用(闭包的参数类型可以从 sorted 的方法签名自动推断,这种情况是可以避免的)在编程中,命名一个东西是最困难的事情之一,我们坚持使用不那么直观的 in 关键字。

无论如何,代码看起来已经更简洁了。

然而,我们可以做得更好:

这里删除了 return 语句,因为在 Swift 中,单行闭包是隐式返回的。

不过,我们还可以做些更深入地研究:

Swift 还隐式地命名了位置参数,所以在上面的例子中,1 是第二个参数,$2 是第三个参数,依此类推。目前代码已经很紧凑了,而且很容易理解,但我们可以更进一步:

在 Swift 中,>操作符是一个名为>的函数。因此,我们可以将它传递给排序方法,使代码更简洁,可读性更强。

这也适用于+=、-=、<、>、==和=等操作符,你可以在标准库中找到它们的定义。这些函数/操作符与普通函数之间的区别是,前者已经在标准库中使用 infix、prefix 或 suffix 关键字显式地声明为操作符。例如,+=函数在 Swift 标准库的这一行上被定义为一个操作符。可以看到,操作符符合几个不同的协议,比如数组和字符串,因为许多不同的类型都有自己的+=函数实现。

更有趣的是,我们可以自定义操作符。库就是一个很好的例子。该库支持用户加载图片,用一系列转换对其进行修改,然后以某种方式输出。自然,这些转换序列的定义在库中反复出现,因此,库的创建者决定定义一个名为–>的新操作符,用于将这些转换链接在一起:

在上面这段比较简洁的代码中,首先声明–>函数,然后将其定义为 infix 操作符。infix 的意思是,要使用这个操作符,必须将它放在它的两个参数之间。你可以编写下面这样的代码:

上面的方法比一堆链式方法或一系列 source. addtarget(…)函数更简短,更容易理解。

4. 基本类型是在标准库中定义的结构

我在上文中提到过,Swift 的基本类型是在标准库中定义的结构,而不是像在其他语言中那样硬编码到编译器中。这很有用,其中一个原因是它允许我们使用一个名为 extension 的 Swift 特性,该特性可以给任何类型添加新功能,包括基本类型,例如:

虽然不是特别有用,但这个例子展示了该语言的可扩展性,因为它允许你做类似“在 Swift 解释器中输入任何数字”这样的事,并调用任何你想要的自定义方法。

5.编译器+解释器+Jupyter Notebook

除了有一个编译器,Swift 还有一个解释器,并提供了对Jupyter Notebook的支持。解释器特别适合学习这门语言,你可以在命令提示符处键入 swift 并立即尝试编写代码,这与使用 Python 的方式非常相似。另一方面,与 Jupyter Notebook 的集成在可视化数据、执行数据探索和编写报告方面非常出色。最后,当你运行生产代码时,可以编译它并利用提供的强大优化功能。

谷歌的总体规划

我在上面的段落中提到了很多特性,但是有一个特性与其他特性不同:Jupyter 支持非常新,实际上是由 S4TF 团队添加的。这是值得注意的,因为它让我们可以了解谷歌在开展这个项目时的心态:他们不只是想为 Swift 创建一个库,他们想要深入地改进 Swift 语言本身,以及它的工具,然后使用该语言的改进版本创建一个新的 Tensorflow 库。

关于这一点,最好看一下 S4TF 团队把大部分时间都花在了哪儿。他们所做的大部分工作都是针对苹果的 Swift 编译库本身。更具体地说,谷歌所做的大部分工作都是在 Swift 编译器存储库里的一个开发分支中。谷歌正在为 Swift 语言添加新功能,首先在他们自己的分支中创建和测试,然后将它们合并到苹果的主分支中。这意味着世界各地的 iOS 设备上运行的标准 Swift 语言最终将包含这些改进。

现在,让我们进入有趣的部分:谷歌在 Swift 中构建了哪些功能?

让我们从大的开始。

可微编程

最近,围绕可微编程有很多炒作。特斯拉的人工智能总监 Andrej Karpathy 将其称为软件2.0,而 Yan LeCun 则宣称:“深度学习已死。可微编程万岁。”另一些人则认为需要创建一套全新的工具,比如新的 Git、新的 IDE,当然还有新的编程语言。

那么,什么是可微编程?

简而言之,可微编程是一种编程范式,在这种范式中,程序本身是可微的。你可以设置一个想要优化的目标,让程序根据目标自动计算它自己的梯度,然后在这个梯度的方向上对自己进行微调。这正是你训练神经网络时所做的工作。

让程序自我调优最吸引人的一点是,它可以创建那些我们似乎完全无法自己编程的程序。考虑这个问题的一个有趣的方式是,你的程序使用它的梯度来对自己进行调整,从而完成某个任务,而且在编程方面比你做得更好。过去几年的情况表明,适用的案例确实越来越多,而且这种增长还没有明显的结束迹象。

一门可微语言

经过这么长时间的介绍,现在是时候介绍谷歌的愿景了,下面是原生可微编程在 Swift 中实现:

这里首先定义一个名为 cube 的简单函数,该函数返回其输入的立方。接下来是令人兴奋的部分:我们创建原函数的导函数,通过在它上面调用 gradient。这里没有使用库或外部代码,gradient 是 S4TF 团队在 Swift 语言中引入的一个新函数。该功能利用对 Swift 内核的修改来自动计算梯度函数。

这是 Swift 的一大新特性。你可以使用任何 Swift 代码,只要它是可微的,就可以自动计算它的梯度。上面的代码没有导入或奇怪的依赖,它只是简单的 Swift。其他大型的机器学习库,比如 PyTorch、TensorFlow 它都支持这个特性,但只有在使用特定库的操作时才会这样。此外,在这些 Python 库中使用梯度不像在普通 Swift 中那样轻量、透明并良好集成。

据我所知,Swift 是第一种为这种做法提供原生支持的主流语言,是一个巨大创新。

为了进一步说明这在现实世界中会是什么样子,请看下面的脚本。下面这个例子说明新特性在标准机器学习训练工作流中的用法:

struct Perceptron: @memberwise Differentiable {var weight: SIMD2<Float> = .random(in: -1..<1)var bias: Float = 0@differentiablefunc callAsFunction(_ input: SIMD2<Float>) -> Float {(weight * input).sum() + biasvar model = Perceptron()let andGateData: [(x: SIMD2<Float>, y: Float)] = [(x: [0, 0], y: 0),(x: [0, 1], y: 0),(x: [1, 0], y: 0),(x: [1, 1], y: 1),for _ in 0..<100 {let (loss,         
声明:本文来自用户分享和网络收集,仅供学习与参考,测试请备份。