Backprop это не отдельная магия, а цепное правило, применённое к вычислительному графу в правильном порядке. На собесе часто дают маленький граф и просят пройти обратный проход руками.
Цепное правило
Если y = f(g(x)), то
dy/dx = f'(g(x)) · g'(x)
Пример: (sin(x²))' = cos(x²)·2x. Самая частая ошибка: забыть множитель производной внутренней функции.
Если переменная влияет на результат через несколько путей, вклады путей складываются:
a = 2x, b = x², L = L(a, b)
dL/dx = dL/da · 2 + dL/db · 2x
Вычислительный граф
Любое вычисление можно записать как граф из простых операций. Возьмём f = (x + y)·z при x = 2, y = -1, z = 3.
Прямой проход:
q = x + y = 1
f = q · z = 3
Обратный проход начинается с df/df = 1 и идёт от выхода ко входам. Каждый узел умножает пришедший сверху градиент на свой локальный:
df/dq = z = 3 df/dz = q = 1
df/dx = df/dq · 1 = 3
df/dy = df/dq · 1 = 3
Правила для типовых узлов
- Сумма копирует градиент на все входы. Поэтому residual-соединения пропускают градиент без потерь.
- Умножение отдаёт каждому входу градиент, умноженный на значение другого входа.
- max и ReLU маршрутизируют: градиент получает только «победивший» вход, у ReLU при x ≤ 0 градиент ноль.
- Разветвление (переменная используется дважды) складывает градиенты.
Отдельно запомните связку softmax и кросс-энтропии: градиент по логитам равен p - y, где p вероятности, y one-hot метка. Для логистической регрессии то же: dL/dz = σ(z) - y.
Прямой и обратный режим
Автодифференцирование бывает двух видов:
- Прямой режим за один проход считает производные всех выходов по одному входу.
- Обратный режим за один проход назад считает производные одного выхода по всем входам.
У нас один скалярный loss и миллионы параметров, поэтому выигрывает обратный режим, это и есть backprop. Обратный проход стоит примерно 2-3 прямых. Платим памятью: все промежуточные активации прямого прохода нужно хранить до backward. Когда памяти не хватает, используют gradient checkpointing: хранят часть активаций, остальные пересчитывают.
Автодифф точен до машинной точности: он не приближает производную разностями, а применяет точные локальные производные.
Затухание и взрыв градиентов
Градиент первого слоя равен произведению множителей по всем слоям выше. Если множители меньше 1 (производная сигмоиды не больше 0.25), произведение экспоненциально убывает, если больше 1, взрывается. Лекарства:
- ReLU-подобные активации;
- правильная инициализация (Xavier, He);
- нормализация (BatchNorm, LayerNorm);
- residual-соединения;
- gradient clipping против взрыва.
Остановка градиента
Иногда значение нужно использовать, но градиент через него не пропускать: цель в DQN, учитель в дистилляции. В PyTorch это .detach(): тензор с теми же данными, но отрезанный от графа.
Частые ошибки
- Взять собственный множитель вместо чужого в узле умножения.
- Забыть сложить градиенты переменной, использованной в нескольких местах.
- Считать, что обратный режим экономит память: наоборот, он её тратит на активации.
Что спросят на собесе
- Пройти прямой и обратный проход для f = (x + y)·z или похожего графа.
- Как узлы суммы, умножения и max передают градиент.
- Почему backprop эффективнее численного градиента и чем он платит.
- Откуда берётся затухание градиента и как с ним борются.
- Чему равен градиент softmax с кросс-энтропией по логитам.