Содержание:
Введение
Базовый сценарий: Простой логический элемент в схеме
Цель
Стратегия №1: Произвольный локальный поиск
Стратегия №2: Числовой градиент
Стратегия №3: Аналитический градиент
Схемы с несколькими логическими элементами
Обратное распространение ошибки
Шаблоны в «обратном» потоке
Пример "Один нейрон"
Становимся мастером обратного распространения ошибки
Интересен тот факт, что SVM является всего лишь отдельным видом очень простой схемы (схемы, которая вычисляет score = a*x + b*y + c, где a,b,c являются весовыми функциями, а x,y представляют собой точки ввода данных). Его можно легко расширить до более сложных функций. Например, давайте запишем двухслойную нейронную сеть, которая выполняет бинарную классификацию. Проход вперед будет выглядеть следующим образом:
// зададим исходные значения x,y
var n1 = Math.max(0, a1*x + b1*y + c1); // активация 1-го скрытого нейрона
var n2 = Math.max(0, a2*x + b2*y + c2); // 2-го нейрона
var n3 = Math.max(0, a3*x + b3*y + c3); // 3-го нейрона
var score = a4*n1 + b4*n2 + c4*n3 + d4; // результат
Вышеуказанное определение является двухслойной нейронной сетью с 3 скрытыми нейронами (n1, n2, n3), в которой используется нелинейность выпрямленного линейного элемента (Rectified Linear Unit или ReLU) для каждого скрытого нейрона. Как вы видите, теперь у нас есть несколько параметров, что означает, что наш классификатор стал более сложным и может усложнять границы принятия решений, а не только использовать простое линейное правило принятия решений как, например, SVM. Еще один способ представления – что каждый из трех скрытых нейронов является линейным классификатором, и в данный момент мы создаем дополнительный линейный классификатор поверх них. Теперь начнем углубляться :). Так, давайте обучим эту двухслойную нейронную сеть. Код выглядит очень похоже на пример кода SVM, приведенный выше, нам нужно изменить только проход вперед и назад:
// произвольные изначальные параметры
var a1 = Math.random() - 0.5; // произвольное число между -0,5 и 0,5
// ... таким же образом инициализируем все прочие параметры
for(var iter = 0; iter < 400; iter++) {
// подбираем произвольную точку ввода данных
var i = Math.floor(Math.random() * data.length);
var x = data[i][0];
var y = data[i][1];
var label = labels[i];
// вычисляем проход вперед
var n1 = Math.max(0, a1*x + b1*y + c1); // активация 1-го скрытого нейрона
var n2 = Math.max(0, a2*x + b2*y + c2); // 2-го нейрона
var n3 = Math.max(0, a3*x + b3*y + c3); // 3-го нейрона
var score = a4*n1 + b4*n2 + c4*n3 + d4; // результат
// вычисляем натяжение сверху
var pull = 0.0;
if(label === 1 && score < 1) pull = 1; // нам нужен больший результат! Тянем вверх.
if(label === -1 && score > -1) pull = -1; // нам нужен меньший результат! Тянем вниз.
// теперь рассчитываем обратный проход для всех параметром модели
// обратное распространение ошибки через последний «результативный» нейрон
var dscore = pull;
var da4 = n1 * dscore;
var dn1 = a4 * dscore;
var db4 = n2 * dscore;
var dn2 = b4 * dscore;
var dc4 = n3 * dscore;
var dn3 = c4 * dscore;
var dd4 = 1.0 * dscore; // фух
// обратное распространение ошибки нелинейностей ReLU, на месте
// т.е. просто устанавливаем градиенты в нулевое значение, если нейроны не «выстреливают»
var dn3 = n3 === 0 ? 0 : dn3;
var dn2 = n2 === 0 ? 0 : dn2;
var dn1 = n1 === 0 ? 0 : dn1;
// обратное распространение до параметров нейрона 1
var da1 = x * dn1;
var db1 = y * dn1;
var dc1 = 1.0 * dn1;
// обратное распространение до параметров нейрона 2
var da2 = x * dn2;
var db2 = y * dn2;
var dc2 = 1.0 * dn2;
// обратное распространение до параметров нейрона 3
var da3 = x * dn3;
var db3 = y * dn3;
var dc3 = 1.0 * dn3;
// фух! Конец обратного распространения ошибки!
// обратите внимание, что мы могли также выполнить обратное распространение на x,y
// но нам не нужны эти градиенты. Мы используем только градиенты
// по нашим параметрам при их обновлении, и опускаем x,y
// добавляем натяжение со стороны регуляризации, подталкивая все множительные
// параметры (т.е. не систематические ошибки) вниз, пропорционально их значениям
da1 += -a1; da2 += -a2; da3 += -a3;
db1 += -b1; db2 += -b2; db3 += -b3;
da4 += -a4; db4 += -b4; dc4 += -c4;
// наконец, выполняем обновление параметра
var step_size = 0.01;
a1 += step_size * da1;
b1 += step_size * db1;
c1 += step_size * dc1;
a2 += step_size * da2;
b2 += step_size * db2;
c2 += step_size * dc2;
a3 += step_size * da3;
b3 += step_size * db3;
c3 += step_size * dc3;
a4 += step_size * da4;
b4 += step_size * db4;
c4 += step_size * dc4;
d4 += step_size * dd4;
// несмотря на громоздкость, это можно использовать на практике.
// готово!
}
Вот таким образом мы обучаем нейронную сеть. Очевидно, вам захочется аккуратно разбить свой код на блоки, но я специально показал вам этот пример в надежде, что он объясняет все четко и понятно. Позже мы рассмотрим наиболее оптимальные способы применения этих сетей, и будем структурировать код намного более аккуратно, модульным и более разумным образом.
А пока, я надеюсь, вы вынесли для себя, что двухслойная нейронная сеть – это, на самом деле, не так уж и сложно: мы пишем выражение переднего прохода, интерпретируем значение в конце в виде результата, после чего подтягиваем это значение в положительном или отрицательном направлении, в зависимости от того, каким это значение должно быть для нашего конкретного примера. Обновление параметра после обратного распространения ошибки гарантирует, что когда мы будем рассматривать этот конкретный пример в будущем, сеть скорее выдаст нам нужное значение, вместо того, которое она выдавала до обновления.
Автор: Irina_Ua