反向传播 tensorflow back propagation

参考:cs231n课程笔记翻译:反向传播笔记

反向传播是利用链式法则递归计算表达式的梯度方法。

机器学习可以看做是数理统计的一个应用,在数理逻辑统计中一个常见的任务就是拟合,也就是给定一些样本点,用合适的曲线解释这些样本点随着自变量的变化关系。

深度学习样本点不再限定为(x,y),而可以是由向量、矩阵等等组成的广义点对(X,Y)。而此时,(X,Y)之间的关系也变得十分复杂,不太可能用一个简单函数表示。然而,人们发现可以用多层神经网络来表示这样的关系,而多层神经网络的本质就是一个多层复合的函数。借用网上找到的一幅图[1],来直观描绘一下这种复合关系。


其对应的表达式:


上面式中的Wij就是相邻两层神经元之间的权值,它们就是深度学习需要学习的参数,也就相当于直线拟合y=k*x+b中的待求参数k和b。

和直线拟合一样,深度学习的训练也有一个目标函数,这个目标函数定义了什么样的参数才算一组“好参数”,不过在机器学习中,一般是采用成本函数(cost function),然后,训练目标就是通过调整每一个权值Wij来使得cost达到最小。cost函数也可以看成是由所有待求权值Wij为自变量的复合函数,而且基本上是非凸的,即含有许多局部最小值。但实际中发现,采用我们常用的梯度下降法就可以有效的求解最小化cost函数的问题。

梯度下降法需要给定一个初始点,并求出该点的梯度向量,然后以负梯度方向为搜索方向,以一定的步长进行搜索,从而确定下一个迭代点,再计算该新的梯度方向,如此重复直到cost收敛。那么如何计算梯度呢?

假设我们把cost函数表示为 H(W_{11}, W_{12}, \cdots , W_{ij}, \cdots, W_{mn}), 那么它的梯度向量[2]就等于 \nabla H  = \frac{\partial H}{\partial W_{11} }\mathbf{e}_{11} + \cdots + \frac{\partial H}{\partial W_{mn} }\mathbf{e}_{mn}, 其中 \mathbf{e}_{ij}表示正交单位向量。为此,我们需求出cost函数H对每一个权值Wij的偏导数。而 BP算法正是用来求解这种多层复合函数的所有变量的偏导数的利器


同样是利用链式法则,BP算法则机智地避开了这种冗余,它对于每一个路径只访问一次就能求顶点对所有下层节点的偏导值。
正如反向传播(BP)算法的名字说的那样,BP算法是反向(自上往下)来寻找路径的。

简单表达式和理解梯度

从简单表达式入手可以为复杂表达式打好符号和规则基础。先考虑一个简单的二元乘法函数f(x,y)=xy。对两个输入变量分别求偏导数还是很简单的:

\displaystyle f(x,y)=xy \to \frac {df}{dx}=y \quad \frac {df}{dy}=x

解释牢记这些导数的意义:函数变量在某个点周围的极小区域内变化,而导数就是变量变化导致的函数在该方向上的变化率。

\frac{df(x)}{dx}= lim_{h\to 0}\frac{f(x+h)-f(x)}{h}

注意等号左边的分号和等号右边的分号不同,不是代表分数。相反,这个符号表示操作符\frac{d}{dx}被应用于函数f,并返回一个不同的函数(导数)。对于上述公式,可以认为h值非常小,函数可以被一条直线近似,而导数就是这条直线的斜率。换句话说,每个变量的导数指明了整个表达式对于该变量的值的敏感程度。比如,若x=4,y=-3,则f(x,y)=-12x的导数\frac{\partial f}{\partial x}=-3。这就说明如果将变量x的值变大一点,整个表达式的值就会变小(原因在于负号),而且变小的量是x变大的量的三倍。通过重新排列公式可以看到这一点(f(x+h)=f(x)+h \frac{df(x)}{dx})。同样,因为\frac{\partial f}{\partial y}=4,可以知道如果将y的值增加h,那么函数的输出也将增加(原因在于正号),且增加量是4h

函数关于每个变量的导数指明了整个表达式对于该变量的敏感程度。

如上所述,梯度\nabla f是偏导数的向量,所以有\nabla f(x)=[\frac{\partial f}{\partial x},\frac{\partial f}{\partial y}]=[y,x]。即使是梯度实际上是一个向量,仍然通常使用类似“x上的梯度”的术语,而不是使用如“x的偏导数”的正确说法,原因是因为前者说起来简单。

我们也可以对加法操作求导:

\displaystyle f(x,y)=x+y \to \frac {df}{dx}=1\quad\frac {df}{dy}=1

这就是说,无论其值如何,x,y的导数均为1。这是有道理的,因为无论增加x,y中任一个的值,函数f的值都会增加,并且增加的变化率独立于x,y的具体值(情况和乘法操作不同)。取最大值操作也是常常使用的:
\displaystyle f(x,y)=max(x,y) \to \frac {df}{dx}=1 (x>=y) \quad\frac {df}{dy}=1 (y>=x)

上式是说,如果该变量比另一个变量大,那么梯度是1,反之为0。例如,若x=4,y=2,那么max是4,所以函数对于y就不敏感。也就是说,在y上增加h,函数还是输出为4,所以梯度是0:因为对于函数输出是没有效果的。当然,如果给y增加一个很大的量,比如大于2,那么函数f的值就变化了,但是导数并没有指明输入量有巨大变化情况对于函数的效果,他们只适用于输入量变化极小时的情况,因为定义已经指明:lim_{h\to 0}

使用链式法则计算复合表达式

现在考虑更复杂的包含多个函数的复合函数,比如f(x,y,z)=(x+y)z。虽然这个表达足够简单,可以直接微分,但是在此使用一种有助于读者直观理解反向传播的方法。将公式分成两部分:q=x+yf=qz。在前面已经介绍过如何对这分开的两个公式进行计算,因为fqz相乘,所以\displaystyle\frac{\partial f}{\partial q}=z,\frac{\partial f}{\partial z}=q,又因为qxy,所以\displaystyle\frac{\partial q}{\partial x}=1,\frac{\partial q}{\partial y}=1。然而,并不需要关心中间量q的梯度,因为\frac{\partial f}{\partial q}没有用。相反,函数f关于x,y,z的梯度才是需要关注的。链式法则指出将这些梯度表达式链接起来的正确方式是相乘,比如\displaystyle\frac{\partial f}{\partial x}=\frac{\partial f}{\partial q}\frac{\partial q}{\partial x}。在实际操作中,这只是简单地将两个梯度数值相乘,示例代码如下:


# 设置输入值
x = -2; y = 5; z = -4

# 进行前向传播
q = x + y # q becomes 3
f = q * z # f becomes -12

# 进行反向传播:
# 首先回传到 f = q * z
dfdz = q # df/dz = q, 所以关于z的梯度是3
dfdq = z # df/dq = z, 所以关于q的梯度是-4
# 现在回传到q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. 这里的乘法是因为链式法则
dfdy = 1.0 * dfdq # dq/dy = 1

最后得到变量的梯度[dfdx, dfdy, dfdz],它们告诉我们函数f对于变量[x, y, z]的敏感程度。这是一个最简单的反向传播。一般会使用一个更简洁的表达符号,这样就不用写df了。这就是说,用dq来代替dfdq,且总是假设梯度是关于最终输出的。

模块化:Sigmoid例子

上面介绍的门是相对随意的。任何可微分的函数都可以看做门。可以将多个门组合成一个门,也可以根据需要将一个函数分拆成多个门。现在看看一个表达式:

\displaystyle f(w,x)=\frac{1}{1+e^{-(w_0x_0+w_1x_1+w_2)}}

在后面的课程中可以看到,这个表达式描述了一个含输入x和权重w的2维的神经元,该神经元使用了sigmoid激活函数。但是现在只是看做是一个简单的输入为x和w,输出为一个数字的函数。这个函数是由多个门组成的。除了上文介绍的加法门,乘法门,取最大值门,还有下面这4种:


\displaystyle f(x)=\frac{1}{x} \to \frac{df}{dx}=-1/x^2
\displaystyle f_c(x)=c+x \to \frac{df}{dx}=1 \displaystyle f(x)=e^x \to \frac{df}{dx}=e^x
\displaystyle f_a(x)=ax \to \frac{df}{dx}=a

其中,函数f_c使用对输入值进行了常量c的平移,f_a将输入值扩大了常量a倍。它们是加法和乘法的特例,但是这里将其看做一元门单元,因为确实需要计算常量c,a的梯度。整个计算线路如下:


———————————————————————————————————————

使用sigmoid激活函数的2维神经元的例子。输入是[x0, x1],可学习的权重是[w0, w1, w2]。一会儿会看见,这个神经元对输入数据做点积运算,然后其激活数据被sigmoid函数挤压到0到1之间。

————————————————————————————————————————

在上面的例子中可以看见一个函数操作的长链条,链条上的门都对wx的点积结果进行操作。该函数被称为sigmoid函数\sigma (x)。sigmoid函数关于其输入的求导是可以简化的(使用了在分子上先加后减1的技巧):

\displaystyle\sigma(x)=\frac{1}{1+e^{-x}}
\displaystyle\to\frac{d\sigma(x)}{dx}=\frac{e^{-x}}{(1+e^{-x})^2}=(\frac{1+e^{-x}-1}{1+e^{-x}})(\frac{1}{1+e^{-x}})=(1-\sigma(x))\sigma(x)

可以看到梯度计算简单了很多。举个例子,sigmoid表达式输入为1.0,则在前向传播中计算出输出为0.73。根据上面的公式,局部梯度为(1-0.73)*0.73~=0.2,和之前的计算流程比起来,现在的计算使用一个单独的简单表达式即可。因此,在实际的应用中将这些操作装进一个单独的门单元中将会非常有用。该神经元反向传播的代码实现如下:

w = [2,-3,-3] # 假设一些随机数据和权重
x = [-1, -2]

# 前向传播
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid函数

# 对神经元反向传播
ddot = (1 - f) * f # 点积变量的梯度, 使用sigmoid函数求导
dx = [w[0] * ddot, w[1] * ddot] # 回传到x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # 回传到w
# 完成!得到输入的梯度

实现提示:分段反向传播。上面的代码展示了在实际操作中,为了使反向传播过程更加简洁,把向前传播分成不同的阶段将是很有帮助的。比如我们创建了一个中间变量dot,它装着wx的点乘结果。在反向传播的时,就可以(反向地)计算出装着wx等的梯度的对应的变量(比如ddotdxdw)。


本节的要点就是展示反向传播的细节过程,以及前向传播过程中,哪些函数可以被组合成门,从而可以进行简化。知道表达式中哪部分的局部梯度计算比较简洁非常有用,这样他们可以“链”在一起,让代码量更少,效率更高。

反向传播实践:分段计算

看另一个例子。假设有如下函数:

\displaystyle f(x,y)=\frac{x+\sigma(y)}{\sigma(x)+(x+y)^2}

首先要说的是,这个函数完全没用,读者是不会用到它来进行梯度计算的,这里只是用来作为实践反向传播的一个例子,需要强调的是,如果对xy进行微分运算,运算结束后会得到一个巨大而复杂的表达式。然而做如此复杂的运算实际上并无必要,因为我们不需要一个明确的函数来计算梯度,只需知道如何使用反向传播计算梯度即可。下面是构建前向传播的代码模式:

x = 3 # 例子数值
y = -4

# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 分子中的sigmoi          #(1)
num = x + sigy # 分子                                    #(2)
sigx = 1.0 / (1 + math.exp(-x)) # 分母中的sigmoid         #(3)
xpy = x + y                                              #(4)
xpysqr = xpy**2                                          #(5)
den = sigx + xpysqr # 分母                                #(6)
invden = 1.0 / den                                       #(7)
f = num * invden # 搞定!                                 #(8)

┗|`O′|┛ 嗷~~,到了表达式的最后,就完成了前向传播。注意在构建代码s时创建了多个中间变量,每个都是比较简单的表达式,它们计算局部梯度的方法是已知的。这样计算反向传播就简单了:我们对前向传播时产生每个变量(sigy, num, sigx, xpy, xpysqr, den, invden)进行回传。我们会有同样数量的变量,但是都以d开头,用来存储对应变量的梯度。注意在反向传播的每一小块中都将包含了表达式的局部梯度,然后根据使用链式法则乘以上游梯度。对于每行代码,我们将指明其对应的是前向传播的哪部分。


# 回传 f = num * invden
dnum = invden # 分子的梯度                                         #(8)
dinvden = num                                                     #(8)
# 回传 invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                                #(7)
# 回传 den = sigx + xpysqr
dsigx = (1) * dden                                                #(6)
dxpysqr = (1) * dden                                              #(6)
# 回传 xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# 回传 xpy = x + y
dx = (1) * dxpy                                                   #(4)
dy = (1) * dxpy                                                   #(4)
# 回传 sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below  #(3)
# 回传 num = x + sigy
dx += (1) * dnum                                                  #(2)
dsigy = (1) * dnum                                                #(2)
# 回传 sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy                                 #(1)
# 完成! 嗷~~

猜你喜欢

转载自blog.csdn.net/shaopeng568/article/details/79812126
今日推荐