一、概述
如果用一句话概括TensorFlow,我认为,TensorFlow是一个复杂数学公式的图表达及高性能数据计算平台。怎么理解这句话?
首先,理解“复杂”。复杂不是说用了什么高深难懂的数学函数,函数的难度最多到sigmod, tanh这类。所谓“复杂”是指结构的复杂,即使简单的加、减、倒数、平方等,层层嵌套起来,也能构造一个超级复杂的公式。神经网络的结构,说白了,就是这种层层嵌套的公式结构,每个子公式的输出,是包裹它的更大公式的输入。
其次,为什么要“图表达”?学过数据结构的人知道,数学公式翻译成代码程序,实际先转换成树的数据结构,再递归地进行计算。我们都说“一串数学公式”,公式在我们看来,是序列化的,可是计算机不这么看,因为公式中每部分的优先级不一样,哪个先算哪个后算,我们人类可以根据括号一眼识别,而计算机需要先转成一个树,树的每个节点代表一次最小单位的操作,比如乘法、加法等,像流水线上的操作台,进来一个或几个部件,即输入,经操作后组装成一个新部件,即输出。树是一类较为简单的图结构,TensorFlow使用图,比树拥有更强的表达能力。
再次,如何实现“高性能”?你可能会说用GPU编程、分布式平台。当然,这没错,但是提升性能的第一阶段并不在这里,而是在图优化上。举个SQL数据库的例子,编程用的是SQL语句,构建语句的关键字诸如select, where, order by等,不是数学公式的运算符,当你写完一串SQL语句并点击执行时,数据库系统并不是马上进行查询操作,而是先生成一个包含众多命令的查询执行计划(query execution plan),送给查询优化器进行优化,其中查询计划就是一个图表达,当完成一个查询需求有多种SQL语句写法时,即使你的SQL很糟糕,查询优化器尽可能帮你找到最优且等价的查询计划。TensorFlow也有类似的优化器,当你的数学公式写得很冗长时,对应一个繁冗啰嗦的图表达,优化器会想办法降低图的冗余和没必要的复杂,给你一个简洁的图。
最后,TensorFlow是一个数据计算平台。它非一个公式计算平台,像计算器或Matlab那种,敲完一个公式,结果就出来了,而是构建一个公式计算的模板,可以没有具体数值,但我知道它是怎么算的,这让此类平台多了一点符号计算的味道(事实上用链式法则求梯度时,是需要有符号计算能力的)。这个公式计算模板就是上面的那张图,构建模板本身就是组装图。需要指出的是,组装图是一个阶段,执行图是另一个阶段,每次执行前需要给出数据,包括输入数据和初始化参数的数据,才能进行具体数的计算。从机器学习的角度看,公式计算的图是一个模型的架构图,接着,数据集有训练集和测试集,当模型的架构图搭好后,需要用训练集去训练模型,从而得到模型中参数的具体值,才能接下来在测试集上做预测。
TensorFlow不仅是一个计算平台,也是一个编程的作业平台。它负责提供砖瓦,及施工作业的规范和支持环境,你负责盖你想盖的大厦。大厦的骨架是一个计算图(computation graph),包括的要素有:
- Op: operation的简写,表示图中的节点,负责最小单位的计算,计算的输入是零到多个的Tensors,输出也是零到多个的Tensors
- Tensor:表示数据的多维数组,例如要保存一张灰度图片,用二维数组,维度是height和width,每个元素代表每个像素点的灰度值;要保存一张彩色图片,用三维数组,维度是height, width和channels,每个元素代表每个像素点在每个R,G,B通道的值;要表示一小撮图片集,用四维数组,维度是batch, height, width和channels,每个元素代表每张图片上每个像素点在每个R,G,B通道的值
- Session:执行图,把最小单位的计算Op交给CPUs或GPUs,接收返回的计算结果(Python中是numpy ndarray对象,C/C++中是tensorflow::Tensor实例)
二、组装图阶段
Op构造函数是组装图的砖瓦,调用它创建节点并添加到图中,节点上关联input和output:
- output = Op构造函数( input, …,input )
- output = Op构造函数( 常量,…,常量 )
TensorFlow的Python库有一个默认图,默认情况下每次调用Op构造函数,会向默认图添加节点。默认图对很多应用已经够用,除非你想管理多个自己的图,请参见Graph类。下面的例子,我们创建了两个Constant Op和一个Matmul Op
import tensorflow as tf
# 创建两个Constant op的节点,添加到默认图上
matrix1 = tf.constant([[3., 3.]])
matrix2 = tf.constant([[2.],[2.]])
# 创建一个Matmul op的节点并添加到默认图,用上两个节点的输出作为输入,实现一个矩阵乘法
product = tf.matmul(matrix1, matrix2)
三、执行图阶段
图的执行阶段都是通过一个Session对象完成,因此要首先创建Session对象:
- 无传入参数,session执行本地的默认图
sess = tf.Session()
- 有传入参数
# 创建一个分布式的session,为了后面在TensorFlow cluster的指定machine位置上执行图
sess = tf.Session("grpc://example.org:2222")
Session对象的意义有两点:
- 把前面定义好的计算图,拆解成一个个待执行的操作
- 把一个个操作分配到它可用的计算资源上。计算资源包括:本机CPU,本机GPU(默认的首要计算资源),本机另一个GPU,集群的其他机器。这里可看到,TensorFlow从单机版到多机分布式版,这一扩展是很自然的。此时,被session指定的machine,作为当前session的master,其他机器作为可用计算资源,变成workers。
tf.device("/cpu:0") # 指定使用本机CPU,结束使用需要close()关闭,从指定到结束的区间中创建的op,将在该计算资源上完成计算
tf.device("/gpu:0") # 指定使用本机GPU
tf.device("/gpu:1") # 指定使用本机第二块GPU
tf.device("/job:ps/task:0") # 指定使用一个worker
然后,调用session的run()方法,run()中的参数可以是任何一个你想知道具体值的output,它会根据优化后的图,自动执行需要的所有op,它的返回时一个numpy ndarray对象
result = sess.run(product)
最后,调用close()关闭session,熟悉Python的同学可以使用“with”块简化代码。
四、在IPython中的用法
IPython提供一种交互式的Python开发环境,TensorFlow在此提供了交互式的session,包括:InteractiveSession类,Tensor.eval()和Operation.run()方法。这里可看到,每个Op都有run()方法,每个Tensor实例都有eval()方法。
# 创建一个交互式的session
import tensorflow as tf
sess = tf.InteractiveSession()
x = tf.Variable([1.0, 2.0])
a = tf.constant([3.0, 3.0])
# TensoFlow中的变量都有一个initializer op,执行该op的run()进行初始化
x.initializer.run()
# 在x和a基础上创建一个sub op
sub = tf.sub(x, a)
# 执行output(一个Tensor对象)的eval()
print(sub.eval())
# ==> [-2. -1.]
sess.close()
五、什么是Tensor
TensorFlow顾名思义,tensor是流淌在其中的血液,即用tensor来表示所有在计算图中传递的数据,每个Op的输入和输出都是tensor。从数据结构上看,tensor是一个多维数组,有固定的type, rank和shape。
- Type:即data type,是tensor中元素的数据类型,每个tensor只能有一个data type。常见的有:
tf.float32
,tf.float64
,tf.int8
,tf.int16
,tf.int32
,tf.int64
,tf.uint8
,tf.uint16
,tf.string
,tf.bool
等 - Rank:tensor的维度数,即多维数组的维度数,rank为0的tensor是一个标量,rank为1的tensor是一个向量,rank为2的tensor是一个矩阵。例如,
t=[[1,2,3], [4,5,6], [7,8,9]]
的rank为2。 - Shape:为一个整数的列表,每个整数描述tensor上一个维度的长度。例如,shape为[5]的tensor,对应的rank为1,表明这是一个长度为5的向量;shape为[3,4]的tensor,对应的rank为2,表明这是一个3x4的矩阵;shape为[1,4,3]的tensor,对应的rank为3,表明这是一个1x4x3的张量。
六、什么是Variable
Variable可看成用来保存tensor数据的一段内存缓冲区,模型参数的初始化、保存、更新、持久化到磁盘、从磁盘中还原的过程都是在这里完成。
1、创建Variable:调用Variable构造函数的过程中,会创建一组Op节点:
- Variable Op:该Op的作用是保存具体tensor值到该variable,tensor的shape决定了variable的shape,通常一旦决定,variable的shape是固定的
- Initializer Op:该Op实际是一个附带初始值的assign Op,即赋值操作
- 提供具体初始值的Op:产生的初始值传入Variable构造函数,通常是生成特殊常量的操作(如:zeros Op), 或生成随机数的操作(如:random_normal Op)
- 生成常量的操作:
tf.zeros()
,tf.zeros_like()
,tf.ones()
,tf.ones_like()
,tf.fill()
,tf.constant()
- 生成序列的操作:
tf.linspace()
,tf.range()
- 生成随机数的操作:
tf.random_normal()
,tf.truncated_normal()
,tf.random_uniform()
,tf.random_shuffle()
,tf.random_crop()
,tf.multinomial()
,tf.random_gamma()
- 生成常量的操作:
# 创建两个Variable对象
weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35), name="weights")
biases = tf.Variable(tf.zeros([200]), name="biases")
创建的Variable实例可以绑定到不同的device上。如果有更改Variable值的操作(如:赋值操作v.assign()
,tf.train.Optimizer
中参数更新操作),则必须在指定的device上执行操作。
with tf.device("/cpu:0" 或 "/gpu:0" 或 "/job:ps/task:7"):
v = tf.Variable(...)
2、初始化Variable:执行模型中的Op前,必须先把涉及的所有Variable实例都初始化完毕,最方便的办法是调用tf.global_variables_initializer()
,此方法在计算图上增加一个Op节点,负责执行所有variable的initializer。
# 增加一个Op节点,负责执行所有variable initializers,但此处只创建不执行
init_op = tf.global_variables_initializer()
# 执行图
with tf.Session() as sess:
# 此时,才执行上面的op,完成所有初始化工作
sess.run(init_op)
...
如果使用已创建Variable实例的初始值,可通过variable的initialized_value()
访问其上附带的初始值。做初始化工作的相关函数有:
tf.variables_initializer(var_list, name='init')
:初始所给列表中的变量,即并行执行每个变量自己的initialziertf.global_variables_initializer()
:初始化所有全局变量,相当于tf.variable_initializers(tf.global_variables())
tf.local_variables_initializer()
:初始化所有局部变量tf.is_variable_initialized(variable)
:判断所给变量是否已初始化tf.report_uninitialized_variables(var_list=None, name='report_uninitialized_variables')
:列出var_list
中所有尚未初始化的变量,var_list
默认是global_variables()
+local_variables()
tf.assert_variables_initialized(var_list=None)
:检查var_list
中的变量是否都已初始化
3、持久化和还原Variable:由tf.train.Saver
负责,调用该类的构造函数向图中添加save Op和restore Op,关联到图中所有或指定的variable。持久化到的文件叫“checkpoint file”,记录了一对对variable name=>tensor value,可使用inspect_checkpoint
库的print_tensors_in_checkpoint_file()
函数进行查看。
- 持久化到文件:
# 创建两个变量
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
# 有变量就要有处理初始化的op
init_op = tf.global_variables_initializer()
# 要持久化就要有save op
saver = tf.train.Saver()
# 执行图
with tf.Session() as sess:
# 先执行初始化
sess.run(init_op)
# 执行模型
...
# 最后持久化到/tmp/model.ckpt
save_path = saver.save(sess, "/tmp/model.ckpt")
- 从文件中还原:
# 创建两个变量
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
# 要持久化就要有save op,但省去了init op
saver = tf.train.Saver()
# 执行图
with tf.Session() as sess:
# 从文件中还原变量
saver.restore(sess, "/tmp/model.ckpt")
# 执行模型
...
- 指定持久化和还原的变量:如果所有涉及的Variable实例未还原干净,则需对落下的Variable实例进行初始化
# 创建两个变量
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
# 创建只处理变量v2的saver,且改名
saver = tf.train.Saver({"my_v2": v2}) # 传参是一个dict(新名字=>要处理的变量)
4、更新Variable:每执行一遍图,就会更新一次图中的Variable实例
# 创建一个变量
state = tf.Variable(0, name="counter") # 初始值可以是一个简单的常量
# 创建相关的其他op,包括:constant op, add opp, assign op
one = tf.constant(1)
new_value = tf.add(state, one)
update = tf.assign(state, new_value) # 构成一个回路,为了后面不断更新变量state,但此时不执行赋值
# 创建全局变量的初始化op
init_op = tf.global_variables_initializer()
# 执行图
with tf.Session() as sess:
# 先执行初始化
sess.run(init_op)
# 执行variable op,获取变量state的值
print(sess.run(state))
# 更新三次变量state
for _ in range(3):
sess.run(update)
print(sess.run(state))
七、谈谈Fetch和Feed
1、Fetch:通过session的run()
读取Variable和Op的output,此时若未执行过计算,前面那些被依赖的Op将被执行
result = sess.run([mul, intermed]) # mul和intermed为两个op的output
print result
2、Feed:除了通过tf.constant()
和tf.Variable()
向图中注入具体数据外,还可通过tf.placeholder()
创建一个“feed” operation,在后面执行的sess.run([...], feed_dict={...})
中传入具体数据
# 创建两个"feed" operation
input1 = tf.placeholder(tf.float32)
input2 = tf.placeholder(tf.float32)
output = tf.mul(input1, input2)
# 执行图
with tf.Session() as sess:
# 调用run()时传入具体值
result = sess.run([output], feed_dict={input1:[7.], input2:[2.]})
print(result)