Траектория «Нейросети» · конспект 2 из 12

Градиентный спуск и обратное распространение ошибки

О чём эта тема
В конспекте 1 веса для XOR подбирались вручную. Здесь рассматривается способ находить веса автоматически: минимизировать функцию ошибки методом градиентного спуска. Обратное распространение ошибки вводится как способ эффективно вычислить нужные для этого производные в сети из нескольких слоёв. В конце — первая рабочая реализация обучаемого нейрона.
Аннотация
Конспект посвящён тому, как нейрон подбирает веса автоматически. Сначала вводится функция ошибки и объясняется, почему для градиентных методов квадрат отклонения удобнее модуля. Далее напоминается определение производной как предела и вводится градиент в пространстве нескольких весов, после чего формулируется идея градиентного спуска, обсуждаются выбор скорости обучения и проблема локальных минимумов. Центральная часть — метод обратного распространения ошибки с разбором прямого и обратного прохода по сети. В завершение обучаемый нейрон реализуется на numpy, чтобы каждый шаг алгоритма был виден в коде в явном виде.
Пререквизиты
Конспект 1 (нейрон, веса, функция активации, сигмоида). Понятие производной из курса математического анализа — здесь оно вводится формально ещё раз, но базовая интуиция пригодится.
Мотивация
Веса для XOR в конспекте 1 были подобраны вручную — задача с двумя входами и известной структурой это ещё позволяет. Для сети с тысячами весов и незнакомой заранее зависимостью подбор руками невозможен. Нужен автоматический способ находить веса, минимизирующие ошибку сети на обучающих данных — это и называется обучением нейросети.

1. Что значит «обучиться»?

Схема: стрелки по кругу связывают слова «Try» (попробовать), «Fail» (ошибиться) и «Success» (успех)
Обучение как повторяющийся цикл проб и ошибок

Обучиться — значит по обучающей выборке подобрать веса \(w\), при которых предсказания сети \(\hat y\) как можно ближе к истинным значениям \(y\). Чтобы формально сравнивать варианты весов, нужна числовая мера несовпадения \(\hat y\) и \(y\) — функция ошибки. Возможных мер несколько; две употребительные — среднее абсолютное отклонение (MAE) и среднеквадратичное отклонение (MSE):

\[ \mathrm{MAE} = \frac{1}{m}\sum_{i=1}^m |y_i-\hat y_i|, \qquad \mathrm{MSE} = \frac{1}{m}\sum_{i=1}^m (y_i-\hat y_i)^2 \tag{2.1}\]

Обе меры равны нулю при точном совпадении и растут с ростом расхождения, но для дальнейших целей между ними есть решающая разница: \((y-\hat y)^2\) дифференцируема по \(\hat y\) при любом значении, а \(|y-\hat y|\) не имеет производной в точке \(y=\hat y\) (излом графика). Поскольку метод обучения, рассматриваемый ниже, использует производную функции ошибки, из двух мер выбирают квадратичную. Для одного обучающего примера с выходом одного нейрона ошибку по традиции берут в виде

\[ E = \tfrac12(y-\hat y)^2 \tag{2.2}\]

Множитель \(\tfrac12\) на расположение минимума не влияет — он введён исключительно для того, чтобы при дифференцировании (2.2) двойка из показателя степени сократилась с \(\tfrac12\); минимум \(E\) при этом остаётся ровно там же, где и минимум \((y-\hat y)^2\).

Типичная ошибка Считают MAE «неправильной» мерой ошибки, раз для обучения градиентными методами обычно берут MSE. MAE вполне корректно измеряет качество предсказаний (и менее чувствительна к редким большим выбросам, чем MSE) — проблема не в её смысле, а в том, что она хуже подходит именно для градиентных методов оптимизации из-за неопределённой производной в нуле.

2. Производная и градиент

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

Производная функции \(C\) в точке \(w_0\) — предел отношения приращения функции к приращению аргумента при стремлении последнего к нулю:

\[ C'(w_0) = \lim_{\Delta x \to 0} \frac{C(w_0+\Delta x)-C(w_0)}{\Delta x} \tag{2.3}\]

Отношение под знаком предела — это угловой коэффициент секущей, проходящей через точки \((w_0, C(w_0))\) и \((w_0+\Delta x, C(w_0+\Delta x))\). Производная — это то значение, к которому угловой коэффициент секущей стремится, когда вторая точка приближается к первой, то есть угловой коэффициент касательной в точке \(w_0\).

Тренажёр: производная как предел секущей

Синяя линия — секущая через точки \(w_0\) и \(w_0+\Delta x\) на кривой \(C(w)=0{,}5w^2\), \(w_0=-2\). Уменьшайте \(\Delta x\) и наблюдайте, как угловой коэффициент секущей приближается к истинной производной \(C'(w_0)\).

2.1. Градиент: производная по нескольким весам

Ошибка \(E\) зависит не от одного веса, а от всех весов сети сразу: \(E=E(w_1,\ldots,w_n)\). Для каждого веса в отдельности можно вычислить частную производную \(\partial E/\partial w_i\) — производную по \(w_i\) при остальных весах, зафиксированных как константы. Вектор всех частных производных называется градиентом:

\[ \nabla E(w) = \left(\frac{\partial E}{\partial w_1},\ldots,\frac{\partial E}{\partial w_n}\right) \tag{2.4}\]

Градиент указывает направление, в котором \(E\) растёт быстрее всего в окрестности точки \(w\). Соответственно, направление наискорейшего убывания \(E\) — противоположное, \(-\nabla E(w)\).

Типичная ошибка Вычисляя \(\partial E/\partial w_i\), забывают, что остальные веса на этот момент считаются константами. Частная производная — это обычная производная функции одной переменной \(w_i\), при вычислении которой все остальные \(w_j\), \(j\ne i\), временно «заморожены».

3. Идея алгоритма обучения: движение против градиента

Раз \(-\nabla E(w)\) указывает направление быстрейшего убывания ошибки, естественный способ подбирать веса — сделать маленький шаг именно в этом направлении, затем пересчитать градиент в новой точке и повторить:

\[ w^{(t+1)} = w^{(t)} - \alpha\,\nabla E\bigl(w^{(t)}\bigr) \tag{2.5}\]

Коэффициент \(\alpha>0\) — скорость обучения (англ. learning rate): она задаёт длину шага. Для одного веса при однозначной связи знака производной и направления изменения правило (2.5) можно проверить прямым рассуждением. Пусть \(E\) как функция одного веса \(w\) имеет вид ямы с минимумом где-то посередине. Рассмотрим точку 1 с некоторым значением \(w_1\) и точку 2 с меньшей ошибкой, \(E_2<E_1\), правее неё; чтобы попасть из точки 1 в точку 2, нужно увеличить \(w\) (\(\Delta w=w_2-w_1>0\)), а производная \(E'(w_1)\) при этом отрицательна (функция убывает слева направо). Рассмотрим и точку 3, из которой в точку 4 с меньшей ошибкой нужно, наоборот, уменьшить \(w\) (\(\Delta w=w_4-w_3<0\)), а производная \(E'(w_3)\) там положительна. В обоих случаях знак \(\Delta w\) противоположен знаку производной — это в точности и есть правило (2.5) при \(n=1\): \(\Delta w = -\alpha E'(w)\).

Типичная ошибка Пишут правило обновления без минуса, \(w^{(t+1)}=w^{(t)}+\alpha\nabla E(w^{(t)})\). Со знаком «плюс» веса будут двигаться в сторону роста ошибки, то есть обучение будет систематически её увеличивать, а не уменьшать.

3.1. Скорость обучения и локальные минимумы

Слишком большая \(\alpha\) заставляет шаг перепрыгивать через минимум и раскачиваться вокруг него или вовсе расходиться; слишком маленькая — делает сходимость крайне медленной. Есть и более фундаментальная проблема: если \(E(w)\) имеет несколько локальных минимумов, градиентный спуск останавливается в том из них, что ближе к стартовой точке, — и он не обязан быть глобальным минимумом.

Тренажёр: локальные минимумы

Шесть шариков расставлены вдоль случайно сгенерированной кривой \(E(w)\) с несколькими локальными минимумами. Запустите спуск — каждый шарик независимо катится против градиента (2.5) в своей точке. Кнопка «новая кривая» генерирует новый рельеф и новую расстановку шариков: одна и та же \(\alpha\) на разных стартовых точках может привести в разные минимумы, и не все из них — глобальный.

На практике с проблемой локальных минимумов борются не перебором всех стартовых точек, а другими средствами (случайной инициализацией весов, модификациями градиентного спуска с «инерцией» и т. п.); подробное рассмотрение этих средств выходит за рамки данного конспекта.

4. Метод обратного распространения ошибки

Правило (2.5) требует градиент \(\nabla E\) — частную производную \(E\) по каждому весу сети. Для одного нейрона с одним слоем весов это несложно (см. §4.1), но в сети с несколькими слоями вес влияет на \(E\) не напрямую, а через цепочку промежуточных нейронов. Метод обратного распространения ошибки — это не отдельный от градиентного спуска алгоритм, а способ эффективно вычислить все нужные частные производные в такой сети с помощью цепного правила дифференцирования сложной функции, переиспользуя уже вычисленные значения, а не пересчитывая их заново для каждого веса.

Прямой проход Обратный проход
Прямой проход вычисляет выход сети; обратный — распространяет ошибку назад для каждого веса

Метод состоит из двух этапов:

4.1. Цепное правило для одного нейрона

Покажем, откуда берётся частная производная \(\partial E/\partial w_i\) для отдельного нейрона с выходом \(\hat y = f(\mathrm{net})\), \(\mathrm{net}=\sum_i x_i w_i\) (для краткости смещение опущено). Ошибка \(E\) зависит от \(w_i\) через две промежуточные величины — \(\hat y\) и \(\mathrm{net}\), — и по цепному правилу:

\[ \frac{\partial E}{\partial w_i} = \frac{\partial E}{\partial \hat y}\cdot\frac{\partial \hat y}{\partial \mathrm{net}}\cdot\frac{\partial \mathrm{net}}{\partial w_i} = -(y-\hat y)\cdot f'(\mathrm{net})\cdot x_i \tag{2.6}\]

(первый множитель — производная (2.2) по \(\hat y\); третий — производная \(\mathrm{net}\) по \(w_i\), равная просто \(x_i\)). Для сигмоиды производная выражается через саму функцию:

\[ f'_{\text{сигм}}(\mathrm{net}) = f_{\text{сигм}}(\mathrm{net})\bigl(1-f_{\text{сигм}}(\mathrm{net})\bigr) = \hat y(1-\hat y) \tag{2.7}\]

Подставив (2.6)–(2.7) в правило обновления (2.5) при \(\alpha=1\), получаем конкретную формулу, которая используется в реализации следующего параграфа:

\[ \Delta w_i = \alpha\,(y-\hat y)\,\hat y(1-\hat y)\,x_i \tag{2.8}\]

Для сети с несколькими слоями та же процедура повторяется слой за слоем справа налево: частная производная \(E\) по весам скрытого слоя выражается через уже вычисленные производные следующего (более близкого к выходу) слоя — отсюда и название «обратное распространение».

Типичная ошибка Считают обратное распространение ошибки самостоятельным методом оптимизации, конкурирующим с градиентным спуском. Обновление весов в (2.8) — это тот же градиентный спуск (2.5); обратное распространение — лишь эффективный порядок вычисления входящего в него градиента.

5. Реализация: свой первый обучаемый нейрон

Соберём формулы (2.6)–(2.8) в код на numpy. Обучающая выборка — четыре примера с тремя входами; целевое значение в каждом примере совпадает со значением первого входа (\(y=x_1\)), так что после обучения веса должны показать: первый вход важен, два остальных — нет.

from numpy import exp, array, random, dot

# обучающая выборка: 4 примера, по 3 входа в каждом
training_set_inputs = array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
# целевой выход — совпадает со значением первого входа
training_set_outputs = array([[0, 1, 1, 0]]).T

# инициализация весов случайными числами из [-1, 1]
random.seed(1)
synaptic_weights = 2 * random.random((3, 1)) - 1

for iteration in range(100000):
    # прямой проход: net = X * w, затем сигмоида (1.7)
    output = 1 / (1 + exp(-dot(training_set_inputs, synaptic_weights)))

    # обратный проход: дельта-правило (2.8) в матричном виде,
    # суммирование по всем 4 примерам выполняется через X^T * (...)
    synaptic_weights += dot(
        training_set_inputs.T,
        (training_set_outputs - output) * output * (1 - output)
    )

print(synaptic_weights)

Число проходов по всей обучающей выборке (здесь — 100000) принято называть числом эпох. Строка внутри цикла, вычисляющая output, — это формула (1.5)+(1.7) (прямой проход); строка, изменяющая synaptic_weights, — это в точности формула (2.8), записанная для всех четырёх обучающих примеров сразу: training_set_inputs.T при умножении на столбец из четырёх значений \((y-\hat y)\hat y(1-\hat y)\) суммирует вклад каждого примера в \(\Delta w_i\) — то же самое, что \(\sum_{\text{примеры}} (y-\hat y)\hat y(1-\hat y)\,x_i\) в (2.8) для каждого веса \(w_i\) по отдельности.

Типичная ошибка Не задав random.seed(1), получают при каждом запуске разные начальные веса и, соответственно, слегка разные (хотя и близкие) итоговые веса. Для воспроизводимости результатов seed фиксируют; это не влияет на то, чему в итоге обучится сеть, только на конкретные числа.

Контрольные вопросы