Перейти к содержанию
Educora
Университет22 мин38 / 42

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

Как PyTorch сам вычисляет производные: вычислительный граф, requires_grad, backward() и .grad, цепное правило, накопление градиентов, torch.no_grad() и градиентный спуск с autograd.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять, как работают вычислительный граф и обратный проход (backpropagation)
  • Вычислять градиенты с помощью requires_grad, .backward() и .grad и проверять их вручную по цепному правилу
  • Обнулять градиенты и правильно использовать torch.no_grad()
  • Писать градиентный спуск с нуля с помощью autograd

В первом уроке мы вывели градиенты линейной регрессии вручную — всего для двух параметров. В современных нейросетях миллионы параметров, в больших языковых моделях — миллиарды; выводить каждую производную на бумаге невозможно. Механизм autograd в PyTorch делает это автоматически: он запоминает каждую операцию с тензорами, а затем одной командой вычисляет точные производные по всем параметрам. Это не приближённые численные разности и не символьная алгебра — это автоматическое дифференцирование.

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

Каждая операция с тензором, у которого requires_grad=True, добавляет узел в ориентированный ациклический граф (DAG). Листья графа — входы и параметры, корень — функция потерь. Во время прямого прохода (forward pass) граф строится и вычисляются значения; во время обратного прохода (backward pass) autograd идёт от корня к листьям и в каждом узле применяет цепное правило. В нейросетях этот процесс называется обратным распространением ошибки (backpropagation).

УзелПрямой проходЛокальная производнаяgrad_fn
uu = w · x∂u/∂w = xMulBackward0
ŷŷ = u + b∂ŷ/∂u = 1, ∂ŷ/∂b = 1AddBackward0
ee = ŷ − y∂e/∂ŷ = 1SubBackward0
LL = e²∂L/∂e = 2ePowBackward0
Граф функции потерь L = (w · x + b − y)². При обратном проходе локальные производные перемножаются снизу вверх: ∂L/∂w = 2e · 1 · 1 · x.

В PyTorch граф динамический (define-by-run): он строится заново при каждом прямом проходе. Поэтому внутри модели можно свободно использовать обычные if и for из Python — граф будет соответствовать тому пути, который реально выполнился.

СпособКак работаетНедостаток
Численное дифференцирование(f(θ + h) − f(θ − h)) / 2hприближённое и требует двух вычислений на каждый параметр
Символьное дифференцированиепреобразует формулу по правилам алгебры (как SymPy)для больших моделей выражения разрастаются; циклы и ветвления обрабатывать трудно
Автоматическое дифференцирование (autograd)объединяет локальные производные выполненных операций по цепному правилунужна дополнительная память для хранения промежуточных значений

requires_grad, backward() и .grad

Python
import torch

x = torch.tensor(3.0, requires_grad=True)
y = x ** 2 + 2 * x + 1
print(y)
y.backward()
print(x.grad)
Ожидаемый результат
tensor(16., grad_fn=<AddBackward0>)
tensor(8.)
  • requires_grad=True говорит PyTorch: «по этому тензору понадобится производная, запоминай операции». Параметры (веса nn.Linear и т. д.) делают это автоматически.
  • grad_fn показывает, какая операция создала тензор; это его узел в графе.
  • y.backward() вычисляет dy/dx и записывает результат в .grad листа: для y = x² + 2x + 1 при x = 3 dy/dx = 2x + 2 = 8.

Если параметр — вектор, то .grad — вектор той же формы: по частной производной на каждый элемент. Например, для f = ∑ vᵢ² имеем ∂f/∂vᵢ = 2vᵢ:

Python
import torch

v = torch.tensor([1.0, -2.0, 3.0], requires_grad=True)
f = (v ** 2).sum()
f.backward()
print(v.grad)
Ожидаемый результат
tensor([ 2., -4.,  6.])

Флаг requires_grad можно менять и потом: p.requires_grad_(False) «замораживает» параметр — его градиент не вычисляется, и при обучении он не меняется. Этим пользуются при переносе обучения (transfer learning): большинство слоёв предобученной сети замораживают, а под новую задачу дообучают только последний слой. Это мы увидим в уроке о свёрточных сетях.

Цепное правило

dL/dx = dL/dy · dy/dx ∂L/∂w = ∂L/∂ŷ · ∂ŷ/∂wdL/dx = dL/dy · dy/dx ∂L/∂w = ∂L/∂ŷ · ∂ŷ/∂w
где:
  • Lпотери (корень графа)
  • y, ŷпромежуточные значения (внутренние узлы графа)
  • x, wлистья: входы и параметры

Производная сложной функции равна произведению локальных производных. Если переменная влияет на потери по нескольким путям, произведения по всем путям складываются: ∂L/∂x = ∑ₖ ∂L/∂yₖ · ∂yₖ/∂x.

Пример 1: градиент линейной модели

ŷ = w · x + b, L = (ŷ − y)². При w = 2, b = 1, x = 3, y = 10 найди ∂L/∂w и ∂L/∂b по цепному правилу.

Показать решение
Прямой проход: ŷ = 2 · 3 + 1 = 7, L = (7 − 10)² = 9.
∂L/∂ŷ = 2(ŷ − y) = −6.
∂ŷ/∂w = x = 3 ⇒ ∂L/∂w = −6 · 3 = −18.
∂ŷ/∂b = 1 ⇒ ∂L/∂b = −6.
Оба градиента отрицательны: увеличение w и b уменьшит потери — и правда, ŷ = 7 меньше цели 10.
Python
import torch

w = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)
x, y = torch.tensor(3.0), torch.tensor(10.0)
y_hat = w * x + b
loss = (y_hat - y) ** 2
print(type(loss.grad_fn).__name__, type(y_hat.grad_fn).__name__)
loss.backward()
print(loss.item(), w.grad.item(), b.grad.item())
Ожидаемый результат
PowBackward0 AddBackward0
9.0 -18.0 -6.0
Autograd подтверждает найденные вручную −18 и −6. У x и y нет requires_grad — это данные, а не параметры.
σ(z) = 1 / (1 + e⁻ᶻ) σ′(z) = σ(z) · (1 − σ(z))σ(z) = 1 / (1 + e⁻ᶻ) σ′(z) = σ(z) · (1 − σ(z))
где:
  • σ(z)сигмоида, значения в интервале (0; 1)
  • σ′(z)её производная; максимум 0,25 при z = 0

Доказательство: σ′(z) = e⁻ᶻ / (1 + e⁻ᶻ)² = σ(z) · e⁻ᶻ / (1 + e⁻ᶻ), а последняя дробь равна 1 − σ(z).

Пример 2: градиент сигмоидного нейрона

y = σ(w · x), w = 0, x = 2. Найди dy/dw.

Показать решение
z = w · x = 0, y = σ(0) = 0,5.
dy/dw = σ′(z) · dz/dw = σ(z)(1 − σ(z)) · x = 0,5 · 0,5 · 2 = 0,5.
Следствие: так как σ′ никогда не превышает 0,25, градиент уменьшается как минимум в 4 раза на каждом сигмоидном слое. После 10 слоёв это множитель 4¹⁰ ≈ 10⁶ — проблема затухающего градиента. Именно поэтому в глубоких сетях преобладает ReLU.
Python
import torch

w = torch.tensor(0.0, requires_grad=True)
x = torch.tensor(2.0)
y = torch.sigmoid(w * x)
y.backward()
print(y.item(), w.grad.item())
Ожидаемый результат
0.5 0.5
Пример 3: переменная влияет по двум путям

f = x · y + x², x = 2, y = 3. Найди ∂f/∂x и ∂f/∂y. Что здесь делает autograd?

Показать решение
x входит в два узла графа: u = x · y и v = x².
Первый путь: ∂u/∂x = y = 3. Второй путь: ∂v/∂x = 2x = 4.
Пути складываются: ∂f/∂x = 3 + 4 = 7; ∂f/∂y = x = 2.
Autograd делает то же самое: суммирует в .grad все потоки градиента, приходящие в x. Именно отсюда и берётся накопление градиентов.

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

Градиенты накапливаются: обнуление и torch.no_grad()

.backward() не заменяет старый градиент, а прибавляет к нему. Ниже производная y = x³ при x = 2 равна 3x² = 12, но после трёх вызовов .grad равен 36:

Python
import torch

x = torch.tensor(2.0, requires_grad=True)
for i in range(3):
    y = x ** 3
    y.backward()
    print(x.grad)
x.grad.zero_()
print(x.grad)
with torch.no_grad():
    z = x * 2
print(z.requires_grad, x.detach().requires_grad)
Ожидаемый результат
tensor(12.)
tensor(24.)
tensor(36.)
tensor(0.)
False False
  • x.grad.zero_() обнуляет градиент на месте (подчёркивание _ в конце означает операцию на месте). В следующих уроках это будет делать optimizer.zero_grad().
  • В блоке with torch.no_grad(): граф не строится: его используют при обновлении параметров и при оценке модели (инференсе), это экономит память и время.
  • .detach() возвращает тензор с теми же данными, но отсоединённый от графа — например, чтобы вывести значение или преобразовать его в NumPy.

Градиентный спуск с autograd

Теперь соберём всё вместе и подгоним линейную модель к 20 зашумлённым точкам прямой y = 2x + 1. На этот раз мы не пишем формулы производных — autograd вычисляет их на каждом шаге. Пять шагов этого цикла — основа всего глубокого обучения: прямой проход → потери → backward → обновление внутри no_grad → обнуление градиентов.

Python
import torch

torch.manual_seed(42)
X = torch.linspace(0, 1, 20)
Y = 2 * X + 1 + 0.1 * torch.randn(20)
w = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
lr = 0.5
for step in range(201):
    loss = ((w * X + b - Y) ** 2).mean()
    loss.backward()
    with torch.no_grad():
        w -= lr * w.grad
        b -= lr * b.grad
    w.grad.zero_()
    b.grad.zero_()
    if step % 50 == 0:
        print(f'step {step:3d}  loss {loss.item():.4f}')
print(f'w = {w.item():.3f}, b = {b.item():.3f}')
Ожидаемый результат
step   0  loss 4.4302
step  50  loss 0.0085
step 100  loss 0.0084
step 150  loss 0.0084
step 200  loss 0.0084
w = 1.907, b = 1.068
Всего за 50 шагов потери упали с 4,43 до 0,0085. Итоговые потери близки к дисперсии шума (0,1² = 0,01) — опуститься ниже значило бы переобучиться. w и b не равны ровно 2 и 1, потому что для 20 зашумлённых точек лучшая прямая именно такая.

Хорошо запомни этот цикл: в следующих уроках мы его сократим, но суть не изменится. Метод step() оптимизатора torch.optim.SGD выполняет для каждого параметра именно p -= lr * p.grad внутри no_grad, а zero_grad() очищает градиенты. Слои вроде nn.Linear сами создают w и b и сами включают requires_grad. То есть «волшебный» высокоуровневый код — лишь удобная обёртка над этими пятью строками.

Главное

  • Autograd строит вычислительный граф из операций с тензорами, а backward() применяет цепное правило от корня к листьям.
  • Градиенты листьев с requires_grad=True хранятся в .grad; backward() вызывают для скаляра.
  • Цепное правило: dL/dx = dL/dy · dy/dx; для сигмоиды σ′ = σ(1 − σ).
  • Градиенты накапливаются — обнуляй их после каждого шага; обновления и оценка выполняются внутри torch.no_grad().
  • Цикл обучения: прямой проход → потери → backward → обновление → обнуление.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
x = 1, y = x³ + 4x. Чему равно x.grad после y.backward()?