V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
darluc
V2EX  ›  Go 编程语言

使用 Go 语言学会 Tensorflow - 译

  •  1
     
  •   darluc · 2018-07-15 01:30:39 +08:00 · 2318 次点击
    这是一个创建于 2325 天前的主题,其中的信息可能已经有所发展或是发生改变。

    阅读全文

    Tensorflow 并不是一个专门用于机器学习的库,相反的,它是一个通用的用于图计算的库。它的核心部分是用 C++ 实现的,同时还有其它语言的接口库。Go 语言版本的接口库与 Python 版本的并不一样,它不仅有助于我们使用 Go 语言调用 Tensorflow,同时有助于我们了解 Tensorflow 的底层实现。

    接口库

    Tensorflow 官方发布的代码库包含:

    • C++ 源代码:Tensorflow 核心功能高层 & 底层操作的代码实现。
    • Python 接口库 & Python 功能库:接口库是通过 C++ 代码自动生成的,这样我们可以使用 Python 直接调用到 C++ 的方法:numpy 核心代码也是这样实现的。功能库则是对接口库方法的组合调用,它实现了大家所熟知的高层 API 接口。
    • Java 接口库
    • Go 接口库

    我作为一名 Go 开发者,且不是 Java 爱好者,很自然地选择了使用 Go 版本的接口库,研究它能完成哪些任务。

    Go 接口库

    首件值得注意的事,正如它的维护者们承认的,就是 Go 接口库缺少对 变量 支持:这些接口被设计成用于使用训练好的模型,而不是从零开始训练模型。这在 Installing Tensorflow for Go 中交待得很清楚。

    Tensorflow 提供了 Go 程序接口。这些接口特别适于加载 Python 库所创建的模型,然后在 Go 应用中执行。

    如果我们对于训练机器学习模型不那么感兴趣:那就恰好!不过,若你对训练模型感兴趣的话,这里有一点建议:

    作为一名真正的 Go 爱好者,应当寻求便宜之道!请使用 Python 来定义和训练模型;之后,你总是能用 Go 来加载并使用它们的。

    简言之:Go 接口库可以用来导入并定义常量图;这里说的「常量」是指没有训练过程参与,所以没有可用于训练的变量。

    让我们立刻开始用 Go 来调用 Tensorflow:创建我们的第一个应用程序。

    接下来,我假设你们已经安装了 Go 环境,并且已经按照 README 编译并安装了 Tensorflow 的接口库。

    理解 Tensorflow 的数据结构

    我要在这里重申一下 Tensorflow 的定义(我为大家从 Tensorflow 站点的说明中划出了重点):

    TensorFlow™ 是一个使用数据流图进行数值计算的开源软件库。图中的节点代表数学操作,而图中的边则代表节点间相互联系的多维数据数组(张量)。

    我们可以把 Tensorflow 看作是一种描述性语言,类似于 SQL,你可以用它描述你的需求,让底层引擎(数据库)解析你的 query 语句,检查语法和语义错误,将其转化为它的内部描述,优化并计算出结果:最后返回给你正确的结果。

    所以,我们使用 API 接口时,实际是在描述一个图:当我们将它放入一个 Session 中,并且开始 Run 时,图的求值过程就开始了。

    理解这些之后,让我们尝试定义一个计算图,并且在一个 Session 中计算它。API 文档能为我们清楚地提供 tensorflow (缩写 tf )和 op 包的方法列表。

    如你所见,这两个包包含了我们对图进行定义和计算所需要的一切。

    前一个包包含了构建类似 Graph 本身等基础「空」结构的方法,后一个则是最重要的包,它包含了从 C++ 实现里自动生成的接口方法。

    假设我们想要计算矩阵 A 和 x 的乘积:

    $ A = \begin{pmatrix}1 & 2\\-1 &-2\end{pmatrix}, x = \begin{pmatrix}10\\100\end{pmatrix} $

    我假设读者已经知道 tensorflow 图定义的概念,知道什么是占位符而且知道它们如何工作。下面的代码是用户第一次使用 Python 接口时可能会做的尝试。我们将其命名为 attempt1.go

    package main
    
    import (
    	"fmt"
    	tf "github.com/tensorflow/tensorflow/tensorflow/go"
    	"github.com/tensorflow/tensorflow/tensorflow/go/op"
    )
    
    func main() {
    	// 让我们描述我们的需求:创建图
    
    	// 我们想要定义两个运行时使用的 placeholder
    	// 第一个 placeholder A 是 [2, 2] 整数张量
    	// 第二个 placeholder 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 通过我们的定义来构建图。
    	// 实体的图是通过我们用域和操作组合起来定义的“抽象”图生成的。
    
    	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())
    	}
    
    	// 为了使用占位符,我们必须创建含有数值的张量传入网络中
    	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))
    	}
    }
    

    代码内的注释非常丰富,请大家仔细阅读每行注释。

    如果是 Python 版 Tensorflow 的使用者,现在已经可以期待代码编译后能完美运行了。我们看看是否能如愿呢:

    go run attempt1.go

    会得到如下结果:

    panic: failed to add operation "Placeholder": Duplicate node name in graph: 'Placeholder'

    稍等,这里发生了什么?错误提示很明显,有两个同名的占位符都叫作“ PlaceHolder “。

    第一课:节点 ID

    使用 Python 接口时,每当我们调用定义操作的方法时,无论它是否已经被调用过,都会生成不同的节点。下面的代码就会很顺利的返回结果 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 版本里,则不同,之前程序就因为 Ax 都叫作 Placeholder 而导致运行失败。我们可以总结如下:

    Go 语言版 API 接口每次在我们调用定义操作的方法时,不会自动为节点生成新的名称:操作名称是固定的,而且我们没法改变它。

    问答时间:

    • 关于 Tensorflow 系统我们学到了什么?对于一个图来说,它的每一个节点都必须有唯一的名称。节点是以各自的名字来区分的。
    • 节点名称是否与定义它的操作名称相同?是的,更确切地讲,不完全是,只是名称的结尾部分相同。

    为了说明第二个答案,让我们来修复节点的重名问题。

    第二课:作用域

    正如我们刚才看到,Python 版的 API 接口会在每次定义操作时,自动生成一个新的名字。从底层实现来看,Python 接口调用了 C++ 的 Scope 类的 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 类型部分我们看到唯一能够返回一个新的 Scope 的方法是 SubScope(namespace string)

    引用文档如下:

    调用 SubScope 方法会返回一个新的 Scope,使得所有加入图中的操作都被置于命名空间 ‘ namespace ’ 中。如果命名空间与作用域中已有的命名空间重名,则会加上后缀。

    使用后缀进行冲突管理与在 C++ 中使用 WithOpName 方法不同WithOpName 在同一个作用域内的操作名称后加上 suffix 后缀(这样 Placeholder 就变成了 Placeholder_1 ),而 Go 使用的 SubScope 的方法则是对作用域名称增加后缀名 suffix

    这点差异会产生完全不同的图,不过尽管不同(节点放在不同的作用域中),从计算角度看它们是等价的。

    让我们修改一下占位符的定义过程,定义两个不同的节点,然后打印出 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 系统我们学到了什么?一个节点可由它被定义的作用域所区分。作用域就是从图的根节点直到操作节点的路径。有两种方式可以定义执行相同操作的节点:在不同的作用域中定义操作( 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 矩阵相乘!看起来 int64MatMul 唯一不能接受的参数类型。

    属性 ‘ T ’ 的取值 int64,不在允许的列表中:half,float,double,int32,complex32,complex64,complex128

    这是什么列表?为什么我们可以将两个 int32 类型的矩阵相乘却不支持 int64 类型?

    让我们继续研究这个问题,搞清楚到底发生了什么。

    阅读全文

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   6003 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 03:08 · PVG 11:08 · LAX 19:08 · JFK 22:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.