如果把神经网络模型比作一个黑箱,把模型参数比作黑箱上面一个个小旋钮,那么根据通用近似理论(universal approximation theorem),只要黑箱上的旋钮数量足够多,而且每个旋钮都被调节到合适的位置,那这个模型就可以实现近乎任意功能(可以逼近任意的数学模型)。
显然,不是由人工调这些旋钮的(参数),所谓的机器学习,就是通过程序来自动调节这些参数。神经网络不仅参数众多(少则十几万,多则上亿),而且网络是由线性层和非线性层交替叠加而成,上层参数的变化会对下层的输出产生非线性的影响,因此,早期的神经网络流派一度无法往多层方向发展,因为他们找不到能用于任意多层网络的、简洁的自动调节参数的方法。
直到上世纪80年代,祖师爷辛顿发明了反向传播算法,用输出误差的均方差(就是loss值)一层一层递进地反馈到各层神经网络,用梯度下降法来调节每层网络的参数。至此,神经网络才得以开始它的深度之旅。
本文用python自己动手实现梯度下降和反向传播算法。请点击这里到Github上查看源码。
梯度下降
Figure 1: Gradient Decsent梯度下降法是一种将输出误差反馈到神经网络并自动调节参数的方法,它通过计算输出误差的loss值(J)对参数W的导数,并沿着导数的反方向来调节W,经过多次这样的操作,就能将输出误差减小到最小值,即曲线的最低点。
虽然Tensorflow、Pytorch这些框架都实现了自动求导的功能,但为了彻底理解参数调节的过程,还是有必要自己动手实现梯度下降和反向传播算法。我相信你和我一样,已经忘了之前学的微积分知识,因此,到可汗学院复习下Calculus
和Multivariable Calculus是个不错的方法,或是拜读这篇关于神经网络矩阵微积分的文章。
Figure2是求导的基本公式,其中最重要的是Chain Rule,它通过引入中间变量,将“y对x求导”的过程转换为“y对中间变量u求导,再乘以u对x求导”,这样就将一个复杂的函数链求导简化为多个简单函数求导。
如果你不想涉及这些求导的细节,可以跳过具体的计算,领会其思想就好。
反向传播
def forward_backward(x, y):
z1 = x @ W1 + b1
a1 = relu(z1)
outp = a1 @ W2 + b2
loss = mse(outp, y)
mse_grad(outp, y)
lin_grad(a1, outp, W2, b2)
relu_grad(z1, a1)
lin_grad(x, z1, W1, b1)
对于神经网络模型:Linear -> ReLu -> Linear -> MSE(Loss function)来说,反向传播就是根据链式法则对求导,用输出误差的均方差(MSE)对模型的输出求导,并将导数传回上一层神经网络,用于它们来对w、b和x(上上层的输出)求导,再将x的导数传回到它的上一层神经网络,由此将输出误差的均方差通过递进的方式反馈到各神经网络层。
对于求导的第一步是为这个函数链引入中间变量:
接着第二步是对各中间变量求导,最后才是将这些导数乘起来。
首先,反向传播的起点是对loss function求导,即。:
def mse_grad(outp, targ):
outp.g = 2 / outp.shape[0] * (outp.squeeze() - targ.float()).unsqueeze(-1)
mse_grad()之所以用unsqueeze(-1)给导数增加一个维度,是为了让导数的shape和tensor shape保持一致。
def lin_grad(inp, outp, w, b):
inp.g = outp.g @ w.t()
w.g = inp.t() @ outp.g
b.g = outp.g.sum(0)
linear层的反向传播是对求导,它也是一个函数链,也要先对中间变量求导再将所有导数相乘:
这些中间变量的导数分别是:
对向量求导,指的是对向量所有的标量求偏导(),即:,这个横向量也称为y的梯度。
这里,是一个向量,因此,
。
这个矩阵称为雅克比矩阵,它是个对角矩阵,因为,因此。
同理,。
因此,所有中间导数相乘的结果:
lin_grad()中的inp.g、w.g和b.g分别是求的导数,以inp.g为例,它等于,且需要乘以前面各层的导数,即outp.g @ w.t(),之所以要用点积运算符(@)而不是标量相乘,是为了让它的导数shape和tensor shape保持一致。同理,w.g和b.g也是根据相同逻辑来计算的。
def relu_grad(inp, outp):
inp.g = (inp > 0.).float() * outp.g
ReLu层的求导相对来说就简单多了,当输入 <= 0时,导数为0,当输入 > 0时,导数为1。
Testing
求导运算终于结束了,接下来就是验证我们的反向传播是否正确。验证方法是将forward_backward()计算的导数和Pytorch自动微分得到的导数相比较,如果它们相近,就认为我们的反向传播算法是正确的。
xg = x_train.g.clone()
w1g = W1.g.clone()
w2g = W2.g.clone()
b1g = b1.g.clone()
b2g = b2.g.clone()
x2 = x_train.clone().requires_grad_(True)
w11 = W1.clone().requires_grad_(True)
b11 = b1.clone().requires_grad_(True)
w22 = W2.clone().requires_grad_(True)
b22 = b2.clone().requires_grad_(True)
def forward_backward2(x, y):
l1 = x @ w11 + b11
z1 = relu(l1)
outp = z1 @ w22 + b22
return mse(outp, y)
loss = forward_backward2(x_train, y_train)
loss.backward()
首先,将计算好的参数导数保存到w1g、b1g、w2g和b2g中,再用Pytorch的自动微分来求w11、b11、w22和b22的导数。
def test_near(a, b): return np.allclose(a, b)
assert test_near(w1g, w11.grad)
assert test_near(b1g, b11.grad)
assert test_near(w2g, w22.grad)
assert test_near(b2g, b22.grad)
最后,用np.allclose()来比较导数间的差异,如果有任何一个导数不相近,assert就会报错。结果证明,我们自己动手实现的算法是正确的。
END
反向传播是遵循链式法则的,它将前向传播的输出作为输入,输入作为输出,通过递进的方式将求导这个动作从后向前传递回各层。神经网络参数的求导需要进行矩阵微积分计算,根据这些导数的反方向来调节参数,就可以让模型的输出误差的优化到最小值。
网友评论