第2章
构建第一个神经网络
现在我们已经快速地对神经网络进行了复习,这是一个好的起点,为了不让大家抓狂,我们先编写一个非常简单的神经网络。我们将为几个函数搭建基本框架,以便你更好地了解将要使用的许多API的背后详情。我们将从头到尾完整地开发一个神经网络应用程序,以便你熟悉神经网络中包含的所有基本组件。这种实现并不完美或包罗万象,也不是必须这样实现。正如我提到的,这只为本书的其余章节提供一个框架。这是一个非常基本的神经网络,具有保存及加载网络和数据的功能。这将为你打下基础,让你能够编写自己的神经网络。
在本章中,我们将讨论以下主题:
- 神经网络训练
- 术语
- 突触
- 神经元
- 前向传播
- Sigmoid函数
- 后向传播
- 误差计算
技术要求
你需要在系统上安装Microsoft Visual Studio。
观看以下视频,以了解编码过程:http://bit.ly/2NYJa5G 。
2.1 一个简单的神经网络
我们首先展示简单神经网络的基本形式。它由带有2个输入的输入层、带有3个神经元(有时称为节点)的隐藏层和由单个神经元组成的最终输出层构成。当然,神经网络可以包含更多层(以及每层包含更多神经元),一旦深入学习,你会见到更多,但是现在这些已经足够了。请记住,如下标有N的节点都是一个单独的神经元—做一个不恰当的比喻,它们都具有自己的大脑,如图2-1所示。
我们将神经网络分解成三个基本部分:输入层、隐藏层和输出层。
输入层:这是网络的初始数据。对于每个输入,其输出到隐藏层的值是初始输入值。
隐藏层:这是网络的核心和灵魂,也是程序发挥魔力的根本。该层中的神经元为每个输入配置权重。这些权重随机设置初始值,并在网络训练时进行调整,以使神经元的输出更接近预期结果(如果幸运的话)。
输出层:这是神经网络在执行计算后得到的输出。简单案例中的输出将设置为true、false,或者on、off。神经元为每个输入配置权重,这些输入来自先前的隐藏层。虽然通常只有一个输出神经元,但如果需要或想要多个输出神经元,也可以设置更多神经元。
2.2 神经网络训练
如何训练神经网络?基本上,我们将提供一组输入数据以及我们期望看到的对应于输入的结果数据。然后,该数据将通过网络运行,直到神经网络了解了我们的目的。我们将训练、测试、训练、测试、训练、测试,直到神经网络了解了数据(或无法了解,但这是其他问题)。继续训练,直到满足一些指定的停止条件,例如误差率阈值。让我们来快速了解一下在训练神经网络时会用到的一些术语。
后向传播:数据通过网络运行后,将输出数据和预期的正确结果进行验证调整。我们通过在网络的每个隐藏层中后向传播(backprop或back propagation)来达到这一目的。最终的结果是,这会调整隐藏层中每个神经元输入的权重以及误差率。
完美情况下,每个后向传播层都应该使网络输出更接近我们期望的结果,并且使误差率越来越接近0。但误差率可能永远不会达到0,因此即使看起来差别不大,误差率0.0000001也可能超出我们的接受范围。
偏差:偏差允许我们修改函数,以便我们为网络中的每个神经元生成更好的输出。简而言之,偏差允许我们将激活函数值向左或向右偏移,而改变权重则会改变Sigmoid的陡度或垂直方向的偏移。
动量:动量仅将先前权重更新的一小部分添加到当前权重更新中。动量用于防止系统收敛于局部最小值而非全局最小值。高动量可帮助提高系统的收敛速度,然而,你必须小心,因为将此参数设置得太高会造成超出最小值的风险,导致系统不稳定。另一方面,过低的动量无法有效地避免局部最小值,并且会减慢系统的训练速度。因此,设置合适的动量值对于成功至关重要,并且你需要花费大量时间对其进行调参。
Sigmoid函数:激活函数定义每个神经元的输出。Sigmoid函数可能是最常用的激活函数,它将输入转换为介于0到1之间的值,用于生成初始权重。典型的Sigmoid函数能够接收输入值,并从该值生成输出值和相应的导数。
学习率:学习率通过控制权重大小和学习过程中网络的偏差变化来改变系统的整体学习速度。
我们已经掌握了这些术语,现在开始深入研究代码。本书使用的是Visual Studio的Community版本,但你可以使用任何喜欢的版本。
需要的话,可以随意下载该软件进行试验并修改。你可以用神经网络做任何你喜欢或想做的事情,我们为你提供了源代码。眼见为虚,动手为实。开始实践吧,学习这些优秀的开源贡献者为我们提供的代码!请记住,这个神经网络只是为了让你对自己编写的东西有所了解,并教你一些关于神经网络的基础知识。
从一些简短的代码片段开始,这些代码片段将为本章的其余部分奠定基础。首先从一个称为突触的小东西开始,它将一个神经元连接到另一个神经元。接下来编写单个神经元代码,最后讨论前向和后向传播以及这两种传播对我们的意义。为了便于理解,我们展示其中的一些代码片段。
2.2.1 突触
你可能会问,什么是突触?简而言之,它是一种将一个神经元连接到另一个神经元,以及容纳权重和权重增量的容器,如下所示:
2.2.2 神经元
我们已经讨论过神经元,现在是时候用代码来呈现它了,以便开发人员更好地理解!如你所见,我们有输入和输出突触、偏差、偏差增量、梯度,以及神经元的实际值。神经元计算其输入的加权和,加上偏差,然后决定输出是否应该“启动”—置于“激活”状态:
2.2.3 前向传播
以下是基本的前向传播过程的代码:
要实现ForwardPropagation,基本上要对每个突触的所有输入求和,并通过运行Sigmoid函数得到运行结果以获得输出。CalculateValue函数为我们执行此操作。
2.2.4 Sigmoid函数
Sigmoid函数是一个激活函数,正如我们之前所说,它也许是当今使用最广泛的函数之一。图2-2是Sigmoid函数的形状(回忆关于激活函数的部分)。它唯一的目的(非常抽象地说)是将来自外边缘的值处理成接近0和1,而不必担心大于此值。这样可以处理某些值过大的情况。
你可能会问,C#代码中的Sigmoid函数是什么样子的?如下所示:
Sigmoid类提供输出和求导两个函数。
2.2.5 后向传播
对于后向传播来说,首先我们将计算输出层的梯度,然后让这些值通过隐藏层(反转在前向传播中的方向),更新权重,最后让值通过输出层,如下:
2.2.6 计算误差
我们采取实际值减去神经网络预测值的方法来计算误差。误差越接近0,就越好。请注意,以下误差计算几乎不可能达到0,只有很小的概率得到0:
2.2.7 计算梯度
通过Sigmoid函数的导数来计算梯度:
2.2.8 更新权重
我们用学习率乘以梯度的方式来更新权重,然后加上动量并乘以先前的增量。最后通过运行每个输入突触计算最终值:
2.2.9 计算值
为了计算值,我们从Sigmoid函数中获取输出并加上偏差值:
2.3 神经网络函数
以下是我们要开发的函数,它们奠定了神经网络的基础:
- 创建新网络
- 导入网络
- 手动输入用户数据
- 导入数据集
- 训练网络
- 测试网络
有了这些函数,让我们开始编码吧!
2.3.1 创建新网络
创建新网络的代码如下:
注意该函数中的返回值。这是一个流畅的界面,意味着我们可以将各种函数链接在一起形成一条语句。与传统的界面相比,许多人更喜欢这种类型的界面,但你可以随意地修改代码。以下是一个流畅界面的示例。信不信由你,这是一个完整的神经网络:
2.3.2 导入现有网络
此函数允许我们导入以前保存的网络。再次强调,请注意返回值,它是形成流畅界面的关键:
获取以前保存的网络的文件名。打开后,将其反序列化为我们将要处理的网络结构(如果由于某种原因无效,请中止该操作!):
创建一个新网络和要填充的神经元列表:
复制之前保存的学习率和动量:
从导入的网络数据创建输入层:
从导入的网络数据创建隐藏层:
从导入的数据创建输出层神经元:
最后,创建将所有内容联系在一起的突触:
以下是我们手动输入的数据,以供神经网络使用:
2.3.3 导入数据集
以下是导入数据集的方式:
反序列化数据并返回:
2.3.4 网络运算
为了测试网络,我们做了一个简单的前向和后向传播运算,描述如下:
进行前向传播运算,如下所示:
得到并返回运算结果,如下所示:
2.3.5 导出网络
导出当前的网络信息,如下所示:
2.3.6 训练网络
有两种训练网络的方法。一种是最小误差值法,另一种是最大误差值法。这两个函数都有默认值,但你可以为训练设置不同的阈值,如下所示:
在前面的两个函数定义中,调用神经网络Train函数来执行实际训练。该函数在训练循环的每次迭代中依次为每个数据集调用前向和后向传播函数,如下所示:
2.3.7 测试网络
此函数允许我们测试神经网络。再次注意返回值,它是构成流畅界面的关键。对于更高、更抽象层中最常用的函数,我将把函数写得更加流畅通用,如下所示:
从用户获取输入数据,如下所示:
进行计算,如下所示:
打印出结果,如下所示:
2.3.8 计算前向传播
此函数用于根据提供的值,计算前向传播值,如下所示:
2.3.9 将网络导出为JSON格式
此函数用于导出网络。对我们来说,导出意味着将数据序列化为可以读懂的JSON格式,如下所示:
2.3.10 导出数据集
此函数用于导出数据集信息。与导出网络一样,将以可以读懂的JSON格式完成:
2.4 神经网络
在编写了许多辅助但重要的函数之后,现在我们将注意力转向神经网络的核心部分,即网络本身。在神经网络中,网络部分是一个包罗万象的宇宙,一切都蕴含其中。在此结构中,我们需要存储神经元的输入层、输出层和隐藏层,以及学习率和动量,如下所示:
神经元连接
每个神经元必须连接到其他神经元,神经元构造函数将处理所有输入神经元与突触的连接,如下所示:
2.5 例子
我们已经创建了代码,现在通过示例来了解它是如何使用的。
2.5.1 训练到最小值
在这个例子中,将使用我们编写的代码来训练网络,让其达到最小值或阈值,如图2-3所示。在每一步中,网络都会提示输入正确的数据,从而为我们节省应对数据交互操作的时间。在生产过程中,你可能希望在没有任何用户干预的情况下传递参数,以把其作为服务或微服务运行。
2.5.2 训练到最大值
在这个例子中,我们将网络训练到最大值,而非最小值,如图2-4所示。我们手动输入希望使用的数据,以及预期的结果。然后完成训练。完成后,我们输入测试数据并测试神经网络效果。
2.6 小结
在本章中,我们学习了如何从头开始编写完整的神经网络。没有进一步深入讲解具体细节,但已经涵盖了重要的基础知识,而且我们用纯C#代码实现了相关内容。现在我们应该比刚开始时更好地理解了神经网络的概念以及含义。
下一章我们将开始探索更复杂的网络结构,例如循环和卷积神经网络。还有很多内容要讲述,保持良好的编码状态!