Странное схождение в простой нейронной сети

Некоторое время я боролся с созданием упрощенного NN в Java. Я работал над этим проектом и выключался в течение нескольких месяцев, и я хочу закончить его. Моя главная проблема заключается в том, что я не знаю, как правильно реализовать обратное распространение (все источники используют Python, математический жаргон или объясняют идею слишком кратко). Сегодня я попытался вывести идеологию самостоятельно, и я использую следующее правило:

обновление веса = ошибка * sigmoidDerivative (ошибка) * вес самого;
ошибка = вывод - фактическая; (последний слой)
error = sigmoidDerivative (ошибка предыдущего слоя) * вес, прикрепляющий этот нейрон к нейрону, дающий ошибку (промежуточный слой)

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

То, что я пытаюсь обучить: для входов 1-9 ожидаемый результат: (x*1.2+1)/10. Это просто правило, которое пришло ко мне случайно. Я использую NN со структурой 1-1-1 (3 слоя, 1 сеть / слой). В приведенной ниже ссылке я приложил два прогона: в одном я использую тренировочный набор, который следует правилу (x * 1.2 + 1) / 10, а в другом я использую (x*1.2+1)/100. При делении на 10 первый вес уходит в бесконечность; при делении на 100 второй вес стремится к 0. Я продолжал пытаться отлаживать его, но я понятия не имею, что мне нужно искать или что не так. Любые предложения очень ценятся. Заранее спасибо и отличного дня всем вам!

https://wetransfer.com/downloads/55be9e3e10c56ab0d6b3f36ad990ebe120171210162746/1a7b6f

В качестве обучающих примеров я использую 1->9 и их соответствующие результаты, следуя приведенному выше правилу, и я запускаю их в течение 100_000 эпох. Я регистрирую ошибку каждые 100 эпох, поскольку ее легче построить с меньшим количеством точек данных, в то же время сохраняя 1000 точек данных для каждого ожидаемого выхода 9. Код для обратного распространения и обновления веса:

    //for each layer in the Dweights array
    for(int k=deltaWeights.length-1; k >= 0; k--)
    {
        for(int i=0; i<deltaWeights[k][0].length; i++)     // for each neuron in the layer
        {
            if(k == network.length-2)      // if we're on the last layer, we calculate the errors directly
            {
                outputErrors[k][i] = outputs[i] - network[k+1][i].result;
                errors[i] = outputErrors[k][i];
            }
            else        // otherwise the error is actually the sum of errors feeding backwards into the neuron currently being processed * their respective weight
            {
                for(int j=0; j<outputErrors[k+1].length; j++)
                {                         // S'(error from previous layer) * weight attached to it
                    outputErrors[k][i] += sigmoidDerivative(outputErrors[k+1][j])[0] * network[k+1][i].emergingWeights[j];
                }
            }
        }

        for (int i=0; i<deltaWeights[k].length; i++)           // for each neuron
        {
            for(int j=0; j<deltaWeights[k][i].length; j++)     // for each weight attached to that respective neuron
            {                        // error                S'(error)                                  weight connected to respective neuron                
                deltaWeights[k][i][j] = outputErrors[k][j] * sigmoidDerivative(outputErrors[k][j])[0] * network[k][i].emergingWeights[j];
            }
        }
    }

    // we use the learning rate as an order of magnitude, to scale how drastic the changes in this iteration are
    for(int k=deltaWeights.length-1; k >= 0; k--)       // for each layer
    {
        for (int i=0; i<deltaWeights[k].length; i++)            // for each neuron
        {
            for(int j=0; j<deltaWeights[k][i].length; j++)     // for each weight attached to that respective neuron
            {
                deltaWeights[k][i][j] *=  1;       // previously was learningRate; MSEAvgSlope

                network[k][i].emergingWeights[j] += deltaWeights[k][i][j];
            }
        }
    }

    return errors;

Изменить: быстрый вопрос, который приходит на ум: так как я использую сигмоид в качестве своей функции активации, должны ли мои входные и выходные нейроны быть только между 0-1? Мой вывод находится между 0-1, но мои входы буквально 1-9.

Edit2: нормализовал входные значения до 0,1-0,9 и изменил:

    outputErrors[k][i] += sigmoidDerivative(outputErrors[k+1][j])[0] * network[k+1][i].emergingWeights[j];     

чтобы:

    outputErrors[k][i] = sigmoidDerivative(outputErrors[k+1][j])[0] * network[k+1][i].emergingWeights[j]* outputErrors[k+1][j];       

так что я держу знак самой ошибки вывода. Это исправило тенденцию Бесконечности в первом весе. Теперь, с пробегом / 10, первый вес стремится к 0, а с пробегом / 100, второй вес стремится к 0. Все еще надеясь, что кто-то подойдет, чтобы прояснить ситуацию для меня.:(

1 ответ

Решение

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

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

Я могу дать вам несколько советов, описав прямой и обратный проход в псевдокоде.

Как примечание, я использую i для ввода, j для скрытого и k для выходного слоя. Смещение входного слоя тогда bias_i, Вес w_mn для весов, соединяющих один узел к другому. Активация a(x) и это производная a'(x),

Пропуск вперед:

for each n of j
       dot = 0
       for each m of i
              dot += m*w_mn
       n = a(dot + bias_i)

То же самое относится к выходному слою k и скрытый слой j, Следовательно, просто замените j от k а также i от j для этого шага.

Обратный проход:

Рассчитать дельту для выходных узлов:

for each n of k
       d_n = a'(n)(n - target)

Вот, target ожидаемый результат, n вывод текущего узла вывода. d_n является дельтой этого узла. Важным примечанием здесь является то, что производные логистической функции и функции tanh содержат выходные данные исходной функции, и эти значения не нужно переоценивать. Логистическая функция f(x) = 1/(1+e^(-x)) и это производная f'(x) = f(x)(1-f(x)), Поскольку значение на каждом выходном узле n был ранее оценен с f(x)можно просто подать заявку n(1-n) как производная. В приведенном выше случае это вычисляет дельту следующим образом:

d_n = n(1-n)(n - target)

Таким же образом рассчитайте дельты для скрытых узлов.

for each n of j
      d_n = 0
      for each m of k
             d_n += d_m*w_jk
      d_n = a'(n)*d_n

Следующим шагом является обновление веса с использованием градиентов. Это делается с помощью алгоритма, называемого градиентным спуском. Не вдаваясь в подробности, это можно сделать следующим образом:

for each n of j
      for each m of k
            w_nm -= learning_rate*n*d_m

То же самое относится и к слою выше. Просто замени j от i а также k от j,

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

Другие вопросы по тегам