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 | import os |
就可以去掉警告信息)
但是有时候若要训练大量深度神经网络,代码可能会变得复杂、让人理不清头绪。为此,TensorFlow开发团队提供了一种对图进行可视化的工具TensorBoard,意图使得开发人员更容易理解、调试和优化TF程序。此外,TensorBoard还可以画出一些定量指标
将前面的代码做一些修改
1 | import tensorflow as tf |
在定义好图以后,运行相关联的会话之前,可以通过tf.summary.FileWriter
对象将运行过程中的事件文件保存下来。将上面文件保存以后,在终端中运行如下命令
1 | $ python [yourprogram].py |
然后在浏览器中访问URL http://localhost:6006
,可以看到在TensorBoard中已经给出了对应的计算图,如图所示
但是,图中的各个操作名称并没有使用程序里定义的名称(是“Const”而不是“a”)。为了使TensorBoard中的变量名称与程序中的一样,需要在定义操作的时候明确写出它们的名字,例如
1 | import tensorflow as tf |
常量
在上面的程序中,使用tf.constant
定义了一个常量张量。这个方法的完整签名为
1 | tf.constant(value, dtype=None, shape=None, name='Const', verify_shape=False) |
TensorFlow的常量与numpy一样具有广播机制:
1 | import tensorflow as tf |
TF还提供了其它函数来快速创建某些特殊常量,例如
tf.zeros(shape, dtype=tf.float32, name=None)
创建了一个形状为shape
的张量,该张量所有值都为0tf.zeros_like(input_tensor, dtype=None, name=None, optimize=True)
创建了一个形状和数据类型都与input_tensor
相同的张量,新张量所有值都为0tf.ones(shape, dtype=tf.float32, name=None)
与tf.ones_like(input_tensor, dtype=None, name=None, optimize=True)
的作用与上述两个函数类似,只不过这两个函数得到的张量所有值都是1tf.fill(dims, value, name=None)
创建一个形状为dims
的张量,其中每个值都为value
tf.linspace
和tf.range
使用一个序列来创建常量。注意tf.range
是不可迭代的- 其它与随机初始化有关的操作不详赘
需要注意的是,由于zeros_like
和ones_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中有很多关于除法的操作,例如div
、divide
、truediv
、floordiv
、realdiv
、truncatediv
、floor_div
等,需要细读文档来了解它们的区别。简单说,tf.div
做的是“TF风格”的除法,而tf.divide
做的是python风格的除法
变量
概述
常量的值一经初始化后就不能再修改了。此外,其最大问题是它会被存储在图的定义中。可以使用如下代码查看图的定义:
1 | import tensorflow as tf |
因此当常量很大时,载入计算图就变得非常吃力。因此,对需要大量内存的数据,应该使用变量或者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 | init = tf.global_variables_initializer() |
也可以只初始化一部分变量
1 | init = tf.variables_initializer([a, b], name='init_ab') |
也可以初始化一个变量
1 | W = tf.Variable(tf.zeros([784, 10])) |
初始化以后,要打印该变量的值,有两种方法。一种是直接run这个变量
1 | # W is a 784 x 100 variable of random values |
另一种是使用tf.Variable.eval()
1 | with tf.Session() as sess: |
变量在创建之后就可以赋值,但是注意如下代码片段会产生意想不到的结果
1 | W = tf.Variable(10) |
这段代码的输出是10,而不是100。为什么呢?因为W.assign(100)
是一个操作,并没有真的将100赋值给W
。前面提到,每个操作都必须得执行才能产生效果,因此需要修改成下面这样
1 | W = tf.Variable(10) |
这里对W
初始化方法的调用可以略去,因为assign_op
做了初始化操作。实际上,初始化操作在源码里就是调用了assign
操作,将变量的初始值赋给变量自身
再来看一个例子
1 | my_var = tf.Variable(2, name='my_var') |
这段代码执行以后会输出8,因为每次调用my_var_times_two
都会将my_var
自乘2(笔者感觉像是C++和Java里的*=
)。类似于C++和Java里的+=
和-=
,TF提供了类似的操作assign_add
和assign_sub
。不过这两个使用之前必须要初始化变量
每个会话都会单独维护一份对某变量的拷贝。例如
1 | W = tf.Variable(10) |
会输出20和8
可以用一个变量去初始化另一个变量,例如
1 | # W is a random 700 x 100 tensor |
这种做法很常见,但是不安全。更安全的写法是
1 | # W is a random 700 x 100 tensor |
这样可以保证W
在用来初始化U
之前自己已经被初始化过
占位符
回顾之前提到的TF程序的最大特点:其由图的定义和图的执行两部分构成。TF提供了一种操作,使得图可以不知道计算时候需要什么样的值也能被定义出来。这种操作就称为占位符(placeholder)。定义占位符时,不需要提供具体的值,只需要指出数据的类型(必传参数)、形状和名字。真正运行计算图时,使用字典为占位符代表的变量传值,例如
1 | a = tf.placeholder(tf.float32, shape=[3]) |
注意placeholder
的shape
参数可以为None
(默认也为None
)。当其为None
时,意味着可以接受任何形状的变量。这种做法易于构建计算图,但是对调试是个噩梦。而且此时后面的所有类型推导都无效,使得很多操作符都无法工作,因为很多操作符需要确定形状的变量
需要注意的是,使用字典可以给任意可feed的张量赋值(tf.Graph.is_feedable(tensor)
)。占位符指明的是必须用字典赋值的张量,其它通过计算产生的张量也可以通过这种方法被覆盖,例如
1 | a = tf.add(2, 5) |
这里会打出45。因为a
的值在会话里被通过feed_dict
覆盖了
延迟加载
所谓延迟加载(lazy loading),指的是某个对象在需要时才被创建/初始化的做法。考虑如下例子
1 | x = tf.Variable(10, name='x') |
其对应的延迟加载版本为
1 | x = tf.Variable(10, name='x') |
两者不管是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 | # 假设图g有5个操作符: a, b, c, d, e |