翻译:穆文&韩小阳 校对:寒小阳 & 龙心尘
导 读
之前的课程里介绍了自然语言处理当中的一些问题,以及设计出来的一些相应的算法。research的东西还是落地到工程应用上比较有价值,之前也手撸过一些toy project,不过这些实现要用在工程中,总是有那么些虚的,毕竟稳定性和效率未必能够保证。所幸的是,深度学习热度持续升温的大环境下,各种大神和各家大厂也陆续造福民众,开源了一些深度学习框架,在这些开源框架的基础上去搭建和实现自己想要的深度学习网络结构就简单和稳定得多了。
有时候选择多了也是麻烦,对框架感兴趣的同学可以查看深度学习框架对比维基百科中对12个开源的package比对。这里简单提几个最常见和可能会用到的深度学习开源框架的特点。
caffe提供了很便捷的神经网络搭建和命令行工具,加之model zoo里面大量预训练好的模型(主要是图像相关的)可以做fine-tuning,因此使用在图像相关的研究和应用上非常方便。
Theano以及搭建于其之上的Keras和Lasagne似乎颇受research派系同学的偏爱,自动求导是它的优势之一。
MXnet对显存的利用率高,并且支持C++, Python, Julia, Matlab, JavaScript, Go, R, Scala这么多种语言,编写起来也比较简易。
Torch是facebook用的深度学习package,定义新网络层比较简单,不过Lua倒不算大家熟知的编程语言。
Tensorflow是Google提供资金研发的,比较全,支持分布式,同时有Google这样的亲爹在,我猜资源倾斜也是迟早的事情。
今天的重点自然是Tensorflow,其他的框架也都很好,大家可以自行尝试。
1.Tensorflow
首先提提Tensorflow和theano,它俩都是python封装的深度学习库,非常容易上手,说起来Tensorflow还是受Theano启发,借鉴了一部分它的思想。不同之处在于,Tensorflow 对分布式系统支持更好,同时还是Google提供资金研发的,而Theano 是一个学术性质的项目。
Tensorflow 可以对定义在张量(tensors,你可以先简单理解成标量、向量或者矩阵,一会儿会提到)上的函数自动求导,因此神经网络中BP算法可以很轻松地实现。
在开始Tensorflow之前,需要先让大家对Tensorflow的过程有个直观的理解。
在Tensorflow里:
严格意义上来说TensorFlow算是一个编程系统,它使用图来表示计算任务,图中的节点被称之为operation(可以缩写成op),一个节点获得0个或者多个张量(tensor,下文会介绍到),执行计算,产生0个或多个张量。TensorFlow的一个图描述了一个计算过程,为了进行计算,图必须在会话(Session)里被启动,会话(Session)将图的op分发到CPU或GPU之类的设备上,同时提供执行op的方法,这些方法执行后,将产生的张量(tensor)返回。返回的张量因语言不同而有不同,在python里是numpy ndarry对象;在C/C++语言中,是tensorflow::Tensor实例。
下面咱们来详细说说上面提到的概念。
1.1 什么是张量
既然Tensorflow里面的定义和运算都是基于张量这个概念,我们就先来看看,什么是张量。
1.2 Tensorflow 与 Numpy
# numpy定义与操作In [23]: import numpy as npIn [24]: a = np.zeros((2,2)); b = np.ones((2,2))In [25]: np.sum(b, axis=1)Out[25]: array([ 2., 2.])In [26]: a.shapeOut[26]: (2, 2)In [27]: np.reshape(a, (1,4))Out[27]: array([[ 0., 0., 0., 0.]])
# 对应的Tensorflow定义与操作In [31]: import tensorflow as tfIn [32]: tf.InteractiveSession()In [33]: a = tf.zeros((2,2)); b = tf.ones((2,2))In [34]: tf.reduce_sum(b, reduction_indices=1).eval()Out[34]: array([ 2., 2.], dtype=float32)In [35]: a.get_shape()Out[35]: TensorShape([Dimension(2), Dimension(2)])In [36]: tf.reshape(a, (1, 4)).eval()Out[36]: array([[ 0., 0., 0., 0.]], dtype=float32)
以上代码中提到的 session和.eval()将在下文细述,而关于TensorShape,大家可以简单理解成类似Python中tuple的类型。
为了方便记忆,我们把numpy和Tensorflow中的部分定义和操作做成了一张一一对应的表格,方便大家查看。
Numpy | Tensorflow |
---|---|
a = np.zeros((2,2)); b = np.ones((2,2)) | a = tf.zeros((2,2)), b = tf.ones((2,2)) |
np.sum(b, axis=1) | tf.reduce_sum(a,reduction_indices=[1]) |
a.shape | a.get_shape() |
np.reshape(a, (1,4)) | tf.reshape(a, (1,4)) |
b*5+1 | b*5+1 |
np.dot(a,b) | tf.matmul(a, b) |
a[0,0], a[:,0], a[0,:] | a[0,0], a[:,0], a[0,:] |
Tensorflow的输出要稍微注意一下,我们需要显式地输出(evaluation,也就是说借助eval()函数)!
In [37]: a = np.zeros((2,2))In [38]: ta = tf.zeros((2,2))In [39]: print(a)[[ 0. 0.] [ 0. 0.]]In [40]: print(ta)Tensor("zeros_1:0", shape=(2, 2), dtype=float32)In [41]: print(ta.eval())[[ 0. 0.][ 0. 0.]]
上面是一个示例的代码,大家可以理解Tensorflow是通过计算图(computation graph)定义一个计算过程的,这个过程不产生数值结果,那想看到具体内容怎么办呢?我们要借助.eval()函数输出。
用Tensorflow编写的程序一般由两部分构成,一是构造部分,包含了计算流图,二是执行部分,通过session 来执行图中的计算,具体可以参考Tensorflow文档。
我们先来看看怎么构建图。构件图的第一步是创建源节点(source op)。源节点不需要任何输入,它的输出传递给其它节点(op)做运算。python库中,节点构造器的返回值即当前节点的输出,这些返回值可以传递给其它节点(op)作为输入。
TensorFlow Python库中有一个默认图(default graph),在默认图的基础上,节点构造器(op 构造器)可以为其增加节点。这个默认图对许多程序来说已经足够用了,更多管理视图的细节可以阅读官方Graph类文档。
我们来看一个简单的构建图例子:
import tensorflow as tf# 创建一个常量节点, 产生一个1x2矩阵,这个op被作为一个节点# 加到默认视图中# 构造器的返回值代表该常量节点的返回值matrix1 = tr.constant([[3., 3.]])# 创建另一个常量节点, 产生一个2x1的矩阵matrix2 = tr.constant([[2.], [2.]])# 创建一个矩阵乘法matmul节点,把matrix1和matrix2作为输入:product = tf.matmul(matrix1, matrix2)
上面代码里的默认图现在有三个节点,两个constant()节点和matmul() 节点。不过这仅仅是构建图,为了真正进行矩阵的乘法,你必须在会话(Session,马上提到)里启动这个图。
上面我们知道了Tensorflow需要先构造一个图用于计算,但是图怎么启动呢?启动图的第一步需要创建一个Session对象。比如:
# 创建session,启动默认图sess = tf.Session()# 调用sess的'run()' 方法来执行矩阵乘法节点操作,传入'product'作为该方法的参数。'product'代表了矩阵乘法节点的输出,传入它是告诉方法我们希望取回矩阵乘法节点的输出。#整个执行过程是自动化的,会话负责传递节点所需的全部输入。节点通常是并发执行的。# 函数调用'run(product)'会触发图中三个节点(上面例子里提到的两个常量节点和一个矩阵乘法节点)的执行。# 返回值'result'是一个numpy 'ndarray'对象。result = sess.run(product)print result# 结果为[[12.]]# 完成任务,记得关闭会话sess.close()
Session对象在使用完成后,记得关闭以释放资源,当然,除了显式调用close关闭外,也可以使用with代码来自动完成关闭动作:
# 用with代码来自动完成session里的图运算并关闭with tf.Session() as sess: result = sess.run([product]) print result
为了便于使用像IPython这样的python交互环境,可以使用InteractiveSession代替Session类,使用Tensor.eval()和Operation.run()方法代替Session.run()。这样做的好处是可以在ipython中保持默认session处于打开状态:
# 进入一个交互式Tensorflow会话import tensorflow as tfsess = tf.InteractiveSession()x = tf.Variable([1.0, 2.0])a = tf.constant([3.0, 3.0]);# 使用初始化器的run()方法初始化xx.initializer.run()# 增加一个减法节点,从x减去a。运行减法op,输出结果sud = tf.sub(x, a)print sub.eval()# 结果为[-2. -1.]
我们一直在说,Tensorflow是支持分布式的深度学习框架/包,这是因为它能将图定义转换成分布式执行的操作,以充分利用可以利用的计算资源(如CPU或GPU)。不过一般情况下,你不需要显式指定使用CPU还是GPU,Tensorflow能自动检测。如果检测到GPU,Tensorflow会优先使用找到的第一个GPU来执行操作。
如果机器上有超过一个可用的GPU,默认状况下除了第一个外的其他GPU是不参与计算的。为了让Tensorflow使用这些GPU,你必须将节点运算明确地指派给它们执行。其中with...Device语句用来指派特定的CPU或GPU操作:
# 手动指定给某个gpu执行with tf.Session() as sess: with tf.device("/gpu:1"): matrix1 = tf.constant([[3., 3.]]) matrix2 = tf.constant([[2.], [2.]]) product = tf.matmul(matrix1, matrix2)
指定设备的书写格式如下:
我们训练一个模型的时候,会用到Tensorflow中的变量(Variables),我们需要它来保持和更新参数值,和张量一样,变量也保存在内存缓冲区当中。
有很多同学会问,前面不是提到了一个概念叫做张量,为什么还需要这个新的变量呢?需要说明一下的是,如果大家仔细看之前的代码,会发现我们所用到的张量都是常值张量(constant tensors),而非变量,而参数值是需要动态调整的内容。
比如下面的代码里我们设定了一组权重为变量:
In [32]: W1 = tf.ones((2,2))In [33]: W2 = tf.Variable(tf.zeros((2,2)), name="weights")In [34]: with tf.Session() as sess: print(sess.run(W1)) sess.run(tf.initialize_all_variables()) print(sess.run(W2)) ....:[[ 1. 1.] [ 1. 1.]][[ 0. 0.][ 0. 0.]]
说一个小细节,注意到上面第34步tf.initialize_all_variables,我们要预先对变量初始化(initialization) Tensorflow 的变量必须先初始化然后才有值!而常值张量是不需要的
再具体一点,比如下面的代码,其实38和39步,我们初始化定义初值是可以通过常数或者随机数等任何一种方式初始化的,但是直到第40步才真正通过Tensorflow的initialize_all_variables对这些变量赋初值。
In [38]: W = tf.Variable(tf.zeros((2,2)), name="weights")In [39]: R = tf.Variable(tf.random_normal((2,2)), name="random_weights")In [40]: with tf.Session() as sess: ....: sess.run(tf.initialize_all_variables()) ....: print(sess.run(W)) ....: print(sess.run(R)) ....:
比如我们来看一个计算图中变量的状态更新过程,代码如下:
In [63]: state = tf.Variable(0, name="counter")In [64]: new_value = tf.add(state, tf.constant(1))In [65]: update = tf.assign(state, new_value)In [66]: with tf.Session() as sess: sess.run(tf.initialize_all_variables()) print(sess.run(state)) for _ in range(3): sess.run(update) print(sess.run(state))0123
上面的代码定义了一个如下的计算图,同时其中变量的状态是循环变化的:*
如果想取回定义的计算图中的节点运算输出结果,可以在使用Session对象的run()调用执行图时,传入一些张量,这些张量可以帮助你取回结果。而且不仅仅是单个节点的状态或者结果,可以输出多个节点的结果,比如下面这个简单例子:
input1 = tf.constant(3.0)input2 = tf.constant(4.0)input3 = tf.constant(5.0)intermed = tf.add(input2, input3)mul = tf.mul(input1, intermed)with tf.Session() as sess: result = sess.run([mul, intermed]) print result# print# 输出最后的乘法结果,和之前的加法结果[27.0, 9.0]
1.8里我们提到了在计算图中引入张量,以获取节点状态或者输出结果。Tensorflow还提供了feed机制,该机制可以临时替代图中的任意操作中的张量,也就是说,可以对图中任何操作提交补丁,直接插入一个新的张量。
feed可以使用一个张量值临时替换某个操作的输出结果,你只需要提供feed数据作为run()调用的参数。需要说明的是,feed只在调用它的方法内有效,方法结束则feed就会消失。最常见的用例是将某些特殊的操作指定为feed操作,标记的方法是使用tf.placeholder()为这些操作创建占位符(可以先想成一个容器,这个在之后的内容里会提到,不要着急)。
input1 = tf.placeholder(tf.types.float32)input2 = tf.placeholder(tf.types.float32)output = tf.mul(input1, input2)# 手动提供feed数据作为run的参数with tf.Session() as see: print sess.run([output], feed_dict={input:[7.], input2:[2.]})# print# 结果是[array([ 14.], dtype=float32)]
这个部分呢,就先简单给大家介绍Tensorflow的一些常用对象,基本操作和设计思想。之后会针对常见的问题(回归,图像分类/CNN, 自然语言处理/RNN)逐个进行讲解。欢迎大家继续关注。