基於 Go 語言來理解 Tensorflow
Tensorflow並非一套特定機器學習庫——相反,其屬於一套通用型計算庫,負責利用圖形表達計算過程。其核心通過C++語言實現,同時亦繫結有多種其它語言。與Python繫結不同的是,Go程式語言繫結不僅允許使用者在Go環境當中使用TensorFlow,同時亦可幫助大家深入瞭解TensorFlow的內部運作原理。
什麼是繫結?
從官方說明的角度來看,TensorFlow的開發者們公佈了:
- C++原始碼:TensorFlow的真正核心,負責具體實現這套機器學習庫的各高/低層級操作。
- Python繫結與Python庫:這些繫結由C++實現程式碼所自動生成,這意味著我們能夠藉此利用Python呼叫C++函式:舉例來說,我們可以藉此實現numpy。另外,這套庫還將呼叫與繫結相結合,旨在定義TensorFlow使用者們所熟知的各類高層級API。
- Java繫結。
- Go繫結。
作為Go的忠誠支持者,我當然對Go繫結給予了高度關注,希望瞭解其適用於支援哪些任務型別。
Go繫結說明
Gopher (由Takuya Ueda(@tenntenn)建立,基於Creative Commons 3.0 Attribution許可)
與TensorFlow Logo結合在一起。
首先需要強調的是用於進行自身維護的Go API缺少Variable支援能力:此API的設計目標在於使用經過訓練的模型,而非從零開始執行模型訓練。這一點在說明文件中的“Go語言環境下TensorFlow安裝”部分作出了明確提示:
TensorFlow提供多種可在Go程式設計中使用之API。這些API的主要作用在於載入由Python語言建立的模型,並在Go應用程式之內執行這些模型。
如果我們不關注機器學習模型的訓練,那麼這些API不會引發任何麻煩。但如果大家需要進行模型訓練,那麼請注意以下建議:
作為一位真正的Go語言支持者,請以簡單作為基本指導原則!使用Python以定義並訓練模型; 您始終可以載入經過訓練的模型並隨後在Go環境中加以使用。
簡而言之:Go繫結可用於匯入並定義常量圖; 在這裡的語境下,所謂常量是指不涉及任何訓練過程,因此不存在經過訓練的變數。
現在我們將利用Go語言深入探索TensorFlow世界:建立我們的第一款應用程式。
在接下來的內容中,我們假定大家已經擁有一套Go環境,並根據README文件中的講解對TensorFlow繫結進行了編譯與安裝。
瞭解TensorFlow結構
讓我們再次對TensorFlow的概念進行重申(當然,這裡是我個人總結出的概念,與TensorFlow網站中的描述有所不同):
TensorFlow™為一套開源軟體庫,負責利用資料流圖進行數值計算。圖形中的各個節點代表數學運算,而圖形邊緣則代表著各節點之間進行通訊的多維資料陣列(即張量)。
我們可以將TensorFlow視為一種描述性語言,其與SQL有點類似,大家可以在其中描述您所需要的內容,並由底層引擎(即資料庫)解析您的查詢、檢查語法與語義錯誤,將其轉換為專有表達、優化並得出計算結果:通過這一系列流程,我們將最終得出正確結果。
因此,在我們使用任何可用的API時,我們實際上是在對一個圖形進行描述:此圖形的評估起點始於我們將其放置於Session當中並明確決定在該會話內Run此圖形。
瞭解到這一點,接下來讓我們嘗試定義一個計算圖,並在一個Session當中對其進行評估。根據API說明文件的內容,我們可以明確找到tensorflow(簡稱為tf)& op軟體包之內的可用方法列表。
如大家所見,這兩個軟體包當中包含一切對圖形進行定義與評估所必需的要素。
前者包含構建基礎性“空”結構——例如Graph本身——所需要的函式,而後者則包含各類最為重要的包,薦為由C++實現程式碼所自動生成的繫結。
然而,假定我們需要計劃A與x之間的矩陣乘法,其中:
這裡,假定大家已經熟悉了張量圖的定義方式,並清楚瞭解佔位符的概念及其實際作用。以下程式碼為TensorFlow Python繫結使用者所作出的初步嘗試。我們在這裡將此檔案命名為attempt1.go
package main import ( "fmt" tf "github.com/tensorflow/tensorflow/tensorflow/go" "github.com/tensorflow/tensorflow/tensorflow/go/op") func main() { // Let's describe what we want: create the graph // We want to define two placeholder to fill at runtime // the first placeholder A will be a [2, 2] tensor of integers // the second placeholder x will be a [2, 1] tensor of intergers // Then we want to compute Y = Ax // Create the first node of the graph: an empty node, the root of our graph root := op.NewScope() // Define the 2 placeholders A := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 2))) x := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 1))) // Define the operation node that accepts A & x as inputs product := op.MatMul(root, A, x) // Every time we passed a `Scope` to an operation, we placed that // operation **under** that scope. // As you can see, we have an empty scope (created with NewScope): the empty scope // is the root of our graph and thus we denote it with "/". // Now we ask tensorflow to build the graph from our definition. // The concrete graph is created from the "abstract" graph we defined // using the combination of scope and op. graph, err := root.Finalize() if err != nil { // It's useless trying to handle this error in any way: // if we defined the graph wrongly we have to manually fix the definition. // It's like a SQL query: if the query is not syntactically valid // we have to rewrite it panic(err.Error()) } // If here: our graph is syntatically valid. // We can now place it within a Session and execute it. var sess *tf.Session sess, err = tf.NewSession(graph, &tf.SessionOptions{}) if err != nil { panic(err.Error()) } // In order to use placeholders, we have to create the Tensors // containing the values to feed into the network var matrix, column *tf.Tensor // A = [ [1, 2], [-1, -2] ] if matrix, err = tf.NewTensor([2][2]int64{ {1, 2}, {-1, -2} }); err != nil { panic(err.Error()) } // x = [ [10], [100] ] if column, err = tf.NewTensor([2][1]int64{ {10}, {100} }); err != nil { panic(err.Error()) } var results []*tf.Tensor if results, err = sess.Run(map[tf.Output]*tf.Tensor{ A: matrix, x: column, }, []tf.Output{product}, nil); err != nil { panic(err.Error()) } for _, result := range results { fmt.Println(result.Value().([][]int64)) }}
我們為以上程式碼編寫了詳盡的註釋,希望大家認真關注以提升理解效果。
現在,TensorFlow-Python使用者認為上述程式碼能夠順利完成編譯並確切起效。讓我們看看其判斷是否正確:
go run attempt1.go
下面來看得出的結果:
panic: failed to add operation "Placeholder": Duplicate node name in graph: 'Placeholder'
很明顯,這裡出現了問題。可以看到,同一“Placeholder”名稱之下存在兩個計算“Placeholder”。
結論一:節點ID
每當我們呼叫一項方法以定義一項運算時,Python API都會生成不同節點——無論此前該方法是否曾經接受過呼叫。事實上,以下程式碼能夠返回結果3,且不會引發任何問題。
import tensorflow as tf a = tf.placeholder(tf.int32, shape=()) b = tf.placeholder(tf.int32, shape=()) add = tf.add(a,b) sess = tf.InteractiveSession() print(sess.run(add, feed_dict={a: 1,b: 2}))
我們可以驗證此程式是否正確建立兩個節點並輸出其佔位符名稱: print(a.name, b.name)生成Placeholder:0 Placeholder_1:0。因此, b 佔位符為Placeholder_1:0 而a 佔位符為Placeholder:0。
不過在Go語言中,上述程式會發生錯誤,這是因為A與x皆會被稱為Placeholder。我們可以得出以下結論:
Go API不會在我們每次呼叫一項用於定義運算的函式時自動生成新的名稱:因此,運算名稱是固定的,意味著我們無法加以修改。
提問時間:
- 到現在,我們瞭解到關於TensorFlow架構的哪些結論? 一套圖形中的每個節點皆必須擁有一個惟一名稱。每個節點皆由其名稱作為標識。
- 節點的名稱與用於定義該節點的運算名稱是否相同? 是的,或者更具體地講,節點名稱屬於運算名稱中的最後一部分。
為了進一步澄清第二個問題,下面我們嘗試解決節點名稱重複問題。
結論二:範圍
如大家所見,Python API會在每次定義一項運算時自動建立一個新的名稱。著眼於底層,Python API會呼叫Scope類中的C++方法WithOpName。以下為scope.h當中列出的方法說明及其特徵:
/// Return a new scope. All ops created within the returned scope will have /// names of the form <name>/<op_name>[_<suffix]. Scope WithOpName(const string& op_name) const;
大家可能已經注意到,此方法用於對節點進行命名以返回Scope,這意味著節點名稱實際上就是一個Scope。所謂Scope,即為一條由root /(空圖形)到op_name的完整路徑。
當我們嘗試新增一個擁有同樣從/到op_name路徑的節點時,WithOpName方法會相應新增一條_<suffix>字尾(其中<suffix>的為一個計數器),這意味著同一範圍之內可存在重複節點。
瞭解了這一點,為解決節點名稱重複的問題,我們顯然需要在type Scope當中找到WithOpName方法。遺憾的是,此方法並不存在。
相反,通過查詢type Scope相關說明文件,我們發現惟一能夠返回新Scope的方法只有SubScope(namespace string)。
下面來看文件中的說明內容:
SubScope會返回一個新的Scope,此Scope負責確保全部被新增至圖形中的運算被命名為“namespace”。如果此名稱空間與範圍內的現有名稱空間相沖突,則為其新增一個字尾。
使用字尾的衝突管理機制與C++ WithOpName方法有所區別:WithOpName會在同一範圍內的運算名稱之後新增suffix(因此Placeholder會變為Placeholder_1); 而Go的SubScope會將suffix新增至範圍名稱之後。
這種差異意味著最終生成的圖形也將完全不同,然而這種圖形層面的區別(即將節點放置在不同範圍之下)並不會對計算結果造成任何改變——二者在計算上仍然等效。
下面我們變更該佔位符定義以定義兩個不同的節點,而後Print其Scope名稱。
我們通過變更以下程式碼行建立檔案attempt2.go:
A := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 2))) x := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 1)))
變更之後:
// define 2 subscopes of the root subscopes, called "input". In this // way we expect to have a input/ and a input_1/ scope under the root scope A := op.Placeholder(root.SubScope("input"), tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 2))) x := op.Placeholder(root.SubScope("input"), tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 1))) fmt.Println(A.Op.Name(), x.Op.Name())
照常對其進行編譯及執行: go run attempt2.go。結果如下所示:
input/Placeholder input_1/Placeholder
提問時間:
到現在,我們瞭解到關於TensorFlow架構的哪些結論? 一個節點完全由其定義所在的Scope負責標識。該範圍為一條路徑,我們利用其實現由圖形root到目標節點的追蹤。我們可以通過兩種節點定義方式確保其執行同樣的運算:在不同Scope當中定義該運算(Go風格)或者變更運算名稱(Python會自動執行這一操作,我們亦可在C++中以手動方式執行)。
到這裡,我們已經解決了節點命名重複的問題,但仍有另一個問題需要加以探討。
panic: failed to add operation "MatMul": Value for attr 'T' of int64 is not in the list of allowed values: half, float, double, int32, complex64, complex128
為何MatMul節點會出現定義錯誤?我們只是希望將兩項tf.int64指標相乘!看起來,MatMul似乎單單無法接受int64類指標。
int64的attr ‘T’值並不符合允許值的定義要求: half, float, double, int32, complex64, complex128
這裡列出的定義要求到底是什麼意思?為什麼我們能夠將兩項int32指標相乘,卻無法對兩項int64指標進行同樣的運算?
下面我們將逐步解決這個問題。
結論三:TensorFlow型別系統
下面我們著眼於原始碼內容,看看C++對MatMul運算作出的宣告:
REGISTER_OP("MatMul") .Input("a: T") .Input("b: T") .Output("product: T") .Attr("transpose_a: bool = false") .Attr("transpose_b: bool = false") .Attr("T: {half, float, double, int32, complex64, complex128}") .SetShapeFn(shape_inference::MatMulShape) .Doc(R"doc( Multiply the matrix "a" by the matrix "b". The inputs must be two-dimensional matrices and the inner dimension of "a" (after being transposed if transpose_a is true) must match the outer dimension of "b" (after being transposed if transposed_b is true). *Note*: The default kernel implementation for MatMul on GPUs uses cublas. transpose_a: If true, "a" is transposed before multiplication. transpose_b: If true, "b" is transposed before multiplication. )doc");
此行程式碼為MatMul運算定義了一個介面:具體來講,我們可以利用REGISTER_OP巨集對該運算作出以下描述:
- 名稱: MatMul
- 引數: a、b
- 屬性(可選引數): transpose_a、transpose_b
- 支援的模板T型別: half, float, double, int32, complex64, complex128
- 輸出形式: 自動推斷
- 文件
這套巨集不會呼叫任何C++程式碼,但我們可以從中看到,在對一項運算進行定義時,即使使用一套模板,我們亦必須保證其中的T型別(或者屬性)存在於受支援型別列表當中。實際上,.Attr(“T: {half, float, double, int32, complex64, complex128}”)屬性會將T型別約束為該列表當中的一個具體值。
正如教程當中所提到,即使是在使用模板T時,我們同樣需要面向各受支援過載明確進行核心註冊。此核心採用CUDA方式以引用以併發方式執行的各C/C++函式。
正因為如此,MatMul的作者決定僅支援以上列出的幾種型別,並將int64排除在外。其作出這一決定的理由有二:
- 用於監督:有可能是這樣,畢竟TensorFlow的作者仍然是人類!
- 為了支援那些無法完全支援int64運算的裝置——具體來講,一部分受支援硬體可能無法充分完成這類運算過程。
再回到問題身上來:現在解決辦法已經非常明確。我們需要將受支援型別的引數傳遞至MatMul處。
這裡我們建立attempt3.go以利用int32引用每一行程式碼中的int64。
這裡只需要注意一點:Go繫結擁有自己的一組型別,且其與Go型別(幾乎)屬於1:1對映關係。當我們將各值包饋送至圖形當中時,我們必須尊重這一原始對映關係(例如在定義tf.Int32佔位符時饋送Int32)。在從圖形中提取數值時同樣遵循此理。 返回自Tensor評估的*tf.Tensor型別擁有Value()方法,而此方法則返回一個必須被轉換為正確型別的interface{}(這一點已經在圖形架構當中有所體現)。
編譯並執行go run attempt3.go。結果如下:
input/Placeholder input_1/Placeholder [[210] [-210]]
萬歲!
到這裡,我們已經展示了完整的attempt3程式碼; 大家可以對其進行構建與執行(當然,如果發現了改進空間,您亦可為其作出貢獻)。
提問時間:
到現在,我們瞭解到關於TensorFlow架構的哪些結論? 每一項運算都擁有自己的一組關聯核心。作為一種描述性語言,TensorFlow屬於強型別語言。其不僅要求使用者遵守C++型別規則,同時亦要求在運算註冊階段指定特定型別方可實現功能。
總結
通過利用Go語言定義並執行圖形,我們得以更好地理解TensorFlow框架的底層結構。而通過試錯法,我們亦得以一步步解決各個簡單問題,最終掌握與圖形、節點以及型別系統相關的重要知識。
相關文章
- 基於Go語言來理解TensorflowGo
- 使用 Go 語言來理解 TensorflowGo
- 用 Go 語言理解 TensorflowGo
- TensorFlow支援Go語言了Go
- goweb,基於go語言API框架GoWebAPI框架
- Go語言基於go module方式管理包(package)GoPackage
- goweb,基於go語言的API框架GoWebAPI框架
- 基於go語言學習工廠模式Go模式
- Go 語言 Web tail -f 工具, 基於 WebSocketGoWebAI
- Go語言基礎Go
- 滴滴基於Go語言的DevOps重塑之路Godev
- 【工具】一款基於go語言的agentGo
- Go語言基礎-序言Go
- 【Go語言基礎】sliceGo
- 深入理解Go語言的sliceGo
- 基於go語言gin框架的web專案骨架Go框架Web
- 【Python】一款基於go語言的agentPythonGo
- [Go]Go 語言基礎拾遺(一)Go
- [06 Go語言基礎-包]Go
- Go語言基礎語法總結Go
- [go語言]-深入理解singleflightGo
- Go語言————1、初識GO語言Go
- 【搞定Go語言】第2天4:Go語言基礎之流程控制Go
- 基於 Web 的 Go 語言 IDE - Wide 1.5.2 釋出!WebGoIDE
- Go 語言學習路線來啦Go
- Go語言核心36講(Go語言基礎知識一)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識二)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識三)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識四)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識五)--學習筆記Go筆記
- Go語言核心36講(Go語言基礎知識六)--學習筆記Go筆記
- 基於課程學習(Curriculum Learning)的自然語言理解
- 從零開始——GO語言基礎語法Go
- GO語言Go
- Go 語言關於 Type Assertions 的 坑Go
- go語言基礎之——iota的用法Go
- Go語言:包管理基礎知識Go
- 十九、Go語言基礎之併發Go