Странное схождение в простой нейронной сети
Некоторое время я боролся с созданием упрощенного 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
,
Чтобы обновить смещения, просто суммируйте дельты подключенных узлов, умножьте это на скорость обучения и вычтите этот продукт из определенного смещения.