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

AnguliaChao發表於2018-10-06

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 發生在解析之後,執行之前。

單句用:

julia>:(1+2)
        1+2
julia>(1+2)
      3

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

A = quote
      code1
      code2
    end

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

for (i,expr) in enumerate(A.args)
    println(n,expr)

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

julia> expr=:(x^y)
:(x ^ y)
julia> expr1=:($x^y)
:(-2 ^ y)
julia> expr2=:(x^$y)
:(x ^ 2)

巨集

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

julia> macro timeit(ex)
quote
local t0 = time()
local val = $(esc(ex))
local t1 = time()
print("elapsed time in seconds: ")
@printf "%.3f" t1 - t0
val
end
end

julia> using Printf

julia>  @timeit 10^5
elapsed time in seconds: 0.063100000

julia>  @timeit 10+1
elapsed time in seconds: 0.00011

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

3. 高效能運算

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

後Python時代, 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> using Sys
Julia>Sys.WORD_SIZE
64

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

julia> bitstring(5)
"0000000000000000000000000000000000000000000000000000000000000101"

julia> bitstring(-5)
"1111111111111111111111111111111111111111111111111111111111111011"

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

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

julia> sample_add(a,b)= a + b
sample_add (generic function with 1 method)

julia> @code_native sample_add(6,7)
        .text
; Function sample_add {
; Location: REPL[42]:1
        pushq   %rbp
        movq    %rsp, %rbp
; Function +; {
; Location: int.jl:53
        leaq    (%rcx,%rdx), %rax
;}
        popq    %rbp
        retq
        nopw    (%rax,%rax)
;}

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

2. 溢位(overflow)

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

julia>  typemax(Int64)
9223372036854775807

julia> typemin(Int64)
-9223372036854775808

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

julia> 9223372036854775806 + 1
9223372036854775807
julia> 9223372036854775806 + 1 + 1
-9223372036854775808
julia> 2^64
0
julia> 2^65
0

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

3. 超大整數(BigInt)

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

julia> big(9223372036854775806) + 1 + 1
9223372036854775808
julia> big(2)^64
18446744073709551616

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

4. 整型互換(Type Conversion)

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

julia> UInt64(UInt32(1))
0x0000000000000001
julia> UInt32(UInt64(1))
0x00000001
julia> UInt32(typemax(UInt64))
ERROR: InexactError: trunc(UInt32, 18446744073709551615)
Stacktrace:
 [1] throw_inexacterror(::Symbol, ::Any, ::UInt64) at .\boot.jl:567
 [2] checked_trunc_uint at .\boot.jl:597 [inlined]
 [3] toUInt32 at .\boot.jl:686 [inlined]
 [4] UInt32(::UInt64) at .\boot.jl:721
 [5] top-level scope at none:0

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 優化加減操作。

julia> function sample_diff(x)
       n = length(x); d = 1/(n-1)
       s = zero(eltype(x))
       s = s + (x[2] - x[1]) / d
       for i = 2:length(x)-1
       s = s + (x[i+1] - x[i+1]) / (2*d)
       end
       s = s + (x[n] - x[n-1])^2/d
       end
sample_diff (generic function with 1 method)


julia> function sample_diff_fast(x)
       n=length(x); d = 1/(n-1)
       s = zero(eltype(x))
       @fastmath s = s + (x[2] - x[1]) / d
       @fastmath for i = 2:n-1
       s = s + (x[i+1] - x[i+1]) / (2*d)
       end
       @fastmath s = s + (x[n] - x[n-1])^2/d
       end
sample_diff_fast (generic function with 1 method)

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

julia> t=rand(5000)

julia> sample_diff(t)
604.8332524539333

julia> sample_diff_fast(t)
604.8332524539333

julia> usingBenchmarkTools
julia> @benchmark sample_diff(t)
BenchmarkTools.Trial:
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     7.146 μs (0.00% GC)
  median time:      7.253 μs (0.00% GC)
  mean time:        7.341 μs (0.00% GC)
  maximum time:     30.187 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     4

julia> @benchmark sample_diff_fast(t)
BenchmarkTools.Trial:
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     1.835 μs (0.00% GC)
  median time:      1.920 μs (0.00% GC)
  mean time:        1.948 μs (0.00% GC)
  maximum time:     13.056 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     10

2. KBN 求和(KBN summation)

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

julia> sum([1 1e-200 -1])
0.0

julia> using KahanSummation
julia> sum_kbn([1 1e-200 -1])
1.0e-200

julia> @benchmark sum([1 1e-200 -1])
BenchmarkTools.Trial:
  memory estimate:  224 bytes
  allocs estimate:  5
  --------------
  minimum time:     112.085 ns (0.00% GC)
  median time:      113.931 ns (0.00% GC)
  mean time:        147.602 ns (19.42% GC)
  maximum time:     45.530 μs (99.56% GC)
  --------------
  samples:          10000
  evals/sample:     925


julia> @benchmark sum_kbn([1 1e-200 -1])
BenchmarkTools.Trial:
  memory estimate:  384 bytes
  allocs estimate:  11
  --------------
  minimum time:     227.825 ns (0.00% GC)
  median time:      231.045 ns (0.00% GC)
  mean time:        289.537 ns (17.16% GC)
  maximum time:     79.454 μs (99.52% GC)
  --------------
  samples:          10000
  evals/sample:     530

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(n, m) = "base case"
f(n::Number, m::Number) = "n and m are both numbers"
f(n::Number, m) = "n is a number"
f(n, m::Number) = "m is a number"
f(n::Integer, m::Integer) = "n and m are both integers"

以上五行程式碼會返回一個存在五個方法的函式 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 地址)進行包新增。

新增官方註冊包:

(v1.0) pkg> add AccurateArithmetic
   Cloning default registries into C:\Users\XXX\.julia\registries
   Cloning registry General from "https://github.com/JuliaRegistries/General.git"
  Updating registry at `C:\Users\XXX\.julia\registries\General`
  Updating git-repo `https://github.com/JuliaRegistries/General.git`
 Resolving package versions...
 Installed AccurateArithmetic ─ v0.1.3
  Updating `C:\Users\XXX\.julia\environments\v1.0\Project.toml`
  [22286c92] + AccurateArithmetic v0.1.3
  Updating `C:\Users\XXX\.julia\environments\v1.0\Manifest.toml`
  [22286c92] + AccurateArithmetic v0.1.3
  [2a0f44e3] + Base64
  [8ba89e20] + Distributed
  [b77e0a4c] + InteractiveUtils
  [8f399da3] + Libdl
  [37e2e46d] + LinearAlgebra
  [56ddb016] + Logging
  [d6f4376e] + Markdown
  [9a3f8284] + Random
  [9e88b42a] + Serialization
  [6462fe0b] + Sockets
  [8dfed614] + Test

從第三方網址安裝:

(v1.0) pkg> add https://github.com/bensadeghi/DecisionTree.jl
   Cloning git-repo `https://github.com/bensadeghi/DecisionTree.jl`
  Updating git-repo `https://github.com/bensadeghi/DecisionTree.jl`
 Resolving package versions...
 Installed ScikitLearnBase ─ v0.4.1
 Installed Compat ────────── v1.2.0
  Updating `C:\Users\XXX\.julia\environments\v1.0\Project.toml`
  [7806a523] + DecisionTree v0.8.1+ #master (https://github.com/bensadeghi/DecisionTree.jl)
  Updating `C:\Users\XXX\.julia\environments\v1.0\Manifest.toml`
  [34da2185] + Compat v1.2.0
  [7806a523] + DecisionTree v0.8.1+ #master (https://github.com/bensadeghi/DecisionTree.jl)
  [6e75b9c4] + ScikitLearnBase v0.4.1
  [ade2ca70] + Dates
  [8bb1440f] + DelimitedFiles
  [76f85450] + LibGit2
  [a63ad114] + Mmap
  [44cfe95a] + Pkg
  [de0858da] + Printf
  [3fa0cd96] + REPL
  [ea8e919c] + SHA
  [1a1011a3] + SharedArrays
  [2f01184e] + SparseArrays
  [10745b16] + Statistics
  [cf7118a7] + UUIDs
  [4ec0a83e] + Unicode

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

(v1.0) pkg> help
  Welcome to the Pkg REPL-mode. To return to the julia> prompt, either press backspace when the input line is empty or press Ctrl+C.

  Synopsis

  pkg> [--env=...] cmd [opts] [args]

  Multiple commands can be given on the same line by interleaving a ; between the commands.
  ...
  build: run the build script for packages
  pin: pins the version of packages
  free: undoes a pin, develop, or stops tracking a repo.
  ...
  instantiate: downloads all the dependencies for the project
  resolve: resolves to update the manifest from changes in dependencies of developed packages
  generate: generate files for a new project
  preview: previews a subsequent command without affecting the current state
  precompile: precompile all the project dependencies
  gc: garbage collect packages not used for a significant time
  activate: set the primary environment the package manager manipulates

目前看來在對開發包管理的基本操作上,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 庫:

julia> Pkg.update()
julia> Pkg.add("DecisionTree")
julia> Pkg.add("ScikitLearn")

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

julia> using ScikitLearn
julia> using DecisionTree

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

# Create a random dataset
Random.seed!(42)
X = sort(5 * rand(80))
XX = reshape(X, 80, 1)
y = sin.(X)
y[1:5:end] += 3 * (0.5 .- rand(16))

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

# Fit regression model
regr_1 = DecisionTreeRegressor()
regr_2 = DecisionTreeRegressor(pruning_purity_threshold=0.05)
regr_3 = RandomForestRegressor(n_trees=20)
fit!(regr_1, XX, y)
fit!(regr_2, XX, y)
fit!(regr_3, XX, y)

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

# Predict
X_test = 0:0.01:5.0
y_1 = predict(regr_1, hcat(X_test))
y_2 = predict(regr_2, hcat(X_test))
y_3 = predict(regr_3, hcat(X_test))

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

# Import the necessary modules and libraries
import numpy as np
from sklearn.tree import DecisionTreeRegressor

#run in Python IDE
import matplotlib.pyplot as plt

# Create a random dataset
rng = np.random.RandomState(1)
X = np.sort(5 * rng.rand(80, 1), axis=0)
y = np.sin(X).ravel()
y[::5] += 3 * (0.5 - rng.rand(16))

# Fit regression model
regr_1 = DecisionTreeRegressor()
regr_2 = DecisionTreeRegressor(max_depth=5)
regr_1.fit(X, y)
regr_2.fit(X, y)

# Predict
X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]
y_1 = regr_1.predict(X_test)
y_2 = regr_2.predict(X_test)

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

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

julia> @timeit sample_dt()
elapsed time in seconds: 0.030501-element Array{Float64,1}:

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

t0 = time.time()
# Import the necessary modules and libraries
rng = np.random.RandomState(1)
X = np.sort(5 * rng.rand(80, 1), axis=0)
y = np.sin(X).ravel()
y[::5] += 3 * (0.5 - rng.rand(16))
regr_1 = DecisionTreeRegressor()
#regr_2 = DecisionTreeRegressor(max_depth=5)
regr_1.fit(X, y)
#regr_2.fit(X, y)
t1 = time.time()-t0
print(t1*1000)
#1993.1974411010742

雖然上述例子並不算十分嚴謹,不過相對來說,通常對於初學者或者演算法使用者,不涉及自己 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

相關文章