可终于来到反向传播了。。。之前更新得实在是太慢了。。。这里因为贫僧已经过了一次,所以很多地方会直接忽略掉。
反向传播是利用链式法则递归计算表达式的梯度的方法。理解反向传播过程及其精妙之处对于理解、实现、设计和调试神经网络非常关键(疯狂暗示)。
问题陈述:核心问题是给定函数 f(x) f ( x ) ,其中 x x 是输入数据的向量,需要计算函数 f f 关于 x x 的梯度,也就是 ∇f(x) ∇ f ( x ) 。
通常就是要计算出损失函数( f f 和损失函数 L L 相关)的梯度(这意味着要计算权重、偏置和输入数据的梯度,但是实际运用中通常只计算权重和偏置的),然后通过反向传播计算出参数的梯度然后更新权重矩阵 W W 。
梯度其实就是:
偏导、链式法则等基础部分直接略过,这部分不懂的请自行翻看高数(同济版的话是上册,这些已经是非常基础的知识了)课本。。。
但是这里还是提一下作者说的方向传播的意思,首先举个例子:
存在这么一个公式 f(x,y,z)=(x+y)z f ( x , y , z ) = ( x + y ) z ,令 q=x+y,x=−2,y=5,z=−4 q = x + y , x = − 2 , y = 5 , z = − 4 ,那么变量梯度的计算过程可以用下图表示:
因为梯度计算的时候是根据链式法则递归地向前计算梯度(红色数字部分就是对应的梯度),一直到网络的输入端,所以可以认为梯度是从计算链路中“回流”(这就是作者说的反向的意思,不是什么高端的东西)。
这里的求导部分很简答,所以不细讲。值得记录的是下面这部分:
其实就是直接将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,z)=(x+y)z f ( x , y , z ) = ( x + y ) z 令 q=x+y,f=qz q = x + y , f = q z ,那么 ∂f∂q=z,∂f∂z=q ∂ f ∂ q = z , ∂ f ∂ z = q ,因为 q=x+y q = x + y ,所以 ∂q∂x=1,∂q∂y=1 ∂ q ∂ x = 1 , ∂ q ∂ y = 1 ,所以 ∂f∂x=∂f∂q∂q∂x ∂ 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课程笔记翻译:反向传播笔记!
其实关键思路还是上面提到的那个关键公式,有点像按照模块来进行微分。
原来的笔记里面提到了两点:
a += b
而不是a = a + b
,因为后者是覆写的方式。但是贫僧觉得这只是方便理解的方式,对结果不会产生影响(不过可能会对计算速度产生影响,至少在C里面是这样,Python应该也是这样)。不过为了遵循微积分中的多元链式法则,最好还是这么做,不然可能会让其他看代码的人感到一头雾水。贫僧觉得这部分里面提到的思路挺有意思。
作者是直接将乘法、加法、 max m a x 函数看成类似数字电路里面的门,然后给这些门单元规定了对应的行为:
上面看起来比较难理解的话可以看下面这个例子(图里面线上方绿色的数字是输入值,线下方红色的数字是这个节点对应的梯度):
注意乘法门单元的一种特殊情况:如果乘法门单元的其中一个输入很小,另一个很大,那么它会将大的梯度分配给小的梯度,把小的梯度分配给大的梯度。因为在线性分类器里面权重和输入直接进行点积 ωTxi ω T x i ,所以输入数据大小直接影响到权重梯度大小(所以要对数据进行预处理)。例如如果计算过程中所有输入数据 xi 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:万恶之源