【导读】本文为大家带来了一篇纯手工神经网络实现教程,在不依赖主流框架的情况下,通过numpy实现基本的神经网络功能,帮助大家更好的理解神经网络的实现过程。
https://towardsdatascience.com/lets-code-a-neural-network-in-plain-numpy-ae7e74410795
介绍:
使用像Keras,TensorFlow或PyTorch这样的高级框架,能让我们快速地构建非常复杂的模型。然而,理解内部原理和基本概念对初学者来说同样重要。这篇文章将从实践出发,尝试利用我们的知识,仅用Numpy来构建一个完全可操作的神经网络。本文的最后,将会在一个简单的分类问题上测试模型,并将它的表现和Keras构建的神经网络作比较。
注意:本文包含大量用Python编写的代码片段,源代码参考GitHub仓库:
https://github.com/SkalskiP/ILearnDeepLearning.py。
图1. 神经网络结构样例
事前准备:
在开始编程之前,让我们先准备一个路线图。我们的目标是创建一个程序,能够创建具有指定结构(层的数量和大小以及适当的激活函数)的密集连接的神经网络。图1就是这种神经网络的一个例子。最重要的是,我们必须要能够训练我们的神经网络并使用它进行预测。
图2.神经网络蓝图
上图展示了在训练神经网络的过程中会执行哪些操作,同时也展示了在一次迭代的不同阶段,我们要更新和读取多少个参数。如何构建正确的数据结构,以及巧妙地管理数据状态,是我们训练神经网络过程中最困难的任务之一。这里我们将不会详细描述上图每个参数具体的变化细节。
图3.第l层的权重矩阵W和偏置向量b的维度
神经网络层的初始化:
我们从初始化每一层的权重矩阵W和偏置向量b开始。以图3为参考,初始化W和b。上标[l]表示当前层的索引(从1开始计数),值n表示给定层中的单元索引。假设我们NN的表示信息将以类似于代码段1中所示的列表的形式传递给我们的程序。列表中的每个元素都是一个描述单个神经网络层基础参数的字典:input_dim提供给神经网络层,作为输入向量的维度,output_dim为神经网络层经过激活函数输出的向量维度,activation是在层内使用的激活函数。
代码段1.神经网络结构
如果你对这部分内容很熟悉,你可能会有一些疑问:上面的一些参数是不是非必要的?没错,你的想法是正确的,一层的输出向量也是下一层的输入,因此只要知道其中一个向量的大小就够了。但是,我们故意使用以下表示,来保持所有层中的对象一致,以便让第一次接触的人更容易理解代码。
代码段2.初始化权重矩阵和偏置向量的函数
请大家来关注,我们在这一部分必须完成的主要任务 —— 参数的初始化。已经看过代码段2,并且对Numpy有使用经验的读者,应该注意到了,矩阵W和向量b被小随机数填充。这种方法并非偶然,权值不能用相同的数字初始化,否则会带来破环对称性的问题。基本上,如果权值是相同的话,不管输入X是什么,隐含层中的所有单元都会是相同的。某种程度上,无论我们训练模型多久,以及我们的网络有多深,我们都会陷入初始状态而没有任何下降的希望。
使用较小的值可在第一次迭代期间提高算法的效率。看一下sigmoid函数的图像,如图4所示,我们可以看到它对于大的输入值,输出值几乎不变,这对我们NN的学习速度有显著影响。总而言之,使用小随机数初始化参数是一种简单的方法,但它保证了我们的算法有足够好的起点。准备好的参数值将存储在python字典中,其中包含主键,以标识其母层。函数末尾将字典作为返回值,我们可以在算法的下一阶段调用其内容。
图4.算法中使用的激活函数
激活函数:
在我们将要使用的所有函数中,有一些非常简单但功能强大。激活函数仅仅用一行代码就能编写,但它们能为神经网络提供所需的非线性和表达能力。没有激活函数,我们的神经网络将变成线性函数的组合。激活函数有很多,但在本文中我们只会用到其中两个-sigmod和ReLU。为了能够完整循环并通过前向和后向传播,我们还必须准备他们的衍生函数。
代码段3.ReLU和Sigmoid函数和他们的衍生函数
前向传播:
设计好的神经网络有一个简单的结构。信息在一个方向上流动,它以X矩阵的形式传递,经过隐含层的神经元,产生预测向量Y_hat。为了便于阅读,我们将前向传播分为两个独立的函数——向前传播一层和穿过整个NN。
代码段4.单层前向传播
这部分代码可能是最简单易懂的。给定来自前一层的输入,我们计算仿射变换Z,然后应用所选择的激活函数。通过使用NumPy,我们可以同时利用向量化——执行矩阵运算,对整个层和整批输入数据进行操作。这消除了迭代,并加快了我们的计算速度。除了计算得到的矩阵A,我们的函数还返回一个中间值Z。如图2所示。反向传播中我们需要用到Z。
图5.单层前向传播的参数矩阵维度
使用事先准备好的单层前向传播函数,我们可以很容易的构建出整个前向传播结构。这是一个稍微复杂的函数,它的功能不仅是执行预测,还组织存储中间值。它返回Python字典,其中包含为特定层计算的A值和Z值。
代码段5.前向传播整体结构
损失函数:
为了监控我们的进度,并确保我们朝着正确的方向前进,我们应该定期计算损失函数的值。“一般来说,损失函数旨在显示我们与'理想'解决方案的距离。”它是根据我们计划解决的问题选择的,像Keras这样的框架有很多损失函数可供选择。因为我打算测试我们的NN以便对点进行二分类,所以我决定使用交叉熵,它由以下公式定义。为了获得有关学习过程的更多信息,我还决定实现一个能够计算准确性的函数。
代码段6.计算损失函数和准确率
反向传播:
不幸的是,许多缺乏经验的深度学习爱好者认为,后向传播是一种令人生畏和难以理解的算法。微分和微分代数的组合,经常会阻碍那些没有扎实的数学基础的人。所以如果你现在不能理解,不要太担心。相信我,我们都经历过同样的阶段。
代码段7.单层反向传播
通常人们会将反向传播与梯度下降混淆,但事实上这些是两个独立的问题。第一个目的是有效地计算梯度,而第二个目的是使用计算的梯度进行优化。在NN中,我们计算关于参数的损失函数的梯度(前面讨论过),但是反向传播可以用于计算任何函数的导数。这种算法的本质是递归使用微积分的链式法则计算导数。该过程对于一个神经网络层由以下公式描述。由于本文主要关注实际实现,将省略推导。
图6.单层的前向和后向传播
就像前向传播一样,我决定将计算分成两个独立的函数。第一个函数在代码段7中展示——专注于单层,并用NumPy中重写了上面的公式。第二个代表完全后向传播,主要任务是读取和更新三个字典的值。我们首先计算损失函数相对于预测向量的导数,即前向传播的结果。这非常简单,因为它只需要重写以下公式,然后从末尾开始遍历神经网络层,并根据图6所示的图计算所有参数的导数。最后,函数返回一个包含我们需要的动态的python字典。
代码段8.完全后向传播
更新参数:
该函数的目标是使用梯度优化更新网络的参数。我们试图通过这种方式让损失函数更接近最小值。为完成此任务,我们将使用两个字典作为函数参数:params_values,用于存储参数的当前值;;grads_values,用于存储针对这些参数计算的损失函数导数。现在,你只需要在每层中应用以下等式即可,这是一个非常简单的优化算法,也是更高级优化器的一个很好的起点。
代码段9.使用梯度下降更新参数
整合代码:
我们终于准备好了。我们已经准备好了所有必要的代码,现在只需要按照正确的顺序将它们组合在一起。为了更好地理解操作顺序,可以再次查看图2,函数将返回经过训练优化后的权值,和在训练期间准确率的历史变化。为了进行预测,只需使用更新后的权重矩阵和一组测试数据运行完整的前向传播过程即可。
代码段10.训练模型
Numpy vs Keras:
现在是时候看看我们的模型能否解决一个简单的分类问题,我们生成了一个分属两个类的点组成的数据集,如图7所示,同时尝试训练模型来对这些点进行分类。为了比较,还准备了一个基于Keras的模型。两种模型都具有相同的结构和学习率。尽管如此,这场竞争仍然非常不公平,因为我们的事先准备非常简单,但即便如此,NumPy和Keras模型在测试集上都实现了95%左右的准确率,虽然Numpy所花费的时间要多出几十倍来。
图7.测试数据集
声明
领取专属 10元无门槛券
私享最新 技术干货