Глубокое обучение

Backpropagation: как нейронные сети учатся

Geoffrey Hinton и возрождение нейронных сетей

В 2006 году Джефри Хинтон опубликовал работу о «deep belief networks», которая перезапустила интерес к нейронным сетям после долгой «зимы AI». Он показал, что глубокие сети можно обучать через предобучение слоёв по очереди, обойдя проблему vanishing gradients. Через 6 лет его студент Alex Krizhevsky применил backprop с ReLU и dropout на GPU - и AlexNet изменил всё.

Цели урока

  • Понимать chain rule как математическую основу backpropagation
  • Различать forward и backward pass и знать их вычислительную сложность
  • Читать и понимать computational graph для простых функций
  • Использовать PyTorch autograd для вычисления градиентов

Backpropagation переоткрывали 4 раза: 1960 (Kelley), 1970 (Linnainmaa), 1974 (Werbos), 1986 (Rumelhart). Один алгоритм - вся глубокая сеть. Каждый GPT токен - один forward + backward pass. GPT-4 с 1.8 триллиона параметров использует тот же chain rule, что решает y = sin(x²) за 2 строки кода.

  • **Обучение GPT-4** - без backpropagation вычисление 1.8T градиентов было бы экономически невозможным: в миллиарды раз дороже
  • **AlphaFold2 (DeepMind)** - backprop обучает сеть предсказывать 3D-структуру белков, революция в молекулярной биологии
  • **Tesla Autopilot** - сеть обучается на миллионах кадров с камер, backprop корректирует миллионы параметров после каждого batch
  • **Stable Diffusion** - каждый шаг denoising обратного диффузии - это forward pass, DDPM обучается через backprop по всем шагам
  • **GitHub Copilot** - Codex (GPT-3 fine-tuned на коде) обучен через backprop на 159 GB исходного кода

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

  • Нейронные сети: от биологии к математике

Chain Rule: производная сложной функции

Backpropagation переоткрывали четыре раза: Kelley (1960), Linnainmaa (1970), Werbos (1974), Rumelhart (1986). Один и тот же алгоритм. Математика за ним - **chain rule (правило цепочки)**: если f зависит от g, а g зависит от x, то производная f по x = произведение локальных производных. Формально: если y = f(g(x)), то dy/dx = df/dg * dg/dx.

Нейронная сеть - это просто очень длинная цепочка функций. ResNet-152 - 152 таких звена. GPT-4 - тысячи. Chain rule позволяет вычислить, как изменение каждого параметра влияет на итоговую ошибку, не разворачивая всю цепочку вручную.

**Для нескольких переменных** chain rule работает так же. Если z = f(x, y), x = x(t), y = y(t), то dz/dt = (∂z/∂x) * (dx/dt) + (∂z/∂y) * (dy/dt). В нейронных сетях это означает: градиент по весу зависит от того, через какие нейроны проходит сигнал.

**Численная проверка градиентов** (gradient checking) - инструмент отладки. Если аналитический и численный градиенты отличаются более чем на 1e-5, в коде ошибка. Karpathy рекомендует всегда проверять градиенты при написании собственных слоёв. `torch.autograd.gradcheck` делает это автоматически.

Дано: y = sin(x²). Чему равна dy/dx по chain rule?

Forward и Backward pass

Каждый токен GPT-4 - это один forward pass и один backward pass. Forward pass: данные идут от входа к выходу, сеть делает предсказание. Backward pass: ошибка идёт от выхода ко входу, вычисляются градиенты для каждого из 1.8 триллиона параметров. Это и есть backpropagation - chain rule, применённый системно.

**Vanishing gradients:** при использовании sigmoid производная <= 0.25. Через 10 слоёв: 0.25^10 ≈ 0.000001 - градиент исчезает. **Exploding gradients:** если веса > 1, градиенты экспоненциально растут и становятся NaN. Решения: ReLU (vanishing), gradient clipping (exploding), residual connections (оба). ResNet решил vanishing gradients в 2015 именно через skip connections.

ПроблемаСимптомРешение
Vanishing gradientsПервые слои не обучаются, loss не падаетReLU, residual connections, batch norm
Exploding gradientsLoss = NaN, веса → ±∞Gradient clipping, правильная инициализация
Dead neuronsНейрон всегда выдаёт 0 (ReLU)Leaky ReLU, правильный learning rate

**Ключевое наблюдение:** backward pass имеет ту же вычислительную сложность, что и forward pass - O(n), где n - количество операций. Это значит, что градиенты для ВСЕХ параметров (миллионов или миллиардов) получаются за один проход. Именно это сделало deep learning экономически возможным.

Что происходит с градиентами в глубокой сети при использовании sigmoid во всех слоях?

Computational Graph: карта вычислений

Миллионы операций в сети - как организовать вычисление их градиентов? **Computational graph** - направленный ациклический граф (DAG), где каждый узел - операция, рёбра - потоки данных. При forward pass каждый узел вычисляет результат и запоминает локальные градиенты. При backward pass - передаёт градиент назад. Это не абстракция - именно так хранится граф в PyTorch.

Красота computational graph: каждый узел знает только свою **локальную** операцию. Узел «+» знает, что производная суммы по каждому входу = 1. Узел «*» знает, что производная произведения по одному входу = другой вход. Всю остальную работу делает chain rule - просто умножаем локальные градиенты вдоль пути. Никакого глобального знания о сети не нужно.

Этот класс Value - упрощённая версия того, как работает PyTorch внутри. Andrej Karpathy создал на его основе библиотеку **micrograd** (100 строк кода), которая реализует полноценный autograd. Понимание этого кода - ключ к пониманию PyTorch.

**Паттерн `grad +=`** (а не `grad =`) важен. Если переменная используется в нескольких операциях, градиенты от всех путей **суммируются**. Это следствие multivariate chain rule: dL/dx = сумма (dL/dyi * dyi/dx) по всем yi, зависящим от x. В BERT один embedding токена используется в сотнях attention операций - градиенты от всех путей суммируются.

В computational graph для f = (x + y) · z при x=-2, y=5, z=-4, чему равен ∂f/∂y?

Autograd: автоматическое дифференцирование

Вычислять градиенты вручную для 1.8 триллиона параметров - невозможно. PyTorch делает это автоматически через **autograd**: каждая операция с тензорами записывается в computational graph, и при вызове `.backward()` градиенты вычисляются для всех параметров за один проход. Тот же алгоритм что и выше - просто в 100 строк C++ вместо Python.

На практике autograd используется в тренировочном цикле. Forward pass вычисляет loss, backward pass вычисляет градиенты, optimizer обновляет веса. Этот паттерн одинаков для XOR на 2 нейронах и для LLaMA-70B.

**optimizer.zero_grad() - критически важен!** По умолчанию PyTorch **накапливает** градиенты (+=, а не =). Без обнуления градиенты от предыдущего batch добавятся к текущим, и обучение сломается. Частая ошибка в production-коде.

**torch.no_grad()** отключает построение computational graph. Используйте при inference и валидации - это экономит память (не хранятся промежуточные значения для backward pass) и ускоряет вычисления на 20-40%.

Для отладки градиентов: `tensor.grad_fn` показывает, какая операция создала тензор. `tensor.requires_grad` - отслеживается ли он. Если `.grad` равен `None` после `.backward()` - тензор не был частью computational graph (забыли `requires_grad=True`).

Backpropagation - это алгоритм обучения нейронных сетей. Backprop = градиентный спуск.

Backpropagation - алгоритм **вычисления градиентов** через chain rule. Градиентный спуск (или Adam, SGD, RMSProp) - алгоритм, который **использует** эти градиенты для обновления весов.

Разделение ответственности: backprop вычисляет dL/dw для каждого веса, а optimizer решает, как обновить веса. Один и тот же backprop с разными optimizer (SGD, Adam, RMSProp) даёт разные результаты. Adam делает градиентный спуск с адаптивным learning rate - но градиенты приходят от backprop.

Зачем вызывать optimizer.zero_grad() перед loss.backward()?

Ключевые идеи

  • **Chain rule** - математическая основа: производная сложной функции = произведение локальных производных. Backpropagation - это chain rule, применённый к computational graph
  • **Forward pass** вычисляет предсказание, **backward pass** вычисляет градиенты для всех весов. Оба имеют сложность O(n)
  • **Computational graph** организует вычисления: каждый узел знает только свой локальный градиент. PyTorch строит этот граф автоматически
  • **Autograd** автоматизирует всё: .backward() вычисляет градиенты, optimizer.step() обновляет веса. optimizer.zero_grad() - обязателен
  • Vanishing gradients (sigmoid) убили первые глубокие сети. ReLU + residual connections (ResNet, 2015) решили проблему
  • Backprop переоткрывали 4 раза. Geoffrey Hinton в 2006 вернул глубокие сети из AI-зимы через deep belief networks
  • Backprop != gradient descent. Backprop вычисляет gradients. Adam/SGD решают, что с ними делать

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

Backpropagation - центральный алгоритм deep learning:

  • Нейронные сети — Backprop обучает сети, которые построили в предыдущем уроке
  • PyTorch vs TensorFlow — Фреймворки реализуют autograd по-разному
  • Gradient Descent — Backprop вычисляет градиенты, GD использует их для обновления весов

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

  • Backpropagation был открыт в 1970-х, но нейронные сети «выстрелили» только в 2012. Что изменилось - алгоритмы, данные или оборудование?
  • Почему PyTorch по умолчанию накапливает градиенты (+=), а не перезаписывает (=)? В каких ситуациях накопление полезно?
  • Если autograd вычисляет градиенты автоматически, зачем разработчику понимать, как работает backpropagation?

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

  • dl-01 — Архитектура нейронной сети, которую backprop обучает
  • dl-03 — PyTorch и TensorFlow реализуют autograd по-разному
  • ml-09-gradient-descent — Backprop вычисляет градиенты, gradient descent их использует
  • calc-08-chain-rule — Chain rule - математическая основа всего backprop
  • calc-18-partial — Частные производные для многомерного chain rule
  • ml-28-optimizers — Adam, RMSProp, SGD - алгоритмы на выходе backprop
Backpropagation: как нейронные сети учатся

0

1

Войти