2.4 认识变量
从初识TensorFlow开始,变量这个名词就一直都很重要,因为深度模型往往所要获得的就是通过参数和函数对某一或某些具体事物的抽象表达。而那些未知的数据需要通过学习而获得,在学习的过程中它们不断变化,最终收敛达到较好的表达能力,因此它们无疑属于变量。
当训练模型时,用变量来存储和更新参数。变量存放于内存的缓存区。建模时它们需要被明确地初始化,模型训练后它们必须被存储到磁盘。这些变量的值可在之后模型训练和分析时被加载。
通过之前的学习,可以列举出以下TensorFlow的函数:
var=tf.get_variable(name,shape,initializer=initializer)
上述函数都和TensorFlow的参数有关,主要包含在以下两类中:
● tf.Variable类。
● tf.train.Saver类。
从变量存在的整个过程来看,其主要包括变量的创建、初始化、更新、保存和加载。
2.4.1 变量的创建
当创建一个变量时,一个张量作为初始值将被传入构造函数Variable()。TensorFlow提供了一系列操作符来初始化张量,初始值是常量或是随机值。注意,所有这些操作符都需要指定张量的shape。变量的shape通常是固定的,但TensorFlow提供了高级的机制来重新调整其行列数。
可以创建以下类型的变量:常数、序列、随机数。
【例2-2】 创建常数变量的例子。
运行程序,输出如下:
【例2-3】 创建数字序列变量的例子。
运行程序,输出如下:
此外,TensorFlow 有几个操作用来创建不同分布的随机张量。注意,随机操作是有状态的,并在每次评估时会创建新的随机值。
下面是一些相关函数的介绍:
(1)tf.random_normal
该函数用于从正态分布中输出随机值。其语法格式为:
其中,参数含义为:
● shape:一维整数或 TensorFlow数组,表示输出张量的形状。
● mean:dtype 类型的0-D张量或TensorFlow值,表示正态分布的均值。
● stddev:dtype 类型的0-D张量或 TensorFlow值,表示正态分布的标准差。
● dtype:输出的类型。
● seed:一个TensorFlow整数,用于为分发创建一个随机种子。
● name:操作的名称(可选)。
函数将返回一个指定形状的张量,通过符合要求的随机值填充。
(2)tf.truncated_normal
该函数生成的值遵循具有指定平均值和标准差的正态分布,与 tf.random_normal 不同之处在于其平均值大于2个标准差的值将被丢弃并重新选择。其语法格式为:
其中,参数含义为:
● shape:一维整数或TensorFlow数组,表示输出张量的形状。
● mean:dtype 类型的 0-D 张量或 TensorFlow值,表示截断正态分布的均值。
● stddev:dtype 类型的 0-D 张量或TensorFlow值,表示截断前正态分布的标准偏差。
● dtype:输出的类型。
● seed:一个TensorFlow整数,用于为分发创建随机种子。
● name:操作的名称(可选)。
函数返回指定形状的张量,通过随机截断的符合要求的值填充。
(3)tf.random_uniform
该函数从均匀分布中输出随机值。语法格式为:
其中,生成的值在[minval,maxval) 范围内遵循均匀分布。下限 minval 包含在范围内,而上限 maxval 被排除在外。参数含义为:
● shape:一维整数或 TensorFlow数组,表示输出张量的形状。
● minval:dtype 类型的0-D张量或TensorFlow值。生成的随机值范围的下限;默认为0。
● maxval:dtype 类型的0-D张量或TensorFlow值。要生成的随机值范围的上限。如果dtype是浮点,则默认为1。
● dtype:输出的类型有float16、float32、float64、int32、orint64。
● seed:一个 TensorFlow整数。用于为分布创建一个随机种子。
● name:操作的名称(可选)。
函数用于填充随机均匀值的指定形状的张量。
(4)tf.random_shuffle
函数用于随机地将张量沿其第一维度打乱。语法格式为:
张量沿着维度0被重新打乱,使得每个value[i][j]被映射到唯一一个 output[m][j]。例如,一个3×2张量可能出现的映射是:
参数含义为:
● value:将被打乱的张量。
● seed:一个TensorFlow整数,用于为分布创建一个随机种子。
● name:操作的名称(可选)。
● 返回:与value具有相同的形状和类型的张量,沿着它的第一个维度打乱。
(5)tf.random_crop
函数用于随机地将张量裁剪为给定的大小。语法格式为:
以一致选择的偏移量将一个形状size部分从value中切出。需要的条件是value.shape >=size。
如果大小不能裁剪,请传递该维度的完整大小。例如,可以使用 size=[crop_height,crop_width,3]裁剪RGB图像。
cifar10中就有利用该函数随机裁剪24×24大小的彩色图片的例子,代码如下:
distorted_image=tf.random_crop(reshaped_image,[height,width,3])
random_crop函数的参数的含义为:
● value:向裁剪输入张量。
● size:一维张量,大小等级为value。
● seed:TensorFlow整数,用于创建一个随机的种子。
● name:此操作的名称(可选)。
函数与value具有相同的秩并且与size具有相同形状的裁剪张量。
(6)tf.multinomial
函数为从多项式分布中抽取样本。语法格式为:
其中,参数含义为:
● logits:形状为[batch_size,num_classes]的二维张量;每个切片[i,:]表示所有类的非标准化对数概率。
● num_samples:表示0维张量,为每行切片绘制的独立样本数。
● seed:TensorFlow整数,用于为分发创建一个随机种子。
● name:操作的名称(可选)。
函数返回绘制样品的形状 [batch_size,num_samples]。
(7)tf.random_gamma
函数为从每个给定的伽玛分布中绘制shape样本。语法格式为:
其中,alpha是形状参数,beta是尺度参数。其他参数含义为:
● shape:一维整数张量或 TensorFlow 数组。输出样本的形状是按照 alpha/beta-parameterized分布绘制的。
● alpha:一个张量或者TensorFlow值或者dtype类型的N-D数组。
● beta:一个张量或者TensorFlow值或者dtype类型的N-D数组,默认为1。
● dtype:alpha、beta的类型,输出float16,float32或float64。
● seed:一个TensorFlow整数,用于为分布创建一个随机种子。
● name:操作的名称(可选)。
函数返回具有dtype类型值的带有形状tf.concat(shape,tf.shape(alpha+beta))的张量。
(8)tf.set_random_seed
函数用于设置图形级随机seed。作用在于可以在不同的图中重复那些随机变量的值。语法格式为:
set_random_seed(seed)
可以从两个seed中获得依赖随机seed的操作:图形级seed和操作级seed。seed必须是整数,对大小没有要求,只是作为图形级和操作级标记使用,本节将介绍如何设置图形级seed。
它与操作级别seed的关系如下:
● 如果既没有设置图层级也没有设置操作级别的seed,则使用随机seed进行该操作。
● 如果设置了图形级seed,但操作seed没有设置,则系统确定性地选择与图形级seed结合的操作seed,以便获得唯一的随机序列。
● 如果未设置图形级 seed,但设置了操作 seed,则使用默认的图层 seed 和指定的操作seed来确定随机序列。
● 如果图层级seed和操作seed都被设置,则两个seed将一起用于确定随机序列。
具体来说,使用seed应牢记以下3点:
1)要在会话里的不同图中生成不同的序列,请不要设置图层级seed或操作级seed。
2)要为会话中的操作在不同图中生成相同的可重复序列,请为该操作设置seed。
3)要使所有操作生成的随机序列在会话中的不同图中都可重复,请设置图形级别seed。
【例2-4】 创建随机变量。
上述函数都含有seed参数,属于操作级seed。
在TensorFlow中,提供了range()函数用于创建数字序列变量,有以下两种形式:
● range(limit,delta=1,dtype=None,name='range').
● range(start,limit,delta=1,dtype=None,name='range').
该数字序列开始于start并且将以delta为增量扩展到不包括limit时的最大值结束,类似Python的range函数。
【例2-5】 利用range函数创建数字序列。
运行程序,输出如下:
2.4.2 变量的初始化
变量的初始化必须在模型的其他操作运行之前先明确地完成。最简单的方法就是添加一个对所有变量初始化的操作,并在使用模型前首先运行这个操作。使用 tf.global_variables_initializer()来添加一个操作对变量进行初始化。例如:
有时候会需要用另一个变量的初始化值给当前变量进行初始化。由于 tf.global_variables_initializer()是并行地初始化所有变量,所以用其他变量的值初始化一个新的变量时,要使用其他变量的 initialized_value()属性。可以直接把已初始化的值作为新变量的初始值,或者把它当作张量计算,得到一个值赋予新变量。例如:
assign()函数也有初始化的功能。TensorFlow 中 assign()函数可用于对变量进行更新,包括变量的value和shape。涉及以下函数:
● tf.assign(ref,value,validate_shape=None,use_locking=None,name=None).
● tf.assign_add(ref,value,use_locking=None,name=None).
● tf.assign_sub(ref,value,use_locking=None,name=None).
● tf.variable.assign(value,use_locking=False).
● tf.variable.assign_add(delta,use_locking=False).
● tf.variable.assign_sub(delta,use_locking=False).
这6个函数本质上是一样的,都用于对变量值进行更新,其中tf.assign还可以更新变量的shape。
其中tf.assign是用value的值赋给ref,这种赋值会覆盖掉原来的值,即更新而不会创建一个新的tensor;tf.assign_add相当于利用ref=ref+value来更新ref;tf.assign_sub相当于利用ref=ref-value来更新ref;tf.variable.assign相当于tf.assign(ref,value);tf.variable.assign_add和tf.variable.assign_sub也是同理。
tf.assign函数的语法格式为:
tf.assign(ref,value,validate_shape=None,use_locking=None,name=None)
其中,参数含义为:
● ref:一个可变的张量,应该来自变量节点,节点可能未初始化,参考下面的例子。
● value:张量,必须具有与ref相同的类型,是要分配给变量的值。
● validate_shape:一个可选的bool,默认为True。如果为True,则操作将验证“value”的形状是否与分配给张量的形状相匹配;如果为False,“ref”将对“值”的形状进行引用。
● use_locking:一个可选的bool,默认为True。如果为True,则分配将受锁保护;否则,表示该行为是未定义的,可能会显示较少的争用。
● name:操作的名称(可选)。
函数返回一个在赋值完成后将保留“ref”新值的张量。
现在举3个例子,说明3个问题:
【例 2-6】 assign 操作会初始化相关的节点,并不需要 tf.global_variables_initializer()初始化,但是并非所有的节点都会被初始化。
【例 2-7】 tf.assign()操作可以改变变量的 shape,只需要令参数 validate_shape=False默认为True。
【例2-8】 assign会在图中产生额外的操作,可用tf.Variable.load(value,session)实现从图外赋值不产生额外的操作。
另外,这里还应该说明的是,还有3种读取数据的方法:Feeding、文件中读取、加载预训练数据。它们都属于给变量初始化的方式。为了不引起混淆,必须说明的是常量也是变量,而3种读取数据的方法都是读取常量的方法,但依然是初始化的一种常见方式。
2.4.3 变量的更新
虽然assign()函数有对变量进行更新的作用,但是此处探讨的更新却不是如此简单。事实上,我们不需要做什么具体的事情,因为TensorFlow会自动求导求梯度,根据代价函数自动更新参数。这是全局参数的更新,也是由 TensorFlow 学习的机制自动确定的。那么TensorFlow如何知道究竟哪个是变量,哪个是常量呢?很简单,tf.variable()里面有一个布尔型的参数trainable,表示这个参数是否为需要学习的变量,而它默认为True,因此很容易被忽略,就这样TensorFlow图会把它加入GraphKeys.TRAINABLE_VARIABLES,从而对其进行更新。
2.4.4 变量的保存
对于训练的变量,成功的话都是有意义的,需要将其保存在文件里,方便以后的测试和再训练,这就是weights文件,是必不可少的。
2.4.5 变量的加载
加载变量和保存变量是一正一反的过程,保存变量是要把模型里的变量信息保存到weights文件里,而加载变量就是要把这些有意义的变量值从weights文件加载到模型里。
2.4.6 共享变量和变量命名空间
前面应用的变量都是单个变量的简单使用,在实际的应用过程中,特别是在使用递归神经网络的时候,若创建一个比较复杂的模块,通常会共享一些变量的值,这时就需要变量可以共享。
先举一个简单的例子,来看看什么时候需要使用共享变量。假设现在有一批(x,y)的数据,我们预先知道这些数据的分布满足公式:
y=weight×x+bias
但是公式中的weight和bias我们不知道,需要通过训练才能得到。同时在训练过程中,每训练10个样本就用一批测试数据去测试一下损失值是多少。
实际过程中的训练数据和测试数据都采用模拟生成数据的方式代替,通过例2-9实现。【例2-9】 训练y=weight×x+bias的TensorFlow代码。
在以上代码中,tf.placeholder()的作用相当于一个占位变量,它的实际值在构建图的过程中没有提及,需要在会话中执行计算时通过参数feed_dict传递进去。
在此将公式的计算过程放到了函数inference()中。因为训练过程就是训练inference函数中的weight和bias变量,而测试过程需要用到相同的weight和bias,所以训练过程和测试过程都会调用inference函数。在构建训练过程中,先调用inference()构建y=weight×x+bias的向前计算过程的操作和变量,计算得到的值train_y会和实际数据的train_label值做差的平方运算,得到一个衡量它们之间相差多大的值train_loss(通常称作损失函数,用来衡量训练过程的结果和实际数据的结果相差多大)。然后通过调用tf.train.GradientDescentOptimizer得到随机梯度下降的优化函数,它的作用是根据损失函数的值,帮助用户在训练过程中确定参数更新的方向,以及更新参数的大小,目标是让下次计算的损失值变小。在计算图中,最后的train_op就是训练过程中最后一步的操作。
在会话中,循环1000次计算,每次计算取一个样本值,通过feed_dict传给train_x和train_y,通过执行 train_op 计算一次当前样本的损失值,并且通过优化函数自动更新一次weight和bias的参数,目标是让更新后的weight和bias的值再次计算时,损失值往变小的方向走。每执行10次train_op,就执行一次测试,测试的计算逻辑基本和训练过程一样,就是将样本的值放进去计算,测试结果的损失值并将其打印出来。
用户所期望的是随着计算步数的增加,测试过程得到的损失值会慢慢减少,最终接近于0,并且变量weight和bias的值也慢慢接近于2和10。
但是实际的打印结果是:
从实际的结果来看,计算的test_loss的损失值一直都没有变化。这是为什么呢?
这是因为在第二次调用 inference()函数时,虽然调用的是同一个函数,但是在第二次调用时重新构建了一份新的weight和bias的变量,所以在训练过程中修正的其实是第一个inference()函数构造的weight和bias的值,而测试过程用到的weight和bias值并没有更新。
那么,如何实现训练过程和测试过程使用的是同一套变量呢?可以使用TensorFlow的变量作用域和共享变量来实现变量共享。
变量作用域机制在TensorFlow中主要由以下两部分组成。
● tf.get_variable(<name>,<shape>,<initializer>):创建或返回给定名称的变量。
● tf.variable_scope(<scope_name>):管理传给get_variable()的变量名称的作用域。
先说作用域,变量作用域通过 tf.variable_scope(<scope_name>)来声明,其作用类似于C++的namespace。
共享变量tf.get_variable怎么用呢?不同于tf.variable直接定义一个变量,tf.get_variable可能会新定义一个变量,也可能共享前面已经出现的变量,这取决于当前位置的变量作用域是否是重用(reuse)的(当前位置的变量作用域的reuse属性通过tf.get_variable_scope().reuse_variable()来设置)。
tf.get_variable是新建变量还是共享前面已经定义的变量,通过如下逻辑来判断:
● 如果当前位置变量的作用域不可重用,并且前面已经有重名的变量,则报错。
● 如果当前位置变量的作用域不可重用,并且前面没有重名的变量,则新建一个变量。
● 如果当前位置变量的作用域可重用,但是前面没有出现过同样名字的变量,则报错。
● 如果当前位置变量的作用域可重用,前面也出现过相同名字的变量,则共享前面的变量。
调用tf.get_variable创建变量时,必须给变量指定一个名字,将这个指定的名字加在变量所处的变量命名空间的名字后面,作为这个变量实际的名字,只有实际的名字相同,才认为两个变量的名字相同。所以,不同变量作用域下名字相同的变量被认为是不同的变量名字。
那么,对于前面的例子,要达到共享变量的目的,只需要将变量的定义部分修改成tf.get_variable()的方式,并且加上变量命名空间和变量reuse设定就可以了。修改后的代码如【例2-10】所示。
【例2-10】 采用共享变量的方式训练y=weight×x+bias的TensorFlow代码。
运行程序,输出如下:
可以看到,测试的损失值越来越接近于0,这说明两个inference()函数中的操作和变量可以共享了。
变量的 reuse 属性的作用范围可以向它的子作用域传递。当跳出作用域的范围时,变量的reuse会变成上一层作用域的reuse的值,代码为: