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