天天看点

《深入了解TensorFlow》笔记——Chapter 4.2 模型参数模型参数使用流程保存和恢复模型参数变量作用域

文章目录

  • 模型参数使用流程
    • 创建模型参数
    • 初始化模型参数
    • 更新模型参数
  • 保存和恢复模型参数
    • 保存模型参数
    • 恢复模型参数
  • 变量作用域
    • tf.Variable的局限
    • 变量作用域的优势
    • 变量作用域的使用

模型参数是指模型的权重值和偏置值。TensorFlow模型参数的使用方法,包括模型参数的创建、初始化和更新,以及从模型参数的存储和恢复方法。

模型参数使用流程

TF中模型参数的使用流程如下图所示:

《深入了解TensorFlow》笔记——Chapter 4.2 模型参数模型参数使用流程保存和恢复模型参数变量作用域

图中,

tf.Variable()

类实现了数据流图上的存储节点,它能够在操作执行完成后仍然保存变量值,因此我们使用它来存储模型参数。为了训练模型,我们需要一次创建、初始化以及更新模型参数。

tf.train.Saver()

类是辅助训练的工具类,它实现了存储模型参数的变量和checkpoint文件的读写。Checkpoint文件是以<变量名,张量值>形式序列化存储模型参数的二进制文件,它是用户持久化存储模型参数的推荐文件格式,扩展名为ckpt。

“存储”是指将变量中模型参数定期写入到checkpoint file中,一般按照固定训练步数设置检查点,进行模型参数保存。

“恢复”是指读取chekpoint file中的模型参数,进行推理或者继续训练。

TF提供选择性存储和恢复部分任意变量的方法,使得用户可以灵活改造模型,并基于之前的训练结果进行参数微调。

创建模型参数

创建模型参数,主要是为了确定模型参数的基本属性,包括初始值、数据类型、张量形状和变量名称等。一般使用tf.Variable()创建变量。

示例代码:

import tensorflow as tf
W = tf.Variable(initial_value=tf.random_normal(shape=1, 4), mean=100, stddev=0.35, name='W')
           

其中,initial_value参数表示在会话中变量设置的初始值,他接受张量和生成张量的方法,如

tf.random_normal()

方法。它也接受通过convert_to_tensor()方法转换为张量的数据类型。除了生成符合正态分布的张量外,TF还提供许多生成特定张量的方法,主要包括三类:

  1. 符合某种统计分布的随机张量

    (1)

    tf.random_normal

    ,正态分布

    (2)

    tf.truncated_normal

    ,截尾正态分布

    (3)

    tf.random_uniform

    ,均匀分布

    (4)

    tf.multinomial

    ,多项式分布

    (5)

    tf.random_gamma

    ,伽马分布

    (6)

    tf.random_shuffle

    ,按维度打乱

    (7)

    tf.random_crop

    ,按形状裁剪
  2. 符合生成规则的序列张量

    (1)

    tf.linspace

    ,初始值为start值,生成规则(stop - start) / (num - 1)

    (2)

    tf.range

    ,初始值为start值,生成规则 +delta
  3. 常量张量

    (1)

    tf.zeros

    ,初始值为0,tf.zeros([2, 3], tf.float32)

    (2)

    tf.zeros_like

    , 初始值为0,tf.zeros_like([[1, 2], [3, 4]])

    (3)

    tf.ones

    ,初始值为1,tf.ones([2, 3], tf.float32)

    (4)

    tf.ones_like

    ,初始值为1, tf.ones_like([[1, 2], [3, 4]])

    (5)

    tf.fill

    ,初始值为value, tf.fill([2, 3], 9)

    (6)

    tf.constant

    ,初始值为value, tf.constant(-1, shape=[2, 3])

注,用户不能直接使用变量实例作为新变量的初始值,不然在执行初始化操作时,两个变量会出现循环依赖。

根据上面示例,我们采用

tf.random_normal()

返回的随机张量初始化模型参数W,因此该变量的子图不再包含初始值,而是一个依赖前置操作random_normal。同时tf.random_normal()是定义在graph中的一个操作,所以需要variables_initializer()进行初始化。

当我们在会话中执行初始化操作时(如

tf.global_variables_initializer

),程序内部调用变量W的Assign操作,而Assign操作有依赖random_normal操作。因此,初始化变量的完整过程是程序执行random_normal操作,将返回的张量输入到Assign操作,最后Assign操作将这些张量赋值到变量W。

初始化模型参数

TF提供了两种初始化变量的选择,一种是传入初始值,然后执行初始化操作赋值;另一种是从checkpoint文件中恢复变量的值。本小节只关注前者。

最常用的初始化操作便是

tf.global_variables_initalizers()

,只要在Session中执行它,程序就会自动初始化全局变量。示例如下:

import tensorflow as tf

w = tf.Variable(tf.random_normal(shape=(1, 4), stddev=0.35), name='weight')
b = tf.Variable(tf.zeros([4]), name='bias')
with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	print(sess.run([w, b]))

           

除了一次性初始化所有变量外,TF还支持初始化部分变量,即

tf.variables_initalizer()

方法。通过变量列表var_list显式地初始化部分变量。示例如下:

import tensorflow as tf

w = tf.Variable(tf.random_normal(shape=(1, 4), stddev=0.35), name='weight')
b = tf.Variable(tf.zeros([4]), name='bias'
# partial initialization
with tf.Session() as sess:
    tf.variables_initializer([w]).run()
    print(w.eval()
           

变量列表var_list本质上是同类变量的几何。在创建变量时,我们通过collections参数显式地制定变量所属的集合类别,不同类别集合拥有不同关键字。如果没有指定collections参数,那么variable默认属于

GraphKey.GLOBAL_VARIABLES

,即该变量将添加到全局变量集合。如果将variable的参数trainable设置为True,那么该变量会加入训练参数集合,类别关键字为

GraphKeys.TRAINABLE_VARIABLES

TF内置的五种变量集合:

  • tf.global_variables

    , 类别关键字GraphKeys.GLOBAL_VARIABLES,跨设备的全局变量集合
  • tf.local_variables

    , 类别关键字GraphKeys.LOCAL_VARIABLES,进程内本地变量集合
  • tf.model_variables

    , 类别关键字GraphKeys.MODEL_VARIABLES,进程内模型参数变量的集合
  • tf.trainable_variables

    , 类别关键字GraphKeys.TRAINABLE_VARIABLES,存储需要训练的模型参数变量集合
  • tf.moving_average_variables

    ,类别关键字GraphKeys.MOVING_AVERAGE_VARIABLES,使用指数移动平均的变量集合。

【问】什么是使用指数移动平均的变量?

此外,

tf.global_variables_initializer()

方法便是通过

tf.variables_initializer()

方法实现的。其定义如下:

def globla_variables_initializer():
	return variables_initializer(var_list=global_variables())
           

类似的

tf.local_variables_initializer()

也是如此实现的。值得一提的是,在执行分布式训练任务时,

tf.global_variables_initializer()

方法能够实现跨设备全局变量初始化。

当需要检验变量是否成功初始化时,TF提供了相关的判断和断言方法:

  • tf.is_variable_initialized()

    ,检查变量是否初始化
  • tf.report_uninitialized_variables()

    ,获取未初始化的变量集合
  • tf.assert_variables_initialized()

    ,断言变量已经初始化

更新模型参数

模型参数都是保存在变量中,因此更新模型参数主要是指更新存储在变量中的模型参数。

变量是数据流图中的存储节点,也是3种节点中唯一有状态的节点。对于无状态的节点,例如计算节点,他的输出由输入tensor和节点操作共同确定。因为节点表示的操作是不变的,所以无状态节点的输出是有输入张量唯一确定。如:

a = tf.placeholder(tf.float32)
b = a * a
           

在不改变乘法操作的情况下,b的值由输入张量a唯一确定。

而对于有状态的节点,如存储节点,他的输出还会受到内部状态影响:

x = tf.placeholder(shape=[4, 1], dtype=tf.float32)
w = tf.Variable(tf.random_normal(shape=(1, 4), stddev=0.35), name='weight')
b = tf.Variable(tf.zeros(4), name='bias')
y = tf.matmul(w, x) + b
with tf.Session() as sess:
	tf.variables_initializer(tf.global_variables()).run()
	sess.run(y, feed_dict={x: [[1], [2], [3], [4]]})
           

在不改变矩阵成分和加法操作的情况下,y的输出值由输入x,变量w和变量b共同决定。

更新模型参数本质上就是对变量中保存的模型参数重新赋值。TF提供的更新变量的方法包括:

  • tf.assign(),直接赋值
  • tf.assign_add(),加法赋值
  • tf.assign_sub(),减法赋值

    后两种赋值方法本质上是第一种的封装。变量更新示例:

import tensorflow as tf
w = tf.Variable(tf.random_normal(shape=(1, 4), stddev=0.35), name='weight')
with tf.Session() as sess:
	tf.variables_initializer(tf.global_variables()).run()
	w.eval()
	tf.assign_add(w, tf.ones([1, 4])).eval()
           

当利用TF训练模型师,

Optimizer.apply_gradients()

方法内部也会调用上述变量跟新方法完成模型参数更新。在多线程训练任务重,可能涉及对变量的并发读写,因此TF为变量提供了更加安全的更新方法,通过加锁机制确保数据一致性。

tf.assign()

方法的输入参数use_locking默认为True,即TF默认对变量的并发读写加锁。

保存和恢复模型参数

tf.train.Saver是辅助训练的工具类,它实现了存储模型参数和读写checkpoint文件的功能。当创建Saver示例时,graph将会添加一对SaveOp & RestoreOp操作。其中,SaveOp负责向checkpoint file写入模型参数,RestoreOp负责从checkpoint file读取模型参数。

保存模型参数

tf.train.Saver()构造方法的主要输入参数:

  • var_list,Saver存储的变量集合,默认值为全局变量集合
  • reshape,是否允许从checkpoint file中恢复时改变变量形状,默认值为True
  • sharded,是否将checkpoint file中的变量轮循放置在所有设备上,默认值为True
  • max_to_keep,保留最近的检查点的个数,默认值为5
  • restore_sequentially,是否按顺序恢复所有变量,当模型较大时顺序恢复可以降低内存使用,默认值为True。

在创建Saver示例时,我们可以通过var_list参数设置想要存储的变量集合,默认为全局变量集合。变量在checkpoint file中是以键值对形式<变量名,张量值>序列化存储的。其中变量名的默认值为变量操作的名称,不便于我们未来恢复时寻找特定变量,因此在变量命名时赋予一个有实际意义的名称。

【注】var_list中的变量不能出现重复名称,否则会导致错误。

Saver存储变量的示例如下:

#!/usr/bin/env python
# coding=utf-8
import tensorflow as tf
import os
import shutil

if os.path.exists('./temp'):
    print('=> deleting old temporary directory ...')
    shutil.rmtree('./temp')
    print('=> creating new temporary directory ...')
    os.makedirs('./temp')
else:
    print('=> creating temporary directory ...')
    os.makedirs('./temp')

# create variables
x = tf.placeholder(dtype=tf.float32)
w = tf.Variable(0.0, name='weight')
b = tf.Variable(1.0, name='bias')
y = w*x + b

# create saver instance
saver = tf.train.Saver(var_list=tf.global_variables())
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for i in range(4):
        sess.run(tf.assign_add(w, 1.0))
        sess.run(tf.assign_sub(b, 0.001))
        print('w={}, b={}'.format(w.eval(), b.eval()))
        saver.save(sess, 'temp/test_{:02d}.ckpt'.format(i+1))
        print('=> checkpoint file have been successfuly written down')
           

我们通过

tf.train.Saver(var_list=tf.global_variables())

指令,将全局变量都进行了存储。如果过只需要存储部分模型参数变量,则可以通过python list 或者 dict显式指定。

恢复模型参数

当需要基于某个checkpoint file继续训练模型时,应该使用Saver.restore()方法恢复文件中的变量值,而不是使用tf.variables_initializer()方法为他们设置初始值。Saver.restore()方法需要显式地设置加载变量值的Session和存储变量的checkpoint file path。示例如下:

#!/usr/bin/env python
# coding=utf-8
import tensorflow as tf
import os
import shutil


# create variables
x = tf.placeholder(dtype=tf.float32)
w = tf.Variable(0.0, name='weight')
b = tf.Variable(1.0, name='bias')
y = w * x + b

saver = tf.train.Saver()
with tf.Session() as sess:
    for i in range(4):
        model_path = './temp/test_{:02d}.ckpt'.format(i+1)
        print('=> load model params from checkpoint file ...')
        saver.restore(sess, model_path)

        for j in range(2):
            sess.run(tf.assign_sub(w, 1.0))
            sess.run(tf.assign_add(b, 1.0))
            print('=> w={}, b={}'.format(w.eval(), b.eval()))
# 输出
"""
=> load model params from checkpoint file ...
=> w=0.0, b=1.999000072479248
=> w=-1.0, b=2.99900007247924
=> load model params from checkpoint file ...
=> w=1.0, b=1.9980000257492065
=> w=0.0, b=2.99800014495849
=> load model params from checkpoint file ...
=> w=2.0, b=1.996999979019165
=> w=1.0, b=2.99699997901916
=> load model params from checkpoint file ...
=> w=3.0, b=1.996000051498413
=> w=2.0, b=2.99600005149841
"""
           

变量作用域

上节我们详细介绍了tf.Variable()类的创建和使用,以及模型参数的使用流程。但是,对于编写深度神经网络模型时,这种瀑布流式的模型定义方法就无法轻松解决了。因此,本节通过使用更灵活和具有层次化结构的变量作用域,来帮助我们处理更加复杂的模型。

tf.Variable的局限

以CNN为例,该模型包含两个卷积层,示例代码如下:

def image_filter(input_image):
	conv1_weight = tf.Variable(tf.random_normal([5, 5, 32, 32]), name='conv1_w')
	conv1_bias = tf.Variable(tf.zeros([32]), name='conv1_b')
	conv1 = tf.nn.conv2d(input_images, conv1_weight, 
						strides=[1, 1, 1, 1], padging='SAME')
	relu1 = tf.nn.relu(conv1 + conv1_bias)

	conv2_weight = tf.Variable(tf.random_normal([3, 3, 32, 64]), name='conv1_w')
	conv2_bias = tf.Variable(tf.zeros([64]), name='conv2_b')
	conv2 = tf.nn.conv2d(input_images, conv2_weight, 
						strides=[1, 1, 1, 1], padging='SAME')
	relu2 = tf.nn.relu(conv2 + conv2_bias)
	return relu2
           

上述网络定义包含了四个不同的模型参数conv1_weight, conv1_bias, conv2_weight, and conv2_bias。随着网络深度的增加,代码复杂度将持续增加。

同时,这种方法还存在模型复用问题。假设要多次调用改模型,

tf.Variable()

方法在每次调用模型时都会重新创建变量,但他们存储的是相同的模型参数。随着模型复用次数的增加,内存开销也不断上升。

变量作用域的优势

事实上,网络层数增加到500层,常用的网络结构单元也只有十几种。非常自然地想法便是,编写管理各类网络的方法,在该方法内部定义该类网络的结构和参数。同时该方法在复用模型时,允许共享该层模型参数。TF中的变量作用域机制以一种优雅、轻量、非入侵式的方式实现了上述想法,有效解决了tf.Variable的局限性。

TF的变量作用域机制主要有

tf.get_variable()

方法和

tf.variable_scope()

方法实现。前者负责创建或获取指定名称的变量,后者负责管理传入

tf.get_variable()

方法的变量名称的名字空间。

tf.get_variable()

方法的主要输入参数:

  • name:变量名称
  • shape:变量形状
  • initializer:初始化方法,例如,

    tf.constant_initializer()

    ,

    tf.random_uniform_initializer()

    ,

    tf.random_normal_initializer()

    等等

它和tf.Variable()方法的不同在于,后者直接使用初始值initial_value,前者是在运行时根据张量的形状动态初始化变量。

示例代码:

def conv_relu(input_image, kernel_size, bias_size):
	# create weight variable
	weight = tf.get_variable('weight', kernel_size, tf.random_normal_initializer())
	# create bias variable
	bias = tf.get_variable('bias', bias_size, tf.constant_initializer(0.0))
	conv = tf.nn.conv2d(input_image, weight, strides=[1, 1, 1, 1], padding='SAME')
	output = tf.nn.relu(conv + bias)
	return output
           

变量作用域的使用

我们根据上述conv_relu方法,使用变量作用域,创建两层图像卷积网络:

def image_filter(input_image):
	with tf.variable_scope('conv1'):
		# create 'conv1/weight' and 'conv1/bias' variables
		layer1 = conv_relu(input_image, [5, 5, 32, 32], [32])
	with tf.variable_scope('conv2'):
		# create 'conv2/weight' and 'conv2/bias' variables
		layer2 = conv_relu(layer1, [3, 3, 32, 64], [64])
	return layer2
           

变量作用域将with上下文语句块中定义的变量都加上作用域前缀(i.e. ‘conv1’ or ‘conv2’),这样能够通过不同的变量作用域区分同类网络层。但是,还没有解决模型复用的问题,如果第二次调用image_filter,

tf.get_variable()

方法就会报错(变量已存在,无法再定义)。我们只需要给

tf.variable_scope()

方法加上reuse参数即可,表示共享该作用域内的变量,修改后代码如下所示:

def image_filter(input_image):
	with tf.variable_scope('conv1', reuse=True):
		# create 'conv1/weight' and 'conv1/bias' variables
		layer1 = conv_relu(input_image, [5, 5, 32, 32], [32])
	with tf.variable_scope('conv2', reuse=True):
		# create 'conv2/weight' and 'conv2/bias' variables
		layer2 = conv_relu(layer1, [3, 3, 32, 64], [64])
	return layer2
           

同时,变量作用域也支持嵌套定义:

with tf.variable_scope('foo'):
	with tf.variable_scope('bar'):
		v = tf.get_variable('v', [1])
assert v.name == 'foo/bar/v:0'
           

此外,变量作用域还能为作用域内所有变量设置初始化方法:

with tf.variable_scope("foo", initializer=tf.constant_initializer(0.4)):
	v = tf.get_variable('v', [1])
	assert v.eval() == 0.4
	w = tf.get_variable('w', [2], initializer=tf.constant_initializer(0.1))
	assert w.eval() == 0.1
	with tf.variable_scope('bar'):
		v = tf.get_variable('v', [3])
		assert v.eval() == 0.4
	with tf.variable_scope('conv', initializer=tf.random_normal_initializer(0.1))
		v = tf.get_variable('v', [1])
		assert v.eval() == 0.1
           

继续阅读