基於Go語言來理解Tensorflow

weixin_33763244發表於2017-06-06

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繫結說明

\\

03887aabd31fd1f6b97f2260bc3fb652.png

\\

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之間的矩陣乘法,其中:

\\

5050fa9b83475cc4e0e82834f5403b73.png

\\

這裡,假定大家已經熟悉了張量圖的定義方式,並清楚瞭解佔位符的概念及其實際作用。以下程式碼為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排除在外。其作出這一決定的理由有二:

\\
  1. 用於監督:有可能是這樣,畢竟TensorFlow的作者仍然是人類!\\t
  2. 為了支援那些無法完全支援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框架的底層結構。而通過試錯法,我們亦得以一步步解決各個簡單問題,最終掌握與圖形、節點以及型別系統相關的重要知識。

\\

檢視英文連結:https://pgaleone.eu/tensorflow/go/2017/05/29/understanding-tensorflow-using-go/?from=timeline\u0026amp;isappinstalled=0

\\

感謝陳思對本文的審校。

\\

給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ@丁曉昀),微信(微訊號:InfoQChina)關注我們。

相關文章