CS231n笔记(4)反向传播

版权声明:本文为博主原创文章,如未特别声明,均默认使用CC BY-SA 3.0许可。 https://blog.csdn.net/Geek_of_CSDN/article/details/82057430

可终于来到反向传播了。。。之前更新得实在是太慢了。。。这里因为贫僧已经过了一次,所以很多地方会直接忽略掉。

简介

反向传播是利用链式法则递归计算表达式的梯度的方法。理解反向传播过程及其精妙之处对于理解、实现、设计和调试神经网络非常关键(疯狂暗示)。

问题陈述:核心问题是给定函数 f ( x ) ,其中 x 是输入数据的向量,需要计算函数 f 关于 x 的梯度,也就是 f ( x )

通常就是要计算出损失函数( f 和损失函数 L 相关)的梯度(这意味着要计算权重、偏置和输入数据的梯度,但是实际运用中通常只计算权重和偏置的),然后通过反向传播计算出参数的梯度然后更新权重矩阵 W

梯度

梯度其实就是:

f ( x ) = [ f x , f y ] = [ y , x ]

偏导、链式法则等基础部分直接略过,这部分不懂的请自行翻看高数(同济版的话是上册,这些已经是非常基础的知识了)课本。。。

但是这里还是提一下作者说的方向传播的意思,首先举个例子:

存在这么一个公式 f ( x , y , z ) = ( x + y ) z ,令 q = x + y , x = 2 , y = 5 , z = 4 ,那么变量梯度的计算过程可以用下图表示:

这里写图片描述

扫描二维码关注公众号,回复: 3074433 查看本文章

因为梯度计算的时候是根据链式法则递归地向前计算梯度(红色数字部分就是对应的梯度),一直到网络的输入端,所以可以认为梯度是从计算链路中“回流”(这就是作者说的反向的意思,不是什么高端的东西)。

模块化:Sigmoid例子

这里的求导部分很简答,所以不细讲。值得记录的是下面这部分:

σ ( x ) = 1 1 + e x d σ ( x ) d x = e x ( 1 + e x ) 2 = ( 1 + e x 1 1 + e x ) ( 1 1 + e x ) = ( 1 σ ( x ) ) σ ( x )

其实就是直接将sigmoid求导之后的公式直接封装成了一个函数,到时候要调用的时候就可以直接调用了:

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其实是中间变量,装着w和x的点乘结果。反向传播的时候就可以直接结算出装着w和x等的梯度对应的变量(例如ddot)(这里的具体做法其实看上面的程序就基本上可以理解了)。

例子:

假设有这个函数:

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

要对上面这个函数来进行梯度计算的话就要用到下面这个关键公式: f ( x , y , z ) = ( x + y ) z q = x + y , f = q z ,那么 f q = z , f z = q ,因为 q = x + y ,所以 q x = 1 , q y = 1 ,所以 f x = f q q x

这里直接给出笔记里面的程序

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)
# 回传 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)
# 完成! 嗷~~

话说上面的程序中的翻译来自CS231n课程笔记翻译:反向传播笔记

其实关键思路还是上面提到的那个关键公式,有点像按照模块来进行微分。

原来的笔记里面提到了两点:

  1. 对前向传播变量进行缓存:这是因为在计算反向传播的时候这些中间变量会变得很有用。虽然可以直接重新计算这些变量,但是会浪费掉一定的计算资源
  2. 在不同分支的梯度要相加:原文的意思是要将两个梯度的相加写成a += b而不是a = a + b,因为后者是覆写的方式。但是贫僧觉得这只是方便理解的方式,对结果不会产生影响(不过可能会对计算速度产生影响,至少在C里面是这样,Python应该也是这样)。不过为了遵循微积分中的多元链式法则,最好还是这么做,不然可能会让其他看代码的人感到一头雾水。

回传流中的模式

贫僧觉得这部分里面提到的思路挺有意思。

作者是直接将乘法、加法、 m a x 函数看成类似数字电路里面的门,然后给这些门单元规定了对应的行为:

  1. 加法门单元:把输出的梯度等量地发给所有输入,这一行为与输入值在前向传播时的值无关(因为假发操作的局部梯度都是简单的+1,这就相当于输出的梯度乘以1.0然后得到输入的维度,所以所有输入的梯度实际上等于输出的梯度)。
  2. 取最大值门单元:取最大值门单元将梯度传给其中一个输入,这个输入是在前向传播中值最大的那个输入(因为在取最大值门中最高值对应的局部梯度是1.0,其余的是0)。
  3. 乘法门单元:取输入激活数据,对它们进行交换,然后乘以梯度(因为局部梯度就是两个输入相互交换之后对应的输入值,然后最终对应的梯度要根据链式法则来乘以输出值的梯度)。

上面看起来比较难理解的话可以看下面这个例子(图里面线上方绿色的数字是输入值,线下方红色的数字是这个节点对应的梯度):

这里写图片描述

注意乘法门单元的一种特殊情况:如果乘法门单元的其中一个输入很小,另一个很大,那么它会将大的梯度分配给小的梯度,把小的梯度分配给大的梯度。因为在线性分类器里面权重和输入直接进行点积 ω T x i ,所以输入数据大小直接影响到权重梯度大小(所以要对数据进行预处理)。例如如果计算过程中所有输入数据 x i 乘以1000,那么权重的梯度会增大1000倍,这样就要降低学习率来弥补(因为学习率会直接和梯度相乘,学习率就相当于步长,梯度是方向,但是现在方向的大小增大了1000倍,所以如果步长不变的话那么步长乘以梯度的结果会变大1000倍,那么最终走的一步会很大,这样要么导致在最优点附近震荡,要么直接无法找到最优值)。

用向量化操作计算梯度

# 前向传播
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)

这里要注意的其实就是矩阵的维度,不记得的话建议翻下线代课本,还是基础的东西。

参考

CS231n课程笔记翻译:反向传播笔记:贫僧偷懒。。。直接看译文了。。。
Backpropagation, Intuitions:万恶之源

猜你喜欢

转载自blog.csdn.net/Geek_of_CSDN/article/details/82057430