CS20 05. 变量共享与实验管理

本章内容英文版参见CS20 2018Winter 第五章讲义 。大部分材料翻译自讲义内容,已得到老师授权。类似的声明适用于已经发布的CS20/CS20SI下的所有文章

变量共享

命名域 (Name scope)

之前的代码在运行时可以使用TensorBoard查看计算图。但是图里节点零零散散的,比较乱,因为TensorBoard不知道哪些节点之间比较相似,哪些节点应该被汇集到一起。这样会使代码调试起来比较麻烦,尤其是如果模型比较复杂,包含上百个操作符的时候更是这样。解决办法是通过tf.name_scope声明一个代码块,将相关操作都定义在这个代码块中,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
with tf.name_scope('embed'):
embed_matrix = tf.get_variable('embed_matrix',
shape=[VOCAB_SIZE, EMBED_SIZE],
initializer=tf.random_uniform_initializer())
embed = tf.nn.embedding_lookup(embed_matrix, center_words, name='embedding')

with tf.name_scope('loss'):
nce_weight = tf.get_variable('nce_weight', shape=[VOCAB_SIZE, EMBED_SIZE],
initializer=tf.truncated_normal_initializer())
nce_bias = tf.get_variable('nce_bias', initializer=tf.zeros([VOCAB_SIZE]))

loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weight,
biases=nce_bias,
labels=target_words,
inputs=embed,
num_sampled=NUM_SAMPLED,
num_classes=VOCAB_SIZE), name='loss')

TensorBoard中有三种边

  • 灰色实箭头,表示数据流,例如tf.add(x + y)xy的值
  • 橙色实箭头,是引用边,表示操作符A会和操作符B交互
  • 虚线箭头,表示控制依赖。具体的控制依赖可以使用tf.Graph.control_dependencies(control_inputs)来设定

变量域

tf.name_scope相比,tf.variable_scope除了可以创造一个命名空间以外,还可以方便进行变量共享。举个例子:假设现在要创建一个包含两个隐藏层的神经网络, 然后用两个不同的输入x1x2调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x1 = tf.truncated_normal([200, 100], name='x1')
x2 = tf.truncated_normal([200, 100], name='x2')

def two_hidden_layers(x):
assert x.shape.as_list() == [200, 100]
w1 = tf.Variable(tf.random_normal([100, 50]), name="h1_weights")
b1 = tf.Variable(tf.zeros([50]), name="h1_biases")
h1 = tf.matmul(x, w1) + b1
assert h1.shape.as_list() == [200, 50]
w2 = tf.Variable(tf.random_normal([50, 10]), name="h2_weights")
b2 = tf.Variable(tf.zeros([10]), name="2_biases")
logits = tf.matmul(h1, w2) + b2
return logits

logits1 = two_hidden_layers(x1)
logits2 = two_hidden_layers(x2)

这样,对每个输入(这里是x1x2),TensorFlow都会创建整个一套变量,包括h1_weights、h2_weights等等。但是实际上,我们希望网络对所有输入共享某些相同的变量。因此不应该使用tf.Variable,而应该使用tf.get_variable。这样,TF会先检查变量是否已经存在。如果已经存在了,就复用它;如果不存在,就新建一个。但是如果直接改写上述代码,会得到报错信息:

ValueError: Variable h1_weights already exists, disallowed. Did you mean to set reuse=True or reuse=tf.AUTO_REUSE in VarScope?

为了避免这种现象的发生,需要把所有要使用的变量放在一个变量域里,并将该域设置为可复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fully_connected(x, output_dim, scope):
with tf.variable_scope(scope) as scope:
w = tf.get_variable("weights", [x.shape[1], output_dim], initializer=tf.random_normal_initializer())
b = tf.get_variable("biases", [output_dim], initializer=tf.constant_initializer(0.0))
return tf.matmul(x, w) + b

def two_hidden_layers(x):
h1 = fully_connected(x, 50, 'h1')
h2 = fully_connected(h1, 10, 'h2')

with tf.variable_scope('two_layers') as scope:
logits1 = two_hidden_layers(x1)
scope.reuse_variables()
logits2 = two_hidden_layers(x2)

这里是为每一层都编写一段代码。如果网络结构有很多层,而且每层逻辑相近,可以让代码变得更加可复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fully_connected(x, output_dim, scope):
with tf.variable_scope(scope) as scope:
w = tf.get_variable('weights', [x.shape[1], output_dim], initializer=tf.random_normal_initializer())
b = tf.get_variable('biases', [output_dim], initializer=tf.constant_initializer(0.0))
return tf.matmul(x, w) + b

def two_hidden_layers(x):
h1 = fully_connected(x, 50, 'h1')
h2 = fully_connected(h1, 10, 'h2')

with tf.variable_scope('two_layers') as scope:
logits1 = two_hidden_layers(x1)
scope.reuse_variables()
logits2 = two_hidden_layers(x2)

创建变量时一般会把变量放在图的不同部分,但有时可能需要一种更方便的方法来访问它们。通过调用tf.collection可以访问某一特定集合的变量

1
tf.get_collection(key, scope=None)

其中key是变量集合名称,scope是变量域。默认情况下,所有变量都存放在tf.GraphKeys.GLOBAL_VARIABLES里,如果要获取my_scope域中的所有变量,就传参给参数scope。对于所有可训练变量,变量存放在tf.GraphKeys.TRAINABLE_VARIABLES里。也可以通过tf.add_to_collection创建自己的集合(集合中的内容甚至可以不是变量而是其他操作符)。这个场景最典型的应用是,tf.train.Optimizer下的子类默认优化tf.GraphKeys.TRAINABLE_VARIABLES中的变量

管理实验

深度学习实验的运行时间通常会非常长,例如如果在单显卡的机器上训练一个神经翻译模型即便不用一个月也得需要好几天。如果运行过程中代码突然崩了,把代码从头跑一遍实在是很残忍。鲁棒的框架应该可以在任意时间点因为任何原因暂停并继续训练

tf.train.Saver

比较好的做法是在经过若干step或者epoch后周期性地保存模型参数,这样如果需要的话可以从该时间点恢复模型。具体做法是调用tf.train.Saver对象的save方法。在TensorFlow的术语表里,保存图变量的这一步称为检查点(checkpoint),因此要保存多个模型,就要创建多个检查点。通常使用global_step这一变量来跟踪训练总共执行了多少步,这样可以保存多个模型。首先,使用如下语句定义global_step变量

1
global_step = tf.Variable(0, dtype=tf.int32, trainable=False, name='global_step')

然后要将这个变量传递给优化器,这样才会在每一步之后将它加一

1
optimizer = tf.train.GradientDescentOptimizer(lr).minimize(loss,global_step=global_step)

最后通过如下代码周期性保存模型

1
2
3
4
5
6
7
8
9
10
11
12
# define model

# create a saver object
saver = tf.train.Saver()

# launch a session to execute the computation
with tf.Session() as sess:
# actual training loop
for step in range(training_steps):
truesess.run([optimizer])
trueif (step + 1) % 1000 == 0:
true saver.save(sess, 'checkpoint_directory/model_name', global_step=global_step)

使用如下代码恢复模型。如果目标目录没有检查点文件,就从头开始训练

1
2
3
ckpt = tf.train.get_checkpoint_state(os.path.dirname('checkpoints/checkpoint'))
if ckpt and ckpt.model_checkpoint_path:
saver.restore(sess, ckpt.model_checkpoint_path)

如果只想保存若干指定变量而不是整个图,可以在初始化Saver对象时指定

1
2
3
4
5
6
7
8
# 以字典形式传入
saver = tf.train.Saver({'v1': v1, 'v2': v2})

# 以列表形式传入
saver = tf.train.Saver([v1, v2])

# 传入列表相当于是传入以变量操作名为键的字典
saver = tf.train.Saver({v.op.name: v for v in [v1, v2]})

获取训练过程的统计摘要

在训练的过程中,可以使用TensorBoard提供的工具将统计信息摘要并可视化,这些信息包括当前损失函数值、平均损失函数值和准确率等等。一般会将这些操作定义在一个命名域中

1
2
3
4
5
6
7
8
def _create_summaries(self):
with tf.name_scope("summaries"):
tf.summary.scalar("loss", self.loss)
tf.summary.scalar("accuracy", self.accuracy)
tf.summary.histogram("histogram loss", self.loss)
# because you have several summaries, we should merge them all
# into one op to make it easier to manage
self.summary_op = tf.summary.merge_all()

由于得到的是一个操作符,因此需要运行它

1
loss_batch, _, summary = sess.run([model.loss, model.optimizer, model.summary_op], feed_dict=feed_dict)

然后将结果通过FileWriter对象写出,就可以在TensorBoard的Scalars页面看到结果

1
writer.add_summary(summary, global_step=step)

控制代码的随机性

为了复现他人论文的结果,有时候需要对有随机性的函数施加一些控制。通常来讲,有两种做法:

  • 在操作级别设定随机数种子。例如所有随机张量允许在初始化时传入种子值:my_var = tf.Variable(tf.truncated_normal((-1., 1.), stddev=0.1, seed=0))

  • 在图级别设置随机种子:tf.set_random_seed(seed)。这样可以使同样的结果在另一个图上复现。例如下列代码分别保存为a.py和b.py,执行得到的输出相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import tensorflow as tf

    tf.set_random_seed(2)
    c = tf.random_uniform([], -10, 10)
    d = tf.random_uniform([], -10, 10)

    with tf.Session() as sess:
    trueprint sess.run(c)
    trueprint sess.run(d)

自动微分

目前所编写的代码里,我们并没有手动求过梯度,而只是构建出一个正向传播的计算图,TensorFlow会处理反向的路径。其使用的具体方法称为反向模式自动微分(reverse mode automatic differentiation),该方法可以使计算函数\(t\)的导数所花的时间大致等同于计算原函数值的时间。原理是在图中加入额外的计算节点和边。例如,假设要计算\(C\)\(I\)的梯度,TensorFlow先会寻找连接这两个节点的路径,找到以后,TF会从\(C\)开始,反向向\(I\)移动,并针对反向遍历路径时遇到的所有操作新增一个节点,这个节点包含\(C\)对该节点的部分梯度,使用链式法则求出。部分梯度使用tf.gradients来计算

坚持原创技术分享,您的支持将鼓励我继续创作!