為Go語言設(shè)計的機器學(xué)習(xí)庫Gorgonia:對標(biāo)TensorFlow與Theano
Gorgonia 是一個促進(jìn)在 Go 中進(jìn)行機器學(xué)習(xí)的庫,旨在更容易地編寫與評估涉及多維數(shù)組的數(shù)學(xué)方程。如果這聽起來很像 Theano 或者 TensorFlow,原因是三者的想法非常相似。具體而言,Gorgonia 像 Theano 一樣相當(dāng)?shù)图?但又像 Tensorflow 一樣具有更高目標(biāo)。
項目地址:https://github.com/chewxy/gorgonia
項目介紹:https://blog.chewxy.com/2016/09/19/gorgonia/
- 可執(zhí)行自動微分
- 可執(zhí)行符號微分
- 可執(zhí)行梯度下降優(yōu)化
- 可執(zhí)行數(shù)值穩(wěn)定
- 提供了許多便利功能,以助于創(chuàng)建神經(jīng)網(wǎng)絡(luò)
- 非???與 Theano 和 Tensorflow 的速度相當(dāng))
- 支持 CUDA / GPGPU 計算(OpenCL 尚不支持,需發(fā)送拉取請求)
- 將支持分布式計算
一、為何選擇 Gorgonia?
Gorgonia 的使用主要方便了開發(fā)者。如果你使用 Go 廣泛地堆棧,便可以在熟悉而舒適的環(huán)境中創(chuàng)建生產(chǎn)就緒的機器學(xué)習(xí)系統(tǒng)。
機器學(xué)習(xí)/人工智能通?;\統(tǒng)地分為兩個階段:(a)建立多種模型并測試、再測試的試驗階段,(b)以及模型在測試與試用之后被部署的部署階段。這兩個階段不可或缺且作用各異,就像數(shù)據(jù)科學(xué)家和數(shù)據(jù)工程師之間的區(qū)別。
原則上這兩個階段使用的工具也不同:實驗階段通常使用 Python / Lua(使用 Theano,Torch 等),而后這個模型會使用性能更高的語言來重新編寫,如 C++(使用 dlib、mlpack 等)。當(dāng)然,如今差距正在慢慢縮短,人們也經(jīng)常會進(jìn)行工具共享,比如 Tensorflow 便可用來填補差距。
而 Gorgonia 的目的,卻是在 Go 環(huán)境中完成相同的事情。目前 Gorgonia 性能相當(dāng)高,其速度與 Theano 和 Tensorflow 相當(dāng)(由于目前 Gorgonia 存在 一個 CUDA 缺陷,所以還未完成官方基準(zhǔn)測試;另外,因為實現(xiàn)可能會稍有不同,所以很難去比較一個精確的以牙還牙模型)。
二、安裝
安裝包是 go-get 的: go get -u github.com/chewxy/gorgonia.
Gorgonia 所使用的依賴很少且都很穩(wěn)定,所以目前不需要代管工具。下表為 Gorgonia 所調(diào)用的外部軟件包列表,并按照它所依賴的順序進(jìn)行了排列(已省略子軟件包):
三、保持更新
Gorgonia 的項目有一個郵件列表和 Twitter 帳戶,官方更新和公告會發(fā)布到這兩個網(wǎng)站:
- https://groups.google.com/forum/#!forum/gorgonia
- https://twitter.com/gorgoniaML
四、用法
Gorgonia 通過創(chuàng)建計算圖來工作,而后將其執(zhí)行。請把它當(dāng)作一種僅限于數(shù)學(xué)函數(shù)方面的編程語言;事實上,這應(yīng)當(dāng)作為用戶思考的主要實例。它創(chuàng)建的計算圖是一個 AST。
微軟 CNTK 的 BrainScript 可能是用來說明計算圖的構(gòu)建與運行并不相同的最佳實例,所以用戶對于這二者,應(yīng)當(dāng)運用不同的思維模式。
雖然 Gorgonia 的實現(xiàn)并不像 CNTK 的 BrainScript 那樣強制性分離思維,但語法確實略有裨益。
此處舉出一個實例——若要定義一個數(shù)學(xué)表達(dá)式 z = x + y,應(yīng)當(dāng)這樣做:
- package mainimport ( "fmt"
- "log"
- . "github.com/chewxy/gorgonia")func main() { g := NewGraph() var x, y, z *Node var err error
- // define the expression
- x = NewScalar(g, Float64, WithName("x"))
- y = NewScalar(g, Float64, WithName("y"))
- z, err = Add(x, y) if err != nil {
- log.Fatal(err)
- } // compile into a program
- prog, locMap, err := Compile(g) if err != nil {
- log.Fatal(err)
- } // create a VM to run the program on
- machine := NewTapeMachine(prog, locMap) // set initial values then run
- Let(x, 2.0) Let(y, 2.5) if machine.RunAll() != nil {
- log.Fatal(err)
- }
- fmt.Printf("%v", z.Value()) // Output: 4.5}
你可能會發(fā)現(xiàn),它比其他類似的軟件包更顯冗長。如 Gorgonia 并未編譯為可調(diào)用的函數(shù),而是特地編譯為需要TapeMachine 來運行的program;此外它還需要手動調(diào)用一個 Let(...)。
作者卻認(rèn)為這是好事——能夠?qū)⑷说乃季S轉(zhuǎn)變?yōu)闄C器的思維。它在我們想查清哪里出錯的時候很有幫助。
五、虛擬內(nèi)存系統(tǒng)
當(dāng)前版本的 Gorgonia 有兩個虛擬內(nèi)存系統(tǒng):
- TapeMachine
- LispMachine
它們功能不同,采取的輸入也不同。TapeMachine 通常更善于執(zhí)行靜態(tài)表達(dá)式(即計算圖并不改變);由于其靜態(tài)特性,它適用于一次編寫,多次運行的表達(dá)式(如線性回歸,SVM 等)。
LispMachine 則是將圖形設(shè)為輸入,并直接在圖形的節(jié)點上執(zhí)行。如果圖形有所改變,只需新創(chuàng)建一個輕量級 LispMachine 來執(zhí)行便可。LispMachine 適于諸如創(chuàng)建大小不固定的循環(huán)神經(jīng)網(wǎng)絡(luò)這類的任務(wù)。
在 Gorgonia 發(fā)布之前存在第三個虛擬內(nèi)存,它基于堆棧,且與 TapeMachine 相似,但能夠更妥善地處理人工梯度。當(dāng)作者解決了所有的問題后,它也許就能重見天日。
六、微分
Gorgonia 執(zhí)行符號與自動微分,而這兩個過程存在細(xì)微差別。作者認(rèn)為這樣理解是最合適的:自動微分是在運行時所做的微分,與圖表的執(zhí)行同時發(fā)生;符號微分是在編寫階段所做的微分。
此處「運行時」當(dāng)然是指表達(dá)式圖的執(zhí)行,而非程序的實際運行。
通過介紹這兩個虛擬內(nèi)存系統(tǒng),便很容易理解 Gorgonia 如何執(zhí)行符號與自動微分。使用與上文相同的示例,讀者 能夠發(fā)現(xiàn)此處并沒有進(jìn)行微分。這次用 LispMachine 做一次嘗試吧:
- package mainimport ( "fmt"
- "log"
- . "github.com/chewxy/gorgonia")func main() { g := NewGraph() var x, y, z *Node var err error
- // define the expression
- x = NewScalar(g, Float64, WithName("x"))
- y = NewScalar(g, Float64, WithName("y"))
- z, err = Add(x, y) if err != nil {
- log.Fatal(err)
- } // set initial values then run
- Let(x, 2.0) Let(y, 2.5) // by default, LispMachine performs forward mode and backwards mode execution
- m := NewLispMachine(g) if m.RunAll() != nil {
- log.Fatal(err)
- }
- fmt.Printf("z: %v\n", z.Value()) xgrad, err := x.Grad() if err != nil {
- log.Fatal(err)
- }
- fmt.Printf("dz/dx: %v\n", xgrad) ygrad, err := y.Grad() if err != nil {
- log.Fatal(err)
- }
- fmt.Printf("dz/dy: %v\n", ygrad) // Output:
- // z: 4.5
- // dz/dx: 1
- // dz/dy: 1}
當(dāng)然,Gorgonia 同樣支持更傳統(tǒng)的的符號微分,比如在 Theano 中:
- package mainimport ( "fmt"
- "log"
- . "github.com/chewxy/gorgonia")func main() { g := NewGraph() var x, y, z *Node var err error
- // define the expression
- x = NewScalar(g, Float64, WithName("x"))
- y = NewScalar(g, Float64, WithName("y"))
- z, err = Add(x, y) if err != nil {
- log.Fatal(err)
- } // symbolically differentiate z with regards to x and y
- // this adds the gradient nodes to the graph g
- var grads Nodes
- grads, err = Grad(z, x, y) if err != nil {
- log.Fatal(err)
- } // compile into a program
- prog, locMap, err := Compile(g) if err != nil {
- log.Fatal(err)
- } // create a VM to run the program on
- machine := NewTapeMachine(prog, locMap) // set initial values then run
- Let(x, 2.0) Let(y, 2.5) if machine.RunAll() != nil {
- log.Fatal(err)
- }
- fmt.Printf("z: %v\n", z.Value()) xgrad, err := x.Grad() if err != nil {
- log.Fatal(err)
- }
- fmt.Printf("dz/dx: %v | %v\n", xgrad, grads[0]) ygrad, err := y.Grad() if err != nil {
- log.Fatal(err)
- }
- fmt.Printf("dz/dy: %v | %v\n", ygrad, grads[1]) // Output:
- // z: 4.5
- // dz/dx: 1 | 1
- // dz/dy: 1 | 1}
雖然人們在 Gorgonia 中,能嗅到支持 dualValue 存在條件下進(jìn)行正向模式微分的舊版痕跡,但其目前僅執(zhí)行反向模式自動微分(又名反向傳播)。正向模式微分也許在將來能夠回歸。
七、圖表
確實存在許多計算圖或表達(dá)式圖的有關(guān)說法,但它究竟是什么?請將它想象成你想要的數(shù)學(xué)表達(dá)式的 AST。此處為上述示例的圖形(但還有一個向量和一個標(biāo)量加法):
順便一提,Gorgonia 的圖形打印能力很強。此處為方程式 y = x² 及其導(dǎo)數(shù)的圖形示例:
圖形很容易閱讀。表達(dá)式從下往上進(jìn)行構(gòu)建,而導(dǎo)數(shù)是由上向下構(gòu)建的。因此每個節(jié)點的導(dǎo)數(shù)大致處于同一水平。
紅色輪廓的節(jié)點表示它們是根節(jié)點,而綠色輪廓則表示為葉節(jié)點,帶有黃色背景的節(jié)點表示為輸入節(jié)點,而虛線箭頭則表示哪個節(jié)點是指向節(jié)點的梯度節(jié)點。
具體而言,比如 c42011e840 (dy/dx) 便表示輸入 c42011e000(即 x)的梯度節(jié)點。
八、節(jié)點渲染
節(jié)點是這樣渲染的:
補充說明:
- 如果它是輸入節(jié)點,則 Op 行不會顯示。
- 如果沒有綁定到節(jié)點的值,將顯示為 NIL。但若有值和梯度存在,它將極盡所能顯示綁定到節(jié)點的值。
九、使用 CUDA
此外,還存在附加要求:
- 需要 CUDA toolkit 8.0。安裝這個程序?qū)惭b nvcc 編譯器,這是使用 CUDA 運行代碼所必備的。
- go install github.com/chewxy/gorgonia/cmd/cudagen。這是 cudagen 程序的安裝網(wǎng)址。運行 cudagen 將生成與 Gorgonia 有關(guān)的 CUDA 相關(guān)代碼。
- 務(wù)必使用 UseCudaFor 選項,務(wù)必使代碼中的 CUDA 操作能夠手動啟用。
- runtime.LockOSThread() 必須在虛擬內(nèi)存正在運行的主函數(shù)中調(diào)用。CUDA 需要線程親和性,因此必須鎖定 OS 線程。
1. 示例
所以,我們該如何使用 CUDA 呢?假設(shè)有一個文件 main.go:
- import ( "fmt"
- "log"
- "runtime"
- T "github.com/chewxy/gorgonia"
- "github.com/chewxy/gorgonia/tensor")func main() { g := T.NewGraph() x := T.NewMatrix(g, T.Float32, T.WithName("x"), T.WithShape(100, 100)) y := T.NewMatrix(g, T.Float32, T.WithName("y"), T.WithShape(100, 100)) xpy := T.Must(T.Add(x, y)) xpy2 := T.Must(T.Tanh(xpy)) prog, locMap, _ := T.Compile(g) m := T.NewTapeMachine(prog, locMap, T.UseCudaFor("tanh"))
- T.Let(x, tensor.New(tensor.WithShape(100, 100), tensor.WithBacking(tensor.Random(tensor.Float32, 100*100))))
- T.Let(y, tensor.New(tensor.WithShape(100, 100), tensor.WithBacking(tensor.Random(tensor.Float32, 100*100))))
- runtime.LockOSThread() for i := 0; i < 1000; i++ { if err := m.RunAll(); err != nil {
- log.Fatalf("iteration: %d. Err: %v", i, err)
- }
- }
- runtime.UnlockOSThread()
- fmt.Printf("%1.1f", xpy2.Value())
- }
如果它正常運行:
go run main.go
CUDA 不會被使用。
如果程序要使用 CUDA 運行,那么必須進(jìn)行調(diào)用:
- go run main.go
即便如此,也只有 tanh 函數(shù)使用 CUDA。
2. 解釋
使用 CUDA 的要求這么復(fù)雜,主要與其性能有關(guān)。正如 Dave Cheney 的名言:cgo 不是 Go。但不幸的是,使用 CUDA 必定需要 cgo;而若要使用 cgo,則需做出大量權(quán)衡。
因此,解決方案是將 CUDA 相關(guān)代碼嵌套于構(gòu)建標(biāo)記 cuda 中,以這樣的方式默認(rèn)未使用 cgo(好吧,只用了一點點,你仍然可以使用 cblas 或 blase)。
安裝 CUDA toolkit 8.0 的原因是:存在許多 CUDA 計算能力,為它們生成代碼將產(chǎn)生一個毫無益處的巨大二進(jìn)制。相反,用戶會傾向于為他們特定的計算能力編譯。
最后,要求制定使用 CUDA 操作的明確規(guī)范,是由于 cgo 調(diào)用的成本問題。目前為了實現(xiàn)批量的 cgo 調(diào)用而在進(jìn)行額外的努力,但是直到完成之前,該解決方案都會是特定操作的「升級」關(guān)鍵。
3. CUDA 支持的操作
迄今為止,只有極基本的簡單操作能夠支持 CDUA:
元素一元運算:
- abs
- sin
- cos
- exp
- ln
- log2
- neg
- square
- sqrt
- inv (reciprocal of a number)(數(shù)字的倒數(shù))
- cube
- tanh
- sigmoid
- log1p
- expm1
- softplus
元素二進(jìn)制操作——只有算術(shù)運算支持 CUDA:
- add
- sub
- mul
- div
- pow
根據(jù)對作者個人項目的大量剖析,發(fā)現(xiàn)真正重要的是 tanh、sigmoid、expm1、exp 和 cube,即激活函數(shù)。其他使用 MKL + AVX 的操作正常運行,且并非造成神經(jīng)網(wǎng)絡(luò)緩慢的主因。
4. CUDA 的改進(jìn)
在一項次要的基準(zhǔn)測試中,CUDA 的謹(jǐn)慎使用(此情況通常調(diào)用 sigmoid)顯示出非 CUDA 代碼的大幅改進(jìn)(考慮到 CUDA 內(nèi)核十分樸素且未優(yōu)化):
BenchmarkOneMilCUDA-8 300 3348711 ns/op
BenchmarkOneMil-8 50 33169036 ns/op
十、API 的穩(wěn)定性
Gorgonia 的 API 如今并不穩(wěn)定,它將從 1.0 版本開始慢慢穩(wěn)定。
1.0 版本是測試覆蓋率達(dá)到 90%時所定義的,并且相關(guān)的 Tensor 方法已經(jīng)完成。
十一、路線圖
這是依照重要性排序所列出的 Gorgonia 的目標(biāo):
- 80%以上的測試覆蓋率。目前 Gorgonia 的覆蓋率為 50%,tensor 為 80%。
- 更高級的操作(如 einsum)。目前的 Tensor 操作符非常原始。
- 軟件包中的 TravisCI。
- 軟件包中的 Coveralls。
- 清除測試。測試是多年積累的結(jié)果,妥當(dāng)?shù)刂貥?gòu)它們將大有裨益。若條件允許,使用表格驅(qū)動測試。
- 提升性能,特別是應(yīng)當(dāng)重新分配,將系統(tǒng)類型的影響最小化。
- 將 Op 界面從半輸出公開/更改為全輸出,以此提高 Op 的可擴展性(或者為擴展性創(chuàng)建一個 Compose 的 Op 類型)。這樣每個人都可以制作自定義的 Op。
- 為了跟隨 CUDA 的實現(xiàn),重構(gòu) CuBLAS 以及 Blase 軟件包。
- 分布式計算。嘗試多個機器上傳播作業(yè)并彼此通信已至少 3 次,但無一成功。
- 更好地記錄做出某些決定的原因,并從宏觀上對 Gorgonia 進(jìn)行設(shè)計。
- 高階導(dǎo)數(shù)優(yōu)化算法(LBFGS)
- 無導(dǎo)數(shù)的優(yōu)化算法
十二、目標(biāo)
Gorgonia 的主要目標(biāo)是成為一個基于機器學(xué)習(xí)/圖形計算,能夠跨多臺機器進(jìn)行擴展的高性能庫。它應(yīng)將 Go(簡單的編譯和部署過程)的呼吁帶至機器學(xué)習(xí)領(lǐng)域。這條路還很漫長,然而我們已然邁出了第一步。
其次要目標(biāo)是為非標(biāo)準(zhǔn)的深度學(xué)習(xí)和神經(jīng)網(wǎng)絡(luò)相關(guān)事物提供一個探索平臺,其中包括 neo-hebbian 學(xué)習(xí)、角切割算法、進(jìn)化算法等。
顯然,由于你在 Github 上閱讀的可能性最大,Github 將構(gòu)建該軟件包工作流程的主要部分以完善該軟件包。
參見:CONTRIBUTING.md
十三、貢獻(xiàn)者與重要貢獻(xiàn)者
我們歡迎任何貢獻(xiàn)。但還有一類新的貢獻(xiàn)者,稱為重要貢獻(xiàn)者。
重要貢獻(xiàn)者是對庫的運作方式和/或其周圍環(huán)境有深刻理解的人。此處舉出重大貢獻(xiàn)者的例子:
- 撰寫了大量與特定功能/方法的原因/機制,及不同部分相互影響的方式的有關(guān)文檔
- 編寫代碼,并對 Gorgonia 更復(fù)雜的連接的部分進(jìn)行了測試
- 編寫代碼和測試,并且至少接受 5 個拉取請求
- 對軟件包的某些部分提供專家分析(比如你可能是優(yōu)化一個功能的浮點操作專家)
- 至少回答了 10 個支持性問題
重要貢獻(xiàn)者列表將每月更新一次(如果有人使用 Gorgonia)。
十四、如何獲得支持
如今最好的支持方式,便是在 Github 上留言。
十五、常見問題
為什么在測試中似乎出現(xiàn)了 runtime.GC() 的隨機調(diào)用?
答案非常簡單:軟件包的設(shè)計使其以特定的方式使用 CUDA:具體而言,一個 CUDA 設(shè)備及其景況會綁定一個虛擬內(nèi)存,而不是軟件包。這意味著對于每個創(chuàng)建的虛擬內(nèi)存,其每一設(shè)備每一個虛擬內(nèi)存都會創(chuàng)建不同的 CUDA 景況。因此在其他可能正在使用 CUDA 的應(yīng)用程序中,所有操作都能夠正常運行(然而這需要進(jìn)行壓力測試)。
CUDA 的景況只有在虛擬內(nèi)存回收垃圾(經(jīng)由終結(jié)器函數(shù)的幫助)時才會被銷毀。在測試中大約會創(chuàng)建 100 個虛擬內(nèi)存,并且大多數(shù)垃圾回收是隨機的;當(dāng)景況被使用過多時,會導(dǎo)致 GPU 內(nèi)存耗盡。
因此,在任何可能使用 GPU 的測試結(jié)束時,會調(diào)用 runtime.GC() 來強制垃圾回收,以釋放 GPU 內(nèi)存。
人們在生產(chǎn)過程中不太可能啟動過多的虛擬內(nèi)存,因此這并不是問題。若有問題,請在 Github 上留言,我們會想辦法為虛擬內(nèi)存添加一個 Finish() 方法。
十六、許可
Gorgonia 根據(jù) Apache 2.0 的變體授權(quán)。其所有意圖與目的 Apache 2.0 的許可相同,除了重要貢獻(xiàn)者(如軟件包的商業(yè)支持者),其他人均不能直接從中獲得商業(yè)利潤。但從 Gorgonia 的衍生直接獲利是可行的(如在產(chǎn)品中使用 Gorgonia 作為庫)。所有人都可將 Gorgonia 用于商業(yè)目的(如用于業(yè)務(wù)軟件)。
各類其他版權(quán)聲明
這是在寫 Gorgonia 的過程中有所啟發(fā)和進(jìn)行改編的軟件包和庫(使用的 Go 軟件包已經(jīng)在上文做出了聲明):
【本文是51CTO專欄機構(gòu)機器之心的原創(chuàng)譯文,微信公眾號“機器之心( id: almosthuman2014)”】