Chapter 3. TensorFlow Fundamentals
Introduction to Computation Graphs
本节涵盖了计算机图的基础知识,不涉及tensorflow上下文(context)。这其中包含有 节点、边、依赖,同时我们也提供了例子来说明比较重要的原则(key principles)。如果已经有计算图编程的经历或者对计算图(computation graphs)熟悉了可以跳过这一小节。
Graph basics
在Tensorflow编程中的核心就是使用Tensorflow API 在代码中表述计算图(computation graph)。 所谓的计算图就是指定数据类型的有向图。这个图是用户定义的计算结构。本质上(in essce),就是将一系列的函数链接到一起,每次传递任意个函数的输出到下一个链接中。使用这种方式我们可以较小的代码块来构建比较复杂的数据变换。例如我们大家都知道的(well-understood)数学函数。 接下来让我们看看赤裸裸的例子吧( bare-bones example)。
3-1在上图中的里, 我们看到了一个简单加法的有向图表示:圆圈代表这个加法函数, 两个输入值通过箭头指向了函数。输出的结果是1+2的和: 3。输出的结果可以传递到其他函数中,或者返回到客户端。
上图也可以使用一个函数来表示:
3-2以上的例子说明了如何使用节点(nodes)和边(edges)构建基本的图(graph)。让我们来看看他们(nodes\edges)的属性:
Nodes(节点): 一般情况下 会使用圆形、椭圆形或者矩形来表示对图形上下文中的数据进行某种计算或操作。在上述例子中,操作“add”就是图中唯一的节点。
Edges(边): 边往往是向节点(操作)中传递真实值或者从操作中传出的真实值。往往使用箭头来表示。在上面的加法例子中,输入的1和2都是由边来输入到节点中的, 而输出值3 是由边从节点中带出去的。从概念上讲(Conceptually),我们可以将边看作不同操作之间的链接,链接中携带着从一个节点到下一个节点的信息。
现在,这里有一个更有趣的例子:
3-3在这个图中有更多的事情!
所有的数据从左边传播到右边(传播的方向和箭头方向一致),所以我们从左到右一步一步的开始解释:
1、 在最先开始,我们看见有两个值流入(flowing into)图中,即 5和3。这两个值可能是来自不同的图中,也可能是来自一个文件中,或者是从客户端输入的。
2、 每一个值分别传递到了“输入(inputs)”节点,并且显示的初始化了输入节点,在图中标记为“a”和“b”。“输入”节点简单地传递给予它们的值 - 节点a接收值5并且将相同的数目输出到节点c和d,节点b接受了3之后,也是做相同的动作。
3、 节点c 是一个乘法操作。它分别(respectively)从节点a和b中接收值5和3, 并将结果15 输出到节点e 。然而节点d将相同的输入相加,并将结果8输出到节点e中。
4、 节点e是图中最后一个节点。最后,节点e接收了15和8,并相加。将最终的结果23 便是图中的输出结果。
上图可以用以下的等式表示:
如果想要知道当a=5 , b=3 的时候e的结果。 我们可以进行后推:
就是这样,结算完成! 这里有一些概念需要指出来:
* 使用“输入”节点的模式是有用的。因为它允许我们依赖将单个输入值中继到巨大数量的未来节点。 如果我们没有这样做,客户端(或者传递初始值的任何人)都必须将每个输入值显式传递到图形中的多个节点。 这样,客户端只需注意传递适当的值一次,并且这些输入的任何重复使用都被抽象掉。 我们稍后将更多地讨论抽象图表。
* 突击测验:那个节点最先运行?是先运行乘法节点c,还是加法节点d?
答案:我们也不知道。 从这个图中,不可能知道c和d中哪一个将首先执行。 一些人可能从左到右和从上到下读取图,并且简单地假设节点c将首先运行,但是重要的是注意,图可以容易地用c在c的顶部绘制。 其他人可能认为这些节点并发运行,但由于各种实现细节或硬件限制,这种情况也不是不会发生。 实际上,最好将它们视为彼此独立运行。 因为节点c不依赖于来自节点d的任何信息,所以它不必等待节点d做任何事情以完成其操作。 反之亦然:节点d不需要来自节点c的任何信息。 我们将在本章稍后讨论更多关于依赖。
接下来,稍微修改一下这个图:
主要有两个变化:
1、 含有3的输入节点b 现在可以直接将值传递到e中
2、 E节点的add函数替换成了sum函数。因为sum s是对两个以上的对象操作。
注意我们如何能够在节点之间添加一个边,似乎有其他节点“在路上”。通常情况下,任何节点可以将其输出传递到图中的任何一个未来节点,而不管在它们之间进行多少计算。如图所示:
3-4从以上的这两个图,我们可以看到抽象图的输入的好处。 我们可以操纵图形内部发生的细节,不过客户端只需要知道向相同的两个输入节点发送信息。同时可以进一步扩展这个抽象,并可以这样绘制我们的图。
3-5通过这样做,我们可以将整个节点序列看做具有输入和输出的离散结构快。这样可以更贱容易的将可视化链接在计算组里面,而不必担心每个片段的具体细节。
Dependencies(依赖)
此处需要说明的是 有一种类型的节点之间的链接是不允许创建的,最常见的就是创建无法解决的循环依赖性链接。(PS 其实就是离散数学里面的 闭环)。为了没解释环形依赖,我们准备先用实例解释什么是依赖。请在看这张图:
3-6依赖的概念如下: 任何节点A, 如果对于计算后来的B而言是必须的,那么A就称为B的依赖。如果节点A和B不需要任何来自对方的信息,那么可以称之为不相互依赖。
为了直观地表示这一点,让我们看看如果乘法节点c无法完成其计算(无论什么原因)会发生什么:
3-7设想一下,由于节点e 需要来自节点c的输出。所以e会等待c的数据。显而易见,节点e依赖于节点c和节点d。它们将信息直接馈送到最终加法函数中。然而,可能稍微不太明显看到输入a和b也是e的依赖。 如果其中一个输入无法将其数据传递到图中的下一个函数,会发生什么?
3-8正如你所见,删除其中一个输入会停止实际发生的大部分计算,这表明依赖具有传递性。也就是说,如果A依赖于B,并且B依赖于C,则A依赖于C。在这种情况下,最终节点e 就依赖于节点c和d,节点c,同时依赖于输入节点b。所以,最终的节点e是依赖于输入节点b。同理,节点e也依赖于输入节点a。除此以外,我们对依赖做出了区分:
1、 我们说e是直接的依赖与c和d。所以,这就意味着数据必须直接来自节点c和d,以便节点e执行。
2、 我们说节点e是间接的依赖于节点a和b。这意味着a和b的输出不直接馈送到节点e。a和b的值会馈送到中间的节点,该中间节点也是e的直接依赖或间接依赖。这意味着节点可以间接地依赖于具有在其间的许多层中介体的节点(并且那些中间体中的每一个也是依赖)。
最后,让我们看看如果我们将图形的输出重定向到它的早期部分会发生什么:
3-9好吧, 不幸的是它好像飞不起来(PS 这句话的意思是和tensor flow 有关的。原句是: Well, unfortunately it looks like that isn’t going to fly.)。现在我们尝试着将节点e的输出值传递给节点b,并且期望可以通过计算使得图循环。可现在的问题是,节点e已经是节点a的直接依赖,同时节点e又间接的依赖于节点b(之前有提到)。这就导致了无论是b还是e要执行,双方都要等待对方节点才能完成自己的计算。
3-10或许你很聪明的决定我们可以提供给初始状态将值送入节点b或者e中。毕竟这是我们的图, 可以给节点e设置初始值为1的输出嘛。这样就可启动图了。
以上是图前几个循环的状态。这个图创建了无线反馈循环, 同种的大多数趋近于无穷大。很好!然而,对于像Tensorflow这样的软件,这种类型的无限循环却很糟糕,有以下几个原因:
1、 由于这是一个无限循环,所以程序的交互不会很优雅的停止。
2、 依赖循环变的无穷大,这是因为每个后续迭代取决于所有先前的迭代。更糟糕的是,每个节点不会算作单个依赖: 因为每次输出改变值的时候都会重新计算。这使得我们不能跟踪依赖信息。这条是诸多原因中最重要的一条。
3、 通常情况下,你会遇到这种情况,传递的值爆炸成巨大的正数(他们最终会溢出),巨大的负数(你会最终下溢),或接近零(在这一点 每次迭代几乎没有额外的意义)。
基于以上的这些, tensroflow不会真正的执行循环依赖,这不是一件坏事情。在实际使用过程中,我们通过复制有限数量的图形的版本,并且将它们并排放置(placing them side-by-side),按顺序将它们相互馈送,来模拟这种类型的依赖。上述的过程通常称为“展开(unrolling)”图,会在以后的循环神经网络(recurrent neural networks)中涉及。
为了将展开的“图”可视化之后看起来像图形,我们将展开上述图形循环依赖5次之后的图形:
3-11如果你分析一下这个图,会发现节点和边的序列都是相同的(重复了5次)。注意观察原始输入值(用在图的顶部和底部跳跃的箭头表示)是如何获取并传递到每一个副本,因为它们都需要通过图去复制每次的迭代。通过展开我们的图像,可以模拟有用的循环依赖,同时保持确定性计算。
现在我们了解依赖关系,我们可以谈谈为什么跟踪它们是有用的。
想象一下(image for a moment),我们只想获得上一个例子中 节点C的输出(乘法计算的节点)。我们已经定义了整个图,其中包含有节点b,以及依赖与它的节点c,还有后面出现的节点e。那么,我们是不是要计算整个图,即使我们不需要节点d和e的值?
显然不需要。只需观察图,就可以看出,如果我们只想要c的输出,有没有必要浪费时间计算所有的节点。
那么现在问题来了: 我们如何确定计算机只计算必要的节点,而不手动的告诉它呢? 答案是: 使用依赖。
这背后的概念相当简单,我们只需要确保每个节点都有一个直接(非间接)依赖的节点列表。我们从一个空堆栈开始,这将最终保存我们想要运行的所有节点。从获取输出的节点开始。显然输出必须执行,所以我们将它添加到我们的堆栈。再看看输出节点的依赖关系列表 :这意味着这些节点必须运行以计算我们的输出,所以我们将它们全部添加到堆栈。现在我们查看所有这些节点,看看它们的直接依赖关系,并将它们添加到堆栈。我们继续这个模式一直回到图中,直到没有依赖性剩下来运行,这样我们保证我们有所有的节点我们需要运行图,只有那些节点。此外,堆栈将以一种方式排序,当我们遍历它时,我们保证能够运行堆栈中的每个节点。要注意的主要事情是跟踪已经计算的节点并将它们的值存储在内存中 -:这样我们不会反复对同一个节点再次进行计算。通过这样做,我们能够确保我们的计算尽可能精简,这可以节省大量图形上的处理时间。
PS: 上述部分《数据结构》中计算图的依赖关系。可以参看严蔚敏的教材。
Defining Computation Graphs in TensorFlow
在本书中会剖析多种多样复杂的机器学习模型。尽管在tensroflow中定义他们的过程很相似。当你深入了解各种数学概念并且学习如何实现并理解Tensroflow工作模式的时候,这会为你打良好的基础。很不错,这个工作流很简单,也容易记住。只有两步骤:
1、 定义计算图
2、 运行图(有数据)
显然,当图不存在的时候,你不能去运行图!但是这是一个重要的区别,因为当编写自己的代码,TensorFlow的功能的绝对量可能是压倒性的。通过一次只担心这个工作流程的一部分,它可以帮助您更周详地构建代码,并帮助指向下一步工作。
By worrying about only one portion of this workflow at a time, it can help you structure your code more thoughtfully as well as aide in pointing you towards the next thing to work on.
本节会关注于有关Tensorflow定义图的基础概念,下一节会创建并且运行图。最后,我们会将前两节结合在一起,并且显示我们如何创建在多个运行中改变并接收不同数据的图。
Building your first TensorFlow graph
这是一张我们很熟悉的图:
3-12现在看看代码是怎么写的:
3-13Ps: 上图代码我有所改动,感觉不够完美,故添加了两行用来指定python和编码方式.本书的源代码是:
import tensorflow as tf
a = tf.constant(5, name="input_a")
b = tf.constant(3, name="input_b")
c = tf.mul(a,b, name="mul_c")
d = tf.add(a,b, name="add_d")
e = tf.add(c,d, name="add_e")
来让我们一行一行的解释吧.首先注意是import 语句:
import tensorflow as tf
通常情况下会导入TensorFlow的类库并且给赋予一个别名tf。别名一般按照惯例用tf, 因为当我们在使用Tensorflow里的函数的时候, tf要比Tensorflow写起来简单的多。
接下来,再看两个变量赋值语句:
a = tf.constant(5, name="input_a")
b = tf.constant(3, name="input_b")
这就是我们定义的“input”节点,a和b。本行中使用了第一个Tensorflow的操作: tf.constant(). 在tensroflow中,任何在图中的计算几点被称为 操作(operation ),或者简称为Op。 Ops可以将0甚至是tensor对象作为输入和输出的。为了创建一个Operation , 你需要调用关联Python的构造函数,。而在本案例中,调用 tfs.constant() 函数会创建一个“常量( constant )”操作(Op)。该方法接受单一张量(Tensor)值,并且将该值输出到于其直接链接的节点上。为了方便起见,该函数会自动将标量数5和3转换为Tensor对象。我们也会传递一个可选的字符串参数name,用于标识创建的节点。
此时,不需要担心你是否完全的理解“操作(Operation)”和“张量(Tensor)对象”。接下来会有更加细致的讲解。
c = tf.mul(a,b, name="mul_c")
d = tf.add(a,b, name="add_d")
这里我们是在图中定义了另外两个节点, 这两个节点都会使用到前面定义的节点。节点C使用了tf.mul() 操作, 该操作可以将两个输入相乘的结果输出。同样的,节点d 使用的是tf.add() 操作,这个操作是会将两个输入相加,将相加的结果作为输出。我们再次给每个Ops传递name参数(以后这个参数会出现很多次)。请注意,我们不必独立于节点定义图的边缘 - --当你在TensorFlow中创建节点时,软件会为你绘制连接,包括操作需要计算的所有输入。
e = tf.add(c,d, name="add_e")
这是在图中定义的最后一个节点e。e使用的tf.add与节点d 相似。不过这次是将节点c和d作为输入了。如上图表中描述的一样。
这样一来,我们的第一个图已经定义完了,虽然图很小。 如果你在Python脚本或shell中执行上面的代码,它会运行,但是它实际上不会做任何事情。 记住,这只是过程的定义部分
为了简要了解运行图形的情况,我们可以在末尾添加以下两行以获得我们的图形,以输出最终节点:
sess = tf.Session()
sess.run(e)
如果你使用的是交互式的环境,例如 python 脚本或者是Jpyter、iPython Notebook 的话,你会看到正确的输出:
PS: 这里面已经说了是交互式的。这里用的是ipython 方式
>>> sess = tf.Session()
>>> sess.run(e)
23
现在已经足够了:我们实际上得到这个运行在实时代码!【兴奋。。。】
Exercise: Building a Basic Graph in TensorFlow
现在就可以做这个练习了。在这个练习中,你会编写自己的第一个Tensroflow 图, 运行它的各个部分,并且会第一次接触到难以置信的工具---TensorBoard。当你完成的时候,感觉使用很不错的体验感,同时也会建立了一个基本的Tensroflow 图。
现在我们要正式的在Tensorflow中定义图了! 确保你已经安装了Tensorflow,并且启动了一个你一直在使用的一个python 环境(Virtualenv, Conda, Docker) 。
P73
Thinking with tensors
在学习计算图形的基础知识的时候,简单的标量数是庞大,我们已经掌握了“flow(流)”,现在让我们熟悉“tensor(张量)”。(PS: 张量或许可以解决标量数据量庞大的问题)
正如前面所提到的,张量(tensor)只是n维矩阵的抽象。所以,1-D张量等同于向量(vector),2-D的张量等同于矩阵(matrix),基于以上,可以说是n维张量。基于以上几点,我们把之前例子中图修改成使用tensor的形式:
3-14现在,我们把两个分割的输入节点替换掉成单一节点,同时传入的是一个向量(vector,或者说是一维张量,1-D tensor)。本图会与之前的例子相比较而言,会有以下有点:
1、 客户端只需要把输入传递给一个单一节点即可,并且简化了图。
2、 具有直接依赖的节点现在只需要跟中一个依赖即可,而不是两个。
3、 如果你原意,可以选择图来包含任意长度的向量。这会使得图更加灵活。还可以使强制图的执行要求,并且强制输入长度为2 (或者是任意你想要的长度)。
我们通过修改之前的代码来实现这些变化:
3-15PS: 全文完整代码如下:
3-16除了调整了变量名称, 我们在这里还做了两个主要的改变:
1、 我们将两个分离的节点a和b合并成了一个节点(现在叫做节点a)。传递一个数字列表(list,数组),该list 可以被转化1D 张量。
2、 我们的乘法和加法操作现在使用的是tf.reduce_prod() 和 tf.reduce_sum() 方法,这些方法以前传递的是标量值,现在需要使用tensor作对输入, 获取tensor的所有的值,分别相乘或者求和。
在Tensorflow中,所有从节点传递到节点的数据都是张量(tensor)对象。正如我们所见,TensorFlow的操作可以看做标准的Python类型,例如 整型、字符串并且可以自动地转化为tensor。这里有很多方式可以创建Tensor对象。
在本书中,在讨论程序片段的时候可以用“tensor”和“Tensor”互换。(PS: 哈哈,还记得python 区分大小写么 :) )
Python Native Types
Tensorflow 可以传递python中的数字、布尔值、字符串或者是以上任何类型的列表。String 值会转化为0-D tensor(或者是标量),列表(list)会转化为一个1D tensor(向量、vector),列表的列表值会转化为2D tensor (矩阵、matrix)等等。此处给出一个小图表:
3-17TensorFlow data types
回到宿舍写
使用python类型可以快速且简单的去指定tensor 对象,这种原型设计理念很有用。但是,这样做有一个致命的缺陷。Tensorflow 拥有庞大的数据类型,但是python 的基本数据类型是很难明确的说明你想要什么类型的数据。实际上,Tensorflow是可以推断你想要的数据类型。对于一些像String一样的简单类型是可以这样做的,但是有一些是不能的。例如,在python 中所有的整型(integer)是同一个类型,但是在TensorFlow中有8-bit、32-bit、以及64-bit 的整型可以用。在将数据传递到TensorFlow时,有一些方法可以将数据转换为适当的类型,但某些数据类型仍然可能难以正确声明,例如复数。正是因为如此,通常会手动定义的Tensor对象作为NumPy数组。
NumPy array
TensorFlow 是和NumPy 紧密集成的,NumPy是用于n维数组科学计算的包(package)。如果你没有numpy 的编程经验,强烈建议你查看用于类库的丰富的教程和文档,Numpy已经成为科学语言的而一部分了。Tensorflow 里的语言是基于某些NumPy转化而来的,实际上np.int32 == tf.int32语句的返回结果是True! 任何NumPy数组都是可以转化为Tensorflow OP的,最美的是你只需要很小的代价就可以轻松地指定你需要的数据类型。
回到宿舍 翻译
STRING DATA TYPES
作为奖励,您可以在运行图形之前和之后使用numpy库的功能,因为从Session.run返回的张量是NumPy数组。 这里是一个如何创建NumPy数组的例子,映射上面的例子:
3-18虽然Tensorflow被设计为可以理解NumPy 类型的数据,但是反之并不是如此(PS: NumPy不会识别Tensorflow 数据)。不要去尝试着使用tf.int32 初始化Numpy 数组!
【注解】当然从技术的角度上将Numpy自然也是可以自动地识别数据类型,但是这最好是你想要把Tensor对象的显示数字属性。当你处理庞大的图形时候,最好不要想要寻找那些对象导致TypeMismatchError ! 还有在处理字符串的时候,不要在你创建字符串Tensor的时候指定dtype。
使用NumPy的时候建议手动指定Tensor对象。
Tensor shape
在TensorFlow类库中,你会经常的看到涉及有tensor 形状的函数和操作。在TensorFlow术语中,shape 不仅仅描述了张量(tensor)的数字维度,也描述每个维度的长度。Tensor的形状既可以是Python列表(lists),也可以指包含有序整数集的元组(tuples):列表中存在与维度一样多的数字,每个数字描述其对应维度的长度。例如,列表 [2,3] 描述的就是一个2D形状的tensor , 第一维度的长度是2, 第二维度的长度是3。注意 无论是元组(用一对()来包裹)还是列表(用一对[] 来包裹),都可以定义形状。 来,让我们看看过多的实例:
3-19除了能够为每个维指定固定长度之外,您还可以通过将None传递为维的值来分配灵活的长度。而且,将值None传递为形状(而不是使用包含None的列表/元组),将告诉TensorFlow允许任何形状的张量。也就是说,张量具有任何量的维度和每个维度的任何长度
如果你需要画出图形中间的张量形状,你可以使用tf.shape操作。这个操作只接收tensor对象,并返回一个inte32 的向量,这个向量就是你想要的形状。
3-20记住,tf.shape 和其他的操作一样,只有在一个Session(会话)里面的时候才会运行。
提醒
Tensors只是矩阵的超集!
TensorFlow operations
正如之前所说的,Tensorflow操作(也称为Ops)是执行Tensor对象上执行计算的节点。在计算完毕之后会返回0 或者是更多的Tensor,这些返回值可以被用图中其他的后续操作。为了创建操作对象(称之为Python构造器),需要传递Tensor计算所需要的参数(称之为输入),以及适当地创建Op所需的任何附加信息(称为属性)。Python构造器会返回个句柄被操作的输出(这个输出有可能是0 或者是多个Tensor对象),它的输出可以被传递到其他的操作上或者是Session.run:
3-21ZERO-INPUT, ZERO-OUTPUT OPERATIONS
P87
3-22
除了输入和属性以外,每一个操作构造器(Operation constructor)都可以接受一个参数—name 作为输入。 正如我们在上面的练习中看到的,提供了一个name,允许我们通过一个描述性字符串来引用一个特定的Op:
3-23在这个例子中,我们给一个加法操作添加“my_add_op”名称。这个名称会在以后使用一些例如TensorBoard工具的时候应用到。
You may find that you’ll want to reuse the same name for different Operations in a graph.
Instead of manually adding prefixes or suffixes to each name, you can use a name_scope to group operations together programmatically. We’ll go over the basic use of name scopes in the exercise at the end of this chapter.
更多的操作
(Overloaded operators)
===================
PS: 感觉这次篇幅太长,分为两篇
网友评论