Visual Basic .NET 中的神经网络基础






4.96/5 (18投票s)
在 VB.NET 中实现神经网络的基础知识
范围
在本文(希望是这个小型系列的第一篇)中,我们将探讨如何在 Visual Basic .NET 中实现一个神经网络,即一个能够处理输入数据并调整其内部机制以学会产生期望结果的模型。稍后我们会对此有更多了解。本文将侧重于神经网络及其行为的通用定义,并提供一个简单的实现供读者测试。在最后一节,我们将编写一个能够交换两个变量的小型网络。
引言
第一个定义
“神经网络”一词通常用于指代由神经元组成的网络或电路。我们可以区分两种类型的神经网络:a)生物神经网络和 b)人工神经网络。显然,在软件开发中,我们这里指的是人工神经网络,但这类实现其基本模型和灵感来源于其自然对应物,因此简要考虑我们所说的生物神经网络的功能可能是有益的。
自然神经网络
这些是由生物神经元组成的网络,是生物体典型的特征。神经元/细胞互连在外周神经系统或中枢神经系统中。在神经科学中,神经元群通过它们执行的生理功能来识别。
人工神经网络
人工神经网络是数学模型,可以通过电子介质实现,它们模仿生物神经网络的功能。简单来说,我们将有一组能够解决人工智能领域特定问题的“人工神经元”。像自然神经元一样,人工神经网络可以“学习”,通过时间和尝试,了解问题的本质,从而在解决问题方面变得越来越高效。
神经元
在进行这个简单的铺垫之后,应该很清楚,在一个网络中,无论是自然的还是人工的,被称为“神经元”的实体都具有至关重要的重要性,因为它接收输入,并且在某种程度上负责正确的数据处理,最终产生结果。想想我们的大脑:它是一个由 860 亿个神经元(或多或少)组成的奇妙超级计算机。这些实体数量惊人,它们不断地交换和存储信息,运行着 10^14 个突触。正如我们所说,人工模型正试图捕捉和复制神经元的基本功能,它基于三个主要部分
- 细胞体(Soma)或细胞主体
- 轴突(Axon),神经元的输出线
- 树突(Dendrite),神经元的输入线,通过突触从其他轴突接收数据
细胞体对输入信号进行加权求和,并检查它们是否超过某个阈值。如果超过,神经元会激活自身(产生动作电位),否则保持静默状态。人工模型试图模仿这些子部分,目标是创建一个互连实体的数组,该数组能够根据接收到的输入进行自我调整,并不断将产生的结果与预期情况进行比较。
网络如何学习
通常,神经网络理论确定了三种主要方法,通过这些方法网络可以学习(其中,“学习”我们现在意指——神经网络修改自身以能够对给定输入产生特定结果的过程)。关于 Visual Basic 的实现,我们将只关注其中一种,但介绍所有范式是有用的,以便更好地了解全局。为了使神经网络(NN)能够学习,它必须被“训练”。如果拥有一组由输入值和输出值组成的数据集,训练可以是监督的。通过这些数据,网络可以学会推断一个神经元与其他神经元之间的关系。另一种方法是无监督学习,它基于训练算法,仅依靠输入数据来修改网络的权重,从而产生能够通过概率方法对接收到的信息进行分组的网络。最后一种方法是强化学习,它不依赖于呈现的数据,而是依赖于探索算法,这些算法产生输入,然后由一个代理进行测试,该代理将检查这些输入对网络的影响,试图确定神经网络在给定问题上的性能。在本文中,当我们开始编码时,我们将看到第一个介绍的情况,即监督训练。
监督训练
那么,让我们更近距离地考察一下这种方法。用监督方式训练神经网络意味着什么?正如我们所说,这主要涉及呈现一组输入和输出数据。假设我们想教我们的网络对两个数字进行求和。在这种情况下,遵循监督训练范式,我们必须向网络提供输入数据(例如 [1;5]),并告诉它我们期望的结果(在本例中为 [6])。然后,必须应用一个特定的算法来评估网络的当前状态,通过处理我们的输入和输出数据来调整它。我们将在示例中使用的算法称为反向传播。
反向传播
误差反向传播是一种技术,在这种技术中,我们首先初始化我们的网络(通常是神经元权重的随机值),然后转发我们的输入数据,将结果与我们期望的输出数据进行匹配。然后,我们计算获得的值与期望值之间的偏差,得到一个 delta 因子,该因子必须反向传播到我们的神经元,以根据我们计算的误差量调整它们的初始状态。通过反复试验和重复,向网络呈现多组输入和输出数据,每次都重复真实值和理想值之间的匹配。在一定时间内,这种操作将产生越来越精确的输出,校准网络每个组件的权重,并最终完善其处理接收数据的能力。有关反向传播的详细解释,请参阅参考文献部分。
创建神经网络
现在我们已经了解了关于神经网络的一些初步概念,我们应该能够开发一个响应讨论范式的模型。在不深入进行过多的数学解释(除非你想更好地理解我们将要看到的内容,否则这些解释并非真正需要)的情况下,我们将一步步地编写一个简单而功能齐全的神经网络,并在完成后进行测试。我们需要考虑的第一件事是神经网络的结构:我们知道它由神经元组织而成,神经元本身是相互连接的。但我们不知道如何连接。
图层
这正是层(layers)发挥作用的地方。层是共享或多或少共同功能的神经元组。例如,考虑输入数据的入口点。这将是输入层,其中包含一组共享通用功能的神经元,在这种情况下,功能仅限于接收和转发信息。然后我们肯定会有一个输出层,它将包含接收先前处理结果的神经元。在这些层之间可能存在许多层,通常称为“隐藏层”,因为用户无法直接访问它们。这些隐藏层的数量以及每个隐藏层包含的神经元数量,很大程度上取决于我们要解决问题的性质和复杂性。总而言之,每个网络将由层组成,每层将包含一定数量的预定神经元。
神经元和树突
从“人工”角度来看,我们可以将神经元构想成一个暴露某个值的实体,该值通过迭代调整,并通过树突与其他神经元绑定,在我们的例子中,树突将由具有初始随机权重的子实体表示。训练过程将包括向输入层神经元输入数据,这些神经元通过树突将其值传递到上一层,上一层将执行相同的操作,直到达到输出层。最后,我们计算当前输出与期望输出之间的差值(delta),然后沿着网络回溯,调整树突权重、神经元值以及每个偏差值,以纠正网络本身。然后,我们开始另一轮训练。
准备网络类
在了解了网络的结构之后,我们可以勾画出一些类来管理网络的各种实体。在接下来的代码片段中,我将概述 Dendrite、Neuron 和 Layer 类,我们将在实现 NeuralNetwork
类时一起使用它们。
Dendrite 类
Public Class Dendrite
Dim _weight As Double
Property Weight As Double
Get
Return _weight
End Get
Set(value As Double)
_weight = value
End Set
End Property
Public Sub New()
Me.Weight = r.NextDouble()
End Sub
End Class
首先是 Dendrite
类:如你所见,它只包含一个名为 Weight
的属性。在初始化 Dendrite
时,会给我们的 dendrite
分配一个随机的 Weight
。Weight
属性的类型是 Double
,因为我们的输入值将在零和一之间,所以在小数位数方面我们需要很高的精度。稍后会详细介绍。这个类不需要其他属性或函数。
Neuron 类
Public Class Neuron
Dim _dendrites As New List(Of Dendrite)
Dim _dendriteCount As Integer
Dim _bias As Double
Dim _value As Double
Dim _delta As Double
Public Property Dendrites As List(Of Dendrite)
Get
Return _dendrites
End Get
Set(value As List(Of Dendrite))
_dendrites = value
End Set
End Property
Public Property Bias As Double
Get
Return _bias
End Get
Set(value As Double)
_bias = value
End Set
End Property
Public Property Value As Double
Get
Return _value
End Get
Set(value As Double)
_value = value
End Set
End Property
Public Property Delta As Double
Get
Return _delta
End Get
Set(value As Double)
_delta = value
End Set
End Property
Public ReadOnly Property DendriteCount As Integer
Get
Return _dendrites.Count
End Get
End Property
Public Sub New()
Me.Bias = r.NextDouble()
End Sub
End Class
接下来是 Neuron
类。正如你可以想象的,它将暴露一个 Value
属性(类型为 Double
,原因与上面相同),以及一系列潜在的 Dendrite
s,其数量将取决于我们当前神经元所连接的层的神经元数量。因此,我们有一个 Dendrite
属性,一个 DendriteCount
(返回 Dendrite
s 的数量),以及两个将在重新校准过程中使用的属性,即 Bias
和 Delta
。
Layer 类
Public Class Layer
Dim _neurons As New List(Of Neuron)
Dim _neuronCount As Integer
Public Property Neurons As List(Of Neuron)
Get
Return _neurons
End Get
Set(value As List(Of Neuron))
_neurons = value
End Set
End Property
Public ReadOnly Property NeuronCount As Integer
Get
Return _neurons.Count
End Get
End Property
Public Sub New(neuronNum As Integer)
_neuronCount = neuronNum
End Sub
End Class
最后是 Layer
类,它只是一个神经元数组的容器。在调用 New
方法时,用户必须指明该层需要有多少个神经元。我们将在下一节中介绍这些类如何在完整的神经网络中进行交互。
NeuralNetwork 类
我们的 NeuralNetwork
可以被看作是层的一个列表(每个层将继承底层的层属性,即神经元和树突)。神经网络必须被启动(或置于运行状态)并进行训练,所以我们可能会有两个可用于此目的的方法。网络初始化除了其他属性外,还必须指定一个我们称之为“学习率”的参数。这将是我们用于权重重新计算的变量。顾名思义,学习率是一个决定网络学习速度的因素。由于它是一个纠正因子,学习率必须准确选择:如果它的值太高,但存在大量的可能输入,网络可能学不好,甚至学不会。总的来说,一个好的做法是将学习率设置为一个相对小的值,如果网络的有效重新校准变得太慢,则增加它。
让我们来看一个几乎完整的 NeuralNetwork
类
Public Class NeuralNetwork
Dim _layers As New List(Of Layer)
Dim _learningRate As Double
Public Property Layers As List(Of Layer)
Get
Return _layers
End Get
Set(value As List(Of Layer))
_layers = value
End Set
End Property
Public Property LearningRate As Double
Get
Return _learningRate
End Get
Set(value As Double)
_learningRate = value
End Set
End Property
Public ReadOnly Property LayerCount As Integer
Get
Return _layers.Count
End Get
End Property
Sub New(LearningRate As Double, nLayers As List(Of Integer))
If nLayers.Count < 2 Then Exit Sub
Me.LearningRate = LearningRate
For ii As Integer = 0 To nLayers.Count - 1
Dim l As Layer = New Layer(nLayers(ii) - 1)
Me.Layers.Add(l)
For jj As Integer = 0 To nLayers(ii) - 1
l.Neurons.Add(New Neuron())
Next
For Each n As Neuron In l.Neurons
If ii = 0 Then n.Bias = 0
If ii > 0 Then
For k As Integer = 0 To nLayers(ii - 1) - 1
n.Dendrites.Add(New Dendrite)
Next
End If
Next
Next
End Sub
Function Execute(inputs As List(Of Double)) As List(Of Double)
If inputs.Count <> Me.Layers(0).NeuronCount Then
Return Nothing
End If
For ii As Integer = 0 To Me.LayerCount - 1
Dim curLayer As Layer = Me.Layers(ii)
For jj As Integer = 0 To curLayer.NeuronCount - 1
Dim curNeuron As Neuron = curLayer.Neurons(jj)
If ii = 0 Then
curNeuron.Value = inputs(jj)
Else
curNeuron.Value = 0
For k = 0 To Me.Layers(ii - 1).NeuronCount - 1
curNeuron.Value = curNeuron.Value + _
Me.Layers(ii - 1).Neurons(k).Value * curNeuron.Dendrites(k).Weight
Next k
curNeuron.Value = Sigmoid(curNeuron.Value + curNeuron.Bias)
End If
Next
Next
Dim outputs As New List(Of Double)
Dim la As Layer = Me.Layers(Me.LayerCount - 1)
For ii As Integer = 0 To la.NeuronCount - 1
outputs.Add(la.Neurons(ii).Value)
Next
Return outputs
End Function
Public Function Train(inputs As List(Of Double), outputs As List(Of Double)) As Boolean
If inputs.Count <> Me.Layers(0).NeuronCount Or _
outputs.Count <> Me.Layers(Me.LayerCount - 1).NeuronCount Then
Return False
End If
Execute(inputs)
For ii = 0 To Me.Layers(Me.LayerCount - 1).NeuronCount - 1
Dim curNeuron As Neuron = Me.Layers(Me.LayerCount - 1).Neurons(ii)
curNeuron.Delta = curNeuron.Value * (1 - curNeuron.Value) * _
(outputs(ii) - curNeuron.Value)
For jj = Me.LayerCount - 2 To 1 Step -1
For kk = 0 To Me.Layers(jj).NeuronCount - 1
Dim iNeuron As Neuron = Me.Layers(jj).Neurons(kk)
iNeuron.Delta = iNeuron.Value *
(1 - iNeuron.Value) * _
Me.Layers(jj + 1).Neurons(ii).Dendrites(kk).Weight *
Me.Layers(jj + 1).Neurons(ii).Delta
Next kk
Next jj
Next ii
For ii = Me.LayerCount - 1 To 0 Step -1
For jj = 0 To Me.Layers(ii).NeuronCount - 1
Dim iNeuron As Neuron = Me.Layers(ii).Neurons(jj)
iNeuron.Bias = iNeuron.Bias + (Me.LearningRate * iNeuron.Delta)
For kk = 0 To iNeuron.DendriteCount - 1
iNeuron.Dendrites(kk).Weight = iNeuron.Dendrites(kk).Weight + _
(Me.LearningRate * Me.Layers(ii - 1).Neurons(kk).Value * iNeuron.Delta)
Next kk
Next jj
Next ii
Return True
End Function
End Class
New() 方法
当我们的网络初始化时,它需要一个学习率参数和一层列表。处理这个列表,你可以看到每一层是如何生成神经元和树突的,这些神经元和树突被分配给它们各自的父级。调用神经元和树突的 New()
方法,将导致它们的初始值和权重的随机分配。如果传入的层数少于两层,子程序将退出,因为神经网络至少必须有输入层和输出层。
Sub New(LearningRate As Double, nLayers As List(Of Integer))
If nLayers.Count < 2 Then Exit Sub
Me.LearningRate = LearningRate
For ii As Integer = 0 To nLayers.Count - 1
Dim l As Layer = New Layer(nLayers(ii) - 1)
Me.Layers.Add(l)
For jj As Integer = 0 To nLayers(ii) - 1
l.Neurons.Add(New Neuron())
Next
For Each n As Neuron In l.Neurons
If ii = 0 Then n.Bias = 0
If ii > 0 Then
For k As Integer = 0 To nLayers(ii - 1) - 1
n.Dendrites.Add(New Dendrite)
Next
End If
Next
Next
End Sub
Execute() 函数
正如我们所说,我们的网络必须有一个函数,通过这个函数我们可以处理输入数据,使其在网络中移动,并收集最终结果。下面的函数就是这样做的。首先,我们将检查输入的正确性:如果输入的数量与输入层神经元的数量不同,则无法执行该函数。每个神经元都必须被初始化。对于第一层,即输入层,我们只需将输入值分配给神经元的 Value
属性。对于其他层,我们计算一个加权和,由当前神经元的 Value
加上前一层神经元的 Value
乘以 dendrite
的权重得到。最后,我们对计算出的 Value
应用 Sigmoid 函数,我们将在下面进行分析。处理所有层后,我们的输出层神经元将接收一个结果,该结果是函数将以 List(Of Double)
的形式返回的参数。
Function Execute(inputs As List(Of Double)) As List(Of Double)
If inputs.Count <> Me.Layers(0).NeuronCount Then
Return Nothing
End If
For ii As Integer = 0 To Me.LayerCount - 1
Dim curLayer As Layer = Me.Layers(ii)
For jj As Integer = 0 To curLayer.NeuronCount - 1
Dim curNeuron As Neuron = curLayer.Neurons(jj)
If ii = 0 Then
curNeuron.Value = inputs(jj)
Else
curNeuron.Value = 0
For k = 0 To Me.Layers(ii - 1).NeuronCount - 1
curNeuron.Value = curNeuron.Value + _
Me.Layers(ii - 1).Neurons(k).Value * curNeuron.Dendrites(k).Weight
Next k
curNeuron.Value = Sigmoid(curNeuron.Value + curNeuron.Bias)
End If
Next
Next
Dim outputs As New List(Of Double)
Dim la As Layer = Me.Layers(Me.LayerCount - 1)
For ii As Integer = 0 To la.NeuronCount - 1
outputs.Add(la.Neurons(ii).Value)
Next
Return outputs
End Function
Sigmoid() 函数
Sigmoid 函数是一种数学函数,以其典型的“S”形而闻名。它对每个实数输入值都有定义。我们在神经网络编程中使用这种函数,是因为它的可微性——这是反向传播的要求——并且因为它引入了非线性到我们的网络中(或者说,它使我们的网络能够学习不产生线性组合的输入之间的相关性)。此外,对于每个实数值,Sigmoid 函数都返回一个介于零和一之间(不包括上限)的值。该函数具有使其在反向传播方面非常适合的特性。
Train() 函数
网络以随机值初始化,所以——如果没有重新校准——它们返回的结果本身也相当随机,或多或少是如此。如果没有训练过程,一个刚启动的网络几乎是无用的。我们用“训练”一词定义了神经网络在给定输入集上持续运行的过程,并且其结果不断与预期输出集进行匹配。当我们发现输出与网络返回的值之间存在差异时,我们便会重新校准网络本身的每个权重和值,从而使我们想要的和网络得到的结果越来越接近。
在 VB 代码中,类似于这样
Public Function Train(inputs As List(Of Double), outputs As List(Of Double)) As Boolean
If inputs.Count <> Me.Layers(0).NeuronCount Or outputs.Count <> _
Me.Layers(Me.LayerCount - 1).NeuronCount Then
Return False
End If
Execute(inputs)
For ii = 0 To Me.Layers(Me.LayerCount - 1).NeuronCount - 1
Dim curNeuron As Neuron = Me.Layers(Me.LayerCount - 1).Neurons(ii)
curNeuron.Delta = curNeuron.Value * (1 - curNeuron.Value) * _
(outputs(ii) - curNeuron.Value)
For jj = Me.LayerCount - 2 To 1 Step -1
For kk = 0 To Me.Layers(jj).NeuronCount - 1
Dim iNeuron As Neuron = Me.Layers(jj).Neurons(kk)
iNeuron.Delta = iNeuron.Value *
(1 - iNeuron.Value) * _
Me.Layers(jj + 1).Neurons(ii).Dendrites(kk).Weight *
Me.Layers(jj + 1).Neurons(ii).Delta
Next kk
Next jj
Next ii
For ii = Me.LayerCount - 1 To 0 Step -1
For jj = 0 To Me.Layers(ii).NeuronCount - 1
Dim iNeuron As Neuron = Me.Layers(ii).Neurons(jj)
iNeuron.Bias = iNeuron.Bias + (Me.LearningRate * iNeuron.Delta)
For kk = 0 To iNeuron.DendriteCount - 1
iNeuron.Dendrites(kk).Weight = iNeuron.Dendrites(kk).Weight + _
(Me.LearningRate * Me.Layers(ii - 1).Neurons(kk).Value * iNeuron.Delta)
Next kk
Next jj
Next ii
Return True
End Function
和往常一样,我们检查输入的正确性,然后通过调用 Execute()
方法启动我们的网络。然后,从最后一层开始,我们处理每个神经元和树突,通过应用输出差值来纠正每个值。对树突权重也将做同样的事情,引入网络的学习率,如上所述。在一个训练回合结束时(或者更现实地说,在完成数百个回合后),我们将开始观察到网络输出将变得越来越精确。
一个测试应用程序
在本节中,我们将看到一个简单的测试应用程序,用于创建一个相当琐碎的网络,用于交换两个变量。在接下来的内容中,我们将考虑一个包含三层的网络:一个由两个神经元组成的输入层,一个由四个神经元组成的隐藏层,以及一个由两个神经元组成的输出层。如果我们给予足够的监督训练,我们期望我们的网络能够交换我们呈现的值,换句话说,能够将输入神经元 #1 的值转移到输出神经元 #2,反之亦然。
在可下载的 NeuralNetwork
类中,您会发现我们未在文章中分析的几个方法,它们涉及图形和文本网络渲染。我们将在示例中使用它们,您可以通过下载源代码来查阅它们。
初始化网络
使用我们的类,可以通过声明网络并为其提供学习率和起始层来简单地初始化神经网络。例如,在测试应用程序中,您可以看到这样的代码片段
Dim network As NeuralNetwork
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim layerList As New List(Of Integer)
With layerList
.Add(2)
.Add(4)
.Add(2)
End With
network = New NeuralNetwork(21.5, layerList)
End Sub
我将我的网络定义为 Form
的全局变量,然后——在 Load()
事件中——我创建了所需的层,并将它们传递给网络初始化器。这将导致我们的层正确填充指定的神经元数量,每个元素都以随机值、delta、bias 和权重开始。
运行网络
运行和训练网络也是非常简单的过程。它们都只需要调用适当的方法,并传递一组输入和/或输出参数。例如,对于 Execute()
方法,我们可以这样写
Dim inputs As New List(Of Double)
inputs.Add(txtIn01.Text)
inputs.Add(txtIn02.Text)
Dim ots As List(Of Double) = network.Execute(inputs)
txtOt01.Text = ots(0)
txtOt02.Text = ots(1)
其中 txtIn01
、txtIn02
、txtOt01
、txtOt02
是 Form
的 TextBox
es。我们用上面的代码所做的,只是从两个 TextBox
es 获取输入,将它们用作网络的输入,并在另一个 TextBox
es 对中写入返回的值。在训练的情况下,程序也是相同的。
交换两个变量
在下面的视频中,您可以看到一个示例训练会话。运行的程序与您在文章末尾下载源代码时找到的程序相同。正如您所见,通过从随机值开始,网络将学会交换它接收到的信号。0 和 1 的值不应被视为精确数字,而应被视为“给定强度的信号”。通过观察训练结果,您可以注意到值的强度如何趋近于 1,或趋近于零,实际上从未达到极端,但仍然代表了它们。因此,在“密集”训练以教会网络在输入对为 [0,1] 的情况下,我们期望输出为 [1,0] 之后,我们可以观察到我们得到类似 [0.9999998,0.000000015] 的结果,其中起始信号通过其强度,被表示为最接近的有利值,就像神经元在激活能级方面一样。
YouTube 上的演示视频:
https://www.youtube.com/embed/zymAdf_zMtQ
源代码
到目前为止我们讨论的源代码可以在以下链接下载
参考文献
历史
- 2015 年 9 月 4 日:初始版本