用 Go 語言理解 Tensorflow

用 Go 語言理解 Tensorflow

Tensorflow 並非一個嚴格意義上的機器學習庫,它是一個使用圖來表示計算的通用計算庫。它的核心功能由 C++ 實現,經過封裝,能在各類不一樣的語言下運行。它的 Golang 版和 Python 版不一樣,Golang 版 Tensorflow 不只能讓你經過 Go 語言使用 Tensorflow,還能讓你理解 Tensorflow 的底層實現。javascript

封裝

根據官方說明,Tensorflow 開發者發佈瞭如下內容:前端

  • C++ 源碼:底層和高層的具體功能由 C ++ 源碼實現,它是真正 Tensorflow 的核心。java

  • Python 封裝與Python 庫:由 C++ 實現自動生成的封裝版本,經過這種方式咱們能夠直接用 Python 來調用 C++ 函數:這也是 numpy 的核心實現方式。node

    Python 庫經過將 Python 封裝版的各類調用結合起來,組成了各類廣爲人知的高層 API。react

  • Java 封裝android

  • Go 封裝ios

做爲一名 Gopher 而非一名 java 愛好者,我對 Go 封裝給予了極大的關注,但願瞭解其適用於何種任務。git

譯註,這裏說的」封裝「也有說法叫作」語言界面「github

Go 封裝

圖爲 Gopher(由 Takuya Ueda @tenntenn 建立,遵循 CC 3.0 協議)與 Tensorflow 的 Logo 結合在一塊兒。golang


首先要注意的是,代碼維護者本身也認可了,Go API 缺乏 Variable 支持,所以這個 API 僅用於使用訓練好的模型,而不能用於進行模型訓練。

在文檔 Installing Tensorflow for Go 中已經明確提到:

TensorFlow 爲 Go 編程提供了一些 API。這些 API 特別適合加載在 Python 中建立的模型,讓其在 Go 應用 中運行。

若是咱們對訓練機器學習模型沒興趣,那這個限制是 OK 的。

可是,若是你打算本身訓練模型,請看下面給的建議:

做爲一名 Gopher,請讓 Go 保持簡潔!使用 Python 去定義、訓練模型,在這以後你隨時均可以用 Go 來加載訓練好的模型!(意思就是他們懶得開發唄)

簡而言之,golang 版 tensorflow 能夠導入與定義常數圖(constant graph)。這個常數圖指的是在圖中沒有訓練過程,也沒有須要訓練的變量。

讓咱們用 Golang 深刻研究 Tensorflow 吧!首先建立咱們的第一個應用。

我建議讀者在閱讀下面的內容前,先準備好 Go 環境,以及編譯、安裝好 Tensorflow Go 版(編譯、安裝過程參考 README)。

理解 Tensorflow 的結構

先複習一下什麼是 Tensorflow 吧!(這是我我的的理解,和官網的有所不一樣)

TensorFlow™ 是一個採用數據流圖(data flow graphs),用於數值計算的開源軟件庫。節點(Nodes)在圖中表示數學操做,圖中的線(edges)則表示在節點間相互聯繫的多維數據數組,即張量(tensor)。

咱們能夠把 Tensorflow 看作一種相似於 SQL 的描述性語言,首先你得肯定你須要什麼數據,它會經過底層引擎(數據庫)分析你的查詢語句,檢查你的句法錯誤和語法錯誤,將查詢語句轉換爲私有語言表達式,進行優化以後運算得出計算結果。這樣,它能保證將正確的結果傳達給你。

所以,咱們不管使用什麼 API 實質上都是在描述一個圖。咱們將它放在 Session 中做爲求值的起點,這樣作肯定了這個圖將會在這個 Session 中運行。

瞭解這一點,咱們能夠試着定義一個計算操做的圖,並將其放在一個 Session 中進行求值。

API 文檔中明確告知了 tensorflow(簡稱 tf)包與 op 包中的可用方法列表。

在這個列表中咱們能夠看到,這兩個包中包含了一切咱們須要用來定義與評價圖的方法。

tf 包中包含了各類構建基礎結構的函數,例如 Graph(圖)。op 包是最重要的包,它包含了由 C++ 實現自動生成的綁定等功能。

如今,假設咱們要計算 AAA 與 xxx 的矩陣乘法:

我假定大家都熟悉 tensorflow 圖的定義,都瞭解 placeholder 並知道它們的工做原理。

下面的代碼是一位 Tensorflow Python 用戶第一次嘗試時會寫的代碼。讓咱們給這個文件取名爲 attempt1.go

package main

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

func main() {
    // 第一步:建立圖

    // 首先咱們須要在 Runtime 定義兩個 placeholder 進行佔位
    // 第一個 placeholder A 將會被一個 [2, 2] 的 interger 類型張量代替
    // 第二個 placeholder x 將會被一個 [2, 1] 的 interger 類型張量代替

    // 接下來咱們要計算 Y = Ax

    // 建立圖的第一個節點:讓這個空節點做爲圖的根
    root := op.NewScope()

    // 定義兩個 placeholder
    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 輸入的 op 節點
    product := op.MatMul(root, A, x)

    // 每次咱們傳遞一個域給一個操做的時候,
    // 咱們都要將操做放在在這個域下。
    // 如你所見,如今咱們已經有了一個空做用域(由 newScope)建立。這個空做用域
    // 是咱們圖的根,咱們能夠用「/」表示它。

    // 如今讓 tensorflow 按照咱們的定義創建圖吧。
    // 依據咱們定義的 scope 與 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())
    }

    // 爲了使用 placeholder,咱們須要建立傳入網絡的值的張量
    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'

等等,爲何會這樣呢?

問題很明顯。上面代碼裏出現了 2 個重名的「Placeholder」操做。

第 1 課:node IDs

每次在咱們調用方法定義一個操做的時候,無論他是否在以前被調用過,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}))複製代碼

咱們能夠驗證一下這個問題,看看程序是否建立了兩個不一樣的 placeholder 節點: print(a.name, b.name)

它打印出 Placeholder:0 Placeholder_1:0

這樣就清楚了,a placeholder 是 Placeholder:0b placeholder 是 Placeholder_1:0

可是在 Go 中,上面的程序會報錯,由於 Ax 都叫作 Placeholder。咱們能夠由此得出結論:

每次咱們調用定義操做的函數時,Go API 並不會自動生成新的名稱。所以,它的操做名是固定的,咱們無法修改。

提問時間:

  • 關於 Tensorflow 的架構咱們學到了什麼?

    圖中的每一個節點都必須有惟一的名稱。全部節點都是經過名稱進行辨認。

  • 節點名稱與定義操做符的名稱是否相同?

    是的,也可說節點名稱是操做符名稱的最後一段。

接下來讓咱們修復節點名稱重複的問題,來弄明白上面的第二個提問。

第 2 課:做用域

正如咱們所見,Python API 在定義操做時會自動建立新的名稱。若是研究底層會發現,Python API 調用了 C++ Scope 類中的 WithOpName 方法。

下面是該方法的文檔及特性,參考 scope.h

/// 返回新的做用域。全部在返回的做用域中的 op 都會被命名爲
/// <name>/<op_name>[_<suffix].
Scope WithOpName(const string& op_name) const;複製代碼

注意這個方法,返回一個做用域 Scope 來對節點進行命名,所以節點名稱事實上就是做用域 Scope

Scope 就是從根 /(空圖)追溯至 op_name完整路徑

WithOpName 方法在咱們嘗試添加一個有着相同的 /op_name 路徑的節點時,爲了不在相同做用域下有重複的節點,會爲其加上一個後綴 _<suffix><suffix> 是一個計數器)。

瞭解了以上內容,咱們能夠經過在 type Scope 中尋找 WithOpName 來解決重複節點名稱的問題。然而,Go tf API 中沒有這個方法。

若是查閱 type Scope 的文檔,咱們能夠看到惟一能返回新 Scope 的方法只有 SubScope(namespace string)

下面引用文檔中的內容:

SubScope 將會返回一個新的 Scope,這個 Scope 能確保全部的被加入圖中的操做都被放置在 ‘namespace’ 的命名空間下。若是這個命名空間和做用域中已經存在的命名空間衝突,將會給它加上後綴。

這種加後綴的衝突處理和 C++ 中的 WithOpName 方法不一樣WithOpName 是在操做名後面suffix,它們都在一樣的做用域內(例如 Placeholder 變成 Placeholder_1),而 Go 的 SubScope 是在做用域名稱後面suffix

這將致使這兩種方法會生成徹底不一樣的圖(節點在不一樣的做用域中了),可是它們的計算結果倒是同樣的。

讓咱們試着改一改 placeholder 定義,讓它們定義兩個不一樣的節點,而後打印 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)))複製代碼

改爲

// 在根定義域下定義兩個自定義域,命名爲 input。這樣
// 咱們就能在根定義域下擁有 input/ 和 input_1/ 兩個定義域了。
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 風格)二、改變操做名稱(咱們在 C++ 中能夠這麼作,Python 版會自動這麼作)

如今,咱們已經解決了節點命名重複的問題,可是如今咱們的控制檯中出現了另外一個問題:

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 的類型。

Value for attr ‘T’ of int64 is not in the list of allowed values: half, float, double, int32, complex64, complex128

上面這個列表是什麼?爲何咱們能計算 2 個 int32 矩陣的乘積卻不能計算 int64 的乘積?

下面咱們將解決這個問題。

第 3 課: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.複製代碼

這幾行代碼爲 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 的類型限制在了這個類型列表中。
tensorflow 教程中提到,當時模版 T 時,咱們須要對全部支持的重載運算在內核進行註冊。這個內核會使用 CUDA 方式引用 C/C++ 函數,進行併發執行。

MatMul 的做者多是出於如下 2 個緣由僅支持上述類型而將 int64 排除在外的:

  1. 疏忽:這個是有可能的,畢竟 Tensorflow 的做者也是人類呀!
  2. 爲了支持不能使用 int64 的設備,可能這個特性的內核實現不能在各類支持的硬件上運行。

回到咱們的問題中,已經很清楚如何解決問題了。咱們須要將 MatMul 支持類型的參數傳給它。

讓咱們建立 attempt3.go ,將全部 int64 的地方都改爲 int32

有一點須要注意:Go 封裝版 tf 有本身的一套類型,基本與 Go 自己的類型 1:1 相映射。當咱們要將值傳入圖中時,咱們必須遵循這種映射關係(例如定義 tf.Int32 類型的 placeholder 時要傳入 int32)。從圖中取值同理。

*tf.Tensor 類型將會返回一個張量 evaluation,它包含一個 Value() 方法,此方法將返回一個必須轉換爲正確類型的 interface{}(這是從圖的結構瞭解到的)。

運行 go run attempt3.go,獲得結果:

input/Placeholder input_1/Placeholder
[[210] [-210]]複製代碼

成功了!

下面是 attempt3 的完整代碼,你能夠編譯並運行它。(這是一個 Gist,若是你發現有啥能夠改進的話歡迎來gist.github.com/galeone/096…

package main                                        

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

func main() {                                       
    // 第一步:建立圖

    // 首先咱們須要在 Runtime 定義兩個 placeholder 進行佔位
    // 第一個 placeholder A 將會被一個 [2, 2] 的 interger 類型張量代替
    // 第二個 placeholder x 將會被一個 [2, 1] 的 interger 類型張量代替

    // 接下來咱們要計算 Y = Ax

    // 建立圖的第一個節點:讓這個空節點做爲圖的根
    root := op.NewScope()                       

    // 定義兩個 placeholder
    // 在根定義域下定義兩個自定義域,命名爲 input。這樣
    // 咱們就能在根定義域下擁有 input/ 和 input_1/ 兩個定義域了。
    A := op.Placeholder(root.SubScope("input"), tf.Int32, op.PlaceholderShape(tf.MakeShape(2, 2)))   
    x := op.Placeholder(root.SubScope("input"), tf.Int32, op.PlaceholderShape(tf.MakeShape(2, 1)))   
    fmt.Println(A.Op.Name(), x.Op.Name())       

    // 定義接受 A 與 x 輸入的 op 節點
    product := op.MatMul(root, A, x)            

    // 每次咱們傳遞一個域給一個操做的時候,
    // 咱們都要將操做放在在這個域下。
    // 如你所見,如今咱們已經有了一個空做用域(由 newScope)建立。這個空做用域
    // 是咱們圖的根,咱們能夠用「/」表示它。

    // 如今讓 tensorflow 按照咱們的定義創建圖吧。
    // 依據咱們定義的 scope 與 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())                  
    }                                           

    // 爲了使用 placeholder,咱們須要建立傳入網絡的值的張量 
    var matrix, column *tf.Tensor               

    // A = [ [1, 2], [-1, -2] ] 
    if matrix, err = tf.NewTensor([2][2]int32{{1, 2}, {-1, -2}}); err != nil {                       
        panic(err.Error())                  
    }                                           
    // x = [ [10], [100] ] 
    if column, err = tf.NewTensor([2][1]int32{{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().([][]int32))                                            
    }
}複製代碼

提問時間:

關於 Tensorflow 的架構咱們學到了什麼?

每一個操做都有本身的一組關聯內核。Tensorflow 是一種強類型的描述性語言,它不只遵循 C++ 類型規則,同時要求在 op 註冊時需定義好類型才能實現其功能。

總結

使用 Go 來定義與處理一個圖讓咱們可以更好地理解 Tensorflow 的底層結構。經過不斷地試錯,咱們最終解決了這個簡單的問題,一步一步地掌握了圖、節點以及類型系統的知識。

若是你以爲這篇文章有用,請點個贊或者分享給別人吧~


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索