DL3-Computational Graph & Backpropagation

DL3-Computational Graph & Backpropagation

这个系列文章是我对李宏毅教授深度学习课程的一个笔记。

今天要说的是怎么算gradient:我们都知道算gradient就是backpropagation,其实就是一个很有效率的计算gradient的一个方法,算出gradient就可以算gradient descent。这节课要用computational graph(很多开源工具都使用计算图的方式,比如tensorflow(他的计算图的定义方式和课程中的不太一样)...)来算gradient descent。

那什么是computational graph?

所谓的computational graph意思就是说这个graph他是一种语言,这个语言是用来描述一个function用的,我们知道neural network就是一个function,所以我们需要描述function的语言。computational graph就是一种语言,你可以看着他的graph以后,一眼就可以看出来我们要描述的function长什么样,其实graph有很多种定义方法,但是我们通常使用node来表示一个变量,他可以是一个scalar,vector甚至是tensor,这里的每一个edge代表了一个operation也就是一个简单的function。

computational graph

看图上的例子 y = f(g(h(x))) 这个函数拆解成简单的形式 u = h(x) , v = g(u) , y = f(v) ,这里的x是一个variable,把x带到h这个function里面去就得到了u这个output,箭头就代表了h这个function。接下来把u丢到g这个function里面得到v,把v丢到f这个function中就得到output y。有时候一个function他可以有不止一个input,如 a = f(b,c) ,也就是画两个箭头就行了都指向一个输出a节点就行了。

那computational graph要怎么去画呢?

这个graph的好处就是,我们在这个graph上算variable的gradient是很容易的。

在去根据这个graph算variable的gradient之前,我们需要先了解一下链式法则:

那我们还拿刚才那个为例子,加入链式法则:

把每一个箭头的偏微分都算出来。

如果我们现在想同时计算 \frac {\partial e}{\partial a}\frac {\partial e}{\partial b} 呢(a = 3,b = 2的情况下)?

如果同时算这两者的话,我们需要一个reverse mode(反向的做法),我们刚才看的都是正向的做法,从a走到e,从b走到e,但如果要同时就算 \frac {\partial e}{\partial a}\frac {\partial e}{\partial b} 我们就需要使用反向的做法,这样做的效率高,当然你是可以分两次算的,但是比较的慢,如果你反向算的话,你可以同时算出 \frac {\partial e}{\partial a}\frac {\partial e}{\partial b} 。怎么做呢?从e开始走,把数字1放在e的这个节点上,然后从这个节点上散播出去。

这样做的好处就是,如果今天是computational graph,你最终的function的output只有一个value,你又需要算大量的不同值的偏微分的时候,用reverse mode会比刚才的forward mode还要更有效率,而在做neural network的时候,我们需要的就是reverse mode。那为什么呢?因为我们要做neural network的时候,我们需要计算偏微分的对象其实是哪个cost function(loss function),我们要对cost function算偏微分,而cost function的output就是一个scalar,所以如果你把cost function用computational graph来描述的话,他只有一个output,那今天我们又要同时算这个network里面所有参数对cost function的偏微分,所以我们从root开始做reverse mode,对cost function算微分是比较有效率的。

那另外一个状况是有时候会有parameter sharing:

我们可能会有parameter sharing的状况,也就是同样的variable,他出现在这个graph数个地方。这个时候要怎么计算他的偏微分呢?这件事情在neural network里面是常常的发生。比如,在CNN里面就有share variable,其实RNN他其实也是share variable,他把同一个function在整个RNN上反复的使用,他也是share variable。

有share variable要怎么去做呢?举一个例子,假如有一个function y = xe^{x^{2}} ,这个式子如果我们用computational graph来描述的话,就是上面图上的样子。接下来计算 \frac {\partial y}{\partial x} ,我们要把graph上的edge上的微分都算出来,我们需要把node上的x看成是不同的x来看待,就假设这些x都是有下标的。那算出了edge上的所有偏微分,那 \frac {\partial y}{\partial x} 他的值是什么呢?我们把那三个x,当成是不同的variable来看,那实际上 \frac {\partial y}{\partial x} 是什么呢?我们这里算出了三个偏微分,它的值并不是一样的,那么实际上 \frac {\partial y}{\partial x} ,意思就是把那算个对x的偏微分都加起来。所以今天要考虑share variable的case,我们就要把同样的参数,先当做不一样的参数来看,然后算完以后再加起来。

那接下来就说一说neural network的东西,上面讲的都是general的东西,那如果有一个fully connected feedforward network,那用computational graph来算他的微分的话,我们需要怎么去做呢?我们先复习一下,一般的讲法:

一般我们说要算微分就用backpropagation,这个backpropagation里面分成两个阶段,一个是forward pass,一个是back pass。

那怎么去求error signal(误差信号)呢?

  1. 首先将原来的整个network进行逆转,那这个network的input就是y对c的偏导,这个network的input是一个vector,这个vector里面存放的是y对c的gradient;
  2. 把这个vector丢进一排的neural里面,这个一排的neural他们的activation function并不是sigmoid,而是一排OPA(放大器),放大的倍数就是原来没有逆转时候的network的那个neural的activation function的微分值,得到一组output,这个时候的output就是最后一层的error signal,也就是 \delta^{l}
  3. 然后在把这组output乘上一组weight,这一组weight是原来layer L的那组matrix weight的transpose(在forward的时候乘上的是matrix weight[Z = WX+b],在backward乘上的就是weight的transpose)。然后丢到下一组的opa里面。

那接下来用computational graph来微分,当然在计算微分之前,先来看看network的computational graph,那他长什么样子呢?

那接下来我们要算gradient descent,也就是微分,那我们算微分的对象并不是y,其实是cost function。

接下来,就是要计算参数对cost function c的偏微分,c是一个scalar,对所有的network来说,最后cost function的output value都是一个scalar。

先每一个edge上的微分都算出来,然后算出来以后就用reverse mode从c这个地方逆向的走回来,你就可以把所有的参数对c的偏微分都算出来。

接下来我们的问题就是我的a和z都是vector,那他是怎么去算微分的呢?

我们把vector对vector做微分其实就是jacobian matrix(雅可比行列式)。

我们现在就从c这个地方一路往回走:

我们算y对c的偏微分,c只有一维,而y是一个vector,求出的偏微分就是一个扁的长条状的东西,用jacobian matrix来表示的话,他的宽的长度和y的长度一致,他的长度和c的长度一样,也就是形成一个1*y dimension的vector。如果 i\ne r 的话,y和cost function没有关系。

在继续往回走:

因为 z^{2}是一个vector,y也是一个vector,那么z^{2}对y的偏微分所生成的jacobian matrix是一个方阵(因为他们两个vector的维度一定是相等的)。 如果 i\ne j ,他们之间没有关系就是0。如果今天用的不是sigmoid function使用的是softmax,那他还是diagonal matrix吗?他就不是了,因为在做softmax的时候,z的每一个dimension都会影响到y的每一个dimension,那就没有 i\ne j 微分 = 0的状况。

然后继续算下去,下面把biases拿了下去,就是为了简单:

然后我们算 W^{2}z^{2} 的jacobian matrix:

先假设 W^{2}是一个(m * n)维的matrix,那显然 z^{2} 就是m维, a^{1} 就是n维。我们可以把 W^{2}z^{2} 的偏微分看做是一个tensor(三维),我们没有这样的思维去运算,所以把 W^{2}当做 vector来看,应当做是二维的。

结果就是最左边的长度是结果的长度1,最后一个matrix宽是结果的element的数目
两个问题
  1. 好像用computational graph用revise mode算所有参数的偏微分,感觉只有back pass没有forward pass。右边传统的讲法有forward pass和back pass。那右边那个forward pass在哪里?你还是需要forward pass,你想想看,我们要得到这些matrix的真正的值,我们需要a是多少,需要知道节点的value是多少,所以你还是需要先算一个forward pass,把所有节点的值都算出来,你才能算出所有的edge上的偏微分。才能够把一路上的东西都算出来,所以你还是先需要forward pass才能够跑revise mode,所以和右边那个是一样的。
  2. 你想要检查一下左边和右边是不是一样的,右边哪里有transpose,左边没有transpose。为什么?我们算出z对c的偏微分以后还要和w对z的偏微分相乘。 \delta 是一个vector,a也是一个vector,你把这两个vector相乘以后,要得到一个matrix,这个matrix的size和weight matrix的哪个size是一样的,这个时候你需要对backward pass得到的 \delta 先做transpose,才能和左边的a乘在一起,所以做完transpose以后, \delta 里面的transpose就不见了,左边和右边算出的结果会是一样的。

前面说的是fully connected feedforward network,接下来要说Recurrent network,其实原理都是一样的:

  1. 画出computational graph;
  2. 把偏微分都算出来;
  3. 然后reverse mode跑一把。

先看一下Recurrent network function:

那如果我们把recurrent network转换为computational graph,他会长圣魔样子呢?

首先有一个input x与w相乘得到n(没有在式子中出现)。 这个只是一部分。

如果我们把recurrent network完整的画出来的话(简化):

这是一个时间点,在下一个时间点:

当RNN考虑的是total cost,所以你要把 c^{1}\ c^{2}\ c^{3} 统统的加起来得到c,这个c就是total cost。

接下来我们就需要把每一个edge上的偏微分都算出来:

我们c开始计算。

1,2,3这三个节点算的偏微分,他们其实是共用参数的,所以你需要把 \frac{\partial C}{\partial W^{h}} 都算出来,然后全部加起来,才是真正的 \frac{\partial C}{\partial W^{h}} 的偏微分。

编辑于 2018-03-30