Алгоритм обратного распространения нейронной сети застревает на обучающем паттерне XOR - PullRequest
12 голосов
/ 21 февраля 2012

Обзор

Итак, я пытаюсь понять механику нейронных сетей. Я до сих пор не до конца понимаю математику, но думаю, что понимаю, как ее реализовать. В настоящее время у меня есть нейронная сеть, которая может выучить шаблоны тренировок AND, OR и NOR. Однако я не могу заставить его реализовать шаблон XOR. Моя прямая связь нейронная сеть состоит из 2 входов, 3 скрытых и 1 выхода. Веса и смещения произвольно установлены между -0,5 и 0,5 и выходами генерируются с помощью сигмоидальной функции активации

Алгоритм

Пока, я предполагаю, что допустил ошибку в алгоритме обучения, который описан ниже:

  1. Для каждого нейрона в выходном слое укажите значение error, равное desiredOutput - actualOutput - , перейдите к шагу 3
  2. Для каждого нейрона в скрытом или входном слое (работающем в обратном направлении) укажите значение error, которое является суммой всех forward connection weights * the errorGradient of the neuron at the other end of the connection - , перейдите к шагу 3
  3. Для каждого нейрона, используя предоставленное значение error, сгенерируйте error gradient, равное output * (1-output) * error. - перейти к шагу 4
  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 эпох, однако, когда я тренируюсь с XOR, выходы становятся фиксированными и никогда не сходятся. Итак, что я делаю не так? Есть идеи?

Редактировать

Следуя советам других, я начал заново и внедрил свою нейронную сеть без классов ... и это работает. Я все еще не уверен, где моя проблема заключается в приведенном выше коде, но он где-то там.

Ответы [ 8 ]

7 голосов
/ 21 февраля 2012

Это удивительно, потому что вы используете достаточно большую сеть (едва) для изучения 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.

5 голосов
/ 29 сентября 2012

Я недавно столкнулся с той же проблемой.Наконец я нашел решение, как написать код, решающий XOR с помощью алгоритма MLP.

Кажется, что проблема XOR проста в освоении, но не для MLP, потому что она не является линейно разделимой.Таким образом, даже если с вашим MLP все в порядке (я имею в виду, что в вашем коде нет ошибок), вы должны найти хорошие параметры, чтобы иметь возможность изучить проблему XOR.

Два скрытых и один выходной нейрон в порядке.2 основные вещи, которые вы должны установить:

  • , хотя у вас есть только 4 обучающих образца, вы должны запустить обучение на пару тысяч эпох.
  • , если вы используете скрытые сигмовидные слоино при линейном выходе сеть будет сходиться быстрее

Вот подробное описание и пример кода: http://freeconnection.blogspot.hu/2012/09/solving-xor-with-mlp.html

3 голосов
/ 05 мая 2014

Небольшая подсказка - , если выход вашего NN, кажется, дрейфует к 0,5 , тогда все в порядке!

Алгоритм, использующий только скорость обучения и смещение, простослишком просто, чтобы быстро выучить XOR.Вы можете либо увеличить количество эпох, либо изменить алгоритм.

Я рекомендую использовать импульс:

  • 1000 эпох
  • learningRate = 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
1 голос
/ 24 февраля 2012

Комментарий ЛиКао, призванный упростить мою реализацию и избавиться от объектно-ориентированных аспектов, решил мою проблему. Недостаток алгоритма, описанный выше, неизвестен, однако теперь у меня есть работающая нейронная сеть, которая намного меньше.

Не стесняйтесь и дальше рассказывать о проблеме с моей предыдущей реализацией, поскольку другие могут столкнуться с такой же проблемой в будущем.

1 голос
/ 23 февраля 2012

Прошло много времени с тех пор, как я сам в последний раз внедрил Нейронную Сеть, но я думаю, что ваша ошибка в следующих строках:

bias += Parameters.LEARNING_RATE * errorGradient;

и

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

Первая из этих строк не должна быть там вообще. Смещение лучше всего моделировать как вход нейрона, который зафиксирован на 1. Это поможет сделать ваш код намного проще и чище, потому что вам не придется обрабатывать смещение каким-либо особым образом.

Другое дело, что я думаю, что знак неверен в обоих этих выражениях. Думайте об этом так:

  1. Ваш градиент указывает в направлении самого крутого подъема, поэтому, если вы пойдете в этом направлении, ваша ошибка станет больше.

  2. То, что вы делаете здесь, это добавление чего-то к весам, если ошибка уже положительная, то есть вы делаете ее более положительной. Если оно отрицательное, вы вычитаете что-то, то есть вы делаете его более отрицательным.

Если я не пропустил что-то в вашем определении ошибки или расчете градиента, вы должны изменить эти строки на:

bias -= Parameters.LEARNING_RATE * errorGradient;

и

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

У меня была похожая ошибка в одной из моих ранних реализаций, и она привела к точно такому же поведению, то есть к сети, которая обучалась в простых случаях, но не после того, как обучающие данные стали более сложными.

1 голос
/ 22 февраля 2012

Я бы предложил вам сгенерировать сетку (скажем, от [-5, -5] до [5,5] с шагом, равным 0,5), узнать ваше MLP на XOR и применить его к сетке. На графике можно увидеть какую-то границу. Если вы будете делать это на каждой итерации, вы увидите эволюцию границы и сможете контролировать процесс обучения.

0 голосов
/ 29 февраля 2016

Я не вижу ничего плохого в коде, но у меня была похожая проблема с моей сетью, не сходящейся для XOR, поэтому решил опубликовать свою рабочую конфигурацию.

3 входных нейрона (один из них имеет фиксированное смещение 1,0)
3 скрытых нейрона
1 выходной нейрон

Веса, случайно выбранные между -0,5 и 0,5.
Функция активации сигмовидной кишки.

Скорость обучения = 0,2
Импульс = 0,4
Эпоха = 50000

Сошлись 10 раз.

Одна из ошибок, которую я сделал, заключалась в том, что я не подключал вход смещения к выходному нейрону, и это означало бы, что для той же конфигурации он сходился только 2 раза из 10, а остальные восемь раз терпели неудачу, потому что 1 и 1 будут выводить 0,5 .

Еще одна ошибка заключалась в недостаточном количестве эпох. Если бы я только сделал 1000, то выходы имеют тенденцию быть около 0,5 для каждого теста. С эпохами> = 8000, то есть 2000 раз для каждого теста, он начал выглядеть так, как будто он работает (но только при использовании импульса).

При выполнении 50000 эпох не имело значения, использовался импульс или нет.

Другая вещь, которую я попытался, состояла в том, чтобы не применять функцию сигмоида к выходным нейронам (что, как мне кажется, было предложено в предыдущем посте), но это разрушило сеть, потому что выходная (1-выходная) часть ошибки уравнение теперь может иметь отрицательное значение, что означает, что веса были обновлены таким образом, что это привело к увеличению ошибки.

0 голосов
/ 21 февраля 2012

Я немного заржавел в нейронных сетях, но я думаю, что была проблема с реализацией XOR с одним персептроном: в основном нейрон способен разделять две группы решений по прямой линии, но одна прямая линия недостаточно для задачи XOR ...

Здесь должно быть ответом!

...