12.1 TensorFlow
12.1.1 TensorFlow 是什么
TensorFlow支持各种异构平台,支持多CPU/GPU、服务器、移动设备,具有良好的跨平台的特性;TensorFlow架构灵活,能够支持各种网络模型,具有良好的通用性;此外,TensorFlow架构i具有良好的可扩展性,对OP的扩展支持,Kernel特化方面表现出众。
TensorFlow最初由Google大脑的研究员和工程师开发出来,用于机器学习和神经网络方面的研究,于2015.10宣布开源,在众多深度学习框架中脱颖而出,在Github上获得了最多的Star量。
12.1.2 TensorFlow的设计理念是什么
TensorFlow的设计理念主要体现在两个方面:
(1)将图定义和图运算完全分开。TensorFlow 被认为是一个“符号主义”的库。我们知道,编程模式通常分为命令式编程(imperative style programming)和符号式编程(symbolic style 皮肉gramming)。命令式编程就是编写我们理解的通常意义上的程序,很容易理解和调试,按照原有逻辑执行。符号式编程涉及很多的嵌入和优化,不容易理解和调试,但运行速度相对有所提升。现有的深度学习框架中,Torch是典型的命令式的,Caffe、MXNet采用了两种编程模式混合的方法,而TensorFlow完全采用符号式编程。
符号式计算一般是先定义各种变量,然后建立一个数据流图,在数据流图中规定各个变量间的计算关系,最后需要对数据流图进行编译,但此时的数据流图还是一个空壳,里面没有任何实际数据,只有把需要运算的输入放进去后,才能在整个模型中形成数据流,从而形成输出值。
例如:
代码语言:javascript复制t = 8 9
print(t)
在传统的程序操作中,定义了 t 的运算,在运行时就执行了,并输出 17。而在 TensorFlow中,数据流图中的节点,实际上对应的是 TensorFlow API 中的一个操作,并没有真正去运行:
代码语言:javascript复制import tensorflow as tf
t = tf.add(8,9)
print(t)
#输出 Tensor{"Add_1:0",shape={},dtype=int32}
(2)TensorFlow 中涉及的运算都要放在图中,而图的运行只发生在会话(session)中。开启会话后,就可以用数据去填充节点,进行运算;关闭会话后,就不能进行计算了。因此,会话提供了操作运行和 Tensor 求值的环境。
例如:
代码语言:javascript复制import tensorflow as tf
#创建图
a = tf.constant([4.0,5.0])
b = tf.constant([6.0,7.0])
c = a * b
#创建会话
sess = tf.Session()
#计算c
print(sess.run(c)) #进行矩阵乘法,输出[24.,35.]
sess.close()
12.1.3 TensorFlow特点有哪些
1. 高度的灵活性
TensorFlow并不仅仅是一个深度学习库,只要可以把你的计算过程表示成一个数据流图的过程,我们就可以使用TensorFlow来进行计算。TensorFlow允许我们用计算图的方式建立计算网络,同时又可以很方便的对网络进行操作。用户可以基于TensorFlow的基础上用Python编写自己的上层结构和库,如果TensorFlow没有提供我们需要的API的,我们也可以自己编写底层的C 代码,通过自定义操作将新编写的功能添加到TensorFlow中。
2. 真正的移植性
TensorFlow 可以在 CPU 和 GPU 上运行,可以在台式机、服务器、移动设备上运行。你想在你的笔记本上跑一下深度学习的训练,或者又不想修改代码,想把你的模型在多个CPU上运行, 亦或想将训练好的模型放到移动设备上跑一下,这些TensorFlow都可以帮你做到。
3. 多语言支持
TensorFlow采用非常易用的python来构建和执行我们的计算图,同时也支持 C 的语言。我们可以直接写python和C 的程序来执行TensorFlow,也可以采用交互式的ipython来方便的尝试我们的想法。当然,这只是一个开始,后续会支持更多流行的语言,比如Lua,JavaScript 或者R语言。
4. 丰富的算法库
TensorFlow提供了所有开源的深度学习框架里,最全的算法库,并且在不断的添加新的算法库。这些算法库基本上已经满足了大部分的需求,对于普通的应用,基本上不用自己再去自定义实现基本的算法库了。
5. 完善的文档
TensorFlow的官方网站,提供了非常详细的文档介绍,内容包括各种API的使用介绍和各种基础应用的使用例子,也包括一部分深度学习的基础理论。
自从宣布开源以来,大量人员对TensorFlow做出贡献,其中包括Google员工,外部研究人员和独立程序员,全球各地的工程师对TensorFlow的完善,已经让TensorFlow社区变成了Github上最活跃的深度学习框架。
12.1.4 TensorFlow的系统架构是怎样的
整个系统从底层到上层可分为七层:
设备层:硬件计算资源,支持CPU、GPU
网络层:支持两种通信协议
数值计算层:提供最基础的计算,有线性计算、卷积计算
高维计算层:数据的计算都是以数组的形式参与计算
计算图层:用来设计神经网络的结构
工作流层:提供轻量级的框架调用
构造层:最后构造的深度学习网络可以通过TensorBoard服务端可视化
12.1.5 TensorFlow编程模型是怎样的
TensorFlow 的编程模型:让向量数据在计算图里流动。那么在编程时至少有这几个过程:1、构建图;2、启动图;3、给图输入数据并获取结果。
1. 构建图
TensorFlow 的图的类型是 tf.FGraph,它包含着计算节点和tensor的集合。
这里引用了两个新概念:tensor和计算节点。我们先介绍tensor,一开始我们就介绍了,我们需要把数据输入给启动的图才能获取计算结果。那么问题来了,在构建图时用什么表示中间计算结果?这个时候tensor的概念就需要引入了。类型是 tf.Tensor,代表某个计算节点的输出,一定要看清楚是“代表”。它主要有两个作用:
(1)构建不同计算节点直接的数据流
(2)在启动图时,可以设置某些tensor的值,然后获取指定tensor的值。这样就完成了计算的输入输出功能。
如下代码所示:
代码语言:javascript复制inImage = tf.placeholder(tf.float32,[32,32,3],"inputImage")
processedImage = tf.image.per_image_standardization(inImage,"processedImage")
这里inImage和processedImage都是tensor类型。它们代表着计算节点输出的数据,数据的值具体是多少在启动图的时候才知道。上面两个方法调用都传递了一个字符串,它是计算节点的名字,最好给节点命名,这样我们可以在图上调用get_tensor_by_name(name)获取对应的tensor对象,十分方便。(tensor名字为“<计算节点名字>:<tensor索引>”)
创建tensor时,需要指定类型和shape。对不同tensor进行计算时要求类型相同,可以使用 tf.cast 进行类型转换。同时也要求 shape (向量维度)满足运算的条件,我们可以使用 tf.reshape 改变shape。
现在了解计算节点的概念,其功能是对tensor进行计算、创建tensor或进行其他操作,类型是tf.Operation。获取节点对象的方法为get_operation_by_name(name)。
构建图,如下代码:
代码语言:javascript复制g=tf.Graph()
with g.as_default():
input_data=tf.placeholder(tf.float32,[None,2],"input_data")
input_label=tf.placeholder(tf.float32,[None,2],"input_label")
W1=tf.Variable(tf.truncated_normal([2,2]),name="W1")
B1=tf.Variable(tf.zeros([2]),name="B1")
output=tf.add(tf.matmul(input_data,W1),B1,name="output")
cross_entropy=tf.nn.softmax_cross_entropy_with_logits(logits=output,labels=input_label)
train_step=tf.train.AdamOptimizer().minimize(cross_entropy,name="train_step")
initer=tf.global_variables_initializer()
上面的代码中我们创建了一个图,并在上面添加了很多节点。我们可以通过调用get_default_graph()获取默认的图。
Input_data,input_label,W1,B1,output,cross_entropy都是tensor类型,train_step,initer,是节点类型。
有几类tensor或节点比较重要,下面介绍一下:
(1)placeholder
Tensorflow,顾名思义, tensor代表张量数据,flow代表流,其最初的设计理念就是构建一张静态的数据流图。图是有各个计算节点连接而成,计算节点之间流动的便是中间的张量数据。要想让张量数据在我们构建的静态计算图中流动起来,就必须有最初的输入数据流。而placeholder,翻译过来叫做占位符,顾名思义,是给我们的输入数据提供一个接口,也就是说我们的一切输入数据,例如训练样本数据,超参数数据等都可以通过占位符接口输送到数据流图之中。使用实例如下代码:
代码语言:javascript复制import tensorflow as tf
x = tf.placeholder(dtype=tf.float32,shape=[],name='x')
y = tf.placeholder(dtpe=tf.float32,shape=[],nmae='y')
z = x*y
with tf.Session() as sess:
prod = sess.run(z,feed_dict={x:1.,y:5.2})
print(prod)
[out]:5.2
(2)variable
无论是传统的机器学习算法,例如线性支持向量机(Support Vector Machine, SVM),其数学模型为y = <w,x> b,还是更先进的深度学习算法,例如卷积神经网络(Convolutional Neural Network, CNN)单个神经元输出的模型y = w*x b。可以看到,w和b就是我们要求的模型,模型的求解是通过优化算法(对于SVM,使用 SMO[1]算法,对于CNN,一般基于梯度下降法)来一步一步更新w和b的值直到满足停止条件。因此,大多数机器学习的模型中的w和b实际上是以变量的形式出现在代码中的,这就要求我们在代码中定义模型变量。
代码语言:javascript复制import tensorflow as tf
a = tf.Variable(2.)
b = tf.Variable(3.)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer()) #变量初始化
print(sess.run(a*b))
[out]:6.
[1] Platt, John. "Sequential minimal optimization: A fast algorithm for training support vector machines." (1998).
(3)initializer
由于tensorflow构建的是静态的计算流图,在开启会话之前,所有的操作都不会被执行。因此为了执行在计算图中所构建的赋值初始化计算节点,需要在开启会话之后,在会话环境下运行初始化。如果计算图中定义了变量,而会话环境下为执行初始化命令,则程序报错,代码如下:
代码语言:javascript复制import tensorflow as tf
a = tf.Variable(2.)
b = tf.Variable(3.)
with tf.Session() as sess:
#sess.run(tf.global_variables_initializer()) #注释掉初始化命令
print(sess.run(a*b))
[Error]: Attempting to use uninitialized value Variable
2. 启动图
先了解session的概念,然后才能更好的理解图的启动。 图的每个运行实例都必须在一个session里,session为图的运行提供环境。Session的类型是tf.Session,在实例化session对象时我们需要给它传递一个图对象,如果不显示给出将使用默认的图。Session有一个graph属性,我们可以通过它获取session对应的图。
代码如下:
代码语言:javascript复制numOfBatch=5
datas=np.zeros([numOfBatch,2],np.float32)
labels=np.zeros([numOfBatch,2],np.float32)
sess=tf.Session(graph=g)
graph=sess.graph
sess.run([graph.get_operation_by_name("initer")])
dataHolder=graph.get_tensor_by_name("input_data:0")
labelHolder=graph.get_tensor_by_name("input_label:0")
train=graph.get_operation_by_name("train_step")
out=graph.get_tensor_by_name("output:0")
for i inrange(200):
result=sess.run([out,train],feed_dict={dataHolder:datas,labelHolder:labels})
if i0==0:
saver.save(sess,"./moules")
sess.close()
代码都比较简单,就不介绍了。不过要注意2点:1.别忘记运行初始化节点,2.别忘记close掉session对象以释放资源。
3. 给图输入数据并获取结果
代码:
代码语言:javascript复制for i inrange(200):
result=sess.run([out,train],feed_dict={dataHolder:datas,labelHolder:labels})
这里主要用到了session对象的run方法,它用来运行某个节点或tensor并获取对应的值。我们一般会一次传递一小部分数据进行mini-batch梯度下降来优化模型。
我们需要把我们需要运行的节点或tensor放入一个列表,然后作为第一个参数(不考虑self)传递给run方法,run方法会返回一个计算结果的列表,与我们传递的参数一一对应。
如果我们运行的节点依赖某个placeholder,那我们必须给这个placeholder指定值,怎么指定代码里面很清楚,给关键字参数feed_dict传递一个字典即可,字典里的元素的key是placeholder对象,value是我们指定的值。值的数据的类型必须和placeholder一致,包括shape。值本身的类型是numpy数组。
这里再解释一个细节,在定义placeholder时代码如下:
代码语言:javascript复制input_data=tf.placeholder(tf.float32,[None,2],"input_data")
input_label=tf.placeholder(tf.float32,[None,2],"input_label")
shape为[None,2],说明数据第一个维度是不确定的,然后TensorFlow会根据我们传递的数据动态推断第一个维度,这样我们就可以在运行时改变batch的大小。比如一个数据是2维,一次传递10个数据对应的tensor的shape就是[10,2]。可不可以把多个维度指定为None?理论上不可以!
12.1.6 如何基于TensorFlow搭建VGG16
介绍完关于tensorflow的基础知识,是时候来一波网络搭建实战了。虽然网上有很多相关教程,但我想从最标准的tensorflow代码和语法出发(而不是调用更高级的API,失去了原来的味道),向大家展示如何搭建其标准的VGG16网络架构。话不多说,上代码:
代码语言:javascript复制import numpy as np
import tensorflow as tf
def get_weight_variable(shape):
return tf.get_variable('weight', shape=shape, initializer=tf.truncated_normal_initializer(stddev=0.1))
def get_bias_variable(shape):
return tf.get_variable('bias', shape=shape, initializer=tf.constant_initializer(0))
def conv2d(x, w, padding = 'SAME', s=1):
x = tf.nn.conv2d(x, w, strides=[1, s, s, 1], padding = padding)
return x
def maxPoolLayer(x):
return tf.nn.max_pool(x, ksize = [1, 2, 2, 1],
strides = [1, 2, 2, 1], padding = 'SAME')
def conv2d_layer(x,in_chs, out_chs, ksize, layer_name):
with tf.variable_scope(layer_name):
w = get_weight_variable([ksize, ksize, in_chs, out_chs])
b = get_bias_variable([out_chs])
y = tf.nn.relu(tf.bias_add(conv2d(x,w,padding = 'SAME', s=1), b))
return y
def fc_layer(x,in_kernels, out_kernels, layer_name):
with tf.variable_scope(layer_name):
w = get_weight_variable([in_kernels,out_kernels])
b = get_bias_variable([out_kernels])
y = tf.nn.relu(tf.bias_add(tf.matmul(x,w),b))
return y
def VGG16(x):
conv1_1 = conv2d_layer(x,tf.get_shape(x).as_list()[-1], 64, 3, 'conv1_1')
conv1_2 = conv2d_layer(conv1_1,64, 64, 3, 'conv1_2')
pool_1 = maxPoolLayer(conv1_2)
conv2_1 = conv2d_layer(pool1,64, 128, 3, 'conv2_1')
conv2_2 = conv2d_layer(conv2_1,128, 128, 3, 'conv2_2')
pool2 = maxPoolLayer(conv2_2)
conv3_1 = conv2d_layer(pool2,128, 256, 3, 'conv3_1')
conv3_2 = conv2d_layer(conv3_1,256, 256, 3, 'conv3_2')
conv3_3 = conv2d_layer(conv3_2,256, 256, 3, 'conv3_3')
pool3 = maxPoolLayer(conv3_3)
conv4_1 = conv2d_layer(pool3,256, 512, 3, 'conv4_1')
conv4_2 = conv2d_layer(conv4_1,512, 512, 3, 'conv4_2')
conv4_3 = conv2d_layer(conv4_2,512, 512, 3, 'conv4_3')
pool4 = maxPoolLayer(conv4_3)
conv5_1 = conv2d_layer(pool4,512, 512, 3, 'conv5_1')
conv5_2 = conv2d_layer(conv5_1,512, 512, 3, 'conv5_2')
conv5_3 = conv2d_layer(conv5_1,512, 512, 3, 'conv5_3')
pool5 = maxPoolLayer(conv5_3)
pool5_flatten_dims = int(np.prod(pool5.get_shape().as_list()[1:]))
pool5_flatten = tf.reshape(pool5,[-1,pool5_flatten_dims])
fc_6 = fc_layer(pool5_flatten, pool5_flatten_dims, 4096, 'fc6')
fc_7 = fc_layer(fc_6, 4096, 4096, 'fc7')
fc_8 = fc_layer(fc_7, 4096, 10, 'fc8')
return fc_8