如何利用 C# 实现神经网络的感知器模型?

前几天我们介绍了 如何利用 C# 对神经网络模型进行抽象,在这篇图文中,我们抽象了单个神经元 Neuro,激活函数 IActivationFunction,网络层 Layer,网络结构 Network,以及监督学习 ISupervisedLearning 和非监督学习 IUnsupervisedLearning 算法。通过这些抽象,我们可以得到关于神经网络的整体框架。

今天,我们就在此基础上来继承或实现这些抽象类和接口,构造最简单的一种神经网络模型 —— 单层感知器,即用感知器学习算法训练的单层神经网络。


感知器学习算法

该学习算法用于训练具有阈值激活功能的激活神经元单层神经网络。感知器是一种线性分类器,它根据将一组权重与特征向量相结合的线性预测函数进行预测。详细的算法可以参考维基百科中的相关部分:

https://en.wikipedia.org/wiki/Perceptron

感知器

上面,我们简要的介绍了感知器学习算法,接下来我们写相应的代码。

1. 实现激活函数 IActivationFunction

通常情况下感知器神经网络的激活函数有两种,第一种是阈值函数,第二种是符号函数。

阈值函数:

public class ThresholdFunction : IActivationFunction
{
    public double Function(double x)
    {
        return (x >= 0) ? 1 : 0;
    }

    public double Derivative(double x)
    {
        return 0;
    }

    public double Derivative2(double y)
    {
        return 0;
    }
}

符号函数:

public class SignFunction : IActivationFunction
{
    public double Function(double x)
    {
        return x >= 0 ? 1 : -1;
    }

    public double Derivative(double x)
    {
        return 0;
    }

    public double Derivative2(double y)
    {
        return 0;
    }
}

2. 继承抽象神经元类 Neuron

public class ActivationNeuron : Neuron
{
    // 阈值
    public double Threshold { get; set; } = 0.0;
    
    // 激活函数
    public IActivationFunction ActivationFunction { get; set; }
    
    // 构造函数
    public ActivationNeuron(int inputs, IActivationFunction function)
        : base(inputs)
    {
        ActivationFunction = function;
    }

    // 初始化权值阈值
    public override void Randomize()
    {
        base.Randomize();
        Threshold = Rand.NextDouble()*(RandRange.Length) + RandRange.Min;
    }

    // 计算神经元的输出
    public override double Compute(double[] input)
    {
        if (input.Length != InputsCount)
            throw new ArgumentException("输入向量的长度错误。");

        double sum = 0.0;
        for (int i = 0; i < Weights.Length; i++)
        {
            sum += Weights[i]*input[i];
        }
        
        sum += Threshold;
        double output = ActivationFunction.Function(sum);
        Output = output;
        return output;
    }
}

3. 继承抽象神经网络层类 Layer

该类的主要作用是:实例化该层中的每个神经元,并为每个神经元设置激活函数。

public class ActivationLayer : Layer
{
    public ActivationLayer(int neuronsCount, int inputsCount, IActivationFunction function)
        : base(neuronsCount, inputsCount)
    {
        for (int i = 0; i < Neurons.Length; i++)
            Neurons[i] = new ActivationNeuron(inputsCount, function);
    }

    public void SetActivationFunction(IActivationFunction function)
    {
        for (int i = 0; i < Neurons.Length; i++)
        {
            ((ActivationNeuron)Neurons[i]).ActivationFunction = function;
        }
    }
}

4. 继承抽象的神经网络类 Network

public class ActivationNetwork : Network
{
    public ActivationNetwork(IActivationFunction function, int inputsCount, params int[] neuronsCount)
        : base(inputsCount, neuronsCount.Length)
    {
        // neuronsCount 指定神经网络每层中的神经元数量。
        for (int i = 0; i < Layers.Length; i++)
        {
            Layers[i] = new ActivationLayer(
                neuronsCount[i],
                (i == 0) ? inputsCount : neuronsCount[i - 1],
                function);
        }
    }

    public void SetActivationFunction(IActivationFunction function)
    {
        for (int i = 0; i < Layers.Length; i++)
        {
            ((ActivationLayer)Layers[i]).SetActivationFunction(function);
        }
    }
}

写完这个激活网络的实体类 ActivationNetwork 之后,我们就可以构造神经网络的结构了。

ActivationNetwork network = new ActivationNetwork(
    new SigmoidFunction(), // sigmoid 激活函数
    3, // 3个输入
    new int[] {4, 1} //两层 中间层4个神经元,输出层1个神经元
    );

5. 实现感知器学习规则。

由于感知器学习是有监督学习,所以要实现 ISuperviseLearning 接口。

public class PerceptronLearning : ISupervisedLearning
{
    // 神经网络
    private readonly ActivationNetwork _network;
    // 学习率
    private double _learningRate = 0.1;

    // 学习率, [0, 1].
    public double LearningRate
    {
        get { return _learningRate; }
        set
        {
            _learningRate = Math.Max(0.0, Math.Min(1.0, value));
        }
    }

    public PerceptronLearning(ActivationNetwork network)
    {
        if (network.Layers.Length != 1)
        {
            throw new ArgumentException("无效的神经网络,它应该只有一层。");
        }

        _network = network;
    }
    // 单个训练样本
    public double Run(double[] input, double[] output)
    {
        double[] networkOutput = _network.Compute(input);
        Layer layer = _network.Layers[0];
        double error = 0.0;

        for (int j = 0; j < layer.Neurons.Length; j++)
        {
            double e = output[j] - networkOutput[j];
            if (e != 0)
            {
                ActivationNeuron perceptron = layer.Neurons[j] as ActivationNeuron;

                if (perceptron == null)
                    throw new Exception("神经元为null。");
                
                for (int i = 0; i < perceptron.Weights.Length; i++)
                {
                    perceptron.Weights[i] += _learningRate * e * input[i];
                }
                perceptron.Threshold += _learningRate * e;
                
                error += Math.Abs(e);
            }
        }

        return error;
    }
    
    // 所有训练样本
    public double RunEpoch(double[][] input, double[][] output)
    {
        double error = 0.0;
        for (int i = 0, n = input.Length; i < n; i++)
        {
            error += Run(input[i], output[i]);
        }
        return error;
    }
}

6. 感知器模型的应用

首先,我们利用感知器模型处理and问题。

double[][] inputs = new double[4][];
double[][] outputs = new double[4][];

//(0,0);(0,1);(1,0)
inputs[0] = new double[] {0, 0};
inputs[1] = new double[] {0, 1};
inputs[2] = new double[] {1, 0};

outputs[0] = new double[] {0};
outputs[1] = new double[] {0};
outputs[2] = new double[] {0};

//(1,1)
inputs[3] = new double[] {1, 1};
outputs[3] = new double[] {1};


ActivationNetwork network = new ActivationNetwork(
    new ThresholdFunction(), 2, 1);

PerceptronLearning teacher = new PerceptronLearning(network);
teacher.LearningRate = 0.1;

int iteration = 1;
while (true)
{
    double error = teacher.RunEpoch(inputs, outputs);
    Console.WriteLine(@"迭代次数:{0},总体误差:{1}", iteration, error);

    if (error == 0)
        break;
    iteration++;
}

Console.WriteLine();

ActivationNeuron neuron = network.Layers[0].Neurons[0] as ActivationNeuron;

Console.WriteLine(@"Weight 1:{0}", neuron.Weights[0].ToString("F3"));
Console.WriteLine(@"Weight 2:{0}", neuron.Weights[1].ToString("F3"));
Console.WriteLine(@"Threshold:{0}", neuron.Threshold.ToString("F3"));

得到结果如下图所示:

and

其次,我们利用感知器模型处理or问题。

double[][] inputs = new double[4][];
double[][] outputs = new double[4][];

//(0,0)
inputs[0] = new double[] {0, 0};
outputs[0] = new double[] {0};

//(1,1);(0,1);(1,0)
inputs[1] = new double[] {0, 1};
inputs[2] = new double[] {1, 0};
inputs[3] = new double[] {1, 1};

outputs[1] = new double[] {1};
outputs[2] = new double[] {1};
outputs[3] = new double[] {1};


ActivationNetwork network = new ActivationNetwork(
    new ThresholdFunction(), 2, 1);

PerceptronLearning teacher = new PerceptronLearning(network);
teacher.LearningRate = 0.1;

int iteration = 1;
while (true)
{
    double error = teacher.RunEpoch(inputs, outputs);
    Console.WriteLine(@"迭代次数:{0},总体误差:{1}", iteration, error);

    if (error == 0)
        break;
    iteration++;
}

Console.WriteLine();
ActivationNeuron neuron = network.Layers[0].Neurons[0] as ActivationNeuron;

Console.WriteLine(@"Weight 1:{0}", neuron.Weights[0].ToString("F3"));
Console.WriteLine(@"Weight 2:{0}", neuron.Weights[1].ToString("F3"));
Console.WriteLine(@"Threshold:{0}", neuron.Threshold.ToString("F3"));

得到结果如下图所示:


or

最后,我们处理一个稍微复杂一些的问题,比如有以下三类数据:

第一类:(0.1,0.1);(0.2,0.3);(0.3,0.4);(0.1,0.3);(0.2,0.5)

第二类:(0.1,1.0);(0.2,1.1);(0.3,0.9);(0.4,0.8);(0.2,0.9)

第三类:(1.0,0.4);(0.9,0.5);(0.8,0.6);(0.9,0.4);(1.0,0.5)

我们用 Echart 把这三类数据用不同的颜色表示:

原始数据

通常情况下,这些数据会存储在文件中,这里为了演示方便,我们手动赋值了。

double[][] inputs = new double[15][];
double[][] outputs = new double[15][];

//(0.1,0.1);(0.2,0.3);(0.3,0.4);(0.1,0.3);(0.2,0.5)
inputs[0] = new double[] {0.1, 0.1};
inputs[1] = new double[] {0.2, 0.3};
inputs[2] = new double[] {0.3, 0.4};
inputs[3] = new double[] {0.1, 0.3};
inputs[4] = new double[] {0.2, 0.5};

outputs[0] = new double[] {1, 0, 0};
outputs[1] = new double[] {1, 0, 0};
outputs[2] = new double[] {1, 0, 0};
outputs[3] = new double[] {1, 0, 0};
outputs[4] = new double[] {1, 0, 0};

//(0.1,1.0);(0.2,1.1);(0.3,0.9);(0.4,0.8);(0.2,0.9)
inputs[5] = new double[] {0.1, 1.0};
inputs[6] = new double[] {0.2, 1.1};
inputs[7] = new double[] {0.3, 0.9};
inputs[8] = new double[] {0.4, 0.8};
inputs[9] = new double[] {0.2, 0.9};

outputs[5] = new double[] {0, 1, 0};
outputs[6] = new double[] {0, 1, 0};
outputs[7] = new double[] {0, 1, 0};
outputs[8] = new double[] {0, 1, 0};
outputs[9] = new double[] {0, 1, 0};

//(1.0,0.4);(0.9,0.5);(0.8,0.6);(0.9,0.4);(1.0,0.5)
inputs[10] = new double[] {1.0, 0.4};
inputs[11] = new double[] {0.9, 0.5};
inputs[12] = new double[] {0.8, 0.6};
inputs[13] = new double[] {0.9, 0.4};
inputs[14] = new double[] {1.0, 0.5};

outputs[10] = new double[] {0, 0, 1};
outputs[11] = new double[] {0, 0, 1};
outputs[12] = new double[] {0, 0, 1};
outputs[13] = new double[] {0, 0, 1};
outputs[14] = new double[] {0, 0, 1};

搭建感知器网络,输入数为2,输出层神经元个数为分类数3,并进行训练。

ActivationNetwork network = new ActivationNetwork(
    new ThresholdFunction(), 2, 3);

PerceptronLearning teacher = new PerceptronLearning(network);
teacher.LearningRate = 0.1;

int iteration = 1;

while (true)
{
    double error = teacher.RunEpoch(inputs, outputs);
    Console.WriteLine(@"迭代次数:{0},总体误差:{1}", iteration, error);

    if (error == 0)
        break;
    iteration++;
}
迭代误差

输出感知器的权值和阈值,通过这两个值我们就能得到三条分割直线。

ActivationLayer layer = network.Layers[0] as ActivationLayer;
for (int i = 0; i < 3; i++)
{
    Console.WriteLine(@"神经元:{0}", i + 1);
    Console.WriteLine(@"Weight 1:{0}", layer.Neurons[i].Weights[0]);
    Console.WriteLine(@"Weight 2:{0}", layer.Neurons[i].Weights[1]);
    Console.WriteLine(@"Threshold:{0}",
        ((ActivationNeuron) layer.Neurons[i]).Threshold);
}
分割线

通过以上的讲解,我们就把感知器神经网络搞定了。我们可以看到该网络可以处理andor等线性可分的问题,也可以处理一些简单的多分类问题。但对线性不可分的问题就无能为力了。后面我们会介绍误差反传网络可以进行非线性可分的分类问题。好了今天就到这里吧!See You!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容