用 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前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索