原文:https://pgaleone.eu/tensorflow/go/2017/05/29/understanding-tensorflow-using-go/

Tensorflow 并不是一个严格意义上的机器学习库,它是一个使用图来表示计算的通用计算库。它的核心功能由 C++ 实现,通过封装,能在各种不同的语言下运行。它的 Golang 版和 Python 版不同,Golang 版 Tensorflow 不仅能让你通过 Go 语言使用 Tensorflow,还能让你理解 Tensorflow 的底层实现。

绑定(The bindings)

根据官方说明,Tensorflow 的开发者发布了以下内容:

  • C++ 源码:真正的 Tensorflow 的核心,具体实现了 Tensorflow 高级和低级操作。
  • Python 绑定和 Python 库:绑定是由 C++ 实现自动生成的,通过这种方式,我们可以使用Python 调用 C++ 函数:例如,这就是实现 numpy 核心的方法。此外,这个库组合了对绑定的调用,以便定义每个使用 Tensorflow 的人都非常熟悉的高级 API。
  • Java 绑定
  • Go 绑定

注:个人觉得 binding 不应该翻译为封装,面向对象常说的封装的英文是 Encapsulation。因此在这里选择直译绑定。

作为一名 Gopher 而非一名 java 爱好者,我对 Go 封装给予了极大的关注,希望了解其适用于何种任务。

Go 绑定(The Go bindings)


图为 Gopher(由 Takuya Ueda @tenntenn 创建,遵循 CC 3.0 协议)与 Tensorflow 的 Logo 结合在一起。

TensorFlow 提供了用于 Go 程序的 API。这些 API 特别适合于加载用 Python 创建的模型并在 Go 应用程序中执行它们。

如果我们对训练机器学习模型没兴趣,完全没有问题。但是,如果你打算自己训练模型,请看下面给的建议:

作为一名 Gopher,请让 Go 保持简洁!使用 Python 去定义、训练模型,在这之后你随时都可以用 Go 来加载训练好的模型!

简而言之,Go 版 tensorflow 可以导入与定义常数图(constant graph)。这个常数图指的是在图中没有训练过程,也没有需要训练的变量。

让我们用 Golang 深入研究 Tensorflow 吧!首先创建我们的第一个应用。

我建议读者在阅读下面的内容前,先准备好 Go 环境,以及编译、安装好 Tensorflow Go 版(编译、安装过程参考 README.md)。

注:之前的博文中有详细的安装过程,仅供参考。

理解 Tensorflow 架构 (Understand Tensorflow structure)

先回顾一下什么是 Tensorflow 吧!(这是我个人的理解,和官网的有所不同)

TensorFlow™ 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。

我们可以把 Tensorflow 看做一种类似于 SQL 的描述性语言,首先你得确定你需要什么数据,它会通过底层引擎(数据库)分析你的查询语句,检查你的句法错误和语法错误,将查询语句转换为私有语言表达式,进行优化之后运算得出计算结果。这样,它能保证将正确的结果传达给你。

因此,我们无论使用什么 API 实质上都是在描述一个图。我们将它放在 Session 中作为求值的起点,这样做确定了这个图将会在这个 Session 中运行。

了解这一点,我们可以试着定义一个计算操作的图,并将其放在一个 Session 中进行求值。

API 文档 中明确告知了 tensorflow(简称 tf)包与 op 包中的可用方法列表。

正如我们所看到的,这两个包有定义和计算图形所需的所有内容。

tf 包中包含了各种构建基础结构的函数,例如 Graph(图)。op 包是最重要的包,它包含了由 C++ 实现自动生成的绑定等功能。

现在,假设我们要计算 AAA 与 xxx 的矩阵乘法:
A=(12−1−2),x=(10100)A= \begin{pmatrix} 1 & 2 \\ -1 & -2 \end{pmatrix} , x= \begin{pmatrix} 10 \\ 100 \end{pmatrix} A=(1−1​2−2​),x=(10100​)

我想读者已经熟悉了 tensorflow 图的定义思想,并且知道 placeholder 以及其是如何工作的。

下面的代码是 Tensorflow Python 绑定用户的第一次尝试。让我们把这个文件称为attempt1.go。

package mainimport ("fmt""github.com/tensorflow/tensorflow/tensorflow/go/op"tf "github.com/tensorflow/tensorflow/tensorflow/go"
)func main() {// 第一步:创建图// 首先我们需要在 Runtime 定义两个 placeholder 进行占位// 第一个 placeholder A 将会被一个 [2, 2] 的 interger 类型张量代替// 第二个 placeholder x 将会被一个 [2, 1] 的 interger 类型张量代替// 接下来我们要计算 Y = Ax// 创建图的第一个节点:让这个空节点作为图的根root := op.NewScope()// 定义两个 placeholderA := 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.Sessionsess, 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.Tensorif 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' (Stacktrace: goroutine 1 [running]:
runtime/debug.Stack(0x24, 0x4e4a728, 0x404c160)

等等,为什么会这样呢?问题很明显。上面代码里出现了 2 个重名的 “Placeholder” 操作。

第一课:节点 ID(Lesson 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 placeholderPlaceholder:0 ,而 b placeholderPlaceholder_1:0

但是在 Go 中,上面的程序会报错,因为 Ax 都叫做 Placeholder。我们可以由此得出结论:

每次我们调用定义操作的函数时,Go API 并不会自动生成新的名称。因此,它的操作名是固定的,我们没法修改。

提问时间:

  • 关于 Tensorflow 的架构我们学到了什么?图中的每个节点都必须有唯一的名称。所有节点都是通过名称进行辨认。
  • 节点名称与定义操作符的名称是否相同?是的,也可说节点名称是操作符名称的最后一段。

为了弄清楚第二个答案,让我们解决重复节点名称的问题。

第 2 课:作用域 (Lesson 2: Scoping)

正如我们所见,Python API 在定义操作时会自动创建新的名称。如果研究底层会发现,Python API 调用了 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 的完整路径。

为了避免在相同作用域下有重复的节点,WithOpName 方法会在我们尝试添加一个有着相同的 /op_name 路径的节点时,为其加上一个后缀 _<suffix><suffix> 是一个计数器)。

了解了这一点,为了解决重复节点名的问题,我们希望在 type Scope 中找到 WithOpName 方法。遗憾的是,目前 Go tf API 中还没有这种方法。

当查看 type Scope 的文档,我们可以看到返回新 Scope 的惟一方法是 SubScope(namespace string)

引用的文档中的内容:

SubScope 会返回一个 Scope ,该 Scope 将会使添加到图中的所有操作使用“namespace”这个命名空间。如果这个命名空间与范围内的现有命名空间冲突,则添加后缀。

这种加后缀的冲突处理和 C++ 中的 WithOpName 方法不同,C++ 中的 WithOpName 是在操作名后面加suffix,它们都在同样的作用域内(例如 Placeholder 变成 Placeholder_1),而 Go 的 SubScope 是在作用域名称后面加 suffix

这种不同会致使生成完全不同的图,尽管不同(节点被放置在不同的 Scope),但是从计算上来说,它们是等价的。

尝试修改 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 的架构我们学到了什么?

节点完全由其定义所在的 Scope 标识。这个 Scope 是我们从图的根节点追溯到指定节点的一条路径。有两种方法来定义执行同一种操作的节点:1、将其定义放在不同的作用域中(Go 风格)2、改变操作名称(我们在 C++ 中可以这么做,Python 版会自动这么做)

至此解决了节点命名重复的问题,但是现在控制台中出现了一个新的问题,如下所示:

panic: failed to add operation "MatMul": Value for attr 'T' of int64 is not in the list of allowed values: bfloat16, half, float, double, int32, complex64, complex128; NodeDef: {{node MatMul}} = MatMul[T=DT_INT64, transpose_a=false, transpose_b=false](input/Placeholder, input_1/Placeholder); Op<name=MatMul; signature=a:T, b:T -> product:T; attr=transpose_a:bool,default=false; attr=transpose_b:bool,default=false; attr=T:type,allowed=[DT_BFLOAT16, DT_HALF, DT_FLOAT, DT_DOUBLE, DT_INT32, DT_COMPLEX64, DT_COMPLEX128]> (Stacktrace: goroutine 1 [running]:
runtime/debug.Stack(0xc42000c0f0, 0x40636bf, 0x4)

为什么 MatMul 节点的定义出错了?我们要做的仅仅是计算两个 tf.int64 矩阵的乘积而已!看问题描述似乎是 MatMul 不能接受 int64 的类型。

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

上面这个列表是什么?为什么能计算 2 个 int32 矩阵的乘积却不能计算 int64 的乘积?

下面将解决这个问题。

第 3 课:Tensorflow 类型系统(Lesson 3: Tensorflow typing system)

深入研究一下 源代码 来看 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
  • 参数: ab
  • 属性(可选参数): transpose_atranspose_b
  • 模版 T 支持的类型: halffloatdoubleint32complex64complex128
  • 输出类型: 自动识别
  • 文档

这个宏没有包含任何 C++ 代码,但是它告诉了我们当在定义一个操作的时候,即使它使用模版定义,我们也需要指定特定类型 T 支持的类型(或属性)列表。

实际上,属性 .Attr("T: {half, float, double, int32, complex64, complex128}")T 的类型限制在了这个类型列表中。

tensorflow 教程 中有提到,当使用模版 T 时,我们需要对所有支持的重载运算在内核中进行注册。内核会使用 CUDA 的方式引用 C/C++ 函数,进行并发执行。

或许 MatMul 的作者可能是出于以下的两个原因,才使支持的类型把 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

完全正确!

下面是 attempt3.go 文件的完整代码,你可以编译并运行它。(还请记住,这是一个Gist,如果您看到一些可以改进的地方,您可以提供一些帮助)。

package mainimport ("fmt""github.com/tensorflow/tensorflow/tensorflow/go/op"tf "github.com/tensorflow/tensorflow/tensorflow/go"
)func main() {// 第一步:创建图// 首先我们需要在 Runtime 定义两个 placeholder 进行占位// 第一个 placeholder A 将会被一个 [2, 2] 的 interger 类型张量代替// 第二个 placeholder x 将会被一个 [2, 1] 的 interger 类型张量代替// 接下来我们要计算 Y = Ax// 创建图的第一个节点:让这个空节点作为图的根root := op.NewScope()// 在根定义域下定义两个自定义域,命名为 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.Sessionsess, 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.Tensorif 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 来定义与处理一个图(graph)可以让我们能够更好地理解 Tensorflow 的底层结构。通过不断地试错,最终解决了这个看似简单的问题,一步一步地掌握了图(graph)、节点(node)以及类型系统(the typing system)的知识。

由于个人理解的不同,虽参考了现有的译文,但还是针对一些点使用了自己的翻译,不足之处,还望谅解!

用 Go 语言理解 Tensorflow相关推荐

  1. 基于Go语言来理解Tensorflow

    Tensorflow并非一套特定机器学习库--相反,其属于一套通用型计算库,负责利用图形表达计算过程.其核心通过C++语言实现,同时亦绑定有多种其它语言.与Python绑定不同的是,Go编程语言绑定不 ...

  2. 使用Go语言来理解Tensorflow

    原文:Understanding Tensorflow using Go 作者:P. Galeone 翻译:雁惊寒 译者注:本文通过一个简单的Go绑定实例,让读者一步一步地学习到Tensorflow有 ...

  3. 【工大SCIR】基于动态图交互网络的多意图口语语言理解框架

    论文名称:AGIF: An AdaptiveGraph-Interactive Framework for Joint Multiple Intent Detection and SlotFillin ...

  4. NLPer福利-中文语言理解基准测【CLUEbenchmark】

    NLPer福利-中文语言理解基准测[CLUEbenchmark] 公众号:ChallengeHub 官方链接:https://www.cluebenchmarks.com Github链接:https ...

  5. BERT论文翻译:用于语言理解的深度双向Transformer的预训练

    Jacob Devlin Ming-Wei Chang Kenton Lee kristina Toutanova Google AI Language {jacobdevlin, mingweich ...

  6. OpenAI NLP最新进展:通过无监督学习提升语言理解

    编译 | reason_W 编辑 | 明 明 出品 | AI科技大本营 [AI 科技大本营导读]近日,OpenAI 在其官方博客发文介绍了他们最新的自然语言处理(NLP)系统.这个系统是可扩展的.与任 ...

  7. 谁说GPT只擅长生成?清华研究力证:GPT语言理解能力不输BERT

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 作者|张倩.小舟 来源|机器之心 一直以来,GPT 模型的语言生成能 ...

  8. 超越谷歌BERT!依图推出预训练语言理解模型ConvBERT,入选NeurIPS 2020

    机器之心发布 机器之心编辑部 在本文中,本土独角兽依图科技提出了一个小而美的方案--ConvBERT,通过全新的注意力模块,仅用 1/10 的训练时间和 1/6 的参数就获得了跟 BERT 模型一样的 ...

  9. 《预训练周刊》第5期:智源等研究力证:GPT语言理解能力不输BERT、盘点NLP预训练「兵器谱」...

    No.05 智源社区 预训练组 预 训 练 研究 观点 资源 活动 关于周刊 超大规模预训练模型是当前人工智能领域研究的热点,为了帮助研究与工程人员了解这一领域的进展和资讯,智源社区整理了第5期< ...

最新文章

  1. 阿里云Ecs挂载云盘
  2. Java双大括号_什么是Java中的双BRACE初始化?
  3. WINDOWS基础 ---- 系统目录
  4. Gitee X Serverless Devs 邀你来“领赏”啦!
  5. 投资学习网课笔记(part5)--基金第五课
  6. linux vnc检查,检查Ubuntu VNC设置(避免远程登陆)
  7. 很多人都忽视了账号基建重要性
  8. 初步设计了一下视频工具合集的界面
  9. 【codevs1282】约瑟夫问题
  10. 如何用计算机寒假计划表,如何制定寒假学习计划表
  11. esp32之arduino配置下载提速
  12. c语言程序设计题答案,C语言程序设计习题(含答案).doc
  13. 华为路由器配置VRRP
  14. cwe_checker 二进制静态漏洞检测工具的安装与使用
  15. oracle数据库lpad,Oracle数据库之oracle中的decode的使用LPAD
  16. 软件工程(2018)结对编程第2次作业
  17. HEKA.FitMaster.v2.15(用来分析和测试那些通过Patchmaster或Pulse得
  18. 集成树模型系列之一——随机森林
  19. 第4章【思考与练习2】数据文件high-speed rail.csv存放着世界各国高速铁路的情况。对世界各国高铁的数据进行绘图分析。使用Basemap绘制地图及使用Pyecharts绘制地图。
  20. 【转】【青春励志】当幸福来敲门——我的考研故事

热门文章

  1. Python最热,PyTorch增速是TF的13倍:2019数据分析/机器学习工具调查发布
  2. Python的流程控制 - for序列
  3. SQL Server 2016新特性:Query Store
  4. ansible register when: result | succeeded when: item.rc != 0
  5. CentOS 7 程序自启动的问题
  6. java ADT生成带签名的apk
  7. Extranet MPLS ×××
  8. 柏堰工业园有做机器人的吗_合肥柏堰科技园推进机器人应用 促产业转型升级...
  9. serverless 框架_研发的未来在哪里?Serverless 云开发来了!
  10. Linux Kernel TCP/IP Stack — 协议栈收包处理流程