當前位置:
首頁 > 知識 > 後Python時代,Julia告訴你速度和靈活性真的都可以有

後Python時代,Julia告訴你速度和靈活性真的都可以有

機器之心原創

作者Angulia

編輯:Hao

8 月份,Julia 1.0 發布,在社區內引發了極大的關注。之後不久,機器之心推薦了一篇簡單的中文教程。在最新的這篇文章中,作者對 Julia 的眾多特性進行了介紹,同時簡略介紹了 Julia 在機器學習和深度學習方面的資源儲備。

1. 簡介

Julia 是 Jeff Bezanson、Stefan Karpinski、Viral Shah 和 Alan Edelman 幾位科學家於 2009 年開始研發、2012 年首次發布的動態語言。設計它的最初目的是為了高性能的數值分析和計算機編程,科學家們發現目前的編程語言如 MatLab、C、Python、Ruby 在各自的優勢領域發揮很好,但每種語言也有其自身不可避免的缺陷。所以科學家們野心勃勃地提出了對 Julia 的暢想:希望它可以有 C 一樣的速度,對複雜公式處理跟 Matlab 一樣友好,可視化或者粘合性跟 Python 一樣方便,同時還兼具 Ruby 的動態性。而從最初版本發行至最新的 1.0 版(2018 年 8 月 8 日),Julia 也一直持續地提升高效性和易用性,並引入新功能。

Julia 設計的新穎性在於它有一個支持參數多態的類型(Type)架構,支持多重派發 (Multiple-Dispatch) 編程方式,同時允許並發、並行、分散式計算等。另在數值計算方面,Julia 使用元編程以及一些高效內嵌庫函數,在易用性和高效性方面都做了很多改進。同時對於數據科學家和機器學習愛好者來說,必要的機器學習庫和深度學習支持包也是一個重要的考察點,本文就從上述介紹的諸多 Julia 特性入手,同時簡略介紹了 Julia 在機器學習和深度學習方面的資源儲備。

2. 元編程

元編程(MetaProgramming)是 Julia 語言中一個非常核心的概念,它是代碼優化和提升的基礎,所以首先我們就從元編程的概念入手,通過比較小的代碼實例來體會元編程的思想。(運行本文的代碼實例或需要安裝部分包,對 Julia 的包安裝不熟悉的讀者在運行之前可以先參考文章第三部分關於 Julia 依賴包添加管理的內容,同時注意不同版本的 Julia 在包名和函數所屬包方面有較大變化,本文的代碼實例都基於 Julia 1.0 運行。)

Julia 程序執行過程分兩步,一是 AST 進行解析(parsing),之後由編譯器運行(evaluation)。Metaprogramming 發生在解析之後,運行之前。

單句用:

單句的執行隔斷可以使用冒號「:」。如果是較長的多行代碼或者一個代碼片段,使用 quote…...end 形式:

如果需要執行 A,使用 eval() 函數。如果需要查看 A 的結構,用 fieldnames()。如果想看整段代碼的解析,那麼就用 dump(),同時你還可以查看所有的 args list。

根據每行的子表達式 (sub-expr),可以看到程序是分步執行的,那麼我們就可以在其中的某一步進行數值的修改:

在 Julia 中,宏(Macros)可以簡單理解為函數一樣的存在,接受輸入然後返回我們需要的返回值。不同之處在於,宏接受的輸入不是單純的數值(value)而是表達式(expression)。我們在代碼解析階段對輸入的表達式進行處理或者修改,返回擴展後的表達式,與上一部分元編程結合,那麼在代碼編寫過程中,我們對希望處理的流程代碼嵌入宏的表達處理以及中斷執行的嵌套組合句式 (quote-end),從而在代碼解析時就會跳入宏的處理流程,得到修改後的代碼表達式並得以執行,通過以下的代碼示例應用這部分思想:

通過宏來計算任意函數的執行時間,節省了代碼量,也相應做到了效率的提高。

3. 高性能計算

Julia 問世之初就以『性能媲美 C++』聞名,所以高性能計算(high performance computing)是這個語言的一大亮點,無論是之前的元編程還是宏,都體現了該語言本身效率至上的特點。從 built-in 的數據結構、程序設計思路,以及重構函數、簡化循環等等方面,Julia 對於性能優化做了很多工作,鋪陳展開一文難以盡敘,所以本文側重介紹 Julia 在數值類型與計算方面為高性能做出的改進和特色性質。

上圖引用自維基百科

https://commons.wikimedia.org/wiki/File:Type-hierarchy-for-julia-numbers.png

參考如上的 Julia 數值設計

A. 整型 (Integers)

1. 編碼

為了最大程度上利用用戶設備的計算性能,Julia 的數值類型(Number Type)設計儘可能貼近硬體運算力。首先對於整型(Integers),默認精度大小取決於用戶使用的操作系統(OS)或者 CPU,常見的就是 Int32 與 Int64 兩種整數類型,而對於 Int 類型來說,它代表的類型也默認成你的系統支持位寬 (bit width),用如下代碼可以檢查當前 Julia 環境整型的默認位寬:

同時在不進行特別指定的情況下,Julia 默認使用帶符整型 (signed integer),上述提到的這麼多數值類型均以二進位數字的形式存儲,用 bits() 函數可以幫助我們查看數值的二進位表示:

測試時本機用的是 64 位操作系統,所以正整數 5 用 64 位的二進位數進行存儲,-5 則利用 64 位相應的二進位補碼儲存。

對於整型(Int),我們知道 Python 中的整型在 C 語言里的長整型(Long)上來實現,浮點數 (Float) 則由雙精度浮點數(Double)來實現。但是在 Julia 中,因為直接採用二進位數值存儲的緣故,整型或者浮點型的數值都可以被稱為 bits 類型,而這樣的數值類型可以支持一種稱之為封裝(box)的操作,即將數值在內存中存儲的同時,加上一個表示它類型的前綴,可以簡單理解為類似於在 C 程序中指定一個整型變數,然而 Julia 的 JIT 編譯器在編譯代碼時卻能夠做到很好地去除不必要的封裝/解封操作(box/unbox),從而不必產生冗餘的彙編代碼。下面是一個簡單的整數加法的實例代碼,我們調用一個宏來看它產生的編譯代碼(類似彙編程序),可以看到生成的代碼簡單地對兩個整數進行了加和,而沒有多餘的封裝後的解封代碼了。

那麼通過對比我們就知道,少了類型判別和轉換,數值操作的效率自然也就得到了提高。更進一步,在這樣的數值類型基礎上建立數組,所佔的存儲空間是整片連續的存儲空間,類型的標籤前綴也會載入在數組的起始位置,類似的存放方式不僅解除了指針引用(pointer dereference)的麻煩,同時也能方便進一步的數組優化操作。

2. 溢出(overflow)

相信有過編程經驗的讀者對溢出這個概念都不陌生,它體現在我們使用的數值超出了該類型所能表示的最大範圍或最小範圍,由此衍生出一系列代碼異常。而在 Julia 中大家還記得上一小節我們提到過整型的默認長度表示會根據你的操作系統來給出,那麼這就潛在地幫我們規避了一部分溢出風險。譬如,你在使用 Int 類型,而你的 Julia 環境根據你的 64 位系統默認判斷使用 Int64,那麼 64 位整型能表示的整數上界與下界就可以通過如下代碼獲知:

那麼如果你的程序中生成的整數還是不幸超過了這個範圍呢?

如上代碼做出了很好的說明,針對你的溢出值根據機器默認機器二進位值表示的上限,不再予以任何增長,而其二進位表示在溢出後也變為 0。和 Python 和 Ruby 這樣的動態語言橫向對比呢?Python 採用了一種自動擴容的方式,首先加入對每種數值類型的溢出檢測,當檢測到溢出行為時,Python 自動將你的數字進行更高範圍表示類型的升級(如超過 Int16 的表示範圍,自動升值你的數據為 Int32 類型),一定程度來說他賦予了程序靈活性,幫助用戶省事,但是代價就是嚴格動態檢查的時間損耗以及仍然隱藏的 bug 風險。而 Julia 就選擇在這方面跟隨 C 的思想,釋放效率,嚴格根據機器二進位碼錶示的上限對你的數值表示進行界定,但是也不需要你一直手動制定類型(默認跟隨你的操作系統類型),一定程度上解放了用戶可操作的範圍。

3. 超大整數(BigInt)

根據上一節我們講到的 Julia 對數值溢出的嚴格控制,那麼如果真的需要用到『越界』整數怎麼辦呢?Julia 給出了一種稱之為 BigInt() 的數值類型,你可以自由地使用超大數值來為你的程序服務了:

誠然超大整型的使用會比使用默認整型在速度上有明顯劣勢。不過針對用戶的具體情況,它的存在既使得你正常使用的整型與溢出絕緣,同時又為你的特殊需要打開了方便的使用介面。

4. 整型互換(Type Conversion)

之前提過 Julia 默認使用的都是帶符號整型的表示(Signed Integer),如果你在實際應用中不需要,那麼可以使用無符整型的構造函數 UInt32() 或 UInt64(),如下:

32 位二進位表示的無符整型數可以用同樣的構造函數自由轉換,而一旦嘗試用 UInt32 表示範圍之外的大數字,就會報同樣的越界錯誤。相同的規則也同樣適用於 16 位、8 位無符整型。

B. 浮點數(Floating Number)

Julia 的浮點數構造函數為 Float64(),它的特性以及標準制定都遵從被廣泛接受的 IEEE 754(Python、C++等語言的浮點數均遵從此標準),其默認位寬都是 64 位而不取決於你本身的機器或者系統位寬。

浮點數類型的基本常用操作與其他語言沒有差別,然而考慮浮點數計算時,精度和效率是不得不考慮的一個方面。Julia 在設計普通的運算操作時,同時照顧了大部分計算的效率和精確度,例如 sum()、+/- 等操作。然而當我們面對一些非常規數 (denormal number) 或者時間敏感的計算操作時,Julia 也提供了二者權衡(tradeoff)的函數。

1. @fastmath 宏

@fastmath 是 Julia 提供的一個宏函數,它通過一定程度上放寬浮點數的 754 標準來實現計算速度的大幅提升,比如它用自己改進的計算操作變體替代普通的計算操作,或者在執行(evaluation)階段對部分代碼的執行順序進行重整,又或者採用另外的機制跳過 NAN 或者 INF 值的檢測等。這些變化都可以一定程度上提高整體代碼的執行速度。然而由於計算過程中的近似取整或者約等於,結果會存在一定的精度損失。

我們通過兩個實現同樣功能的函數對比來初步體會一下 @fastmath 的用法,兩個函數都實現了對一個一維數組的元素兩兩求差再加和,區別在於在進行加減操作時,原始函數用正常的加減,而 fast 版本增添使用了 @fastmath 優化加減操作。

對比運行結果如下,由於加減操作並不複雜二者精度幾乎沒有相差,而計算效率上使用 fastmath 的版本比原始函數快了很多。如果涉及密集的乘除次冪運算,二者之間的區別會更加明顯:

2. KBN 求和(KBN summation)

我們知道在求和操作中或多或少會有近似約等於的計算誤差存在,當被求和數處在相近的量級,誤差就可以近似被忽略,然而如果數字之間量級相差很大(如百萬加百萬分之一),誤差難免時,Julia 提供了另一種精確計算的函數 sum_kbn(),參考如下示例代碼,sum_kbn() 保存了求和結果的精確度,但同時付出更多一點的時間代價。

3. 多重派發(Multiple-Dispatch)

正如之前提過的,在函數定義和調用的時候,Python 之類的動態語言弱化了類型判斷,提供了更加靈活的介面,但是每次執行時都需要進行一次類型判斷也一定程度上影響了代碼性能。Julia 在類似的情況下提出了自己的折中方法——多重派發。

Julia 語言里函數的定義都是泛化的(Generic),即同一個函數可以接受多個類型的參數。函數里具體的一種參數組合可以被稱為函數的一種方法(method),我們定義這樣一種新方法的過程就被稱為函數重載(Overloading),即同樣的函數名稱但是接受了不同的參數組合。

在 Julia 里如果我們為自己的函數定義了一系列這樣的方法,則它們會被存儲在一個虛方法列表(virtual method table, vtable),函數不屬於任意一個類型,也就是類似於一塊全局區域,調用函數運行時,Julia 根據你指定的參數組合會搜尋匹配的方法選擇執行。上述過程就被稱為多重派發,多重派發機制是 Julia 區別於 C++、Python 等語言所獨有的。

雖然以上語言均支持函數重載,然而 C++或 Python 等語言的重載均建立在類上,並不具備泛化的性質,具體來說就是每一個方法的調用都是類似於 obj.method() 的形式,函數特屬於某個類,並不支持多個類型,其所有的虛方法存儲在各自對應的類或類型之中,而 Julia 的虛方法列表 (vtable) 存儲在函數本身內部,因此多重派發是一種更加泛化簡單的用法。我們也繼續通過簡單的代碼實例來體會多重派發的用法:

以上五行代碼會返回一個存在五個方法的函數 f(),在執行時也會尋找和你傳入的參數類型適合的方法。

4. Julia 依賴包添加、更新和管理

除去語言本身包括的基本 built-in 函數,Julia 也像 Python 一樣提供開發包(package)的安裝與管理。Julia 語言中的 Pkg 模塊就提供了自動管理和增刪包的功能,調用 Pkg.status() 可以很輕易地查看已經安裝的包及其對應版本。Julia 的包分為官方註冊(registered)的包(均列在 https://pkg.julialang.org/ 中)與第三方(unofficial)的包。註冊包以可查找的列表的方式存儲在 METADATA.jl 文件中(https://github.com/JuliaLang/METADATA.jl),你可以很容易地根據你查找到的包名稱進行安裝,執行命令 Pkg.add(『packageName』)。除此之外,針對第三方開發的包的安裝,你依然可以用相同的 Pkg.add() 命令,在參數中提供這個第三方包所在的 URL(例如 github repo 地址)進行包添加。

添加官方註冊包:

從第三方網址安裝:

除了支持多個來源的包添加方式以外,Julia 的包管理器還可以根據你的需求用 Pkg.update() 進行包的版本更新。Julia 的 Pkg 管理器支持的基本操作可參考以下示例:

目前看來在對開發包管理的基本操作上,Julia 做的還是比較完善。而包管理工具的另一重要模塊就是多個包依賴關係以及版本控制。Julia1.0 後的 Pkg 模塊可以自動進行相關包依賴的檢測而無需手動配置,同時為了應對不同項目之間包的版本互不兼容的問題,Julia 提出了獨立環境(Enviroment)的解決方式,用戶可以為自己的不同項目創建獨立的環境,在各自環境下選擇對應版本的包進行添加和控制。

由於 Pkg() 是 Julia 自帶的包管理工具,所以無論對於 Linux、Windows,或者 MacOS 系統均有良好的支持(本文涉及實驗均在 Windows 10 下完成,同時從 Julia 最初版本到目前的 1.0 版本,很多包名或者函數所屬包都發生改動,本文默認都是在最近發布的 Julia1.0 上進行的包添加和函數調用,與文章版本不同的用戶需要注意更改引入的包名,才能成功運行函數)。

5. 機器學習/深度學習

對於廣大從事演算法和數據科學的人士,機器學習與深度學習的資源和第三方庫便利性也是非常重要的權衡標準。Julia 問世時間並不長所以對比當前的 Python 來說,它沒有類似 Python 中 Scikit-Learn 這樣比較全面的演算法庫,不過常見的監督學習模型(如決策樹、SVM、Bayes)、無監督學習 KMeans 等演算法模型也均有第三方包實現,另一方面 Julia 也提供 Scikit-Learn 的介面方便用戶使用。初步測試對比結果:

基於同樣的隨機生成的數據集,Julia 訓練決策樹需要 31ms,而 Python Scikit-Learn 需要 1993ms。

1. 決策樹

為了幫助讀者體會 Julia 中機器學習相關包的調用,我們使用決策樹作為一個實例,體會構建模型、喂入數據以及進行測試的一系列過程。在此省去關於決策樹的理論介紹,如需了解決策樹背後的理論知識,可以參考我們之前的文章:

首先我們載入所需要的決策樹庫 ScikitLearn 庫:

ScikitLearn 提供了很多常見演算法實現的介面,也可以提供給 Julia 調用,我們聲明將要用到的所有包。

載入了必要的包之後,我們隨機生成訓練數據與對應標籤:

接著使用決策樹的構造函數分別建立幾個參數不同的回歸器模型作為對比,同時擬合我們生成的數據集。

訓練好模型之後,我們可以繼續用 predict 函數進行測試:

為了讓讀者對 Python 和 Julia 在基礎的機器模型運行上有一個比較直觀的對比,我們也同時用 Python 的 Scikit-Learn 機器學習庫構建一個最簡單的決策樹模型,從運行時間上做基礎對比,如下:

我們將兩種語言下的決策樹模型從訓練到測試都封裝為一個函數,然後測試它們分別的耗時:

在 Julia 的 DecisionTree 模塊中,運行結果執行時間為 31ms 左右:

在 Python 中運行結果執行時間為 1993ms:

雖然上述例子並不算十分嚴謹,不過相對來說,通常對於初學者或者演算法使用者,不涉及自己 DIY 的優化設計,直接調用模型訓練以及預測,Julia 下實現確實是性能更高的選擇。同時從以上代碼流程可以看出,Julia 機器學習的演算法從訓練到測試模型的整體過程與 Python Scikit-Learn 的整體流程是相近的,也易於學習和遷移。如果你需要可視化,一樣可以用 Pyplot 提供的介面進行數據的呈現。隨著 Julia 社區的成長,相信以後也會出現更多、優化更好的學習演算法。

2. 深度學習

在原生 Julia 上,MIT 的 PhD 學生 Chiyuan Zhang 開發實現了 Julia 自己的深度學習框架 Mocha,支持基本的網路結構、損失函數的定義與 GPU 上的模型訓練測試,同時平台也提供了一些基本網路的 pre-train 模型。

另一個開源工作是 FluxML,它提供圖像、文本與強化學習等多項任務的模型與調用,目前很多工作也正在開發中。

如果是從主流框架遷移,Julia 也提供了 TensorFlow 的介面,讓用戶能夠方便地遷移代碼,或者靈活使用 TensorFlow 已實現的模型與方法。

參考資料:

https://github.com/JuliaStats/MLBase.jl

http://julialang.org

https://github.com/bensadeghi/DecisionTree.jl

http://scikit-learn.org/stable/

https://docs.julialang.org/en/v1/

https://github.com/pluskid/Mocha.jl http://www.deeplearningbook.org/contents/intro.html

https://mochajl.readthedocs.io/en/latest/tutorial/ijulia-imagenet.html

https://github.com/FluxML/model-zoo/

https://github.com/malmaud/TensorFlow.jl

http://scikit-learn.org/stable/auto_examples/tree/plot_tree_regression.html

https://github.com/cstjean/ScikitLearn.jl/blob/master/examples/Decision_Tree_Regression_Julia.ipynb

Julia: High Performance Programming

Learning Julia-Build high-performance applications for scientific computing

Julia-CookBook

Getting Started with Julia Programming

本文為機器之心原創,轉載請聯繫本公眾號獲得授權。

------------------------------------------------


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 機器之心 的精彩文章:

BAIR講述如何利用深度強化學習控制靈活手
深度 | 從算力到半導體供應鏈,硬體如何決定機器學習的研究趨勢

TAG:機器之心 |