Lecture note 2: TensorFlow Ops
1. Fun with TensorBoard
在TensorFlow中,我们将常量、变量、操作符统称为操作符(Ops)。TensorFlow不仅是一个软件库,还包括一套软件,包括TensorFlow、TensorBoard和TensorServing。为了最大限度地利用TensorFlow,我们应该知道如何将以上所有的应用结合在一起。在这节课中,我们将首先介绍TensorBoard。
TensorBoard是图形可视化软件,包括任何标准的TensorFlow安装。用谷歌自己的话来说:“你将使用TensorFlow的计算——就像训练一个巨大的深度神经网络——可能是复杂而令人困惑的。为了更容易地理解、调试和优化TensorFlow程序,我们已经包含了一套叫做TensorBoard的可视化工具。
当完全配置时,TensorBoard将会是这样的。图像从TensorBoard的网站截取。
当用户在tensorboard被激活的TensorFlow程序中执行某些操作时,这些操作将被导出到一个事件文件中。TensorBoard能够将这些事件文件转换为图形,从而能够洞察模型的行为。尽早学会使用TensorBoard,经常会让TensorFlow的工作变得更加有趣和高效。
让我们写下你的第一个TensorFlow项目用TensorBoard来可视化 它。
a = tf.constant(2)
b = tf.constant(3)
x = tf.add(a, b)
with tf.Session() as sess:
print sess.run(x)
为了在这个程序中激活Tensorboard,在建立graph后加入一行,正好就在进行训练之前
writer = tf.summary.FileWriter(logs_dir, sess.graph)
上面的一行代码是创建一个writer对象来将操作写到事件文件中,存储在文件夹logsdir中。您可以选择logsdir为“./graphs”之类的东西。
import tensorflow as tf
a = tf.constant(2)
b = tf.constant(3) x = tf.add(a, b)
with tf.Session() as sess:
writer = tf.summary.FileWriter('./graphs', sess.graph)
print sess.run(x)
# close the writer when you’re done using it
writer.close()
接下来,到终端,运行程序。确保您当前的工作目录与运行Python代码的位置相同。
$ python [yourprogram.py]
$ tensorboard --logdir="./graphs"
打开浏览器,转到http://localhost:6006/(或运行tensorboard命令后返回的链接)。
转到网页,你会看到这样的东西:
点击Graph,可以看到3个节点的图
a = tf.constant(2)
b = tf.constant(3)
x = tf.add(a, b)
“Const”和“Const_1”对应于a和b,而节点“Add”对应于x。我们给它们的名称(a、b和x)是我们在需要时访问它们的。它们对内部TensorFlow来说毫无意义。要让TensorBoard显示你的操作的名称,你必须显式地命名它们。
a = tf.constant([2, 2], name="a")
b = tf.constant([3, 6], name="b")
x = tf.add(a, b, name="add")
现在,如果你再次运行TensorBoard,你会看到这张图:
图本身定义了操作和依赖关系,但不显示值。它只关心在运行会话session时获取值。快速提醒一下,如果你忘了:
tf.Session.run(fetches, feed_dict=None, options=None, run_metadata=None)
注意:如果您已经多次运行您的代码,将会有多个事件文件’~/dev/cs20si/graphs/lecture01’,TF只显示最新的图并显示多个事件文件的警告。要删除警告,删除您不再需要的所有事件文件。
2. Constant types
Link to documentation: https://www.tensorflow.org/api_docs/python/constant_op/“>https://www.tensorflow.org/api_docs/python/constant_op/
你可以用constants创建标量或张量的值。
tf.constant(value, dtype=None, shape=None, name='Const', verify_shape=False)
# constant of 1d tensor (vector)
a = tf.constant([2, 2], name="vector")
# constant of 2x2 tensor (matrix)
b = tf.constant([[0, 1], [2, 3]], name="b")
你可以创建一个张量,它的元素是一个特定的值,它与numpy相似。
numpy.zeros, numpy.zeros_like, numpy.ones, numpy.ones_lik的相同点:
tf.zeros(shape, dtype=tf.float32, name=None)
# create a tensor of shape and all elements are zeros
tf.zeros([2, 3], tf.int32) ==> [[0, 0, 0], [0, 0, 0]]
tf.zeros_like(input_tensor, dtype=None, name=None, optimize=True)
# create a tensor of shape and type (unless type is specified) as the input_tensor but all elements are zeros.
# input_tensor is [0, 1], [2, 3], [4, 5]]
tf.zeros_like(input_tensor) ==> [[0, 0], [0, 0], [0, 0]]
tf.ones(shape, dtype=tf.float32, name=None)
# create a tensor of shape and all elements are ones
tf.ones([2, 3], tf.int32) ==> [[1, 1, 1], [1, 1, 1]]
tf.ones_like(input_tensor, dtype=None, name=None, optimize=True)
# create a tensor of shape and type (unless type is specified) as the input_tensor but all elements are ones.
# input_tensor is [0, 1], [2, 3], [4, 5]]
tf.ones_like(input_tensor) ==> [[1, 1], [1, 1], [1, 1]]
tf.fill(dims, value, name=None)
# create a tensor filled with a scalar value.
tf.fill([2, 3], 8) ==> [[8, 8, 8], [8, 8, 8]]
你可以创建一个常数序列
tf.linspace()
tf.linspace(start, stop, num, name=None)
# create a sequence of num evenly-spaced values are generated beginning at start. If num > 1, the values in the sequence increase by stop - start / num - 1, so that the last one is exactly stop
# start, stop, num must be scalars
# comparable to but slightly different from numpy.linspace
# numpy.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)
tf.linspace(10.0, 13.0, 4, name="linspace") ==> [10.0 11.0 12.0 13.0]
tf.range()
tf.range(start, limit=None, delta=1, dtype=None, name='range')
# create a sequence of numbers that begins at start and extends by increments of delta up to but not including limit
# slight different from range in Python
# 'start' is 3, 'limit' is 18, 'delta' is 3
tf.range(start, limit, delta) ==> [3, 6, 9, 12, 15]
# 'start' is 3, 'limit' is 1, 'delta' is -0.5
tf.range(start, limit, delta) ==> [3, 2.5, 2, 1.5]
# 'limit' is 5
tf.range(limit) ==> [0, 1, 2, 3, 4]
注意,与NumPy或Python序列不同,TensorFlow序列是不可迭代的。
for _ in np.linspace(0, 10, 4): # OK
for _ in tf.linspace(0, 10, 4): # TypeError("'Tensor' object is not iterable.")
for _ in range(4): # OK
for _ in tf.range(4): # TypeError("'Tensor' object is not iterable.")
你也可以从某些分布中产生随机常数
tf.random_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)
tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)
tf.random_uniform(shape, minval=0, maxval=None, dtype=tf.float32, seed=None, name=None)
tf.random_shuffle(value, seed=None, name=None)
tf.random_crop(value, size, seed=None, name=None)
tf.multinomial(logits, num_samples, seed=None, name=None)
tf.random_gamma(shape, alpha, beta=None, dtype=tf.float32, seed=None, name=None)
3. Math Operations
TensorFlow的数学运算是相当标准的,非常类似于NumPy。
详细请见https://www.tensorflow.org/api_docs/python/math_ops/arithmetic_operators
a = tf.constant([3, 6])
b = tf.constant([2, 2])
tf.add(a, b) # >[5 8]
tf.add_n([a, b, b]) # >[7 10]. Equivalent to a + b + b
tf.mul(a, b) # >[6 12] because mul is element wise
tf.matmul(a, b) # >ValueError
tf.matmul(tf.reshape(a, shape=[1, 2]),
tf.reshape(b, shape=[2, 1])) # >[[18]]
tf.div(a, b) # >[1 3]
tf.mod(a, b) # >[1 0]
下面是Python中的操作表,由““Fundamentals of Deep Learning”的作者提供。
4. Data Types
Python Native Types
TensorFlow采用Python的原有类型,例如Python布尔值、数值(整数、浮点数)和字符串。单个值将被转换为0-d张量(或标量),lists将被转换为1-d张量(向量),lists的list将被转换为2-d张量(矩阵),以此类推。下面的例子来自“TensorFlow for Machine Intelligence”。
t_0 = 19 # Treated as a 0-d tensor, or "scalar"
tf.zeros_like(t_0) # ==> 0 tf.ones_like(t_0) # ==> 1
t_1 = [b"apple", b"peach", b"grape"] # treated as a 1-d tensor, or "vector"
tf.zeros_like(t_1) # ==> ['' '' '']
tf.ones_like(t_1) # ==> TypeError: Expected string, got 1 of type 'int' instead.
t_2 = [[True, False, False],
[False, False, True],
[False, True, False]] # treated as a 2-d tensor, or "matrix"
tf.zeros_like(t_2) # ==> 2x2 tensor, all elements are False
tf.ones_like(t_2) # ==> 2x2 tensor, all elements are True
TensorFlow Native Types
和NumPy一样,TensorFlow也有自己的数据类型,就像你看到的tf.int32,tf。float32。
下面是来自TensorFlow官方文档的当前TensorFlow数据类型的列表。
NumPy Data Types
现在,您可能已经注意到了NumPy和TensorFlow之间的相似之处。TensorFlow被设计为与Numpy无缝集成,这一方案已经成为数据科学的通用语言。
TensorFlow的数据类型是基于NumPy的;实际上,np.int32==tf.int32返回True。您可以将NumPy类型传递给TensorFlow操作。
例子:
tf.ones([2, 2], np.float32) ==> [[1.0 1.0], [1.0 1.0]]
还记得tf.Session.run(fetches)吗?如果它需要一个tensor,那么输出就是一个NumPy数组
大多数情况下,你可以交替使用TensorFlow类型和NumPy类型。
Note 1: 这里有一个字符串数据类型的捕获。对于数值型和布尔型类型,TensorFlow和NumPy dtypes都是匹配的。然而,tf.string,由于NumPy处理字符串的方式,字符串在NumPy中没有确切的匹配。TensorFlow仍然可以从NumPy中导入字符串数组,这完全可以——只是不要在NumPy中指定dtype!
Note 2: TensorFlow和NumPy都是n-d数组库。NumPy支持ndarray,但不提供创建张量函数和自动计算导数的方法,也不提供GPU支持。所以TensorFlow还是赢了!
Note 3:使用Python类型来指定TensorFlow对象是快速而简单的,并且它对于原型设计非常有用。然而,这样做有一个很重要的陷阱。Python类型缺乏显式地声明数据类型的能力,但是TensorFlow的数据类型更具体。例如,所有的整数都是相同的类型,但是TensorFlow有8位、16位、32位和64位的整数。因此,如果使用Python类型,TensorFlow必须推断出您的数据类型。
当您将数据转换为TensorFlow时,可以将数据转换为适当的类型,但是某些数据类型仍然难以正确地声明,例如复数。因此,将手动定义的张量对象创建为NumPy数组是很常见的。但是,在可能的情况下,总是使用TensorFlow类型,因为TensorFlow和NumPy都可以发展到这样一个点,这样的兼容性就不再存在了。
5. Variables 变量
常数一直都很有趣,但我认为你们现在已经足够了解变量了。常数和变量的不同点在于:
1.常数是恒定的,而变量可以被赋值,它的值可以被改变。
2.一个常量的值存储在图中,它的值在加载图的任何地方都被复制。变量是单独存储的,并且可以在一个参数服务器上生存。
第2点基本上意味着常量存储在图定义中。当常量在内存中,每次加载图形时都会很慢。要查看图的定义和存储在图的定义中的内容,只需打印出图的原始buf。原生buf表示协议缓冲区,“Google的语言中立、平台无关、可扩展的用于序列化结构化数据的机制——考虑XML,但更小、更快、更简单。”
import tensorflow as tf
my_const = tf.constant([1.0, 2.0], name="my_const") print tf.get_default_graph().as_graph_def()
Output:
node {
name: "my_const"
op: "Const"
attr {
key: "dtype"
value {
type: DT_FLOAT
}
}
attr {
key: "value"
value {
tensor {
dtype: DT_FLOAT
tensor_shape {
dim {
size: 2
}
}
tensor_content: "\000\000\200?\000\000\000@"
}
}
}
}
versions {
producer: 17
}
Declare variables
要声明一个变量,你需要创建一个类 tf.Variable的实例。请注意,tf.constant,tf.Variable 不是tf.variable 因为tf.constant 是一个 op,而 tf.Variable 是一个类。
#create variable a with scalar value
a = tf.Variable(2, name="scalar")
#create variable b as a vector
b = tf.Variable([2, 3], name="vector")
#create variable c as a 2x2 matrix
c = tf.Variable([[0, 1], [2, 3]], name="matrix")
# create variable W as 784 x 10 tensor, filled with zeros
W = tf.Variable(tf.zeros([784,10]))
tf.Variable holds several ops:
x = tf.Variable(...)
x.initializer # init x.value() # read op x.assign(...) # write op x.assign_add(...) # and more
你要在使用变量前先初始化他. 如果在初始化这些变量之前尝试使用这些变量,那么就会报错:尝试使用未初始化的张量。Attempting to use uninitialized value tensor.
最简单的方法就是一次性初始化所有的变量,用tf.global_variables_initializer()
init = tf.global_variables_initializer()
with tf.Session() as sess: tf.run(init)
提示:当你使用tf.run()来初始化的时候,并没有给他们任何值。
要初始化一个变量的子集,您需要使用tf.variables_initializer(),加上你想要初始化的变量列表
init_ab = tf.variables_initializer([a, b], name="init_ab")
with tf.Session() as sess: tf.run(init_ab)
还可以分别初始化每个变量,用 tf.Variable.initializer
# create variable W as 784 x 10 tensor, filled with zeros
W = tf.Variable(tf.zeros([784,10]))
with tf.Session() as sess:
tf.run(W.initializer)
另一个初始化的方法是从保存文件里重新加载。接下来会讲。
Evaluate values of variables
如果输出初始化后的变量,我们只能看到tensor
# W is a random 700 x 100 variable object
W = tf.Variable(tf.truncated_normal([700, 10]))
with tf.Session() as sess:
sess.run(W.initializer) print W
>> Tensor("Variable/read:0", shape=(700, 10), dtype=float32)
如果想要获取变量的值,需要使用eval()
# W is a random 700 x 100 variable object
W = tf.Variable(tf.truncated_normal([700, 10]))
with tf.Session() as sess:
sess.run(W.initializer)
print W.eval()
>> [[-0.76781619 -0.67020458 1.15333688 ..., -0.98434633 -1.25692499
-0.90904623]
[-0.36763489 -0.65037876 -1.52936983 ..., 0.19320194
-0.38379928 0.44387451]
[ 0.12510735 -0.82649058 0.4321366 ..., -0.3816964 0.70466036 1.33211911]
...,
[ 0.9203397 -0.99590844 0.76853162 ..., -0.74290705 0.37568584 0.64072722]
[-0.12753558 0.52571583 1.03265858 ..., 0.59978199 -0.91293705 -0.02646019]
[ 0.19076447 -0.62968266 -1.97970271 ..., -1.48389161 0.68170643 1.46369624]]
Assign values to variables
可以通过 tf.Variable.assign() 给变量赋值
W = tf.Variable(10) W.assign(100)
with tf.Session() as sess:
sess.run(W.initializer)
print W.eval() # >> 10
为什么是10而不是100呢?W.assign(100)没有将W赋值为100,而是创建了一个赋值操作。要让这个操作起作用,需要在session中运行它。
W = tf.Variable(10)
assign_op = W.assign(100)
with tf.Session() as sess:
sess.run(assign_op)
print W.eval() # >> 100
注意:我们并没有初始化W,因为assign()已经为我们初始化了。事实上,初始化操作是分配给变量本身初始值的赋值操作。
# in the source code
self._initializer_op = state_ops.assi(self._variable,self._initial_value,
validate_shape=validate_shape).op
有趣的例子:
# create a variable whose original value is 2
a = tf.Variable(2, name="scalar")
# assign a * 2 to a and call that op a_times_two
a_times_two = a.assign(a * 2)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
# have to initialize a, because a_times_two op depends on the value of a sess.run(a_times_two) # >> 4
sess.run(a_times_two) # >> 8
sess.run(a_times_two) # >> 16
为了实现简单的变量加减, TensorFlow 有tf.Variable.assign_add() 和 tf.Variable.assign_sub() 方法, 与tf.Variable.assign(),不同,tf.Variable.assign_add() 和 tf.Variable.assign_sub() 不能初始化变量, 因为这些操作依赖变量的初始值。
W = tf.Variable(10)
with tf.Session() as sess:
sess.run(W.initializer)
print sess.run(W.assign_add(10)) # >> 20
print sess.run(W.assign_sub(2)) # >> 18
因为TensorFlow会话单独维护值,每个会话都可以为在图中定义的变量拥有自己的当前值。
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)) # >> 20 print sess2.run(W.assign_sub(2)) # >> 8
print sess1.run(W.assign_add(100)) # >> 120 print sess2.run(W.assign_sub(50)) # >> -42
sess1.close()
sess2.close()
当然,您可以声明一个依赖于其他变量的变量。
假设,你想声明一个U = W * 2
# W is a random 700 x 100 tensor
W = tf.Variable(tf.truncated_normal([700, 10]))
U = tf.Variable(W * 2)
这样的话,就要保证在初始化U之前用 initialized_value() 先初始化W。
U = tf.Variable(W.intialized_value() * 2)
6. InteractiveSession
你有时候会看到InteractiveSession 而不是 Session,他们两唯一的区别是唯一的区别是,InteractiveSession使自己成为默认的会话,这样您就可以调用run()或eval(),而不需要显式地调用会话。
这在交互式shell和IPython笔记本中是很方便的,因为它可以避免通过显式的会话对象来运行操作。
但是,当您有多个会话要运行时,这是很复杂的。
sess = tf.InteractiveSession()
a = tf.constant(5.0)
b = tf.constant(6.0)
c = a * b
# We can just use 'c.eval()' without passing 'sess'
print(c.eval())
sess.close()
用tf.InteractiveSession.close()来关闭一个 InteractiveSession。
tf.get defaultsession()返回当前线程的默认会话。
返回的会话将是Session或Session.as_default()。
7. Control Dependencies ##
有时,我们会有两个独立的操作但是你想要指定应该先运行哪个op,那就要使用 tf.Graph.control_dependencies(control_inputs)
例子:
# your graph g have 5 ops: a, b, c, d, e with
g.control_dependencies([a, b, c]):
# `d` and `e` will only run after `a`, `b`, and `c` have executed.
d = ...
e = …
8. Placeholders and feed_dict ##
记得第一节课说TensorFlow程序包括两个阶段,
阶段1:建立一张图
阶段2:使用一个会话来执行图中的操作。
因此,我们可以先建立图,而不需要知道计算所需的值。
这等价于定义x,y的函数,不知道x,y的值。
例如:f(x, y) = x*2 + y
x,y是实际值的占位符。
在图建立完成后,我们或客户端可以在需要执行计算时提供自己的数据。
定义一个占位符,我们使用:
tf.placeholder(dtype, shape=None, name=None)
Dtype是指定占位符值数据类型的必需参数。
shape指定了被接受为占位符的实际值的张量的形状。
shape=None意味着任何形状的张量都将被接受。
使用shape=None很容易构造图,但调试的时候却是噩梦。
你应该尽可能详细地定义你的占位符的shape。
你也可以给你的占位符起一个名字,就像你在TensorFlow中的任何其他操作一样。
更多关于官方文档中占位符的信息。
# create a placeholder of type float 32-bit, shape is a vector of 3 elements
a = tf.placeholder(tf.float32, shape=[3])
# create a constant of type float 32-bit, shape is a vector of 3 elements
b = tf.constant([5, 5, 5], tf.float32)
# use the placeholder as you would a constant or a variable
c = a + b # Short for tf.add(a, b)
If we try to fetch c, we will run into error.
with tf.Session() as sess: print(sess.run(c))
>> NameError
在运行的时候会报错,是因为计算C,需要a的值,但是a只是一个占位符,没有实际的值。我们需要先给a喂进一个值
with tf.Session() as sess:
# feed [1, 2, 3] to placeholder a via the dict {a: [1, 2, 3]}
# fetch value of c print(sess.run(c, {a: [1, 2, 3]}))
>> [6. 7. 8.]
让我们看看在 TensorBoard上是什么样的。加上
writer = tf.summary.FileWriter('./my_graph', sess.graph)
并且在终端上输入
$ tensorboard --logdir='my_graph'
正如你所看到的,占位符和其他操作一样被处理,3是占位符的shape。
在前面的例子中,我们为占位符提供一个单一的值。如果我们想要将多个数据点提供给占位符,该怎么样呢?
这是一个合理的假设,因为我们经常需要在训练或测试集中通过多个数据点进行计算。
我们可以通过遍历数据集并以一个值一次的feed来输入占位符,以满足占位符的任何数据点。
with tf.Session() as sess:
for a_value in list_of_a_values:
print(sess.run(c, {a: a_value}))
你也可以把值喂给一个tensor,任何一个可以被喂值的tensor都可以。判断一个tensor是否可以被喂值,用
tf.Graph.is_feedable(tensor)
# create Operations, Tensors, etc (using the default graph)
a = tf.add(2, 5) b = tf.mul(a, 3)
# start up a `Session` using the default graph
sess = tf.Session()
# define a dictionary that says to replace the value of `a` with 15
replace_dict = {a: 15}
# Run the session, passing in `replace_dict` as the value to`feed_dict`
sess.run(b, feed_dict=replace_dict) # returns 45
feed_dict对您的模型进行测试是非常有用的。当你有一个大的图并且想要测试某些部分时,你可以提供虚拟的值。因此,TensorFlow不会浪费时间去做不必要的计算
9. The trap of lazy loading* ##
我看到的最常见的TensorFlow的非bug的 bug(我曾经提到过)是我的朋友Danijar和我所称的“lazy loading”。
lazy loading是指当您延迟声明/初始化一个对象直到它被加载时,它指的是一个编程模式。
在TensorFlow中
,意思是你推迟创建一个op,直到你需要计算它。
例如,这是正常的加载:你在建立图时创建了op z。
x = tf.Variable(10, name='x')
y = tf.Variable(20, name='y')
z = tf.add(x, y)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for _ in range(10):
sess.run(z)
writer.close()
这是当某人决定聪明,并使用延迟加载来保存一行代码时所发生的事情:
x = tf.Variable(10, name='x')
y = tf.Variable(20, name='y')
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for _ in range(10):
sess.run(tf.add(x, y))
# create the op add only when you need to compute it
writer.close()
让我们在TensorBoard上看看这个图。
正常的加载图看起来和我们期望的一样。
好吧,节点“Add”缺失了,这是可以理解的,因为我们在给FileWriter写了图之后添加了“Add”。
这使得阅读图表变得更加困难,但它并不是一个错误。那么,有什么大不了的呢?
让我们看一下图像的定义。
记住,要打印出图形的定义,我们使用:
print tf.get_default_graph().as_graph_def()
在正常加载图的protobuf中,图的原型只有1个节点“添加”:
node {
name: "Add"
op: "Add"
input: "x/read"
input: "y/read"
attr {
key: "T"
value {
type: DT_INT32
}
}
}
而在延迟加载的图上的protobuf中有10个“Add”节点。
每次你想要计算z时,它都会添加一个新的节点“添加”。
node {
name: "Add"
op: "Add"
...
}
node {
name: "Add_9"
op: "Add"
...
}
你可能会想:“这太蠢了。为什么我要多次计算相同的值呢?并且认为这是一个没有人会提交的错误。
它发生的频率比你想象的要高。例如,你可能想要计算相同的损失函数,或者在一定数量的培训样本之后进行一些预测。
在发现解它之前,你已经计算了数千次,并在图中添加了数千个不必要的节点。
你的图定义变得冗余,加载速度慢,并且代价昂贵。
有两种方法可以避免这种bug。
首先,在可以的时候,总是将操作的定义和它们的执行分开。
但是如果不可能,因为你想将相关的操作组分组到类中,您可以使用Python属性来确保你的函数在第一次调用时只加载一次。
这不是一门Python课程,所以我不会深入探讨如何去做。
但是,如果你想知道,看看Danijar Hafner的这篇精彩的博客文章。