65.9K
CodeProject 正在变化。 阅读更多。
Home

Neuronal.NET - 易于实现的神经网络

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (36投票s)

2014年7月4日

MIT

6分钟阅读

viewsIcon

37765

downloadIcon

2761

如何在 C# 中实现一个简单的神经网。

Sample Image - maximum width is 600 pixels

引言

本文将以字符识别为例,讲解如何实现一个易于使用的神经网络。

神经网络需要 3 个要素

  • 训练集(给定问题的已知答案)
  • 神经网络
  • 一种告诉网络它已改进的方法

这个人工网络使用进化算法进行训练。

以下是训练新神经网络的最简短示例
首先,添加对 Synapse.dll 的引用。
就是这样。

using Brains;

Brain ANN = new Brain(5, 4, 3);

    ANN.SetInput(List<double> traindata);

    while (ANN.score<10)
    {
        ANN.score=grade(ANN.Outputs);
    }

Bitmap finish=ANN.ToBitmap();

该网络将有 5 个输入、4 个输出和 3 个隐藏层。它几乎与第一张图相似。
当然,您必须定义评分函数。这只是一个双精度数,告诉网络它所做的操作是否符合您的要求。

作为用户,您看不到训练算法,所有神奇之处都发生在 ANN.score 的 setter 中
score 值的 setter 是进化算法的接口,它试图始终获得比之前更高的分数。
简而言之,它的工作原理是

  • 变异
  • 通过所选项目
  • 繁殖

注意: 用户根据自己的规则设置分数,因此如果实际输出更接近期望输出,分数应该稍高一些。

背景

关于神经网络,您需要了解几件事

最重要的是神经元。这是一个具有许多输入和一个双精度输出的构造。

Sample Image - maximum width is 600 pixels

为了有用地使用神经元,它们在许多层中相互连接。其中大多数是隐藏的,只有输出层和输入层可以从“外部”看到。
如果您查看第一张图,可以看到每个圆圈都是一个神经元。

任何给定神经元(输入层除外)的输入将是前一层所有神经元的所有输出。输入层的输出是用户提供的列表的成员。
本项目使用 arctan(x)/pi 作为激活函数(连续函数,将所有有理数值归一化到 -1 和 1 之间)。

每个神经元的输出公式如下

Output=Tranferfuction(sum(inputs*weights))

每个神经元的权重,在此称为基因组,是神经网络正常工作的关键因素。每个神经元都为连接到它的每个神经元存储一个双精度值。在这种情况下,该因子可以在 -4 和 +4 之间。这允许在所有输出的总和中进行抵消。神经网络在后台维护一个字典以提高速度。

通过将权重设置为某些(未知)值,神经网络可以“学习”输入和输出之间的关系。

要实际训练神经网络,必须测试所有权重的组合,以找到全局最优解。无法知道您已找到最佳解决方案。
尽管训练算法试图达到全局最优,但不能保证找到最优解。

最简单的神经元实现将是

class neuron
{ neuron
{
    double output
    {
    get {tranferfunction(sum(previouslayer));}
    private set{}
    }
}

这当然是一种简化。实际代码看起来更像这样

double sum() 
{ 
    double back = 0; 
    for (int i = 0; i < previous.Count; i++) { back += previous[i].output * genome[i]; } 
    if (Parent.Transferfunction==Transferfunction.Stepfunction) 
    { 
        back = back / previous.Count; 
    } 
    back = transfer(back); 
    return back; 
}

double ret = double.PositiveInfinity;                    
public double output 
{ 
get 
{ 
    if (!double.IsInfinity(ret)) { return ret; } 
    else { 
        double outp; 
        if (Parent.antworten.ContainsKey(name) == false) 
        { 
            outp = sum(); 
            lock (Parent) { Parent.antworten.Add(name, outp); } 
            return outp; 
        } 
        else { Parent.antworten.TryGetValue(name, out outp); return outp; } } 
} 

set { ret = value; } 

}

有关神经网络的精彩阅读,请参阅

使用神经网络

这个示例项目将展示如何训练网络来区分 3 个字母:A、B 和 C。
这意味着我们有一个 Bitmap 作为输入,3 个输出。每个输出是介于 0 和 1 之间的数字。

您要做的第一件事是考虑一种方法,将位图转换为 0 和 1 的列表
下面的示例图片

apply_image(Bitmap image)
{
    List<double>input = new List<double>();

    brightness = image.GetPixel(x, y).GetBrightness();

    if (brightness > 0.5) { input.Add(1); } 
    else  input.Add(0);
}

如果图片分辨率为 320x120,那么网络的输入将过多。
相反,您只获取每 20像素(或其他类似值)。这已经足够了。

下面的代码包含训练神经网络所需的所有信息。

神经网络的不确定性在于隐藏层的数量和大小。
感知机的通用规则是它们根本不需要隐藏层。
如果您想实现更高的功能,例如 XOR 问题或乘法,您将需要更多的隐藏层。
当然,训练神经网络进行乘法是浪费时间,因为乘法对任何程序员来说都是一项简单的任务。

注意:Score 属性会稍微更改现有网络,以便下次表现更好。
它会维护一个内部列表,其中包含配置和相应的分数,以生成更好的配置。
NET.StopTraining(); 获取迄今为止最佳的配置。

B 的完美分数将是 0 1 0。

Bitmap A = new Bitmap(@"A.bmp"); 
Bitmap B = new Bitmap(@"B.bmp"); 
Bitmap C = new Bitmap(@"C.bmp"); 
Bitmap T1 = new Bitmap(@"TEST1.bmp"); 
Bitmap T2 = new Bitmap(@"TEST2.bmp"); 

Brain NET = new Brain(100, 3,0); 
NET.Trainmode = Trainmode.Fast; 
NET.Transferfunction = Transferfunction.Arctan; 
NET.Outputmax = 1; 
var inputs = apply_image(A); 
double score = 0; 
while (NET.Score<2.9) 
{ 
    score = 0; 
    inputs = apply_image(A); 
    NET.SetInput(inputs); 
    score += NET[0]; 
    score -= NET[1]; 
    score -= NET[2]; 
    inputs = apply_image(B); 
    NET.SetInput(inputs); 
    score -= NET[0]; 
    score += NET[1]; 
    score -= NET[2]; 
    inputs = apply_image(C); 
    NET.SetInput(inputs); 
    score -= NET[0]; 
    score -= NET[1]; 
    score += NET[2];
    NET.Score = score;
}
NET.StopTraining();

//See whole code in demo project!!

Sample Image - maximum width is 600 pixelsSample Image - maximum width is 600 pixels

神经网络应该能够正确判断案例 2 仍然是 A(较小,不在中心,并且有一些伪影)。

上述测试用例的最终输出如下

  • A: 0.98 0.02 0.03
  • 坏的 A: 0.86 0.13 0.09
  • C: 0.02 0.01 0.98

这告诉我们训练已经成功,网络实际上“知道”A、B 和 C 之间的区别!
要自己测试代码,请查看演示项目。

学习算法

每个神经元都有一组称为基因组的权重。整个网络可以看作是双精度数的列表,因为只有每个神经元的每个权重才重要。
如果训练网络,则只需更改此列表。

您要做的第一件事是保留所有分数和基因组的列表。为此,它基本上是一个双精度数列表和一个分数。此片段发生在 score 的 setter 中,因为用户负责分数和 trainingset

list<genome> max;

if (value > max.score) 
{
    if (Trainmode == Trainmode.Fast) 
    { 
        if (best.Count == 0) {best.Add(new Genome(this));} 
        var max = best.MaxBy(x => x.score); 
        if (value > max.score) { score = value; Generation++;
        best.Add(new Genome(this)); 
    }
    else { max.SetGenesTo(this); this.mutate(); } 
}

从优秀基因组列表中,选择最好的一个。之后,一些权重(2%)会被随机更改。

这称为变异和选择。繁殖通过复制最佳基因组来实现。
实际实现更复杂,因为它处理更多基因组作为最佳基因组,以避免局部最小值。

您可以选择是想快速学习还是尝试避免非最优解。为此,我实现了 trainmode 属性。
您可以将其设置为 fast(快速)或 quality(高质量)。前者始终选择最佳基因组并进一步开发它。后者 trainmode 训练 10 个基因组,然后选择最好的一个。
之后,它会根据最好的基因组再次开发 10 个基因组。这样,训练速度会慢很多,但您可以取得更好的结果。

如果您想了解更多关于其他训练方法的信息,请参阅背景部分中的链接!!

关注点

在我编写代码的过程中,我一直想看看网络在做什么。这就是为什么我实现了 mynet.ToBitmap(); mynet.ShowGraph(); ,它们将所有输入和参数转换为一张漂亮的图片。

大多数人将神经网络视为一种人工智能。本文使用的简单前馈网络并非智能。您更可以将神经网络视为一个数学公式。您不知道公式,但您知道某些输入参数(trainingset)的答案。学习算法会改变矩阵乘法中的正确参数,从而找到训练集的通用公式。

© . All rights reserved.