选自tryolabs
作者:JoaquínThu
设备之心编译
参与:Panda、Racoon
Python 并不完美,而 Swift 则正在谷歌和苹果的一起养育下茁壮成长,有望成长为深度学习行业一门新的重点语言。近期,Tryolabs 的科研工程师 Joaquín Alori 发布了一篇长文,从 Python 的缺点一路谈到了谷歌在 Swift 设备学习方面的大计划,并且文中还给出了相当多有些详细的代码实例。可微分编程真如 Yann LeCun 所言的那样会作为新一代的程序研发范式吗?Swift 又将在其中扮演怎么样的角色?亦许你能在这篇文案中找到答案。
近期,国外一小哥在 tryolabs 上写了一篇博文,为咱们详尽地介绍了 Python 的缺陷与相比之下 Swift 的优良,解释了为何 Swift 版的 TensorFlow 将来在设备学习行业有非常好的发展前景。其中包括海量代码示例,展示了怎样用 Swift 优雅地编写设备学习程序。
两年之前,谷歌的一个小团队起始科研让 Swift 语言作为首个在语言层面上一流地整合了可微分编程能力的主流语言。该项目的科研范围着实与众区别,况且亦取得了有些出色的初期科研成果,似乎离公众应用亦并不很远了。
尽管如此,该项目却并未在设备学习社区导致多大反响,况且非常多实践者还对此浑然不觉。导致这种结果的重点原由之一是语言的选取。设备学习社区的非常多人很大程度上并不关心 Swift,谷歌科研它亦让人们感到疑惑;由于 Swift 重点用来研发 iOS 应用罢了,在数据科学生态系统中几乎毫无存在感。
不外,事实却并非如此,只需粗略地瞧瞧谷歌这个项目,就能发掘这是一个庞大且雄心勃勃的计划,乃至足以将 Swift 确立为设备学习行业的关键成员。另外,即使咱们 Tryolabs 亦重点运用 Python,但咱们还是认为 Swift 是一个绝佳的选取;亦因此呢,咱们决定写这篇文案以帮忙世人认识谷歌的计划。
但在深入 Swift 以及「可微分编程」的真正含义之前,咱们应该先回顾一下当前的情况。
Python,你怎么了?!
到日前为止,Python 都依然是设备学习行业最常被使用的语言,谷歌亦有海量用 Python 编写的设备学习软件库和工具。那样,为何还要用 Swift?Python 有什么问题吗?
直接说吧,Python 太慢了。另一,Python 的并行性表现并欠好。
为了应对这些缺点,大都数设备学习项目在运行计算密集型算法时,都会运用用 C/C++/Fortran/CUDA 写的软件库,而后再运用 Python 将区别的底层运算组合到一块。针对大部分项目而言,这种做法其实效果很好;但总体概况而言,这会产生有些问题。咱们先瞧瞧其中有些问题。
外边二进制文件
为每一个计算密集型运算都调用外边二进制文件会限制研发者的工作,让她们只能在算法的表层的一小部分上进行研发。例如,编写自定义的卷积执行方式是没法实现的,除非研发者愿意运用 C 等语言来进行研发。大部分程序员都不会选取这么做,要么是由于她们无编写低层高性能代码的经验,要么则是由于在 Python 研发环境与某个低层语言环境之间来回切换会变得过于麻烦。
这会导致一种不幸的状况:程序员会尽力尽可能少地写繁杂代码,并且默认状况更倾向于调用外边软件库的运算。针对设备学习这般动态发展的行业来讲,这并不是一个好现象,由于非常多东西都还并未确定下来,还非常需要新想法。
对软件库的抽象理解
让 Python 代码调用更低层代码并不如将 Python 函数映射成 C 函数那样简单。不幸的现实是:设备学习软件库的创建者必须为了性能而做出有些研发上的选取,而这又会让事情变得更加繁杂。举个例子,在 TensorFlow 图(graph)模式中(这是该软件库中独一的性能模式),你的 Python 代码在你认为会运行时常常并不运行。在这儿,Python 实质上的功效是底层 TensorFlow 图的某种元编程(metaprogramming)语言。
其研发流程为:研发者首要运用 Python 定义一个网络,而后 TensorFlow 后端运用该定义来构建网络并将其编译为一个 blob,而研发者却再亦没法拜访其内部。编译之后,该网络才最终能够运行,研发者能够起始向其馈送数据以便训练和推理。这种工作方式让调试工作变得非常困难,由于在网络运行时,你没法运用 Python 认识其中到底出现了什么。你亦没法运用 pdb 等办法。即使你想运用古老但好用的 print 调试办法,你亦只能运用 tf.print 并在你的网络中构建一个 print 节点,这又必须连接到网络中的另一个节点,况且在 print 得到任何信息之前还必须进行编译。
不外亦存在更加直接的处理方法。用 PyTorch 时,你的代码必须像用 Python 同样命令式地运行,独一不透明的状况是运行在 GPU 上的运算是异步式地执行的。这一般不会有问题,由于 PyTorch 对此很智能,它会等到用户交互操作所依赖的所有异步骤用都结束之后才会转让掌控权。尽管如此,亦还是有有些问题存在,尤其是在基准评测(benchmarking)等任务上。
行业滞后
所有这些可用性问题不仅让写代码更困难,况且还会引起产业界毫无必要地滞后于学术界。始终败兴都有论文在科研怎样调节神经网络中所用的低层运算,并在这一过程中将准确度提高几个百分点,然则产业界仍然需要很长期才可实质应用这些发展。
一个原因是即使这些算法上的改变可能本身比较简单,但上面说到的工具问题还是让它们非常难以实现。因此呢,因为这些改进可能只能将准确度提高 1%,因此企业可能会认为为此进行投入并不值得。针对小型设备学习研发团队而言,这个问题尤为显著,由于她们常常缺乏可包袱实现/整合成本的规模经济。
因此呢,企业常常会直接忽略这些进步,直到这些改进被加入到 PyTorch 或 TensorFlow 等软件库中。这能节省企业的实现和整合成本,但亦会引起产业界滞后学术界一两年时间,由于这些软件库的守护者基本不会立即实现每篇新论文提出的新办法。
举个详细的例子,可变形卷积似乎能够提高大都数卷积神经网络(CNN)的性能表现,但论文发布大概 2 年之后才显现第1个开源的实现。不仅如此,将可变形卷积的实现整合进 PyTorch 或 TensorFlow 的过程非常麻烦,况且最后这个算法亦并没得到广泛的运用。PyTorch 直到近期才加入对它的支持,至于官方的 TensorFlow 版本,迄今仍无见到。
此刻,假设说有 n 篇能将准确度提升 2% 的论文都遇到了这种状况,那样产业界将错失准确度明显提高 (1.02^n)% 的机会,而原由不外是无合适的工具罢了。倘若 n 很大,那就太让人遗憾了。
速度
在某些状况中,同期运用 Python 与快速软件库依然还是会很慢。确实,倘若是用 CNN 来执行图像归类,那样运用 Python 与 PyTorch/TensorFlow 会火速。另外,就算在 CUDA 环境中编写全部网络,性能亦可能并不会得到太多提高,由于大卷积占据了大部分的推理时间,而大卷积又已然有了经过良好优化的代码实现。但状况并非总是如此。
倘若不是完全用低层语言实现的,那样由非常多小运算构成的网络常常最容易显现性能问题。举个例子,Fast.AI 的 Jeremy Howard 曾在一篇博客文案中表达了自己对用 Swift 来做深度学习研发的热爱,他暗示尽管运用了 PyTorch 那出色的 JIT 编译器,他仍然没法让 RNN 的工作速度比肩完全用 CUDA 实现的版本。
另外,针对延迟程度很重要的状况,Python 亦不是一种非常好的语言;况且 Python 亦不可很好地应用于与传感器通信等非常底层的任务。为认识决这个问题,有些机构的做法是仅用 Python 和 PyTorch/TensorFlow 研发模型。这般,在实验和训练新模型时,她们就能利用 Python 的易用性优良。而在之后的生产安排时,她们会用 C++ 重写她们的模型。不确定她们是会完全重写,还是会运用 PyTorch 的 tracing 功能或 TensorFlow 的图模式来简单地将其串行化,而后再围绕它运用 C++ 来重写 Python。不管是哪种方式,都需要重写海量 Python 代码。针对小机构而言,这般做常常成本过高。
所有这些问题都是众所周知的。公认的深度学习教父之一 Yann LeCun 就曾说设备学习需要一种新语言。他与 PyTorch 的创建者之一 Soumith Chintala 曾在一组推文中讨论了几种可能的候选语言,其中说到了 Julia、Swift 以及改进 Python。另一方面,Fast.AI 的 Jeremy Howard 似乎已然下定决心站队 Swift。
谷歌接受了挑战
幸运的是,谷歌的 Swift for TensorFlow(S4TF)团队接过了这一困难。不仅如此,她们的全部项目发展还非常透明。她们还发布了一份非常详实的文档( https://github.com/tensorflow/swift/blob/master/docs/WhySwiftForTensorFlow.md),其中仔细地介绍了她们做出这一决定的历程,并解释了她们为这一任务思虑过的其它语言并最后选中 Swift 的原由。
在她们思虑过的语言中,最值得关注的包含:
Go:在这份文档中,她们暗示 Go 过于依赖其接口供给的动态调度,况且倘若要实现她们想要的特性,必须对这门语言进行大刀阔斧的修改。这与 Go 语言的保持简单和小表面积的哲学不符。相反,Swift 的协议和扩展都有很高的自由度:你想要调度有多静态,就能有多静态。另一,Swift 亦相当繁杂,况且还在越来越繁杂,因此再让它繁杂点以满足谷歌想要的特性并不是什么大问题。
C++ 和 Rust:谷歌的目的用户群是哪些大部分工作都运用 Python 的人,她们更感兴趣的是花时间思考模型和数据,而不是思考怎样精细地管理内存或所有权(ownership)。Rust 和 C++ 的繁杂度都足够,但都很注重底层细节,而这在数据科学和设备学习研发中一般是不恰当的。
Julia:倘若你在 HackerNews 或 Reddit 上读到过任何相关 S4TF 的帖子,那样最常看到的评论是:「为啥不选 Julia?」在前面说到的那份文档中,谷歌说到 Julia 看起来亦特别有潜能,但她们并未给出不选 Julia 的可靠理由。她们说到 Swift 的社区比 Julia 大得多,事实确实如此,然而 Julia 的研究社区和数据科学社区却比 Swift 大得多,而这些社区的人才更可能更加多地运用 S4TF。要记住,谷歌团队的 Swift 专业人才更加多,毕竟发起 S4TF 项目的正是 Swift 的创建者 Chris Lattner,相信这在谷歌的决定中起到了重大的功效。
一种新语言:作者认为她们在宣言中说得很好:「创建一种语言的工作量多得吓人。」这需要太长的时间,而设备学习又发展得太快。
那样,Swift 的优良在哪里?
简单来讲,Swift 让你可几乎完全用 Python 的方式在非常高的层面上进行编程,同期又能够保准非常快的速度。数据专家可像运用 Python 同样来运用 Swift,同期可用 Swift 内置的已优化设备学习库来进行更加精细的研发,例如管理内存,乃至当常用的 Swift 代码约束太大时还能降至指针层面进行操作。
本文的目的不是介绍 Swift 语言,因此不会连篇累牍地仔细介绍其特性。倘若你想仔细认识这门语言,看官方文档就够了。这儿只会介绍 Swift 的几个亮点,并期盼这能吸引人们去尝试它。下面几节将按随机次序介绍 Swift 的有些亮点,因此排序与它们的重要程度无关。之后,本文将深入介绍可微分编程,并聊聊谷歌在 Swift 上的大计划。
亮点一
Swift 速度火速。这是作者在起始运用 Swift 时所做的第1项测试。作者写了有些短脚本来评定 Swift 与 Python 和 C 的相对表现。说实话,这些测试并不尤其繁杂。亦便是用整型数填充一个数组,而后再将它们所有加起来。这个测试本身并不可透彻地认识 Swift 在各样状况下的速度表现,但作者想认识的是 Swift 能否达到 C 同样的速度,而不是 Swift 是不是总能和 C 同样快。
第1组比较作者选的是 Swift vs Python。为了让对应的每一行所执行的任务一致,作者对某些地区的花括号的位置进行了调节。
import time | import Foundation|result = [] | var result = [Int]()for it in range(15): | for it in 0.. start = time.time() | let start = CFAbsoluteTimeGetCurrent() for _ in range(3000): | for _ in 0.. result.append(it) | result.append(it)} sum_ = sum(result) | let sum = result.reduce(0, +) end = time.time() | let end = CFAbsoluteTimeGetCurrent() print(end - start, sum_) | print(end - start, sum) result = [] | result = []}
尽管在这个特定的代码段中,Python 与 Swift 代码看起来句法相近,但运行结果显示这个 Swift 脚本的运行速度比 Python 脚本的运行速度快 25 倍。在这个 Python 脚本中,最外层的循环每执行一次平均耗时 360 μs,相比之下 Swift 的是 14 μs。差别非常显著。
另一,亦还有其它有些事情值得重视。例如,+ 既是一个运算符亦是一个函数,它会被传递给 reduce(后面我会仔细介绍);CFAbsoluteTimeGetCurrent 揭示了 Swift 在传承下来的 iOS 命名空间方面的怪异特性;.< 范围运算符让你能够选取该范围是不是包括区间端点以及哪个端点。
然则,这个测试并不可说明 Swift 有多快。要晓得 Swift 有多快,咱们得将其与 C 来比比看。我亦这般做了,但让人失望的是,初始结果并欠好。用 C 编写的版本平均耗时 1.5 μs,比咱们的 Swift 代码快 10 倍。Uh oh.
不外老实讲,这般比较其实并不公平。这段 Swift 代码并没运用动态数组,因此呢当数组规模变大时,它会在内存堆中持续重新分配位置。这亦寓意着它会在每一个附加(append)的数组上执行边界检测。为了佐证这一点,咱们来瞧瞧关联定义。Swift 的标准类型包含整型、浮点数和数组,它们并无硬编码到编译器中,而是标准库中所定义的结构体(struct)。因此呢,按照数组的附加(append)定义,咱们能够认识到非常多信息。晓得了这一点后,我的测试方式乃至能够包含预分配数组的内存以及运用指针来填充数组。这般得到的脚本其实亦并不是很长:
import Foundation// Preallocating memoryvar result = ContiguousArray(repeating: 0, count: 3001)for it in 0.. let start = CFAbsoluteTimeGetCurrent()
// Using a buffer pointer for assignment result.withUnsafeMutableBufferPointer({ buffer infor i in 0.. buffer[i] = it } }) let sum = result.reduce(0, +) let end = CFAbsoluteTimeGetCurrent() print(end - start, sum)
这段新代码耗时 3 μs,速度已然达到 C 的一半,能够说是很不错的结果了。不外为了进行完整的比较,作者继续对代码进行了剖析,以便认识该代码的 Swift 版本和 C 版本的差异到底能够做到多小。事实证明,作者之前运用的 reduce 办法会毫无必要地间接运用 nextPartialResult 函数执行有些计算,这能够供给非必需的泛化能力。在运用指针重写了这段代码之后,作者最后让这段代码达到了与 C 同等的速度。然则,这显然不符合咱们运用 Swift 的目的,由于这种操作本质上便是写更冗长更丑陋的 C 语言。尽管如此,晓得在确实需要时能够达到 C 的速度亦是一件好事。
总结:运用 Swift,你没法在执行 Python 层面的工作时得到 C 语言等级的速度,但你能在两者之间取得良好的平衡。
亮点二
Swift 采用的函数签名办法亦特别有趣。它们的最基本形式其实相当简单:
func greet(person: String, town: String) -> String { return "Hello \(person)! Glad you could visit from \(town)."}
greet(person: "Bill", town: "Cupertino")
其函数签名由参数名加它们的类型形成,没其它多余花哨的东西。独一区别寻常的是 Swift 需要你在调用该函数时供给参数名,因此呢你在调用上面的 greet 时必须写下 person 和 town,如上面代码段中最后一行所示。
当咱们向其中引入参数标签时,状况还会变得更加有趣。
func greet(_ person: String, from town: String) -> String { return "Hello \(person)! Glad you could visit from \(town)."}
greet("Bill", from: "Cupertino")
顾名思义,参数标签便是函数的参数的标签,况且它们是在函数签名中各自的参数之前声明的。在上面的示例中,from 是 town 的参数标签,_ 是 person 的参数标签。针对最后一个标签,作者运用的是,由于 _ 在 Swift 中是一个特殊字母,其含义是:「在调用这个参数时不供给任何参数名。」
有了参数标签,每一个参数都有两个区别的名字:一个是参数标签,在调用该函数时运用;另一个是参数名,在函数的主体定义中运用。这看起来似乎有些任性,但会让你的代码更易读。
瞧瞧上面的函数签名,基本就像是在读英语。「Greet person from town.」上面的函数调用看起来亦一样清楚直白:「Greet Bill from Cupertino.」倘若无参数标签,就有些含混不清了:「Greet person town.」咱们不晓得这儿的 town 是什么意思。这是咱们此刻所处的城镇吗?还是咱们为了面见这个人而将要前去的城镇?又或是这个人本来来处的城镇?倘若无参数标签,咱们就必须阅读函数主体才可知晓实质状况,或采用让函数名或参数名更长更直白的办法。倘若你有海量参数,那样状况将变得非常繁杂;在作者看来这会引起代码变得更丑况且会让函数名变得毫无必要地长。参数标签更加好看,况且亦更易扩展,况且幸运的是它们亦在 Swift 中得到了广泛的应用。
亮点三
Swift 广泛地运用了闭包(closure)。因此呢,有有些捷径可让该语言的运用更接近人的直觉。这个来自 Swift 的文档的示例展现了这些捷径简洁明了又拥有很强的表现力的特性。
咱们的目的是将下面的数组向后排序:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
倘若用不那样地道的 Swift 代码形式,可为数组运用 sorted 办法,并采用一个自定义函数来定义按逐对次序比较数组元素的方式,就像这般:
func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2}var reversedNames = names.sorted(by: backward)
backward 函数一次可比较两项,倘若这两项的次序与所需次序同样,则返回 true;否则便返回 false。sorted 数组办法需要这般一个函数做为一个输入才可晓得怎样对数组进行排序。顺便一提,咱们还能够看到这儿运用了参数标签 by——这是如此的简洁明了。
倘若咱们采用更地道的 Swift,能够发掘运用闭包能更好地完成这项任务。
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
{} 之间的代码是一个正被定义的闭包,同期亦被传递用作 sorted 的一个参数。你亦许从未听说过闭包,但其实很简单,闭包便是一个获取上下文的未命名的函数你能够将其看作是加强版的 Python lambda。该闭包中的关键词 in 的功效是掰开该闭包的参数及其主体。: 等更直观的关键词已被签名类型定义所占用(在这个案例中,该闭包的参数类型是从 sorted 的签名中自动推导出来的,因此呢能够避免运用 :),况且咱们都晓得命名是编程中最艰难的事情之一,因此为此只能继续运用不那样直观的关键词了。
不管从哪个方向看,这段代码都已然简洁了许多。
但咱们还可能做得更好:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
这儿咱们移除了 return 语句,这是由于在 Swift 中,单行闭包就暗含了 return。
即便如此,咱们还能继续更进一步:
reversedNames = names.sorted(by: { $0 > $1 } )
Swift 亦有暗含的命名位置参数,因此在上面的案例中,$0 是第1个参数,$1 是第二个参数,$2 是第三个参数等等。这个代码已然很紧凑了,况且非常容易理解,然则咱们乃至还能做得更好:
reversedNames = names.sorted(by: >)
在 Swift 中,> 运算符便是一个名为 > 的函数。因此呢,咱们能够将其传递给 sorted 办法,使咱们的代码达到极端简洁和可读的程度。
这种操作适用于 +=、-=、、== 和 = 等运算符,你能够在标准库中查看它们的定义。这些函数/运算符与普通函数之间的差异是前者已在标准库中运用 infix、prefix 或 suffix 关键词显式地声明为运算符。举个例子,+= 函数在 Swift 标准库的这一行( https://github.com/apple/swift/blob/1ed846d8525679d2811418a5ba29405200f6e85a/stdlib/public/core/Policy.swift#L468)中被定义成为了一个运算符。能够看到,这个运算符遵循多个区别的协议,例如 Array 和 String,由于非常多区别的类型都有自己的 += 函数实现。
更进一步,咱们还能定义自己的自定义运算符。GPUImage2 软件库便是一个很好的例子。这个软件库让用户能够加载图像,运用一系列变换来修改它,而后再以某种方式来输出它。很自然,这些变换序列的定义会在该库中持续反复显现,因此呢这个库的创建者决定定义一个新的运算符 →,可用于将这些变换链接到一块。
func -->(source:T, destination:T) -> T { source.addTarget(destination) return destination}infix operator --> : AdditionPrecedence
在以上稍微简化过的代码中,首要声明了 --> 函数,而后其被定义为了一个 infix 运算符。infix 的意思是倘若要运用这个运算符,就必须将其安置在两个参数之间。这让你能够写出如下的代码:
let testImage = UIImage(named:"WID-small.jpg")!let toonFilter = SmoothToonFilter()let luminanceFilter = Luminance()let filteredImage = testImage.filterWithPipeline{input, output in input --> toonFilter --> luminanceFilter --> output // Interesting part}
比起一大堆互相链接的办法或一长串 source.addTarget(...) 函数,上面的代码要简短和容易多了。
亮点四
前面作者已然说到过,Swift 的基本类型是标准库中定义的结构体,况且并无硬编码到编译器中,由于它们一般是用其它语言写的。这特别有用处,一大原由是让咱们能够运用名叫扩展(extension)的 Swift 特性,其让咱们能够向任意类型添加新特性,包含基本类型。操作方式是这般的:
extension Double { var radians: Double { return self * (Double.pi / 180) }}360.radians // -> 6.28319
尽管这个例子并不是很有用,但亦展示 Swift 这门语言的扩展能力,由于这能让你做非常多事情,例如向 Swift 解释器输入任何数字以及在其上调用任何你想用的自定义办法。
最后一个亮点
除了持有编译器之外,Swift 还拥有解释器并且支持 Jupyter Notebook。在学习这门语言时,解释器尤其好用,由于它支持直接在命令提示符处输入 swift,而后马上起始代码测试。Python 亦具备差不多同样的功能。另一方面,因为整合了 Jupyter Notebook,因此呢能够容易进行可视化、执行数据探索和编写报告。最后,当你需要运行生产代码时,你能够编译它并利用 LLVM 供给的出色优化能力。
谷歌的大计划
作者在前面的章节中说到了 Swift 的有些特性,但其中有一个特性与其它区别:Jupyter Notebook 是新加入的,况且事实上正是由于 S4TF 团队加入的。这非常值得一说,由于这能让咱们一窥谷歌投入这个项目时的想法:她们不仅想为 Swift 语言本身创建一个软件库,况且她们还想深入地改进这门语言本身以及关联工具,而后再运用这门语言的改进版本创建一个新的 TensorFlow 软件库。
只要瞧瞧 S4TF 团队在那些工作上投入的时间最多就能看出这一点。她们到日前为止做的大部分工作都是在苹果机构的 Swift 编译器代码库本身上完成的。更详细而言,谷歌日前完成的大部分工作都在 Swift 编译器代码库中的一个 dev 分支中。谷歌正为 Swift 语言本身添加新特性——她们首要会在自己的分支中创建和测试这些新特性,而后会将它们合并到苹果的主分支中。这寓意着运行在世界各地的 iOS 设备上的标准 Swift 语言最后将能集成这些改进。
此刻来谈谈更实在的东西:谷歌正为 Swift 构建什么特性?
首要说个大特性。
可微分编程
近来,可微分编程炒得确实很热。特斯拉的人工智能负责人 Andrej Karpathy 叫作之为软件 2.0(Software 2.0),Yann LeCun 乃至宣叫作:「深度学习已死,可微分编程万岁。」另有些人则说有必要创建一套全新的工具了,包含新的 Git、新的 IDE 以及新的编程语言。Wink wink.
因此,什么是可微分编程?
简而言之,可微分编程是一种程序自己可被微分的编程范式。这让你能够设定一个你想要优化的详细目的,让你的程序能够按照这个目的自动计算自己的梯度,而后再在这个梯度的方向上优化自己。这和训练神经网络完全同样。
倘若能让程序自己优化自己,咱们亦许就能创造出咱们自己完全没法编写出来的程序。想想这一点还挺有趣:你的程序能够运用梯度针对特定任务优化自己,因此呢它的编程能力比你还强。过去几年的发展已然显示在越来越多的案例已然显现了这种状况,况且日前咱们还看不到这一发展趋势的终点。
一种可微分的语言
写了这么长的介绍之后,最终能够谈谈谷歌为 Swift 研发的原生可微分编程版本了。
func cube(_ x: Float) -> Float { return x * x * x}let cube = gradient(of: cube)cube(2) // 8.0cube(2) // 12.0
这儿咱们首要定义了一个简单的函数 cube,其返回的结果是输入的立方。接下来便是激动人心的部分了:咱们只需在原始函数上调用 gradient,就能创建原始函数的导数函数。这儿无运用任何软件库或外边代码,gradient 只是由于 S4TF 团队为 Swift 语言引入的一个新函数。该函数利用了 S4TF 团队对 Swift 内核进行的修改,能够实现梯度函数的自动计算。
这是 Swift 的一个重大新特性。针对任意 Swift 代码,只要是可微分的,都能够自动计算梯度。上面的代码无导入任何东西或奇怪的依赖包,就只是纯粹的 Swift。PyTorch、TensorFlow 或其它任何大型设备学习库都支持这一功能,但前提是你要运用特定于库的特定运算。况且在这些 Python 库中操作梯度并不如单纯用 Swift 那样轻量、透明,况且哪些库集成亦不如 Swift 原生集成那样好。
这是 Swift 语言的一个重大新特性;况且能够说 Swift 是首个为这一特性供给原生支持的主流语言
为了进一步说明这在实质应用中的运用方式,以下应用于一个标准设备学习训练流程的脚本更完整透彻展示了这一新特性:
struct Perceptron: @memberwise Differentiable { var weight: SIMD2 = .random(in: -1.. var bias: Float = 0
@differentiable func callAsFunction(_ input: SIMD2) -> Float { (weight * input).sum() + bias }}var model = Perceptron()let andGateData: [(x: SIMD2, 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.. let (loss, loss) = valueWithGradient(at: model) { model -> Float invar loss: Float = 0for (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。第1个很简单,其功效是实例化类和结构体,让咱们能够像调用函数同样调用它们。这儿,Perceptron 结构体被实例化为了 model,而后 model 又在 let ŷ = model(x) 中被做为一个函数而调用。在这般操作时,实质上调用的是 callAsFunction 办法。倘若你曾经用过 Keras 或 PyTorch 模型,你必定晓得这是一种处理模型/层的常用方式。但 Keras 和 PyTorch 这两个库运用了 Python 的 *call* 办法来实现它们各自的 call 和 forward;Swift 之前无这般的特性,于是谷歌把它加了进去。
上面的脚本中还有一个有趣的新特性:valueWithGradient。该函数会返回在特选定评定的函数或闭包的结果值和梯度。在以上案例中,咱们定义并用作 valueWithGradient 的输入的闭包实质上是咱们的损失函数。这个损失函数的输入是咱们的模型,因此当咱们说 valueWithGradient 会在特定的点评定咱们的函数时,咱们的意思是其会运用有特定权重配置的模型评定咱们的损失函数。计算了以上的值和梯度之后,咱们能够把值打印出来(这是咱们的损失)并运用梯度更新模型的权重。重复这一过程一百次,咱们就训练了一个模型。咱们还能够拜访损失函数内部的 andGateData,这是 Swift 闭包能够获取其周边上下文的又一案例。
微分外边代码
Swift 还有一个神奇的特性:咱们不仅能够微分 Swift 运算,还能微分外边的、非 Swift 的软件库——只需咱们在 Swift 中手动定义这些运算操作的导数。这寓意着你能够运用 C 软件库中有些非常快速的实现或有些 Swift 还不具备的运算操作。你只需将其导入到你的项目中、编写导数代码,而后就能够在你的大型神经网络中运用这些运算操作,让反向传播等功能无缝运行。
另外,这件事做起来其实非常简单:
import Glibc // we import pow and log from herefunc 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), // 8gradient(of: powerOf2)(3) // 5.545
Glibc 是一个 C 软件库,因此呢 Swift 编译器并不晓得其运算操作的导数是什么。经过运用 @derivative,咱们能够为编译器供给相关这些外边运算操作的导数的信息,而后搭配 Swift 的原生运算,能够非常容易地构建出大型的可微分网络。在这个示例中,咱们导入了 Glibc 的 pow 和 log,并用它们创建了 powerOf2 函数及其导数。
为 Swift 研发的新 TensorFlow 软件库的当前版本就正在运用这一特性进行研发。这个库从 TF Eager 软件库的 C API 导入了其所有运算操作,但其不是将 TensorFlow 的自动微分系统直接接上去,而是要指定每一个基本运算操作的导数,而后再让 Swift 处理。然则,并非所有运算都需要这种操作,由于许多运算都是更基本运算组合而成的,因此呢 Swift 能够自动推断它们的导数。但因为这个库的当前版本基于 TF Eager,因此呢存在一个大缺点:TF Eager 非常慢,因此呢 Swift 的这个版本亦很慢。这个问题应该只是暂时性的,随着与 XLA(经过 x10)和 MLIR 的整合,这个问题能够得到处理。
话虽如此,实质上 Swift TensorFlow API 已然初具规模,谷歌的研发者已然能够运用这个 API 进行研发了。运用它,你能够这般训练一个简单模型:
import TensorFlowlet hiddenSize: Int = 10struct IrisModel: Layer { var layer1 = Dense(inputSize: 4, outputSize: hiddenSize, activation: relu) var layer2 = Dense(inputSize: hiddenSize, outputSize: hiddenSize, activation: relu) var layer3 = Dense(inputSize: hiddenSize, outputSize: 3)
@differentiable func callAsFunction(_ input: Tensor) -> Tensor { 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 inlet logits = model(firstTrainFeatures) return softmaxCrossEntropy(logits: logits, labels: firstTrainLabels)}print("Current loss: \(loss)")
能够看到,这与之前的无导入的模型训练脚本非常类似。它的设计非常类似 PyTorch,真是太棒了。
与 Python 的互操作性
Swift 日前仍面临的一大问题是当前的设备学习和数据科学生态系统仍处在起步周期。幸运的是,谷歌正在处理这个问题,其方式是为 Swift 纳入 Python 互操作性。其想法是让研发者可在 Swift 代码中编写 Python 代码;经过这种方式,数量庞大的 Python 软件库就能为 Swift 所用了。
这种操作的一种典型用例是用 Swift 训练模型,而后用 Python 的 matplotlib 来绘制图表:
import Pythonprint(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();但这种操作是可行的。
谷歌成功实现 Python 互操作性的办法是引入了 PythonObject 类型,其可暗示 Python 中的任何对象。Python 互操作性被限定在单个 Swift 软件库中,因此呢 S4TF 团队仅需为 Swift 语言本身添加少量功能,例如添加少量改进以适应 Python 的极端动态性。至于此刻的 Python 支持已然达到了何种程度,日前尚不清楚她们将怎样处理 with 语句等更地道的 Python 元素,况且能够肯定地说还有其它有些极端状况有待思虑;尽管如此,此刻已然实现的成果就已然很不错了。
而在 Swift 与其它语言的整合方面,作者对 Swift 的最早的兴趣点之一便是想瞧瞧它在处理实时计算机视觉任务上的表现。由于这个原由,作者最后找到了 OpenCV 的一个 Swift 版本,而经过 FastAI 的论坛,最后找到了一个大有潜能的 OpenCV 封装类(wrapper):SwiftCV。然则,这个库很奇怪。OpenCV 是用 C++ 构建的(并且刚才废弃了其 C API),而 Swift 日前并不支持 C++(不外将会支持)。因此呢,SwiftCV 必须将 OpenCV 代码封装在 C++ 代码的一个兼容 C 的子集中,而后再以 C 软件包的形式导入。之后,才可将其封装到 Swift 中。
S4TF 项目的当前状态
尽管作者对 S4TF 项目始终不吝赞美之辞,但亦必须承认其还不足以支持通常的生产运用。其新的 API 仍在持续变化,这个新的 TensorFlow 库的性能亦仍然不是很好;即便其数据科学生态系统正在发展壮大,但总体仍处在起步周期。最重要的是,其 Linux 支持状况很奇怪,日前官方仅支持 Ubuntu。思虑到所有这些问题,要保准所有这些问题即时得到处理,还有非常多工作要做。
谷歌正在奋斗提高其性能,包含近期添加的 x10 以及在让 MLIR 达到标准方面所做的工作。另一,谷歌还有有些项目致力于在 Swift 中复制许多 Python 数据科学生态系统的功能,例如 SwiftPlot、类似 Pandas 的 Penguin、类似 Scikit-learn 的 swiftML。
但最让人惊讶的是苹果机构亦与谷歌在同一方向上推动 Swift 的发展。在苹果的 Swift 发展路线图上,下一个重大版本的重点目的是在非苹果平台上创立持续发展增长的 Swift 软件生态系统。这一目的亦反映在了苹果对多个项目的支持上,例如 Swift Server Work Group、类似 numpy 的 Numerics、一个运行在 Linux 上的官方语言服务器以及将 Swift 移植到 Windows 系统的工作。
另外,Fast.ai 的 Sylvain Gugger 亦正为 FastAI 构建一个 Swift 版本,而 Jeremy Howard 亦已然将 Swift 课程纳入到了她们的广受欢迎的在线课程中。另一,第1批基于 S4TF 关联软件库的学术论文亦正陆持续续发布出来。
总结
在作者自己看来,尽管 Swift 特别有可能发展成设备学习生态系统的一大关键角色,但危害仍然存在。其中最大的危害是:尽管 Python 存有缺陷,但针对大部分设备学习任务来讲已然足够好了。针对许多已然熟练 Python 的人来讲,惯性可能太大,亦无换成另一种语言的理由。另一,谷歌已然不是一次两次放弃大型项目了,而 S4TF 的有些关键人员的脱离亦让人担忧。
给出了这些免责声明之后,作者仍然觉得 Swift 是一门很棒的语言,这些新增的功能亦极具创新性,相信它们最后能在设备学习社区找到自己的位置。因此呢,倘若你亦想为这个潜能无穷的项目添砖加瓦,此刻便是很好的机会。Swift 在设备学习行业的地位还远未确立,还有非常多工拥有待研发。随着 Swift 设备学习生态系统的连续发展,此刻的小项目亦许将来能够成长为巨大的社区项目。
原文链接: https://tryolabs.com/blog/2020/04/02/swift-googles-bet-on-differentiable-programming/
|