深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]

  反向传播就是求梯度值,然后通过梯度下降的方式对损失函数进行迭代优化的过程。在通常情况下,直接对一个复杂的函数一步到位写出其解析导数(关于数值导数和解析导数的求法可以查看这篇博客)是不太现实的,因此,一般需要再结合链式法则(chain rule)来进行计算。下面主要讲解链式法则的应用。

1. A simple example

  现在,假设你已经具备了求解偏导数的基本知识。首先,先看一个简单的示例:f(x,y,z)=(x+y)z。这个表达式很简单,可以直接进行微分,但是我们将采用链式法则来处理它,这将有助于理解反向传播背后的逻辑。要注意的是,这个表达式可以分解为两个表达式:q=x+y和f=qz。此外,我们知道如何分别计算这两个表达式的导数。f是q和z的乘积,所以∂f/∂q=z,∂f/∂z=q,q是x和y的加,所以∂q/∂x=1,∂q/∂y=1。但是,我们不关心中间值q上的梯度∂f/∂q,所以它的值没有用处。相反,我们最终感兴趣的是f相对于其输入x,y,z的梯度。链式法则告诉我们,将这些梯度表达式“chain”在一起的正确方法是乘法。例如,∂f/∂x=(∂f/∂q)·(∂q/∂x)。实际上,这只是两个梯度之间的乘法。我们用一个代码示例来说明这一点:

# set some inputs
x = -2; y = 5; z = -4

# perform the forward pass
q = x + y # q becomes 3
f = q * z # f becomes -12

# perform the backward pass (backpropagation) in reverse order:
# first backprop through f = q * z
dfdz = q # df/dz = q, so gradient on z becomes 3
dfdq = z # df/dq = z, so gradient on q becomes -4
# now backprop through q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. And the multiplication here is the chain rule!
dfdy = 1.0 * dfdq # dq/dy = 1

  我们只需要获得变量[dfdx,dfdy,dfdz]的梯度,它告诉我们变量x,y,z对函数f的灵敏度。这是最简单的反向传播示例。接下来,我们将使用一个更简洁的符号来省略df前缀。例如,我们将只写dq而不是df/dq,并且总是假设梯度是在最终的输出上计算的。
这种计算也可以通过线路图(circuit diagram)很好地可视化:
深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]
  上图中,正向传播计算从输入到输出的值(以绿色显示)。然后,反向过程执行反向传播,反向传播从末端开始,递归地应用链式法则计算梯度(以红色显示)到circuit的输入端。梯度可以被认为是在circuit中反向流动。光看着这个图可能你还是会有点难以理解每个线的梯度值是怎么计算的,但是没关系,我将对(下次补上)下一个例子Sigmoid函数的circuit diagram的每个红色数字是怎么计算的进行解释说明。

2. Modularity: Sigmoid example

  首先,引出一个术语:gate。它指一个函数,也可以理解为上一个例子中的circuit diagram的每个节点。比如q=x+y是一个gate,f=qz也是一个gate。任何一类可微函数都可以作为一个gate,我们可以把多个gate组合成一个gate,或者在方便的时候把一个gate分解成多个gate。下面看例子:
深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]
  这个表达式描述了一个使用sigmoid激活函数的二维神经元(输入x和权重w)。但现在让我们简单地把它看作是从输入w,x到一个数的函数。该函数由多个gate组成,可以拆解为更简单的函数:
深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]
其中,函数fc、fa分别用常数c来转换输入,用常数a来缩放输入。这些是加法和乘法在技术上的特殊情况,但是我们在这里把它们作为(新的)一元gate来介绍,因为我们不需要常数c,a的梯度。整个circuit如下所示:
深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]
  上图中的输入是[x0,x1],神经元的(待学习)权重是[w0,w1,w2]。sigmoid神经元可以将输入缩放到0~1的范围内。
  不过,sigmoid函数σ(x)其实是可以直接写出解析导数的,而且形式非常漂亮,如下(具体实现这里就不再赘述,可以查看这篇博客):
深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]

3. Backprop in practice: Staged computation

  像上面的例子那样,将一个函数拆分成多个非常简单的一元函数然后用链式法则进行导数求解固然是可以的。但是,这样做效率很低,特别是在函数形式很复杂的时候。因此,在应用链式法则时,也要注意将具有良好导数形式的函数视为一个整体来进行求解(也就是能不拆那么细就不拆那么细),这样可以大大减少运算量。接下来,让我们看另一个例子,这个例子由sigmoid函数σ(x)组成:
深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]
  首先,先给出这个函数的前向传播代码:

x = 3 # example values
y = -4

# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # sigmoid in numerator   #(1)
num = x + sigy # numerator                               #(2)
sigx = 1.0 / (1 + math.exp(-x)) # sigmoid in denominator #(3)
xpy = x + y                                              #(4)
xpysqr = xpy**2                                          #(5)
den = sigx + xpysqr # denominator                        #(6)
invden = 1.0 / den                                       #(7)
f = num * invden # done!                                 #(8)

  注意一下代码的构造方式:它包含多个中间变量,每个中间变量都只是简单的表达式,我们已经知道其局部梯度。因此,计算反向传播梯度的过程是很容易的:我们将向后走,对于前向过程中的每个变量(sigy,num,sigx,xpy,xpysqr,den,invden),我们将有相同的变量名,但是一个以d开头的变量,它将保持circuit输出相对于该变量的梯度。此外,请注意,反向传播中的每一个片段都将涉及到计算该表达式的局部梯度,并使用乘法将其与该表达式上的梯度链接起来。对于每一行,我们还强调它所指的前向传播的哪一部分。下面是反向传播代码:

# backprop f = num * invden
dnum = invden # gradient on numerator                             #(8)
dinvden = num                                                     #(8)
# backprop invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                                #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden                                                #(6)
dxpysqr = (1) * dden                                              #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# backprop xpy = x + y
dx = (1) * dxpy                                                   #(4)
dy = (1) * dxpy                                                   #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below  #(3)
# backprop num = x + sigy
dx += (1) * dnum                                                  #(2)
dsigy = (1) * dnum                                                #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy                                 #(1)
# done! phew

  在实现过程中,需要注意两个问题:

  1. 缓存前向传递的变量。为了计算反向传递,有一些在前向传递中使用的变量是非常有用的。所以,你缓存这些变量,并使它们在反向传播期间可用。如果不好缓存的话,那么在反向传播的时候重新计算它们也是可以的(但很浪费)。
  2. 梯度在分叉处增加。正向表达式多次涉及变量x,y,因此在执行反向传播时,必须小心使用+=而不是=来累积这些变量上的梯度(否则会覆盖它)。这遵循微积分中的多变量链规则,即如果一个变量分支到circuit的不同部分,那么流回到它的梯度将增加。

4. Summary

  在许多情况下,可以从直观的角度来解释反向传播梯度。例如,神经网络中最常用的三个gate(add、mul、max)都有非常简单的解释,即它们在反向传播过程中的行为。考虑以下示例circuit:
深度学习笔记(三)—— 反向传播[Back Propagation] & 计算图[Computational Graph]
  以上图为例,我们可以看到:

  • add gate总是取其输出上的梯度,并将其平均分配给所有输入,而不管它们在前向传递期间的值是什么。这是因为加法运算的局部梯度仅为+1.0,因此所有输入上的梯度将完全等于输出上的梯度,因为它将乘以1.0(并且保持不变)。在上面的示例circuit中,请注意add gate将2.00的梯度传播到它的两个输入,值相等且不变。
  • max gate路由梯度。与add gate将梯度不变地分布到其所有输入不同,max gate将梯度(不变)恰好分布到其一个输入(在前向传递期间具有最高值的输入)。这是因为max gate的局部梯度对于最高值为1.0,对于所有其他值为0.0。在上面的示例circuit中,max操作将梯度2.00路由到z变量,该变量的值高于w,并且w上的梯度保持为零。
  • multiply gate不太容易理解。它的局部梯度是输入值,在链式法则期间,它的局部梯度乘以其输出上的梯度。在上例中,x上的梯度为-8.00,即-4.00 x 2.00。

  需要注意的是,如果multiply gate的一个输入非常小,而另一个非常大,那么multiply gate将执行一些不太好的操作:它将为小输入指定一个相对较大的梯度,为大输入指定一个微小的梯度。注意,在线性分类器中,权重与输入进行点积wTxi(相乘),这意味着数据的规模对权重梯度的大小有影响。例如,如果在预处理过程中,将所有输入的样本数据xi乘以1000,那么权重的梯度将是1000倍,因此会降低学习率。这就是为什么预处理非常重要。



*本博客翻译总结自CS231n课程作业网站,该网站需要*才能访问。

上一篇:UVa 10735 - Euler Circuit (网络流)


下一篇:德勤 Intern - online assessment prep(一)