Содержание:
Введение Базовый сценарий: Простой логический элемент в схеме Цель Стратегия №1: Произвольный локальный поиск
Часть 2:
Стратегия №2: Числовой градиент
Часть 3:
Стратегия №3: Аналитический градиент
Часть 4:
Схемы с несколькими логическими элементами Обратное распространение ошибки
Часть 5:
Шаблоны в «обратном» потоке Пример "Один нейрон"
Часть 6:
Становимся мастером обратного распространения ошибки
Всем привет, я аспирант по вычислительной технике в Стэнфорде. Несколько лет, как часть моего исследования, я работал над глубинным обучением, и среди нескольких связанных с ним моих любимых проектов — ConvNetJS – Javascript-библиотека для обучения нейронных сетей. Javascript позволяет хорошо визуализировать то, что происходит, и дает возможность разобраться с различными настройками гиперпараметров, но я все равно регулярно слышу от людей пожелания рассмотреть эту тему более подробно. Эта статья (которую я планирую постепенно расширить до объема нескольких глав) – это моя скромная попытка сделать это. Я размещаю ее в сети, вместо того, чтобы создать в формате PDF, как и следует поступать с книгами, потому что она должна (и я надеюсь, будет) в конечном итоге содержать анимации/демонстрации и пр.
Мой личный опыт работы над нейронными сетями показывает, что все становится намного понятнее, когда я начинаю игнорировать полностраничность, плотные производные уравнений обратного распространения ошибки, и просто начинаю писать код. Поэтому, это руководство будет содержать очень немного математики (мне не кажется, что это необходимо, и это может местами даже усложнить понимание простых концепций). Так как моя специальность – вычислительная техника и физика, я, вместо этого, буду развивать эту тему с точки зрения хакера. Мое изложение будет сосредоточено вокруг кода и физической интуиции, вместо математических производных. В основном я буду пытаться представить алгоритмы таким образом, какой я хотел бы использовать, когда я только начинал.
"… все стало намного понятнее, когда я начал писать код"
Наверняка вы захотите перескочить и сразу начать изучать нейронные сети, как их можно использовать на практике и пр. Но прежде чем мы возьмемся за это, я бы хотел, чтобы мы обо всем этом забыли. Давайте сделаем шаг назад и разберемся, что, по сути, происходит. Давайте сначала поговорим о схемах реальных значений.
Глава 1: Схемы реальных значений
По моему мнению, лучший способ представить себе нейронные сети – это в качестве схем реальных значений, в которых реальные значения (вместо булевых {0,1}) «протекают» вдоль границ и взаимодействуют в логических элементах. Однако, вместо таких логических элементов, как AND, OR,NOT и пр., у нас есть двоичные логические элементы, такие как * (умножить), + (прибавить), max, или унарные логические элементы, такие как exp и пр. Однако, в отличие от обычных булевых схем, у нас в конечном итоге также будут градиенты, протекающие по тем же границам схемы, но в обратном направлении. Но мы забегаем вперед. Давайте сконцентрируемся и начнем с простого.
Базовый сценарий: Простой логический элемент в схеме
Давайте сначала рассмотрим единичную простую схему с одним логическим элементом. Вот пример:
Схема берет два исходных реальных значения x и y и перемножает x * y с помощью логического элемента *.
Javascript-версия этого уравнения будет выглядеть очень просто, например, так:
var forwardMultiplyGate = function(x, y) { return x * y; }; forwardMultiplyGate(-2, 3); // returns -6.
А в математическом виде мы можем рассмотреть этот логический элемент в качестве воспроизведения функции с реальными значениями:
f(x,y)=xy
Как и в этом примере, все наши логические элементы будут брать одно или два исходных значения и выдавать одно выходное значение.
Цель
Проблема, изучение которой нас интересует, выглядит следующим образом:
1. Мы вводим в приведенную схему определенные исходные значения (например, x = -2, y = 3)
2. Схема вычисляет выходное значение (например, -6)
3. В результате этого возникает ключевой вопрос: Как нужно незначительно изменить исходное значение, чтобы увеличить результат?
В данном случае, в какую сторону нам следует изменить x,y, чтобы получить число больше, чем -6? Следует учесть, что, например, x = -1.99 и y = 2.99 дает в результате x * y = -5.95, что уже больше, чем -6.0. Не пугайтесь:-5.95 лучше (выше), чем -6.0. Это улучшение на 0.05, даже несмотря на то, что величина -5.95 (расстояние от нуля) иногда ниже.
Стратегия №1: Произвольный локальный поиск
Хорошо. У нас есть схема, у нас есть несколько исходных значений и нам просто нужно немного их изменить, чтобы увеличить выходное значение? В чем сложность? Мы можем легко «помочь» схеме вычислить результат для любого заданного значения x и y. Разве это не просто? Почему бы не изменить x и y произвольно и не отследить, какое из изменений дает наилучший результат:
// схема, содержащая пока один логический элемент var forwardMultiplyGate = function(x, y) { return x * y; }; var x = -2, y = 3; // some input values // пытаемся произвольно изменить x,y на небольшую величину и отследить, что дает наилучший результат var tweak_amount = 0.01; var best_out = -Infinity; var best_x = x, best_y = y; for(var k = 0; k < 100; k++) { var x_try = x + tweak_amount * (Math.random() * 2 - 1); // немного изменяем x var y_try = y + tweak_amount * (Math.random() * 2 - 1); // немного изменяем y var out = forwardMultiplyGate(x_try, y_try); if(out > best_out) { // вот оптимальное улучшение! Следим за изменением x и y best_out = out; best_x = x_try, best_y = y_try; } }
Когда я это запустил, я получил best_x = -1.9928, best_y = 2.9901, и best_out = -5.9588. Опять же, -5.9588 выше, чем -6.0. Ну, мы закончили, правильно? Не совсем: Это идеальная стратегия для маленьких проблем с небольшим количеством логических элементов, если в вашем распоряжении есть время на вычисление, но это не сработает, если мы захотим разобраться с большими схемами с миллионами исходных значений. Оказывается, мы можем больше, но об этом в следующей части.

