CS20SI 02. 操作

TensorBoard

在上一讲,给出了一小段TensorFlow代码,这段代码是比较简单的,容易理解

(如果运行时看见警告信息The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.,可以在import tensorflow之前加上这么一段代码:

1
2
3
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf

就可以去掉警告信息)

但是有时候若要训练大量深度神经网络,代码可能会变得复杂、让人理不清头绪。为此,TensorFlow开发团队提供了一种对图进行可视化的工具TensorBoard,意图使得开发人员更容易理解、调试和优化TF程序。此外,TensorBoard还可以画出一些定量指标

将前面的代码做一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import tensorflow as tf


a = tf.constant(2)
b = tf.constant(3)
x = tf.add(a, b)

# 在2017年的课件里,下面这一行写在了session中。但是2018年的课件老师说明
# 应该写在图定义之后,执行session之前
writer = tf.summary.FileWriter('./graphs', tf.get_default_graph())
with tf.Session() as sess:
print(sess.run(x))

writer.close()

在定义好图以后,运行相关联的会话之前,可以通过tf.summary.FileWriter对象将运行过程中的事件文件保存下来。将上面文件保存以后,在终端中运行如下命令

1
2
$ python [yourprogram].py
$ tensorboard --logdir="./graphs" --port 6006

然后在浏览器中访问URL http://localhost:6006,可以看到在TensorBoard中已经给出了对应的计算图,如图所示

TensorBoard中的计算图
TensorBoard中的计算图

但是,图中的各个操作名称并没有使用程序里定义的名称(是“Const”而不是“a”)。为了使TensorBoard中的变量名称与程序中的一样,需要在定义操作的时候明确写出它们的名字,例如

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


a = tf.constant(2, name='a')
b = tf.constant(3, name='b')
x = tf.add(a, b, name='add')
writer = tf.summary.FileWriter('./graphs', tf.get_default_graph())

with tf.Session() as sess:
print(sess.run(x))

writer.close()

常量

在上面的程序中,使用tf.constant定义了一个常量张量。这个方法的完整签名为

1
tf.constant(value, dtype=None, shape=None, name='Const', verify_shape=False)

TensorFlow的常量与numpy一样具有广播机制:

1
2
3
4
5
6
7
8
import tensorflow as tf
a = tf.constant([2, 2], name='a')
b = tf.constant([[0, 1], [2, 3]], name='b')
x = tf.multiply(a, b, name='mul')
with tf.Session() as sess:
trueprint(sess.run(x))
# >> [[0 2]
# [4 6]]

TF还提供了其它函数来快速创建某些特殊常量,例如

  • tf.zeros(shape, dtype=tf.float32, name=None)创建了一个形状为shape的张量,该张量所有值都为0
  • tf.zeros_like(input_tensor, dtype=None, name=None, optimize=True)创建了一个形状和数据类型都与input_tensor相同的张量,新张量所有值都为0
  • tf.ones(shape, dtype=tf.float32, name=None)tf.ones_like(input_tensor, dtype=None, name=None, optimize=True)的作用与上述两个函数类似,只不过这两个函数得到的张量所有值都是1
  • tf.fill(dims, value, name=None)创建一个形状为dims的张量,其中每个值都为value
  • tf.linspacetf.range使用一个序列来创建常量。注意tf.range是不可迭代的
  • 其它与随机初始化有关的操作不详赘

需要注意的是,由于zeros_likeones_like这两个函数得到的张量的数据类型也由输入数据指定,因此会得到一些意料之外的结果。例如:

  • t_1 = ['apple', 'peach', 'banana']tf.zeros_like(t_1)会得到['', '', ''],而tf.ones_like(t_1)会抛出类型异常
  • t_1 = [False, True, True]tf.zeros_like(t_1)会得到[False, False, False],而tf.ones_like(t_1)会得到[True, True, True]

定义常量时,数据类型是一个很微妙的点。首先,尽量不要使用Python的原生类型,因为TF需要去推断具体的类型。例如,在Python里,整数只对应一个int型,但是在TF里有8-bit、16-bit、32-bit和64-bit四种。其次,尽管目前numpy的数据类型和TF的数据类型可以无缝对接,但是仍然应该尽量使用TF的数据类型(例如tf.float32),因为可能会有一天两者发展到不再互相兼容的时候。而且numpy的数组对GPU计算不太友好

TensorFlow中有很多关于除法的操作,例如divdividetruedivfloordivrealdivtruncatedivfloor_div等,需要细读文档来了解它们的区别。简单说,tf.div做的是“TF风格”的除法,而tf.divide做的是python风格的除法

变量

概述

常量的值一经初始化后就不能再修改了。此外,其最大问题是它会被存储在图的定义中。可以使用如下代码查看图的定义:

1
2
3
4
import tensorflow as tf

my_const = tf.constant([1., 2.], name='my_const')
print(tf.get_default_graph().as_graph_def())

因此当常量很大时,载入计算图就变得非常吃力。因此,对需要大量内存的数据,应该使用变量或者reader。定义变量的方法与定义常量类似

1
a = tf.Variable(2, name='a')

注意,这里是tf.Variable,首字母大写,而常量是tf.constant,首字母小写。两者之间有差异的原因是tf.Variable是一个类,而tf.constant是一个操作符。实际上,tf.Variable里面包含了一些操作,例如初始化(initializer)、读(value())、写(assign(...))等

推荐使用tf.get_variable来创建变量。这个方法一方面适用于共享变量,不会每次都会创建一个新变量;另一方面,如果真的遇到了命名冲突,系统会出错,而不是自己处理

初始化

使用变量之前,必须要初始化。可以使用如下方法来查看没有被初始化的变量

1
print(session.run(tf.report_uninitialized_variables()))

最简单的方法是一次初始化所有变量

1
2
3
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)

也可以只初始化一部分变量

1
2
3
init = tf.variables_initializer([a, b], name='init_ab')
with tf.Session() as sess:
sess.run(init)

也可以初始化一个变量

1
2
3
W = tf.Variable(tf.zeros([784, 10]))
with tf.Session() as sess:
sess.run(W.initializer)

初始化以后,要打印该变量的值,有两种方法。一种是直接run这个变量

1
2
3
4
5
# W is a 784 x 100 variable of random values
V = tf.get_variable("normal_matrix", shape=(784, 10), initializer=tf.truncated_normal_initializer())
with tf.Session() as sess:
truesess.run(tf.global_variables_initializer())
trueprint(sess.run(V))

另一种是使用tf.Variable.eval()

1
2
3
with tf.Session() as sess:
truesess.run(tf.global_variables_initializer())
trueprint(V.eval())

变量在创建之后就可以赋值,但是注意如下代码片段会产生意想不到的结果

1
2
3
4
5
W = tf.Variable(10)
W.assign(100)
with tf.Session() as sess:
sess.run(W.initializer)
print(W.eval())

这段代码的输出是10,而不是100。为什么呢?因为W.assign(100)是一个操作,并没有真的将100赋值给W。前面提到,每个操作都必须得执行才能产生效果,因此需要修改成下面这样

1
2
3
4
5
6
W = tf.Variable(10)
assign_op = W.assign(100)
with tf.Session() as sess:
# sess.run(W.initializer)
sess.run(assign_op)
print(W.eval())

这里对W初始化方法的调用可以略去,因为assign_op做了初始化操作。实际上,初始化操作在源码里就是调用了assign操作,将变量的初始值赋给变量自身

再来看一个例子

1
2
3
4
5
6
7
8
my_var = tf.Variable(2, name='my_var')
my_var_times_two = my_var.assign(2 * my_var)

with tf.Session() as sess:
sess.run(my_var.initializer)
sess.run(my_var_times_two)
sess.run(my_var_times_two)
print(my_var.eval())

这段代码执行以后会输出8,因为每次调用my_var_times_two都会将my_var自乘2(笔者感觉像是C++和Java里的*= )。类似于C++和Java里的+=-=,TF提供了类似的操作assign_addassign_sub。不过这两个使用之前必须要初始化变量

每个会话都会单独维护一份对某变量的拷贝。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
W = tf.Variable(10)

sess1 = tf.Session()
sess2 = tf.Session()

sess1.run(W.initializer)
sess2.run(W.initializer)

print(sess1.run(W.assign_add(10)))
print(sess2.run(W.assign_sub(2)))

sess1.close()
sess2.close()

会输出20和8

可以用一个变量去初始化另一个变量,例如

1
2
3
# W is a random 700 x 100 tensor
W = tf.Variable(tf.truncated_normal([700, 10]))
U = tf.Variable(2 * W)

这种做法很常见,但是不安全。更安全的写法是

1
2
3
# W is a random 700 x 100 tensor
W = tf.Variable(tf.truncated_normal([700, 10]))
U = tf.Variable(2 * W.initialized_value())

这样可以保证W在用来初始化U之前自己已经被初始化过

占位符

回顾之前提到的TF程序的最大特点:其由图的定义和图的执行两部分构成。TF提供了一种操作,使得图可以不知道计算时候需要什么样的值也能被定义出来。这种操作就称为占位符(placeholder)。定义占位符时,不需要提供具体的值,只需要指出数据的类型(必传参数)、形状和名字。真正运行计算图时,使用字典为占位符代表的变量传值,例如

1
2
3
4
5
6
a = tf.placeholder(tf.float32, shape=[3])
b = tf.constant([5, 5, 5], tf.float32)
c = a + b # short for tf.add(a, b)

with tf.Session() as sess:
print(sess.run(c, {a: [1, 2, 3]}))

注意placeholdershape参数可以为None(默认也为None)。当其为None时,意味着可以接受任何形状的变量。这种做法易于构建计算图,但是对调试是个噩梦。而且此时后面的所有类型推导都无效,使得很多操作符都无法工作,因为很多操作符需要确定形状的变量

需要注意的是,使用字典可以给任意可feed的张量赋值(tf.Graph.is_feedable(tensor))。占位符指明的是必须用字典赋值的张量,其它通过计算产生的张量也可以通过这种方法被覆盖,例如

1
2
3
4
5
6
a = tf.add(2, 5)
b = tf.multiply(a, 3)

with tf.Session() as sess:
replace_dict = {a: 15}
print(sess.run(b, feed_dict=replace_dict))

这里会打出45。因为a的值在会话里被通过feed_dict覆盖了

延迟加载

所谓延迟加载(lazy loading),指的是某个对象在需要时才被创建/初始化的做法。考虑如下例子

1
2
3
4
5
6
7
8
9
10
11
x = tf.Variable(10, name='x')
y = tf.Variable(20, name='y')
z = tf.add(x, y)

# you create the node for add node before executing the graph
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
writer = tf.summary.FileWriter('./my_graph/l2', sess.graph)
for _ in range(10):
sess.run(z)
writer.close()

其对应的延迟加载版本为

1
2
3
4
5
6
7
8
9
10
x = tf.Variable(10, name='x')
y = tf.Variable(20, name='y')

# you create the node for add node before executing the graph
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
writer = tf.summary.FileWriter('./my_graph/l2', sess.graph)
for _ in range(10):
sess.run(tf.add(x, y))
writer.close()

两者不管是TensorBoard中的图还是打印出的图定义都有区别,而图定义中的区别非常明显。使用tf.get_default_graph().as_graph_def()打出图定义对应的protobuf文件,可以发现正常加载的图里只有一个Add节点,而延迟加载的版本里有10个!更确切地说,每循环一次,都会产生一个Add节点。当循环次数变多的时候,图就会变得非常大,加载缓慢,变量传递代价极高

可以有两种方法避免这种现象的出现:

  • 将操作符的定义与计算分开
  • 使用Python的property来保证函数在第一次调用之后也被装载(确切说是使用了装饰器)

Danijar的一篇博客给出了一个例子,可供参考

其它杂项

交互式会话

可以通过tf.InteractiveSession()定义一个交互式会话。这种会话和普通tf.Session的差别是它被默认设为默认会话,因此定义以后可以直接调用run()eval()操作,不用再显式地打开一个会话。这种会话在IPython notebook里比较有用,但是当创建多个会话时,行为会变得复杂

控制依赖

可以通过计算图的control_dependencies来控制操作符执行的顺序

1
2
3
4
5
# 假设图g有5个操作符: a, b, c, d, e
with g.control_dependencies([a, b, c]):
# `d`和`e`在`a`, `b`, `c`执行后才能执行
d = ...
e = ...
坚持原创技术分享,您的支持将鼓励我继续创作!