使用Go語言來理解Tensorflow

編程語言 Go語言 Python Java 科技優家 2017-06-08

作者:P. Galeone

翻譯:雁驚寒

譯者注:本文通過一個簡單的Go綁定實例,讓讀者一步一步地學習到Tensorflow有關ID、作用域、類型等方面的知識。以下是譯文。

Tensorflow並不是機器學習方面專用的庫,而是一個使用圖來表示計算的通用計算庫。它的核心是用C++實現的,並且還有不同語言的綁定。Go語言綁定是一個非常有用的工具,它與Python綁定不同,用戶不僅可以通過Go語言使用Tensorflow,還可以瞭解Tensorflow的底層實現。

綁定

Tensorflow的開發者正式發佈了:

  • C++源代碼:真正的Tensorflow核心,實現了具體的高級和低級操作。
  • Python綁定和Python庫:這個綁定是由C++實現自動生成的,這樣我們可以使用Python來調用C++函數。此外,這個庫將調用融合到了綁定中,以便定義更高級別的API。
  • Java綁定。
  • Go綁定。

作為一個Go開發者而不是一個Java愛好者,我開始關注Go綁定,以便了解他們創建了什麼樣的任務。

Go綁定

使用Go語言來理解Tensorflow

地鼠與Tensorflow的徽標

首先要注意的是,Go API缺少對Variable的支持:該API旨在使用已經訓練過的模型,而不是從頭開始訓練模型。安裝Tensorflow for Go的時候已經明確說明了:

TensorFlow提供了可用於Go程序的API。這些API特別適合於加載用Python創建並需要在Go程序中執行的模型。

如果我們對培訓ML模型不感興趣,萬歲!相反,如果你對培訓模型感興趣,那就有一個建議:

作為一個真正的Go開發者,保持簡單!使用Python定義並訓練模型;你可以隨時使用Go來加載並使用訓練過的模型!

簡而言之,go綁定可用於導入和定義常量圖;在這種情況下,常量指的是沒有經過訓練的過程,因此沒有可訓練的變量。

現在,開始用Go來深入學習Tensorflow吧:讓我們來創建第一個應用程序。

在下文中,我假設讀者已經準備好Go環境,並按照README中的說明編譯並安裝了Tensorflow綁定。

理解Tensorflow結構

讓我們來重複一下什麼是Tensorflow:

TensorFlow™是一款使用數據流圖進行數值計算的開源軟件庫。圖中的節點表示數學運算,而圖的邊表示在節點之間傳遞的多維數據數組(張量)。

我們可以把Tensorflow視為一種描述性語言,這有點像SQL,你可以在其中描述你想要的內容,並讓底層引擎(數據庫)解析你的查詢、檢查句法和語義錯誤、將其轉換為內部表示形式、進行優化並計算出結果:所有這一切都會給你正確的結果。

因此,當我們使用任何一個API時,我們真正做的是描述一個圖:當我們把圖放到Session中並顯式地在Session中運行圖時,圖的計算就開始了。

知道了這一點之後,讓我們試著來定義一個計算圖並在一個Session中進行計算吧。API文檔為我們提供了tensorflow(簡寫為 tf)和op包中所有方法的列表。

我們可以看到,這兩個包包含了我們需要定義和計算圖形的所有內容。

前者包含了構建一個基本的“空”結構(就像Graph本身)的功能,後者是包含由C++實現自動生成綁定的最重要的包。

然而,假設我們要計算A與x的矩陣乘法,其中

我假設讀者已經熟悉了tensorflow圖定義的基本思想,並且知道佔位符是什麼以及它們如何工作。下面的代碼是對Tensorflow Python綁定的第一次嘗試。我們來調用這個文件attempt1.go

package main

import (
    "fmt"
    tf "github.com/tensorflow/tensorflow/tensorflow/go"
    "github.com/tensorflow/tensorflow/tensorflow/go/op"
)

func main {
    // 這裡,我們打算要: 創建圖

    // 我們要定義兩個佔位符用於在運行的時候傳入
    // 第一個佔位符 A 將是一個 [2, 2] 的整數張量
    // 第二個佔位符 x 將是一個 [2, 1] 的整數張量

    // 然後,我們要計算 Y = Ax

    // 創建圖的第一個節點: 一個空的節點,位於圖的根
    root := op.NewScope

    // 定義兩個佔位符
    A := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 2)))
    x := op.Placeholder(root, tf.Int64, op.PlaceholderShape(tf.MakeShape(2, 1)))

    // 定義接收A和x作為輸入參數的操作節點
    product := op.MatMul(root, A, x)

    // 每次我們把一個`Scope`穿給操作符的時候,把操作放在作用域的下面。
    // 這樣,我們有了一個空的作用域(由NewScope創建):空的作用域是圖的根,因此可以用“/”來表示。

    // 現在,我們讓tensorflow按照我們的定義來創建圖。
    // 把作用域和OP結合起來,創建具體的圖。

    graph, err := root.Finalize
    if err != nil {
        // 這裡沒辦法處理這個錯誤:
        // 如果我們錯誤的定義了圖,我們必須手工修改這個定義。

        // 這就跟SQL查詢一樣:如果查詢語句在語法上有問題,我們只能重新寫
        panic(err.Error)
    }

    // 如果在這裡,圖在語法上是正確的。
    // 我們就可以把它放到一個Session裡,並執行。

    var sess *tf.Session
    sess, err = tf.NewSession(graph, &tf.SessionOptions{})
    if err != nil {
        panic(err.Error)
    }

    // 要使用佔位符,我們必須創建一個Tensors,這個Tensors包含要反饋到網絡的數值
    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”的操作。

第一節課: 節點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是從根 /(空的圖)到op_name的完整路徑

當我們嘗試添加一個具有與/到op_name相同路徑的節點時,WithOpName方法會添加一個後綴_<suffix>(其中<suffix>是一個計數器),因此它將成為同一範圍內的重複的節點。

知道了這一點之後,為了解決重複節點名的問題,我們期望在Scope類型中找到WithOpName方法。可悲的是,這種方法並不存在。

相反,查看Scope類型的文檔,我們可以看到唯一的一個方法:SubScope,它返回一個新的Scope。

文檔裡是這麼說的:

SubScope返回一個新的Scope,這將導致添加到圖中的所有操作都將以“namespace”為命名空間。如果命名空間與作用域內現有的命名空間衝突,則會添加一個後綴。

使用後綴的衝突管理與C++的WithOpName不同:WithOpName是在操作名之後添加後綴,但還是在同一作用域內(因此佔位符變為了Placeholder_1),而Go的SubScope是在作用域名稱後添加後綴。

這種差異會產生完全不同的圖,但它們在計算上是等效的。

我們來改變佔位符的定義,以此來定義兩個不同的節點,此外,我們來打印一下作用域的名稱。

讓我們創建文件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的架構,我們學到了什麼?節點完全是由被定義的作用域來標識的。作用域是我們從圖的根到達節點的路徑。有兩種定義節點的方法:在不同的作用域(Go語言)中定義操作或更改操作名稱。

我們解決了重複節點名稱的問題,但另一個問題顯示在我們的終端上。

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矩陣!從這段錯誤提示來看,int64是MatMul唯一不接受的類型。

int64類型的attr ‘T’的值不在允許的值列表中:half,float,double,int32,complex64,complex128

這個列表是什麼?為什麼我們可以做兩個int32類型矩陣的乘法,而不是int64?

我們來解決這個問題,瞭解為什麼會出現這種情況。

第三節課:Tensorflow的類型系統

我們來看一下源代碼,尋找MatMul操作的C++聲明:

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宏來聲明瞭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,把所有引用int64的行改為int32。

有一點需要注意:Go綁定有自己的一組類型,與Go的類型的一一對應。當我們將值輸入到圖中時,我們必須關注映射關係。從圖形中獲取值時,必須做同樣的事情。

執行go run attempt3.go。結果:

input/Placeholder input_1/Placeholder
[[210] [-210]]

萬歲!

提問時間

關於Tensorflow的架構,我們學到了什麼?每個操作都與自己的一組內核相關聯。被視為描述性語言的Tensorflow是一種強大的類型語言。它不僅要遵守C++類型規則,而且還要在op的註冊階段只實現某些指定類型的能力。

結論

使用Go來定義並執行一個圖,使我們有機會更好地瞭解Tensorflow的底層結構。使用試錯法,我們解決了這個簡單的問題,我們一步一步地學到了有關圖、節點和類型系統這些新東西。

相關推薦

推薦中...