1 TensorFlow 基础
TensorFlow 是单词 Tensor 和 Flow 的合成。Tensor 是张量,可以认为是多维数组。一个数字叫做标量(Scalar),一维数组叫做向量(Vector),二维及以上数组叫做矩阵(Matrix),Tensor 可认为是她们的统称。Flow 是流,表示张量之间的计算转化过程,一个节点通过运算流入另一个节点。
1.1 计算图
1.1.1 计算图的两个阶段
TensorFlow 程序分为两个阶段,定义计算图和执行计算图。两个阶段的分界线是 with tf.Session() as sess.
程序中如果没有显示定义计算图,TensorFlow 会自动维护一个默认的计算图,通过 tf.get_default_graph 函数可以获取当前默认的计算图。通常情况下,定义的神经网络都在一个计算图中,使用默认的计算图就足够了。
import tensorflow as tf
g1 = tf.Graph()
with g1.as_default():
v = tf.get_variable('v', initializer=tf.zeros_initializer(shape=[1]))
g2 = tf.Graph()
with g1.as_default():
v = tf.get_variable('v', initializer=tf.ones_initializer(shape=[1]))
with tf.Session(graph=g1) as sess:
tf.initializer_all_variables().run()
with tf.variable_scope('', reuse=True):
# it will output [0.]
print(sess.run(tf.get_variable('v')))
with tf.Session(graph=g2) as sess:
tf.initializer_all_variables().run()
with tf.variable_scope('', reuse=True):
# it will output [1.]
print(sess.run(tf.get_variable('v')))
1.1.2 计算图指定计算的设备
方式一
- 通过 tf.Graph.device 函数来指定运行计算的设备。
g = tf.Graph() with g.device('/gpu:0'): result = a + b
方式二
import os # 这种方式实际验证是有效的,但是方式一,笔者验证是无效的,但是很多书籍包括官方的文档却说是有效的 os.environ["CUDA_VISIBLE_DEVICES"] = "0"
1.2 Session
Session 拥有并管理 TensorFlow 程序运行时的所有资源。开启 Session 后,一定要记得关闭,通常利用 Python 的 with 语句隐式关闭 Session。
Session 有两种:tf.Session() 和 tf.InteractiveSession。前者用于程序中,后者用在交互式环境中,如 Terminal 或 Jupyter。
1.1.1 Session 资源配置
Session 运行时的一些资源配置可以通过 ConfigProto Protocol Buffer 来配置。
config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)
sess1 = tf.InteractiveSession(config=config)
sess2 = tf.Session(config=config)
通过 ConfigProto 可以配置类似并行的线程数、GPU 分配策略、运算超时时间等参数。
allow_soft_palcement 默认为 false,设置为 TRUE 时,当出现以下任意一种情况时,GPU 上的运算会放到 CPU 上运行。
- 运算无法在 GPU 上执行;
- 没有 GPU 资源(比如运算被指定在第二个 GPU 上运行,但是机器只有一个 GPU);
- 运算的输入包含对 GPU 计算结果的引用。
通常这个参数被设置为 TRUE。
log_device_placement, 当为 TRUE 时,日志中将会记录每个节点被安排在了哪个设备上以方便调试。但在生产环境中将这个参数设置为 false,以减少日志量。
1.3 Variable
tf.Variable 有四种维度信息:是否是常量、是否是可训练的、维度(shape)、type;
1.3.1 初始化
使用 tf.Varible 初始化一个变量。变量可以使用随机数初始化,也可以使用常数初始化。
函数名称 | 随机数分布 | 主要参数 |
---|---|---|
tf.random_normal | 正太分布 | 平均值、标准差、取值类型 |
tf.truncated_normal | 正太分布,超过2个标准差重新随机 | 平均值、标准差、取值类型 |
tf.random_uniform | 平均分布 | 最小、最大取值、取值类型 |
tf.random_gamma | Gamma 分布 | 形状参数 alpha、尺度参数 beta、取值类型 |
函数名称 | 功能 | 样例 |
---|---|---|
tf.zeros | 产生全0的数组 | tf.zeros([2, 3], int32) -> [[0, 0, 0], [0, 0, 0]] |
tf.ones | 产生全1的数组 | tf.ones([2, 3], int32) -> [[1, 1, 1], [1, 1, 1]] |
tf.fill | 产生一个全部为给定数字的数组 | tf.fill([2, 3], 9) -> [[9, 9, 9], [9, 9, 9]] |
tf.constant | 产生一个给定值的常量 | tf.constant([1, 2, 3]) -> [1, 2, 3] |
# 初始化例子
weights = tf.Variable(tf.zeros([3]))
w1 = tf.Variable(weights.initialized_value())
w2 = tf.Variable(weights.initialized_value() * 1.0)
w3 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
# 注意区别 w3 与上面的不同点,w3 是用随机数生成函数产生,w1 和 w2 是用常数产生
sess.run(w3.initializer)
# 一次性初始化所有变量
init_op = tf.initialize_all_variables()
sess.run(init_op)
1.3.2 collection
正向其他语言中有 Collection 一样,TensorFlow 也有 collection 概念。TensorFlow 中的 collection 更多地表示存储变量的容器。
所有变量都会被自动加入 GraphKeys.VARIABLES 这个集合。通过 tf.all_variables 函数可以拿到当前计算图上所有变量。
所有可训练的变量都会被自动加入 GraphKeys.TRAINABLE_VARIABLES 集合。如果想获得所有可训练的变量,可以使用 tf.trainable_variables 函数得到。
1.3.3 变量的维度和类型
TensorFlow 支持 14 种不同的类型,主要包括了实数(tf.float32, tf.float64)、整数(tf.int8, tf.int16, tf.int32, tf.uint8)、布尔型 (tf.bool)、复数 (tf.complex64, tf.complex128)。
维度是可以变的,但是通常很少这样使用。变更维度通过设置参数 validate_shape=False
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, name='w1'))
w2 = tf.Variable(tf.random_normal([2, 2], stddev=1, name='w2'))
# 尽管可以这样操作,但是实际很少这样用
tf.assign(w1, w2, validate_shape=False)
1.3.4 占位符(placeholder)
很多编程语言中都有占位符这一概念,TensorFlow 提供了 placeholder. 通过 placeholder,程序可以在运行时提供数据。
运行时,使用 feed_dict 传入参数。feed_dict 是一个字典,字典中给出每个用到的 placeholder 值。key 为变量名,不是变量定义时 name 参数中的值。
import tensorflow as tf
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, name='w1'))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, name='w2'))
x = tf.placeholder(tf.float32, shape=(1, 2), name="input")
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
sess = tf.Session()
init_op = tf.global_variables_initializer()
sess.run(init_op)
print(sess.run(y, feed_dict={x: [[0.7,0.9]]}))
1.4 变量管理
当变量非常多或者网络模型非常复杂时,就需要一个好的方式来传递和管理神经网络中的参数。TensorFlow 提供了通过变量名称来创建和获取变量的机制——tf.get_variable 和 tf.variable_scope 函数。
1.4.1 tf.get_variable
前文已经介绍过,通过 tf.Variable 函数可以创建一个变量。除此之外,TensorFlow 还提供了 tf.get_variable 函数来创建或者获取变量。代码如下:
# 下面这两行代码是等价的,下面两行代码都是创建变量
v1 = tf.get_varible('v1', shape=[1], initializer=tf.constant_initializer(1.0))
v2 = tf.Varible(tf.constant(1.0, shape=[1], name='v2'))
tf.get_variable 创建变量时,需要提供初始化方法。TensorFlow 中提供了7种不同的初始化方法,如下表:
初始化函数 | 功能 | 主要参数 |
---|---|---|
tf.constant_initializer | 将变量初始化为给定常量 | 常量的取值 |
tf.random_normal_initializer | 将变量初始化为满足正态分布的随机值 | 正态分布的均值和标准差 |
Tf.truncated_normal_initializer | 将变量初始化为满足正态分布的随机值,但如果随机出来的值偏离平均值超过 2 个标准差,那么这组数将会被重新随机 | 正态分布的均值和标准差 |
tf.random_uniform_initializer | 将变量初始化为满足平均分布的随机值 | 最大、最小值 |
tf.uniform_unit_scaling_initializer | 将变量初始化为满足平均分布但不影响输出数量级的随机值 | factor (山城随机值时乘的系数) |
tf.zeros_initializer | 将变量设置为全0 | 变量维度 |
tf.ones_initializer | 将变量设置为全1 | 变量维度 |
tf.get_variable 和 tf.Variable 的另一个区别在于,tf.get_variable中,name 属性是一个必选项,而tf.Variable中则是一个可选项。当tf.get_variable创建一个变量时,如果这个变量已经存在就会报错,这样就可以避免算法开发人员的疏忽而产生的错误。
1.4.2 tf.variable_scope
当 tf.get_variable 获取一个已经创建的变量时,需要通过 tf.variable_scope 函数来生成一个上下文管理器,并明确指定在这个上下文管理器中,tf.get_variable 将直接获取已经生成的变量。代码如下:
with tf.variable_scope("foo"):
v = tf.get_variable("v", [1], initializer=tf.constant_initializer(1.0))
with tf.variable_scope("foo", reuse=True):
v1 = tf.get_variable("v", [1])
# Terminal will output true.
print(v == v1)
注意,当将 tf.variable_scope 函数的 reuse 参数设置为 TRUE 时,这个上下文管理器内所有的 tf.get_variable 函数会直接获取已经创建的变量。但是,如果变量不存在,就会报错。相反,如果 tf.variable_scope 函数的 reuse 参数被设置为 None 或 False 时,tf.get_variable 将会创建新的变量。如果同名的变量已经存在,此时也会报错。例子如下:
# tf.variable_scope 可以嵌套
with tf.variable_scope("root"):
print(tf.get_variable_scope().reuse)
with tf.variable_scope("foo", reuse=True):
print(tf.get_variable_scope().reuse)
with tf.variable_scope("bar"):
print(tf.get_variable_scope().reuse)
print(tf.get_variable_scope().reuse)
tf.variable_scope 就相当于一个命名空间,在其中创建的变量会加上当前命名空间名作为前缀。例子如下:
v1 = tf.get_variable("v", [1])
print(v1.name)
with tf.variable_scope("foo",reuse=True):
v2 = tf.get_variable("v", [1])
print(v1.name)
with tf.variable_scope("foo"):
with tf.variable_scope("bar"):
v3 = tf.get_variable("v", [1])
print(v3.name)
v4 = tf.get_variable("v1", [1])
print(v4.name)
实际中,会将 reuse 作为一个参数,这样可以动态决定创建还是获取变量。例子如下:
import tensorflow as tf
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500
def get_weight_variable(shape, regularizer):
weights = tf.get_variable("weights", shape, initializer=tf.truncated_normal_initializer(stddev=0.1))
if regularizer != None: tf.add_to_collection('losses', regularizer(weights))
return weights
def inference(input_tensor, regularizer, reuse=False):
with tf.variable_scope('layer1', reuse=reuse):
weights = get_weight_variable([INPUT_NODE, LAYER1_NODE], regularizer)
biases = tf.get_variable("biases", [LAYER1_NODE], initializer=tf.constant_initializer(0.0))
layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)
with tf.variable_scope('layer2', reuse=reuse):
weights = get_weight_variable([LAYER1_NODE, OUTPUT_NODE], regularizer)
biases = tf.get_variable("biases", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))
layer2 = tf.matmul(layer1, weights) + biases
return layer2
x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
y = inference(x)
# 通过调用 inference(new_x, True) 使用训练好的神经网络进行推导
new_x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='new_x-input')
new_y = inference(new_x, True)
1.5 持久化
训练好的模型需要保存下来,之后每次预测时,只需要提前加载模型即可。
1.5.1 保存模型
import tensorflow as tf
'''保存模型'''
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name='v1')
v2 = tf.Variable(tf.constant(1.0, shape=[1]), name='v2')
result = v1 + v2
init_op = tf.initializer_all_variables()
# 声明 tf.train.Saver 类用于保存模型
saver = tf.train.Saver()
with tf.Session() as sess:
sess.run(init_op)
# 将模型保存到 /path/to/model/model.ckpt 文件
saver.save(sess, '/path/to/model/model.ckpt')
TensorFlow 模型一般会保存在后缀为 .ckpt 的文件中。虽然上面程序制定了一个文件路径,但是在这个文件目录下会出现三个文件,因为 TensorFlow 会将计算图的结构和图上参数取值分开保存。
第一个文件为:model.ckpt.meta,她保存了 TensorFlow 计算图的结构。
第二个文件为:model.ckpt,她保存了 TensorFlow 程序中的每一个变量的取值。
第三个文件为:checkpoint 文件,这个文件保存了一个目录下所有的模型文件列表。
1.5.2 加载模型
方式一:
import tensorflow as tf
'''加载模型'''
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name='v1')
v2 = tf.Variable(tf.constant(1.0, shape=[1]), name='v2')
result = v1 + v2
init_op = tf.initializer_all_variables()
# 声明 tf.train.Saver 类用于保存模型
saver = tf.train.Saver()
with tf.Session() as sess:
saver.restore(sess, '/path/to/model/model.ckpt')
print(sess.run(result))
这种方式加载模型中没有运行变量的初始化过程,而是将变量的值通过已经保存的模型加载进来。
方式二:
import tensorflow as tf
'''加载模型'''
# 直接加载持久化的图
saver = tf.train.import_meta_graph('/path/to/model/model.ckpt/model.ckpt.meta')
with tf.Session() as sess:
saver.restore(sess, '/path/to/model/model.ckpt')
# 通过张量的名称来获取张量,输出 [3.]
print(sess.run(tf.get_default_graph().get_tensor_by_name('add:0')))
方式二,不会重复定义图上的运算,直接加载已经持久化的图。注意通过代码对比上面两种方式的不同点。
1.5.3 保存或加载部分变量
有时可能只需要保存或者加载部分变量,比如现在有一个训练好的五层神经网络,想尝试六层网络,此时,直接加载前五层的参数到新模型中即可。
为了保存或者加载部分变量,在声明 tf.train.Saver 类时可以提供一个列表来指定需要保存或者加载的变量。比如,saver = tf.train.Saver([v1]), 此时只有 v1 变量会被加载进来,但如果之后用到没有被加载进来的变量,便会报变量未初始化的错误。
1.5.4 通过保存或加载重命名变量
例子如下:
'''加载模型'''
# 这里声明的变量名称和已经保存的模型中的变量的名称不同。
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name = "other-v1")
v2 = tf.Variable(tf.constant(1.0, shape=[1]), name = "other-v2")
# 使用一个 dict 来重命名变量。这个字典制定了原来名称为 v1 的变量现在加载到变量 v1中(名称为 other-v1),名称为 v2 的变量加载到变量 v2 中(名称为 other-v2)
saver = tf.train.Saver({"v1": v1, "v2": v2})
注意仔细体会上面的例子,这里的顺序是将 dict 中的 key 对应的变量赋值为 value。
TensorFlow 提供了这一功能的目的之一是:方便使用变量的滑动平均值。神经网络的优化方法中有一种方法叫滑动平均模型,在 TensorFlow 中,每一个变量的滑动平均值是通过影子变量维护的,所以要获取变量的滑动平均值实际上就是获取这个影子变量的取值。如果在加载模型时直接将影子变量映射到变量自身,那么在使用训练好的模型时就不需要再强调函数来获取变量的滑动平均值了。这样就大大方便了滑动平均模型的使用。
一个保存滑动平均模型的例子:
import tensorflow as tf
'''使用滑动平均'''
v = tf.Variable(0, dtype=tf.float32, name="v")
for variables in tf.global_variables():
print(variables.name)
ema = tf.train.ExponentialMovingAverage(0.99)
maintain_averages_op = ema.apply(tf.global_variables())
for variables in tf.global_variables():
print(variables.name)
'''保存滑动平均模型'''
saver = tf.train.Saver()
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
sess.run(tf.assign(v, 10))
sess.run(maintain_averages_op)
# 保存的时候会将v:0 v/ExponentialMovingAverage:0这两个变量都存下来。
saver.save(sess, "Saved_model/model1.ckpt")
print(sess.run([v, ema.average(v)]))
'''加载滑动平均模型'''
v = tf.Variable(0, dtype=tf.float32, name="v")
# 通过变量重命名将原来变量v的滑动平均值直接赋值给v。
saver = tf.train.Saver({"v/ExponentialMovingAverage": v})
with tf.Session() as sess:
saver.restore(sess, "Saved_model/model1.ckpt")
print sess.run(v)
为了方便加载时重命名滑动平均变量,tf.train.ExponentialMovingAverage 类提供了 variables_to_restore 函数来生成 tf.train.Saver 类所需要的变量重命名字典。实际项目中会使用的方式:
import tensorflow as tf
v = tf.Variable(0, dtype=tf.float32, name="v")
ema = tf.train.ExponentialMovingAverage(0.99)
print(ema.variables_to_restore())
# 注意下面一行的用法
saver = tf.train.Saver(ema.variable_to_restore())
with tf.Session() as sess:
saver.restore(sess, "Saved_model/model1.ckpt")
print(sess.run(v))
1.5.5 保存部分 Graph
有时并不需要保存整个 Graph 的信息,只需要保存部分 Graph 即可,比如在测试或者离线预测时,只需要知道如何从神经网络的输入层经过前向传播计算得到输出层即可,而不需要类似于变量初始化、模型保存等辅助节点的信息。这种情况在迁移学习时就会遇到。而且,将变量取值和计算图结构分成不同的文件存储有时候也不方便,于是 TensorFlow 提供了convert_variables_to_constants 函数,通过这个函数可以将计算图中的变量及其取值通过常量的方式保存,这样整个 TensorFlow 计算图就可以统一存放在一个文件中。例子如下:
import tensorflow as tf
from tensorflow.python.framework import graph_util
'''pb文件的保存方法'''
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name = "v1")
v2 = tf.Variable(tf.constant(1.0, shape=[1]), name = "v2")
result = v1 + v2
init_op = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init_op)
# 导出当前计算图的 GraphDef 部分,只需要这一部分就可以完成从输入层到输出层的计算过程。
graph_def = tf.get_default_graph().as_graph_def()
# 将图中的变量及其取值转化为常量,同时将图中不必要的节点去掉。
# 一些系统运算也会被转化为计算图中的节点(比如变量初始化操作)。如果只关心程序中定义的某些计算时,和这些无关的节点就没有必要导出并保存了。
# 下面这一行代码中,最后一个参数['add']给出了保存的节点名称。
# 注意,这里给出的是计算节点的名称,所以没有后面的:0
output_graph_def = graph_util.convert_variables_to_constants(sess, graph_def, ['add'])
# 将导出的模型存入文件
with tf.gfile.GFile("Saved_model/combined_model.pb", "wb") as f:
f.write(output_graph_def.SerializeToString())
通过下面的代码可以直接计算定义的加法运算的结果。当只需要得到计算图中某个节点的取值时,这提供了一个更加方便的方法。迁移学习就是使用的这种方式。
'''加载pb文件'''
from tensorflow.python.platform import gfile
with tf.Session() as sess:
model_filename = "Saved_model/combined_model.pb"
with gfile.FastGFile(model_filename, 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
result = tf.import_graph_def(graph_def, return_elements=["add:0"])
print(sess.run(result))
1
1