根据Google的AForge.NET中的源码分析BP反向传播算法原理

    Aforge.net神经网络模型中,有BP学习算法,BP算法直接使用的是激活网络类,而不是用的接口,结果导致使用BP算法,就必须要继承激活网络类来进行覆盖函数调用,而激活网络类如果进行继承的话,会有很多麻烦的事,修改网络结构或是调整算法,并不是很灵活。

    在GOOGLE的这个库中,神经网络算法比较简单,其结构大体是:

    网络包括多个层叠加,而层中包括神经元

    建立一个激活网络的对象,然后指出要分多少层及每层多少个神经层,默认它是三层,一层为输入,一层为输出,输入即是输入的向量,即参数的个数,而输出则是输出的结果,可以是一个或是多个。

    在计算时,先是输入的数据,进入每一层,先从神经元开始计算:

    1、神经元的计算:每个神经元与输入的数据,输入的数据乘以神经元自身的权重,求出总和,然后再加上一个阈值,然后把这个值用激活函数处理一下(激活就是把它转换成处处可求导的),并进行返回给层。

  ActivationNeuron.cs类中的Compute函数:

   public override double Compute( double[] input )
        {
            // check for corrent input vector
            if ( input.Length != inputsCount )
                throw new ArgumentException( "Wrong length of the input vector." );

            // initial sum value
            double sum = 0.0;

            // 每个来自输入的数据的权重都在神经元中有一个对应的权值,这里是把它们计算出来
            for ( int i = 0; i < weights.Length; i++ )
            {
                sum += weights[i] * input[i];
            }
            sum += threshold;

            // local variable to avoid mutlithreaded conflicts
            double output = function.Function( sum ); //注意这里就是对计算的结果调用激活函数来输出,使得输出变得是可求导的。
            // assign output property as well (works correctly for single threaded usage)
            this.output = output;

            return output;
        }

    2、中间层的计算:汇总每一层中,将所有的神经元所算出来的值,作为下一层网络的输入值,传递给下一层的每个神经元中。

 Layer.cs类中的Compute函数。

  public virtual double[] Compute( double[] input )
        {
            // local variable to avoid mutlithread conflicts
            double[] output = new double[neuronsCount];

            // compute each neuron
            for ( int i = 0; i < neurons.Length; i++ )
                output[i] = neurons[i].Compute( input );

            // assign output property as well (works correctly for single threaded usage)
            this.output = output;

            return output;
        }

 然后就是反向传播算法的部分了:

    3、输出网络计算:输出了数据后与目标结果数据进行比较,找出来误差。

  public double Run(double[] input, double[] output)
        {
            //计算网络的输出
            var result = network.Compute(input);

            // 比对结果算出误差
            double error = CalculateError(output);

   //计算出需要更新的数据

   CalculateUpdates(input);

            // 更新网络中神经元的权重及阈值
            UpdateNetwork();

            return error;
        }

    4、对误差的计算:计算出期望的数据与实际数据对于每个神经元偏导数,存在errors数组中

   private double CalculateError(double[] desiredOutput)
        {
            // 当前层与下一层
            Layer layer, layerNext;
            // 当前层与下一层的错误的数组
            double[] errors, errorsNext;
            // 错误的具体值
            double error = 0, e, sum;
            // 神经元的输出结果
            double output;
            // 层的数量
            int layersCount = network.Layers.Length;

            // assume, that all neurons of the network have the same activation function

   //分配,所有的神经元使用相同的激活函数,这里是为了保证从头到尾求导是一致的。
            IActivationFunction function = (network.Layers[0].Neurons[0] as BaseNeuro).ActivationFunction;

            // 先计算最后一层的即输出的结果
            layer = network.Layers[layersCount – 1];
            errors = neuronErrors[layersCount – 1];

            for (int i = 0; i < layer.Neurons.Length; i++)
            {
                output = layer.Neurons[i].Output;
                // 渴望的输出,即真实结果,减去网络输出结果
                e = desiredOutput[i] – output;
                // 对错误调用激活函数中的求导方法进行求导,并保存到errors数组中
                errors[i] = e * function.Derivative2(output);
                //偏差的平方,即方差
                error += (e * e);
            }

            // 计算其它层
            for (int j = layersCount – 2; j >= 0; j–)
            {
                layer = network.Layers[j];
                layerNext = network.Layers[j + 1];
                errors = neuronErrors[j];
                errorsNext = neuronErrors[j + 1];

                // for all neurons of the layer
                for (int i = 0; i < layer.Neurons.Length; i++)
                {
                    sum = 0.0;
                    // for all neurons of the next layer
                    for (int k = 0; k < layerNext.Neurons.Length; k++)
                    {
                        sum += errorsNext[k] * layerNext.Neurons[k].Weights[i];
                    }
                    errors[i] = sum * function.Derivative2(layer.Neurons[i].Output);
                }
            }

            // return squared error of the last layer divided by 2
            return error / 2.0;
        }

 5、计算应该如何更新

 private void CalculateUpdates( double[] input )
        {
            // current neuron
            Neuron neuron;
            // current and previous layers
            Layer layer, layerPrev;
            // 层中神经元需要更新的值
            double[][] layerWeightsUpdates;
            // 层中阈需要更新的值
            double[] layerThresholdUpdates;
            // 层的错误
            double[] errors;
            // 神经元的权重更新
            double[] neuronWeightUpdates;
            // error value
            // double        error;

            // 1 – calculate updates for the first layer
            layer = network.Layers[0];
            errors = neuronErrors[0];
            layerWeightsUpdates = weightsUpdates[0];
            layerThresholdUpdates = thresholdsUpdates[0];

            // learningRate是学习率,而momentum代表动量,学习率乘以动量,代表调整值是多少
            double cachedMomentum = learningRate * momentum;

   //同上,不过这里用的动量的反值就是不动量,代表的是不进行多少调整
            double cached1mMomentum = learningRate * ( 1 – momentum );
            double cachedError;

            // for each neuron of the layer
            for ( int i = 0; i < layer.Neurons.Length; i++ )
            {
                neuron = layer.Neurons[i];
                cachedError = errors[i] * cached1mMomentum;//错误的数据进行多少的保留
                neuronWeightUpdates = layerWeightsUpdates[i];

                // for each weight of the neuron
                for ( int j = 0; j < neuronWeightUpdates.Length; j++ )
                {
                    // 权重的更新方法,变化的值大小,乘以要更新的权重,再加上错误方差乘以输入

       // 需要注意,这里的值是自变化的,意味在进行i循环时,它是在前一次的结果上进行再运算
                    neuronWeightUpdates[j] = cachedMomentum * neuronWeightUpdates[j] + cachedError * input[j];
                }

                // calculate treshold update,计算阈值需要更新的值,这里用的是 变化值 加上错误的方差
                layerThresholdUpdates[i] = cachedMomentum * layerThresholdUpdates[i] + cachedError;
            }

            // 2 – for all other layers 

   // 其它层,其实是一样的,只是其它层要以上一层为输入
            for ( int k = 1; k < network.Layers.Length; k++ )
            {
                layerPrev = network.Layers[k – 1];
                layer = network.Layers[k];
                errors = neuronErrors[k];
                layerWeightsUpdates = weightsUpdates[k];
                layerThresholdUpdates = thresholdsUpdates[k];

                // for each neuron of the layer
                for ( int i = 0; i < layer.Neurons.Length; i++ )
                {
                    neuron = layer.Neurons[i];
                    cachedError = errors[i] * cached1mMomentum;
                    neuronWeightUpdates = layerWeightsUpdates[i];

                    // for each synapse of the neuron
                    for ( int j = 0; j < neuronWeightUpdates.Length; j++ )
                    {
                        // calculate weight update
                        neuronWeightUpdates[j] = cachedMomentum * neuronWeightUpdates[j] + cachedError * layerPrev.Neurons[j].Output;
                    }

                    // calculate treshold update
                    layerThresholdUpdates[i] = cachedMomentum * layerThresholdUpdates[i] + cachedError;
                }
            }
        }

    7、更新网络中神经元的权值与阈值。

  private void UpdateNetwork( )
        {
            // current neuron
            ActivationNeuron neuron;
            // current layer
            Layer layer;
            // layer's weights updates
            double[][] layerWeightsUpdates;
            // layer's thresholds updates
            double[] layerThresholdUpdates;
            // neuron's weights updates
            double[] neuronWeightUpdates;

            // for each layer of the network
            for ( int i = 0; i < network.Layers.Length; i++ )
            {
                layer = network.Layers[i];
                layerWeightsUpdates = weightsUpdates[i];
                layerThresholdUpdates = thresholdsUpdates[i];

                // for each neuron of the layer
                for ( int j = 0; j < layer.Neurons.Length; j++ )
                {
                    neuron = layer.Neurons[j] as ActivationNeuron;
                    neuronWeightUpdates = layerWeightsUpdates[j];

                    // for each weight of the neuron
                    for ( int k = 0; k < neuron.Weights.Length; k++ )
                    {
                        // update weight
                        neuron.Weights[k] += neuronWeightUpdates[k];
                    }
                    // update treshold
                    neuron.Threshold += layerThresholdUpdates[j];
                }
            }
        }

 8、反复计算,直到达到自己想要的目标。

     可以看出,如果想要实现能够反向的误差修正,前提是要保证每个神经元计算中都要处处可以求导,而神经元是一个想怎么设置就怎么设置的东西,所以可以在神经元中加入一个“激活函数”的东西,以保证可以从头到尾处处可以求导,这就是其中关键的地方。

     而通常使用的激活函数是S的函数(sigmoid函数),其函数图像类似太极图中的S形,从理论上已经证明了:通这种方法,可以使用神经网络无限逼近任何函数。在GOOGLE的AForge.Net中提供了这个函数。

    道家阴符派博客--根据Google的AForge.NET中的源码分析BP反向传播算法原理--反向传播算法 1

    道家阴符派博客--根据Google的AForge.NET中的源码分析BP反向传播算法原理--反向传播算法 2

       使用BP反向传播的神经网络算法,它可以针对线性与非线性的函数进行模拟,使用S函数时,对于非线性的函数需要采用更多的中间层来进行抽象才能完成,这是因为在低维度的非线性问题可以转换为高维度的线性问题来进行解决,添加更多的层次,其实就是把问题往更高维度进行映射。

        当然这是理论上的,实际上,网络的结构、参数的选择、数据样本的采集,这些都有很重要的影响,因为神经网络要足够清楚描述好一个函数出来,前提是要有足够的样本能够反映出足够的函数特征,这在很多情况下实际上是不可能的。

        比如在预测金融股票或是天气变化或是地震数据时,容易难以模拟,因为总会出现新的规则特点,导致神经网络在拟合历史数据后,会出现无法容纳更新的规则,导致预测上的失败。

        用生活中的话来说:它会犯经验教条主义,导致无法适合新的变化。

       现今比较热门的深层神经网络,本质上就是多层神经网络,所不同的是,在非常多的网络结构时,神经网络的抽象识别能力会出现诡异的大大增强,更加有意思是七层以上时,这种现象才会出现。

  神经网络中有两个有意思的是,一个是要到了三层,就突然能识别异或逻辑了,而三层以下的神经网络,是连异或逻辑都识别不出来的,而到了七层,会诡异的出现识别能力的大大增强。一个三,一个七,都是古代术数中非常重视的两个数字,它们之间有什么联系?

       BP反向传播算法,这个听起来是比较不错的东西,因为它提供了一种发现误差并自我修正误差的办法,但是它还是有问题的,问题在哪里?

  首先它是反向传播,就如同水波一样,最接近输出的结果,权值修改是越大的,而越往远离则权值越小,实际上是像一个梯子一样逐层向下的,如果层数比如多的情况,这个梯度下降不一定能蔓延到其它层去,也就是说,对于多层的神经网络系统来说,BP反向传播算法,并不是很适合。

  其次它容易出现对于已提供的数据达到很好的契合,但这种契合是一种假象,因为太契合了,结果反而不能容纳新的变化,导致抽象的规律实际上无法反映全局。

  然后最令人诟病的是,计算量太复杂,实在是太复杂了。

  最后还有一个不可容忍的问题,在学习了新的数据后,很快就会把历史数据的规律给遗忘掉了,达不到前后贯穿。

  所以这个反向传播算法,尽管感觉是比较好的,但是实际上问题不少,当然在实际应用中它还是有很多地方比较好用的,只是客观来看,它的适用条件不多,问题也不少。 

  

发表评论

电子邮件地址不会被公开。 必填项已用*标注