首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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.AIJeremy 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:如果你在HackerNewsReddit上阅读任何有关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和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还隐式地命名了位置参数,所以在上面的例子中,$0是第一个参数,$1是第二个参数,$2是第三个参数,依此类推。目前代码已经很紧凑了,而且很容易理解,但我们可以更进一步:

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

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

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

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

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

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

我在上文中提到过,Swift的基本类型是在标准库中定义的结构,而不是像在其他语言中那样硬编码到编译器中。这很有用,其中一个原因是它允许我们使用一个名为extension的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中实现:

这里首先定义一个名为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: 4, outputSize: 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(0, 10, 0.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: [15, 10])
plt.plot(time, position)
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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/0tsoqOTgICZGOUtvhHHN
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券