Модуль: Обратное распространение


Проверка и автоматический backward


Проверка градиентов

Как убедиться, что собственный backward считает производные правильно

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

Мы самостоятельно написали backward и получили градиенты параметров сети. Но работающий код ещё не гарантирует, что эти градиенты правильные.

Можно ошибиться в знаке, забыть один множитель или неправильно передать градиент через слой. Программа при этом может выполняться без ошибок, а формы массивов — полностью совпадать.

Поэтому полезно проверить несколько градиентов другим способом.

Проверяем один вес

Для первого веса первого слоя:

\[ W_{1,1}=0.5. \]

Наш backward получил:

\[ \frac{\partial L}{\partial W_{1,1}}=-184. \]

Теперь попробуем получить ту же производную, вообще не используя формулы backward.

Немного изменим вес

Возьмём очень маленькое число:

\[ h=0.00001. \]

Сначала немного увеличим вес:

\[ w_+=w+h=0.50001. \]

Затем немного уменьшим:

\[ w_-=w-h=0.49999. \]

Во всех остальных местах сеть остаётся неизменной.

Для каждого из двух значений веса выполняем обычный forward и получаем две функции потерь:

\[ L_+=L(w+h), \qquad L_-=L(w-h). \]

Центральная разность

Производную можно приблизительно оценить по значениям функции слева и справа от исходной точки:

\[ \frac{\partial L}{\partial w} \approx \frac{L(w+h)-L(w-h)}{2h}. \]

Для нашей сети получаем:

\[ L_+\approx132.2481600064, \]

\[ L_-\approx132.2518400064. \]

Подставляем:

\[ g_{\text{num}} = \frac{ 132.2481600064-132.2518400064 }{ 2\cdot0.00001 }. \]

Получаем:

\[ g_{\text{num}}\approx-184. \]

Наш backward

\[ g_{\text{backprop}}=-184. \]

Численная проверка

\[ g_{\text{num}}\approx-184. \]

Результаты практически совпали. Это хороший признак того, что производная для этого веса вычислена правильно.

Gradient check — проверка аналитического градиента независимой численной оценкой.

Если два результата близки, backward, скорее всего, работает правильно для проверяемого параметра.

Если расхождение заметное, нужно искать ошибку в обратном проходе.

Тот же расчёт на NumPy
h = 0.00001

W1_plus = W1.copy()
W1_plus[0, 0] += h

z1_plus = x @ W1_plus + b1
a1_plus = np.maximum(0, z1_plus)
pred_plus = a1_plus @ W2 + b2
loss_plus = (pred_plus - y) ** 2


W1_minus = W1.copy()
W1_minus[0, 0] -= h

z1_minus = x @ W1_minus + b1
a1_minus = np.maximum(0, z1_minus)
pred_minus = a1_minus @ W2 + b2
loss_minus = (pred_minus - y) ** 2


numeric = (loss_plus - loss_minus) / (2 * h)

print(numeric)

Получим значение, очень близкое к:

\[ -184. \]

В этом коде нет нашего backward. Мы только дважды выполнили forward с немного разными значениями одного веса.

Именно поэтому такая проверка независима от формул, которыми мы вычисляли градиент.

Почему используется два forward

Можно было бы изменить вес только в одну сторону и сравнить два значения функции:

\[ \frac{L(w+h)-L(w)}{h}. \]

Но центральная разность:

\[ \frac{L(w+h)-L(w-h)}{2h} \]

обычно даёт более точную оценку производной, потому что использует значения функции по обе стороны от исходной точки.

Почему h должен быть маленьким

Производная описывает поведение функции совсем рядом с выбранной точкой. Поэтому \(h\) должен быть небольшим.

Если взять слишком большое \(h\), мы будем измерять изменение функции уже на заметном участке, а не только около точки \(w\).

Но делать \(h\) бесконечно маленьким тоже нельзя: начинают сильнее проявляться погрешности вычислений с числами с плавающей точкой.

Для простых проверок можно пробовать, например:

\[ 10^{-4}, \qquad 10^{-5}, \qquad 10^{-6}. \]

Нас интересует не точное совпадение всех десятичных знаков, а близость результатов.

Gradient check нужен для проверки, а не для обучения

Чтобы численно проверить только один вес, нам уже понадобилось два дополнительных forward-прохода.

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

Поэтому численный градиент не заменяет backward.

Backward — способ эффективно получить градиенты всех параметров.

Gradient check — способ проверить, что некоторые из этих градиентов посчитаны правильно.

Почему backward нужны значения из forward

Во время обратного прохода мы постоянно использовали результаты, полученные раньше.

Например:

  • для производной функции потерь нужен прогноз \(\hat y\);
  • для градиентов \(W_2\) нужны значения скрытого слоя \(a_1\);
  • для ReLU нужно знать, были ли значения \(z_1\) положительными;
  • для градиентов \(W_1\) нужен вход \(x\).

Значит, во время обучения некоторые промежуточные результаты forward нужно сохранить до начала backward.

Forward во время обучения делает две вещи:

получает результат сети;

и оставляет промежуточные значения, которые понадобятся во время backward.

Почему обучение требует больше памяти

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

Но при обучении после forward ещё предстоит backward. Поэтому нужные значения должны оставаться в памяти.

Это одна из причин, почему обучение модели обычно требует больше памяти, чем простой запуск уже обученной сети.

Что мы теперь умеем

Мы умеем самостоятельно выполнить forward.

Умеем пройти вычислительный граф назад и выполнить backward.

Умеем получить градиенты весов и смещений.

Умеем проверить отдельный градиент независимым численным способом.

А нужно ли всегда писать backward руками?

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

Но в реальной нейронной сети могут быть миллионы параметров и огромное количество операций. Писать весь обратный проход вручную каждый раз было бы крайне неудобно.

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

Следующий естественный шаг — автоматизировать backward.

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

Для закрепления реши задачи практического курса