小小千想和您聊一聊

当前位置: 首页> 技术分享> 第2章 Theano基础

第2章 Theano基础

  本章学习目标

  l 掌握Theano方法;

  l 了解Theano的基本概念;

  l 掌握Theano的编程风格。

  “工欲善其事,必先利其器”,初学者除了需要掌握深度学习的理论知识外,还应熟练运用相关的工具库来创建深度学习模型或包装库。Theano是一个Python库,可以在CPU或GPU上进行快速数值计算,从而大大简化程序。本章将对Theano的相关基础知识进行讲解。

  2.1 初识Theano

  Theano是最早的深度学习开发工具之一,是在BSD许可证下发布的一个开源项目,它的开发始于2007,由Yoshua Bengio领导的加拿大蒙特利尔理工学院LISA集团(现MILA)开发,Theano的名字源于一位著名的希腊数学家,它是为深度学习中处理大型神经网络算法所需的计算而专门设计的,擅长处理多维数组库,与其他深度学习库结合使用时十分适合用于数据探索。Theano首次引入了“符号计算图”来描述模型表达式的开源结构,目前,这被看作深度学习研究和开发的行业标准。Theano可以被理解成数学表达式的编译器:用符号式语言定义需要的结果,该框架会对程序进行编译,从而高效运行于GPU或CPU。目前许多优秀的深度学习开源工具库对Theano的底层设计进行了借鉴,如Tensorflow、Keras等,因此,掌握Theano的使用方法可以作为学习其他类似开源工具的铺垫。

  Theano是一个基于Python和Numpy的数值计算工具包,它可以定义最优化以及估值高维度的数学表达式,也可以通过一系列代码优化从而获得更好的硬件性能。

  theano.tensor数据类型包含double、int、uchar、float等,最常用的类型是int和float,具体如下所示:

  数值:iscalar(int类型的变量)、fscalar(float类型的变量)

  一维向量:ivector(int 类型的向量)、fvector(float类型的向量)、

  二维矩阵:fmatrix(float类型矩阵)、imatrix(int类型的矩阵)

  三维float类型矩阵:ftensor3

  四维float类型矩阵:ftensor4

  Theano与Tensorflow 功能十分相似(前面提到,Tensorflow借鉴了Theano的底层设计),因而两者常常被放在一起比较,这两者都偏向底层。Theano 更像一个研究平台,并没有专门用于深度学习的相关接口。例如,Theano中没有现成的神经网络分级,因此,在实际应用中需要从最基本的网络层开始构建所需要的模型。

  经过多年发展,目前有大量基于Theano的开源深度学习库被开发并实际应用,例如 Keras,、Lasagne和Blocks。这些更高层级的wrapper API可以大幅减少开发时间以及开发过程中的麻烦。现在,大多数使用Theano的开发者都会借助辅助API进行开发,Theano已经逐渐形成一套生态系统,可以在使用Theano时借助其他API来降低其使用难度。

  在过去的很长一段时间内,Theano 是深度学习开发与研究的行业标准。由于Theano是由高校的研究团队研发,它的设计初衷是服务于学术研究,这导致深度学习领域的许多学者至今仍在使用 Theano。对于深度学习新手而言,使用Theano 练手对于深入理解模型的原理有很大的好处,但对于开发者而言,还是建议使用Tensorflow等更高效的深度学习工具。

  2.2 安装Theano

  Theano提供了主要的操作系统详细的安装说明:Windows、OS X和Linux。Theano需要一个Python2或Python3包含SciPy的工作环境,Anaconda中基本涵盖了运行Theano所需要的大部分工具包,可以用Anaconda在的机器上快速建立Python和SciPy,以及实用Docker图像。

  本书建议采用含有python3.6的Anaconda来安装Theano,版本为anaconda3-5.1.0-Windows-x86_64.exe,可以根据需要选择相应的版本,下载地址为:https://docs.anaconda.com/anaconda/packages/oldpkglists(如果下载失败可尝试从其他国内网站下载)。下载完成后双击安装包,打开如图2.1所示安装向导,单击【Next】按钮进入下一步骤。

  图2.1 打开安装包并单击【Next】

  直接单击图2.2中的【I Agree】进入下一步骤。

  图2.2 查看协议并单击【I Agree】

  按照如图2.3所示的默认勾选状态单击【Next】进入下一步骤。

  图2.3 选择为所有人进行安装

  本书选择在C盘进行文件的安装如图2.4所示,可根据自身情况选择合适的安装路径,点击【Next】进入下一步骤。

  图2.4 选择安装路径

  勾选如图2.5中的两个选项,单击【Next】开始安装Anaconda。

  图2.5 添加勾选图中第一项以自动配置安装环境

  Anaconda安装完成如图2.6所示。

  图2.6 Anaconda安装完成

  在Anaconda安装完成后会提示安装Microsoft VSCode,如果已经安装了Microsoft VSCode可以直接点击【Skip】跳过该步骤,否则点击【Install Microsoft VSCode】进行安装。

  图2.7 安装Microsoft VSCode

  然后进入如图2.8所示界面,可直接点击【Finish】按钮按成本次安装。

  图2.8 完成安装

  所安装的该版Anaconda缺少Theano所需要的必须的MinGW文件,可以在命令提示符里通过如下命令下载并安装MinGW。

  >>>conda install mingw libpython

  输入上述指令后中间会提示“Proceed?”,具体如图2.9所示,此时键入“y”,按下键盘的回车键,继续自动安装。

  图2.9 安装MinGW的中间环节

  安装完成后的结果如图2.10所示。

  图2.10 MinGW安装完成

  接下来打开cmd,输入以下命令。

  >>>conda install theano

  上述命令执行完成后,会显示“Successfully installed theano-1.0.2”。至此,Theano安装完成,接下配置Theano的环境变量。

  2.3 配置环境变量

  如果没有在安装时勾选自动配置环境变量,则需要手动进行配置环境变量。以本书为例,在系统环境变量的PATH里添加“D:\Anaconda-QF\MinGW\bin;D:\Anaconda-QF\MinGW\x86_64-w64-mingw32\lib;”。上述路径为本书安装路径,大家可以根据自己的实际安装目录进行修改。

  在配置环境变量时请注意使用英文标点分号进行分隔,避免输入错误的路径。

  在cmd的home目录中新建 .theanorc.txt 文件(作为Theano的配置文件,注意名字中的第一个“.”,如果已经存在,则直接修改该文件),设置如下内容:

  1 [blas]

  2 ldflags =-lblas

  3 [gcc]

  4 cxxflags = -ID:\Anaconda-QF\MinGW\x86_64-w64-mingw32\include

  所谓cmd的home目录是指:打开cmd时,在“>”前面的默认路径,经过上述步骤后,便可完成对Theano的环境变量配置。

  2.4 Theano中的符号变量

  使用编程语言进行编程时,需要用到各种变量来存储各种数据信息,Theano虽然是基于Python和Numpy实现的数值计算工具库,但其有自己独立的变量体系。Theano的变量类型被称为符号变量(TensorVariable),它是Theano表达式和运算操作的基本单元。Theano中所有符号变量来源于一个基类:Tensorvariable(),即这些符号变量都是这个类的实例化,而这些符号变量本身的数据类型,通过实例化给定,通过访问对象属性得到object.type,这些在Theano的tensor模块中。

  一般情况下,首先需要导入Theano,否则会返回异常。Theano目前支持7种变量类型:col、matrix、row、scalar、tensor3、tensor4、vector。接下来演示如何使用内置方法定义向量类型的变量,具体如下所示:

  >>>import theano

  >>>import theano.tensor as T

  >>>x=T.vector(name='变量名称',dtype=‘该实例化的符号变量的数据类型’)

  其中,vector()函数需要指定以下两个参数:

  l name:指定变量的名称。

  l dtype:指定变量的数据类型。目前Theano变量支持的数据类型有以下8种:int8、int16、int32、int64、float32、float64、complex64、complex128。

  在创建其他类型的变量时,将vector替换成对应的变量类型即可,比如通过将vector替换成matrix即可创建矩阵类型的变量:T.matrix。

  表2.1列出了常见的符号类型以及符号变量的数据类型。

  表2.1 常见的符号类型

  表2.1中第一栏里面是符号变量的类型,第二栏是符号变量的数据类型,最后一栏broadcastable属性的作用是表示不同shape的矩阵之间是否可以广播。

  上述使用Theano内置的变量定义方法,只适用于处理四维以下的变量,当需要处理更高维的数据时,需要采用自定义变量类型的方法进行定义,自定义变量的一般形式如下所示:

  >>>import theano

  >>>import theano.tensor as T

  >>>mytype=T.TensorType(dtype,broadcastable,name=None,sparse_grad=Fasle)

  使用上述TensorType函数进行自定义变量操作时,需要指定4个参数,其中dtype和broadcastable是必须指定的,也是最常用的参数。

  l name:指定变量的名称。

  l dtype:指定变量的数据类型。目前Theano变量支持的数据类型有以下8种:int8、int16、int32、int64、float32、float64、complex64、complex128。

  l broadcastable:是一个由True或Fasle值构成的布尔类型元组,元组的大小等于变量的维度大小,如果元组中的某一个值为True,则表示变量在对应的维度上的数据可以进行广播(broadcast),否则数据不能广播。

  接下来分别演示几种常见的变量定义方法。

  返回一个0维的numpy.ndarray:

  >>>theano.tensor.scalar(name=None, dtype=config.floatX)

  返回一个1维的numpy.ndarray:

  >>>theano.tensor.vector(name=None, dtype=config.floatX)

  返回一个2维的numpy.ndarray,但是行数保证是1:

  >>>theano.tensor.row(name=None, dtype=config.floatX)

  返回一个2维的numpy.ndarray,但是列数保证是1:

  >>>theano.tensor.col(name=None, dtype=config.floatX)

  返回一个2维的numpy.ndarray:

  >>>theano.tensor.matrix(name=None, dtype=config.floatX)

  返回一个3维的numpy.ndarray:

  >>>theano.tensor.tensor3(name=None, dtype=config.floatX)

  返回一个4维的numpy.ndarray:

  >>>theano.tensor.tensor4(name=None, dtype=config.floatX)

  如果想要创建一个非标准的类型的变量,就需要用到自定义的TensorType。这需要将dtype和broadcasting pattern传入声明函数中。

  创建一个五维向量的代码如下所示。

  dtensor5 = TensorType('float64', (False,)*5)

  x = dtensor5()

  z = dtensor5('z')

  可以通过以下代码对已存在的类型进行重构。

  my_dmatrix = TensorType('float64', (False,)*2)

  x = my_dmatrix() # 定义一个矩阵变量

  print my_dmatrix == dmatrix # 输出为“True”

  TensorType函数有一项重要的参数broadcastable,该参数对变量是否可以进行广播产生影响。广播机制使得不同维度的张量进行加法或者乘法运算成为可能,它可以让程序直接执行异构数据间的运算操作,避开异构数据间运算时维度转换的过程。例如,将一个向量数据与一个高维矩阵相加,如果没有广播的机制,则需要先将低维的数据转换成高维数据才能进行相应的操作符运算。通过广播机制,标量可以直接与矩阵相加,向量可以直接和矩阵相加,标量可以直接和向量相加,广播的运算机制如图2.11所示。


  图2.11 Broadcast的运算机制

  图2.11演示了广播一个行矩阵的过程,其中,T和F分别表示True和False,表示广播沿着哪个维度进行。如果第二个参数是向量,它的维度为(2,),广播模式为(False,)。它将会自动向左展开,匹配矩阵的维度,最终得到维度为(1,2)和Boradcastable为(True,Fale)。

  与numpy的广播机制不同,Theano需要知道哪些维度需要进行广播。当维度可以广播时,广播信息将会以变量的类型给出。

  下面的代码演示了在向量和矩阵的加法运算过程中,行和列是如何进行广播的:


 import theano
  import numpy
  import theano.tensor as T
  r = T.row()
  r.broadcastable
  # (True, False)
  mtr = T.matrix()
  mtr.broadcastable
  # (False, False)
  f_row = theano.function([r, mtr], [r + mtr])
  R = numpy.arange(3).reshape(1,3)
  # R
  # array([[0, 1, 2]])
  M = numpy.arange(9).reshape(3, 3)
  # M
  # array([[0, 1, 2],
  # [3, 4, 5],
  # [6, 7, 8]])
  f_row(R, M)
  # [array([ [ 0., 2., 4.],
  # [ 3., 5., 7.],
  # [ 6., 8., 10.]])]
  c = T.col()
  c.broadcastable
  # (False, True)
  f_col = theano.function([c, mtr], [c + mtr])
  C = numpy.arange(3).reshape(3, 1)
  # C
  # array([[0],
  # [1],
  # [2]])
  M = numpy.arange(9).reshape(3, 3)
  f_col(C, M)
  # [array([ [ 0., 1., 2.],
  # [ 4., 5., 6.],
  # [ 8., 9., 10.]])]
  接下来通过TensorType方法创建一个五维张量类型,将其broadcastable设置成(False,)*5。此时,通过自定义方法新使创建的变量在5个维度上都不再支持广播机制。
  import theano
  import theano.tensor as T
  mytype=T.TensorType('float32',(False,)*5)
  data=mytype('x')
  data.type()


  结果如下所示。



  可以从结果看出,已经成功修改了所创建变量的属性,使其在5个维度上不再支持广播。

  2.5 Theano编程风格

  刚接触Theano时,可能不太适应它的编程风格,这与之前所接触到其他编程方式存在差异。例如,在C++或者Java等语言中,一般先为自变量赋值,然后再把这个自变量作为函数的输入,进行计算因变量,比如要计算“m的n次方”的时候,一般写成如下形式:

  int x = m;

  int y = power(m,n);

  然而在Theano中,一般是先声明自变量(此时不需要为变量赋值),然后编写函数方程,最后再为自变量赋值,计算出函数的输出值,在Theano中一般通过如下表达式来计算“2的2次方”:

  import theano

  x=theano.tensor.iscalar('x') #声明一个int类型的变量x

  y=theano.tensor.pow(x,2) #定义y等于x的平方

  f=theano.function([x],y) #定义函数的自变量为x(输入),因变量为y(输出)

  print(f(2)) #打印出当x=2时,函数f(x)的值

  输出如下所示。

  4

  为了更好的理解Theano的编程风格,接下来通过一个函数的实现来讲解。函数的表达式如下所示。

  通过Theano实现s函数的代码如下:

  import theano

  x = theano.tensor.fscalar('x') #定义浮点型变量x

  y = 1 / (1 + theano.tensor.exp(-x)) #定义变量y

  f = theano.function([x],y) #定义函数f,输入为x,输出为y

  print(f(2)) #打印出当x=2的时候,y的值

  输出如下所示。

  0.46831053

  2.6 Theano中的函数

  函数是Theano的核心设计模块之一,提供了把符号计算图编译为可调用的函数对象的接口,本节将对其中较为常用的函数进行讲解。

  2.6.1 函数的定义

  事实上,在2.6节的内容中已经引入了Theano中一个非常重要的函数:theano.function,该函数主要用于定义一个函数的自变量和返回值(因变量)。

  函数的语法格式如下所示:

  function(inputs, outputs, mode=None, updates=None, givens=None, no_default_u

  pdates=False, accept_inplace=False, name=None,rebuild_

  strict=True, allow_input_downcast=None, profile=None,

  on_unused_input='raise')

  可以看出函数具有很多参数,但通常只会用到inputs、outputs、updates这3个参数,分别表示函数的自变量、函数的返回值(因变量)、共享变量参数更新策略。

  (1)inputs:用于指定函数的自变量列表。python以列表的形式来表示,列表的每一个元素都是一个In类型,In类型的函数有很多参数设置,详细的参数定义建议参考Theano的官方文档,本书将仅对其中较为常用的两个参数进行讲解:

  l variable:指定符号变量。

  l value:指定变量的默认值。

  (2)outputs:指定函数的返回值列表。outputs的值如果为空,则说明没有输出结果;也可以是一个值或者以列表的形式表示多个返回值。如果outputs的值不为空,则每一个返回值都是一个Out类,Out类的构造函数相对简单,一般只需要指定返回的符号变量即可。

  (3)updates:共享变量参数更新策略。通常以字典或元组列表的形式来指定。updates应用最广泛的就是在最优化计算过程中,指定每一次迭代时参数的更新策略。通过updates来对梯度下降算法中的权重参数进行迭代更新。

  当函数同时存在多个自变量和对应的因变量时定义格式如下:

  import theano

  x, y = theano.tensor.fscalars('x', 'y')

  z1= x + y

  z2=x*y

  f =theano.function([x,y],[z1,z2]) #定义x、y为自变量,z1、z2为函数返回值(因变量)

  print(f(2,3)) #返回当x=2,y=3的时候,函数f的因变量z1,z2的值

  输出如下所示

  [array(5., dtype=float32), array(6., dtype=float32)]

  2.6.2 函数的复制

  在实际应用中可能会遇到多个函数之间功能相似,但是参数不同的情况,这是就可以用到Theano中的函数复制功能。例如,用同一个算法对不同的模型进行训练,不同的模型之间采用的训练参数是不同的,这时可以通过函数复制功能将一个函数复制给另一个函数。这两个函数之间具有独立的计算图结构,相互之间并不会有影响。在Theano中,通过copy函数实现函数的复制。

  以累加器为例,下面的函数通过定义一个共享变量state(2.6.3节将会详细介绍共享变量)来累加变量,每一次调用函数accumulator时,state的值都会发生变化。

  import theano

  import theano.tensor as T

  state = theano.shared(0)

  inc = T.iscalar('inc')

  accumulator=theano.function([inc],state,updates=[(state,state+inc)])

  接下来,通过调用accumulator函数来查看输出结果。

  >>>accumulator(10)

  array(0)

  >>>state.get_value()

  array(10)

  新建另一个函数new_accumulator,它实现的功能与accumulator函数完全相同,但累加的变量不同。new_accumulator是定义在new_state上的累加函数,通过copy函数来实现这个功能,通过swap参数来交换两个共享变量。

  >>>new_state = theano.shared(0)

  >>>new_accumulator = accumulator.copy(swap ={state:new_state})

  验证结果如下。

  >>>new_accumulator(100)

  array(0)

  >>>new_state.get_value()

  array(100)

  >>> state.get_value()

  array(10)

  从上面的运行结果可以看出,new_accumulator函数没有对原来的state进行修改。如果只想在原来函数的基础上去除共享变量的更新,同样可以通过copy函数来实现这个功能,通过delete_updates参数来实现该功能。

  >>>null_accumulator = accumulator.copy(delete_updates=True)

  验证结果如下。

  >>>null_accumulator(9000)

  [array(10)]

  >>>state.get_value()

  array(10)

  从上述结果中可以看出,调用null_accumulato函数并没有影响state变量,实际上无论何时调用该函数都会输出同样的结果。

  2.6.3 Theano中重要的函数

  1.求偏导数

  求偏导数的函数theano.grad(),比如之前章节中提到的S函数,当 的时候,对s函数求偏导数的代码如下所示:

  import theano

  x =theano.tensor.fscalar('x') #定义一个float类型的变量x

  y= 1 / (1 + theano.tensor.exp(-x)) #定义变量y

  dx=theano.grad(y,x) #偏导数函数

  f= theano.function([x],dx) #定义函数f,输入为x,输出为s函数的偏导数

  print(f(3)) #计算当x=3的时候,函数y的偏导数

  结果为0.04517666。

  2.共享变量

  共享变量是指各线程公共拥有的变量,这个是为了多线程高效计算、访问而使用的变量。因为深度学习中,整个计算过程基本上是多线程计算的,于是就需要用到共享变量。在程序中,一般把神经网络的参数W(权重)、b(偏差值)等定义为共享变量,因为网络的参数,基本上是每个线程都需要访问。共享变量的定义格式如下所示。

  import theano

  import numpy

  A = numpy.random.randn(3,4) #随机生成一个矩阵A

  x = theano.shared(A) #创建共享变量x

  print(x.get_value())

  通过get_value()可以查看共享变量的数值,通过set_value()可以设置共享变量的数值。

  3.共享变量参数更新

  参数“updates”在theano.function函数中具有非常重要的作用,它是一个包含两个元素的列表或元祖,一般表示形式为updates=[old_w,new_w]。当函数被调用的时候,会将old_w替换成new_w,具体示例如下所示。

  import theano

  w = theano.shared(1) #定义一个初始值为1的共享变量w

  x = theano.tensor.iscalar('x')

  #对函数自变量x,因变量w进行定义,并在函数执行完毕后更新参数w=w+x

  f = theano.function([x], w, updates=[[w, w+x]])

  print(f(3)) #函数输出结果为当x=3时,w的值

  print(w.get_value()) #获取更新后的w的值w=w+x=4

  输出结果如下所示。

  1

  4

  共享变量参数更新主要用于梯度下降算法中。

  2.7 Theano中的符号计算图模型

  前面章节中详细讲解了符号变量(tensorvariable)的定义方法,本节将对符号计算图的相关概念进行讲解。

  Theano处理符号表达式时是通过将符号表达式转换为计算图(graph)来处理的,因此,理解计算图的基本原理和底层的工作机制对于编写和调试Theano代码有着重要意义。

  符号计算图的节点有4种节点类型构成:variable节点(variable nodes,符号变量节点)、type节点(type nodes,类型节点)、apply节点(apply nodes,应用节点)和op节点(op nodes,操作符节点)。接下来将对这四种节点分别进行讲解。

  2.7.1 variable节点

  variable节点是符号表达式中存放信息的数据结构,是Theano使用中最常用到的数据结构,可以分为输入符号变量和输出符号变量。

  l 一个符号变量通常具有下面4个重要的域。

  l type:定义可以在计算中使用的变量,指向type节点(本节后续内容将进行讲解)。

  l owner:可以为None 或者一个变量的apply节点的一个输出(本节后续内容将进行讲解)。

  l index:一个索引值,当owner的值不为None时,如果变量是输入符号变量,则表示该变量在owner所指向的符号表达式中是第index个输入变量;当owner的值为None时,如果变量是输出符号变量,则表示该变量在owner所指向的符号表达式中是第index个输出。

  l name:为变量定义名称,方便用于打印或调试。

  例如,输入以下的命令定义一个符号表达式 :

  import theano

  x = theano.tensor.ivector('x')

  y = -x

  x和y都是变量,即变量类的实例。x和y的type都是 theano.tensor.ivector。y是输出符号变量,而x是输入符号变量。计算的自身是通过apply节点和y.owner来进行访问的。更具体的说,Theano中的每一个变量都是一个基本结构,用来表示在计算中某个具体的点上的基准。通常是符号变量类或者是它的一个子类的实例。

  符号变量中有一个特殊的子类:Constant(常量)。常量就是有着一个额外域data的符号变量,它的值初始化(initialize)后不能再改变。当在计算图中用作Opapplication的输入时,需要假设该输入总是常量的数据域部分,也就是说,需要假设op不会修改该输入。在一个 函数的输入列表中,常量是无需指定的。

  2.7.2 type节点

  type节点定义了一种具体的变量类型以及变量类型的数据类型时,Theano为其指定了数据存储的限制条件,以irow的约束为例,定义type节点的表达式如下所示。

  w = theano.tensor.irow('w')

  上述表达式中定义了一个变量w。代码“irow”中的“i”是“int32”的缩写,表示变量w的数据类型为int32;代码“irow”中的“row”代表变量w的变量类型为row。在Theano中,type用来表示潜在数据对象上的一组约束,这些约束允许Theano定制C代码来处理需要被约束的变量,并对计算图进行静态优化。例如上面的变量w被指定了以下限制条件:

  l 底层必须以numpy.ndarray作为数据结构。

  l 数据类型必须是int32,即变量w必须是一个int32的整数数组。

  l 变量的形态大小必须为(1,n),即第一维的大小必须为1。

  如果不满足上述约束,则会返回TypeError错误。在这些约束条件下,Theano可以生成额外的C代码,用来声明正确的数据类型和基于维度上进行准确次数的循环。

  Theano的type不等同于Python的type或者class。在Theano中,irow和dmatrix都是使用numpy.ndarray来作为潜在的类型进行计算和数据存储的,实际上,在Theano中这两者是不同的type。使用dmatrix时候的约束如下:

  l 底层必须以numpy.ndarray作为数据结构。

  l 数据类型必须是64位的浮点数数组。

  l 变量的形态大小必须为(m,n),在m或n上都没有限制。

  除非特殊声明,后续内容中提到的“type”特指Theano中的type。

  2.7.3 apply节点

  apply节点是内部节点的类型,在theano中表示某一种类型的符号操作符应用到具体的符号变量中。不同于变量节点,apply节点不需要直接被最终用户操作,它们可以通过变量的onwer域来访问,一个apply节点包括三个重要的域。

  l op:指向符号表达式使用函数或转换的位置。

  l inputs:表示函数的参数,即符号表达式的输入参数变量列表。

  l outputs:表示函数的返回值,即符号表达式的输出结果变量列表。

  应该用节点通常是apply类的一个实例。它表示op在一个或多个输入上的应用,这里每个输入都是一个变量。通常,每个op反应了如何从一个输入列表中构建一个apply节点。因此,apply节点可以在 op和输入列表的基础上,通过调用如下代码获得:

  op.make_node(*inputs)

  与Python语言相比,apply节点是Theano中的函数调用,op是Theano中的函数定义。

  2.7.4 op节点

  op节点是在某些类型的输入上定义一个具体的计算,并生成某些类型的输出。它等价于大部分编程语言中的函数定义,op定义了一个符号变量间的运算,以某种类型的符号变量作为输入,输出另一种符号变量,如+、-、sum()、tanh()等。

  理解op节点(函数的定义)和apply节点(函数的应用)之间的差别是十分重要的。通过以下示例来解释两者之间的差异。

  通过Python的语法来理解Theano的计算图结构,假设定义了一个函数 ,将会对该函数生成一个op节点。

  def f(x)

  如果在代码中调用了该函数,那么将生成一个apply节点,并且该节点的op域将指向f节点。

  a = f(x)

  2.7.5 符号计算图模型

  Theano是将符号数学化的计算表示成计算图。这些计算图是由将apply节点和variable节点连接而成,apply节点与函数的应用相连,variable节点与数据相连。具体操作由op实例表示,而数据类型是由type实例表示。接下来通过一个具体的示例来理解符号计算图的结构。

  import theano.tensor as T

  x = T.dmatrix('x')

  y = T.dmatrix('y')

  z = x + y

  上述代码的逻辑非常简单,可以看出,分别定义了两个矩阵变量x和y,定义符号表达式 。该符号表达式转化为对应的符号计算图如图2.12所示。

  图2.12 符号计算图

  图2.12中,箭头表示各节点对所指向的Python对象的引用。变量指向Apply节点的过程是用来表示函数通过对应的owner域来生成自身。这些Apply节点是通过它们的inputs和outputs域来得到它们的输入和输出变量的。例如,变量x和变量y的值不是来自其他计算的结果,因此这两个变量的owner域指向了None,说明这两个变量的值不是由某个Apply节点生成;变量z的owner域指向了图中的Apply节点,这说明该变量的值来自该Apply节点。

  图2.6中,Apply节点的输出指向z,而z的owner域也指回Apply节点的,通过符号变量z的owner域获取其Apply节点:

  >>>z.owner

  Elemwise(add,no_inplace)(x,y)

  通过Apply节点的inputs和outputs域来获取表达式的所有输入符号变量和所有输出符号变量:

  >>>z.owner.inputs

  [x,y]

  >>>z.owner.outputs

  [Elemwise{add,no_inplace}.0]

  上述示例中,表达式是两个矩阵相加,形式比较简单,但对于复杂的表达式或函数,要画出完整的符号计算图是非常困难的。因此,Theano支持把计算图打印到终端或打印到外部文件。打印符号计算图之前需要先对printing模块进行定义。

  有两种模式可以将计算图打印到终端:pp模式和debugprint模式。pp模式的输出结果简洁紧凑,类似于数学表达式;debug print模式的输出更加详细,但相对繁琐。以前文中提到的 为例,分别用pp模式和debugprint模式来查看z的结果,具体如下所示。

  >>>theano.printing.pprint(z)

  '(x+y)'

  >>>theano.printing.debugprint(z)

  Elemwise{add,no_inplace}[id A] ' '

  |x [id B]

  |y [id C]

  2.8 Theano中的条件表达式

  Theano虽然是基于Python的工具包,但它本身是属于符号语言,因此无法直接使用Python中的if语句。IfElse和Switch这两种操作都是基于符号变量建立约束条件。IfElse 将 boolean 作为条件,将两个变量作为输入;Switch将 tensor 作为条件,将两个变量作为输入。Switch 是一个逐元素的操作,这一特性使得它比IfElse更加通用。

  Switch 在两个输出变量上进行评估,而 IfElse 只对一个关于条件的变量进行评估。


 from theano import tensor as T
  from theano.ifelse import ifelse
  import theano, time, numpy
  m,n = T.scalars('m', 'n')
  x,y = T.matrices('x', 'y')
  z_switch = T.switch(T.lt(m, n), T.mean(x), T.mean(y))
  z_lazy = ifelse(T.lt(m, n), T.mean(x), T.mean(y))
  f_switch = theano.function([m, n, x, y],
  z_switch,mode=theano.Mode(linker='vm'))
  f_lazyifelse = theano.function([m, n, x, y],
  z_lazy,mode=theano.Mode(linker='vm'))
  val1 = 0.
  val2 = 1.
  big_mat1 = numpy.ones((10000, 1000))
  big_mat2 = numpy.ones((10000, 1000))
  n_times = 10
  tic = time.clock()
  for i in range(n_times):
  f_switch(val1, val2, big_mat1, big_mat2)
  print('time spent evaluating both values %f sec' % (time.clock() - tic))
  tic = time.clock()
  for i in range(n_times):
  f_lazyifelse(val1, val2, big_mat1, big_mat2)
  print('time spent evaluating one value %f sec' % (time.clock() - tic))


  运行结果如下所示。

  python ifelse_switch.py

  time spent evaluating both values 0.234337 sec

  time spent evaluating one value 0.134876 sec

  在这个例子中,IfElse 操作比Switch花费更少的时间,从结果中看,这次操作中IfElse节省了大约一半的时间。这是因为IfElse只计算了两个变量中的一个。

  只有在使用linker='vm' 或者linker='cvm'的情况下,Ifelse才会计算两个变量,计算时间与Switch相同。综上所述,IfElse 与Switch主要区别有以下两点:

  l IfElse的条件表达式condition只支持标量值,而Switch的条件表达式可以是任意形式的符号变量。在实际使用Theano的过程中,Switch的应用更为广泛。

  l IfElse的运算具有惰性,从上面的例子中可以看出,IfElse的执行过程采用了“短路”策略,只会执行其中一个分支,而Switch会执行全部的分支,当全部分支执行完成后才根据条件表达式condition的值返回执行结果。

  2.9 Theano中的循环

  循环是程序语言中重要的模块之一,Theano的循环操作使用scan模块来实现。scan模块类似于Python的for语句,接下来,本节将对Theano中的循环语句进行讲解(由于scan的参数众多,本书将只对几个相对重要的参数进行讲解,其他参数可以通过访问theano官方文档进一步了解)。

  2.9.1 scan循环的参数

  scan循环的参数有很多,本书仅对以下几个主要参数进行讲解:sequences,outputs_info,non_sequences,fn,n_steps,truncate_gradient,strict。

  1.sequences

  sequences是一个由Theano变量或字典构成的列表,他们的值将作为参数传递给函数fn。列表中的每一个元素都是一个序列,每一次迭代可以传递序列的一个元素或多个元素,具体示例代码如下所示。

  >>> theano.scan(……,

  sequences=[dict(input=sequence1,taps=[-1,-2]),

  sequences2,

  dict(input=sequence3,taps = 3)],

  ……)

  上述代码的sequences参数包含了以下三个参数:sequence1、sequence2、sequence3三个输入序列。

  l sequence1:通常以字典的形式表示,字典中可以包括input(输入序列)和taps(索引)两个key值。上述代码表示在第t次迭代时,sequence1传递给fn的参数有sequence1[t-1]和sequence1[t-2]。

  l sequence2:以普通的Theano变量形式传递,该参数等价于下列代码。

  dict(input = squence2,taps=[0])

  当忽略taps参数时,Theano会默认taps的值为0,因此,在第t次迭代时,sequence2传递给fn的参数为sequence2[t]。

  l sequence3:结合前两个参数的传递过程可以看出,在第t次迭代时,sequence3传递给fn的参数为sequence3[t+3]。

  2.outputs_info

  与sequences的表达相似,outputs_info也是一个由Theano变量或字典构成的列表,列表中的每一个元素是函数fn的输出结果的初始值,具体示例如下所示。

  >>>theano.scan(……,

  outputs_info = [ dict(initial = output1, taps = [-3,-5]),

  output2,

  dict(initial = output 3,taps = 3)]

  ……)

  上述代码的sequences参数包含了以下三个参数:output1,output2,output3

  l output1:以字典的形式进行表示。用字典形式表示outputs_info时,可以包括initial(定义初始值)和taps(索引)两个key值。表示在第t次迭代时,output1传递给fn的参数为output1[t-3]和output1[t-5]。

  l output2:以普通的Theano变量形式传递,该参数等价于下列代码。

  dict(initial = output2,taps=[-1])

  与前面提到的sequence2情况一样,在忽略taps的值时,系统会为taps自动添加默认值,但是要注意,这里的taps默认值为-1。表示在第t次迭代时,output2传递给fn的参数为output2[t-1]。

  l output3:结合前两个参数的传递过程可以看出,output3表示在第t次迭代时,传递给fn的参数为sequence3[t+3]。

  3.non_sequences

  该参数是一个不变量或常数值列表,与前两个参数不同,该参数在迭代过程中不可改变。在实际应用中,一般把该参数设置为模型的权重参数列表。

  4.fn

  该参数是一个函数,fn是scan最核心的组成部分,它定义了每一次循环的处理逻辑,可以返回sequences变量的更新updates。fn既可以用lambda匿名函数来表示,也可以通过自定义函数表示。fn对函数参数的定义顺序和函数输出有严格对应的要求,输入变量顺序为sequences中的变量,outputs_info的变量,non_sequences中的变量。

  5.n_steps

  用来指定scan的迭代次数。sequences与n_steps两个参数中至少存在一个,否则scan无法知道迭代的步数。

  6.truncate_gradient

  这是一个专为循环神经网络训练设计的参数。利用scan来实现BPTT时,truncate_gradient用于指定向前传播的步长值,当值为-1时,表示采用的是传统的BPTT算法;当值大于0时,表示向前执行步长达到truncate_gradient设定值时,会提前结束并返回。这种截断策略可以用于处理传统的BPTT中的梯度消失问题。

  7.strict

  当该参数的值为True时,必须保证所有用到的Theano共享变量都放置在non_sequences参数中。

  2.9.2 scan循环演示

  一般情况下,一个for循环可以表示成一个scan()操作,而且scan是与theano的循环联系最紧密的。使用scan而不是for循环的优势:

  l 迭代的次数是符号graph的一部分。

  l 最小化GPU的迁移(如果用到GPU的话)。

  l 通过连续的步骤计算梯度。

  l 比python中使用theano编译后的for循环稍微快一点。

  l 通过检测实际用到的内存的数量,来降低总的内存使用情况。

  接下来通过几个案例来帮助理解scan的使用方法。

  以逐元素计算为例,通过scan循环演示计算A的k次方。


  import theano
  import theano.tensor as T
  k = T.iscalar('k')
  A = T.vector('A')
  outputs, updates = theano.scan(lambda result, A : result * A,
  non_sequences = A,
  outputs_info=T.ones_like(A), n_steps = k)
  result = outputs [-1]
  fn_Ak = theano.function([A,k], result, updates=updates)
  print(fn_Ak(range(10), 2))


  运行结果如下所示。

  [ 0. 1. 4. 9. 16. 25. 36. 49. 64. 81.]

  上述程序中outputs_info初始化为于A大小相同的全1向量,匿名函数(lambda)的输入依次为outputs_info和non_sequences,分别对应于匿名函数的输入result和A。由于scan函数的输出结果会记录每次迭代fn的输出,result = outputs [-1]表示Theano只需要取最后一次迭代结果,Theano也会对此做相应的优化(不保存中间迭代结果)。

  2.10 Theano中的常用Debug技巧

  Theano是最老牌的深度学习库之一。它灵活的特点使其非常适合学术研究和快速实验,但是,与Tensoflow等商业框架相比,Theano的调试功能非常薄弱。其实Theano本身提供了很多辅助调试的手段,下面就介绍一些调试的技巧,让Theano调试不再那么困难。

  1.通过eval查看或调试表达式结果

  对于shared变量,可以通过value或者get_value来查看变量的值,但对于其他Tensorvariable,符号变量是无法查看对应的值的,这时可以调用eval函数来进行查看。 具体方法如下所示:

  >>>import theano

  >>>import numpy

  >>>a = theano.shared(value = numpy.array([[0,0,1],[1,0,0]]))

  >>>b = a.reshape(shape=(3,2))

  >>>b.eval()

  array([ [0,0],

  [1,1],

  [0,0]])

  除了语法错误,在编写项目时还会遇到许多逻辑错误,比如除0问题

  2.对出错位置定位

  Theano的神经网络在出错时,往往会提供一些出错信息。但是出错信息往往非常模糊,让人难以直接看出具体是哪一行代码出现了问题。这是因为Theano的计算图进行了一些优化,导致出错的时候难以与原始代码对应起来。此时可以通过关闭计算图的优化功能来避免这种问题的发生。THEANO_FLAGS的参数optimizer默认值是“fast_run”,代表最大程度的优化,正常使用中一般会保持该状态,但是如果想让调试信息更详细,就需要关闭一部分优化,将默认值修改为“fast_compile”或者关闭全部优化,将默认值修改为“None”。

  THEANO_FLAGS="device=gpu0,floatX=float32,optimizer=None" python test.py

  3.打印中间结果

  通常有Test Value和Print两种打印中间结果的方法,接下来将分别对这两种方法进行介绍。

  (1)Test Value

  Theano在0.4.0之后的版本中,加入了test values机制,其作用是在计算图编译之前,为symbolic设定一个test_value,这样Theano就可以将这些数据,代入到symbolic表达式的计算过程中,从而完成计算过程的验证,并可以打印出中间过程的运算结果。

  值得注意的是,如果需要使用test_value,需要对compute_test_value的标记进行设置,常见设置如下所示。

  off: 关闭。建议在调试完成后,关闭test_value以提高程序速度。

  ignore: test_value计算出错,不会报错。

  warn: test_value计算出错,进行警告。

  raise: test_value计算出错,会产出错误。

  pdb: test_value计算出错,会进入pdb调试。pdb是python自带的调试工具,功能非常强大,可以在pdb中单步查看各变量的值,甚至可以执行任意python代码,通过import pdb 可以在查看详细中间过程的同时避免过多的使用print。

  (2)Print

  通过 Print方法来输出中间结果,示例如下。


 import theano
  import numpy
  import theano.tensor as T
  x = theano.tensor.dvector('x')
  x_printed = theano.printing.Print('important value')(x)
  f = theano.function([x], x + 3)
  f_with_print = theano.function([x], x_printed +3)
  f([1, 2, 3]) #输出时不打印任何信息
  f_with_print([1, 2, 3]) #输出时打印文字信息及x对应的中间值
  输出如下所示。
  array ([4.,5.,6.])
  important value __str__=[1.,2.,3.]
  array ([4.,5.,6.])


  因为theano是基于计算图以拓扑顺序运行程序的,因此各变量在计算图中被调用执行的顺序,不一定和原代码的顺序一样,无法准确的控制打印出的变量顺序。

  想要知道更详细的关于程序在哪里、什么时候、怎样计算的,可以参考官网的相关内容,网址为:http://deeplearning.net/software/theano/tutorial/debug_faq.html#faq-monitormode。

  Print方法会严重拖慢模型的训练速度,应该尽量避免在用于训练的代码中加入Print方法。该方法可能会阻止一些计算图的优化,例如结果稳定性的优化等,如果程序出现Nan异常,可以考虑把Print去除。

  2.11 本章小结

  通过本章的学习后,大家需掌握Theano的安装及常用的基础语法。Theano库设计和功能非常庞大,本书受篇幅所限只是介绍了其中一小部分常用内容,为了更好的进行后续的学习,可以参考Theano 的官方文档或者相关网络社区等资源进行深入学习。

  2.12 习题

  1.填空题

  (1) Theano首次引入 来描述模型表达式的开源结构。

  (2) Theano有自己独立的变量体系,变量类型被称为 ,它是Theano表达式和运算操作的基本单元。

  (3) 在Theano中创建一个函数,一般是先声明 ,然后编写 ,最后再为 ________赋值。

  (4) 在Theano的函数参数中 一般用于指定函数的自变量列表, 用于指定函数的返回值列表, 用于指定神经网络共享变量参数更新策略。

  (5) 在Theano中,循环操作使用 模块来实现,该模块类似于Python的for语句。

  2.选择题

  (1) Theano目前支持的变量类型不包括过以下哪一种( )。

  A.col B.matrix

  C.tensor2 D.tensor3

  (2) 在Theano的scan循环中,n_steps用来指定scan的( )。

  A.输出结果的初始值 B.迭代次数

  C.向前传播的长度 D.循环的处理逻辑

  (3) apply节点中,op表示( )。

  A.指向符号表达式使用函数或转换的位置

  B.符号表达式的输入参数变量列表

  C.符号表达式的输出结果变量列表

  D.某一种类型的符号操作符应用到具体的符号变量的位置

  (4) pydotprint接口的两个参数中fct和outgile分别表示( )。

  A.待打印的函数,输出文件名 B.输出函数,待打印文件名

  C.待打印文件名,输出函数 D.输出文件名,待打印的函数

  (5) 在Theano的调试中,通过eval可以( )。

  A.验证神经网络计算过程 B.定位程序错误

  C.调试程序错误 D.查看或调试表达式结果

  3.思考题

  (1) 简述Theano中共享变量的意义。

  (2) 简述Theano中scan函数的作用并列举出至少四种该函数中的参数。

上一篇:HTML5工具初识之网页编辑器

下一篇:Python教程之Python网络爬虫入门

QQ技术交流群

千锋Python官方①群
790693323

加入群聊