【问题标题】:ANN regression, linear function approximationANN回归,线性函数逼近
【发布时间】:2017-03-22 08:49:38
【问题描述】:

我已经建立了一个常规的 ANN-BP 设置,其中一个单元位于输入和输出层,4 个节点隐藏在 sigmoid 中。给它一个简单的任务来近似线性f(n) = n,n 在 0-100 的范围内。

问题:无论层数、隐藏层中的单元如何,或者我是否在节点值中使用偏差,它都学会了近似 f(n) = Average(dataset),如下所示:

代码是用 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();
}

主要的 feedForward 和 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; }
}

网络的配置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

【问题讨论】:

    标签: machine-learning neural-network artificial-intelligence regression conv-neural-network


    【解决方案1】:

    我没有仔细查看代码(因为要查看的代码很多,以后需要花费更多时间,而且我不是 100% 熟悉 javascript)。无论哪种方式,我相信斯蒂芬对权重的计算方式进行了一些更改,并且他的代码似乎给出了正确的结果,所以我建议您查看一下。

    这里有几点虽然不一定与计算的正确性有关,但可能仍然有帮助:

    • 您展示了多少个用于训练的网络示例?您是否多次显示相同的输入?您应该多次展示您拥有(输入)的每个示例;对于基于梯度下降的算法来说,仅仅展示一次是不够的,因为它们每次只会在正确的方向上移动一点点。有可能您的所有代码都是正确的,但您只需要给它更多的时间来训练。
    • 像 Stephen 那样引入更多隐藏层可能有助于加快训练速度,也可能是有害的。这通常是您想要针对您的特定情况进行试验的东西。不过,对于这个简单的问题,绝对不需要。我怀疑您的配置和斯蒂芬的配置之间更重要的区别可能是隐藏层中使用的激活函数。您使用了 sigmoid,这意味着所有输入值在隐藏层中都被压缩到低于 1.0,然后您需要非常大的权重将这些数字转换回所需的输出(最高可达100)。 Stephen 对所有层都使用了线性激活函数,在这种特定情况下,这可能会使训练变得更加容易,因为您实际上是在尝试学习线性函数。但在许多其他情况下,最好引入非线性。
    • 将输入和所需输出转换(标准化)到 [0, 1] 而不是 [0, 100] 可能是有益的。这将使您的 sigmoid 层更有可能产生良好的结果(尽管我仍然不确定这是否足够,因为在您打算学习线性函数的情况下,您仍然会引入非线性,并且您可能需要更多隐藏节点来纠正)。在“现实世界”的情况下,您有多个不同的输入变量,通常也会这样做,因为它确保所有输入变量最初都被视为同等重要。你总是可以做一个预处理步骤,将输入标准化为 [0, 1],将其作为网络的输入,训练它以在 [0, 1] 中产生输出,然后添加一个后处理步骤来转换输出回到原来的范围。

    【讨论】:

    • 关于 sigmoid 与线性函数的一个非常有效的观点。感谢史蒂文接手。据我了解,它要么是带归一化的 sigmoid,要么是没有归一化的全方位线性?
    • 关于必须有多个层,这不矛盾吗:en.wikipedia.org/wiki/Universal_approximation_theorem?
    • @LexPodgorny 不,您也可以在线性激活函数的情况下进行归一化。我怀疑那里不太必要,但可能仍然有帮助(较小的误差和梯度可能在数值上更稳定)。至于定理,这仅描述了具有有限数量节点的一层在理论上就足够了。那可能仍然是具有大量节点(有限但巨大)的层,并且需要大量的训练时间。所以并不矛盾。
    • 听起来你在这方面有大量的实践知识。您介意在 Skype 上连接以进行偶尔的互动吗?
    【解决方案2】:

    首先...我真的很喜欢这段代码。我对神经网络知之甚少(刚刚开始),所以如果有的话,请原谅我的不足。

    以下是我所做更改的摘要:

    //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:
    
    	_calculateNetError2 = function(aTarget) {
    		var oOutputLayer = _oThis.getOutputLayer(),
    			nOutputCount = oOutputLayer.length,
    			nError = 0.0,
    			nDelta = 0.0,
    			n;
    
    		for (n=0; n<nOutputCount; n++) {
    			nDelta = aTarget[n] - oOutputLayer.outputs[n];
    			nError += nDelta;
    		}
    
    		_oThis.netError = nError;
    	};

    配置部分现在看起来像这样:

    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: [
    	{nodes : 1, activation : "linear"},
    	{nodes : 5, activation : "linear"},
    	{nodes : 1, activation : "linear"}
    ],
    
    connections: [
    	{from : 0, to : 1}
    	,{from : 1, to : 2}
    ]
    */
    
    
    layers: [
    	{nodes : 1, activation : "linear"},
    	{nodes : 2, activation : "linear"},
    	{nodes : 2, activation : "linear"},
    	{nodes : 2, activation : "linear"},
    	{nodes : 2, activation : "linear"},
    	{nodes : 1, activation : "linear"}
    ],
    
    connections: [
    	 {from : 0, to : 1}
    	,{from : 1, to : 2}
    	,{from : 2, to : 3}
    	,{from : 3, to : 4}
    	,{from : 4, to : 5}
    ]
    
    }

    【讨论】:

    • 感谢您的关注,但我不明白:1)我们为什么要累积delta_weights? 2) 为什么我们需要 4 个隐藏层来进行简单的近似?
    • 我积累错了,谢谢指出。至于深度,在其他轻微的代码更改之后,它的效果更好。我没有减少 1,2,2,1... 学习率 0.06 和动量 0.04。总体而言,代码似乎比实际工作得更好。如果你不同意,那很好。我只是想在学习的同时提供帮助。
    • 谢谢。我刚刚注意到的另一件事,您已经删除了错误的平方,这允许错误符号潜入计算中。这意味着负误差将抵消循环中的正误差,或者负误差可能会累积并阻止我们判断何时停止训练。
    • 只是一个评论,但是在权重更新中引入的除法允许潜在的除以零错误,这意味着我们必须保护输出值将零变成非常小的数字,这是一种不幸且昂贵的黑客攻击。
    • 请原谅我所有的纠正 cmet,但我真的在努力让它发挥作用,非常感谢你的热情和努力。
    猜你喜欢
    • 2017-04-01
    • 2019-04-26
    • 2020-02-23
    • 1970-01-01
    • 1970-01-01
    • 2020-01-28
    • 2019-09-16
    • 2010-12-25
    • 1970-01-01
    相关资源
    最近更新 更多