Tensorflow 入门
计算图
Tensorflow 通过计算图来定义神经网络的结构,每个节点就是一个运算,而常量也被转化为一种永远输出固定值的运算。
import tensorflow as tf
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = a + b
在 Tensorflow 程序中,系统会自动维护一个默认的计算图,通过 tf.get_default_graph 函数可以获取当前默认的计算图。对应张量可以通过 “变量名.graph” 来获取对应张量的计算图
print(a.graph is tf.get_default_graph())
除了使用默认的计算图,Tensorflow 支持通过 tf.Graph 函数来生成新的计算图。同时,不同的计算图上的张量和运算都不会共享。
import tensorflow as tf
g1 = tf.Graph()
with g1.as_default():
v = tf.get_variable("v", [1], initializer = tf.zeros_initializer()) # 设置初始值为0
g2 = tf.Graph()
with g2.as_default():
v = tf.get_variable("v", [1], initializer = tf.ones_initializer()) # 设置初始值为1
with tf.Session(graph = g1) as sess:
tf.global_variables_initializer().run()
with tf.variable_scope("", reuse=True):
print(sess.run(tf.get_variable("v")))
with tf.Session(graph = g2) as sess:
tf.global_variables_initializer().run()
with tf.variable_scope("", reuse=True):
print(sess.run(tf.get_variable("v")))
计算图还可以通过 tf.Graph.device 函数来指定运行计算的设备。这为 Tensorflow 使用 GPU 提供了机制。
g = tf.Graph()
# 指定计算运行的设备
with g.device('/gpu:0'):
result = a + b
最后还有一个集合(collection),它被用来管理不同类别的资源。Tensorflow 自动管理了一些最常用的 集合
- tf.GraphKeys.VARIABLES:所有变量,用于持久化 TensorFlow 模型
- tf.GraphKeys.TRAINABLE_VARIABLES:可学习参数(神经网络中的参数),用于模型训练、生成模型可视化内容
- tf.GraphKeys.SUMMARIES:日志生成相关的张量,用于 Tensorflow 计算可视化
- tf.GraphKeys.QUEUE_RUNNERS:处理输入的QueueRunner,用于输入处理
- tf.GraphKeys.MOVING_AVERAGE_VARIABLES:所有计算了滑动平均值的变量,用于计算变量的滑动平均值
其中可以通过 tf.add_to_collection 将资源加入一个或多个集合中,然后通过 tf.get_collection 获取一个集合里面的所有资源。
张量
在 Tensorflow 程序中,所有的数据都通过张量的形式来表示。从功能的角度上看,张量可以被简单理解为多维数组。其中零价张量表示标量(Scale);一阶张量为向量(vector),也就是一个数组;第 n 价张量可以理解为一个 n 维数组。但张量在 Tensorflow 中的实现并不是直接采用数组的形式,它只是对 Tensorflow 中运算结果的引用。在张量中并没有真正保存数字,它保存的是如何得到这些数字的计算过程。
import tensorflow as tf
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = a + b
print(result)
"""
Tensor("add:0", shape=(2,), dtype=float32)
"""
sess = tf.InteractiveSession ()
print(result.eval())
sess.close()
"""
[3. 5.]
"""
从结果可以看出一个张量主要保存了三个属性:名字(name),维度(shape)和类型(type)。
- 张量的名字是一个张量的唯一标识符,其又分为 "node:src_output" 两部分,其中 node 为节点的名称,src_output 表示当前张量来自节点的第几个输出。
- 维度描述的一个张量的维度信息,shape=(2,) 表示这是一个数组,长度为 2。
- 类型表示张量的数据类型,主要包括 tf.float32,tf.float64,tf.int8,tf.int16,tf.int32,tf.int64,tf.uint8,tf.bool,tf.complex64, tf.complex128。如果不指定类型,Tensorflow 会给出默认的类型。同时,Tensorflow 会对参与运算的所有张量进行类型检查,当发现类型不匹配时会报错。因此一般建议通过指定 dtype 来明确指出变量或者常量的类型,从而避免默认类型可能的类型不匹配问题。
会话
Tensorflow 利用会话(session)来执行计算图中定义好的操作,同时拥有并管理 Tensorflow 程序运行时的所有资源。当所有计算完成之后需要关闭会话来帮组系统回收资源,否则就可能出现资源泄漏的问题。
# 创建一个会话。
sess = tf.Session()
# 使用会话得到之前计算的结果。
print(sess.run(result))
# 关闭会话使得本次运行中使用到的资源可以被释放。
sess.close()
为了避免异常情况而退出导致的资源泄漏,通常使用如下方式定义会话。
with tf.Session() as sess:
print(sess.run(result))
之前介绍过 Tensorflow 会自动生成一个默认的计算图,如果没有特殊指定,运算会自动加入这个计算图中。Tensorflow 中的会话也有类似的机制,但 Tensorflow 不会自动生成默认的会话,而是需要手动指定。但一旦指定了默认的会话就能直接默认在默认会话中运行。
sess = tf.Session()
with sess.as_default():
print(result.eval())
在交互式环境下(比如 Python 脚本或者 Jupyter 的编辑器下),通过设置默认会话的方式来获取张量的取值更加方便。所有 Tensorflow 提供了一种在交互式环境下直接构建默认会话的函数。
sess = tf.InteractiveSession ()
print(result.eval())
sess.close()
不过无论使用哪种方式都可以通过 ConfigProto Protocol Buffer 来配置需要生成的会话。
config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)
sess1 = tf.InteractiveSession(config=config)
sess2 = tf.Session(config=config)
- allow_soft_placement:当某些运算无法被当前的 GPU 支持时,可以自动调整到 CPU上运行,而不是报错。默认为 False。
- log_device_placement:将会记录每个节点被安排在了哪个设备上以方便调试。在生产环境中将这个参数设置为 False 可以减少日志量。
Tensorflow 实现神经网络
首先给出一个网址可以方便地了解神经网络的框架:http://playground.tensorflow.org
前面已经介绍了,Tensorflow 通过计算图来定义变量之间的关系,并通过会话运行需要的求得的张量,因此被调用的张量必须给出明确的初始化方法。主要的初始化方法包括:
范例程序如下
import tensorflow as tf
w1= tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2= tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
x = tf.constant([[0.7, 0.9]])
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
sess = tf.Session()
sess.run(w1.initializer)
sess.run(w2.initializer)
print(sess.run(y))
sess.close()
为简化对张量的初始化,可以使用 tf.global_variables_initializer 对所有张量进行初始化。需要注意的是,张量的类型一旦给定就不能进行修改了。但是对于张量的维度,如果设置参数 validate_shape 为 False,是可以改变的。
上述程序还存在一个问题,其通过 tf.constant 来初始化输入。但 Tensorflow 在每次迭代时都将在计算图中增加一个节点,这样就会使得计算图非常大,而且利用率很低。为避免这个问题,Tensorflow 提供了 placeholder 机制用于提供输入数据。
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]]}))
其中在定义 placeholder 时,这个位置上的数据类型是需要指定的。和其他张量一样,placeholder 的类型也是不可以改变的。不过 placeholder 中数据的维度可以根据提供的数据推到得到,所以不一定要给出。但如果维度是可以确定的,那么给出维度可以降低出错的概率。
同时,除了只提供一个 batch 的训练样例,还可以通过多维数组一次给出多个测试样例。
x = tf.placeholder(tf.float32, shape=(3, 2), name="input")
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
sess = tf.Session()
#使用tf.global_variables_initializer()来初始化所有的变量
init_op = tf.global_variables_initializer()
sess.run(init_op)
print(sess.run(y, feed_dict={x: [[0.7,0.9],[0.1,0.4],[0.5,0.8]]}))
最后需要给出损失函数和优化函数,其中 Tensorflow 支持 7 种不同的优化器,常用的有 tf.train.GradientDescentOptimizer,tf.train.AdamOptimizer,tf.train.MomentumOptimizer 三个。
完整的神经网络样例程序
import tensorflow as tf
from numpy.random import RandomState
# 1. 定义神经网络的参数,输入和输出节点。
batch_size = 8
w1= tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2= tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))
x = tf.placeholder(tf.float32, shape=(None, 2), name="x-input")
y_= tf.placeholder(tf.float32, shape=(None, 1), name='y-input')
# 2. 定义前向传播过程,损失函数及反向传播算法。
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)
y = tf.sigmoid(y)
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0))
+ (1 - y_) * tf.log(tf.clip_by_value(1 - y, 1e-10, 1.0)))
train_step = tf.train.AdamOptimizer(0.001).minimize(cross_entropy)
# 3. 生成模拟数据集。
rdm = RandomState(1)
X = rdm.rand(128,2)
Y = [[int(x1+x2 < 1)] for (x1, x2) in X]
# 4. 创建一个会话来运行TensorFlow程序。
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
# 输出目前(未经训练)的参数取值。
print(sess.run(w1))
print(sess.run(w2))
print("\n")
# 训练模型。
STEPS = 5000
for i in range(STEPS):
start = (i*batch_size) % 128
end = (i*batch_size) % 128 + batch_size
sess.run([train_step, y, y_], feed_dict={x: X[start:end], y_: Y[start:end]})
if i % 1000 == 0:
total_cross_entropy = sess.run(cross_entropy, feed_dict={x: X, y_: Y})
print("After %d training step(s), cross entropy on all data is %g" % (i, total_cross_entropy))
# 输出训练后的参数取值。
print("\n")
print(sess.run(w1))
print(sess.run(w2))
"""
Output:
[[-0.8113182 1.4845988 0.06532937]
[-2.4427042 0.0992484 0.5912243 ]]
[[-0.8113182 ]
[ 1.4845988 ]
[ 0.06532937]]
After 0 training step(s), cross entropy on all data is 1.89805
After 1000 training step(s), cross entropy on all data is 0.655075
After 2000 training step(s), cross entropy on all data is 0.626172
After 3000 training step(s), cross entropy on all data is 0.615096
After 4000 training step(s), cross entropy on all data is 0.610309
[[ 0.02476984 0.5694868 1.6921942 ]
[-2.1977348 -0.23668921 1.1143897 ]]
[[-0.45544702]
[ 0.4911093 ]
[-0.9811033 ]]
"""
从结果可以看出交叉熵在不断下降,网络参数也发生的变化——网络确实在训练中有了优化。