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不,你也可以在线性激活函数的情况下进行归一化。我怀疑那里不太需要,但可能仍然有帮助(更小的错误和渐变可能更稳定)。至于定理,它只描述了一个节点数量有限的层在理论上是足够的。这可能仍然是一个节点数量巨大(有限但巨大)的层次,并且需要大量的训练时间。所以不是矛盾。 –