8

我已經建立了一個常規的ANN-BP設置,其中一個單元在輸入和輸出層以及4個隱藏的sigmoid節點。給它一個簡單的任務來近似線性f(n) = n與範圍0-100中的n。ANN迴歸,線性函數近似

問題:無論層,單元在隱藏層的數目的或是否我在節點使用偏置值都會了解來近似F(N)=平均(數據集),如下所示:

enter image description here

代碼是用JavaScript編寫的概念證明。我定義了三個類:Net,Layer和Connection,其中Layer是一個輸入數組,偏移量和輸出值,Connection是一個二維權重和三角權重數組。下面是層的代碼,所有重要的計算髮生:

Ann.Layer = function(nId, oNet, oConfig, bUseBias, aInitBiases) { 
var _oThis = this; 

var _initialize = function() { 
     _oThis.id  = nId; 
     _oThis.length = oConfig.nodes; 
     _oThis.outputs = new Array(oConfig.nodes); 
     _oThis.inputs = new Array(oConfig.nodes); 
     _oThis.gradients = new Array(oConfig.nodes); 
     _oThis.biases = new Array(oConfig.nodes); 

     _oThis.outputs.fill(0); 
     _oThis.inputs.fill(0); 
     _oThis.biases.fill(0); 

     if (bUseBias) { 
      for (var n=0; n<oConfig.nodes; n++) { 
       _oThis.biases[n] = Ann.random(aInitBiases[0], aInitBiases[1]); 
      } 
     } 
    }; 

/****************** PUBLIC ******************/ 

this.id; 
this.length; 
this.inputs; 
this.outputs; 
this.gradients; 
this.biases; 
this.next; 
this.previous; 

this.inConnection; 
this.outConnection; 

this.isInput = function() { return !this.previous;  } 
this.isOutput = function() { return !this.next;   } 

this.calculateGradients = function(aTarget) { 
    var n, n1, nOutputError, 
     fDerivative = Ann.Activation.Derivative[oConfig.activation]; 

    if (this.isOutput()) { 
     for (n=0; n<oConfig.nodes; n++) { 
      nOutputError = this.outputs[n] - aTarget[n]; 
      this.gradients[n] = nOutputError * fDerivative(this.outputs[n]); 
     } 
    } else { 
     for (n=0; n<oConfig.nodes; n++) { 
      nOutputError = 0.0; 
      for (n1=0; n1<this.outConnection.weights[n].length; n1++) { 
       nOutputError += this.outConnection.weights[n][n1] * this.next.gradients[n1]; 
      } 
      // console.log(this.id, nOutputError, this.outputs[n], fDerivative(this.outputs[n])); 
      this.gradients[n] = nOutputError * fDerivative(this.outputs[n]); 
     } 
    } 
} 

this.updateInputWeights = function() { 
    if (!this.isInput()) { 
     var nY, 
      nX, 
      nOldDeltaWeight, 
      nNewDeltaWeight; 

     for (nX=0; nX<this.previous.length; nX++) { 
      for (nY=0; nY<this.length; nY++) { 
       nOldDeltaWeight = this.inConnection.deltaWeights[nX][nY]; 
       nNewDeltaWeight = 
        - oNet.learningRate 
        * this.previous.outputs[nX] 
        * this.gradients[nY] 
        // Add momentum, a fraction of old delta weight 
        + oNet.learningMomentum 
        * nOldDeltaWeight; 

       if (nNewDeltaWeight == 0 && nOldDeltaWeight != 0) { 
        console.log('Double overflow'); 
       } 

       this.inConnection.deltaWeights[nX][nY] = nNewDeltaWeight; 
       this.inConnection.weights[nX][nY]  += nNewDeltaWeight; 
      } 
     } 
    } 
} 

this.updateInputBiases = function() { 
    if (bUseBias && !this.isInput()) { 
     var n, 
      nNewDeltaBias; 

     for (n=0; n<this.length; n++) { 
      nNewDeltaBias = 
       - oNet.learningRate 
       * this.gradients[n]; 

      this.biases[n] += nNewDeltaBias; 
     } 
    } 
} 

this.feedForward = function(a) { 
    var fActivation = Ann.Activation[oConfig.activation]; 

    this.inputs = a; 

    if (this.isInput()) { 
     this.outputs = this.inputs; 
    } else { 
     for (var n=0; n<a.length; n++) { 
      this.outputs[n] = fActivation(a[n] + this.biases[n]); 
     } 
    } 
    if (!this.isOutput()) { 
     this.outConnection.feedForward(this.outputs); 
    } 
} 

_initialize(); 
} 

主要前饋和backProp函數定義,像這樣:

this.feedForward = function(a) { 
    this.layers[0].feedForward(a); 
    this.netError = 0; 
} 

this.backPropagate = function(aExample, aTarget) { 
    this.target = aTarget; 

    if (aExample.length != this.getInputCount()) { throw "Wrong input count in training data"; } 
    if (aTarget.length != this.getOutputCount()) { throw "Wrong output count in training data"; } 

    this.feedForward(aExample); 
    _calculateNetError(aTarget); 

    var oLayer = null, 
     nLast = this.layers.length-1, 
     n; 

    for (n=nLast; n>0; n--) { 
     if (n === nLast) { 
      this.layers[n].calculateGradients(aTarget); 
     } else { 
      this.layers[n].calculateGradients(); 
     } 
    } 

    for (n=nLast; n>0; n--) { 
     this.layers[n].updateInputWeights(); 
     this.layers[n].updateInputBiases(); 
    } 
} 

連接代碼相當簡單:

Ann.Connection = function(oNet, oConfig, aInitWeights) { 
var _oThis = this; 

var _initialize = function() { 
     var nX, nY, nIn, nOut; 

     _oThis.from = oNet.layers[oConfig.from]; 
     _oThis.to = oNet.layers[oConfig.to]; 

     nIn = _oThis.from.length; 
     nOut = _oThis.to.length; 

     _oThis.weights  = new Array(nIn); 
     _oThis.deltaWeights = new Array(nIn); 

     for (nX=0; nX<nIn; nX++) { 
      _oThis.weights[nX]  = new Array(nOut); 
      _oThis.deltaWeights[nX] = new Array(nOut); 
      _oThis.deltaWeights[nX].fill(0); 
      for (nY=0; nY<nOut; nY++) { 
       _oThis.weights[nX][nY] = Ann.random(aInitWeights[0], aInitWeights[1]); 
      } 
     } 
    }; 

/****************** PUBLIC ******************/ 

this.weights; 
this.deltaWeights; 
this.from; 
this.to; 

this.feedForward = function(a) { 
    var n, nX, nY, aOut = new Array(this.to.length); 

    for (nY=0; nY<this.to.length; nY++) { 
     n = 0; 
     for (nX=0; nX<this.from.length; nX++) { 
      n += a[nX] * this.weights[nX][nY]; 
     } 
     aOut[nY] = n; 
    } 

    this.to.feedForward(aOut); 
} 

_initialize(); 
} 

而且我的激活函數和派生類定義如下:

Ann.Activation = { 
    linear : function(n) { return n; }, 
    sigma : function(n) { return 1.0/(1.0 + Math.exp(-n)); }, 
    tanh : function(n) { return Math.tanh(n); } 
} 

Ann.Activation.Derivative = { 
    linear : function(n) { return 1.0; }, 
    sigma : function(n) { return n * (1.0 - n); }, 
    tanh : function(n) { return 1.0 - n * n; } 
} 
0對於網絡

而且配置JSON如下:

var Config = { 
    id : "Config1", 

    learning_rate  : 0.01, 
    learning_momentum : 0, 
    init_weight  : [-1, 1], 
    init_bias   : [-1, 1], 
    use_bias   : false, 

    layers: [ 
     {nodes : 1}, 
     {nodes : 4, activation : "sigma"}, 
     {nodes : 1, activation : "linear"} 
    ], 

    connections: [ 
     {from : 0, to : 1}, 
     {from : 1, to : 2} 
    ] 
} 

也許,你的經驗眼可以用我的計算髮現這個問題?

See example in JSFiddle

回答

1

首先...我真的很喜歡這段代碼。我對NN的瞭解甚少(剛開始),所以請原諒我的缺點,如果有的話。

這裏是我所做的更改摘要:

//updateInputWeights has this in the middle now: 
 

 
nNewDeltaWeight = 
 
oNet.learningRate 
 
* this.gradients[nY] 
 
/this.previous.outputs[nX] 
 
// Add momentum, a fraction of old delta weight 
 
+ oNet.learningMomentum 
 
* nOldDeltaWeight; 
 

 

 
//updateInputWeights has this at the bottom now: 
 

 
this.inConnection.deltaWeights[nX][nY] += nNewDeltaWeight; // += added 
 
this.inConnection.weights[nX][nY]  += nNewDeltaWeight; 
 

 
// I modified the following: 
 

 
\t _calculateNetError2 = function(aTarget) { 
 
\t \t var oOutputLayer = _oThis.getOutputLayer(), 
 
\t \t \t nOutputCount = oOutputLayer.length, 
 
\t \t \t nError = 0.0, 
 
\t \t \t nDelta = 0.0, 
 
\t \t \t n; 
 

 
\t \t for (n=0; n<nOutputCount; n++) { 
 
\t \t \t nDelta = aTarget[n] - oOutputLayer.outputs[n]; 
 
\t \t \t nError += nDelta; 
 
\t \t } 
 

 
\t \t _oThis.netError = nError; 
 
\t };

Config部分現在看起來是這樣的:

var Config = { 
 
id : "Config1", 
 

 
learning_rate  : 0.001, 
 
learning_momentum : 0.001, 
 
init_weight  : [-1.0, 1.0], 
 
init_bias   : [-1.0, 1.0], 
 
use_bias   : false, 
 

 
/* 
 
layers: [ 
 
\t {nodes : 1, activation : "linear"}, 
 
\t {nodes : 5, activation : "linear"}, 
 
\t {nodes : 1, activation : "linear"} 
 
], 
 

 
connections: [ 
 
\t {from : 0, to : 1} 
 
\t ,{from : 1, to : 2} 
 
] 
 
*/ 
 

 

 
layers: [ 
 
\t {nodes : 1, activation : "linear"}, 
 
\t {nodes : 2, activation : "linear"}, 
 
\t {nodes : 2, activation : "linear"}, 
 
\t {nodes : 2, activation : "linear"}, 
 
\t {nodes : 2, activation : "linear"}, 
 
\t {nodes : 1, activation : "linear"} 
 
], 
 

 
connections: [ 
 
\t {from : 0, to : 1} 
 
\t ,{from : 1, to : 2} 
 
\t ,{from : 2, to : 3} 
 
\t ,{from : 3, to : 4} 
 
\t ,{from : 4, to : 5} 
 
] 
 

 
}

These were my resulting images:

+1

感謝您的關注,但我不明白:1)爲什麼我們在積累delta_weights? 2)爲什麼我們需要4個隱藏層進行簡單近似? –

+0

我錯了積累,謝謝指出。至於深度,在其他輕微代碼更改之後,它的效果更好。我減少了1,2,2,1 ...學習率0.06和動量0.04。總的來說,代碼似乎比它更好。如果你不同意,那很好。我只是在學習時幫助。 –

+0

謝謝。我剛剛注意到的另一件事,你已經刪除了錯誤的平方,這允許一個錯誤標誌潛入計算。這意味着負面的錯誤會消除循環中的正面錯誤,否則負面的錯誤可能會積累並阻止我們測量何時停止訓練。 –

2

我沒有廣泛地關注代碼(因爲它有很多代碼需要考慮,以後需要花費更多時間,並且我對JavaScript不是100%熟悉)。無論如何,我相信斯蒂芬在計算權重方面做了一些改變,他的代碼似乎給出了正確的結果,所以我建議看看。

這裏有幾個點,儘管這不一定是關於計算的正確性,但是仍然可以幫助:

  • 多少例子您顯示網絡進行訓練?你是否多次顯示相同的輸入?您應該多次顯示您擁有(輸入)的每個示例;對於基於梯度下降學習的算法,僅顯示每個示例只有一次是不夠的,因爲它們每次只在正確的方向上移動一點點。你的代碼有可能是正確的,但你只需要多一點時間來訓練。
  • 像斯蒂芬那樣引入更隱藏的層可能有助於加速培訓,或者它可能是有害的。這通常是您想要針對您的特定情況進行試驗的。儘管這個簡單的問題絕對不應該是必要的。我懷疑你的配置和Stephen的配置之間更重要的區別可能是隱藏層中使用的激活功能。你使用了一個sigmoid,這意味着所有的輸入值在隱藏層被壓低到1.0以下,然後你需要非常大的權重來將這些數字轉換回所需的輸出(可以達到一個值100)。 Stephen對所有圖層都使用線性激活函數,在這種特殊情況下,可能會使訓練變得更加容易,因爲您實際上正在嘗試學習線性函數。在許多其他情況下,儘管引入非線性是可取的。
  • 將您的輸入和您想要的輸出轉換(標準化)爲[0,1]而不是[0,100]可能會有好處。這將使得您的sigmoid圖層更有可能產生好的結果(儘管我仍然不確定是否足夠,因爲在您打算學習線性函數的情況下,您仍然引入了非線性,而您可能需要更多的隱藏節點來糾正)。在「現實世界」情況下,如果您有多個不同的輸入變量,通常也會這樣做,因爲它確保所有輸入變量在初始時都被視爲同等重要。您可以始終執行預處理步驟,將輸入標準化爲[0,1],將其作爲網絡輸入,將其訓練以產生[0,1]輸出,然後添加後處理步驟,在該步驟中將輸出回到原來的範圍。
+0

關於sigmoid與線性函數非常有效的一點。謝謝史蒂文撿起來。據我所知,它是S規範化或線性全能而沒有標準化? –

+0

關於必須擁有多個圖層,是否與此相矛盾:https://en.wikipedia.org/wiki/Universal_approximation_theorem? –

+0

@LexPodgorny不,你也可以在線性激活函數的情況下進行歸一化。我懷疑那裏不太需要,但可能仍然有幫助(更小的錯誤和漸變可能更穩定)。至於定理,它只描述了一個節點數量有限的層在理論上是足夠的。這可能仍然是一個節點數量巨大(有限但巨大)的層次,並且需要大量的訓練時間。所以不是矛盾。 –