深度学习入门——用python从轮子开始造神经网络(2)
作者:时棋
话接上期——上期我们造了一个极简风格的原始网络,跑不动的那种。那么问题出在哪里呢?之前仅仅只是用“效率过低”简单概括了下,那么具体是哪里的效率过低拖慢了整体速度呢?经笔者测试,上期的示例代码在计算梯度时,执行一步梯度下降并计算出新梯度需要将近一分钟,而我们的程序要求多次执行梯度下降算法,这样的时间消耗是不可接受的。而且实际上,上期构筑的网络忽略了许多要素,简化的同时可能会丢失许多重要的因素,因此,这期我将在采用更高效的计算梯度的算法的基础上,构筑一个含有完整的输入层、中间层、输出层的2层神经网络,同时加入偏置。
注意,这一期数学推导的占比较大
神经网络流程示意
我们依然以MNIST数据集为例,从训练集中抽取100个作为一轮学习的单元,建立一个中间层含50个神经元的神经网络。
首先,输入层为 X ( 100 ∗ 784 的 数 组 ) X(100*784的数组) X(100∗784的数组),后经过Affine层(加权和与加偏置的操作对应几何中的仿射变换), X ⋅ W 1 + B 1 ( W 1 为 784 ∗ 50 的 数 组 , B 1 为 100 ∗ 50 的 数 组 ) X\cdot W_1+B_1(W_1为784*50的数组,B_1为100*50的数组) X⋅W1+B1(W1为784∗50的数组,B1为100∗50的数组) ,经过激活函数(这里以Sigmoid函数为例)处理得矩阵 Y ( 100 ∗ 50 的 数 组 ) Y(100*50的数组) Y(100∗50的数组),再经过一层Affine层, Y ⋅ W 2 + B 2 ( W 2 为 50 ∗ 10 的 数 组 , B 2 为 100 ∗ 10 的 数 组 ) Y\cdot W_2+B_2(W_2为50*10的数组,B_2为100*10的数组) Y⋅W2+B2(W2为50∗10的数组,B2为100∗10的数组),经过输出层激活函数(这里使用softmax函数),得到最终的预测矩阵,同时为神经网络的学习需要,计算交叉熵误差作为损失函数值。
以上为简略示意图
而在代码实现的过程中,我们实际上是要创建以下几个层:Affine层、sigmoid层、softmax_loss层
误差反向传播算法
那么我们进入正题,如何快速求出梯度?从我们原始的方法开始分析,在上期的代码中,我们是怎么求梯度的?
实际上,我们采取了原始的数值微分方法,通过上下小幅度改变权重矩阵中元素的值,再次计算损失函数,然后相减除以差分。这相当于把正向的流程走了两遍。由于每个元素都要走两遍,那么一共就是784乘10乘2等于15680次,如果是我们这次要建的2层网络,那就是784乘50乘2加上50乘10乘2等于79400次!
这还深度学个啥呀……
有没有办法一遍就能把梯度求出来呢?显然光靠计算机是不行的,求数值解过于耗费算力,如果人工能求出解析解公式,那便会轻松很多,相当于求一个嵌套函数的导数。链式法则实在太适合这 一应用场景了!在前向传播中,每一层都可以有“记忆”,有了记忆,反过去求导数就不用再计算一遍了,同时矩阵中的元素也不需要单独计算偏导了。以上述示意图为例,即为:
∂ z ∂ W 1 = ∂ z ∂ Y 1 ⋅ ∂ Y 1 ∂ Y ⋅ ∂ Y ∂ X 1 ⋅ ∂ X 1 ∂ W 1 \frac{\partial z}{\partial W_1}=\frac{\partial z}{\partial Y_1}\cdot\frac{\partial Y_1}{\partial Y}\cdot\frac{\partial Y}{\partial X_1}\cdot\frac{\partial X_1}{\partial W_1} ∂W1∂z=∂Y1∂z⋅∂Y∂Y1⋅∂X1∂Y⋅∂W1∂X1
W 2 , B 1 , B 2 W_2,B_1,B_2 W2,B1,B2同理
简而言之,现在我们的每一层作为对象所能实现的方法不仅包括前向传播,还有反向传播。
让我们一层层来:
Affine层
这一层最需要理解的就是矩阵的求导,从简单的例子来看一下推导过程:
若输出值为一个常数 L L L,求 L L L关于一维数组 X X X的导数矩阵,则 ∂ L ∂ X = ( ∂ L ∂ x 1 , ∂ L ∂ x 2 , … , ∂ L ∂ x n ) \frac{\partial L}{\partial X}=(\frac{\partial L}{\partial x_1},\frac{\partial L}{\partial x_2},\dots,\frac{\partial L}{\partial x_n})