首页 > 代码库 > Theano中的Graph Structure
Theano中的Graph Structure
译自:http://deeplearning.net/software/theano/extending/graphstructures.html#graphstructures
理解Theano计算原理的关键
建议阅读时间:10分钟
如果不明白内在运行机制,Theano代码的调试工作并非易事。本章就简单介绍了Theano的内部工作机理。
编写Theano code的第一步便是用符号占位符(或符号变量)书写数学表达式。表达式中的操作符包括+,-,**,sum(),tanh()等。所有这些操作符都有内部的ops。一个op表示在某种类型输入上的某种计算产生某种类型的输出,可以理解为常见编程语言中的函数定义。
Theano将符号的数学运算表示为图(Graphs)。这些图是由连接点构成,连接点(interconnected nodes)包括:Apply Node, Variable Node, 以及 Op Node。Apply Node表示将一个op应用到某些variable上。很重要的一点是,能够区分用op表示的计算的定义及其用apply表示的在实际数据上的应用。数据类型用Type的实例表示。以下是一段代码及其对应的图来展示这段代码的结构。从而可以更好的将这些细节联系在一起。
箭头表示对所指向的Python对象的引用。蓝色矩形框表示Apply /node,红色圆角矩形表示Variable Node,绿色圈表示Ops,紫色框表示类型。我们创建Variables,然后应用Apply ops到这些变量,从而得到更多Variables,我们构建二部有向无环图。Variables指向Apply Nodes,这表示函数应用,即通过变量的owner域来产生这些变量。这些Apply Node通过他们的input和output域,轮流指向他们的输入和输出Variables。Apply实例也包含outputs的引用列表,但是本文中的这幅图并没有考虑。
由于x与y两个输入的符号变量并不是其他运算的结果,所以他们的owner域都指向的是None。如果有些符号变量是其他一些运算的结果,则它的ownerfield应该像z一样指向的是另外一个蓝色框。值得注意的是,Apply实例的output域指向的是z,z的owner域指向的是Apply实例,所以是一个双向箭头。
遍历图(Traversing the Graph)
使用owner域,从输出(计算的结果)开始遍历到输入。以下面的code为例来说明:
若输入:type(y.owner),得到结果:<class ‘theano.gof.graph.Apply‘>,也就是说它是Apply Node,连接了op变量和输入来得到这个output。我们可以打印这个用于得到y的op的名称:
从而可知,用elementwise multiplication运算得到了y。这种Multiplication是两个输入之间的:
值得注意的是,第二个输入并不是我们所想象的2。这是因为2首先被broadcasted成为一个和x同样大小的矩阵,然后再做multiplication的运算。这种变化使用的是DimShuffle这种op:
从这个图结构,我们可以很容易理解,这种自动微分是如何进行的,以及符号关系如何被优化以获得更稳定的性能。
图结构(Graph Structure)
本节罗列了Theano内部计算图中常用的结构类型,包括:Apply, Constant, Op, Variable以及Type。
Apply. Apply Node是Theano计算图中的一种内部节点。与Variable Node不同,Apply Node通常并不能直接被终端用户所操作,可以通过Variable的owner域被访问。一个Apply Node是Apply类的实例。它表示了一个Op应用再一个或多个input上,其中每个input都是一个Variable。通常来讲,每个Op都负责知道如何从一个input Varible的列表获得Apply Node。因此,一个Apply Node可以通过调用Op.make_node(*inputs),从一个Op和一个input Variable的列表获得。与Python语言相比,一个Apply Node是一个Theano版本的函数调用,而Op则是Theano版本的函数定义。一个Apply实例包括以下三个重要的域:(1)op:Op决定了使用哪种函数或变形。(2)inputs:Variable列表,表示函数的参数。(3)outputs:Variable列表,表示函数的返回值。通过调用gof.Apply(op,inputs,outputs)来创建Apply实例。
Op.Theano中一个Op定义了在某些类型inputs上的某种类型的运算,从而产生某些类型的输出。这对应于大多数编程语言中的函数定义。从一个input Variable的列表以及一个Op,我们可以构建Apply node表示将该Op应用到这些inputs上。能够理解Op与Apply之间的区别是非常重要的:Op相当于一个函数的定义,而Apply相当于一个函数的调用和使用。如果你想用Theano的结构来解释Python语言,代码是这样的def f(x):...将产生一个名称为f的Op,而代码a=f(x)或g(f(x),5)将产生一个涉及到名为f的Op的Apply Node。
Type.Theano中的Type表示的是对可能的数据对象的约束集合。这些约束允许Theano来对C代码进行裁剪,以能够处理它们,并能对计算图进行优化。举例来说,theano.tensor包中的irow类型定义了下述对数据的约束,也就是说具有irow类型的Variable需要包含:(1)必须是一个numpy.ndarray类型的实例(即isinstance(x,numpy.ndarray));(2)必须是一个32位整数类型的array(str(x.dtype)==‘int32‘);(3)必须是1*N大小(len(x.shape)==2 and x.shape[0]==1)。知道了这些约束,Theano可以在产生例如加法这样的C代码的过程中声明正确的数据类型,包含维度上的正确循环次数。值得注意的是Theano的Type并不等同于Python的type或类。实际上,Theano中irow和dmatrix本质上都是numpy.ndarray类型,按该类型来做运算和存储数据,但是Theano的Type确实有些不同之处。例如,dmatrix的约束结合是:(1)必须是一个numpy.ndarray类型的实例(isinstance(x,numpy.ndarray));(2)必须是一个64位浮点数的array(str(x.dtype)==‘float64‘);(3)必须是大小为M*N的,对M或N没有限制(len(x.shape)==2)。这些约束是与irow的不同。有些情况theano Type可以完全对应到Python类型的情况,如这里的double对应Python的float。除非特别指明,我们说的Type都是指Theano Type。
Variable.Variable是Theano中主要的数据结构。我们操作的符号输入是Variables,我们使用各种各样的符号在这些inputs得到的结果也是Variables。举个例子来说明:import theano x=theano.tensor.ivector() y=-x。其中x与y都是Variables,也就是Variable类的实例。这里x与y的类型都是theano.tensor.ivector。不像x,y是经过计算得到的Variable。y是运算的输出Variable,x对应的是它的输入Variable。计算本身用另外一种Node,即Apply Node来表示,可以通过y.owner来访问。更具体来讲,一个Variable是Theano中的一个基本结构,它表示的是在计算过程中的某个点。通常来讲,它是Variable类的或它的子类的实例。一个Variable r包含以下四个重要的域:(1)type: 运算中Variable所采用的值的类型;(2)owner:要么是None要么是Apply Node(当Variable是output时);(3)index:r在owner.outputs中的索引owner.outputs[index]=r,若owner=None即可忽略;(4)name: 在pretty-printing以及debugging中使用的字符串。Variable有一个特殊的子类:Constant。
Constant.Constant是一个具有额外域的Variable,该域为data,仅有一次设置机会。当在计算图(Computation Graph)中用作Op的input时,假设该input总会取data域中的值。进一步讲,假设Op在任何情况下都不会改变这种输入。这就意味着Constant适合于好多优化:C代码中的constant内联,constant folding等。一个Constant不必要通过function函数来指定一个输入Variable的列表。如果这样做的话会带来异常。
图结构扩展(Graph Structures Extension)
当我们开始编译Theano函数时,我们也计算一些额外的信息。本节描述了一些可用信息的一部分。图在编译之初被cloned,所以在编译过程中任何改动并不会影响图结构。每个变量接受一个称为clients的新域。这个域是对图中每个使用该Variable的位置的引用列表。若该长度为0,也就意味着该Variable没有被使用。该变量被使用的每个位置用一个二元组表示。有两种类型的二元组:(1)第一个元素为Apply Node,第二个元素为index:var.clients[*][0].inputs[index]或fgraph.outputs[index]就是这个变量;(2)第一个元素为字符串Output,意思是函数的输出为该变量,第二个元素为index:var.clients[*][0].inpus[index]或fgraph.outputs[index]就是这个变量。
自动微分(Automatic Differentiation)
一旦有了图结构,自动计算微分变得容易。唯一要做的事情就是调用tensor.grad(),该方法从输出开始回溯到输入,通过所有的Apply Nodes来遍历图,apply nodes是那些定义了图中要做哪些运算的节点。对于每个这样的Apply Node,它的op定义了如何计算该node的输出对输入的梯度。值得注意的是若一个op并没有提供这种信息,那么认为梯度没有定义。为了得到整个图的输出对输入的梯度,需要使用链式法则对这些梯度进行组合。该tutorial的下一章会对微分做详细介绍。
优化(Optimization)
当编译一个Theano function时,我们需要给theano.function提供的一个图结构(从输出Variable,我们可以遍历整个图,到达输入Variable。)。整个图结构不但展示了如何从输入计算得到输出,而且给我们提供了对计算进行改进的可能。theano的优化方式时识别并用其它以更快的速度或更稳定的产生相同结果的pattern替换其中的一些pattern。优化也能探测相似的子图,从而确保同一值不被计算两次,或者重构部分图以适应到GPU版本。例如,Theano中一个简单的优化时用x来代替pattern xy/y。更详尽的介绍见文档。
Theano中的Graph Structure