一、概述

如果用一句话概括TensorFlow,我认为,TensorFlow是一个复杂数学公式的图表达及高性能数据计算平台。怎么理解这句话?

首先,理解“复杂”。复杂不是说用了什么高深难懂的数学函数,函数的难度最多到sigmod, tanh这类。所谓“复杂”是指结构的复杂,即使简单的加、减、倒数、平方等,层层嵌套起来,也能构造一个超级复杂的公式。神经网络的结构,说白了,就是这种层层嵌套的公式结构,每个子公式的输出,是包裹它的更大公式的输入。

其次,为什么要“图表达”?学过数据结构的人知道,数学公式翻译成代码程序,实际先转换成树的数据结构,再递归地进行计算。我们都说“一串数学公式”,公式在我们看来,是序列化的,可是计算机不这么看,因为公式中每部分的优先级不一样,哪个先算哪个后算,我们人类可以根据括号一眼识别,而计算机需要先转成一个树,树的每个节点代表一次最小单位的操作,比如乘法、加法等,像流水线上的操作台,进来一个或几个部件,即输入,经操作后组装成一个新部件,即输出。树是一类较为简单的图结构,TensorFlow使用图,比树拥有更强的表达能力。

再次,如何实现“高性能”?你可能会说用GPU编程、分布式平台。当然,这没错,但是提升性能的第一阶段并不在这里,而是在图优化上。举个SQL数据库的例子,编程用的是SQL语句,构建语句的关键字诸如select, where, order by等,不是数学公式的运算符,当你写完一串SQL语句并点击执行时,数据库系统并不是马上进行查询操作,而是先生成一个包含众多命令的查询执行计划(query execution plan),送给查询优化器进行优化,其中查询计划就是一个图表达,当完成一个查询需求有多种SQL语句写法时,即使你的SQL很糟糕,查询优化器尽可能帮你找到最优且等价的查询计划。TensorFlow也有类似的优化器,当你的数学公式写得很冗长时,对应一个繁冗啰嗦的图表达,优化器会想办法降低图的冗余和没必要的复杂,给你一个简洁的图。

最后,TensorFlow是一个数据计算平台。它非一个公式计算平台,像计算器或Matlab那种,敲完一个公式,结果就出来了,而是构建一个公式计算的模板,可以没有具体数值,但我知道它是怎么算的,这让此类平台多了一点符号计算的味道(事实上用链式法则求梯度时,是需要有符号计算能力的)。这个公式计算模板就是上面的那张图,构建模板本身就是组装图。需要指出的是,组装图是一个阶段,执行图是另一个阶段,每次执行前需要给出数据,包括输入数据和初始化参数的数据,才能进行具体数的计算。从机器学习的角度看,公式计算的图是一个模型的架构图,接着,数据集有训练集和测试集,当模型的架构图搭好后,需要用训练集去训练模型,从而得到模型中参数的具体值,才能接下来在测试集上做预测。

TensorFlow不仅是一个计算平台,也是一个编程的作业平台。它负责提供砖瓦,及施工作业的规范和支持环境,你负责盖你想盖的大厦。大厦的骨架是一个计算图(computation graph),包括的要素有:

  1. Op: operation的简写,表示图中的节点,负责最小单位的计算,计算的输入是零到多个的Tensors,输出也是零到多个的Tensors
  2. Tensor:表示数据的多维数组,例如要保存一张灰度图片,用二维数组,维度是height和width,每个元素代表每个像素点的灰度值;要保存一张彩色图片,用三维数组,维度是height, width和channels,每个元素代表每个像素点在每个R,G,B通道的值;要表示一小撮图片集,用四维数组,维度是batch, height, width和channels,每个元素代表每张图片上每个像素点在每个R,G,B通道的值
  3. Session:执行图,把最小单位的计算Op交给CPUs或GPUs,接收返回的计算结果(Python中是numpy ndarray对象,C/C++中是tensorflow::Tensor实例)

二、组装图阶段

Op构造函数是组装图的砖瓦,调用它创建节点并添加到图中,节点上关联input和output:

  1. output = Op构造函数( input, …,input )
  2. 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对象的意义有两点:

  1. 把前面定义好的计算图,拆解成一个个待执行的操作
  2. 把一个个操作分配到它可用的计算资源上。计算资源包括:本机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。

  1. 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
  2. Rank:tensor的维度数,即多维数组的维度数,rank为0的tensor是一个标量,rank为1的tensor是一个向量,rank为2的tensor是一个矩阵。例如,t=[[1,2,3], [4,5,6], [7,8,9]]的rank为2。
  3. 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'):初始所给列表中的变量,即并行执行每个变量自己的initialzier
  • tf.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)