基於Go語言來理解Tensorflow
Tensorflow並非一套特定機器學習庫——相反,其屬於一套通用型計算庫,負責利用圖形表達計算過程。其核心通過C++語言實現,同時亦繫結有多種其它語言。與Python繫結不同的是,Go程式語言繫結不僅允許使用者在Go環境當中使用TensorFlow,同時亦可幫助大家深入瞭解TensorFlow的內部運作原理。
\\什麼是繫結?
\\從官方說明的角度來看,TensorFlow的開發者們公佈了:
\\- C++原始碼:TensorFlow的真正核心,負責具體實現這套機器學習庫的各高/低層級操作。\\t
- Python繫結與Python庫:這些繫結由C++實現程式碼所自動生成,這意味著我們能夠藉此利用Python呼叫C++函式:舉例來說,我們可以藉此實現numpy。另外,這套庫還將呼叫與繫結相結合,旨在定義TensorFlow使用者們所熟知的各類高層級API。\\t
- Java繫結。\\t
- 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)\u0026amp; 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 \u0026amp; 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, \u0026amp;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架構的哪些結論? 一套圖形中的每個節點皆必須擁有一個惟一名稱。每個節點皆由其名稱作為標識。\\t
- 節點的名稱與用於定義該節點的運算名稱是否相同? 是的,或者更具體地講,節點名稱屬於運算名稱中的最後一部分。\
為了進一步澄清第二個問題,下面我們嘗試解決節點名稱重複問題。
\\結論二:範圍
\\如大家所見,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 \u0026lt;name\u0026gt;/\u0026lt;op_name\u0026gt;[_\u0026lt;suffix].\Scope WithOpName(const string\u0026amp; op_name) const;
\\大家可能已經注意到,此方法用於對節點進行命名以返回Scope,這意味著節點名稱實際上就是一個Scope。所謂Scope,即為一條由root /(空圖形)到op_name的完整路徑。
\\當我們嘗試新增一個擁有同樣從/到op_name路徑的節點時,WithOpName方法會相應新增一條_\u0026lt;suffix\u0026gt;字尾(其中\u0026lt;suffix\u0026gt;的為一個計數器),這意味著同一範圍之內可存在重複節點。
\\瞭解了這一點,為解決節點名稱重複的問題,我們顯然需要在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\\t
- 引數: a、b\\t
- 屬性(可選引數): transpose_a、transpose_b\\t
- 支援的模板T型別: half, float, double, int32, complex64, complex128\\t
- 輸出形式: 自動推斷\\t
- 文件\
這套巨集不會呼叫任何C++程式碼,但我們可以從中看到,在對一項運算進行定義時,即使使用一套模板,我們亦必須保證其中的T型別(或者屬性)存在於受支援型別列表當中。實際上,.Attr(\"T: {half, float, double, int32, complex64, complex128}\")屬性會將T型別約束為該列表當中的一個具體值。
\\正如教程當中所提到,即使是在使用模板T時,我們同樣需要面向各受支援過載明確進行核心註冊。此核心採用CUDA方式以引用以併發方式執行的各C/C++函式。
\\正因為如此,MatMul的作者決定僅支援以上列出的幾種型別,並將int64排除在外。其作出這一決定的理由有二:
\\- 用於監督:有可能是這樣,畢竟TensorFlow的作者仍然是人類!\\t
- 為了支援那些無法完全支援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框架的底層結構。而通過試錯法,我們亦得以一步步解決各個簡單問題,最終掌握與圖形、節點以及型別系統相關的重要知識。
\\\\感謝陳思對本文的審校。
\\給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ,@丁曉昀),微信(微訊號:InfoQChina)關注我們。
相關文章
- 基於 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