← 返回卷宗
數據預測

根據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中提供了這個函數。

 sigmoid

 Sigmoid函數圖像

 使用BP反向傳播的神經網絡算法,它可以針對線性與非線性的函數進行模擬,使用S函數時,對於非線性的函數需要採用更多的中間層來進行抽象才能完成,這是因為在低維度的非線性問題可以轉換為高維度的線性問題來進行解決,添加更多的層次,其實就是把問題往更高維度進行映射。

 當然這是理論上的,實際上,網絡的結構、參數的選擇、數據樣本的採集,這些都有很重要的影響,因為神經網絡要足夠清楚描述好一個函數出來,前提是要有足夠的樣本能夠反映出足夠的函數特徵,這在很多情況下實際上是不可能的。

 比如在預測金融股票或是天氣變化或是地震數據時,容易難以模擬,因為總會出現新的規則特點,導致神經網絡在擬合曆史數據後,會出現無法容納更新的規則,導致預測上的失敗。

 用生活中的話來說:它會犯經驗教條主義,導致無法適合新的變化。

 現今比較熱門的深層神經網絡,本質上就是多層神經網絡,所不同的是,在非常多的網絡結構時,神經網絡的抽象識別能力會出現詭異的大大增強,更加有意思是七層以上時,這種現象才會出現。

  神經網絡中有兩個有意思的是,一個是要到了三層,就突然能識別異或邏輯了,而三層以下的神經網絡,是連異或邏輯都識別不出來的,而到了七層,會詭異的出現識別能力的大大增強。一個三,一個七,都是古代術數中非常重視的兩個數字,它們之間有什麼聯繫?

 BP反向傳播算法,這個聽起來是比較不錯的東西,因為它提供了一種發現誤差並自我修正誤差的辦法,但是它還是有問題的,問題在哪裡?

  首先它是反向傳播,就如同水波一樣,最接近輸出的結果,權值修改是越大的,而越往遠離則權值越小,實際上是像一個梯子一樣逐層向下的,如果層數比如多的情況,這個梯度下降不一定能蔓延到其它層去,也就是說,對於多層的神經網絡系統來說,BP反向傳播算法,並不是很適合。

  其次它容易出現對於已提供的數據達到很好的契合,但這種契合是一種假象,因為太契合了,結果反而不能容納新的變化,導致抽象的規律實際上無法反映全局。

  然後最令人詬病的是,計算量太複雜,實在是太複雜了。

  最後還有一個不可容忍的問題,在學習了新的數據後,很快就會把歷史數據的規律給遺忘掉了,達不到前後貫穿。

  所以這個反向傳播算法,儘管感覺是比較好的,但是實際上問題不少,當然在實際應用中它還是有很多地方比較好用的,只是客觀來看,它的適用條件不多,問題也不少。 

  

本文由 三符道長 撰於 2015年5月4日。轉載請註明出處。