谷歌開源 Swift for TensorFlow:我們是不是終于可以放下Python了?
今年3月的TensorFlow開發者峰會上,Google宣布了Swift For TensorFlow項目,提到這一項目將在4月開源。就在4月快過去的時候,Google終于在GitHub上公開了Swift For TensorFlow的源代碼。
提到Swift語言,大家第一個想到的是Apple。所以,很自然地,Swift For TensorFlow,乍聽起來,似乎僅僅是iOS開發者需要關心的事。
然而,其實iOS開發者并不需要關心Swift For TensorFlow,反而是機器學習開發者都需要關心Swift For TensorFlow.
Swift For TensorFlow并不面向iOS開發
目前而言,iOS應用,如果想集成機器學習功能,那可以通過Apple提供的Core ML框架:
Core ML的工作流程如上圖所示,上面的Core ML機器學習模型,可以是你從網上找的現成的,也可能是你自己開發的(一般由基于MXNet、TensorFlow等開發的模型轉換而來)。所以,其實Core ML并不在意你的模型是用Python加TensorFlow編寫的,還是Swift加TensorFlow編寫的,甚至是用MXNet編寫的。最終iOS應用都是通過Core ML框架調用Core ML格式的模型。
所以,Swift For TensorFlow并不面向iOS開發,而是要取代Python!
其實這并不奇怪。雖然人們總是把Swift和Apple聯系起來,但其實Swift之父Chris Lattner在Google Brain(順便提下,Python之父Guido Van Rossum在2012年底離開Google去了Dropbox)。
Lattner在Swift發布前發推:“下個月我將是第一個也是唯一一個有4年swift編程經驗的人 :-)”
Python有什么不好
雖然Python是現在最流行的機器學習語言,但其實在機器學習場景下,Python的問題還真不少:
- 部署麻煩,運行時依賴太多。首先,移動端應用,帶上一大堆Python包,不太現實。其次,很多公司的生產環境,基于運維需求,不想部署大量Python包。目前的補救方法是,使用Python訓練模型,而真正的推理(運用)階段用別的語言,比如C++重寫,這導致了重復勞動,拖慢了開發周期。
- 動態類型,沒有編譯期類型檢查。這導致很多錯誤要到運行時才能發現。在機器學習場景下,這一問題的后果更加嚴重,因為機器學習模型常常需要訓練、運行很長時間。事實上,大型的Python項目都非常依賴單元測試,通過單元測試捕捉很多錯誤。但在機器學習的場景下,單元測試沒有用武之地。普通程序,跑一遍單元測試可能也就不到半小時,發現報錯,改過來再跑一遍就是了。而機器學習,模型跑了半個月,報錯了,發現是代碼編寫錯誤,試求此時心理陰影面積。
- 并發困難,臭名昭著的GIL問題。而機器學習模型對算力的貪婪需求,迫切需要靠并發緩解。
- 性能太差。事實上,像PyTorch這樣的框架,挖空心思補救Python的性能問題。而TensorFlow依靠圖模型(詳見下一節)以及C++、CUDA定制操作來規避Python的性能問題。使用C++、CUDA定制操作帶來了兩個問題:
- C++是一門復雜的語言。尤其是很多研究人員和數據分析人員,并不具備C++經驗。
- 使用C++/CUDA定制TensorFlow操作導致和硬件緊密耦合(CUDA意味著只能在Nvidia的GPU上跑),遷移到新硬件困難。這一點對Google來說尤其關鍵,因為Google除了用Nvidia的GPU外,自家還有TPU。
使用TensorFlow的時候,你真的在寫Python嗎?
讓我們看一段簡短的TensorFlow代碼示例:
- import tensorflow as tf
- x = tf.placeholder(tf.float32, shape=[1, 1])
- m = tf.matmul(x, x)
- with tf.Session() as sess:
- print(sess.run(m, feed_dict={x: [[2.]]}))
這上面是合法的Python代碼,但是仔細看看,這些代碼實際上干了什么,我們就會發現,其實這些代碼構建了一個圖m,然后通過tf.Session()的run方法運行了圖m。
下面一段代碼可能更明顯,我們想迭代數據集dataset,在TensorFlow下需要這樣寫:
- dataset = tf.data.Dataset.range(100)
- iterator = dataset.make_one_shot_iterator()
- next_element = iterator.get_next()
- for i in range(100):
- value = sess.run(next_element)
- assert i == value
我們看到,我們不能直接使用Python迭代數據集,而要通過TensorFlow提供的方法構建迭代器。
這一情況可以類比使用Python訪問SQL數據庫:
- t = ('RHAT',)
- q = 'SELECT * FROM stocks WHERE symbol=?'
- c.execute(q, t)
這里,我們構造了SQL請求語句,然后通過Python“執行”(execute)這些語句。表面上你在寫Python,其實關鍵的邏輯在SQL語句里。更準確地說,你是在用Python構造SQL語句,然后運行構造的語句。這稱為元編程(meta programming)。
同理,在TensorFlow下,表面上你在寫Python,其實關鍵的邏輯都在TensorFlow圖里。更準確地說,你是在用Python構造TensorFlow圖,然后運行構造的圖。
實際上,2017年萬圣節(10月31日),Google發布了TensorFlow Eager Execution(貪婪執行),讓你可以直接使用Python編程,而不是使用Python元編程TensorFlow圖。
使用Eager Execution,上面兩段TensorFlow代碼可以改寫為:
- import tensorflow as tf
- import tensorflow.contrib.eager as tfe
- # 開啟貪婪執行模式
- tfe.enable_eager_execution()
- x = [[2.]]
- m = tf.matmul(x, x)
- print(m)
- dataset = tf.data.Dataset.from_tensor_slices([1, 2, 3, 4, 5, 6])
- dataset = dataset.map(tf.square).shuffle(2).batch(2)
- # Python風格的迭代器類
- for x in tfe.Iterator(dataset):
- print(x)
你看,TensorFlow明明可以“好好地”用Python編程的嘛。之前為什么要這么大費周折地繞一個圈子?
因為性能。
機器學習,尤其是現代的復雜模型,有著極高的算力需求。TensorFlow圖可以很好地應對貪婪的算力需求,而Python則對此力不從心。
TensorFlow圖專門針對機器學習的需求設計,所以可以很好地優化,以提升性能。然而,性能的優化并不是沒有代價的,為了更好地優化,TensorFlow圖對模型有著許多假設(這些假設從另一方面來說也是限制),也要求構造、運行分階段進行(靜態圖模型)。這影響了模型的靈活性和表達力,因此,不支持動態圖模型是TensorFlow的一大痛點。
兼顧性能與靈活性
TensorFlow Eager Executation支持動態圖,但是性能很差(還記得我們之前提到的Python的性能和GIL問題嗎?);常規的TensorFlow性能好,但靈活性不行。那么,有沒有可以兼顧兩者的方案呢?
機器學習社區在這方面做了很多探索。
傳統上,解釋器(TensorFlow Eager Executation本質上是一個解釋器)的性能,常常可以通過JIT來提升。PyTorch就嘗試通過Tracing JIT(跟蹤即時編譯)提升性能(基于Python、支持動態圖模型的PyTorch飽受Python性能問題的困擾)。簡單來說,Tracing JIT統計頻繁執行的操作,將其編譯為機器碼執行,從而優化性能。然而,Tracing JIT有一些問題,包括“展開的”運算可能導致非常長的trace,可能污染trace導致排錯困難,無法利用“以后的代碼”加以優化,等等。
因此,最終TensorFlow選擇了代碼生成這一條路。也就是對動態圖模型代碼進行分析,自動生成對應的TensorFlow圖程序。而正是這一選擇,導致了Python的出局。
Python有一大堆動態特性,使得Python無法被可靠地靜態分析。
那么,就只有兩個選擇:
- 對Python語言進行剪裁,得到一個便于靜態分析的子集。
- 換語言。
實際上,Google在2017年開源過一個Tangent項目。當時做Tangent,是為了解決自動微分(automatic differentiation)問題——自動微分同樣依賴于對代碼進行分析,而Python語言很難分析。然而,Python的類高度依賴動態特性,很難在這樣的子集上得到支持。而如果連類這樣的抽象層級都不支持的話,那基本上已經完全不像Python了。
所以,就換語言吧。
順帶提一下,TensorFlow選擇了靜態分析后生成代碼并編譯的路線,但其實生成代碼并不一定非得使用編譯器。2010年提出的Lightweight Modular Staging(LMS)技術可以支持在運行時進行代碼生成,無需編譯器。不過,LMS技術下,對控制流程的支持需要一些極少數語言(比如Scala)才支持的奇異特性。所以即使用LMS,也一樣需要換掉Python。而TensorFlow之所以沒有選擇LMS,除了可以進行LMS的語言極少之外,還有一個原因是LMS需要用戶介入,比如,在Scala下,需要將數據類型顯式地包裹進Rep類型才能支持LMS。
為什么是Swift?
其實,雖然有這么多編程語言,可供選擇的范圍并不大。
首先,語言的生態系統很重要。選擇一門語言,其實也是選擇這門語言的生態系統
,包括開發環境、調試工具、文檔、教程、庫、用戶。這就排除了創造一門新語言和使用大多數學術性語言兩個選項。
然后,動態性導致大批語言出局。之前已經提過,Python的大量動態特性導致難以可靠地靜態分析。同理,像R、Ruby、JavaScript之類的動態語言,也被排除了。
甚至像TypeScript、Java、C#、Scala這樣的靜態語言也不行,因為在這些語言中,動態分發(dynamic dispatch)非常普遍。具體來說,這些語言的主要抽象特征(類和接口)其實基于高度動態的構造。比如,在Java中,Foo foo = new Bar();foo.m()調用的是Bar類的m方法,而不是Foo類的m方法。
Google自家的Go語言也有這個問題。Go的接口也是動態分發的。而且,Go沒有泛型,當然,Go的map類型具有一些內建在語言中的類似泛型的特性。如果TensorFlow使用Go語言的話,Tensor也得像map一樣內建進Go語言才行,而Go語言社區推崇輕量設計,在Go語言中內建Tensor有悖其核心理念。
那么,剩下的選擇就屈指可數了:
- C++
- Rust
- Swift
- Julia
C++很復雜,而且C++有著未定義行為太多的不好名聲,另外,C++大量依賴C宏和模板元編程。Rust的學習曲線非常陡峭。Julia是個很有趣的語言,雖然它是動態的,但是Julia有很多類型專門化(type specialization)的黑科技,因此也許可以支持TensorFlow圖特征提取。不過,Julia的社區比Swift小,再加上Swift之父在Google Brain,最終TensorFlow選擇了Swift.
當然,需要指出的是,Swift的類同樣是高度動態的,但是Swift有sturt和enum這些靜態的結構,而且這些結構同樣支持泛型、方法、協議(Swift的協議提供類似接口的特性及mixin)。這使得Swift既可以被可靠地靜態分析,又能有可用的高層抽象。
另外,還記得之前我們提到的Python的缺點嗎?讓我們看看Swift在這些方面的表現:
- 部署簡單。Swift可以編譯為機器碼,基于Swift編寫的ML模型可以編譯為簡單易部署的.o/.h文件。
- 靜態類型,提供編譯器檢查。另一方面,靜態類型也讓IDE可以更加智能地提示錯誤。這在實際編程中極有幫助。
- Swift在語言層面還不支持并發,但可以很好地配合pthreads使用。而且Swift即將在語言層面加入并發支持。
- Swift性能很好,對內存的需求也不高。由于Swift在移動端使用很普遍,因此Swift社區很重視性能優化。等顯式內存所有權支持加入后,Swift在很多場景下可以取代C++. Swift基于LLVM(別忘了,Swift之父也是LLVM之父),能夠直接訪問LLVM底層,而LLVM可以為Nvidia和AMD顯卡生成GPU核。因此,未來基于Swift定制TensorFlow操作也會是Swift For TensorFlow的優勢。
當然,目前機器學習社區積累了很多Python組件,因此,Swift For Python也提供了Python互操作性。比如,下面的代碼展示了如何在swift下訪問python的numpy庫(注釋為python代碼):
- import Python
- let np = Python.import("numpy") // import numpy as np
- let a = np.arange(15).reshape(3, 5) // a = np.arange(15).reshape(3, 5)
- let b = np.array([6, 7, 8]) // b = np.array([6, 7, 8])
Python即將沒落?
最后,我們簡單展望下Python的前途。
想想現在Python主要用于什么場景?
教學。是的,Python作為教學語言很合適,但是,僅僅適合作為教學語言,遠遠不夠。上了年紀的讀者可能還記得小時候微機課(是的,那時候還用“微機”稱呼電腦)上教的Logo語言(操作小烏龜畫圖),現在還有多少人用呢?曾經風光無限的Pascal語言就是為教學而開發的,現在還有多少人用呢?而且,教學語言的選擇很大程度上受語言流行程度影響,而不是相反。當年鬧得沸沸揚揚的MIT計算機科學和編程入門課程從scheme換成python,理由之一就是python更流行。
工具。Python用來寫一些小工具不錯,因為Python的標準庫非常出色,寫起來不啰嗦,小工具也不太考慮性能問題,項目規模小,缺乏編譯期類型檢測也不是大問題。但這一塊已經逐漸被Go蠶食,因為Go的標準庫同樣出色,寫起來也很簡潔,性能還比Python好,部署起來還比Python方便。
web開發。Python的web開發,其實更多地是因為Python流行,庫多,容易招人,而不是真的有多么適合web開發。在web開發方面,Python有太多競爭者。古老的PHP仍有活力,PHP 7修正了很多一直被詬病的缺陷,還有Facebook這樣的巨頭為其開發配套工具。Ruby也仍然很受歡迎,很火的Python web開發框架Flask,其實最早就是借鑒了Ruby的Sinatra框架的設計。而高性能web開發越來越強調高IO、非阻塞,Node.js和Go在這方面表現出色,Java社區也出現了Netty和Vert.x(要比庫多,好招人,誰比得過Java?)。所以,Python在這方面實在沒什么優勢。
科學計算。目前而言,Python在這方面仍有顯著優勢。然而,曾幾何時,Fortran也在科學計算領域占據統治地位。現在還有多少人用呢?而且,由于Python的性能問題,實際上大量Python的科學計算庫底層大量依賴C或C++的,有朝一日轉移的話,會比當年的Fortran快太多。
機器學習。不得不說,AI和ML的浪潮,給Python打了強心針。因為科學計算相對而言,關注度沒有那么高。對比一下R,在統計分析領域也很流行,但從來沒有Python這么受關注,這是因為R并不適合寫工具和開發web. 然而,成也蕭何,敗也蕭何,Swift For TensorFlow的出現,就說明Python的機器學習主流語言的地位并不穩固(Python在機器學習領域的流行,更多地是因為科學計算方面的積累和流行程度,而不是真正適合建模機器學習問題)。
所以說,Python的沒落,很有可能喲~