Swift 与谷歌的可微编程项目

阅读数:3 2020 年 5 月 16 日 09:00

Swift与谷歌的可微编程项目

本文最初发布于 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 在网络中构建一个打印节点,该节点必须连接到网络中的另一个节点,并在打印之前进行编译。

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

行业滞后

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

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

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

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

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

速度

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

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

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

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

谷歌接受了挑战

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

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

  • Go :他们在文档中指出,Go 过于依赖其接口提供的动态调度,为了实现想要的功能,他们不得不对语言进行重大修改。这违背了 Go 提倡的简单和体积小的哲学。相反,Swift 的协议和扩展提供了很多自由,你可以根据需要定义调度的静态程度。而且,Swift 语言已经非常复杂了(而且在变得越来越复杂),因此,让它更复杂一点以适应谷歌想要的特性并不会造成很大的问题。
  • C++ &  Rust :谷歌的目标用户群是那些大部分时间都在使用 Python 的人,他们更感兴趣的是花时间考虑模型和数据,而不是管理内存分配或所有权之类的事情。Rust 和 C++ 具有一定的复杂性和对底层细节的关注,这在进行数据科学 / 机器学习开发时通常是不合理的。
  • Julia :如果你在 HackerNews Reddit 上阅读任何有关 S4TF 的帖子,第一个评论通常是“为什么他们不选择 Julia?”谷歌在档中提到,Julia 看起来也很有前途,但是,至于就为什么没有选择它的原因并没有提供坚实的理由。文中提到,Swift 社区比 Julia 大得多,这是事实,但 Julia 科学和数据科学社区比 Swift 大得多,这些社区使用 S4TF 可能会更多。需要记住的是,谷歌团队在 Swift 方面更专业,因为 Swift 的创始人 Chris Lattner 启动了这个项目,所以这可能在决定中起了很大的作用。
  • 一门新语言:我认为他们在宣言中说得很好:“创造一门语言是一项工作量大得荒谬的工作”。这将花费太长时间,而且很可能跟不上机器学习的发展速度。

Swift 酷在哪?

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

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

1. 速度快

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

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

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 标准类型并没有硬编码到编译器中,它们是在标准库中定义的结构体。因此,根据数组的追加定义,我们可以看到发生了很多事情。了解了这一点之后,我通过预先分配数组内存并使用指针来填充数组,从而使对比更加公平。结果脚本并没有长太多:

Swift与谷歌的可微编程项目

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

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

2. 函数签名巧妙

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

Swift与谷歌的可微编程项目

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

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

Swift与谷歌的可微编程项目

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

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

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

3. 大量使用闭包

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

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

Swift与谷歌的可微编程项目

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

)Swift与谷歌的可微编程项目

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

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

Swift与谷歌的可微编程项目

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

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

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

Swift与谷歌的可微编程项目

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

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

Swift与谷歌的可微编程项目

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

Swift与谷歌的可微编程项目

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

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

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

Swift与谷歌的可微编程项目

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

Swift与谷歌的可微编程项目

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

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

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

Swift与谷歌的可微编程项目

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

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

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

谷歌的总体规划

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

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

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

让我们从大的开始。

可微编程

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

那么,什么是可微编程?

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

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

一门可微语言

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

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
@differentiable
func callAsFunction(_ input: SIMD2<Float>) -> Float {
(weight * input).sum() + bias
}
}
var 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, 𝛁loss) = valueWithGradient(at: model) { model -> Float in
var loss: Float = 0
for (x, y) in andGateData {
let ŷ = model(x)
let error = y - ŷ
loss = loss + error * error / 2
}
return loss
}
print(loss)
model.weight -= 𝛁loss.weight * 0.02
model.bias -= 𝛁loss.bias * 0.02
}

同样,上面的代码都是普通的 Swift 代码,没有外部依赖。在这段代码中,我们看到谷歌引入了两个新的 Swift 特性:callAsFunction 和 valueWithGradient。第一个非常简单,它实例化类和结构,然后像调用函数一样调用它们。这里的 Perceptron 结构被实例化为模型,然后在 let ŷ = model(x) 中,模型作为函数被调用。这样做时,callAsFunction 是实际被调用的方法。如果使用过 Keras 或 PyTorch 模型,就会知道这是处理模型 / 层的一种非常常见的方法。虽然这两个库都使用 Python 的 _call_ 方法来实现自己的 call 和 forward 方法,但是 Swift 没有这样的特性,因此谷歌必须添加它。

在上面的脚本中,另一个有趣的新特性是 valueWithGradient。这个函数返回一个函数或闭包在特定点上的结果值和梯度。在上面的例子中,我们定义并用作 valueWithGradient 输入的闭包实际上是损失函数。这个损失函数将我们的模型作为输入,所以当我们说 valueWithGradient 将会在一个特定的点上对函数进行评估时,实际上是,它将会在特定的权重配置下用模型评估损失函数。在计算了前面提到的值和梯度之后,我们打印值(即损失),并使用梯度更新模型权重。重复一百次,我们就有了一个训练好的模型。你会注意到,我们可以在损失函数中访问 andGateData,这是 Swift 闭包能够捕获其封闭上下文的一个例子。

微分外部代码

另一个很棒的特性是,我们不仅可以微分 Swift 操作,还可以区分外部非 Swift 库中的操作,只要我们手动告诉 Swift 它们的导数是什么即可。这意味着,你可以使用 C 语言库来快速实现一些目前在 Swift 中没有的操作,并将其导入到项目中,编写导函数,然后在大型神经网络中使用,让反向传播这样的东西可以无缝地工作。

更重要的是,实现这一点非常简单:

复制代码
import Glibc // we import pow and log from here
func powerOf2(_ x: Float) -> Float {
return pow(2, x)
}
@derivative(of: powerOf2)
func dPowerOf2d(_ x: Float) -> (value: Float, pullback: (Float) -> Float) {
let d = powerOf2(x) * log(2)
return (value: d, pullback: { v in v * d })
}
powerOf2(3), // 8
gradient(of: powerOf2)(3) // 5.545

Glibc 是一个 C 语言库,所以 Swift 编译器不知道其操作的导数是什么。我们可以使用 @derivative 给编译器提供这些信息,然后就可以轻松地使用这些外部操作和本地操作形成一个大的可微网络。在本例中,我们从 Glibc 导入 pow 和 log,并使用它们创建函数 powerOf2 及其导函数。

TensorFlow Library for Swift 的当前版本就是使用这个特性构建的。该库从 TF Eager 库的 C API 中导入所有的操作,但是它没有插入 TensorFlow 的自动微分系统,而是指定了每个基本操作的导数,并让Swift 来处理它。然而,并不是所有的操作都需要,因为许多操作是更基本操作的组合,因此,Swift 可以自动推断出它们的导数。然而,基于TF Eager 库的当前版本有一个很大的缺点:TF Eager 确实很慢,因此,Swift 版本也很慢。这似乎只是一个暂时的问题,它会随着 XLA (通过 x10)和 MLIR 的合并而得到修复。

说到这里,使用这个临时的解决方案让谷歌的开发人员可以开展 Swift TensorFlow API 相关的工作,这个 API 已经开始成形了。下面是一个简单的模型训练作业:

复制代码
import TensorFlow
let hiddenSize: Int = 10
struct IrisModel: Layer {
var layer1 = Dense<Float>(inputSize: 4outputSize: hiddenSize, activation: relu)
var layer2 = Dense<Float>(inputSize: hiddenSize, outputSize: hiddenSize, activation: relu)
var layer3 = Dense<Float>(inputSize: hiddenSize, outputSize: 3)
@differentiable
func callAsFunction(_ input: Tensor<Float>) -> Tensor<Float> {
return input.sequenced(through: layer1, layer2, layer3)
}
}
var model = IrisModel()
let optimizer = SGD(for: model, learningRate: 0.01)
let (loss, grads) = valueWithGradient(at: model) { model -> Tensor<Float> in
let logits = model(firstTrainFeatures)
return softmaxCrossEntropy(logits: logits, labels: firstTrainLabels)
}
print("Current loss: \(loss)")

可以看出,它与我们之前看到的没导入模型的训练脚本非常相似。它有一个非常像 PyTorch 的设计,这很棒。

Python 互操作

目前,Swift 的机器学习和数据科学生态系统仍处于起步阶段,这个问题急需解决。幸运的是,谷歌通过在 Swift 中包含 Python 互操作来解决这个问题。其思想是为了能在 Swift 代码中编写 Python 代码,并以这种方式访问大量的优秀 Python 库。

这方面的典型用例是,用 Swift 训练一个模型,并使用 Python 的 matplotlib 来绘制它:

复制代码
import Python
print(Python.version)
let np = Python.import("numpy")
let plt = Python.import("matplotlib.pyplot")
// let time = np.arange(0100.01)
let time = Array(stride(from: 0, through: 10, by: 0.01)).makeNumpyArray()
let amplitude = np.exp(-0.1 * time)
let position = amplitude * np.sin(3 * time)
plt.figure(figsize: [1510])
plt.plot(timeposition)
plt.plot(time, amplitude)
plt.plot(time, -amplitude)
plt.xlabel("Time (s)")
plt.ylabel("Position (m)")
plt.title("Oscillations")
plt.show()

它看起来像普通的老式 Python,只是添加了 let 和 var 语句。这是谷歌提供的代码示例。我所做的唯一修改是注释掉了一行 Python 代码,并用 Swift 重写了它,这样就可以看到它们是如何结合在一起的。它不像在 Python 中那样简洁,因为我必须使用 makeNumpyArray() 和 Array(),但是它可以工作,这非常棒。

谷歌通过引入 PythonObject 类型实现了这一点,它可以表示 Python 中的任何对象。Python 互操作项目包含在单独的 Swift 库中,因此,S4TF 团队只需要对 Swift 语言本身做一些补充,比如添加一些改进,以适应 Python 异乎寻常的动态性。至于支持程度,我还没有发现他们如何管理其他惯用的 Python 元素,比如 With 语句,而且,我确信还有一些极端情况需要考虑,但是,这已经是一个令人惊奇的特性了。

在讨论 Swift 与其他语言的集成时,起初,我对 Swift 的兴趣之一是确定它在实时计算机视觉任务中的表现。出于这个原因,我最终找到了 OpenCV 的一个 Swift 版本,通过 FastAI 的论坛,我找到了一个很有用的 OpenCV 包装器 SwiftCV 。不过,这个库有点奇怪:OpenCV 是用 C++ 构建的(并且已经弃用了它的 C API),而 Swift 目前还不支持 C++(尽管很快就会支持)。因此,SwiftCV 不得不将 OpenCV 的代码包装成 C++ 代码的 C 兼容子集,然后作为 C 语言库导入。只有这样,才能使用 Swift 将 OpenCV 包装。

我决定将视频支持添加到 SwiftCV 中,因为我需要这个特性,而项目当时还没有提供。我还想在比教程更复杂的情况下测试 Swift 的 C 语言互操作能力。因此,我提交了这个 pull request ,它是一个有用的自包含示例,展示了 Swift 如何通过 C 包装器与 C++ 互操作。这个过程不难,我这样的 Swift 新手都可以完成。

项目现状

尽管我对 S4TF 项目给予了很多赞许,但我不得不承认,它仍然不能用于一般的生产用途。新的 API仍在不断变化,新的 TensorFlow 库性能仍然不是很好,即使它的数据科学生态系统在发展壮大,也还仍然处于起步阶段。最重要的是,Linux 支持仍然很弱,目前只有 Ubuntu 得到官方支持。考虑到这一点,为确保所有这些问题都能迅速得到解决,还有很多工作要做。

谷歌也正在努力提高性能,包括最近增加 x10 以及努力使 MLIR 达标。同时,谷歌的很多项目也在致力于将很多 Python 数据科学生态系统的东西复制到 Swift 生态系统,如 SwiftPlot 、类似 Pandas 的 Penguin 以及类似 Scikit-learn 的 swiftML ,等等。

然而,最令人惊讶的是,苹果正朝着与谷歌相同的方向快速前进。在他们为 Swift 的下一个主要版本制定的路线图中,他们将在非苹果平台上发展Swift 软件生态系统作为首要目标。这反映在苹果对几个项目的支持上,比如 Swift 服务器工作组、类似 numpy 的 Numerics 、运行在 Linux 上的官方语言服务器,以及将 Swift 移植到 Windows 上的工作。

此外,来自 Fast.ai 的 Sylvain Gugger 目前正在构建一个 Swift 版本的 FastAI ,而 Jeremy Howard 已经将 Swift 课程加入到他们非常受欢迎的在线课程中。同时,第一批基于 S4TF 库学术论文已经开始发表了!

结论

在我个人看来,虽然 Swift 有很大的机会成为机器学习生态系统中的关键角色,但仍然存在风险。最大的风险在于,尽管存在缺陷,但 Python 确实足以胜任大部分机器学习任务。对于许多已经熟悉 Python 并认为没有理由切换到另一种语言的人来说,这种惯性可能太大了。此外,谷歌有放弃大型项目的传统,而 S4TF 项目中的一些关键问题让人们忧心忡忡

在一通免责声明之后,我仍然认为 Swift 是一门伟大的语言,而且新添加的内容非常具有创新性,它最终肯定会在机器学习社区中找到自己的位置。因此,如果你想为一个具有巨大增长潜力的项目做出贡献,现在就是开始的好时机。事情还没有完全确定,还有很多工具需要开发,随着 Swift 机器学习生态系统的不断发展,未来小型的个人项目也可能会成为巨大的社区项目。

原文链接:

Swift: Google’s bet on differentiable programming

评论

发布
暂无评论