有了前面的基础,我们接下来将亲自编写一个神经网络,我们力求代码简洁并易于理解,性能则不是考虑的重点。当然,完完全全讲清神经网络还是比较困难的,这需要一些前置知识,也不是我们关注的重点,我们的重点是利用神经网络来服务AR应用。在开始之前,我们再来看一下人类大脑的神经。
这个图我们可以这样看,一个简单的神经可以由一个个的神经元细胞(Neuron )和连接神经细胞的突触(Synapse)相互连接而成。因此,我们可以把神经元细胞抽象为下图所示结构:
一个神经元细胞可以有若干个输入,也可以有若干个输出,每个输入都通过一个突触与外界相连,同理,每一个输出也通过一个突触与外界相连。一个突触实际就是一个通道,连接不同的神经元。所示我们把突触抽象成如下图:
突触的两端是两个神经元细胞,它把两个神经元细胞连接起来,在神经细胞间传递信息。基于上面的分析,我们可以分别编写突触与神经元细胞的代码如下:
public class Neuron
{
private static readonly System.Random Random = new System.Random();
public List<Synapse> InputSynapses;
public List<Synapse> OutputSynapses;
public double Bias;
public double BiasDelta;
public double Gradient;
public double Value;
public Neuron()
{
InputSynapses = new List<Synapse>();
OutputSynapses = new List<Synapse>();
Bias = GetRandom();
}
public Neuron(IEnumerable<Neuron> inputNeurons) : this()
{
foreach (var inputNeuron in inputNeurons)
{
var synapse = new Synapse(inputNeuron, this);
inputNeuron.OutputSynapses.Add(synapse);
InputSynapses.Add(synapse);
}
}
}
public class Synapse
{
public Neuron InputNeuron;
public Neuron OutputNeuron;
public double Weight;
public double WeightDelta;
public Synapse(Neuron inputNeuron, Neuron outputNeuron)
{
InputNeuron = inputNeuron;
OutputNeuron = outputNeuron;
Weight = Neuron.GetRandom();
}
}
在神经元类(Neuron )的定义中,我们定义了若干输入突触(Synapse),也定义了若干输出突触(Synapse),这就是public List InputSynapses 与 public List OutputSynapses;然后我们还定义了偏置值Bias。设置偏置值的目的是对结果做一个偏移(神经元的输出其实已经一种分类法,有输出为一类,没有输出是另一类)。理解偏置很简单,如假设我们有直线方程x1+x2-3=0,画出这个图像如下:
在突触类(Synapse)的定义很简单,连接一个输入神经元细胞,连接一个输出神经元细胞。还有一个权重值以及一个权重值增量。这个类定义很简单,不详述。
因为神经元还应该具备逻辑思考(计算),训练更新突触(Synapse)权重的作用,所以我们还需要把神经元类Neuron扩展成如下:
public class Neuron
{
private static readonly System.Random Random = new System.Random();
public List<Synapse> InputSynapses;
public List<Synapse> OutputSynapses;
public double Bias;
public double BiasDelta;
public double Gradient;
public double Value;
public static double GetRandom()
{
return 2 * Random.NextDouble() - 1;
}
public Neuron()
{
InputSynapses = new List<Synapse>();
OutputSynapses = new List<Synapse>();
Bias = GetRandom();
}
public Neuron(IEnumerable<Neuron> inputNeurons) : this()
{
foreach (var inputNeuron in inputNeurons)
{
var synapse = new Synapse(inputNeuron, this);
inputNeuron.OutputSynapses.Add(synapse);
InputSynapses.Add(synapse);
}
}
public virtual double CalculateValue()
{
return Value = Sigmoid.Output(InputSynapses.Sum(a => a.Weight * a.InputNeuron.Value) + Bias);
}
public double CalculateError(double target)
{
return target - Value;
}
public double CalculateGradient(double? target = null)
{
if (target == null)
return Gradient = OutputSynapses.Sum(a => a.OutputNeuron.Gradient * a.Weight) * Sigmoid.Derivative(Value);
return Gradient = CalculateError(target.Value) * Sigmoid.Derivative(Value);
}
public void UpdateWeights(double learnRate, double momentum)
{
var prevDelta = BiasDelta;
BiasDelta = learnRate * Gradient;
Bias += BiasDelta + momentum * prevDelta;
foreach (var synapse in InputSynapses)
{
prevDelta = synapse.WeightDelta;
synapse.WeightDelta = learnRate * Gradient * synapse.InputNeuron.Value;
synapse.Weight += synapse.WeightDelta + momentum * prevDelta;
}
}
}
GetRandom()方法获取一个[-1,1]之间的随机值。CalculateValue()方法使用Sigmoid激活函数获取当前神经元细胞的输出值。CalculateGradient()方法计算一个梯度值以便训练调整突触的权重值。UpdateWeights()方法用于训练更新突触的权重值。这里只着重解释一下Sigmoid激活函数,Sigmoid激活函数定义如下:
- 定义域:(−∞,+∞)
- 值域:(−1,1)
- 函数在定义域内为连续和光滑函数
- 处处可导,导数为:f′(x)=f(x)(1−f(x))
Sigmoid激活函数的这些性质非常类似神经元细胞的工作方式,一个神经元细胞在获取多个输入后,经过其本身的逻辑思考(计算),这个神经元细胞可以有电平输出以便传递信息,也可以不输出。事实上,通过训练后的神经网络,对于一个未知输入,往往不能得到100%的预测,但却可以得到一个发生的可能性,通常我们认为超过50%则认为事件发生(即有输出),低于50%则认为事件不发生(即无输出)。Sigmoid类的定义如下:
public static class Sigmoid
{
public static double Output(double x)
{
return x < -45.0 ? 0.0 : x > 45.0 ? 1.0 : 1.0 / (1.0 + Mathf.Exp((float)-x));
}
public static double Derivative(double x)
{
return x * (1 - x);
}
}
为了减轻计算压力,输入小于-45我们直接返回0,输入大于45我们直接返回1,处于这两者之间的值我们才进行计算。至此,我们已完成了神经网络最基本的神经元及突触的模拟代码编写。有了这两个最基本的要素,下面我们将构建一个神经网络。