第3章 TensorFlow基础 本章将向读者介绍TensorFlow基础,包括TensorFlow的基本框架、TensorFlow模型相关内容及TensorBoard的基本使用方法。 值得注意的是,本书采用的TensorFlow版本为1.14.0,此时TensorFlow框架已经在向TensorFlow 2转型,因此许多TensorFlow 1版本的API都同时存在于tf模块(TensorFlow 2中不可用)与tf.compat.v1模块(TensorFlow 2中可用)中。为了提高程序的兼容性,本书使用tf.compat.v1模块中的API进行说明(TensorFlow 1.14.0之前的版本没有tf.compat.v1模块,此时读者直接使用tf模块中的相应方法即可)。 3.1TensorFlow的基本原理 想要了解TensorFlow,需要先从TensorFlow的名字理解其大致原理。 TensorFlow可以拆解为两个词进行理解,即Tensor与Flow。其中Tensor是贯穿整个深度学习的重要概念,也是TensorFlow的设计灵魂,其中文意义是张量,读者可以将其简单理解为高维矩阵,或与NumPy中的ndarray进行类比。如图31所示, 图31标量、矢量、矩阵及张量的联系与区别 无形状的数据称作标量,一维数据称作矢量,二维数据称作矩阵,更高维度的数据我们就可以称其为张量。明白这一点后,我们就可以对张量的属性进行一些说明,我们称张量的维数为它的阶,它的形状是一个整数元组,其指定了阵列每个维度的长度。 通过观察图31可以发现,长度为L的一维矢量是将L个标量堆叠的结果; 高度为H、宽度为W的二维矩阵是将H个(W个)长度为W(H)的一维矢量堆叠的结果; 高度为H、宽度为W、通道数为C的三维张量是将C个高度为H、宽度为W的二维矩阵堆叠的结果。以此类推,k维的张量可以通过堆叠N个k-1维张量得到。 例如一张图像形状为(128, 128, 3)(高度为128px,宽度为128px,3通道图像),则可以说这张图像就是一个张量,它的维数(阶)为3,其形状为(128, 128, 3)。如果有10张形状为(128, 128, 3)的图像该如何表示呢?我们将10张图像(三维张量)堆叠(stack)在一起(参照NumPy中的stack方法),得到形状为(10,128,128,3)的张量,其中第一维表示图像数量。此时该张量为四维张量,其形状可以抽象为(N,H,W,C),其中N表示张量中的图像数量(三维张量的数量),H、W、C分别表示图像的高度、宽度和通道数。这种四维张量表示数据的形式也是TensorFlow中模型图像输入采用的常用形式。 Flow则表示张量Tensor在模型中“流动”的过程。具体而言,是指输入的图像(或其他数据)张量Tensor从模型输入层“流动”到模型输出层,变为输出张量Tensor。在“流动”过程中,张量会不断发生改变,从图像张量(或其他输入数据)变换为输出张量(预测结果),如图32所示。 图32张量流动/转换过程 分别理解了Tensor与Flow,则能明白TensorFlow是通过不断将输入张量进行转换得到最终预测的输出张量,以从训练数据中学习到转换规则,从而完成对数据的学习(本质上是一个拟合过程)。最终达到给定输入数据,模型给出其对应预测的过程,显示出其“智能”。 3.2TensorFlow中的计算图与会话机制 TensorFlow(仅限TensorFlow 1.x中,TensorFlow 2.x已将会话机制删除)的核心概念就是它的计算图及其会话机制。 计算图定义了从数据输入到处理,最终到模型输出的依赖关系,TensorFlow将每个操作(数据输入及数值计算)都抽象为一个节点,通过节点之间的依赖关系画出整张计算图。若需要得到计算图中某一个节点的值(或执行某一节点的运算),则需要通过会话查看当前节点的值(执行当前节点的运行)。由于节点间存在依赖关系,当尝试查看某节点的值(执行某节点的运算)时,该节点依赖的所有前驱节点都会被计算(执行)。 下面分别详细介绍计算图与会话机制的基本概念。 3.2.1计算图 图(Graph)由顶点的有穷非空集合和顶点之间边的集合组成,通常以G(V,E)表示。其中,G表示一张图,V是图G中顶点的集合,E是图G中边的集合。类似地,TensorFlow的计算图也有顶点V与边E的概念。其中顶点V即图中的计算节点,可以是TensorFlow中的任何操作,如张量的相加、数据的输入等,而图中的边则表示张量数据,数据在不同的操作节点之间传递(“流动”),以完成数据的处理与学习。 如图33所示,表示一个最多可以完成算术运算(X+Y)×Z的计算图。需要注意的是,这里所表述的“最多”是指该计算图除了能完成(X+Y)×Z以外,它也可以用于完成X+Y运算,下面再来详细解读这一计算图。 图33(X+Y)×Z的计算图 首先,图中有3个数据输入节点,分别用于接收输入的X、Y、Z,以及两个数据操作节点“相加+”和“相乘*”分别完成两个数的加与乘操作,还有两个输出节点(可选)。 当尝试得到结果2时,可以得知“输出结果2”依赖于前驱节点“相乘*”,“相乘*”节点依赖于“数据输入Z”和“相加+”节点,“相加+”节点依赖于“数据输入X”与“数据输入Y”节点。至此,所有节点依赖关系都已经探寻完毕,因此需要得到结果2,就需要输入数据X、Y及Z。同理,若只需得到结果1,通过节点间的依赖关系可以得知,此时仅需要输入数据X和Y,而不需要Z。 可以发现,使用计算图表达运算后,整个过程十分清晰,运算之间的依赖关系也一目了然。除此之外,计算图还有一大好处就是它可以定义一个抽象的运算过程,不依赖于具体的值。如图33所示的计算图仅仅表达该图是一个最多可以完成(X+Y)×Z运算的图,而没有具体输出的结果值,具体的输出结果值只依赖于输出节点(输出结果1或输出结果2)与具体的输入参数值(输入的X、Y与Z)。 下面的程序说明了如何使用TensorFlow定义(X+Y)×Z的计算图,其中tf.placeholder函数将在3.3节中具体说明,在此读者只要从字面上理解该函数是为变量创建了占位符(占据一个位置,而不提供具体值)即可,代码如下: //ch3/define_graph.py import tensorflow.compat.v1 as tf #为输入变量创建占位符,并为每个变量命名 X = tf.placeholder(dtype=tf.float32, name='X') Y = tf.placeholder(dtype=tf.float32, name='Y') Z = tf.placeholder(dtype=tf.float32, name='Z') #结果1为X与Y相加 result1 = X + Y #结果2为(X + Y) * Z result2 = result1 * Z 运行以上程序,则能建立如图33的计算图。 3.2.2会话机制 在TensorFlow中,使用会话来运行计算图。会话中存在fetch(取回)与feed(注入)操作,其中fetch操作表示用户期望运行的操作节点,而feed操作则是指为计算图注入数据。 fetch操作需要用户为会话传入需要运行的计算图中阶段,如果一次想要运行多个节点,可以以列表的形式传入fetch以同时得到多个结果,需要注意的是,会话运行得到的结果结构、形状与传入的fetch张量相同。 feed操作则是指为计算图注入数据,这里的注入数据可以分为两种情况,一种是如321节所使用的placeholder函数,由于placeholder不提供具体值,因此在使用会话运行计算图时,必须为依赖输入数据的节点注入数据。第二种情况,也可以使用feed操作临时改变计算图中的节点值,例如在计算图中定义了一个值为5的节点a,在运行计算图时,可以临时将节点a的值改变为自己想要的值,这种feed仅改变这一次运算中的a节点值,不影响之后的运行。注入数据使用字典的形式,即{变量1: 值1, 变量2: 值2,…}。 TensorFlow提供了两种会话,分别是tf.Session()和tf.InteractiveSession()。使用会话前需要先定义会话变量Session,再使用Session对应的运行节点方法得到结果,这也是这两种会话主要不同的地方,下面就分别对这两种会话加以说明。 tf.Session()适用于运行已经将所有节点定义好的计算图,适用于在Python脚本中使用。第一步先使用tf.Session函数定义会话变量sess,再使用sess.run函数运行图中的节点(fetch)与注入数据(feed)即可。如图34所示具体说明了fetch与feed在run函数中的用法,第一个参数可以传入单个节点或使用任意嵌套的列表结构传入多个节点,第二个参数根据待运行的节点依赖关系或用户意愿以字典的形式传入待注入数据。 图34使用tf.Session创建的会话运行节点的方法 在使用完成后,为了节约计算机运行资源,需要关闭会话。tf.Session()使用方法的代码如下: //ch3/session.py import tensorflow.compat.v1 as tensorflow #导入3.2.1节所定义的计算图 from define_graph import * #方法1:手动开启/关闭会话 #sess = tf.Session() #... 运行计算图 #关闭会话 #sess.close() #方法2:使用with语句让程序自动管理变量(推荐) with tf.Session() as sess: #运行result1(仅依赖X与Y变量),为X和Y变量分别赋值1和2 r1 = sess.run(result1, feed_dict={X: 1, Y: 2}) #运行result2(依赖X、Y与Z变量),为X、Y和Z变量分别赋值1、2和3 r2 = sess.run(result2, feed_dict={X: 1, Y: 2, Z: 3}) #运行result1和result2,为X、Y、Z变量分别赋值4、5、6 r3 = sess.run([result1, result2], feed_dict={X: 4, Y: 5, Z: 6}) #打印不同的运行结果 print(r1, r2, r3) #运行result1和result2,为X、Y变量分别赋值7、8 r4 = sess.run([result1, result2], feed_dict={X: 7, Y: 8}) 图35计算图中节点的运算结果 运行以上程序,可以得到如图35所示的结果。从结果中可以看到,r1、r2及r3的结果分别为3.0、9.0和[9.0, 54.0](由于传入的张量为一个列表,因此结果也是列表),可以看出,当为feed_dict传入不同值时,其具体结果也会不一样,这也进一步说明了该计算图定义了一个计算范式,而只有当运行时根据具体输入值得到具体输出。当尝试仅给定X与Y运行result1和result2节点时,程序会报错,因为result2需要依赖Z值,而此时未给定,自然也无法计算其值。 类似地,tf.InteractiveSession()适用于在交互式命令中使用会话,如shell或IPython中。与tf.Session()不同的是,当调用了tf.InteractiveSession()时,即表示开启了交互式会话,此时不需要显式使用run方法得到节点运算结果,取而代之的是使用节点的eval方法直接得到值,若待运算的节点依赖注入值,则可直接以字典形式传入eval方法。同时,也可以使用和tf.Session相同的方式(先得到会话变量sess,再使用其run方法运行节点)使用tf.InteractiveSession,不过这显然违背了交互式会话的设计初衷。图36说明了如何在shell中使用交互式会话,可以看到直接使用tf.InteractiveSession()即能开启交互式会话,而无须显式的会话变量,eval的使用方法也与Session的run方法类似,具体输出结果类似图35所示,区别在于交互式会话无法同时对多个操作节点求值,而只能使用单个节点/张量的eval方法计算。 图36交互式会话的使用与输出结果 3.3TensorFlow中的张量表示 在TensorFlow中,有几种常见的张量表示法,分别使用tf.constant、tf.Variable、tf.placeholder和tf.SparseTensor,在此仅对前3种进行介绍。 3.3.1tf.constant tf.constant用于在TensorFlow中创建常量张量,创建时需要为函数传入常量的值(value,必须指定)、常量的数据类型(dtype,可选)、张量的形状(shape,可选)、张量的名字(name,可选),以及是否验证张量的形状(verify_shape,可选)。其中只有常量的值是必须指定的,其他参数皆为可选参数。传入的参数有以下几种特殊情形值得注意: (1) 当dtype未指定时,TensorFlow会根据传入的常量值自动推断最合适的类型,TensorFlow中的数据类型在3.4节会详细说明。 (2) 当未指定形状shape时,TensorFlow也会根据传入的常量值自动推断形状。 (3) 当指定的形状shape与传入的常量形状不一致,并且常量值的个数小于指定的shape个数时,TensorFlow会将常量值的最后一个值填充到缺少的shape中得到新的常量,并将新的常量reshape为传入的shape。 (4) 当指定了verify_shape时,要么不传入shape参数,要么传入的shape参数必须与传入的常量值形状一致(这样做其实没有必要,因为此时形状是唯一的)。 tf.constant使用的代码如下: //ch3/tensor_types.py import tensorflow.compat.v1 as tf #建立4个含有常量值的节点 #const1传入整型值 const1 = tf.constant(0) #const2传入浮点数 const2 = tf.constant(0.0) #const3传入含有整型值的list,tf会将其自动转换为const张量 const3 = tf.constant([0, 1]) #const4传入含有整型与浮点数的list,tf会将其自动转换为相应数据类型的const张量 const4 = tf.constant([0, 1.0]) #初始化会话以运行节点 with tf.Session() as sess: #分别运行4个常量值节点及直接打印节点 print(sess.run(const1), const1) print(sess.run(const2), const2) print(sess.run(const3), const3) print(sess.run(const4), const4) 运行以上程序,可以得到如图37所示的结果,从结果可以看出,使用会话运行节点能直接得到节点中的值,如果不通过会话运行而是直接打印节点,则会得到一个张量Tensor,从中可以看出,这个张量/节点的信息,包括名称、形状、数据类型等。const1传入的值为整型的0,其被TensorFlow自动转换为int32类型(TensorFlow中对于整型数的默认类型)。类似地,const2被转换为默认的float32类型。const3由于传入的list都是整型数,因此转换得到张量数据类型为int32,形状为(2,)。同样地,const4中由于同时存在整型数与浮点数,此时将数据都转换为浮点数,因此张量的数据类型为float32。 图37在TensorFlow中创建常量张量 以上程序说明了第1种和第2种传参情形,即不指定dtype或shape参数,让TensorFlow自动推断数据类型与数据形状,对第3种与第4种传参的特殊情形的代码如下: //ch3/tensor_types.py #第3种与第4种特殊传参情况 #指定的形状shape与传入的常量形状不一致,用常量中最后一个值进行填充 #以0填充形状为(2, 3)的数组 const5 = tf.constant(0, shape=[2, 3]) #以1填充形状为(2, 3)的数组 const6 = tf.constant([0, 1], shape=[2, 3]) #以1填充形状为(2, 3)的数组 const7 = tf.constant([[0], [1]], shape=[2, 3]) #指定的常量形状大于shape参数,报错 #const8 = tf.constant([0, 1], shape=[1, 1]) #指定verify_shape参数 #指定verify_shape为True并且常量值形状与给定的shape相同 const9 = tf.constant([[0, 1]], shape=[1, 2], verify_shape=True) #指定verify_shape为True并且常量值形状与给定的shape不同,报错 #const10 = tf.constant([[0, 1]], shape=[2, 1], verify_shape=True) #指定verify_shape为False并且常量值形状与给定的shape相同 const11 = tf.constant([[0, 1]], shape=[2, 1], verify_shape=False) with tf.Session() as sess: print(sess.run(const5), const5) print(sess.run(const6), const6) print(sess.run(const7), const7) #print(sess.run(const8), const8) print(sess.run(const9), const9) #print(sess.run(const10), const10) print(sess.run(const11), const11) 运行以上程序,可以得到如图38所示的结果,从结果可以看出,只有当传入常量中数字的数量小于shape中数字总数时,TensorFlow才会使用常量中最后一个值进行扩充,否则会报错,其本质相当于先将传入常量reshape为(1,),即一行数字,将这行数中的最后一个数填充满shape指定的数字总数,再将这一行数reshape为指定的shape。而verify_shape参数为True时指定了传入常量值形状必须与传入的shape一致。 图38tf.constant的特殊传参情形 3.3.2tf.Variable 在TensorFlow中,使用tf.Variable定义变量,变量常用于存储与更新神经网络中的参数。使用tf.Variable创建变量时,与tf.constant类似,也需要为其传入初始值、形状与数据类型等参数。比较特殊的一点是,tf.Variable定义的变量还可以传入trainable参数,表示定义的该变量是否可训练,其默认值为True。对于神经网络模型中的参数,其都是可训练的,而对于一些辅助的变量,如迭代计数器等,则是不可训练的。 神经网络模型中的权重通常使用随机数进行初始化,TensorFlow中也提供了相应的随机数产生函数,如从均匀分布产生随机数使用tf.random_uniform,使用tf.random_normal产生服从正态分布的随机数等。与tf.constant不同的是,在用会话运行由tf.Variable定义的变量之前,需要先初始化所有定义的变量,使用会话运行tf.global_variables_initializer()以完成所有变量的初始化之后,才可使用会话运行定义的变量节点,否则会报错“尝试使用未初始化的变量值”。 除了需要先将定义的节点初始化以外,tf.Variable与tf.constant使用方法十分类似,在TensorFlow中创建变量的代码如下: //ch3/tensor_types.py #定义初始化值为0的变量,其类型为整型,并定义变量名为var1 var1 = tf.Variable(initial_value=0, name='var1') #定义初始化值为[0., 1]的变量,其类型为浮点型,并定义变量名为var2 var2 = tf.Variable(initial_value=[0., 1], name='var2') #使用随机正态分布值(均值1.0,标准差0.2)初始化变量,形状为(1, 2),并定义变量名为var3 var3 = tf.Variable( initial_value=tf.random_normal(shape=[1, 2], mean=1.0, stddev=0.2), name='var3' ) #使用整型值10初始化变量,指定该变量不可训练,并定义变量名为var4 var4 = tf.Variable(initial_value=10, trainable=False, name='var4') with tf.Session() as sess: #初始化所有定义的变量 sess.run(tf.global_variables_initializer()) print(sess.run(var1), var1) print(sess.run(var2), var2) print(sess.run(var3), var3) print(sess.run(var4), var4) #使用tf.trainable_variables()打印所有可训练变量 for v in tf.trainable_variables(): print(v) 运行以上程序,可以得到如图39所示的结果,从结果可以看出,定义的4个变量var1~var4的值都被正常打印出来了,并且直接打印节点时,可以看到其相应的属性,如名称、形状、数据类型等。读者可以尝试不使用sess.run(tf.global_variables_initializer())运行变量初始化器,查看直接使用会话运行变量节点时程序的报错情况。 图39使用tf.Variable创建变量并打印结果 以上程序还使用了tf.trainable_variables方法得到所有可训练变量并将其一一打印出来,能够发现结果中并没有var4(其trainable参数被指定为False),说明使用tf.Variable定义的变量默认都是可训练的。 3.3.3tf.placeholder 在TensorFlow中,除了使用tf.constant创建常量与使用tf.Variable创建变量,还可以使用tf.placeholder为变量创建占位符。占位符(placeholder),顾名思义,它会为你占据一个位置而不知其具体值。由于计算图仅定义一个计算的范式,而不依赖具体值,所以在搭建计算图的过程中使用占位符作为输入数据的接口是十分常见的做法,当需要运行计算图时再为占位符输入具体值以得到具体的结果。这一点在3.2节介绍计算图时也有提及。 与前面几节提到的创建常量与变量类似,使用tf.placeholder创建占位符时也有数据类型(dtype)、形状(shape)与名称(name)等参数可以指定,其中数据类型是必须指定的,因为占位符不像constant与Variable能自动推断数据类型。不同的是,占位符由于不产生具体值,因此其也没有value、verify_shape等参数。值得注意的是,若创建占位符时未指定shape,则在运行计算图时可以为此占位符传入任意形状的数据。 tf.placeholder在不同参数下用法的代码如下: //ch3/tensor_types.py #定义一个数据类型为float32的占位符,形状任意 plh1 = tf.placeholder(dtype=tf.float32) plh2 = tf.placeholder(dtype=tf.float32, name='plh2') #定义一个数据类型为float32的占位符,形状为(2, 2) plh3 = tf.placeholder(dtype=tf.float32, shape=[2, 2], name='plh3') #定义一个数据类型为float32的占位符,形状的第一维任意,第二维为2 plh4 = tf.placeholder(dtype=tf.float32, shape=[None, 2], name='plh4') #定义一个数据类型为float32的占位符,形状的第一维任意,第二维也任意 plh5 = tf.placeholder(dtype=tf.float32, shape=[None, None], name='plh5') with tf.Session() as sess: print(sess.run(plh1, feed_dict={plh1: 1})) print(sess.run(plh1, feed_dict={plh1: [1, 2]})) print(sess.run(plh1, feed_dict={plh1: [1, 2, 3]})) print(sess.run(plh2, feed_dict={plh2: 2})) print(sess.run(plh3, feed_dict={plh3: [[1, 2], [3, 4]]})) #报错,因为feed的数据形状与placeholder定义的形状不一致 #print(sess.run(plh3, feed_dict={plh3: [[1, 2]]})) print(sess.run(plh4, feed_dict={plh4: [[1, 2]]})) print(sess.run(plh4, feed_dict={plh4: [[1, 2], [3, 4]]})) print(sess.run(plh5, feed_dict={plh5: [[1, 2]]})) print(sess.run(plh5, feed_dict={plh5: [[1, 2], [3, 4]]})) print(sess.run(plh5, feed_dict={plh5: [[1], [2]]})) 代码中定义的plh1未指定shape,因此在传入值的时候可以传入任意形状的数据,plh3指定了数据形状为(2, 2),因此传入的数据形状严格限定为(2, 2),plh4指定的shape为(None, 2),第一维指定为None表示该维度任意,只需数据的第二维长度为2。类似地,可以为plh5传入任意形状的二维数据,需要注意的是,plh5与plh1的区别在于,plh1可以接收任意维度的数据,而plh5只能接收维度大小任意的二维数据。 3.4TensorFlow中的数据类型 本节对TensorFlow中数据类型进行一个简单的介绍。和Numpy中类似(参见2.1.1节),TensorFlow中也有自己定义的数据类型,其所有的类型如表31所示。 表31TensorFlow中的数据类型 数 据 类 型含义说明 tf.float1616位浮点数 tf.float3232位浮点数 tf.float6464位浮点数 tf.bfloat1616位截断浮点数仅在TPU上有原生支持,由tf.float32截断前16位得到。其表示范围与tf.float32相同,但是占用空间仅有其一半。不容易溢出 tf.complex6464位复数 tf.complex128128位复数 tf.int88位有符号整数 tf.int1616位有符号整数 tf.int3232位有符号整数 tf.int6464位有符号整数 tf.uint88位无符号整数 tf.uint1616位无符号整数 tf.uint3232位无符号整数 tf.uint6464位无符号整数 tf.bool布尔型 tf.string字符串 tf.qint8量化操作的8位有符号整数 tf.quint8量化操作的8位无符号整数 tf.quint16量化操作的16位无符号整数 tf.qint32量化操作的32位有符号整数量化表示将具有连续范围的float值以定点近似(如整型)表示。在保证精度近似时压缩模型体积 tf.resource可变资源值本书不使用 tf.variant任意类型值本书不使用 虽然TensorFlow中有这么多不同的数据类型,但是在实际TensorFlow编程中,使用32位的数据类型居多(浮点数与整数都采用32位表示),这也是TensorFlow中默认的数字类型格式,除此以外,使用较多的还有布尔型(tf.bool)及字符串(tf.string)。本书不涉及量化操作的数据类型、tf.resource及tf.variant的使用。 在创建张量时,为dtype参数传入表31中的数据类型即可。在实际使用中,计数器性质的变量等使用tf.int32,而对于输入的图像数据,由于一般将归一化后的数据(像素值大多落在0~1之间或-1~1之间)作为输入,因此一般使用tf.float32,若直接使用没有归一化的图像,则使用tf.uint8即可。tf.string用于接收字符串类型的变量,一般用于文件或图像路径,得到路径后在计算图中再进行数据读取。 读者可能会有疑问,既然Python及Numpy已经有一套自己的数据类型,为什么TensorFlow还需要定义自己的一套数据类型,这是因为在神经网络训练过程中,不仅仅需要在前向过程中的数值运算,更重要的是反向过程中的模型参数优化过程,这涉及求导等运算过程。因此为了配合TensorFlow中的各种运算操作,定义自己的一套适用于这些操作的数据类型也是必不可少的。同时,由于TensorFlow的大多数据类型与NumPy数据类型直接兼容,因此只需要在为网络输入数据时,直接传入NumPy处理好的数据即可(大多数情况这样做,也可以传入字符串而不直接传入数据),此时模型的输入层会根据数据类型的对应关系直接将NumPy数据转换为兼容的TensorFlow类型的数据,进而完成之后的模型运算。在模型输出时,由于输出结果已不需要进行网络前向与反向计算过程,因此不需要使用TensorFlow中的内置数据类型,返回的结果为NumPy中的数据类型。 与NumPy类似,TensorFlow使用tf.cast方法完成数据类型之间的转换,其具体使用方法为tf.cast([待转换变量], dtype=[目标转换类型]),tf.cast用法的代码如下: //ch3/data_type.py import tensorflow.compat.v1 as tf var1 = tf.Variable(1.5, name='var1') #将浮点数转换为int32 re1 = tf.cast(var1, dtype=tf.int32, name='var2') const1 = tf.constant(False, name='const1') #将布尔值转换为float32 re2 = tf.cast(const1, dtype=tf.float32, name='const2') plh1 = tf.placeholder(dtype=tf.string, name='plh1') #将string转换为bool(报错) re3 = tf.cast(plh1, dtype=tf.bool) with tf.Session() as sess: #初始化所有的变量 sess.run(tf.global_variables_initializer()) print(sess.run(var1)) print(sess.run(re1)) print(sess.run(const1)) print(sess.run(re2)) print(sess.run(plh1, feed_dict={plh1: 'TensorFlow is awesome'})) #报错,不允许从string转换为bool #print(sess.run(re3, feed_dict={plh1: 'TensorFlow is great'})) 运行以上程序,可以得到如图310所示的结果,可以看到原本值为1.5的浮点数被转换为整型后值为1,而布尔值False转换为浮点数时变成了0.0,从此图可以看出,TensorFlow数据类型之间的转换规则与Python和NumPy中一致,这也方便了用户的操作。 图310使用tf.cast转换张量的类型 3.5TensorFlow中的命名空间 在TensorFlow中能十分方便地定义命名空间,使用命名空间有两大好处: 一是可以让代码结构更加清晰,使用TensorBoard可视化计算图时更加清晰(将在3.9节说明TensorBoard的用法)。二是通过定义不同的命名空间可以将不同的变量分隔开,这样有利于变量的区分与重用。 在TensorFlow中,有两种方式定义命名空间,分别是tf.name_scope与tf.variable_scope,两者在绝大多数情况下是等价的,只有在使用tf.get_variable函数时有细微的区别,下面就tf.get_variable函数和两种定义命名空间的方式加以说明。 3.5.1tf.get_variable 如3.3节第2部分所讲,在TensorFlow中可以使用tf.Variable定义变量,事实上tf.get_variable函数也是定义变量的一种方法。该函数具体参数与tf.Variable类似,不过表现形式不同,使用tf.Variable创建变量时传入的变量名称name(可选)是将创建的新变量命名为name,而使用tf.get_variable传入的名称name是用来查询是否已经存在名为name的变量,如果有则直接返回该变量,否则该函数将创建一个名为name的新变量。tf.get_variable使用方法的代码如下: //ch3/name_scope.py import tensorflow.compat.v1 as tf #使用tf.Variable创建一个浮点型变量 var1 = tf.Variable(1.2, name='var1') #尝试使用tf.get_variable方法获取定义过的变量var1 var2 = tf.get_variable(name='var1', shape=[]) #查看var2与var1是否指向同一变量 print(var1 == var2) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) print(var1, sess.run(var1)) print(var2, sess.run(var2)) 运行以上程序,读者可以得到var1==var2的结果为False,这说明var2与var1实际上并不指向同一变量,并且使用会话运行得到的两个变量值也不同,从打印的节点信息可以看出,使用tf.get_variable并没有得到已经定义的名为var1的变量,而是自动创建了一个新变量,其名为var1_1。这是因为若想使用tf.get_variable重复使用变量或者得到已经创建的变量,其变量也必须是通过tf.get_variable创建的,而不可以是通过tf.Variable创建的变量,重用变量的写法将在下面讲解命名空间的时候说明。值得注意的是,使用tf.Variable总能够创建新的变量,即使传入的name是一样的,此时TensorFlow会自动解决命名冲突,而使用tf.get_variable则严格按照传入的name寻找或创建变量。 3.5.2tf.name_scope 从设计上来讲,tf.name_scope一般用于操作节点而不用于变量节点,使用tf.name_scope定义命名空间时,需要传入名称name作为空间名称。当tf.name_scope定义的命名空间内存在变量节点时,需要分情况进行说明。当命名空间内有由tf.Variable定义的变量时,空间名称会以前缀的形式加在变量名之前,其结构如scope_name/var_name所示,命名空间允许嵌套,即结构可以为scope_name1/scope_name2/…/var_name,这一点与操作节点的表现形式相同,例如空间内的加操作会被命名为类似scope_name/add的形式。而使用tf.get_variable得到的变量则不受tf.name_scope的影响,其参数中指定的name即最终得到的变量名称。验证这两者不同之处的代码如下: //ch3/name_scope.py #定义一个名为scope1的命名空间 with tf.name_scope('scope1'): #使用tf.Variable定义变量var3 var3_1 = tf.Variable(2.5, name='var3') #使用tf.get_variable定义变量var4 var4_1 = tf.get_variable(name='var4', initializer=0.0) #定义加操作 var5_1 = var3_1 + var4_1 #打印空间内节点信息以查看其名称 print(var3_1) print(var4_1) print(var5_1) 运行以上程序,可以得到如图311所示的结果,从结果可以看出,在命名空间中,由tf.get_variable定义的变量名不受空间名的影响,而由tf.Variable定义的变量名和操作节点都会受其影响。 图311使用tf.name_scope创建命名空间 使用tf.name_scope并不能完成变量的重用,其作用仅为操作等节点加上空间前缀,使计算图结构更加清晰。在TensorFlow中,重用(reuse)属性默认为关闭的,只有通过手动将该属性置为True或自动(tf.AUTO_REUSE)才能完成变量的重用,这个属性只存在于tf.variable_scope而在tf.name_scope不存在。 3.5.3tf.variable_scope 与3.5.2节的tf.name_scope类似,tf.variable_scope也旨在定义计算图的结构,但是区别在于它对于tf.Variable和tf.get_variable得到的变量都产生作用(为其name前加上空间名前缀),并且其还有reuse属性,以完成变量的重用。tf.variable_scope用法的代码如下: //ch3/name_scope.py #使用tf.variable_scope创建命名空间 with tf.variable_scope('scope1'): #使用tf.Variable创建名为var3的变量 var3_2 = tf.Variable(3.5, name='var3') #使用tf.get_variable得到名为var4的变量 var4_2 = tf.get_variable(name='var4', initializer=1.0) #打印命名空间中的变量 print(var3_2) print(var4_2) 运行以上程序,可以得到如图312所示的结果,从结果可以看出,使用tf.Variable和tf.get_variable得到的变量名称都受tf.variable_scope的影响,需要注意的是3.5.2节由tf.name_scope定义的scope1中已经有名为var3的变量,因此TensorFlow在此自动为tf.variable_scope中的var3解决命名冲突,并将其命名为“scope1_1/var3”。 图312使用tf.variable_scope创建命名空间 接下来,我们再使用tf.variable_scope定义一个名为scope1的命名空间,并尝试在这个命名空间中得到以上代码中定义的var4_2,这一过程的代码如下: with tf.variable_scope('scope1', reuse=True): var4_3 = tf.get_variable(name='var4', initializer=10.0) print(var4_3 == var4_2) 如上述代码所示,新定义的名为scope1的命名空间中设置重用(reuse)为True,使用tf.get_variable尝试得到一个名为var4的变量。运行以上代码可以得到var4_3==var4_2的结果为True,这说明两者实际上指向的是同一变量,完成对变量var4_2的重用。若此时尝试在定义的scope1内创建新变量会怎样呢?代码如下: with tf.variable_scope('scope1', reuse=True): var4_3 = tf.get_variable(name='var4', initializer=10.0) var5_2 = tf.get_variable(name='var5', initializer=100) 上述代码尝试在scope1内创建新变量var5_2,var4_3仍然尝试重用变量var4_2,此时运行代码会报错,表示其无法找到能够被重用的名为var5的变量。因为scope1的reuse属性指定为True,这要求命名空间中所有的变量都能够被重用(包括var5_2,事实上scope1中不存在名为var5的变量,因此无法重用)。在这种情况下我们希望代码的行为是var4_3重用变量var4_2,而var5_2是一个被新建的变量,当在命名空间内变量行为不一致时,我们需要将reuse属性置为tf.AUTO_REUSE,表示是否重用这一行为由TensorFlow帮助我们决定,能找到的变量则进行重用,不能找到的就进行新建。正确的写法代码如下: //ch3/name_scope.py with tf.variable_scope('scope1', reuse=tf.AUTO_REUSE): var4_3 = tf.get_variable(name='var4', initializer=10.0) var5_2 = tf.get_variable(name='var5', initializer=100) print(var4_3 == var4_2) print(var5_2) 运行以上代码,可以得到如图313所示的结果,可以发现此时var4_3完成了对变量var4_2的重用,同时var5也被创建成值为100(从dtype为int32可以得知)的变量。笔者建议使用tf.variable时尽量将reuse设置为tf.AUTO_REUSE以防止空间内变量行为不一致的情况。 图313使用tf.variable_scope重用变量 当有嵌套的命名空间时,还可以通过手动指定带有命名空间的变量名完成重用,下面的程序首先嵌套定义命名空间scope_x与scope_y,并在scope_y中定义了一个变量reuse_var,该变量在命名空间的作用下全名变为scope_x/scope_y/reuse_var。接着定义一个名为scope_x的命名空间并指定其中的变量自动进行重用(reuse=tf.AUTO_REUSE),现尝试重用名为scope_y/reuse_var的变量。运行程序后,发现reuse_var2能够成功重用变量reuse_var,这说明重用变量时可以手动指定需要重用的命名空间前缀,代码如下: //ch3/name_scope.py with tf.variable_scope('scope_x'): with tf.variable_scope('scope_y'): reuse_var = tf.get_variable('reuse_var', initializer=1000.0) with tf.variable_scope('scope_x', reuse=tf.AUTO_REUSE): reuse_var2 = tf.get_variable('scope_y/reuse_var', initializer=0.001) 需要注意的是,tf.variable_scope的reuse属性仅对作用域中与tf.get_variable相关的变量起作用,这是因为tf.Variable仅起到新建变量的作用,而没有获取已有变量的作用,读者可以将上面代码中的tf.get_variable改成tf.Variable进行尝试。 3.6TensorFlow中的控制流 程序中除了最常见的顺序结构,还有分支结构(if、switch等)与循环结构(for、while等)等。由于计算图是固定的、静态的,而有时候神经网络模型需要根据模型的不同状态使用动态的分支或者循环结构,因此TensorFlow提供了一套专门用于计算图中的控制流操作,用于计算图中的动态结构。 除了结构以外,TensorFlow还能指定计算图中节点的执行顺序。下面就分别对TensorFlow中的分支结构与循环结构及如何指定节点执行顺序进行讲解。 3.6.1TensorFlow中的分支结构 在程序设计中,常常使用if/else与switch/case语句完成分支结构(虽然Python中没有switch/case语句)。相应地,TensorFlow也提供了两种语句在计算图中的实现形式,与if/else对应的函数为tf.cond,而与switch/case语句对应的函数为tf.case,下面就分别对这两个函数进行介绍。 与if的控制流一样,tf.cond函数一次只对单个分支条件值进行判断,若该值为True,则执行某些函数(true_fn),否则执行另一些函数(false_fn)。tf.cond函数有几个重要的参数,分别为分支条件pred(True张量或者False张量),条件为True时执行的函数true_fn,条件为False时执行的函数false_fn。需要注意的是,TensorFlow要求true_fn与false_fn不可有参数且必须有返回值,并且这两个函数的返回值的数据类型与返回参数个数与结构需要相同,简单来说,若true_fn返回两种类型为float32的张量,则false_fn也必须返回两种类型为float32的张量。tf.cond的具体使用方法的代码如下: //ch3/control_flow.py import tensorflow.compat.v1 as tf #定义常量a与b,其值分别为1.0与2.0 a = tf.constant(1.0, name='a') b = tf.constant(2.0, name='b') #定义一个判断条件的占位符,其类型为tf.bool condition = tf.placeholder(dtype=tf.bool, name='condition') #当condition为True时,返回c=a+b,否则返回c=a-b,此处使用匿名函数实现 c = tf.cond(condition, lambda: a + b, lambda: a - b) #当a 1: lambda: a + b, condition > 2: lambda: a + 2 * b}, default=lambda: a - b, exclusive=False ) #使用键值对定义case,并指定exclusive为True,此时会报错,因为两个条件在condition>2时都 #为True d = tf.case( {condition > 1: lambda: a + b, condition > 2: lambda: a + 2 * b}, default=lambda: a - b, exclusive=True ) #由于计算图中不存在变量,因此不需要使用variable_initializer with tf.Session() as sess: #根据传入不同的condition值得到不同的结果 print(sess.run(c, feed_dict={condition: 1})) print(sess.run(c, feed_dict={condition: 2})) print(sess.run(c, feed_dict={condition: 3})) print(sess.run(d, feed_dict={condition: 1})) print(sess.run(d, feed_dict={condition: 2})) #报错 print(sess.run(d, feed_dict={condition: 3})) 运行以上程序,会发现当exclusive为True并且传入的condition为3时程序会报错并退出,这是因为当condition为3时,此时传入的case多于一个为True。细心的读者再运行以上程序会发现程序会抛出一个WARNING:TensorFlow:case: An unordered dictionary of predicate/fn pairs was provided, but exclusive=False. The order of conditional tests is deterministic but not guaranteed.,这是因为传入的case为字典类型,其本身不保证顺序性,若要消除这个WARNING,只需将传入的case改为有确定性顺序的列表,将传入的case改成如下“列表+元组”的形式即可,代码如下: c = tf.case( [(condition > 1, lambda: a + b), (condition > 2, lambda: a + 2 * b)], default=lambda: a - b, exclusive=False ) 3.6.2TensorFlow中的循环结构 在TensorFlow中,使用tf.while_loop完成循环,其参数需要传入循环条件函数、循环体函数及这两个函数需要传入的参数,tf.while_loop的返回值与循环体函数的返回值形式保持一致,并且条件判断函数与循环体函数的参数列表需要保持一致,因为在循环体函数中有可能对判断条件的参数进行了更改,因此需要循环体函数接收条件判断函数所有的参数,同时要求循环体参数将传入的所有参数进行返回。下面的程序说明了tf.while_loop的用法,实现了一个变量加1的操作,代码如下: //ch3/control_flow.py #定义循环中需要使用的变量i和n i = 0 n = 10 #循环条件函数 def judge(i, n): #当i < sqrt(n)时才执行循环 return i * i < n #循环体函数 def body(i, n): #循环中使i增1 i = i + 1 #返回的参数与输入的参数保持一致 return i, n #为tf.while_loop传入条件函数、循环体函数及参数 new_i, new_n = tf.while_loop(judge, body, [i, n]) with tf.Session() as sess: print(sess.run([new_i, new_n])) 运行以上程序,可以得到最终由循环得到的new_i与new_n分别为4和10,说明当i由0增加到4时,由于4×4>10而跳出循环得到最终结果。 3.6.3TensorFlow中指定节点执行顺序 TensorFlow中可以对计算图中的节点指定执行顺序,这可以使用tf.control_dependencies进行实现,首先需要明确tf.control_dependencies会创建一个作用域,创建该作用域时需要为tf.control_dependencies传入一个操作或者计算图中节点的列表,表示这个列表中的操作需要先于作用域中的操作执行。tf.control_dependencies用法的代码如下: //ch3/control_flow.py #定义一个变量x,其初始值为2 x = tf.Variable(2) #为x定义一个加1操作 x_assign = tf.assign(x, x + 1) #y1的值为x^2 y1 = x ** 2 with tf.control_dependencies([x_assign]): #y2的值为x^2,其需要在执行x_assign之后执行 y2 = x ** 2 with tf.Session() as sess: tf.global_variables_initializer().run() print(sess.run([y1, y2])) 运行程序后,可以得到y1的值为4,而y2的值为9,这说明y1在x未提供加1时即得到了计算,而y2由于有控制依赖,它的计算在x_assign(即x已经加1)后才被执行。如上代码的计算图可以用图314进行表示。 图314含有tf.control_dependencies的计算图 3.7TensorFlow模型的输入与输出 明确了TensorFlow中的基本概念(如张量、计算图等),想要理解TensorFlow中对模型的输入与输出便也不难了。 在TensorFlow中,最为常见的模型输入便是使用tf.placeholder进行实现,在运行计算图时,再为该占位符输入具体的训练数据。由于在训练网络时,常常因为训练数据过多过大而无法一次性全部放入内存,所以在训练模型时可以将数据分为一个个小的batch放入网络进行训练,这也恰好满足了tf.placeholder的特性,在每一次训练迭代时将不同的batch数据放入占位符以完成训练。 模型的输出通常为NumPy类型,在使用tf.Session得到模型输出的具体结果后可以通过处理NumPy数据的一切方法进行后期处理。 3.8TensorFlow的模型持久化 训练神经网络模型常常是一件十分耗时的事情,因此如果能将训练好的网络权值保存到磁盘,当需要使用该权值时再从磁盘中进行恢复以继续训练或者仅仅只进行前向推理,将省下每次需要重复训练的大量时间。 在TensorFlow中,使用saver对模型进行持久化,由tf.train.Saver创建。使用saver既可以完成对模型的保存,也能完成对磁盘上已保存的权值的读取,分别使用其save和restore方法。下面就分别介绍如何使用tf.train.Saver完成权值的保存与读取。 3.8.1模型的保存 当定义好计算图时,可以使用tf.train.Saver的save方法保存计算图中的变量,当使用tf.train.Saver创建对象时,可以为其构造函数传入一个var_list,表示仅保存var_list中指定的变量,否则默认保存计算图中所有的变量。在使用save方法时,需要传入当前所在的Session(因为在不同的Session下,同一模型的参数值有可能不相同)与需要保存的文件名,下面的程序说明了如何使用saver保存计算图中的变量,为简便起见,程序中定义的计算图沿用3.2节第1部分中定义的(X+Y)×Z的计算图,不同的是将X与Y改为tf.Variable而非placeholder(因为saver仅可保存计算图中的变量,而原计算图中全都为placeholder,因此无法保存),代码如下: //ch3/handle_ckpt.py import tensorflow.compat.v1 as tf X = tf.Variable(56.78, dtype=tf.float32, name='X') Y = tf.Variable(12.34, dtype=tf.float32, name='Y') Z = tf.placeholder(dtype=tf.float32, name='Z') #结果1为X与Y相加 result1 = X + Y #结果2为(X + Y) * Z result2 = result1 * Z saver = tf.train.Saver() with tf.Session() as sess: tf.global_variables_initializer().run() saver.save(sess, 'graph.ckpt') 代码中在Session外初始化了一个saver,并不指定var_list,表明需要保存计算图中的所有变量,在会话内使用save方法将计算图中的变量保存成名为graph.ckpt的权重文件。运行以上程序可以发现根目录下多了4个文件,其中3个分别以data、index和meta结尾,还有一个为checkpoint文件,其中以data结尾的文件存储了计算图中变量的具体值,而以index结尾的文件提供了文件索引,以meta结尾的文件存储了计算图结构而没有任何具体值,checkpoint以文本形式存储了最新的一个权值名称,在此不涉及这几个文件内部具体的存储方式,下面通过代码说明如何查看这些权值文件中究竟保存了哪些变量及对应的值。 在TensorFlow中,可以使用tf.train.NewCheckpointReader来查看权值文件中具体存储了哪些变量及其对应的值。使用debug_string方法查看权值文件中具体存在的变量名,使用get_tensor并传入相应的变量名查看其对应的值。tf.train.NewCheckpointReader使用方法的代码如下: new_ckpt = tf.train.NewCheckpointReader('graph.ckpt') print(new_ckpt.debug_string().decode('utf8')) print(new_ckpt.get_tensor('X'), new_ckpt.get_tensor('Y')) 图315查看权值文件中的 变量及其值 运行以上程序,可以得到如图315所示的结果。从结果可以看出,权值文件中保存了两个变量,其名称分别为X和Y,类型为float,并且形状为空(表明是标量),X和Y的值分别为56.78和12.34,这和前面定义计算图过程中的变量值完全一致。 3.8.2模型的读取 通过3.8.1节的学习,我们已经成功保存了网络中的变量,并通过NewCheckpointReader查看了权值文件中的变量名与值。本节对权值文件进行读取,将值恢复到模型中,并进一步进行操作。 在TensorFlow中,恢复模型权值使用saver的restore方法。同样,在创建saver时可以传入一个var_list以指定需要被恢复的变量,restore方法需要传入目标Session和磁盘上权值文件的路径。在进行恢复时,我们常常会读取一个路径下最新保存的权值进行恢复(默认认为最终的权值文件是最好的),这一过程常常使用tf.train.latest_checkpoint来完成,为该函数传入权值文件所在的文件夹,它就会自动得到最新的权值文件路径。读取权值文件中的权重并将其恢复的代码如下: //ch3/handle_ckpt.py #重新定义计算图,并改变变量的初始值 X = tf.Variable(11.1111, dtype=tf.float32, name='X') Y = tf.Variable(22.2222, dtype=tf.float32, name='Y') Z = tf.placeholder(dtype=tf.float32, name='Z') #结果1为X与Y相加 result1 = X + Y #结果2为(X + Y) * Z result2 = result1 * Z var_list = [X] saver = tf.train.Saver(var_list=var_list) with tf.Session() as sess: tf.global_variables_initializer().run() last_ckpt = tf.train.latest_checkpoint('.') print(last_ckpt) saver.restore(sess, last_ckpt) print(sess.run(X)) print(sess.run(Y)) 运行以上程序,可以得到sess.run(X)结果为56.78而非定义的11.1111,这是因为在调用tf.global_variables_initializer对X进行初始化后,又使用saver将权值文件中保存的X值(56.78)恢复到变量X中了,而sess.run(Y)的值则是22.2222,与代码中定义的Y值相同,说明Y值没有被权值文件中恢复的值覆盖,因为代码中指定了var_list中仅含有X变量,所以变量Y的值仍是初始化的值22.2222。 3.9使用TensorBoard进行结果可视化 TensorBoard是TensorFlow的可视化工具包,使用TensorBoard能够可视化实时跟踪模型训练过程中的数据量变化情况,如训练损失等。它还能对图像及计算图等进行可视化。TensorBoard是TensorFlow框架的一大优势,所以本节介绍TensorBoard可视化的用法。 为了保存计算图中的各种信息,首先需要使用tf.summary.FileWriter初始化一个writer对象,表示使用其写入网络中的各种信息。 3.9.1计算图的可视化 为了简便起见,本节使用的是3.8节第1部分所定义的计算图,为了保存代码中定义的计算图,只需要在定义writer时为graph参数传入当前的默认计算图(tf.get_default_graph()得到当前默认的计算图),在程序结束前使用writer.close关闭这个IO对象即可(纯粹是一个好习惯)。下面的程序说明了如何将计算图添加到TensorBoard文件中,其中加粗的代码与TensorBoard操作直接相关,代码如下: //ch3/use_tb.py import tensorflow.compat.v1 as tf X = tf.Variable(56.78, dtype=tf.float32, name='X') Y = tf.Variable(12.34, dtype=tf.float32, name='Y') Z = tf.placeholder(dtype=tf.float32, name='Z') #结果1为X与Y相加 result1 = X + Y #结果2为(X + Y) * Z result2 = result1 * Z #创建一个summary的IO对象,并将计算图添加到summary中 writer = tf.summary.FileWriter('summary', graph=tf.get_default_graph()) with tf.Session() as sess: tf.global_variables_initializer().run() #关闭IO对象 writer.close() 运行以上程序,会发现根目录下多了一个summary文件夹,其中有一个名为events.out.tfevents…的文件。此时在summary文件夹下打开命令行,使用TensorBoard logdir .指令,并在浏览器中输入localhost:6006(6006是TensorBoard默认使用的端口,当然可以更改),此时可以看到如图316所示的结果。 图316使用TensorBoard查看计算图的结构 从结果可以看出,使用TensorBoard画出来的计算图与我们在3.2节第1部分人工绘制的计算图结构一致。不同的是,TensorBoard绘制的计算图更加细致,如果读者尝试双击X或Y节点,则可以看到节点内部更加细化的操作(如初始化操作等)。 3.9.2矢量变化的可视化 在训练模型时,常常需要保存与查看损失函数的变化情况。此时可以使用TensorBoard保存损失以查看损失随着迭代次数的变化情况。在TensorFlow中,使用tf.summary.scalar保存需要记录的标量,为该函数传入名称与对应的张量即可。下面的程序使用了循环完成变量的加1操作,并使用TensorBoard记录了变量的变化过程,代码如下: //ch3/use_tb.py i = tf.Variable(1) writer = tf.summary.FileWriter('summary_1', graph=tf.get_default_graph()) assign_op = tf.assign(i, i + 1) tf.summary.scalar('i', i) merged_op = tf.summary.merge_all() with tf.Session() as sess: tf.global_variables_initializer().run() for e in range(100): sess.run(assign_op) summ = sess.run(merged_op) writer.add_summary(summ, e) writer.close() 在以上代码中,除了使用了tf.summary.scalar来记录张量i以外,还使用了tf.summary.merge_all函数,由于定义的模型可能十分庞大(当然示例代码的计算图很小),模型的各部分都在不同的模块中,此时对于模型的描述(summary)可能分布在各个文件,此时就需要使用tf.summary.merge_all将“散落”在各部分的summary收集起来使其变成一个操作,也就是代码中的merge_op。得到总的merge_op后再使用Session得到描述的具体值,再将该具体值放入IO对象writer(writer.add_summary(sum, e))。使用writer.add_summary时需要注意,除了需要给其传入具体的summary以外,还需要给当前这个summary绑定一个周期e,表示这个summary中的值对应于第e个周期的数据。如以上代码,整体使用一个for循环了100个周期,在每个周期内,先用Session运行加1操作,再得到加1操作后计算图中的i变量值,最后将得到的i值与当前周期e绑定放入writer。 运行以上程序后,并在根目录的summary_1文件夹下使用TensorBoard logdir .命令,可以得到如图317所示的结果,可以看到此时有两个选项SCALARS和GRAPH,在GRAPH选项卡中可以看到我们定义的计算图,在SCALARS里可以看到我们记录的变量i,并且可以发现其值从2一直增加到101(读者可以思考为什么不是从1开始增加到100,以及如何才能使i值从1增加到100)。 图317使用TensorBoard记录标量变化情况 3.9.3图像的可视化 在某些应用场景下,我们需要以图像的形式可视化模型中间层的表示或者对于生成式模型而言,我们需要可视化模型最终生成的结果,此时可以使用TensorBoard对图像进行可视化,与可视化标量类似,使用tf.summary.image函数可以对图像进行可视化,下面的程序说明了使用TensorBoard可视化图像的过程,运行程序前需要确保根目录下有一张名为1.jpg的图像,代码如下: //ch3/use_tb.py with open('1.jpg', 'rb') as f: data = f.read() #图像解码节点,3通道jpg图像 image = tf.image.decode_jpeg(data, channels=3) #确保图像以4维张量的形式表示 image = tf.stack([image] * 3) #添加到日志中 tf.summary.image("image1", image) merged_op = tf.summary.merge_all() writer = tf.summary.FileWriter('summary_2', graph=tf.get_default_graph()) with tf.Session() as sess: #运行并写入日志 summ = sess.run(merged_op) writer.add_summary(summ) writer.close() 需要注意的是,由于在训练模型时,图像数据常常以一个batch的形式放入模型进行训练,一个batch数据为一个4维张量,其形状为(batch_size,H,W,C),batch_size表示batch中的图像数量,H、W和C分别表示图像的高度、宽度与通道数。在使用TensorBoard可视化图像时,其也要求被记录的图像是一个4维张量,TensorBoard会将张量中的每张图像依次显示出来,一共显示batch_size张图像。因此如上代码中,使用tf.image.decode_jpeg对单张图像解码后,其形状为(H,W,C),不符合TensorBoard的要求,因此在之后又使用了tf.stack([image] * 3)将读取的图像堆叠了3次,得到的张量形状为(3,H,W,C),在最后使用TensorBoard可视化图像时会得到3张相同的图像。 运行以上程序,能够得到如图318所示的结果,从结果可以看出,3张相同的图像被依次显示在image1的选项卡下。当然,writer.add_summary此时依旧接收step参数,读者可以传入周期数或者迭代数,以方便查看图像随着周期的变化情况。 图318使用TensorBoard可视化图像 本节简要说明了TensorBoard的用法,分别介绍了使用TensorBoard对于计算图、标量及图像的可视化方法,这三者也是使用TensorBoard最常用的可视化对象。当然,TensorBoard实际上还能完成更多复杂数据的可视化,如使用直方图等可视化张量的统计信息,其使用方法与标量或图像的可视化类似,对于复杂数据的可视化本书在此并不涉及,有兴趣的读者可以自行查阅资料进行学习。 3.10小结 本章就TensorFlow的基础知识进行了讲解,从TensorFlow最基本的计算图与张量等概念讲到TensorBoard在可视化方面的使用。关于TensorFlow还有许多高级的函数与用法,读者可以自行到TensorFlow官网(https://www.tensorflow.org/)进行学习。