神经网络反向传播算法卡在 XOR 训练模式上

Posted

技术标签:

【中文标题】神经网络反向传播算法卡在 XOR 训练模式上【英文标题】:Neural Network Back-Propagation Algorithm Gets Stuck on XOR Training PAttern 【发布时间】:2012-03-11 06:28:38 【问题描述】:

概述

所以我试图掌握神经网络的机制。我仍然没有完全掌握它背后的数学原理,但我想我明白如何实现它。我目前有一个可以学习 AND、OR 和 NOR 训练模式的神经网络。但是,我似乎无法让它实现 XOR 模式。我的前馈神经网络由2个输入、3个隐藏和1个输出组成。权重和偏差随机设置在-0.5和0.5之间 ,并使用 sigmoidal 激活函数

生成输出

算法

到目前为止,我猜我在训练算法中犯了一个错误,如下所述:

    对于输出层中的每个神经元,提供一个error 值,即desiredOutput - actualOutput --转到第 3 步 对于隐藏层或输入层(向后工作)中的每个神经元,提供一个 error 值,它是所有 forward connection weights * the errorGradient of the neuron at the other end of the connection 的总和 --转到第 3 步 对于每个神经元,使用提供的error 值,生成一个等于output * (1-output) * errorerror gradient。 --转到第 4 步 对于每个神经元,将偏差调整为等于current bias + LEARNING_RATE * errorGradient。然后调整每个反向连接的权重等于current weight + LEARNING_RATE * output of neuron at other end of connection * this neuron's errorGradient

我正在在线训练我的神经网络,所以它会在每个训练样本之后运行。

代码

这是运行神经网络的主要代码:

private void simulate(double maximumError) 

    int errorRepeatCount = 0;
    double prevError = 0;

    double error; // summed squares of errors
    int trialCount = 0;

    do 

        error = 0;

        // loop through each training set
        for(int index = 0; index < Parameters.INPUT_TRAINING_SET.length; index++) 

            double[] currentInput = Parameters.INPUT_TRAINING_SET[index];
            double[] expectedOutput = Parameters.OUTPUT_TRAINING_SET[index];
            double[] output = getOutput(currentInput);

            train(expectedOutput);

            // Subtracts the expected and actual outputs, gets the average of those outputs, and then squares it.
            error += Math.pow(getAverage(subtractArray(output, expectedOutput)), 2); 



        

     while(error > maximumError);

现在是train() 函数:

public void train(double[] expected) 

    layers.outputLayer().calculateErrors(expected);

    for(int i = Parameters.NUM_HIDDEN_LAYERS; i >= 0; i--) 
        layers.allLayers[i].calculateErrors();
    


输出层calculateErrors()函数:

public void calculateErrors(double[] expectedOutput) 

    for(int i = 0; i < numNeurons; i++) 

        Neuron neuron = neurons[i];
        double error = expectedOutput[i] - neuron.getOutput();
        neuron.train(error);

    


普通(隐藏和输入)层calculateErrors()函数:

public void calculateErrors() 

    for(int i = 0; i < neurons.length; i++) 

        Neuron neuron = neurons[i];

        double error = 0;

        for(Connection connection : neuron.forwardConnections) 

            error += connection.output.errorGradient * connection.weight;

        

        neuron.train(error);

    


完整的神经元类:

package neuralNet.layers.neurons;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import neuralNet.Parameters;
import neuralNet.layers.NeuronLayer;

public class Neuron 

private double output, bias;
public List<Connection> forwardConnections = new ArrayList<Connection>(); // Forward = layer closer to input -> layer closer to output
public List<Connection> backwardConnections = new ArrayList<Connection>(); // Backward = layer closer to output -> layer closer to input

public double errorGradient;
public Neuron() 

    Random random = new Random();
    bias = random.nextDouble() - 0.5;



public void addConnections(NeuronLayer prevLayer) 

    // This is true for input layers. They create their connections differently. (See InputLayer class)
    if(prevLayer == null) return;

    for(Neuron neuron : prevLayer.neurons) 

        Connection.createConnection(neuron, this);

    



public void calcOutput() 

    output = bias;

    for(Connection connection : backwardConnections) 

        connection.input.calcOutput();
        output += connection.input.getOutput() * connection.weight;

    

    output = sigmoid(output);



private double sigmoid(double output) 
    return 1 / (1 + Math.exp(-1*output));


public double getOutput() 
    return output;


public void train(double error) 

    this.errorGradient = output * (1-output) * error;

    bias += Parameters.LEARNING_RATE * errorGradient;

    for(Connection connection : backwardConnections) 

        // for clarification: connection.input refers to a neuron that outputs to this neuron
        connection.weight += Parameters.LEARNING_RATE * connection.input.getOutput() * errorGradient;

    




结果

当我训练 AND、OR 或 NOR 时,网络通常可以在大约 1000 个 epoch 内收敛,但是当我使用 XOR 训练时,输出变得固定并且永远不会收敛。那么,我做错了什么?有什么想法吗?

编辑

听从其他人的建议,我重新开始并在没有类的情况下实现了我的神经网络......并且它有效。我仍然不确定我的问题出在上面的代码中,但它就在那里。

【问题讨论】:

哇,我已经很久 没有接触过神经网络了。当你说它卡住并且永远不会收敛时,我的第一个想法是你可能达到了局部最小值。我不记得什么样的 NN 架构和条件可以产生这种情况,或者你做了什么来解决它。 NN 实现中存在很多错误的可能性。您的架构应该能够学习 XOR,因此您的实现显然有问题。你应该仔细观察你的神经网络通过误差面的路径,看看它是否朝着正确的方向前进。这可能会给你一些关于出了什么问题的提示。除此之外,您的算法看起来不错,但我还没有将它与文献进行比较,所以最好仔细检查一下。 神经网络实现的一个很好的提示:不要使用面向对象来实现神经网络。尽管能够在对象级别上对神经元和连接进行建模似乎很好,但这通常只会使您的代码比它需要的更复杂和更慢。如果您仔细观察 NN 的作用,就会发现这一切都归结为简单的矩阵和向量运算。如果您以这些方式实现您的网络,您将拥有更简单的代码,并且调试会更容易。 面向对象更有意义。它也更容易阅读。我会尝试消除它,但我宁愿找出问题,因为它存在于我的代码的当前形式中...... 【参考方案1】:

我对神经网络有点生疏,但我认为用一个感知器实现 XOR 存在问题:基本上一个神经元能够通过一条直线分离两组解决方案,但一条直线不是足以解决 XOR 问题...

Here 应该是答案!

【讨论】:

这应该有什么帮助? OP 在他们的网络中已经有一个隐藏层,这是这个页面给出的唯一建议。异或问题。 不是问题,但是正如 larsmans 指出的那样,我正在处理一个 3 层神经网络结构。【参考方案2】:

这令人惊讶,因为您正在使用一个足够大的网络(几乎没有)来学习 XOR。你的算法看起来不错,所以我真的不知道发生了什么。了解您如何生成训练数据可能会有所帮助:您是否只是一遍又一遍地查看样本(1,0,1),(1,1,0),(0,1,1),(0,0,0) 或类似的东西?也许问题在于随机梯度下降导致您跳过稳定最小值。您可以尝试一些方法来解决此问题:也许从您的训练示例中随机采样而不是重复它们(如果您正在这样做)。或者,您也可以修改您的学习算法:

目前你有相当于:

weight(epoch) = weight(epoch - 1) + deltaWeight(epoch)
deltaWeight(epoch) = mu * errorGradient(epoch)

mu 是学习率

一种选择是非常慢慢降低mu的值。

另一种方法是更改​​您对deltaWeight 的定义以包含“动量”

deltaWeight(epoch) = mu * errorGradient(epoch) + alpha * deltaWeight(epoch -1)

其中alpha 是动量参数(介于 0 和 1 之间)。

在视觉上,您可以将梯度下降视为通过将对象放置在曲面上来尝试找到曲面的最小点,然后逐步移动该对象少量,其中任何方向都根据位置向下倾斜它目前位于。问题是您并没有真正进行梯度下降:相反,您进行随机梯度下降,通过从一组训练向量中采样并沿样本看起来向下的任何方向移动来沿方向移动。平均而言,在整个训练数据中,随机梯度下降应该有效,但不能保证有效,因为您可能会陷入来回跳跃永远无法取得进展的情况。慢慢降低学习率意味着你每次都采取越来越小的步骤,这样就不会陷入无限循环。

另一方面,动量使算法类似于滚动橡皮球。由于球的作用,它倾向于向下移动,但它也倾向于继续沿着它之前的方向移动,如果它曾经处于下坡在同一方向上一段时间的拉伸,它会加速。因此,球会跳过一些局部最小值,并且它会更有弹性地防止在目标上来回移动,因为这样做意味着要对抗动量。


有了一些代码并对此进行了更多思考,很明显您的问题在于训练早期层。您成功学习的函数都是线性可分的,因此只有一个层被正确学习是有道理的。我同意 LiKao 的总体实施策略,尽管你的方法应该有效。我对如何调试的建议是弄清楚输入层和输出层之间连接的权重进展情况。

您应该发布Neuron 的其余实现。

【讨论】:

感谢您的回复。我以相同的顺序反复为 NN 提供相同的输入,但是以随机顺序为它们提供服务并没有影响。另外,如果重要的话,我只训练二维 (1,1) (1,0) (0,1) (0, 0)。我添加了动态学习率和动量,但无济于事。所有输出稳定在 0.5 左右 如何计算误差?我猜你的问题出在那儿。请用一些代码示例更新您的问题。 问题已用代码更新。如果您需要更多,请告诉我。 完整的神经元课程已发布。我会检查我的连接和它们的权重,看看我是否注意到任何奇怪的东西。谢谢!【参考方案3】:

我建议您生成一个网格(例如从 [-5,-5] 到 [5,5],步长为 0.5),在 XOR 上学习您的 MLP 并将其应用于网格。用彩色绘制,您可以看到某种边界。 如果您在每次迭代中都这样做,您将看到前沿的演变并可以控制学习。

【讨论】:

我到底应该画什么?错误值?重量?输出? 在 2D 中绘制网格,为每个不同的输出使用不同的颜色(即红色表示输出 = 0)。如果您的网格足够精确(每 0.5 或 0.2 一个点),您会看到一种边界。【参考方案4】:

自从我上次自己实现神经网络以来已经有一段时间了,但我认为你的错误在于:

bias += Parameters.LEARNING_RATE * errorGradient;

connection.weight += Parameters.LEARNING_RATE * connection.input.getOutput() * errorGradient;

这些行的第一行根本不应该在那里。偏差最好建模为固定为 1 的神经元的输入。这将使您的代码更简单、更清晰,因为您不必以任何特殊方式处理偏差。

另一点是,我认为这两个表达式中的符号都是错误的。想一想:

    你的梯度指向最陡的上升方向,所以如果你往那个方向走,你的误差会变大。

    您在这里所做的是在权重中添加一些内容,以防错误已经是正数,即您正在使其更加正数。如果它是负数,你就是在减去一些东西,即你让它变得更负数。

除非我遗漏了您对错误或梯度计算的定义,否则您应该将这些行更改为:

bias -= Parameters.LEARNING_RATE * errorGradient;

connection.weight -= Parameters.LEARNING_RATE * connection.input.getOutput() * errorGradient;

我在早期的一个实现中遇到了类似的错误,它导致了完全相同的行为,即它导致了一个在简单情况下学习的网络,但一旦训练数据变得更加复杂,就不再是这样了。

【讨论】:

感谢您的提示,但是即使在线性可分模式(在 NOR 上测试并且所有输出固定在 0.9...)上,这也会破坏神经网络。我将尝试一种基本的纯矩阵/向量方法,看看我是否至少可以让它工作。实施后,我将更新我的 OP。 @williamg:如果这破坏了您的网络,那么这显然不是正确的解决方法。你的学习率是负的还是正的? @williamg:好的,没关系。我刚刚注意到,您将错误计算为double error = expectedOutput[i] - neuron.getOutput();,而不是我今天早上早些时候想的相反。这意味着,一切都应该没问题。尽管如此,我还是会留下答案,因为这是一个常见的错误。 所以,我简单地根据数组实现了一切......并且它有效。据我所知,我并没有改变算法,只是重新开始并采用了不同的实现方法。结果:一个更小(虽然稍微更混乱)的工作神经网络。【参考方案5】:

LiKao 的评论简化了我的实现并摆脱了面向对象的方面解决了我的问题。上面描述的算法中的缺陷是未知的,但是我现在有一个工作的神经网络要小得多。

请随时继续提供有关我之前实施的问题的见解,因为其他人将来可能会遇到同样的问题。

【讨论】:

【参考方案6】:

不久前我遇到了同样的问题。最后我找到了解决方案,如何编写一个用 MLP 算法解决 XOR 的代码。

XOR 问题似乎是一个易于学习的问题,但它不适用于 MLP,因为它不是线性可分的。因此,即使您的 MLP 没问题(我的意思是您的代码中没有错误),您也必须找到能够学习 XOR 问题的好的参数。

两个隐藏和一个输出神经元很好。您必须设置的两个主要内容:

尽管您只有 4 个训练样本,但您必须运行数千个 epoch 的训练。 如果您使用 sigmoid 隐藏层但使用线性输出,网络会更快收敛

这里是详细描述和示例代码:http://freeconnection.blogspot.hu/2012/09/solving-xor-with-mlp.html

【讨论】:

老兄,谢谢!我的 MLP 总是输出 0.5 时遇到同样的问题,但是将输出节点激活函数切换为线性解决了它!【参考方案7】:

小提示 - 如果您的 NN 的输出似乎向 0.5 漂移,那么一切正常!

仅使用学习率和偏差的算法过于简单,无法快速学习 XOR。您可以增加 epoch 的数量或更改算法。

我的建议是使用动量:

1000 个纪元 学习率 = 0.3 动量 = 0.8 从 [0,1] 中提取的权重 偏差绘制形式 [-0.5, 0.5]

还有一些关键的伪代码(假设前后传播有效):

for every edge:
    previous_edge_weight_change = -1 * learningRate * edge_source_neuron_value * edge_target_neuron_delta + previous_edge_weight * momentum

    edge_weight += previous_edge_weight_change

for every neuron:
    previous_neuron_bias_change = -1 * learningRate * neuron_delta + previous_neuron_bias_change * momentum

    bias += previous_neuron_bias_change

【讨论】:

改变 [0,1] 之间的权重和 [-0.5, 0.5] 之间的偏差也对我有用。它不再停滞在 0.5!想知道为什么这会产生如此大的不同吗?在我使用 1 / sqrt(n_input_nodes + n_output_nodes) 左右的正态分布来初始化权重之前。【参考方案8】:

我看不出代码有什么问题,但我的网络也遇到了类似的问题,无法进行 XOR 收敛,所以我想我会发布我的工作配置。

3 个输入神经元(其中一个是 1.0 的固定偏差) 3 个隐藏神经元 1个输出神经元

权重在 -0.5 和 0.5 之间随机选择。 Sigmoid 激活函数。

学习率 = 0.2 动量 = 0.4 历元 = 50,000

收敛 10/10 倍。

我犯的一个错误是没有将偏置输入连接到输出神经元,这意味着对于相同的配置,它只收敛了 10 次中的 2 次,而其他 8 次失败,因为 1 和 1 会输出 0.5 .

另一个错误是没有做足够的 epoch。如果我只做了 1000 个,那么每个测试用例的输出往往在 0.5 左右。对于每个测试用例,epochs >= 8000 所以 2000 次,它开始看起来可能正在工作(但前提是使用动量)。

在进行 50000 次迭代时,是否使用动量无关紧要。

我尝试的另一件事是不将 sigmoid 函数应用于输出神经元输出(我认为这是之前的帖子所建议的),但这破坏了网络,因为 output*(1-output) 部分错误方程现在可能是负数,这意味着权重以导致误差增加的方式更新。

【讨论】:

以上是关于神经网络反向传播算法卡在 XOR 训练模式上的主要内容,如果未能解决你的问题,请参考以下文章

LSTM 反向传播梯度检查的问题

反向传播算法简记

反向传播算法数学推导

反向传播算法简介

什么叫正向最大匹配算法,反向最大匹配算法

在反向传播算法中更新权重