3.9 TensorFlow中常见元素解读
在了解底层架构的同时,我们下面将介绍构成TensorFlow客户端中最常见的元素。
- 输入(Input):用于训练和测试算法的数据。
- 变量(Variable):可变张量,主要定义算法的参数。
- 输出(Output):存储终端和中间输出的不可变张量。
- 运算或操作(Operation):输入的各种变换以产生期望的输出。
在之前的sigmoid示例中,我们可以找到相关类别的实例,如表3-2中所示。
表3-2 sigmoid例子中相关类别的实例
接下来,我们将详细解读TensorFlow中的这些元素。
3.9.1 在TensorFlow中定义输入
客户端主要以下面三种不同的方式接收数据:
- 使用Python代码在算法的每个步骤中提供数据。
- 将数据预加载并存储为TensorFlow张量。
- 构建输入管道。
接下来,具体看看这三种不同的方式。
1. 使用Python代码提供数据
在第一种方法中,可以使用传统的Python代码方法将数据提供给TensorFlow客户端。在我们之前的示例中,x是此方法的示例。为了从外部数据结构(例如,numpy.ndarray)向客户端提供数据,TensorFlow库提供了一种极佳的数据结构符号,称为占位符(Placeholder),定义为tf.placeholder(...)。前面我们说过,占位符在计算图构建阶段不需要实际数据,相反,我们仅通过执行session.run(...,feed_dict = {placeholder:value})调用的图时提供数据,方法是将外部数据以Python字典的形式传递给feed_dict参数。占位符的定义如下:
tf.placeholder(dtype, shape=None, name=None)
参数如下:
- dtype:这是提供占位符的数据的类型。
- shape:这是占位符的shape,以一维向量给出。
- name:这是占位符的名称,对于调试很重要。
2. 将数据预加载并存储为张量
第二种方法与第一种方法类似,但有一点需要注意。我们不必在计算图执行期间提供数据,因为数据是预先加载的。为了了解这一点,让我们修改一下sigmoid示例,将x定义为占位符:
x = tf.placeholder(shape=[1,10],dtype=tf.float32,name='x')
另外,也将x定义为包含具体数值的张量:
x = tf.constant(value=[[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]], dtype=tf.float32,name='x')
其完整代码将变为如下:
其实,可以发现这里与原来sigmoid示例存在两个主要区别。我们以不同的方式定义了x。现在直接指定一个具体值并将x定义为张量,而不是使用占位符对象并在计算图执行时输入实际值。另外,正如你看到的,我们在session.run(...)中不会提供任何额外的参数。但是,在缺点方面,现在我们无法在session.run(..)处向x提供不同的值并查看输出是如何变化的。
3. 构建数据输入管道
输入管道是专门为需要快速处理大量数据的“重量级”客户端而设计的。这实际上创建了一个保存数据的队列,直到等到需要它的时候为止。TensorFlow还提供了各种预处理步骤(例如,用于调整图像对比度/亮度或者标准化),在将数据提供给算法之前执行。为了提高效率,可以让多个线程并行读取和处理数据。
(1)数据输入管道的结构
TensorFlow数据输入管道也可以被抽象为一个ETL过程(Extract、Transform、Load)。
- Extract:从硬盘上读取数据,可以是本地(HDD或SSD),也可以是网盘(GCS或HDFS)。
- Transform:使用CPU去解析、预处理数据,比如图像解码、数据增强、变换(比如随机裁剪、翻转、颜色变换)、打乱(shuffle)、分批量(batching)。
- Load:将Transform后的数据加载到计算设备,例如GPU、TPU等设备。
这种模式有效地利用了CPU,从而让GPU、TPU等设备专注于进行模型的训练过程(提高了设备的利用率)。
具体来看,典型的管道将包含以下组件:
- 文件名列表。
- 文件名队列,为输入(记录)阅读器生成文件名。
- 用于读取输入的记录阅读器(记录)。
- 解码读取记录的解码器(例如,JPEG图像解码)。
- 预处理步骤(可选)。
- 一个示例(解码输入)队列。
(2)数据输入管道(Pipeline)的优化
随着新的计算设备(例如GPU和TPU)的应用,使得以越来越快的速度训练神经网络成为可能,CPU处理方式很容易遇到海量数据计算的瓶颈。tf.data API提供数据输入管道所需的各种部件,可以对数据输入管道进行优化。
在执行一个训练step之前,必须先做Extract、Transform训练数据,然后将其提供给计算设备上运行的模型。在以前,当CPU在准备数据时,计算设备处于闲置状态;当计算设备执行训练模型时,CPU又处于闲置状态。因此,单个训练step的时间等于CPU准备数据的时间和计算设备执行训练step时间的总和。
Pipelining操作将训练step中的数据准备和模型执行实现了并行操作。当计算设备在执行第N个训练step时,CPU将为第N+1个训练step准备数据。这样,通过两个过程的重叠,单个训练step的时间将取CPU准备数据的时间和计算设备执行训练step的时间中的较大值。
如果我们没有进行管道化(Pipelining)操作,CPU和GPU/TPU中大部分时间处于空闲状态,如图3-10所示。
图3-10 没有使用Pipelining操作的CPU和GPU / TPU处于空闲状态的时间示意图
我们如果使用Pipelining操作,CPU和GPU/TPU中处于空闲状态的时间显著减少,如图3-11所示。
图3-11 使用Pipelining操作的CPU和GPU/TPU处于空闲状态的时间示意图
提示
有关更多信息,请参阅https://tensorflow.google.cn/guide/performance/datasets#input_pipeline_structure上有关数据输入管道的官方说明。
(3)编写输入管道示例
让我们使用TensorFlow编写一个输入管道示例。在这个例子中,我们有三个CSV格式的文本文件(text1.txt,text2.txt和text3.txt),每个文件有5行,每行有10个用逗号分隔的数字(示例行:0.1、0.2、0.3、0.4、0.5、0.6、0.7、0.8、0.9、1.0)。接着,我们具体来看一下。
提示
更多相关信息,请参阅https://tensorflow.google.cn/programmers_guide/reading_data上有关导入数据的TensorFlow官方说明。
首先,和以前一样导入一些重要的库:
import tensorflow as tf import numpy as np
接下来,我们将定义计算图graph和会话session对象:
graph = tf.Graph() session = tf.InteractiveSession(graph=graph)
然后,我们将定义一个文件名队列,一个包含文件名的队列数据结构。这将作为参数传递给reader(后面将被定义)。队列将根据reader的请求生成文件名,以便读者可以使用这些文件名获取文件进而读取数据:
这里,capacity是在给定时间时队列中保存的数据量,shuffle告诉队列是否在发出数据之前对数据进行重新打乱(shuffle)。
TensorFlow有几种不同类型的reader(https://tensorflow.google.cn/api_guides/python/io_ops#Readers上提供了可用reader列表)。由于我们有一些单独的文本文件,其中一行代表一个数据点,因此,这里TextLineReader是最适合的:
reader = tf.TextLineReader()
在定义reader之后,我们可以使用read()函数从文件中读取数据。它输出的是“键-值”对。该键识别出文件和该文件中正在读取的记录(文本行),我们可以省略这个。该值返回reader所读取行的实际值:
key, value = reader.read(filename_queue, name='text_read_op')
下面我们将定义record_defaults,如果发现存在任何错误记录提示,将给出如下输出:
record_defaults = [[-1.0], [-1.0], [-1.0], [-1.0], [-1.0], [-1.0],[-1.0], [-1.0], [-1.0], [-1.0]]
现在,我们将读取的文本行解码为数字列(就像我们的CSV文件一样)。为此,我们使用decode_csv()方法。打开文件(例如,test1.txt),将看到在一行中有10列:
col1, col2, col3, col4, col5, col6, col7, col8, col9, col10 =tf.decode_csv(value, record_defaults=record_defaults)
然后,我们将连接这些列来组成单个张量(称之为特征),这些张量将被传递给另一个方法tf.train.shuffle_batch()。tf.train.shuffle_batch()方法采用先前定义的张量,随机填充张量并输出一批给定的批量大小:
batch_size参数是我们在给定step中采样的数据批量大小,capacity是数据队列的容量(大型队列需要更多内存),min_after_dequeue表示部分元素出队后还留在队列中的最小元素数量。最后,num_threads定义了用于生成一批数据的线程数。如果管道中进行了大量预处理,可以增加这个线程数量。此外,如果我们需要在不进行shuffle的情况下读取数据(与tf.train.shuffle_batch一样),就可以调用tf.train.batch方法。接着,我们将通过以下命令来启动此管道:
coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(coord=coord, sess=session)
可以将tf.train.Coordinator()类看成为线程管理器。它控制着各种管理线程的机制。我们需要f.train.Coordinator()类,因为输入管道产生许多线程来填充入队队列、出列队列以及许多其他任务。接下来,我们将使用之前创建的线程管理器去执行tf.train.start_queue_runners(...)。QueueRunner()保存队列的入队操作,这些操作是在定义输入管道时自动创建的。因此,要填充已定义的队列,我们需要调用tf.train.start_queue_runners函数来启动这些队列运行的程序。
接下来,在指定任务完成之后,我们需要停止相关线程并将它们连接到主线程,否则眼前的程序将无限期挂起。这是通过调用coord.request_stop()和coord.join(threads)来实现的。这个数据输入管道与我们的sigmoid示例相结合,便能够直接从文件中读取数据,如下所示:
3.9.2 在TensorFlow中定义变量
变量在TensorFlow中扮演着重要角色。变量本质上是一个张量,具有特定的形状(shape),该形状(shape)用于定义变量将具有多少个维度以及每个维度的大小。然而,与常规张量不同,变量是可变的,也就意味着变量的值在定义后是可以改变的。这是实现学习模型参数(例如,神经网络权重值)的理想属性,其中权重值在每个学习步骤之后会稍有变化。例如,如果使用x =tf.Variable(0,dtype=tf.int32)定义变量,则可以使用诸如tf.assign(x,x+1)之类的TensorFlow运算来更改该变量的值。但是,如果这样定义张量,例如x = tf.constant(0,dtype = tf.int32),就无法更改该张量的值,它应该保持0的状态直到程序执行结束。
变量的创建很简单。在我们sigmoid的示例中,已经创建了两个变量W和b。在创建变量时,有一些事情非常重要,在这里列出它们并在后续中详细讨论:
- Variable shape:变量形状。
- Data type:数据类型。
- Initial value:初始值。
- Name (optional):名称(可选)。
变量形状是[x,y,z,...]格式的一维向量。列表中的每个值表示相应维度或轴的大小。例如,如果需要具有50行和10列的二维张量作为变量,那其形状将是[50,10]。
变量的维数(形状向量的长度)在TensorFlow中被识别为张量的秩。不要把它与矩阵的秩混淆起来。
提示
TensorFlow中张量的秩表示张量的维数;但对于二维矩阵来说,其秩等于2。
其实,数据类型在确定变量大小方面起着重要作用。有许多不同的数据类型,包括常用的tf.bool、tf.uint8、tf.float32和tf.int32(代码中的字段类型,其意义基本上都是通用的,这和我们平时编写Java或.net程序时用到的布尔类型、浮点类型、整数类型类似,而unit8是8位无符号整型)。每种数据类型都具有属于该类型的单个值所需的位数(bit)。例如,tf.uint8类型需要有8位,而tf.float32需要32位。通常的做法是使用相同的数据类型进行计算,否则会导致数据类型的不匹配。因此,如果需要对两个具有不同数据类型的张量进行转换,需要使用tf.cast(...)操作将一个张量显式地转换为另一个张量的类型。例如,对于具有tf.int32数据类型的x变量,需要将其转换为tf.float32的类型,tf.cast(x,dtype=tf.float32)的调用就是将x变量转换为tf.float32类型。
接下来,我们需要对变量进行初始化,TensorFlow也提供了几种不同的初始化器,包括常数初始化器和正态分布初始化器。TensorFlow一些主要的初始化器如下所示:
- tf.zeros
- tf.constant_initializer
- tf.random_uniform
- tf.truncated_normal
最后,变量的名称(name)将作为一个标识符(ID),使我们能够在图(Graph)中对该变量进行识别。因此,如果我们对计算图进行可视化操作,那变量将通过传递name关键字参数的形式进行显示。如果未指定名称,TensorFlow将使用默认命名方案。
注意
Python变量tf.variable是赋给计算图的,它不是TensorFlow变量命名的一部分。考虑下面的示例,可以在其中指定TensorFlow变量:
a = tf.Variable(tf.zeros([5]),name ='b')
这里,TensorFlow的图将通过名称b而不是a来识别该变量。
3.9.3 定义TensorFlow输出
TensorFlow的输出通常是张量,可以是输入或变量或两者转换后的结果。在我们的例子中,h是一个输出,其中h = tf.nn.sigmoid(tf.matmul(x,W)+ b)。当然,有时候TensorFlow的一个输出也可能变成下一个输入的内容,依次输出下去可以形成一组链式运算或操作,而这里的运算或操作也不一定必须是TensorFlow运算或操作,还可以使用标准Python算法,例如:
x = tf.matmul(w,A) y = x + B z = tf.add(y,C)
3.9.4 定义TensorFlow运算或操作
在https://tensorflow.google.cn/api_docs/python/上查看TensorFlow API,你会发现TensorFlow有大量可用的运算或操作。下面,我们将对TensorFlow中几个常见的运算或操作进行解读。
1. 比较运算
比较运算对于比较两个张量非常有用。对于比较运算部分的详细资料,读者可以查阅https://github.com/tensorflow/docs/tree/master/site/en/api_guides/python中比较运算符的详细内容。当然,对于比较运算工作原理的理解,通过代码层面可能会更直观些。这里,我们给出示例张量x和y:
2. 比较数学运算
TensorFlow允许我们对从简单到复杂的张量执行数学运算。这里我们将讨论TensorFlow中提供的一些数学运算。完整的运算集可在https://github.com/tensorflow/docs/tree/master/site/en/api_guides/python上找到。
3. 比较分散(Scatter)和聚合(Gather)操作
随着人工智能的迅猛发展,尤其是GPU可编程性能的增强以及GPGPU(General Purpose Computing on GPU,在图形处理器上进行通用计算)技术的不断发展,相关研究/技术人员也迫切希望基于流处理器模型的GPU也可以和CPU一样,在支持流程分支的同时,也能够实现对存储器进行灵活的读写操作。其实,Ian Buck在进行早期的GPU通用可编程技术研究时,就发现GPU完成复杂计算任务时存在一个关键性的缺陷,那就是缺乏灵活的存储器操作。所以,他在后来的研究中就增加了对分散和聚合操作的支持,但是结果还是以牺牲一些性能为代价的情况下完成了整个过程。
在GPU中,CUDA中分散和聚合操作实现的结构示意图与第一向量机中的很相似,分散允许将数据输出到非连续的存储器地址内,而聚合则允许从非连续的存储器地址内读取数据。因此,如果认为存储器(如DRAM)是一个二维数组,分散则可以看作利用数组下标或索引将数据写入数组中的任意位置,即a[i] = x,而聚合则可以看作是利用数组下标或索引从数组中的任意位置读出数据,即x = a[i]。
下面,我们给出CUDA中分散(Scatter)和聚合(Gather)操作的结构示意图,如图3-12所示。其中,每个ALU可以看作是一个处理核心,通过分散/聚合操作,多个ALU之间可以共享存储器,实现对任意地址数据的读写操作。
图3-12 CUDA中分散(Scatter)和聚合(Gather)操作的结构示意图
分散和聚合操作在矩阵运算任务中起着至关重要的作用,因为目前来看这两种变体在TensorFlow中是把张量编入索引的唯一方法。换句话说,不能像在NumPy中那样访问TensorFlow中的张量元素。分散操作允许我们将值分配给指定张量的特定索引,而聚合操作允许我们提取指定张量的切片(或单个元素)。以下代码显示了分散和聚合操作的一些变体:
4. 比较与神经网络相关的运算或操作
下面让我们看看几个很有用的神经网络运算或操作,这些将在后面的章节中使用到。这里,我们会对简单元素的转换进行讨论,也会对一组参数对于另一个值的偏导数的运算进行讨论,并给出一个简单的神经网络实现例子。
(1)神经网络的非线性激活
非线性激活能够使神经网络很好地执行许多任务。通常,在神经网络的每一层输出后(最后一层除外)都会有一个非线性的激活转换(激活层)。非线性变换有助于神经网络学习数据中出现的各种非线性模式。这对于解决现实世界中复杂的问题非常有用,因为与线性模式相比,数据通常具有更复杂的非线性模式。
提示
让我们通过一个例子来观察一下非线性激活的重要性。首先,回想一下我们在sigmoid示例中看到的神经网络的计算。如果我们忽视b,那将是这样的:
h = sigmoid(W*x)
假设有一个三层神经网络(W1、W2和W3是层的权重值),其中每层都执行前面的计算;我们可以给出完整计算:
h = sigmoid(W3*sigmoid(W2*sigmoid(W1*x)))
但是,如果我们删除非线性激活(sigmoid),我们就可以得到:
h = (W3 * (W2 * (W1 *x))) = (W3*W2*W1)*x
因此,在没有非线性激活的情况下,可以将三个层降为单个线性层。
如果没有各层之间的非线性激活,深度神经网络就将是一堆相互叠加的线性层而已,而且一组线性层基本上可以压缩成一个更大的线性层。综上所述,如果没有非线性激活,我们就无法创建具有多个层的神经网络。
现在,我们将列出神经网络中两种常用的非线性激活函数以及它们是如何在TensorFlow中实现的:
#x的Sigmoid激活由1 /(1 + exp(-x))给出 tf.nn.sigmoid(x,name=None) #x的ReLU 激活由 max(0,x)给出 tf.nn.relu(x, name=None)
①卷积运算
卷积运算是一种广泛使用的信号处理技术。对于图像,卷积运算可以给出不同的图像效果。这里,图3-13给出了使用卷积进行边缘检测(包括横向边缘检测和纵向边缘检测)的示例。我们可以通过在图像顶部移动卷积过滤器以便在每个位置产生不同的输出来实现边缘检测,如图3-14所示(在本书第5章介绍卷积神经网络时会详细解读卷积运算的工作原理)。具体来说,在每个位置,我们使用与卷积过滤器重叠的图像块(与卷积过滤器相同的大小)对卷积过滤器中的元素进行逐元素乘法,并获取乘法的总和。
图3-13 利用卷积运算在图像中进行边缘检测示意图
提示
源自https://en.wikipedia.org/wiki/Kernel_(image_processing)。
以下是卷积运算的实现:
这里,对于tf.conv2d(...)方法中涉及的input、filter和stride等参数格式而言,TensorFlow对它们的要求是很精确的,下面我们将对这些参数(input、filter、strides、padding)做进一步的解释。
- 输入(input):通常是四维张量,其尺寸应按[batch_size,height,width,channels]排序。
- ★ batch_size:这是一批数据中的数据量(例如,输入的图像和单词等)。我们通常按批量处理数据,因为模型可以使用大型数据集进行深入学习。在给定的训练步骤(step)中,我们随机抽样一小部分可以大致代表完整的数据集的数据,然后重复足够多次该操作,我们便可以很好地逼近这个完整的数据集。此batch_size参数与我们在TensorFlow输入管道示例中讨论的参数相同。
- ★ height and width:这是输入的高度和宽度。
- ★ channels:这是输入的深度(例如,对于RGB图像,将为3通道)。
- 过滤器(filter):这是一个四维张量,表示卷积运算的卷积窗口。过滤器尺寸应为[height,width,in_channels,out_channels]。
- ★ height and width:这是滤镜的高度和宽度(通常小于输入的高度和宽度)。
- ★ in_channels:这是图层输入的通道数。
- ★ out_channels:这是在图层输出中生成的通道数。
- 步幅(strides):这是一个包含四个元素的列表,具体为[batch_stride,height_stride,width_stride,channels_stride]。
- 填充(padding):这里可以选择['SAME','VALID']中的任何一个选项。它能够决定如何处理输入边界附近的卷积运算。VALID操作是在没有填充的情况下执行卷积。如果我们用大小为h的卷积窗口、卷积长度为n的输入,这将给出输出的尺寸(或大小)。输出尺寸的减小会严重限制神经网络的深度。SAME将零填充到边界,使输出具有与输入相同的高度和宽度。
为了更好地了解过滤器大小、步幅和填充是什么,请参见图3-14(我们在本书第5章介绍卷积神经网络时做详细解读)。
图3-14 卷积网络运算示意图
②池化操作
池化运算与卷积运算的行为类似,但最终输出是不同的。我们这里选取的是该位置中图像patch的最大元素,而不是输出过滤器和图像patch中按元素相乘得到的总和(我们在在本书第5章介绍卷积神经网络中会做详细解读),如图3-15所示。
图3-15 最大池化运算示意图
代码运行后返回的结果如下(完整代码和结果,读者可以查看代码文件中的“二维操作(2D卷积和2D最大池化)”部分):
[[[[ 4.] [ 4.]], [[ 8.] [ 8.]]]]
③定义损失
为了让神经网络模型能够学习到有用的东西,我们需要定义一个损失函数。这里有几种可以自动计算TensorFlow中损失的函数。其中,tf.nn.l2_loss是均方误差损失函数,tf.nn.softmax_cross_entropy_with_logits_v2是交叉熵损失函数。交叉熵损失函数在分类任务中能够使模型表现更佳。这里涉及均方误差损失函数和交叉熵损失函数的代码如下:
x = tf.constant([[2,4],[6,8]],dtype=tf.float32) x_hat = tf.constant([[1,2],[3,4]],dtype=tf.float32) # MSE = (1**2 + 2**2 + 3**2 + 4**2)/2 = 15 MSE = tf.nn.l2_loss(x-x_hat) # 神经网络中用于优化网络的常见损失函数 # 使用logits(最后一层的归一输出)代替输出来计算交叉熵,会使数值获得的更加稳定 y = tf.constant([[1,0],[0,1]],dtype=tf.float32) y_hat = tf.constant([[3,1],[2,5]],dtype=tf.float32) # 此函数并不能平均所有数据点的交叉熵损失,我们需要使用reduce_mean函数手动实现这一点 CE = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=y_hat, labels=y))
④神经网络的优化
在定义了神经网络的损失函数之后,我们的目标是随着时间的推移尽量减少这种损失,这个过程就是常说的模型优化工作。换言之,优化器的目标是找到为所有输入提供最小损失的神经网络参数(权重值和偏差)。TensorFlow为我们提供了几种不同的优化器,因此我们不需要从头开始实现相关模型。
图3-16说明了一个简单的优化问题,并显示了优化是如何随时间变化的。该曲线可以想象为损失曲线(对于高维空间的情况,我们称之为损失面),其中x可以被认为是神经网络的参数(在这种情况下,是一个单一权重值的神经网络),y可以被认为是损失。我们初步估计起始点是x=2位置。从这一点开始,我们使用优化器来实现在x=0处得到最小的y(损失)。然而,在实际问题中,损失表面不会像图3-16中所示的这么简单,它会更加复杂。
图3-16 优化过程示意图
在此示例中,我们使用的是常见的梯度下降优化法:GradientDescentOptimizer。learning_rate参数表示在最小化方向上的步长(图3-16中两个圆点之间的距离):
#优化器起到调整神经网络参数的作用,以便最小化工作任务中的错误 tf_x = tf.Variable(tf.constant(2.0,dtype=tf.float32),name='x') tf_y = tf_x**2 minimize_op = tf.train.GradientDescentOptimizer(learning_rate=0.1). minimize(tf_y)
完整代码详见代码文件3_tensorflow_introduction.ipynb中随机优化(Stochastic Optimization)部分。执行该部分代码后,除了会得到图3-16之外,还会得到如下结果:
第 1 个步长上, x: 1.28 , y: 2.5600002 第 2 个步长上, x: 1.0239999 , y: 1.6384 第 3 个步长上, x: 0.8191999 , y: 1.0485759 第 4 个步长上, x: 0.6553599 , y: 0.6710885
从上面的代码运行结果来看,显然,当我们每次调用session.run(minimize_op)执行损失最小化运算时,将会接近tf_x值,进而可以得到最小的tf_y值。
⑤控制流操作
顾名思义,控制流操作控制图中的执行顺序。例如,假设我们需要按以下顺序执行计算:
x = x+5 z = x*2
实际上,如果x = 2,我们应该得到z = 14。下面,让我们尝试以一种简单的方法来实现这一点:
session = tf.InteractiveSession() x = tf.Variable(tf.constant(2.0), name='x') x_assign_op = tf.assign(x, x+5) z = x*2 tf.global_variables_initializer().run() print('z=',session.run(z)) print('x=',session.run(x)) session.close()
我们期望的输出结果是x = 7和z = 14,而在TensorFlow中,上面的代码运行结果却是x = 2和z = 14。引起这种错误的原因是,TensorFlow不关心对象的执行顺序,除非我们在程序中给出明确的执行顺序。我们本部分讨论的控制流操作,就可以实现执行指定顺序的操作。为了得到期望的运行结果(x = 7和z = 14),我们需要对上述代码进行调整,具体如下:
这样一来,我们就可以得到想要的结果(x = 7和z = 14)了。这里,tf.control_dependencies(...)方法是确保在执行嵌套操作之前将会优先执行参数传递给它的运算操作。读者也可以在代码文件“调用tf.control_dependencies(...)方法”部分执行这些代码并查看运行结果。