Машинное обучение

Обратное распространение ошибки

В 1970 году финский математик Сеппо Линнайнмаа описал метод автоматического дифференцирования - способ эффективно вычислять производные сложных функций. В 1986 году Румельхарт, Хинтон и Уильямс применили эту идею к нейросетям и назвали её backpropagation. Казалось, прорыв близко. Но глубокие сети отказывались обучаться: градиенты исчезали, проходя через слои, и первые слои оставались случайными. Наступила зима нейросетей - на 25 лет. Только около 2012 года, когда ReLU заменил sigmoid, а GPU дали вычислительную мощность, backpropagation наконец заработал на полную. Одна и та же идея - но потребовалось 40 лет, чтобы она изменила мир. Давайте разберемся, как именно работает этот алгоритм и почему он так долго ждал своего часа.

  • **Обучение GPT и LLaMA** - каждая итерация обучения большой языковой модели включает forward pass через миллиарды параметров, вычисление loss, и backward pass (backpropagation), который вычисляет градиенты для каждого из этих миллиардов весов за один проход
  • **Распознавание изображений (ResNet)** - residual connections были изобретены именно для борьбы с vanishing gradients, позволив обучать сети с 152 слоями и выиграть ImageNet 2015 с ошибкой ниже человеческой
  • **Autograd в PyTorch** - миллионы разработчиков пишут только forward pass, а backpropagation вычисляется автоматически через вычислительный граф, делая создание нейросетей доступным без глубокого знания математики

Алгоритм, переоткрытый трижды

Обратное распространение - это, по сути, эффективное применение цепного правила, и его открывали несколько раз независимо. В 1970 году финский математик Сеппо Линнайнмаа описал обратный режим автоматического дифференцирования - способ вычислять производные сложных функций, накапливая их от выхода к входу. В 1974 году Пол Вербос в своей докторской диссертации применил эту идею к обучению нейронных сетей, но работа осталась почти незамеченной. Лишь в 1986 году Дэвид Румельхарт, Джеффри Хинтон и Рональд Уильямс опубликовали статью, которая сделала backpropagation общеизвестным методом обучения многослойных сетей. Алгоритм был готов задолго до того, как у мира появились данные и вычислительные мощности, чтобы раскрыть его силу: понадобилось ещё около сорока лет.

Предварительные знания

  • Fully Connected Neural Networks (MLP)

Цепное правило (Chain Rule)

Чтобы обучить нейросеть, нам нужно знать: **как изменение каждого веса влияет на итоговую ошибку?** Это и есть градиент - производная функции потерь по каждому весу. Проблема в том, что нейросеть - это композиция функций: вход проходит через слой 1, потом через слой 2, потом через функцию потерь. Как взять производную от такой цепочки? Ответ дает chain rule (цепное правило) из математического анализа.

**Chain Rule (цепное правило):** Если y = f(g(x)), то производная: `dy/dx = f'(g(x)) * g'(x)` Словами: производная композиции = производная внешней функции * производная внутренней функции. Пример: y = (3x + 2)^2 - Внешняя: f(u) = u^2, f'(u) = 2u - Внутренняя: g(x) = 3x + 2, g'(x) = 3 - dy/dx = 2(3x + 2) * 3 = 6(3x + 2)

Теперь перенесем это на нейросеть. Каждый слой - это функция: z = W*x + b, a = sigma(z). Функция потерь L зависит от выхода последнего слоя. Чтобы узнать dL/dW для конкретного слоя, мы применяем chain rule, **проходя по цепочке от выхода ко входу**. Каждый множитель в цепочке - это локальная производная одного слоя.

**Chain rule при нескольких путях:** Если переменная влияет на выход по нескольким путям, градиенты **складываются**. Например, если x используется в двух слоях, итоговый градиент dL/dx = (градиент по первому пути) + (градиент по второму пути). Это следствие multivariable chain rule и станет особенно важным при работе с residual connections.

Нейросеть состоит из 3 слоев: x -> f -> g -> h -> Loss. Какое выражение правильно описывает градиент dLoss/dx через chain rule?

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

Теперь посмотрим, как chain rule работает в реальной нейросети с несколькими слоями. При forward pass данные текут от входа к выходу: каждый слой получает вход, вычисляет выход и передает его дальше. При backward pass **градиенты текут в обратном направлении**: от функции потерь через каждый слой назад к входу. Каждый слой получает градиент от следующего слоя и вычисляет два вида градиентов: 1. по своим весам - для обновления 2. по своему входу - для передачи предыдущему слою.

**Локальные градиенты для типичных операций:** - **Умножение** z = w * x: dz/dw = x, dz/dx = w - **Сложение** z = x + b: dz/dx = 1, dz/db = 1 - **Sigmoid** a = 1/(1 + exp(-z)): da/dz = a * (1 - a) - **ReLU** a = max(0, z): da/dz = 1 если z > 0, иначе 0 - **MSE Loss** L = (y - t)^2: dL/dy = 2(y - t) Каждый узел запоминает свои значения при forward pass, чтобы использовать их при backward pass для вычисления локальных градиентов.

Обратите внимание на ключевое свойство: **градиент для каждого следующего (более раннего) слоя - это произведение всех локальных градиентов от конца до этого слоя.** Для первого слоя мы перемножили 5 локальных градиентов. Для сети с 50 слоями мы перемножим 100+ чисел. Если эти числа меньше 1, произведение стремится к нулю. Если больше 1 - к бесконечности. Это порождает фундаментальные проблемы, о которых поговорим далее.

**Forward pass сохраняет промежуточные значения!** При backward pass нам нужны значения z1, a1, z2 из forward pass. Поэтому нейросеть хранит все промежуточные активации в памяти до завершения backward pass. Это объясняет, почему обучение требует в 2-3 раза больше памяти GPU, чем инференс (prediction). Для очень глубоких сетей используют gradient checkpointing - пересчитывают промежуточные значения вместо хранения.

В нейросети из 3 слоев при backward pass слой 2 получает dL/da2 от слоя 3. Что слой 2 должен вычислить и передать дальше слою 1?

Vanishing и Exploding Gradients

Мы выяснили, что градиент для ранних слоев - это произведение локальных градиентов всех последующих слоев. В этом скрыта фундаментальная проблема. Вспомним sigmoid: его максимальная производная равна 0.25 (при z = 0). Если в сети 10 слоев с sigmoid, градиент первого слоя содержит произведение ~10 таких множителей: 0.25^10 = 0.00000095. Градиент **исчезает** - первые слои практически не обучаются.

Обратная проблема - **exploding gradients**: если локальные градиенты больше 1, их произведение экспоненциально растет. Веса начинают прыгать, loss уходит в NaN, обучение рушится. Это чаще встречается в рекуррентных сетях (RNN), где один и тот же слой применяется сотни раз (по числу шагов последовательности).

**Историческое влияние:** Backpropagation был описан Линнайнмаа в 1970 году и популяризирован Румельхартом, Хинтоном и Уильямсом в 1986. Но глубокие сети не работали из-за vanishing gradients. Наступила "зима AI": сети с 2-3 слоями считались пределом. Прорыв произошел около 2012 года благодаря трем факторам: 1. ReLU вместо sigmoid 2. мощные GPU для быстрого обучения 3. большие датасеты (ImageNet). Внезапно сети с 8, 16, даже 152 слоями стали обучаемыми.

  • **ReLU (Rectified Linear Unit):** f(z) = max(0, z), производная = 1 при z > 0. Не уменьшает градиент при положительных z. Главная причина успеха глубоких сетей
  • **Residual connections (skip connections):** y = f(x) + x. Градиент может течь через "+" напрямую (производная = 1), минуя слои. Позволяет обучать сети с 100+ слоями (ResNet)
  • **Batch Normalization:** нормализует активации каждого слоя, стабилизируя диапазон градиентов. Ускоряет обучение в 5-10 раз
  • **Правильная инициализация весов:** He init (для ReLU) и Xavier init (для tanh) устанавливают начальные веса так, чтобы дисперсия активаций сохранялась по слоям
  • **Gradient clipping:** для exploding gradients - обрезаем градиент, если его норма превышает порог. Стандартный прием в RNN/Transformer

Сеть из 15 слоев с sigmoid активацией обучается очень медленно: loss почти не уменьшается. Какова наиболее вероятная причина и решение?

Вычислительный граф

Современные фреймворки (PyTorch, TensorFlow) не требуют ручного вычисления градиентов. Вместо этого они строят **вычислительный граф** - DAG (directed acyclic graph), где каждый узел - это операция (сложение, умножение, sigmoid), а ребра - потоки данных. При forward pass фреймворк записывает все операции в граф. При вызове `.backward()` он автоматически проходит по графу в обратном порядке, вычисляя градиенты через chain rule. Это называется **automatic differentiation (autograd)**.

Прелесть вычислительного графа в том, что фреймворку не нужно знать формулу всей функции целиком. Он знает производные для каждой элементарной операции (+, *, exp, log, matmul) и просто применяет chain rule автоматически. Вы пишете forward pass - фреймворк строит граф. Вы вызываете `.backward()` - фреймворк вычисляет все градиенты. Это разделение делает код нейросетей удивительно простым.

**Dynamic vs Static графы:** **PyTorch (dynamic graph):** граф строится заново при каждом forward pass. Можно использовать обычный Python (if/else, циклы). Удобно для отладки и исследований. **TensorFlow 1.x (static graph):** граф строился один раз, потом выполнялся многократно. Быстрее при production inference, но сложнее отлаживать. **TensorFlow 2.x / JAX:** гибридный подход - eager mode по умолчанию (как PyTorch), но можно компилировать в статический граф через `@tf.function` или `jax.jit` для ускорения.

На практике вы почти никогда не вычисляете градиенты вручную. Цикл обучения выглядит так: 1. forward pass через модель 2. вычисление loss 3. `loss.backward()` - autograd вычисляет все градиенты 4. `optimizer.step()` - оптимизатор обновляет веса, используя эти градиенты 5. `optimizer.zero_grad()` - обнуляем градиенты перед следующей итерацией. Backpropagation - это шаг 3, но он полностью автоматизирован.

**optimizer.zero_grad() обязателен!** PyTorch по умолчанию **накапливает** градиенты при каждом вызове `.backward()`. Если не обнулить их перед следующей итерацией, новые градиенты прибавятся к старым. Это полезно для gradient accumulation (эмуляция большого batch size), но в обычном обучении - это баг, который приводит к нестабильности. Всегда вызывайте `optimizer.zero_grad()` в начале или в конце итерации.

Backpropagation - это алгоритм обучения нейросети

Backpropagation - это алгоритм эффективного ВЫЧИСЛЕНИЯ ГРАДИЕНТОВ. Обучение = backprop + optimizer (SGD, Adam, AdaGrad и др.). Backprop говорит 'куда двигаться', optimizer решает 'как далеко и с какой стратегией'

Это частая путаница. Backprop только отвечает на вопрос 'как каждый вес влияет на ошибку?', вычисляя dLoss/dW. Но что делать с этой информацией - решает оптимизатор. SGD просто вычитает lr * grad, Adam дополнительно учитывает историю градиентов и адаптирует lr для каждого веса. Одни и те же градиенты (от backprop) дают разные результаты с разными оптимизаторами.

В PyTorch вы написали forward pass, вызвали loss.backward() и затем optimizer.step(). Какую роль играет каждый из этих вызовов?

Итоги

  • **Chain rule** - фундамент backpropagation: производная композиции функций равна произведению локальных производных, что позволяет вычислять градиенты слой за слоем от выхода к входу
  • **Поток градиентов:** при backward pass каждый слой получает градиент от следующего, умножает на свой локальный градиент и передает предыдущему - forward pass сохраняет промежуточные значения для этих вычислений
  • **Vanishing/Exploding gradients:** sigmoid (макс. производная 0.25) приводит к исчезновению градиентов в глубоких сетях - ReLU (производная 1), residual connections и batch normalization решают эту проблему
  • **Вычислительный граф:** PyTorch/TensorFlow автоматически строят граф операций при forward pass и вычисляют все градиенты при вызове `.backward()` - backprop + optimizer = обучение
  • **Путь длиной в 40 лет:** от идеи Линнайнмаа (1970) через статью Хинтона (1986) до GPU-революции (2012) - backpropagation не изменился, но ReLU, residual connections и вычислительные мощности наконец позволили ему раскрыть свой потенциал

Связанные темы

Backpropagation - центральный механизм обучения нейросетей, связывающий архитектуру сети с методами оптимизации:

  • Функции активации — Выбор активации напрямую определяет поток градиентов: ReLU, LeakyReLU, GELU решают проблему vanishing gradients разными способами, каждый со своими компромиссами
  • Оптимизаторы — Backpropagation вычисляет градиенты, а оптимизатор решает, как их использовать: SGD, Momentum, Adam, AdaGrad - разные стратегии обновления весов на основе одних и тех же градиентов
  • Нейронные сети — Backpropagation - это метод обучения архитектуры, описанной в предыдущем уроке: forward pass вычисляет предсказание, backward pass вычисляет градиенты для обновления весов

Вопросы для размышления

  • Почему произведение множителей меньше 1 в chain rule создает экспоненциальное затухание, а не линейное? Что это означает для разницы между сетью из 10 и 20 слоев?
  • Residual connections добавляют shortcut y = f(x) + x. Как именно '+x' помогает градиентам течь через 100+ слоев? Что происходит с градиентом при обратном проходе через операцию сложения?
  • PyTorch autograd полностью автоматизирует backpropagation. Зачем тогда понимать, как он работает внутри? В каких ситуациях это знание критически важно?

Связанные уроки

  • ml-25-neural-networks — Бэкпроп обучает MLP из прошлого урока
  • ml-27-activation-functions — Производные активаций входят в каждый градиент
  • ml-28-optimizers — Посчитанные градиенты потребляют оптимизаторы
  • calc-08-chain-rule — Бэкпроп - цепное правило, применённое послойно
  • calc-19-gradient — Он вычисляет градиент потерь по всем весам
  • aie-36-fine-tuning
Обратное распространение ошибки

0

1

Войти