Tensorflow:
- 使用图 (graph) 来表示计算任务
- 在被称之为 会话 (Session) 的上下文 (context) 中执行图
- 使用 tensor 表示数据
- 通过 变量 (Variable) 维护状态
- 使用 feed 和 fetch 可以为任意的操作(arbitrary operation) 赋值或者从其中获取数据
一般可将程序看作由两个互相独立的部分组成:
- 构建计算图 (
tf.Graph
) - 运行计算图(使用 `tf.Session``)
计算图
计算图是排列成一个图的一系列 TensorFlow 指令。图由两种类型的对象组成。
- 操作(简称“op”—operation 的缩写):图的节点。描述了消耗和生成张量的计算。
- 张量:图的边。它们代表将流经图的值。大多数 TensorFlow 函数会返回
tf.Tensors
构建一个简单的计算图:
import rensorflow as tf
a = tf.constant(3.0, dtype=tf.float32)
b = tf.constant(4.0) # also tf.float32 implicitly
total = a + b
print(a)
print(b)
print(total)
out:
Tensor("Const_1:0", shape=(), dtype=float32)
Tensor("Const_2:0", shape=(), dtype=float32)
Tensor("add:0", shape=(), dtype=float32)
请注意,打印张量并不会输出值 3.0
、4.0
和 7.0
。上述语句只会构建计算图。这些 tf.Tensor
对象仅代表将要运行的操作的结果。
图中的每个指令都拥有唯一的名称。这个名称不同于使用 Python 分配给相应对象的名称。张量是根据生成它们的指令命名的,后面跟着输出索引,如上文的 "add:0"
所示。
我们如何直观看到我们构建的计算图呢?可以使用Tensorboard工具实现计算图的可视化:
import os
os.mkdir('C:/logfile')
writer = tf.summary.FileWriter('C:/logfile')
writer.add_graph(tf.get_default_graph())
上面的代码将在'C:/logfile'目录中生成一个 event 文件,在cmd中运行tensorboard --logdir=C:\logfile命令后即可在浏览器中输入http://localhost:6006来查看运算图:
会话 (Session)
要评估张量,需要实例化一个 tf.Session
对象(非正式名称为会话)。会话会封装 TensorFlow 运行时的状态,并运行 TensorFlow 操作。如果说 tf.Graph
像一个 .py
文件,那么 tf.Session
就像一个 python
可执行对象。
下面的代码会创建一个 tf.Session
对象,然后调用其 run
方法来评估我们在上文中创建的 total
张量:
sess = tf.Session()
print(sess.run(total))
out:
7.0
当使用 Session.run 请求输出节点时,TensorFlow 会回溯整个图,并流经提供了所请求的输出节点对应的输入值的所有节点。因此此指令会打印预期的值 7.0。
可以将多个张量传递给 tf.Session.run
。run
方法以透明方式处理元组或字典的任何组合,如下例所示:
print(sess.run({'ab':(a, b), 'total':total}))
out:
{'ab': (3.0, 4.0), 'total': 7.0}
在调用 tf.Session.run
期间,任何 tf.Tensor
都只有单个值:
vec = tf.random_uniform(shape=(3,))
out1 = vec + 1
out2 = vec + 2
print(sess.run(vec))
print(sess.run(vec))
print(sess.run((out1, out2)))
out:
[0.10610855 0.10568261 0.37382376]
[0.44892144 0.30279136 0.17507005]
(array([1.9400846, 1.6881588, 1.8300637], dtype=float32),
array([2.9400845, 2.6881588, 2.8300638], dtype=float32))
可以看到,每次调用 run 时,结果都会显示不同的随机值,但在单个 run 期间(out1 和 out2 接收到相同的随机输入值),结果显示的值是一致的。
目前来讲,这个图不是特别有趣,因为它总是生成一个常量结果。图可以参数化以便接受外部输入,也称为占位符。占位符表示承诺在稍后提供值,它就像函数参数。
x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)
z = x + y
print(sess.run(z, feed_dict={x: 3, y: 4.5}))
print(sess.run(z, feed_dict={x: [1, 3], y: [2, 4]}))
out:
7.5
[3. 7.]
可以看到,这里的x,y相当于计算函数z的形参,而feed_dict相当于传给函数的实参。
占位符和其他 tf.Tensors 的唯一不同之处在于如果没有为占位符提供值,那么占位符会抛出错误。
print(sess.run(z))
out:
InvalidArgumentError: You must feed a value for placeholder tensor 'Placeholder' with dtype float
数据集
有了占位符,我们就可以向计算图输入数据,但是对于我们要解决的实际问题来说,利用占位符手动传入参数是非常麻烦的,这时候我们需要使用数据集将数据流式传输到模型。
要从数据集中获取可运行的 tf.Tensor
,必须先将其转换成 tf.data.Iterator
,然后调用迭代器的 get_next
方法。
创建迭代器的最简单的方式是采用 make_one_shot_iterator
方法。例如,在下面的代码中,next_item
张量将在每次 run
调用时从 my_data
阵列返回一行:
my_data = [
[0, 1,],
[2, 3,],
[4, 5,],
[6, 7,],
]
slices = tf.data.Dataset.from_tensor_slices(my_data)
next_item = slices.make_one_shot_iterator().get_next()
while True:
try:
print(sess.run(next_item))
except tf.errors.OutOfRangeError:
break
out:
[0 1]
[2 3]
[4 5]
[6 7]
r = tf.random_normal([10,3])
dataset = tf.data.Dataset.from_tensor_slices(r)
iterator = dataset.make_initializable_iterator()
next_row = iterator.get_next()
sess.run(iterator.initializer)
while True:
try:
print(sess.run(next_row))
except tf.errors.OutOfRangeError:
break
out:
[-0.8426063 -0.10563334 1.7068906 ]
[-0.5716917 -0.366284 0.18615837]
[-1.0454278 0.10079048 0.19581021]
[ 1.4342923 0.15459426 -0.04541921]
[ 0.30469146 -0.5566545 1.2912823 ]
[-0.3311023 -0.9967349 1.6312599]
[0.32272357 1.2039856 0.3193447 ]
[-0.18648322 -0.35564408 0.0854689 ]
[-0.80215 -0.8052359 -0.07297412]
[-1.0230924 -0.72250557 1.8930912 ]
层
可训练的模型必须修改图中的值,以便在输入相同值的情况下获得新的输出值。将可训练参数添加到图中的首选方法是层。
层将变量和作用于它们的操作打包在一起。例如,密集连接层会对每个输出对应的所有输入执行加权和,并应用激活函数。连接权重和偏差由层对象管理。
下面的代码会创建一个 Dense
层,该层会接受一批输入矢量,并为每个矢量生成一个输出值。要将层应用于输入值,请将该层当做函数来调用。例如:
x = tf.placeholder(tf.float32, shape=[None, 3])
#units:输出的维度大小,改变inputs的最后一维
linear_model = tf.layers.Dense(units=1)
y = linear_model(x)
创建好层后我们执行层的初始化:
#调用 tf.global_variables_initializer会创建并返回 TensorFlow 操作的句柄。
#当我们使用tf.Session.run运行该操作时,该操作将初始化所有全局变量。
init = tf.global_variables_initializer()
sess.run(init)
注意,此 global_variables_initializer 仅会初始化创建初始化程序时图中就存在的变量。因此应该在构建图表的最后一步添加初始化程序。
我们现在已经完成了层的初始化,可以像处理任何其他张量一样评估 linear_model 的输出张量了。下面我们执行层:
print(sess.run(y, {x: [[1, 2, 3],[4, 5, 6]]}))
out:
[[1.9250429]
[5.190393 ]]
当然这个输出结果并不唯一,重新初始化并执行层将得到不同结果,这是因为Dense层的初始化是随机的。
层函数还有比较快捷的写法:
x = tf.placeholder(tf.float32, shape=[None, 3])
y = tf.layers.dense(x, units=1)
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(y, {x: [[1, 2, 3], [4, 5, 6]]}))
快捷函数版本是在单次调用中创建和运行层。尽管这种方法很方便,但无法访问 tf.layers.Layer
对象。这会让自省和调试变得更加困难,并且无法重复使用相应的层。
训练
有了上面的基本概念,我们现在已经可以试着训练一个简单的回归模型了。
首先定义一些输入值 x,以及每个输入值的预期输出值 y_true:
x = tf.constant([[1], [2], [3], [4]], dtype=tf.float32)
y_true = tf.constant([[0], [-1], [-2], [-3]], dtype=tf.float32)
接下来,建立一个简单的线性模型,其输出值只有 1 个:
linear_model = tf.layers.Dense(units=1)
y_pred = linear_model(x)
评估预测值:
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(y_pred))
out:
[[1.2374495]
[2.474899 ]
[3.7123485]
[4.949798 ]]
可以看到预测值和真实值相差很大,这是因为模型初始化是随机的,且尚未经过训练。
要优化模型,首先需要定义损失。我们将使用均方误差,这是回归问题的标准损失。 tf.losses
模块提供了一系列常用的损失函数。可以使用它来计算均方误差,具体操作如下所示:
loss = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)
print(sess.run(loss))
out:
27.359104
可以看到当前的均方误差是非常大的。
#0.1为学习率
optimizer = tf.train.GradientDescentOptimizer(0.1)
train = optimizer.minimize(loss)
TensorFlow 提供执行标准优化算法的优化器。这些优化器被实现为 tf.train.Optimizer
的子类。它们会逐渐改变每个变量,以便将损失最小化。最简单的优化算法是梯度下降法, tf.train.GradientDescentOptimizer
实现。它会根据损失相对于变量的导数大小来修改各个变量。
上述代码构建了优化所需的所有图组件,并返回一个训练指令。该训练指令在运行时会更新图中的变量。
按以下方式运行该指令:
for i in range(100):
_, loss_value = sess.run((train, loss))
print(loss_value)
out:
0.011391077
0.010719215
0.010086973
0.009492025
0.0089321695
…
…
…
3.326403e-05
3.130227e-05
2.945621e-05
2.7718925e-05
由于 train 是一个指令而不是张量,因此它在运行时不会返回一个值。为了查看训练期间损失的进展,我们会同时运行损失张量并输出上述结果。可以看到,进行了100次迭代后,loss已经非常小了。
此时查看预测结果:
print(sess.run(y_pred))
out:
[[-0.00824672]
[-1.0039963 ]
[-1.9997458 ]
[-2.9954953 ]]
可以看到预测结果与真实结果非常接近。也就是说我们的回归模型对训练集的拟合表现还不错。
张量
张量是对矢量和矩阵的更高维度的泛化。TensorFlow 在内部将张量表示为基本数据类型的 n 维数组。
在编写 TensorFlow 程序时,操作和传递的主要对象是 tf.Tensor
。tf.Tensor
对象表示一个部分定义的计算,最终会生成一个值。TensorFlow 程序首先会构建一个 tf.Tensor
对象图,详细说明如何基于其他可用张量计算每个张量,然后运行该图的某些部分以获得期望的结果。
tf.Tensor
具有以下属性:
- 数据类型(例如
float32
、int32
或string
) - 形状
张量中的每个元素都具有相同的数据类型,且该数据类型一定是已知的。形状,即张量的维数和每个维度的大小,可能只有部分已知。如果输入的形状也完全已知,则大多数操作会生成形状完全已知的张量,但在某些情况下,只能在执行图时获得张量的形状。
主要特殊张量有:
tf.Variable
tf.constant
tf.placeholder
tf.SparseTensor
除了 tf.Variable
以外,张量的值是不变的,这意味着对于单个执行任务,张量只有一个值。然而,两次评估同一张量可能会返回不同的值;例如,该张量可能是从磁盘读取数据的结果,或是生成随机数的结果。
阶
tf.Tensor
对象的阶是它本身的维数。阶的同义词包括秩或 维。注意,TensorFlow 中的阶与数学中矩阵的阶并不是同一个概念。如下表所示,TensorFlow 中的每个阶都对应一个不同的数学实例:
阶 | 数学实例 |
---|---|
0 | 标量(只有大小) |
1 | 矢量(大小和方向) |
2 | 矩阵(数据表) |
3 | 3 阶张量(数据立体) |
n | n 阶张量(自行想象) |
下面我们看几个例子:
首先是0阶:
ignition = tf.Variable(451, tf.int16)
floating = tf.Variable(3.14159265359, tf.float32)
its_complicated = tf.Variable(12.3 - 4.85j, tf.complex64)
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(ignition))
print(sess.run(floating))
print(sess.run(its_complicated))
print(sess.run(tf.rank(ignition)))
print(sess.run(tf.rank(floating)))
print(sess.run(tf.rank(its_complicated)))
out:
451
3.1415927
(12.3-4.85j)
0
0
0
然后是一阶和二阶:
mymat = tf.Variable([7,11], tf.int16)
mymatC = tf.Variable([[7],[11]], tf.int32)
myxor = tf.Variable([[False, True],[True, False]], tf.bool)
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(mymat))
print(sess.run(tf.rank(mymat)))
print(sess.run(mymatC))
print(sess.run(tf.rank(mymatC)))
print(sess.run(myxor))
print(sess.run(tf.rank(myxor)))
out:
[ 7 11]
1
[[ 7]
[11]]
2
[[False True]
[ True False]]
2
切片
现在我们已经知道各阶的张量如何表示了,接下来我们希望访问张量中某个元素。若 tf.Tensor
是 n 维单元数组,则要访问 tf.Tensor
中的某一单元,需要指定 n 个索引。
0 阶张量(标量)不需要索引,因为其本身便是单一数字。
1 阶张量(矢量),可以通过传递一个索引访问某个数字:
my_vector=tf.Variable([1,3,5,7],tf.int32)
my_scalar = my_vector[2]
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(my_scalar))
out:
5
注意,索引是从0开始的,因此这里my_vector[2]访问的是my_vector的第三个元素5。
2阶张量(矩阵),传递两个数字会如预期般返回一个标量,而传递一个数字则会返回一个矩阵子矢量:
my_matrix=tf.constant([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
my_scalar = my_matrix[1, 2]
my_row_vector = my_matrix[2]
my_column_vector = my_matrix[:, 3]
print(sess.run(my_scalar))
print(sess.run(my_row_vector))
print(sess.run(my_column_vector))
out:
7
[ 9 10 11 12]
[ 4 8 12 16]
符号 : 是 Python 切片语法,意味“不要触碰该维度”。这对更高阶的张量来说很有用,可以帮助访问其子矢量,子矩阵,甚至其他子张量。
形状
张量的形状是每个维度中元素的数量。TensorFlow 在图的构建过程中自动推理形状。这些推理的形状可能具有已知或未知的阶。如果阶已知,则每个维度的大小可能已知或未知。
阶 | 形状 | 维数 | 示例 |
---|---|---|---|
0 | [] | 0-D | 0 维张量。标量。 |
1 | [D0] | 1-D | 形状为 [5] 的 1 维张量。 |
2 | [D0, D1] | 2-D | 形状为 [3, 4] 的 2 维张量。 |
3 | [D0, D1, D2] | 3-D | 形状为 [1, 4, 3] 的 3 维张量。 |
n | [D0, D1, ... Dn-1] | n 维 | 形状为 [D0, D1, ... Dn-1] 的张量。 |
可以通过两种方法获取 tf.Tensor
的形状。在构建图的时候,询问有关张量形状的已知信息通常很有帮助。可以通过查看 shape
属性(属于 tf.Tensor
对象)获取这些信息。该方法会返回一个 TensorShape
对象,这样可以方便地表示部分指定的形状(因为在构建图的时候,并不是所有形状都完全已知)。
也可以获取一个将在运行时表示另一个 tf.Tensor
的完全指定形状的 tf.Tensor
。为此,可以调用 tf.shape
操作。如此一来,您可以构建一个图,通过构建其他取决于输入 tf.Tensor
的动态形状的张量来控制张量的形状。
例如,以下代码展示了如何创建大小与给定矩阵中的列数相同的零矢量:
zeros = tf.zeros(my_matrix.shape[1])
print(sess.run(zeros))
out:
[0. 0. 0. 0.]
张量的元素数量是其所有形状大小的乘积。标量的元素数量永远是 1
。由于通常有许多不同的形状具有相同数量的元素,因此如果能够改变 tf.Tensor
的形状并使其元素固定不变通常会很方便。为此,可以使用 tf.reshape
来重构张量:
rank_three_tensor = tf.ones([3, 4, 5])
matrix = tf.reshape(rank_three_tensor, [6, 10]) # Reshape existing content into a 6x10 matrix.
matrixB = tf.reshape(matrix, [3, -1]) # Reshape existing content into a 3x20 matrix.
matrixAlt = tf.reshape(matrixB, [4, 3, -1]) # -1 tells reshape to calculate the size of this dimension.
print(sess.run(matrix))
print(sess.run(matrixB))
print(sess.run(matrixAlt))
out:
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]]
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]]
[[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]]
数据类型
一个 tf.Tensor
只能有一种数据类型。但是,可以将任意数据结构序列化为 string
并将其存储在 tf.Tensor
中。可以将 tf.Tensor
从一种数据类型转型为另一种(通过 tf.cast
):
float_tensor1 = tf.constant([1, 2, 3],tf.float64)
float_tensor2 = tf.cast(float_tensor1, dtype=tf.float32)
print(float_tensor)
out:
Tensor("Cast_1:0", shape=(3,), dtype=float32)
可以看到,我们通过cast把类型为float64的float_tensor1转化成了类型为float32的float_tensor2。
要检查 tf.Tensor
的数据类型,还可以使用 Tensor.dtype
属性:
print(float_tensor1.dtype)
print(float_tensor2.dtype)
out:
评估张量
计算图构建完毕后,可以运行生成特定 tf.Tensor
的计算并获取分配给它的值。这对于程序调试通常很有帮助,也是 TensorFlow 的大部分功能正常运行所必需的。
评估张量最简单的方法是使用 Tensor.eval
方法。例如:
constant = tf.constant([1, 2, 3])
tensor = constant * constant
with sess.as_default():
print(tensor.eval())
out:
[1 4 9]
eval
方法仅在默认 tf.Session
处于活跃状态时才起作用。
Tensor.eval
会返回一个与张量内容相同的 NumPy 数组。
有时无法在没有背景信息的情况下评估 tf.Tensor
,因为它的值可能取决于无法获取的动态信息。例如,在没有为 placeholder
提供值的情况下,无法评估依赖于 placeholder
的张量。
输出张量
出于调试目的,可能需要输出 tf.Tensor
的值。虽然 tfdbg 提供高级调试支持,但 TensorFlow 也有一个操作可以直接输出 tf.Tensor
的值。
TensorFlow 提供了 tf.Print
操作,该操作会返回其第一个张量参数(保持不变),同时输出作为第二个参数传递的 tf.Tensor
集合。
要正确使用 tf.Print
,必须使用其返回的值。例如:
t = tf.constant([1,2,3],float32)
tf.Print(t, [t]) # This does nothing
t = tf.Print(t, [t]) # Here we are using the value returned by tf.Print
result = t + 1 # Now when result is evaluated the value of `t` will be printed.
在评估 result 时,会评估所有影响 result 的元素。由于 result 依靠 t,而评估 t 会导致输出其输入(t 的旧值),所以系统会输出 t。
变量
TensorFlow 变量是表示程序处理的共享持久状态的最佳方法。
我们使用 tf.Variable
类操作变量。tf.Variable
表示可通过对其运行操作来改变其值的张量。与 tf.Tensor
对象不同,tf.Variable
存在于单个 session.run
调用的上下文之外。
在 TensorFlow 内部,tf.Variable
会存储持久性张量。具体 op 允许读取和修改此张量的值。这些修改在多个 tf.Session
之间是可见的,因此对于一个 tf.Variable
,多个工作器可以看到相同的值。
创建变量
创建变量的最佳方式是调用 tf.get_variable
函数。此函数要求指定变量的名称。其他副本将使用此名称访问同一变量,以及在对模型设置检查点和导出模型时指定此变量的值。tf.get_variable
还允许重复使用先前创建的同名变量,从而轻松定义重复利用层的模型。
要使用 tf.get_variable
创建变量,只需提供名称和形状即可:
my_variable = tf.get_variable("my_variable", [1, 2, 3])
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(my_variable))
out:
[[[-0.86654425 -0.5600589 0.5989642 ]
[ 0.21465802 0.7863096 -0.46472996]]]
这将创建一个名为“my_variable”的变量,该变量是形状为 [1, 2, 3]
的三维张量。默认情况下,此变量将具有 dtype
tf.float32
,其初始值将通过 tf.glorot_uniform_initializer
随机设置。
我们也可以选择为 tf.get_variable
指定 dtype
和初始化器。例如:
my_int = tf.get_variable("my_int", [1, 2, 3], dtype=tf.int32,
initializer=tf.zeros_initializer)
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(my_int))
out:
[[[0 0 0]
[0 0 0]]]
TensorFlow 提供了许多方便的初始化器。或者,也可以将 tf.Variable
初始化为 tf.Tensor
的值。例如:
other_variable = tf.get_variable("other_variable", dtype=tf.int32,
initializer=tf.constant([23, 42]))
init = tf.global_variables_initializer()
sess.run(init)
print(sess.run(other_variable))
out:
[23 42]
请注意,当初始化器是 tf.Tensor
时,不应指定变量的形状,因为将使用初始化器张量的形状。
变量集合
由于 TensorFlow 程序的未连接部分可能需要创建变量,因此能有一种方式访问所有变量有时十分受用。为此,TensorFlow 提供了集合,它们是张量或其他对象(如 tf.Variable
实例)的命名列表。
默认情况下,每个 tf.Variable
都放置在以下两个集合中:
-
tf.GraphKeys.GLOBAL_VARIABLES
- 可以在多台设备间共享的变量, -
tf.GraphKeys.TRAINABLE_VARIABLES
- TensorFlow 将计算其梯度的变量。
如果不希望变量可训练,可以将其添加到 tf.GraphKeys.LOCAL_VARIABLES
集合中。例如,以下代码段展示了如何将名为 my_local
的变量添加到此集合中:
my_local = tf.get_variable("my_local", shape=(),
collections=[tf.GraphKeys.LOCAL_VARIABLES])
或者,也可以指定 trainable=False
(作为 tf.get_variable
的参数):
my_non_trainable = tf.get_variable("my_non_trainable",
shape=(),
trainable=False)
也可以使用自己的集合。集合名称可为任何字符串,且无需显式创建集合。创建变量(或任何其他对象)后,要将其添加到集合中,请调用 tf.add_to_collection
。例如,以下代码将名为 my_local
的现有变量添加到名为 my_collection_name
的集合中:
tf.add_to_collection("my_collection_name", my_local)
tf.add_to_collection("my_collection_name", my_non_trainable)
要检索放置在某个集合中的所有变量(或其他对象)的列表,可以使用:
tf.get_collection("my_collection_name")
out:
[,
]
初始化变量
变量必须先初始化后才可使用。在低级别 TensorFlow API 中进行编程(即显式创建自己的图和会话),则必须明确初始化变量。tf.contrib.slim
、tf.estimator.Estimator
和 Keras
等大多数高级框架在训练模型前会自动为您初始化变量。
显式初始化在其他方面很有用。它允许在从检查点重新加载模型时不用重新运行潜在资源消耗大的初始化器,并允许在分布式设置中共享随机初始化的变量时具有确定性。
要在训练前一次性初始化所有可训练变量,请调用 tf.global_variables_initializer()
。此函数会返回一个操作,负责初始化 tf.GraphKeys.GLOBAL_VARIABLES
集合中的所有变量。运行此操作会初始化所有变量。例如:
sess.run(tf.global_variables_initializer())
如果确实需要自行初始化变量,则可以运行变量的初始化器操作。例如:
my_var=tf.Variable(tf.get_variable("my_var", dtype=tf.int32,
initializer=tf.constant([1,2,3])))
sess.run(my_var.initializer)
您可以查询哪些变量尚未初始化。例如,以下代码会打印所有尚未初始化的变量名称:
print(sess.run(tf.report_uninitialized_variables()))
out:
[b'my_local']
注意,默认情况下,tf.global_variables_initializer
不会指定变量初始化顺序。因此,如果变量的初始值取决于另一变量的值,那么很有可能会出现错误。任何时候,如果在并非所有变量都已初始化的上下文中使用某个变量值(例如在初始化某个变量时使用另一变量的值),最好使用 variable.initialized_value()
,而非 variable
:
vv = tf.get_variable("vv", shape=(), initializer=tf.zeros_initializer())
ww = tf.get_variable("ww", initializer=v.initialized_value() + 1)
sess.run(ww.initializer)
print(sess.run(ww))
out:
1.0
使用变量
要在 TensorFlow 图中使用 tf.Variable
的值,只需将其视为普通 tf.Tensor
即可。要为变量赋值,请使用 assign
、assign_add
方法以及 tf.Variable
类中的友元。例如,以下就是调用这些方法的方式:
v = tf.get_variable("v", shape=(), initializer=tf.zeros_initializer())
assignment = v.assign_add(1)
tf.global_variables_initializer().run()
sess.run(assignment)
out:
1.0
共享变量
TensorFlow 支持两种共享变量的方式:
- 显式传递
tf.Variable
对象。 - 将
tf.Variable
对象隐式封装在tf.variable_scope
对象内。
虽然显式传递变量的代码非常清晰,但有时编写在实现中隐式使用变量的 TensorFlow 函数非常方便。tf.layers
中的大多数功能层以及所有 tf.metrics
和部分其他库实用程序都使用这种方法。
变量作用域允许在调用隐式创建和使用变量的函数时控制变量重用。作用域还允许以分层和可理解的方式命名变量。
例如,假设我们编写一个函数来创建一个卷积/relu 层:
def conv_relu(input, kernel_shape, bias_shape):
# Create variable named "weights".
weights = tf.get_variable("weights", kernel_shape,
initializer=tf.random_normal_initializer())
# Create variable named "biases".
biases = tf.get_variable("biases", bias_shape,
initializer=tf.constant_initializer(0.0))
conv = tf.nn.conv2d(input, weights,
strides=[1, 1, 1, 1], padding='SAME')
return tf.nn.relu(conv + biases)
此函数使用短名称 weights 和 biases,这有利于清晰区分二者。然而,在真实模型中,我们需要很多此类卷积层,而且重复调用此函数将不起作用:
input1 = tf.random_normal([1,10,10,32])
input2 = tf.random_normal([1,20,20,32])
x = conv_relu(input1, kernel_shape=[5, 5, 32, 32], bias_shape=[32])
x = conv_relu(x, kernel_shape=[5, 5, 32, 32], bias_shape = [32])
out:
ValueError: Variable weights already exists, disallowed.
Did you mean to set reuse=True or reuse=tf.AUTO_REUSE in VarScope?
由于期望的操作不清楚(创建新变量还是重新使用现有变量?),因此 TensorFlow 将会失败。不过,在不同作用域内调用 conv_relu 可表明我们想要创建新变量:
def my_image_filter(input_images):
with tf.variable_scope("conv1"):
# Variables created here will be named "conv1/weights", "conv1/biases".
relu1 = conv_relu(input_images, [5, 5, 32, 32], [32])
with tf.variable_scope("conv2"):
# Variables created here will be named "conv2/weights", "conv2/biases".
return conv_relu(relu1, [5, 5, 32, 32], [32])
如果想要共享变量,有两种方法可供选择。首先,您可以使用 reuse=True 创建具有相同名称的作用域:
with tf.variable_scope("model"):
output1 = my_image_filter(input1)
with tf.variable_scope("model", reuse=True):
output2 = my_image_filter(input2)
也可以调用 scope.reuse_variables() 以触发重用:
with tf.variable_scope("model") as scope:
output1 = my_image_filter(input1)
scope.reuse_variables()
output2 = my_image_filter(input2)
由于依赖于作用域的确切字符串名称可能比较危险,因此也可以根据另一作用域初始化某个变量作用域:
with tf.variable_scope("model") as scope:
output1 = my_image_filter(input1)
with tf.variable_scope(scope, reuse=True):
output2 = my_image_filter(input2)
图和会话
TensorFlow 使用数据流图将计算表示为独立的指令之间的依赖关系。这可生成低级别的编程模型,在该模型中,首先定义数据流图,然后创建 TensorFlow 会话,以便在一组本地和远程设备上运行图的各个部分。
数据流是一种用于并行计算的常用编程模型。在数据流图中,节点表示计算单元,边表示计算使用或产生的数据。例如在 TensorFlow 图中,tf.matmul
操作对应于单个节点,该节点具有两个传入边(要相乘的矩阵)和一个传出边(乘法结果)。
在执行程序时,数据流可以为 TensorFlow 提供多项优势:
并行处理。 通过使用明确的边缘来表示操作之间的依赖关系,系统可以轻松识别能够并行执行的操作。
分布式执行。 通过使用明确的边来表示操作之间流动的值,TensorFlow 可以将程序划分到连接至不同机器的多台设备上(CPU、GPU 和 TPU)。TensorFlow 将在这些设备之间进行必要的通信和协调。
编译。 TensorFlow 的 XLA 编译器可以使用数据流图中的信息生成更快的代码,例如将相邻的操作融合到一起。
可移植性。 数据流图是一种不依赖于语言的模型代码表示法。可以使用 Python 构建数据流图,将其存储在 SavedModel 中,并使用 C++ 程序进行恢复,从而实现低延迟的推理。
tf.Graph组成
tf.Graph
包含两类相关信息:
图结构。 图的节点和边,表示各个操作组合在一起的方式,但不规定它们的使用方式。图结构与汇编代码类似:检查图结构可以传达一些有用的信息,但它不包含源代码传达的所有实用上下文信息。
图集合。 TensorFlow 提供了一种在
tf.Graph
中存储元数据集合的通用机制。tf.add_to_collection
函数允许将对象列表与一个键关联(其中tf.GraphKeys
定义了部分标准键),tf.get_collection
允许查询与某个键关联的所有对象。TensorFlow 库的许多部分会使用此资源:例如创建tf.Variable
时,系统会默认将其添加到表示全局变量和可训练变量的集合中。后续创建tf.train.Saver
或tf.train.Optimizer
时,这些集合中的变量将用作默认参数。
构建 tf.Graph
大多数 TensorFlow 程序都以数据流图构建阶段开始。在此阶段,会调用 TensorFlow API 函数,这些函数可构建新的 tf.Operation
(节点)和 tf.Tensor
(边)对象并将它们添加到 tf.Graph
实例中。TensorFlow 提供了一个默认图,此图是同一上下文中的所有 API 函数的明确参数。例如:
调用
tf.constant(42.0)
可创建单个tf.Operation
,该操作可以生成值42.0
,将该值添加到默认图中,并返回表示常量值的tf.Tensor
。调用
tf.matmul(x, y)
可创建单个tf.Operation
,该操作会将tf.Tensor
对象x
和y
的值相乘,将其添加到默认图中,并返回表示乘法运算结果的tf.Tensor
。执行
v = tf.Variable(0)
可向图添加一个tf.Operation
,该操作可以存储一个可写入的张量值,该值在多个tf.Session.run
调用之间保持恒定。tf.Variable
对象会封装此操作,并可以像张量一样使用,即读取已存储值的当前值。tf.Variable
对象也具有assign
和assign_add
等方法,这些方法可创建tf.Operation
对象,这些对象在执行时将更新已存储的值。调用
tf.train.Optimizer.minimize
可将操作和张量添加到计算梯度的默认图中,并返回一个tf.Operation
,该操作在运行时会将这些梯度应用到一组变量上。
大多数程序仅依赖于默认图。
命名指令
tf.Graph
对象会定义一个命名空间(为其包含的 tf.Operation
对象)。TensorFlow 会自动为您的图中的每个指令选择一个唯一名称,但您也可以指定描述性名称,使您的程序阅读和调试起来更轻松。TensorFlow API 提供两种方法来覆盖操作名称:
若 API 函数创建新的
tf.Operation
或返回新的tf.Tensor
,则会接受可选name
参数。例如tf.constant(42.0, name="answer")
会创建一个新的tf.Operation
(名为"answer"
)并返回一个tf.Tensor
(名为"answer:0"
)。若默认图已包含名为"answer"
的操作,则 TensorFlow 会在名称上附加"_1"
、"_2"
等字符,以便让名称具有唯一性。借助
tf.name_scope
函数,可以向在特定上下文中创建的所有操作添加名称作用域前缀。当前名称作用域前缀是一个用"/"
分隔的名称列表,其中包含所有活跃tf.name_scope
的上下文管理器的名称。如果某个名称作用域已在当前上下文中被占用,TensorFlow 将在该作用域上附加"_1"
、"_2"
等字符。例如:
c_0 = tf.constant(0, name="c") # => operation named "c"
# Already-used names will be "uniquified".
c_1 = tf.constant(2, name="c") # => operation named "c_1"
# Name scopes add a prefix to all operations created in the same context.
with tf.name_scope("outer"):
c_2 = tf.constant(2, name="c") # => operation named "outer/c"
# Name scopes nest like paths in a hierarchical file system.
with tf.name_scope("inner"):
c_3 = tf.constant(3, name="c") # => operation named "outer/inner/c"
# Exiting a name scope context will return to the previous prefix.
c_4 = tf.constant(4, name="c") # => operation named "outer/c_1"
# Already-used name scopes will be "uniquified".
with tf.name_scope("inner"):
c_5 = tf.constant(5, name="c") # => operation named "outer/inner_1/c"
请注意,tf.Tensor
对象以输出张量的 tf.Operation
明确命名。张量名称的形式为 "
,其中:
-
"
是生成该张量的操作的名称。" -
""
是一个整数,表示该张量在操作的输出中的索引。
比如:
print(c_2)
out:
Tensor("outer/c:0", shape=(), dtype=int32)
可以看到, "
是生成该张量的操作的名称outer/c
。
类似于张量的对象
许多 TensorFlow 操作都会接受一个或多个 tf.Tensor
对象作为参数。例如,tf.matmul
接受两个 tf.Tensor
对象,tf.add_n
接受一个具有 n
个 tf.Tensor
对象的列表。为了方便起见,这些函数将接受类张量对象来取代 tf.Tensor
,并将它明确转换为 tf.Tensor
(通过 tf.convert_to_tensor
方法)。类张量对象包括以下类型的元素:
tf.Tensor
tf.Variable
numpy.ndarray
-
list
(以及类似于张量的对象的列表) - 标量 Python 类型:
bool
、float
、int
、str
可以使用 tf.register_tensor_conversion_function
注册其他类张量类型。
会话
TensorFlow 使用 tf.Session
类来表示客户端程序(通常为 Python 程序,但也提供了其他语言的类似接口)与 C++ 运行时之间的连接。tf.Session
对象使我们能够访问本地机器中的设备和使用分布式 TensorFlow 运行时的远程设备。它还可缓存关于 tf.Graph
的信息,能够多次高效地运行同一计算。
创建 tf.Session
由于 tf.Session
拥有物理资源(例如 GPU 和网络连接),因此通常(在 with
代码块中)用作上下文管理器,并在您退出代码块时自动关闭会话。您也可以在不使用 with
代码块的情况下创建会话,但应在完成会话时明确调用 tf.Session.close
以便释放资源。
with tf.Session() as sess:
#……
tf.Session.init
接受三个可选参数:
target
。 如果将此参数留空(默认设置),会话将仅使用本地机器中的设备。但是,您也可以指定grpc://
网址,以便指定 TensorFlow 服务器的地址,这使得会话可以访问该服务器控制的机器上的所有设备。请参阅tf.train.Server
以详细了解如何创建 TensorFlow 服务器。例如,在常见的图间复制配置中,tf.Session
连接到tf.train.Server
的流程与客户端相同。分布式 TensorFlow 部署指南介绍了其他常见情形。graph
。 默认情况下,新的tf.Session
将绑定到当前的默认图,并且仅能够在当前的默认图中运行操作。如果您在程序中使用了多个图(更多详情请参阅使用多个图进行编程),则可以在构建会话时指定明确的tf.Graph
。-
config
。 此参数允许您指定一个控制会话行为的tf.ConfigProto
。例如,部分配置选项包括:allow_soft_placement
。将此参数设置为True
可启用“软”设备放置算法,该算法会忽略尝试将仅限 CPU 的操作分配到 GPU 设备上的tf.device
注解,并将这些操作放置到 CPU 上。cluster_def
。使用分布式 TensorFlow 时,此选项允许您指定要在计算中使用的机器,并提供作业名称、任务索引和网络地址之间的映射。详情请参阅tf.train.ClusterSpec.as_cluster_def
。graph_options.optimizer_options
。在执行图之前使您能够控制 TensorFlow 对图实施的优化。gpu_options.allow_growth
。将此参数设置为True
可更改 GPU 内存分配器,使该分配器逐渐增加分配的内存量,而不是在启动时分配掉大多数内存。
使用 tf.Session.run
执行操作
tf.Session.run
方法是运行 tf.Operation
或评估 tf.Tensor
的主要机制。您可以将一个或多个 tf.Operation
或 tf.Tensor
对象传递到 tf.Session.run
,TensorFlow 将执行计算结果所需的操作。
tf.Session.run
要求您指定一组 fetch,这些 fetch 可确定返回值,并且可能是 tf.Operation
、tf.Tensor
或类张量类型,例如 tf.Variable
。这些 fetch 决定了必须执行哪些子图(属于整体 tf.Graph
)以生成结果:该子图包含 fetch 列表中指定的所有操作,以及其输出用于计算 fetch 值的所有操作。例如,以下代码段说明了 tf.Session.run
的不同参数如何导致执行不同的子图:
x = tf.constant([[37.0, -23.0], [1.0, 4.0]])
w = tf.Variable(tf.random_uniform([2, 2]))
y = tf.matmul(x, w)
output = tf.nn.softmax(y)
init_op = w.initializer
with tf.Session() as sess:
# Run the initializer on `w`.
sess.run(init_op)
# Evaluate `output`. `sess.run(output)` will return a NumPy array containing
# the result of the computation.
print(sess.run(output))
# Evaluate `y` and `output`. Note that `y` will only be computed once, and its
# result used both to return `y_val` and as an input to the `tf.nn.softmax()`
# op. Both `y_val` and `output_val` will be NumPy arrays.
y_val, output_val = sess.run([y, output])
out:
[[0.9965293 0.00347069]
[0.08894425 0.9110558 ]]
tf.Session.run
也可以选择接受 feed 字典,该字典是从 tf.Tensor
对象(通常是 tf.placeholder
张量)到在执行时会替换这些张量的值(通常是 Python 标量、列表或 NumPy 数组)的映射。例如:
# Define a placeholder that expects a vector of three floating-point values,
# and a computation that depends on it.
x = tf.placeholder(tf.float32, shape=[3])
y = tf.square(x)
with tf.Session() as sess:
# Feeding a value changes the result that is returned when you evaluate `y`.
print(sess.run(y, {x: [1.0, 2.0, 3.0]})) # => "[1.0, 4.0, 9.0]"
print(sess.run(y, {x: [0.0, 0.0, 5.0]})) # => "[0.0, 0.0, 25.0]"
out:
[1. 4. 9.]
[ 0. 0. 25.]
保存和恢复
TensorFlow 变量是表示由程序操作的共享持久状态的最佳方法。tf.train.Saver
构造函数会针对图中所有变量或指定列表的变量将 save
和 restore
操作添加到图中。Saver
对象提供了运行这些操作的方法,并指定写入或读取检查点文件的路径。
保存变量
创建 Saver
(使用 tf.train.Saver()
)来管理模型中的所有变量。例如,以下代码段展示了如何调用 tf.train.Saver.save
方法以将变量保存到检查点文件中:
# Create some variables.
v1 = tf.get_variable("v1", shape=[3], initializer = tf.zeros_initializer)
v2 = tf.get_variable("v2", shape=[5], initializer = tf.zeros_initializer)
inc_v1 = v1.assign(v1+1)
dec_v2 = v2.assign(v2-1)
# Add an op to initialize the variables.
init_op = tf.global_variables_initializer()
# Add ops to save and restore all the variables.
saver = tf.train.Saver()
# Later, launch the model, initialize the variables, do some work, and save the
# variables to disk.
with tf.Session() as sess:
sess.run(init_op)
# Do some work with the model.
inc_v1.op.run()
dec_v2.op.run()
# Save the variables to disk.
save_path = saver.save(sess, "/tmp/model.ckpt")
print("Model saved in path: %s" % save_path)
out:
Model saved in path: /tmp/model.ckpt
恢复变量
tf.train.Saver
对象不仅将变量保存到检查点文件中,还将恢复变量。注意,当恢复变量时,不必事先将其初始化。例如,以下代码段展示了如何调用 tf.train.Saver.restore
方法以从检查点文件中恢复变量:
tf.reset_default_graph()
# Create some variables.
#对于get_variable(),来说,如果已经创建变量对象,就把那个对象返回
#如果没有创建变量对象的话,就创建一个新的
v1 = tf.get_variable("v1", shape=[3])
v2 = tf.get_variable("v2", shape=[5])
# Add ops to save and restore all the variables.
saver = tf.train.Saver()
# Later, launch the model, use the saver to restore variables from disk, and
# do some work with the model.
with tf.Session() as sess:
# Restore variables from disk.
saver.restore(sess, "/tmp/model.ckpt")
print("Model restored.")
# Check the values of the variables
print("v1 : %s" % v1.eval())
print("v2 : %s" % v2.eval())
out:
Model restored.
v1 : [1. 1. 1.]
v2 : [-1. -1. -1. -1. -1.]
选择要保存和恢复的变量
如果没有向 tf.train.Saver() 传递任何参数,则 Saver 会处理图中的所有变量。每个变量都保存在创建变量时所传递的名称下。
在检查点文件中明确指定变量名称的这种做法有时会非常有用。例如,我们可能已经使用名为"weights"的变量训练了一个模型,而想要将该变量的值恢复到名为"params"的变量中。
有时候,仅保存或恢复模型使用的变量子集也会很有裨益。例如,我们可能已经训练了一个五层的神经网络,现在想要训练一个六层的新模型,并重用该五层的现有权重。可以使用 Saver 只恢复这前五层的权重。
您可以通过向 tf.train.Saver() 构造函数传递以下任一内容,轻松指定要保存或加载的名称和变量:
- 变量列表(将以其本身的名称保存)。
- Python 字典,其中,键是要使用的名称,键值是要管理的变量。
继续前面所示的保存/恢复示例:
tf.reset_default_graph()
# Create some variables.
v1 = tf.get_variable("v1", [3], initializer = tf.zeros_initializer)
v2 = tf.get_variable("v2", [5], initializer = tf.zeros_initializer)
# Add ops to save and restore only `v2` using the name "v2"
saver = tf.train.Saver({"v2": v2})
# Use the saver object normally after that.
with tf.Session() as sess:
# Initialize v1 since the saver will not.
v1.initializer.run()
saver.restore(sess, "/tmp/model.ckpt")
print("v1 : %s" % v1.eval())
print("v2 : %s" % v2.eval())
不难看出,我们只恢复了v2,而没有恢复v1。
注意:
如果要保存和恢复模型变量的不同子集,可以根据需要创建任意数量的 Saver 对象。同一个变量可以列在多个 Saver 对象中,变量的值只有在 Saver.restore() 方法运行时才会更改。
如果在会话开始时仅恢复一部分模型变量,则必须为其他变量运行初始化操作。
检查某个检查点中的变量
我们可以使用 inspect_checkpoint
库快速检查某个检查点中的变量。
继续前面所示的保存/恢复示例:
from tensorflow.python.tools import inspect_checkpoint as chkp
# print all tensors in checkpoint file
chkp.print_tensors_in_checkpoint_file("/tmp/model.ckpt", tensor_name='', all_tensors=True)
# print only tensor v1 in checkpoint file
chkp.print_tensors_in_checkpoint_file("/tmp/model.ckpt", tensor_name='v1', all_tensors=False)
# print only tensor v2 in checkpoint file
chkp.print_tensors_in_checkpoint_file("/tmp/model.ckpt", tensor_name='v2', all_tensors=False)
out:
tensor_name: v1
[1. 1. 1.]
tensor_name: v2
[-1. -1. -1. -1. -1.]
tensor_name: v1
[1. 1. 1.]
tensor_name: v2
[-1. -1. -1. -1. -1.]
可以看到,我们既可以查看检查点中所有张量的值,也可以只查看部分张量的值。
保存和恢复模型
使用 SavedModel
保存和加载模型变量、图和图的元数据。SavedModel 是一种独立于语言且可恢复的序列化格式,使较高级别的系统和工具可创建、使用和转换 TensorFlow 模型。TensorFlow 提供了多种与 SavedModel
交互的方式,包括 tf.saved_model
API、tf.estimator.Estimator
和命令行界面。
创建 SavedModel
的最简单方法是使用 tf.saved_model.simple_save
函数:
simple_save(session,
export_dir,
inputs={"x": x, "y": y},
outputs={"z": z})
这样可以配置 SavedModel
,使其能够通过 TensorFlow Serving 进行加载,并支持 Predict API。