按照如下步骤运行代码:
• git clone https://github.com/omar-florez/scratch_mlp/
• python scratch_mlp/scratch_mlp.py
神经网络是线性模型和非线性模型的巧妙组合。选择并连接这些模型,便能获得一个强大的工具来逼近任何一个数学函数,比如一个能够借助非线性决策边界进行分类的神经网络模型。
接下来我们将以乐高积木为例,从零构建一个神经网络,并探究其内部功能。
神经网络就像是由乐高积木搭建而成
上图描述了训练一个神经网络常用的数学过程。一个神经网络就是很多模块以不同的目标堆叠而成。
- 输入变量X会传递进神经网络,它被存储在一个矩阵中,矩阵的行是观测值,列是维度
- 权重W_1将输入X映射到第一个隐藏层h_1,W_1相当于一个线性核
- Sigmoid函数使隐藏中的函数落到[0,1]区间,产生一个神经激活的数组,h_1=Sigmoid(WX)
但以上运算仅仅组成了一个线性系统,无法进行非线性建模。通过叠加网络层数,增加网络深度,可以学到微妙的非线性交互,从而解决更加复杂的问题,这也是神经网络模型备受推崇的原因之一。
了解神经网络内部结构有什么用?
如果你对神经网络内部结构有所了解,就能在遇到问题的时候知道如何做出调整,并且能够定制相应的策略来完成对算法的测试。
调试机器学习模型是一项复杂的任务。模型在初期可能并不奏效,在新数据上有着较低的准确率,耗费长时间进行训练,占用过多内存,甚至得到为NAN的预测值……在某些情况下,了解算法的运行机制能够让我们更加轻松的解决任务:
- 如果训练耗费了太多的时间,可以选择增加minibatch的大小,从而减小观测值的方差,有助于算法收敛
- 如果得到了NAN值,可能是由于梯度过大导致内存溢出。此时可以考虑降低学习率,减少网络层数或者进行梯度裁剪。
举个例子:学习异或函数
神经网络就像一个黑盒子,现在让我们打开它。从零开始构建一个学习异或函数的神经网络。非线性函数的选择并不是随机的,没有反向传播机制的话很难学会用一条直线分类。
网络拓扑很简单,如下:
- 输入变量X是一个二维向量
- 权重w_1是具有随机初始化数值的2×3矩阵
- 隐藏层包含3个神经元,每个神经元接受观测值的加权和作为输入。也就是下图中高亮部分的内积:z_1 = x_1, x_2
- 权重w_2是具有随机初始化值的3×2的矩阵
- 输出层h_2包含两个神经元,因为函数的输出要么是0(y_1 = [0,1]),要么是1(y_2 = [1, 0])
直接上图:
接下来训练模型。在上述示例中,可训练的参数就是权重。但是应该了解的是,目前的研究正在探索更多可以被优化的参数模型。例如层与层之间的连接,正则化,残差,学习率等。
反向传播是指:在给定的一批具有标签的观察值上,朝着预先定义的损失函数最小化的方向(梯度)更新权重。
网络初始化
随机初始化网络权重
前向传播
其目的主要是把输入变量X向前传递到网络的每一层,直到计算输出层h_2的向量。
以权重w_1为线性核对输入数据X做线性变换:
使用Sigmoid激活函数对加权和进行放缩,得到了第一个隐藏层h_1的值。此时,原始的2D向量被映射到了3D空间。
得到h_2的过程与之类似。接下来计算第一个隐藏层的加权和z_2,h_1现在是输入数据。
然后计算它们的Sigmoid激活函数。向量[0.37166596 0.45414264]代表的是网络对给定数据X计算出的对数概率或者预测向量。
计算整体损失
也就是计算实际值与预测值的差值。损失函数的目标是量化预测向量h_2与标签y,也就是真实值之间的距离,这个距离可以表示误差。
损失函数包括一个正则项,正则项的作用是对较大的权重进行惩罚。因为平方值较大的权重会增大损失函数,而我们的目标是要最小化损失函数。
反向传播
反向传播的目标是沿最小化损失函数的方向更新神经网络的权重。这是一个递归过程,它可以重用之前计算出来的梯度,而且严重依赖微分函数。因为这些更新可以减小损失函数,所以神经网络能够通过这个步骤[学会]逼近具有已知类别的观测值的标签。
与前向传播不同的是,这个步骤是沿着反向的顺序进行的。它首先计算出输出层中损失函数对每个权重的偏导数(dLoss/dW2),然后计算隐藏层的偏导数(dLoss/dW1)。
dLoss/dW2:
链式法则表明,我们可以将一个神经网络的梯度计算分解成多个微分部分:
下表列出了上面用到的一些函数定义以及它们的一阶导数:
Function | First derivative |
---|---|
Loss = (y-h2)^2 | dLoss/dW2 = -(y-h2) |
h2 = Sigmoid(z2) | dh2/dz2 = h2(1-h2) |
z2 = h1W2 | dz2/dW2 = h1 |
z2 = h1W2 | dz2/dh1 = W2 |
更直观地,我们在下图中沿着导数链计算三个偏导数,以更新权重w_2(图中蓝色部分)。
将数值代入到这些偏导数中,就能够计算出w_2的偏导数,如下所示:
最终的结果是一个3×2的矩阵dLoss/dW2,它将会沿着最小化损失函数的方向更新w_2的数值。
dLoss/dW1:
计算用于更新第一个隐藏层w_1权重的链式规则就展现了重复使用已有计算结果的可能。
更直观地,从输出层到权重w_1的计算会用到在后面层中已经计算过的偏导数。
例如,偏导数dLoss/dh_2和dh_2/dz_2在上一节中已经被计算为输出层dLoss/dw_2学习权值的依赖项。
将所有的导数放在一起,就能够再一次执行链式法则,从而更新隐藏层的权重w_1。
最后,给权重赋予新的数值,完成对神经网络的一步训练。
实现
仅使用numpy就可以将上面的数学方程式转换为代码。神经网络在一个循环中进行训练,每次迭代过程会给神经网络传入标准的输入数据。在本例中,我们只考虑每次迭代中的整个数据集。前向传播、损失函数和反向传播的计算都有比较好的泛化。因为我们在每一次循环中都用它们对应的梯度(矩阵dL_dw1和dL_dw2)来更新参数。
代码保存在github中:https://github.com/omar-florez/scratch_mlp
代码运行
下面展示了一些进行了多次迭代训练得到的能够近似异或函数的神经网络。
左图:准确率;中间的图:学习到的决策边界;右图:损失函数
首先,我们来看一下隐藏层具有3个神经元的神经网络为何能力较弱。这个模型学会了用一个简单的决策边界来进行二分类,这个边界一开始是一条直线,但是后来逐渐表现出了非线性的行为。随着训练的持续,右图的损失函数也明显地减小。
隐藏层拥有50个神经元的神经网络模型学习复杂决策边界的能力明显增强了。这虽然能够得到更准确的结果,但也会使梯度发生爆炸,这是训练神经网络时的一个显著问题。当梯度非常大的时候,反向传播中的连乘会产生很大的更新权重。这就是最后几步训练时损失函数突然增大的原因(step>90)。损失函数的正则项计算出了已经变得很大的权重的平方值(sum(W²)/2N)。
可以通过减小学习率来减轻上面的问题。通过实现一个能够随着时间减小学习率的策略来实现。或者通过强制执行一个更强的正则化来实现,可能是 L1 或者 L2。
本文由阿里云开发者社区组织翻译。
文章原标题《Explaining the Math of how neural networks learn》
作者:Omar U. Florez
译者:Elaine
文章为简译,更为详细的内容,请查看原文link