首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

斯坦福CS231n - CNN for Visual Recognition(4)-lecture4反向传播

简单来说,反向传播是一种通过链式法则递归计算表达式的梯度的方法,本节主要介绍了给定f(x)f(x),求解梯度∇f(x)\nabla f(x)的方法。理解反向传播,对神经网络的调整和优化优很大的帮助。

  在神经网络中,对应的是损失函数LL,输入xx包含训练数据和神经网络的权重。比如,损失函数为SVMSVM,输入包括了训练数据xi,yix_i,y_i、权重WW和偏差bb。而训练集是给定的,权重则是可以改变的变量。因此,即使能用反向传播计算输入数据xix_i上的梯度,但在实践为了进行参数更新,通常也只计算参数(比如W,bW,b)的梯度。当然,xix_i的梯度有时仍然有用,比如将神经网络所做的事情可视化,以便于直观理解时。


偏导公式和理解梯度

  考虑f(x,y)=xyf(x,y)=xy,则x,yx,y的偏导分别为

f(x,y)=xy→∂f∂x=y∂f∂y=x

f(x,y) = x y \hspace{1in} \rightarrow \hspace{1in} \frac{\partial f}{\partial x} = y \hspace{0.5in} \frac{\partial f}{\partial y} = x

详细理解

  导数就是变量变化导致的函数在该方向上的变化率。

df(x)dx=limh →0f(x+h)−f(x)h

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

  以上公式中的df(x)dx\frac{df(x)}{dx}作用在ff上,表示对xx求偏导数,表示xx维度上当前点位置周边很小区域的变化率。举个例子,如果x=4,y=−3x=4,y=−3,而f(x,y)=−12f(x,y)=−12,那么xx上的偏导∂f∂x=−3\frac{\partial f}{\partial x}=−3,这个变量xx增大一个很小的量,那么整个表达式会以3倍这个量减小。公式也可以调整下:f(x+h)=f(x)+hdf(x)dxf(x+h)=f(x)+h\frac{df(x)}{dx}。同理,因为∂f∂y=4\frac{\partial f}{\partial y}=4,如果将yy的值增加一个很小的量hh,则整个表达式变化4h4h。 函数关于每个变量的导数指明了整个表达式对于该变量的敏感程度。

梯度是偏导数的向量,所以有∇f=[∂f∂x,∂f∂y]=[y,x]\nabla f = [\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}] = [y, x]。   我们也可以对加法和maxmax函数求导:

f(x,y)=x+y→∂f∂x=1∂f∂y=1

f(x,y) = x + y \hspace{1in} \rightarrow \hspace{1in} \frac{\partial f}{\partial x} = 1 \hspace{0.5in} \frac{\partial f}{\partial y} = 1

f(x,y)=max(x,y)→∂f∂x=1(x>=y)∂f∂y=1(y>=x)

f(x,y) = \max(x, y) \hspace{1in} \rightarrow \hspace{1in} \frac{\partial f}{\partial x} = 1(x >= y) \hspace{0.5in} \frac{\partial f}{\partial y} = 1(y >= x)

  上式maxmax函数,如果该变量比另一个变量大,那么梯度是1,反之为0。例如,若x=4,y=2x=4,y=2,那么结果44,所以函数对于yy就不敏感,所以yy梯度是00,因为对于函数输出是没有效果的。


链式法则计算复合函数偏导

  考虑f(x,y,z)=(x+y)zf(x,y,z)=(x+y)z。当然,我们可以直接求偏导。但是为了帮助我们直观理解反向传播,使用换元法,把原函数拆成两个部分q=x+yq=x+y和f=qzf=qz。对于这两个部分,我们知道怎么求解它们变量上的偏导:∂f∂q=z,∂f∂z=q,∂q∂x=1,∂q∂y=1\frac{\partial f}{\partial q}=z,\frac{\partial f}{\partial z}=q,\frac{\partial q}{\partial x}=1,\frac{\partial q}{\partial y}=1,当然qq的偏导∂f∂q\frac{\partial f}{\partial q}我们并不关心。 通过链式法则,上述的偏导数:∂f∂x=∂f∂q∂q∂x\frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \frac{\partial q}{\partial x}   看下图

代码语言:javascript
复制
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
dfdq = z # df/dq = z
# 再算到了 q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1 恩,链式法则
dfdy = 1.0 * dfdq # dq/dy = 1

  链式法则的结果是,只剩下我们感兴趣的dfdx,dfdy,dfdz,也就是原函数在x,y,zx,y,z上的偏导。这是一个简单的例子,之后的程序里面我们为了简洁,不会完整写出dfdq,而是用dq代替。

  上图的真实值计算线路展示了计算的视觉化过程。前向传播从输入计算到输出(绿色),反向传播从尾部开始,根据链式法则递归地向前计算梯度(显示为红色),一直到网络的输入端。可以认为,梯度是从计算链路中回流。


反向传播的直观理解

  在整个计算线路图中,每个门单元都会得到一些输入并立即计算两个值:这个门的输出值和其输出值关于输入值的局部梯度。门单元完成这两件事是完全独立的。一旦前向传播完毕,在反向传播的过程中,门单元将最终获得整个网络的最终输出值在自己的输出值上的梯度。链式法则指出,门单元应该将回传的梯度乘以它的局部梯度,从而得到整个网络的输出对该门单元的每个输入值的梯度。

  这里对于每个输入的乘法操作是基于链式法则的。该操作让一个相对独立的门单元变成复杂计算线路中不可或缺的一部分,这个复杂计算线路可以是神经网络等。

  下面还以上面例子来对这一过程进行理解。加法门收到了输入[−2,5][-2, 5],计算输出是33。这个门是加法操作,两个输入的局部梯度都是+1+1。网络的其余部分计算出最终值为−12-12。在反向传播时将递归地使用链式法则,算到加法门(是乘法门的输入)的时候,知道加法门的输出的梯度是−4-4。如果网络想要输出值更高,那么加法门的输出要更小(因为梯度−4-4)。继续递归并对梯度使用链式法则,加法门拿到回传梯度,然后把这个梯度分别乘到每个输入值的局部梯度(就是让−4-4乘以xx和yy的局部梯度,xx和yy的局部梯度都是11,所以最终都是−4-4)。可以看到得到了想要的效果:如果x,yx,y减小,那么加法门的输出值减小,而乘法门的输出会增大。

  因此,反向传播可看为门单元之间在通过梯度信号相互通信,只要让它们的输入沿着梯度方向变化,则整个网络的输出值更高。


Sigmoid例子

f(w,x)=11+e−(w0x0+w1x1+w2)

f(w,x) = \frac{1}{1+e^{-(w_0x_0 + w_1x_1 + w_2)}} 这个看似复杂的函数,其实可以看做一些基础函数的组合,这些基础函数及他们的偏导如下:

f(x)=1x→dfdx=−1/x2fc(x)=c+x→dfdx=1f(x)=ex→dfdx=exfa(x)=ax→dfdx=a

f(x) = \frac{1}{x} \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = -1/x^2 \\ f_c(x) = c + x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = 1 \\ f(x) = e^x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = e^x \\ f_a(x) = ax \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = a   整个计算流程如下:

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

  上面的例子中,ww与xx之间的内积分解为一长串的小函数连接完成,而后接的是sigmoid函数σ(x)σ(x),而sigmoid函数看似复杂,导数求解倒是的时候却是有技巧的,如下:

σ(x)=11+e−x→dσ(x)dx=e−x(1+e−x)2=(1+e−x−11+e−x)(11+e−x)=(1−σ(x))σ(x)

\sigma(x) = \frac{1}{1+e^{-x}} \\ \rightarrow \hspace{0.3in} \frac{d\sigma(x)}{dx} = \frac{e^{-x}}{(1+e^{-x})^2} = \left( \frac{1 + e^{-x} - 1}{1 + e^{-x}} \right) \left( \frac{1}{1+e^{-x}} \right) = \left( 1 - \sigma(x) \right) \sigma(x)   举个例子,sigmoid表达式输入为1.01.0,则在前向传播中计算出输出为0.730.73。根据上面的公式,局部梯度为(1−0.73)∗0.73≈0.2(1-0.73)*0.73 \approx0.2。   该神经元反向传播的代码实现如下:

代码语言:javascript
复制
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)。


反向传播实战:复杂函数

f(x,y)=x+σ(y)σ(x)+(x+y)2

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

  我们把这个函数分解成小部分,进行前向和反向传播计算,即可得到结果,前向传播计算的代码如下:

代码语言:javascript
复制
x = 3 # 例子
y = -4

# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 单值上的sigmoid函数
num = x + sigy 
sigx = 1.0 / (1 + math.exp(-x)) 
xpy = x + y      
xpysqr = xpy**2                 
den = sigx + xpysqr
invden = 1.0 / den                                       
f = num * invden # 完成!

  我们并没有一次性把前向传播最后结果算出来,而是刻意留出了很多中间变量,它们都是我们可以直接求解局部梯度的简单表达式。因此,计算反向传播就变得简单了:我们从最后结果往前看,前向运算中的每一个中间变量sigy, num, sigx, xpy, xpysqr, den, invden我们都会用到,只不过后向传回的偏导值乘以它们,得到反向传播的偏导值。反向传播计算的代码如下:

代码语言:javascript
复制
# 局部函数表达式为 f = num * invden
dnum = invden                              
dinvden = num    
# 局部函数表达式为 invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                            
# 局部函数表达式为 den = sigx + xpysqr
dsigx = (1) * dden
dxpysqr = (1) * dden
# 局部函数表达式为 xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# 局部函数表达式为 xpy = x + y
dx = (1) * dxpy                                                   
dy = (1) * dxpy                                                   
# 局部函数表达式为 sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # 注意到这里用的是 += !!
# 局部函数表达式为 num = x + sigy
dx += (1) * dnum                                                  
dsigy = (1) * dnum                                                
# 局部函数表达式为 sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy                                 
# 完事!

注意:

对前向传播变量进行缓存:在计算反向传播时,前向传播过程中得到的一些中间变量非常有用。在实际操作中,最好代码实现对于这些中间变量的缓存,这样在反向传播的时候也能用上它们。如果这样做过于困难,也可以(但是浪费计算资源)重新计算它们。

在不同分支的梯度要相加:如果变量xx,yy在前向传播的表达式中出现多次,那么进行反向传播的时候就要非常小心,使用+=+=而不是==来累计这些变量的梯度(不然就会造成覆写)。这是遵循了在微积分中的多元链式法则,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加。


回传流中的模式

  虽然神经网络结构形式和使用的神经元都不同,但是后向计算中的梯度计算大多可以归到几种常见的模式上。比如,最常见的三种简单运算门(加、乘、最大),他们在反向传播运算中的作用是非常简单和直接的。比如下面这个简单的神经网:

上图里有我们提到的三种门:add,max 和 multiplyadd , max\text{ 和 } multiply。

  • 加运算门在反向传播运算中,不管输入值是多少,取得它output传回的梯度(gradient)然后均匀地分给两条输入路径。因为加法运算的偏导都是+1.0+1.0。
  • maxmax最大门,在反向传播计算中,它只会把传回的梯度回传给一条输入路径。因为max(x,y)max(x,y)只对xx和yy中较大的那个数,偏导为+1.0+1.0,而另一个数上的偏导是00。
  • 乘法门就更好理解了,因为x∗yx*y对xx的偏导为yy,而对yy的偏导为xx,因此在上图中xx的梯度是−8.0-8.0,即−4.0∗2.0-4.0*2.0。

用向量化操作计算梯度

上述内容考虑的都是单个变量情况,但是所有概念都适用于矩阵和向量操作。然而,在操作的时候要注意关注维度和转置操作。

矩阵相乘的梯度:可能最有技巧的操作是矩阵相乘(也适用于矩阵和向量,向量和向量相乘)的乘法操作:

代码语言:javascript
复制
# 前向传播
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)

# 假设我们得到了D的梯度
dD = np.random.randn(*D.shape) # 和D一样的尺寸
dW = dD.dot(X.T) #.T就是对矩阵进行转置
dX = W.T.dot(dD)

提示:要分析维度!注意不需要去记忆dWdW和dXdX的表达,因为它们很容易通过维度推导出来。例如,权重的梯度dWdW的尺寸肯定和权重矩阵WW的尺寸是一样的,而这又是由XX和dDdD的矩阵乘法决定的(在上面的例子中XX和WW都是数字不是矩阵)。总有一个方式是能够让维度之间能够对的上的。例如,XX的尺寸是[10x3][10x3],dDdD的尺寸是[5x3][5x3],如果你想要dWdW和WW的尺寸是[5x10][5x10],那就要dD.dot(X.T)使用小而具体的例子:如果觉得向量化操作的梯度计算比较困难,建议是写出一个很小很明确的向量化例子,在纸上演算梯度,然后对其一般化,得到一个高效的向量化操作形式。

  最后我们用一组图来说明实际优化过程中的正向传播与反向残差传播:


参考资料

链接:http://cs231n.github.io/optimization-2/ 链接:https://zhuanlan.zhihu.com/p/21407711 链接:http://blog.csdn.net/han_xiaoyang/article/details/50321873

下一篇
举报
领券