谷歌為何要養(yǎng)蘋(píng)果的親兒子Swift?原來(lái)意在可微分編程
Python 并不完美,而 Swift 則正在谷歌和蘋(píng)果的共同養(yǎng)育下茁壯成長(zhǎng),有望成長(zhǎng)為深度學(xué)習(xí)領(lǐng)域一門(mén)新的主要語(yǔ)言。近日,Tryolabs 的研究工程師 Joaquín Alori 發(fā)布了一篇長(zhǎng)文,從 Python 的缺點(diǎn)一路談到了谷歌在 Swift 機(jī)器學(xué)習(xí)方面的大計(jì)劃,并且文中還給出了相當(dāng)多一些具體的代碼實(shí)例。可微分編程真如 Yann LeCun 所言的那樣會(huì)成為新一代的程序開(kāi)發(fā)范式嗎?Swift 又將在其中扮演怎樣的角色?也許你能在這篇文章中找到答案。
近日,國(guó)外一小哥在 tryolabs 上寫(xiě)了一篇博文,為我們?cè)敱M地介紹了 Python 的缺陷與相比之下 Swift 的優(yōu)勢(shì),解釋了為什么 Swift 版的 TensorFlow 未來(lái)在機(jī)器學(xué)習(xí)領(lǐng)域有非常好的發(fā)展前景。其中包含大量代碼示例,展示了如何用 Swift 優(yōu)雅地編寫(xiě)機(jī)器學(xué)習(xí)程序。
兩年之前,谷歌的一個(gè)小團(tuán)隊(duì)開(kāi)始研究讓 Swift 語(yǔ)言成為首個(gè)在語(yǔ)言層面上一流地整合了可微分編程能力的主流語(yǔ)言。該項(xiàng)目的研究范圍著實(shí)與眾不同,而且也取得了一些出色的初期研究成果,似乎離公眾應(yīng)用也并不很遠(yuǎn)了。
盡管如此,該項(xiàng)目卻并未在機(jī)器學(xué)習(xí)社區(qū)引起多大反響,而且很多實(shí)踐者還對(duì)此渾然不覺(jué)。造成這種結(jié)果的主要原因之一是語(yǔ)言的選擇。機(jī)器學(xué)習(xí)社區(qū)的很多人很大程度上并不關(guān)心 Swift,谷歌研究它也讓人們感到疑惑;因?yàn)?Swift 主要用來(lái)開(kāi)發(fā) iOS 應(yīng)用而已,在數(shù)據(jù)科學(xué)生態(tài)系統(tǒng)中幾乎毫無(wú)存在感。
不過(guò),事實(shí)卻并非如此,只需粗略地看看谷歌這個(gè)項(xiàng)目,就能發(fā)現(xiàn)這是一個(gè)龐大且雄心勃勃的計(jì)劃,甚至足以將 Swift 確立為機(jī)器學(xué)習(xí)領(lǐng)域的關(guān)鍵成員。此外,即使我們 Tryolabs 也主要使用 Python,但我們還是認(rèn)為 Swift 是一個(gè)絕佳的選擇;也因此,我們決定寫(xiě)這篇文章以幫助世人了解谷歌的計(jì)劃。
但在深入 Swift 以及「可微分編程」的真正含義之前,我們應(yīng)該先回顧一下當(dāng)前的狀況。
Python,你怎么了?!
到目前為止,Python 都依然是機(jī)器學(xué)習(xí)領(lǐng)域最常被使用的語(yǔ)言,谷歌也有大量用 Python 編寫(xiě)的機(jī)器學(xué)習(xí)軟件庫(kù)和工具。那么,為什么還要用 Swift?Python 有什么問(wèn)題嗎?
直接說(shuō)吧,Python 太慢了。另外,Python 的并行性表現(xiàn)并不好。
為了應(yīng)對(duì)這些缺點(diǎn),大多數(shù)機(jī)器學(xué)習(xí)項(xiàng)目在運(yùn)行計(jì)算密集型算法時(shí),都會(huì)使用用 C/C++/Fortran/CUDA 寫(xiě)的軟件庫(kù),然后再使用 Python 將不同的底層運(yùn)算組合到一起。對(duì)于大部分項(xiàng)目而言,這種做法其實(shí)效果很好;但總體概況而言,這會(huì)產(chǎn)生一些問(wèn)題。我們先看看其中一些問(wèn)題。
外部二進(jìn)制文件
為每個(gè)計(jì)算密集型運(yùn)算都調(diào)用外部二進(jìn)制文件會(huì)限制開(kāi)發(fā)者的工作,讓他們只能在算法的表層的一小部分上進(jìn)行開(kāi)發(fā)。比如,編寫(xiě)自定義的卷積執(zhí)行方式是無(wú)法實(shí)現(xiàn)的,除非開(kāi)發(fā)者愿意使用 C 等語(yǔ)言來(lái)進(jìn)行開(kāi)發(fā)。大部分程序員都不會(huì)選擇這么做,要么是因?yàn)樗麄儧](méi)有編寫(xiě)低層高性能代碼的經(jīng)驗(yàn),要么則是因?yàn)樵?Python 開(kāi)發(fā)環(huán)境與某個(gè)低層語(yǔ)言環(huán)境之間來(lái)回切換會(huì)變得過(guò)于麻煩。
這會(huì)造成一種不幸的情況:程序員會(huì)盡力盡量少地寫(xiě)復(fù)雜代碼,并且默認(rèn)情況更傾向于調(diào)用外部軟件庫(kù)的運(yùn)算。對(duì)于機(jī)器學(xué)習(xí)這樣動(dòng)態(tài)發(fā)展的領(lǐng)域來(lái)說(shuō),這并不是一個(gè)好現(xiàn)象,因?yàn)楹芏鄸|西都還并未確定下來(lái),還非常需要新想法。
對(duì)軟件庫(kù)的抽象理解
讓 Python 代碼調(diào)用更低層代碼并不如將 Python 函數(shù)映射成 C 函數(shù)那么簡(jiǎn)單。不幸的現(xiàn)實(shí)是:機(jī)器學(xué)習(xí)軟件庫(kù)的創(chuàng)建者必須為了性能而做出一些開(kāi)發(fā)上的選擇,而這又會(huì)讓事情變得更加復(fù)雜。舉個(gè)例子,在 TensorFlow 圖(graph)模式中(這是該軟件庫(kù)中唯一的性能模式),你的 Python 代碼在你認(rèn)為會(huì)運(yùn)行時(shí)常常并不運(yùn)行。在這里,Python 實(shí)際上的作用是底層 TensorFlow 圖的某種元編程(metaprogramming)語(yǔ)言。
其開(kāi)發(fā)流程為:開(kāi)發(fā)者首先使用 Python 定義一個(gè)網(wǎng)絡(luò),然后 TensorFlow 后端使用該定義來(lái)構(gòu)建網(wǎng)絡(luò)并將其編譯為一個(gè) blob,而開(kāi)發(fā)者卻再也無(wú)法訪問(wèn)其內(nèi)部。編譯之后,該網(wǎng)絡(luò)才終于可以運(yùn)行,開(kāi)發(fā)者可以開(kāi)始向其饋送數(shù)據(jù)以便訓(xùn)練和推理。這種工作方式讓調(diào)試工作變得非常困難,因?yàn)樵诰W(wǎng)絡(luò)運(yùn)行時(shí),你沒(méi)法使用 Python 了解其中究竟發(fā)生了什么。你也沒(méi)法使用 pdb 等方法。即使你想使用古老但好用的 print 調(diào)試方法,你也只能使用 tf.print 并在你的網(wǎng)絡(luò)中構(gòu)建一個(gè) print 節(jié)點(diǎn),這又必須連接到網(wǎng)絡(luò)中的另一個(gè)節(jié)點(diǎn),而且在 print 得到任何信息之前還必須進(jìn)行編譯。
不過(guò)也存在更加直接的解決方案。用 PyTorch 時(shí),你的代碼必須像用 Python 一樣命令式地運(yùn)行,唯一不透明的情況是運(yùn)行在 GPU 上的運(yùn)算是異步式地執(zhí)行的。這通常不會(huì)有問(wèn)題,因?yàn)?PyTorch 對(duì)此很智能,它會(huì)等到用戶交互操作所依賴(lài)的所有異步調(diào)用都結(jié)束之后才會(huì)轉(zhuǎn)讓控制權(quán)。盡管如此,也還是有一些問(wèn)題存在,尤其是在基準(zhǔn)評(píng)測(cè)(benchmarking)等任務(wù)上。
行業(yè)滯后
所有這些可用性問(wèn)題不僅讓寫(xiě)代碼更困難,而且還會(huì)導(dǎo)致產(chǎn)業(yè)界毫無(wú)必要地滯后于學(xué)術(shù)界。一直以來(lái)都有論文在研究如何調(diào)整神經(jīng)網(wǎng)絡(luò)中所用的低層運(yùn)算,并在這一過(guò)程中將準(zhǔn)確度提升幾個(gè)百分點(diǎn),但是產(chǎn)業(yè)界仍然需要很長(zhǎng)時(shí)間才能實(shí)際應(yīng)用這些進(jìn)展。
一個(gè)原因是即使這些算法上的改變可能本身比較簡(jiǎn)單,但上面提到的工具問(wèn)題還是讓它們非常難以實(shí)現(xiàn)。因此,由于這些改進(jìn)可能只能將準(zhǔn)確度提升 1%,所以企業(yè)可能會(huì)認(rèn)為為此進(jìn)行投入并不值得。對(duì)于小型機(jī)器學(xué)習(xí)開(kāi)發(fā)團(tuán)隊(duì)而言,這個(gè)問(wèn)題尤為明顯,因?yàn)樗麄兺狈韶?fù)擔(dān)實(shí)現(xiàn)/整合成本的規(guī)模經(jīng)濟(jì)。
因此,企業(yè)往往會(huì)直接忽略這些進(jìn)步,直到這些改進(jìn)被加入到 PyTorch 或 TensorFlow 等軟件庫(kù)中。這能節(jié)省企業(yè)的實(shí)現(xiàn)和整合成本,但也會(huì)導(dǎo)致產(chǎn)業(yè)界滯后學(xué)術(shù)界一兩年時(shí)間,因?yàn)檫@些軟件庫(kù)的維護(hù)者基本不會(huì)立即實(shí)現(xiàn)每篇新論文提出的新方法。
舉個(gè)具體的例子,可變形卷積似乎可以提升大多數(shù)卷積神經(jīng)網(wǎng)絡(luò)(CNN)的性能表現(xiàn),但論文發(fā)布大概 2 年之后才出現(xiàn)第一個(gè)開(kāi)源的實(shí)現(xiàn)。不僅如此,將可變形卷積的實(shí)現(xiàn)整合進(jìn) PyTorch 或 TensorFlow 的過(guò)程非常麻煩,而且最后這個(gè)算法也并沒(méi)得到廣泛的使用。PyTorch 直到最近才加入對(duì)它的支持,至于官方的 TensorFlow 版本,至今仍沒(méi)有見(jiàn)到。
現(xiàn)在,假設(shè)說(shuō)有 n 篇能將準(zhǔn)確度提升 2% 的論文都遇到了這種情況,那么產(chǎn)業(yè)界將錯(cuò)失準(zhǔn)確度顯著提升 (1.02^n)% 的機(jī)會(huì),而原因不過(guò)是沒(méi)有合適的工具罷了。如果 n 很大,那就太讓人遺憾了。
速度
在某些情況中,同時(shí)使用 Python 與快速軟件庫(kù)依然還是會(huì)很慢。確實(shí),如果是用 CNN 來(lái)執(zhí)行圖像分類(lèi),那么使用 Python 與 PyTorch/TensorFlow 會(huì)很快。此外,就算在 CUDA 環(huán)境中編寫(xiě)整個(gè)網(wǎng)絡(luò),性能也可能并不會(huì)得到太多提升,因?yàn)榇缶矸e占據(jù)了大部分的推理時(shí)間,而大卷積又已經(jīng)有了經(jīng)過(guò)良好優(yōu)化的代碼實(shí)現(xiàn)。但情況并非總是如此。
如果不是完全用低層語(yǔ)言實(shí)現(xiàn)的,那么由很多小運(yùn)算組成的網(wǎng)絡(luò)往往最容易出現(xiàn)性能問(wèn)題。舉個(gè)例子,F(xiàn)ast.AI 的 Jeremy Howard 曾在一篇博客文章中表達(dá)了自己對(duì)用 Swift 來(lái)做深度學(xué)習(xí)開(kāi)發(fā)的熱愛(ài),他表示盡管使用了 PyTorch 那出色的 JIT 編譯器,他仍然無(wú)法讓 RNN 的工作速度比肩完全用 CUDA 實(shí)現(xiàn)的版本。
此外,對(duì)于延遲程度很重要的情況,Python 也不是一種非常好的語(yǔ)言;而且 Python 也不能很好地應(yīng)用于與傳感器通信等非常底層的任務(wù)。為了解決這個(gè)問(wèn)題,一些公司的做法是僅用 Python 和 PyTorch/TensorFlow 開(kāi)發(fā)模型。這樣,在實(shí)驗(yàn)和訓(xùn)練新模型時(shí),他們就能利用 Python 的易用性?xún)?yōu)勢(shì)。而在之后的生產(chǎn)部署時(shí),他們會(huì)用 C++ 重寫(xiě)他們的模型。不確定他們是會(huì)完全重寫(xiě),還是會(huì)使用 PyTorch 的 tracing 功能或 TensorFlow 的圖模式來(lái)簡(jiǎn)單地將其串行化,然后再?lài)@它使用 C++ 來(lái)重寫(xiě) Python。不管是哪種方式,都需要重寫(xiě)大量 Python 代碼。對(duì)于小公司而言,這樣做往往成本過(guò)高。
所有這些問(wèn)題都是眾所周知的。公認(rèn)的深度學(xué)習(xí)教父之一 Yann LeCun 就曾說(shuō)機(jī)器學(xué)習(xí)需要一種新語(yǔ)言。他與 PyTorch 的創(chuàng)建者之一 Soumith Chintala 曾在一組推文中討論了幾種可能的候選語(yǔ)言,其中提到了 Julia、Swift 以及改進(jìn) Python。另一方面,F(xiàn)ast.AI 的 Jeremy Howard 似乎已經(jīng)下定決心站隊(duì) Swift。
谷歌接受了挑戰(zhàn)
幸運(yùn)的是,谷歌的 Swift for TensorFlow(S4TF)團(tuán)隊(duì)接過(guò)了這一難題。不僅如此,他們的整個(gè)項(xiàng)目進(jìn)展還非常透明。他們還發(fā)布了一份非常詳實(shí)的文檔(https://github.com/tensorflow/swift/blob/master/docs/WhySwiftForTensorFlow.md),其中詳細(xì)地介紹了他們做出這一決定的歷程,并解釋了他們?yōu)檫@一任務(wù)考慮過(guò)的其它語(yǔ)言并最終選中 Swift 的原因。
在他們考慮過(guò)的語(yǔ)言中,最值得關(guān)注的包括:
Go:在這份文檔中,他們表示 Go 過(guò)于依賴(lài)其接口提供的動(dòng)態(tài)調(diào)度,而且如果要實(shí)現(xiàn)他們想要的特性,必須對(duì)這門(mén)語(yǔ)言進(jìn)行大刀闊斧的修改。這與 Go 語(yǔ)言的保持簡(jiǎn)單和小表面積的哲學(xué)不符。相反,Swift 的協(xié)議和擴(kuò)展都有很高的自由度:你想要調(diào)度有多靜態(tài),就能有多靜態(tài)。另外,Swift 也相當(dāng)復(fù)雜,而且還在越來(lái)越復(fù)雜,所以再讓它復(fù)雜點(diǎn)以滿足谷歌想要的特性并不是什么大問(wèn)題。
C++ 和 Rust:谷歌的目標(biāo)用戶群是那些大部分工作都使用 Python 的人,他們更感興趣的是花時(shí)間思考模型和數(shù)據(jù),而不是思考如何精細(xì)地管理內(nèi)存或所有權(quán)(ownership)。Rust 和 C++ 的復(fù)雜度都足夠,但都很注重底層細(xì)節(jié),而這在數(shù)據(jù)科學(xué)和機(jī)器學(xué)習(xí)開(kāi)發(fā)中通常是不合理的。
Julia:如果你在 HackerNews 或 Reddit 上讀到過(guò)任何有關(guān) S4TF 的帖子,那么最??吹降脑u(píng)論是:「為啥不選 Julia?」在前面提到的那份文檔中,谷歌提到 Julia 看起來(lái)也很有潛力,但他們并未給出不選 Julia 的靠譜理由。他們提到 Swift 的社區(qū)比 Julia 大得多,事實(shí)確實(shí)如此,然而 Julia 的科研社區(qū)和數(shù)據(jù)科學(xué)社區(qū)卻比 Swift 大得多,而這些社區(qū)的人才更可能更多地使用 S4TF。要記住,谷歌團(tuán)隊(duì)的 Swift 專(zhuān)業(yè)人才更多,畢竟發(fā)起 S4TF 項(xiàng)目的正是 Swift 的創(chuàng)建者 Chris Lattner,相信這在谷歌的決定中起到了重大的作用。
一種新語(yǔ)言:作者認(rèn)為他們?cè)谛灾姓f(shuō)得很好:「創(chuàng)建一種語(yǔ)言的工作量多得嚇人。」這需要太長(zhǎng)的時(shí)間,而機(jī)器學(xué)習(xí)又發(fā)展得太快。
那么,Swift 的優(yōu)勢(shì)在哪里?
簡(jiǎn)單來(lái)說(shuō),Swift 讓你可幾乎完全用 Python 的方式在非常高的層面上進(jìn)行編程,同時(shí)又可以保證非??斓乃俣?。數(shù)據(jù)科學(xué)家可像使用 Python 一樣來(lái)使用 Swift,同時(shí)可用 Swift 內(nèi)置的已優(yōu)化機(jī)器學(xué)習(xí)庫(kù)來(lái)進(jìn)行更加精細(xì)的開(kāi)發(fā),比如管理內(nèi)存,甚至當(dāng)常用的 Swift 代碼約束太大時(shí)還能降至指針層面進(jìn)行操作。
本文的目的不是介紹 Swift 語(yǔ)言,所以不會(huì)連篇累牘地詳細(xì)介紹其特性。如果你想詳細(xì)了解這門(mén)語(yǔ)言,看官方文檔就夠了。這里只會(huì)介紹 Swift 的幾個(gè)亮點(diǎn),并希望這能吸引人們?nèi)L試它。下面幾節(jié)將按隨機(jī)順序介紹 Swift 的一些亮點(diǎn),所以排序與它們的重要程度無(wú)關(guān)。之后,本文將深入介紹可微分編程,并聊聊谷歌在 Swift 上的大計(jì)劃。
亮點(diǎn)一
Swift 速度很快。這是作者在開(kāi)始使用 Swift 時(shí)所做的第一項(xiàng)測(cè)試。作者寫(xiě)了一些短腳本來(lái)評(píng)估 Swift 與 Python 和 C 的相對(duì)表現(xiàn)。說(shuō)實(shí)話,這些測(cè)試并不特別復(fù)雜。也就是用整型數(shù)填充一個(gè)數(shù)組,然后再將它們?nèi)考悠饋?lái)。這個(gè)測(cè)試本身并不能透徹地了解 Swift 在各種情況下的速度表現(xiàn),但作者想了解的是 Swift 能否達(dá)到 C 一樣的速度,而不是 Swift 是否總能和 C 一樣快。
第一組比較作者選的是 Swift vs Python。為了讓對(duì)應(yīng)的每一行所執(zhí)行的任務(wù)一致,作者對(duì)某些地方的花括號(hào)的位置進(jìn)行了調(diào)整。
- import time | import Foundation|
- result = [] | var result = [Int]()for it in range(15): | for it in 0..<15 {
- start = time.time() | let start = CFAbsoluteTimeGetCurrent()
- for _ in range(3000): | for _ in 0..<3000 {
- 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 = []}
盡管在這個(gè)特定的代碼段中,Python 與 Swift 代碼看起來(lái)句法相近,但運(yùn)行結(jié)果表明這個(gè) Swift 腳本的運(yùn)行速度比 Python 腳本的運(yùn)行速度快 25 倍。在這個(gè) Python 腳本中,最外層的循環(huán)每執(zhí)行一次平均耗時(shí) 360 μs,相比之下 Swift 的是 14 μs。差別非常明顯。
另外,也還有其它一些事情值得注意。比如,+ 既是一個(gè)運(yùn)算符也是一個(gè)函數(shù),它會(huì)被傳遞給 reduce(后面我會(huì)詳細(xì)介紹);CFAbsoluteTimeGetCurrent 揭示了 Swift 在傳承下來(lái)的 iOS 命名空間方面的怪異特性;.< 范圍運(yùn)算符讓你可以選擇該范圍是否包含區(qū)間端點(diǎn)以及哪個(gè)端點(diǎn)。
但是,這個(gè)測(cè)試并不能說(shuō)明 Swift 有多快。要知道 Swift 有多快,我們得將其與 C 來(lái)比比看。我也這樣做了,但讓人失望的是,初始結(jié)果并不好。用 C 編寫(xiě)的版本平均耗時(shí) 1.5 μs,比我們的 Swift 代碼快 10 倍。Uh oh.
不過(guò)老實(shí)講,這樣比較其實(shí)并不公平。這段 Swift 代碼并沒(méi)使用動(dòng)態(tài)數(shù)組,因此當(dāng)數(shù)組規(guī)模變大時(shí),它會(huì)在內(nèi)存堆中不斷重新分配位置。這也意味著它會(huì)在每個(gè)附加(append)的數(shù)組上執(zhí)行邊界檢查。為了佐證這一點(diǎn),我們來(lái)看看相關(guān)定義。Swift 的標(biāo)準(zhǔn)類(lèi)型包括整型、浮點(diǎn)數(shù)和數(shù)組,它們并沒(méi)有硬編碼到編譯器中,而是標(biāo)準(zhǔn)庫(kù)中所定義的結(jié)構(gòu)體(struct)。因此,根據(jù)數(shù)組的附加(append)定義,我們可以了解到很多信息。知道了這一點(diǎn)后,我的測(cè)試方式甚至可以包括預(yù)分配數(shù)組的內(nèi)存以及使用指針來(lái)填充數(shù)組。這樣得到的腳本其實(shí)也并不是很長(zhǎng):
- import Foundation// Preallocating memoryvar result = ContiguousArray<Int>(repeating: 0, count: 3001)for it in 0..<15 {
- let start = CFAbsoluteTimeGetCurrent()
- // Using a buffer pointer for assignment
- result.withUnsafeMutableBufferPointer({ buffer infor i in 0..<3000 {
- buffer[i] = it
- }
- })
- let sum = result.reduce(0, +)
- let end = CFAbsoluteTimeGetCurrent()
- print(end - start, sum)
這段新代碼耗時(shí) 3 μs,速度已經(jīng)達(dá)到 C 的一半,可以說(shuō)是很不錯(cuò)的結(jié)果了。不過(guò)為了進(jìn)行完整的比較,作者繼續(xù)對(duì)代碼進(jìn)行了剖析,以便了解該代碼的 Swift 版本和 C 版本的差異究竟可以做到多小。事實(shí)證明,作者之前使用的 reduce 方法會(huì)毫無(wú)必要地間接使用 nextPartialResult 函數(shù)執(zhí)行一些計(jì)算,這可以提供非必需的泛化能力。在使用指針重寫(xiě)了這段代碼之后,作者最終讓這段代碼達(dá)到了與 C 同等的速度。但是,這顯然不符合我們使用 Swift 的目的,因?yàn)檫@種操作本質(zhì)上就是寫(xiě)更冗長(zhǎng)更丑陋的 C 語(yǔ)言。盡管如此,知道在確實(shí)需要時(shí)可以達(dá)到 C 的速度也是一件好事。
總結(jié):使用 Swift,你沒(méi)法在執(zhí)行 Python 層面的工作時(shí)獲得 C 語(yǔ)言等級(jí)的速度,但你能在兩者之間取得良好的平衡。
亮點(diǎn)二
Swift 采用的函數(shù)簽名方法也很有趣。它們的最基本形式其實(shí)相當(dāng)簡(jiǎn)單:
- func greet(person: String, town: String) -> String {
- return "Hello \(person)! Glad you could visit from \(town)."
- }
- greet(person: "Bill", town: "Cupertino")
其函數(shù)簽名由參數(shù)名加它們的類(lèi)型構(gòu)成,沒(méi)其它多余花哨的東西。唯一不同尋常的是 Swift 需要你在調(diào)用該函數(shù)時(shí)提供參數(shù)名,因此你在調(diào)用上面的 greet 時(shí)必須寫(xiě)下 person 和 town,如上面代碼段中最后一行所示。
當(dāng)我們向其中引入?yún)?shù)標(biāo)簽時(shí),情況還會(huì)變得更加有趣。
- func greet(_ person: String, from town: String) -> String {
- return "Hello \(person)! Glad you could visit from \(town)."
- }
- greet("Bill", from: "Cupertino")
顧名思義,參數(shù)標(biāo)簽就是函數(shù)的參數(shù)的標(biāo)簽,而且它們是在函數(shù)簽名中各自的參數(shù)之前聲明的。在上面的示例中,from 是 town 的參數(shù)標(biāo)簽,_ 是 person 的參數(shù)標(biāo)簽。對(duì)于最后一個(gè)標(biāo)簽,作者使用的是,因?yàn)?_ 在 Swift 中是一個(gè)特殊字母,其含義是:「在調(diào)用這個(gè)參數(shù)時(shí)不提供任何參數(shù)名。」
有了參數(shù)標(biāo)簽,每個(gè)參數(shù)都有兩個(gè)不同的名字:一個(gè)是參數(shù)標(biāo)簽,在調(diào)用該函數(shù)時(shí)使用;另一個(gè)是參數(shù)名,在函數(shù)的主體定義中使用。這看起來(lái)似乎有些任性,但會(huì)讓你的代碼更易讀。
看看上面的函數(shù)簽名,基本就像是在讀英語(yǔ)?!窯reet person from town.」上面的函數(shù)調(diào)用看起來(lái)也同樣清楚直白:「Greet Bill from Cupertino.」如果沒(méi)有參數(shù)標(biāo)簽,就有些含混不清了:「Greet person town.」我們不知道這里的 town 是什么意思。這是我們現(xiàn)在所處的城鎮(zhèn)嗎?還是我們?yōu)榱嗣嬉?jiàn)這個(gè)人而將要前去的城鎮(zhèn)?又或是這個(gè)人原本來(lái)處的城鎮(zhèn)?如果沒(méi)有參數(shù)標(biāo)簽,我們就必須閱讀函數(shù)主體才能知曉實(shí)際情況,或者采用讓函數(shù)名或參數(shù)名更長(zhǎng)更直白的方法。如果你有大量參數(shù),那么情況將變得非常復(fù)雜;在作者看來(lái)這會(huì)導(dǎo)致代碼變得更丑而且會(huì)讓函數(shù)名變得毫無(wú)必要地長(zhǎng)。參數(shù)標(biāo)簽更加好看,而且也更容易擴(kuò)展,而且幸運(yùn)的是它們也在 Swift 中得到了廣泛的應(yīng)用。
亮點(diǎn)三
Swift 廣泛地使用了閉包(closure)。因此,有一些捷徑可讓該語(yǔ)言的使用更接近人的直覺(jué)。這個(gè)來(lái)自 Swift 的文檔的示例展現(xiàn)了這些捷徑簡(jiǎn)潔明了又具有很強(qiáng)的表現(xiàn)力的特性。
我們的目標(biāo)是將下面的數(shù)組向后排序:
- let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
如果用不那么地道的 Swift 代碼形式,可為數(shù)組使用 sorted 方法,并采用一個(gè)自定義函數(shù)來(lái)定義按逐對(duì)順序比較數(shù)組元素的方式,就像這樣:
- func backward(_ s1: String, _ s2: String) -> Bool {
- return s1 > s2
- }var reversedNames = names.sorted(by: backward)
backward 函數(shù)一次可比較兩項(xiàng),如果這兩項(xiàng)的順序與所需順序一樣,則返回 true;否則便返回 false。sorted 數(shù)組方法需要這樣一個(gè)函數(shù)作為一個(gè)輸入才能知道如何對(duì)數(shù)組進(jìn)行排序。順便一提,我們還可以看到這里使用了參數(shù)標(biāo)簽 by——這是如此的簡(jiǎn)潔明了。
如果我們采用更地道的 Swift,可以發(fā)現(xiàn)使用閉包能更好地完成這項(xiàng)任務(wù)。
- reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
{} 之間的代碼是一個(gè)正被定義的閉包,同時(shí)也被傳遞用作 sorted 的一個(gè)參數(shù)。你也許從未聽(tīng)說(shuō)過(guò)閉包,但其實(shí)很簡(jiǎn)單,閉包就是一個(gè)獲取上下文的未命名的函數(shù)你可以將其看作是增強(qiáng)版的 Python lambda。該閉包中的關(guān)鍵詞 in 的作用是分開(kāi)該閉包的參數(shù)及其主體。: 等更直觀的關(guān)鍵詞已被簽名類(lèi)型定義所占用(在這個(gè)案例中,該閉包的參數(shù)類(lèi)型是從 sorted 的簽名中自動(dòng)推導(dǎo)出來(lái)的,因此可以避免使用 :),而且我們都知道命名是編程中最艱難的事情之一,所以為此只能繼續(xù)使用不那么直觀的關(guān)鍵詞了。
不管從哪個(gè)角度看,這段代碼都已經(jīng)簡(jiǎn)潔了許多。
但我們還可能做得更好:
- reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
這里我們移除了 return 語(yǔ)句,這是因?yàn)樵?Swift 中,單行閉包就暗含了 return。
即便如此,我們還能繼續(xù)更進(jìn)一步:
- reversedNames = names.sorted(by: { $0 > $1 } )
Swift 也有暗含的命名位置參數(shù),所以在上面的案例中,$0 是第一個(gè)參數(shù),$1 是第二個(gè)參數(shù),$2 是第三個(gè)參數(shù)等等。這個(gè)代碼已經(jīng)很緊湊了,而且非常容易理解,但是我們甚至還能做得更好:
- reversedNames = names.sorted(by: >)
在 Swift 中,> 運(yùn)算符就是一個(gè)名為 > 的函數(shù)。因此,我們可以將其傳遞給 sorted 方法,使我們的代碼達(dá)到極端簡(jiǎn)潔和可讀的程度。
這種操作適用于 +=、-=、<、>、== 和 = 等運(yùn)算符,你可以在標(biāo)準(zhǔn)庫(kù)中查看它們的定義。這些函數(shù)/運(yùn)算符與普通函數(shù)之間的差異是前者已在標(biāo)準(zhǔn)庫(kù)中使用 infix、prefix 或 suffix 關(guān)鍵詞顯式地聲明為運(yùn)算符。舉個(gè)例子,+= 函數(shù)在 Swift 標(biāo)準(zhǔn)庫(kù)的這一行(https://github.com/apple/swift/blob/1ed846d8525679d2811418a5ba29405200f6e85a/stdlib/public/core/Policy.swift#L468)中被定義成了一個(gè)運(yùn)算符??梢钥吹?,這個(gè)運(yùn)算符遵循多個(gè)不同的協(xié)議,比如 Array 和 String,因?yàn)楹芏嗖煌念?lèi)型都有自己的 += 函數(shù)實(shí)現(xiàn)。
更進(jìn)一步,我們還能定義自己的自定義運(yùn)算符。GPUImage2 軟件庫(kù)就是一個(gè)很好的例子。這個(gè)軟件庫(kù)讓用戶可以加載圖像,使用一系列變換來(lái)修改它,然后再以某種方式來(lái)輸出它。很自然,這些變換序列的定義會(huì)在該庫(kù)中不斷反復(fù)出現(xiàn),因此這個(gè)庫(kù)的創(chuàng)建者決定定義一個(gè)新的運(yùn)算符 →,可用于將這些變換鏈接到一起。
- func -->(source:T, destination:T) -> T {
- source.addTarget(destination)
- return destination
- }
- infix operator --> : AdditionPrecedence
在以上稍微簡(jiǎn)化過(guò)的代碼中,首先聲明了 --> 函數(shù),然后其被定義為了一個(gè) infix 運(yùn)算符。infix 的意思是如果要使用這個(gè)運(yùn)算符,就必須將其放置在兩個(gè)參數(shù)之間。這讓你可以寫(xiě)出如下的代碼:
- 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
- }
比起一大堆互相鏈接的方法或一長(zhǎng)串 source.addTarget(...) 函數(shù),上面的代碼要簡(jiǎn)短和容易多了。
亮點(diǎn)四
前面作者已經(jīng)提到過(guò),Swift 的基本類(lèi)型是標(biāo)準(zhǔn)庫(kù)中定義的結(jié)構(gòu)體,而且并沒(méi)有硬編碼到編譯器中,因?yàn)樗鼈兺ǔJ怯闷渌Z(yǔ)言寫(xiě)的。這很有用處,一大原因是讓我們可以使用名叫擴(kuò)展(extension)的 Swift 特性,其讓我們可以向任意類(lèi)型添加新特性,包括基本類(lèi)型。操作方式是這樣的:
- extension Double {
- var radians: Double {
- return self * (Double.pi / 180)
- }
- }
- 360.radians // -> 6.28319
盡管這個(gè)例子并不是很有用,但也展示 Swift 這門(mén)語(yǔ)言的擴(kuò)展能力,因?yàn)檫@能讓你做很多事情,比如向 Swift 解釋器輸入任何數(shù)字以及在其上調(diào)用任何你想用的自定義方法。
最后一個(gè)亮點(diǎn)
除了擁有編譯器之外,Swift 還具有解釋器并且支持 Jupyter Notebook。在學(xué)習(xí)這門(mén)語(yǔ)言時(shí),解釋器尤其好用,因?yàn)樗С种苯釉诿钐崾痉庉斎?swift,然后立馬開(kāi)始代碼測(cè)試。Python 也具備差不多一樣的功能。另一方面,由于整合了 Jupyter Notebook,因此可以輕松進(jìn)行可視化、執(zhí)行數(shù)據(jù)探索和編寫(xiě)報(bào)告。最后,當(dāng)你需要運(yùn)行生產(chǎn)代碼時(shí),你可以編譯它并利用 LLVM 提供的出色優(yōu)化能力。
谷歌的大計(jì)劃
作者在前面的章節(jié)中提到了 Swift 的一些特性,但其中有一個(gè)特性與其它不同:Jupyter Notebook 是新加入的,而且事實(shí)上正是由 S4TF 團(tuán)隊(duì)加入的。這非常值得一說(shuō),因?yàn)檫@能讓我們一窺谷歌投入這個(gè)項(xiàng)目時(shí)的想法:他們不僅想為 Swift 語(yǔ)言本身創(chuàng)建一個(gè)軟件庫(kù),而且他們還想深入地改進(jìn)這門(mén)語(yǔ)言本身以及相關(guān)工具,然后再使用這門(mén)語(yǔ)言的改進(jìn)版本創(chuàng)建一個(gè)新的 TensorFlow 軟件庫(kù)。
只要看看 S4TF 團(tuán)隊(duì)在哪些工作上投入的時(shí)間最多就能看出這一點(diǎn)。他們到目前為止做的大部分工作都是在蘋(píng)果公司的 Swift 編譯器代碼庫(kù)本身上完成的。更具體而言,谷歌目前完成的大部分工作都在 Swift 編譯器代碼庫(kù)中的一個(gè) dev 分支中。谷歌正為 Swift 語(yǔ)言本身添加新特性——他們首先會(huì)在自己的分支中創(chuàng)建和測(cè)試這些新特性,然后會(huì)將它們合并到蘋(píng)果的主分支中。這意味著運(yùn)行在世界各地的 iOS 設(shè)備上的標(biāo)準(zhǔn) Swift 語(yǔ)言最終將能集成這些改進(jìn)。
現(xiàn)在來(lái)談?wù)劯鼘?shí)在的東西:谷歌正為 Swift 構(gòu)建什么特性?
首先說(shuō)個(gè)大特性。
可微分編程
近來(lái),可微分編程炒得確實(shí)很熱。特斯拉的人工智能負(fù)責(zé)人 Andrej Karpathy 稱(chēng)之為軟件 2.0(Software 2.0),Yann LeCun 甚至宣稱(chēng):「深度學(xué)習(xí)已死,可微分編程萬(wàn)歲?!沽硪恍┤藙t說(shuō)有必要?jiǎng)?chuàng)建一套全新的工具了,包括新的 Git、新的 IDE 以及新的編程語(yǔ)言。Wink wink.
所以,什么是可微分編程?
簡(jiǎn)而言之,可微分編程是一種程序自身可被微分的編程范式。這讓你可以設(shè)定一個(gè)你想要優(yōu)化的具體目標(biāo),讓你的程序可以根據(jù)這個(gè)目標(biāo)自動(dòng)計(jì)算自己的梯度,然后再在這個(gè)梯度的方向上優(yōu)化自己。這和訓(xùn)練神經(jīng)網(wǎng)絡(luò)完全一樣。
如果能讓程序自己優(yōu)化自己,我們也許就能創(chuàng)造出我們自己完全無(wú)法編寫(xiě)出來(lái)的程序。想想這一點(diǎn)還挺有趣:你的程序可以使用梯度針對(duì)特定任務(wù)優(yōu)化自身,因此它的編程能力比你還強(qiáng)。過(guò)去幾年的發(fā)展已經(jīng)表明在越來(lái)越多的案例已經(jīng)出現(xiàn)了這種情況,而且目前我們還看不到這一發(fā)展趨勢(shì)的終點(diǎn)。
一種可微分的語(yǔ)言
寫(xiě)了這么長(zhǎng)的介紹之后,終于可以談?wù)劰雀铻?Swift 開(kāi)發(fā)的原生可微分編程版本了。
- func cube(_ x: Float) -> Float {
- return x * x * x
- }
- let cube𝛁 = gradient(of: cube)
- cube(2) // 8.0
- cube𝛁(2) // 12.0
這里我們首先定義了一個(gè)簡(jiǎn)單的函數(shù) cube,其返回的結(jié)果是輸入的立方。接下來(lái)就是激動(dòng)人心的部分了:我們只需在原始函數(shù)上調(diào)用 gradient,就能創(chuàng)建原始函數(shù)的導(dǎo)數(shù)函數(shù)。這里沒(méi)有使用任何軟件庫(kù)或外部代碼,gradient 只是由 S4TF 團(tuán)隊(duì)為 Swift 語(yǔ)言引入的一個(gè)新函數(shù)。該函數(shù)利用了 S4TF 團(tuán)隊(duì)對(duì) Swift 內(nèi)核進(jìn)行的修改,可以實(shí)現(xiàn)梯度函數(shù)的自動(dòng)計(jì)算。
這是 Swift 的一個(gè)重大新特性。對(duì)于任意 Swift 代碼,只要是可微分的,都可以自動(dòng)計(jì)算梯度。上面的代碼沒(méi)有導(dǎo)入任何東西或奇怪的依賴(lài)包,就只是純粹的 Swift。PyTorch、TensorFlow 或其它任何大型機(jī)器學(xué)習(xí)庫(kù)都支持這一功能,但前提是你要使用特定于庫(kù)的特定運(yùn)算。而且在這些 Python 庫(kù)中操作梯度并不如單純用 Swift 那樣輕量、透明,而且那些庫(kù)集成也不如 Swift 原生集成那么好。
這是 Swift 語(yǔ)言的一個(gè)重大新特性;而且可以說(shuō) Swift 是首個(gè)為這一特性提供原生支持的主流語(yǔ)言
為了進(jìn)一步說(shuō)明這在實(shí)際應(yīng)用中的使用方式,以下應(yīng)用于一個(gè)標(biāo)準(zhǔn)機(jī)器學(xué)習(xí)訓(xùn)練流程的腳本更完整透徹展示了這一新特性:
- 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 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 寫(xiě)的,不帶任何依賴(lài)包。在這段代碼中,我們可以看到谷歌為 Swift 引入的兩個(gè)新特性:callAsFunction 和 valueWithGradient。第一個(gè)很簡(jiǎn)單,其作用是實(shí)例化類(lèi)和結(jié)構(gòu)體,讓我們可以像調(diào)用函數(shù)一樣調(diào)用它們。這里,Perceptron 結(jié)構(gòu)體被實(shí)例化為了 model,然后 model 又在 let ŷ = model(x) 中被作為一個(gè)函數(shù)而調(diào)用。在這樣操作時(shí),實(shí)際上調(diào)用的是 callAsFunction 方法。如果你曾經(jīng)用過(guò) Keras 或 PyTorch 模型,你一定知道這是一種處理模型/層的常用方式。但 Keras 和 PyTorch 這兩個(gè)庫(kù)使用了 Python 的 *call* 方法來(lái)實(shí)現(xiàn)它們各自的 call 和 forward;Swift 之前沒(méi)有這樣的特性,于是谷歌把它加了進(jìn)去。
上面的腳本中還有一個(gè)有趣的新特性:valueWithGradient。該函數(shù)會(huì)返回在特定點(diǎn)評(píng)估的函數(shù)或閉包的結(jié)果值和梯度。在以上案例中,我們定義并用作 valueWithGradient 的輸入的閉包實(shí)際上是我們的損失函數(shù)。這個(gè)損失函數(shù)的輸入是我們的模型,所以當(dāng)我們說(shuō) valueWithGradient 會(huì)在特定的點(diǎn)評(píng)估我們的函數(shù)時(shí),我們的意思是其會(huì)使用有特定權(quán)重配置的模型評(píng)估我們的損失函數(shù)。計(jì)算了上述的值和梯度之后,我們可以把值打印出來(lái)(這是我們的損失)并使用梯度更新模型的權(quán)重。重復(fù)這一過(guò)程一百次,我們就訓(xùn)練了一個(gè)模型。我們還可以訪問(wèn)損失函數(shù)內(nèi)部的 andGateData,這是 Swift 閉包可以獲取其周?chē)舷挛牡挠忠话咐?/p>
微分外部代碼
Swift 還有一個(gè)神奇的特性:我們不僅可以微分 Swift 運(yùn)算,還能微分外部的、非 Swift 的軟件庫(kù)——只需我們?cè)?Swift 中手動(dòng)定義這些運(yùn)算操作的導(dǎo)數(shù)。這意味著你可以使用 C 軟件庫(kù)中一些非??焖俚膶?shí)現(xiàn)或一些 Swift 還不具備的運(yùn)算操作。你只需將其導(dǎo)入到你的項(xiàng)目中、編寫(xiě)導(dǎo)數(shù)代碼,然后就可以在你的大型神經(jīng)網(wǎng)絡(luò)中使用這些運(yùn)算操作,讓反向傳播等功能無(wú)縫運(yùn)行。
此外,這件事做起來(lái)其實(shí)非常簡(jiǎn)單:
- 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), // 8
- gradient(of: powerOf2)(3) // 5.545
Glibc 是一個(gè) C 軟件庫(kù),因此 Swift 編譯器并不知道其運(yùn)算操作的導(dǎo)數(shù)是什么。通過(guò)使用 @derivative,我們可以為編譯器提供有關(guān)這些外部運(yùn)算操作的導(dǎo)數(shù)的信息,然后搭配 Swift 的原生運(yùn)算,可以非常輕松地構(gòu)建出大型的可微分網(wǎng)絡(luò)。在這個(gè)示例中,我們導(dǎo)入了 Glibc 的 pow 和 log,并用它們創(chuàng)建了 powerOf2 函數(shù)及其導(dǎo)數(shù)。
為 Swift 開(kāi)發(fā)的新 TensorFlow 軟件庫(kù)的當(dāng)前版本就正在使用這一特性進(jìn)行開(kāi)發(fā)。這個(gè)庫(kù)從 TF Eager 軟件庫(kù)的 C API 導(dǎo)入了其所有運(yùn)算操作,但其不是將 TensorFlow 的自動(dòng)微分系統(tǒng)直接接上去,而是要指定每個(gè)基礎(chǔ)運(yùn)算操作的導(dǎo)數(shù),然后再讓 Swift 處理。但是,并非所有運(yùn)算都需要這種操作,因?yàn)樵S多運(yùn)算都是更基本運(yùn)算組合而成的,因此 Swift 可以自動(dòng)推斷它們的導(dǎo)數(shù)。但由于這個(gè)庫(kù)的當(dāng)前版本基于 TF Eager,因此存在一個(gè)大缺點(diǎn):TF Eager 非常慢,因此 Swift 的這個(gè)版本也很慢。這個(gè)問(wèn)題應(yīng)該只是暫時(shí)性的,隨著與 XLA(通過(guò) x10)和 MLIR 的整合,這個(gè)問(wèn)題可以得到解決。
話雖如此,實(shí)際上 Swift TensorFlow API 已經(jīng)初具規(guī)模,谷歌的開(kāi)發(fā)者已經(jīng)可以使用這個(gè) API 進(jìn)行開(kāi)發(fā)了。使用它,你可以這樣訓(xùn)練一個(gè)簡(jiǎn)單模型:
- import TensorFlowlet hiddenSize: Int = 10struct 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> inlet logits = model(firstTrainFeatures)
- return softmaxCrossEntropy(logits: logits, labels: firstTrainLabels)
- }
- print("Current loss: \(loss)")
可以看到,這與之前的無(wú)導(dǎo)入的模型訓(xùn)練腳本非常相似。它的設(shè)計(jì)非常類(lèi)似 PyTorch,真是太棒了。
與 Python 的互操作性
Swift 目前仍面臨的一大問(wèn)題是當(dāng)前的機(jī)器學(xué)習(xí)和數(shù)據(jù)科學(xué)生態(tài)系統(tǒng)仍處于起步階段。幸運(yùn)的是,谷歌正在解決這個(gè)問(wèn)題,其方式是為 Swift 納入 Python 互操作性。其想法是讓開(kāi)發(fā)者可在 Swift 代碼中編寫(xiě) Python 代碼;通過(guò)這種方式,數(shù)量龐大的 Python 軟件庫(kù)就能為 Swift 所用了。
這種操作的一種典型用例是用 Swift 訓(xùn)練模型,然后用 Python 的 matplotlib 來(lái)繪制圖表:
- 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()
這看起來(lái)就像是單純的 Python 代碼加了一點(diǎn) let 和 var 語(yǔ)句。這是由谷歌提供的一段代碼示例。作者只做了一項(xiàng)修改,即注釋掉了一行 Python 代碼,并用 Swift 對(duì)其進(jìn)行了重寫(xiě)??梢钥吹剑@兩者在這里竟然可以交互得如此之好。這項(xiàng)任務(wù)完成起來(lái)并不如完全使用 Python 那樣清晰簡(jiǎn)潔,因?yàn)槲覀儽仨毷褂?makeNumpyArray() 和 Array();但這種操作是可行的。
谷歌成功實(shí)現(xiàn) Python 互操作性的方法是引入了 PythonObject 類(lèi)型,其可表示 Python 中的任何對(duì)象。Python 互操作性被限定在單個(gè) Swift 軟件庫(kù)中,因此 S4TF 團(tuán)隊(duì)僅需為 Swift 語(yǔ)言本身添加少量功能,比如添加少量改進(jìn)以適應(yīng) Python 的極端動(dòng)態(tài)性。至于現(xiàn)在的 Python 支持已經(jīng)達(dá)到了何種程度,目前尚不清楚他們將如何處理 with 語(yǔ)句等更地道的 Python 元素,而且可以肯定地說(shuō)還有其它一些極端情況有待考慮;盡管如此,現(xiàn)在已經(jīng)實(shí)現(xiàn)的成果就已經(jīng)很不錯(cuò)了。
而在 Swift 與其它語(yǔ)言的整合方面,作者對(duì) Swift 的最早的興趣點(diǎn)之一就是想看看它在處理實(shí)時(shí)計(jì)算機(jī)視覺(jué)任務(wù)上的表現(xiàn)。因?yàn)檫@個(gè)原因,作者最終找到了 OpenCV 的一個(gè) Swift 版本,而通過(guò) FastAI 的論壇,最終找到了一個(gè)大有潛力的 OpenCV 封裝類(lèi)(wrapper):SwiftCV。但是,這個(gè)庫(kù)很奇怪。OpenCV 是用 C++ 構(gòu)建的(并且剛剛廢棄了其 C API),而 Swift 目前并不支持 C++(不過(guò)將會(huì)支持)。因此,SwiftCV 必須將 OpenCV 代碼封裝在 C++ 代碼的一個(gè)兼容 C 的子集中,然后再以 C 軟件包的形式導(dǎo)入。之后,才能將其封裝到 Swift 中。
S4TF 項(xiàng)目的當(dāng)前狀態(tài)
盡管作者對(duì) S4TF 項(xiàng)目一直不吝贊美之辭,但也必須承認(rèn)其還不足以支持一般的生產(chǎn)使用。其新的 API 仍在不斷變化,這個(gè)新的 TensorFlow 庫(kù)的性能也仍然不是很好;即便其數(shù)據(jù)科學(xué)生態(tài)系統(tǒng)正在發(fā)展壯大,但總體仍處于起步階段。最重要的是,其 Linux 支持情況很奇怪,目前官方僅支持 Ubuntu??紤]到所有這些問(wèn)題,要保證所有這些問(wèn)題及時(shí)得到解決,還有很多工作要做。
谷歌正在努力提升其性能,包括最近添加的 x10 以及在讓 MLIR 達(dá)到標(biāo)準(zhǔn)方面所做的工作。另外,谷歌還有一些項(xiàng)目致力于在 Swift 中復(fù)制許多 Python 數(shù)據(jù)科學(xué)生態(tài)系統(tǒng)的功能,比如 SwiftPlot、類(lèi)似 Pandas 的 Penguin、類(lèi)似 Scikit-learn 的 swiftML。
但最讓人驚訝的是蘋(píng)果公司也與谷歌在同一方向上推動(dòng) Swift 的發(fā)展。在蘋(píng)果的 Swift 發(fā)展路線圖上,下一個(gè)重大版本的主要目標(biāo)是在非蘋(píng)果平臺(tái)上建立不斷發(fā)展增長(zhǎng)的 Swift 軟件生態(tài)系統(tǒng)。這一目標(biāo)也反映在了蘋(píng)果對(duì)多個(gè)項(xiàng)目的支持上,比如 Swift Server Work Group、類(lèi)似 numpy 的 Numerics、一個(gè)運(yùn)行在 Linux 上的官方語(yǔ)言服務(wù)器以及將 Swift 移植到 Windows 系統(tǒng)的工作。
此外,F(xiàn)ast.ai 的 Sylvain Gugger 也正為 FastAI 構(gòu)建一個(gè) Swift 版本,而 Jeremy Howard 也已經(jīng)將 Swift 課程納入到了他們的廣受歡迎的在線課程中。另外,第一批基于 S4TF 相關(guān)軟件庫(kù)的學(xué)術(shù)論文也正陸陸續(xù)續(xù)發(fā)表出來(lái)。
總結(jié)
在作者本人看來(lái),盡管 Swift 很有可能發(fā)展成機(jī)器學(xué)習(xí)生態(tài)系統(tǒng)的一大關(guān)鍵角色,但風(fēng)險(xiǎn)仍然存在。其中最大的風(fēng)險(xiǎn)是:盡管 Python 存有缺陷,但對(duì)于大部分機(jī)器學(xué)習(xí)任務(wù)來(lái)說(shuō)已經(jīng)足夠好了。對(duì)于許多已經(jīng)熟悉 Python 的人來(lái)說(shuō),慣性可能太大,也沒(méi)有換成另一種語(yǔ)言的理由。另外,谷歌已經(jīng)不是一次兩次放棄大型項(xiàng)目了,而 S4TF 的一些關(guān)鍵人員的脫離也讓人擔(dān)憂。
給出了這些免責(zé)聲明之后,作者仍然覺(jué)得 Swift 是一門(mén)很棒的語(yǔ)言,這些新增的功能也極具創(chuàng)新性,相信它們最終能在機(jī)器學(xué)習(xí)社區(qū)找到自己的位置。因此,如果你也想為這個(gè)潛力無(wú)窮的項(xiàng)目添磚加瓦,現(xiàn)在就是很好的時(shí)機(jī)。Swift 在機(jī)器學(xué)習(xí)領(lǐng)域的地位還遠(yuǎn)未確立,還有很多工具有待開(kāi)發(fā)。隨著 Swift 機(jī)器學(xué)習(xí)生態(tài)系統(tǒng)的持續(xù)發(fā)展,現(xiàn)在的小項(xiàng)目也許未來(lái)可以成長(zhǎng)為巨大的社區(qū)項(xiàng)目。
原文鏈接:https://tryolabs.com/blog/2020/04/02/swift-googles-bet-on-differentiable-programming/
本文為機(jī)器之心編譯,轉(zhuǎn)載請(qǐng)聯(lián)系本公眾號(hào)獲得授權(quán)。