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

Обучение глубоких нейронных сетей по сути представляет собой процесс многократного вычисления градиентов и обновления параметров.

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


Основные концепции

1. Что такое автоматическое дифференцирование

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

Autograd в PyTorch используетдинамический граф вычислений(Define-by-Run) подход: каждый прямой проход строит ориентированный ациклический граф (DAG) в реальном времени, записывая каждую операцию и её входы и выходы; во время обратного распространения он обходит граф в обратном порядке и вычисляет градиент каждого узла по очереди.

2. Атрибут requires_grad

Атрибут Tensorrequires_gradуправляет тем, нужно ли отслеживать градиенты для этого тензора:

Пример

import torch

# Создайте тензор, который требует отслеживания градиента (по умолчанию requires_grad=False)
x = torch.tensor(3.0, requires_grad=True)
print(x)              # tensor(3., requires_grad=True)
print(x.requires_grad)  # True

# Его также можно изменить после создания
y = torch.tensor(2.0)
print(y.requires_grad)   # False
y.requires_grad_(True)   # Модификация на месте (обратите внимание на завершающее подчёркивание)
print(y.requires_grad)   # True

# Результаты операций с тензорами, у которых requires_grad=True, автоматически наследуют requires_grad=True
z = x * y
print(z.requires_grad)   # True

3. grad_fn и граф вычислений

Каждый тензор, созданный операцией, записываетgrad_fn, который указывает на узел операции, создавший его. Это "скелет" графа вычислений:

Пример

import torch

x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

z = x ** 2 + y * 3    # z = x² + 3y

print(z)           # tensor(13., grad_fn=<AddBackward0>)
print(z.grad_fn)   # <AddBackward0 object>

# Проследите цепочку операций, создавших z
print(z.grad_fn.next_functions)
# ((<PowBackward0 object>, 0), (<MulBackward0 object>, 0))
# Видно, что z состоит из операции возведения в степень и операции умножения

backward() Обратное распространение

1. Вызов backward() для скалярного вывода

Вызов.backward()на конечном скаляре (значении функции потерь), и Autograd автоматически вычислит градиенты всех листовых узлов в обратном порядке по графу вычислений, сохраняя результаты в.gradатрибуте каждого тензора:

Пример

import torch

x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

# Прямой проход: z = x² + 3y
z = x ** 2 + y * 3

# Обратный проход: автоматически вычисляются dz/dx и dz/dy
z.backward()

# Просмотр градиентов
print(x.grad)   # tensor(4.) ← dz/dx = 2x = 2×2 = 4
print(y.grad)   # tensor(3.) ← dz/dy = 3

# Математическая проверка:
# z = x² + 3y
# dz/dx = 2x = 2×2 = 4 ✓
# dz/dy = 3 ✓

2. Проблема накопления при нескольких вызовах backward()

Градиенты Autogradнакапливаются, а не перезаписываются. Каждый разbackward()вызывается, градиенты будут добавлены к.gradсуществующим значениям. Это самая распространённая ошибка в циклах обучения:

Пример

import torch

x = torch.tensor(2.0, requires_grad=True)

# Первое обратное распространение
loss = x ** 2
loss.backward()
print(x.grad)    # tensor(4.) ← dL/dx = 2x = 4

# Второе обратное распространение (без обнуления!)
loss = x ** 2
loss.backward()
print(x.grad)    # tensor(8.) ← Накоплено! Не 4, а 4+4=8

# ✅ Правильный подход: обнуляйте градиенты перед каждым обратным распространением
x.grad.zero_()   # Обнуление на месте (обратите внимание на подчёркивание в конце)
loss = x ** 2
loss.backward()
print(x.grad)    # tensor(4.) ← Верно

При обучении нейронной сети перед каждымbackward()calloptimizer.zero_grad()обнулить градиенты, иначе градиенты будут продолжать накапливаться и приведут к некорректным обновлениям параметров.

3. Вызов backward(gradient) для нескалярного выхода

Если выход — это вектор или матрица, а не скаляр,backward()нужно передатьgradientпараметр с той же формой, что и выход (т.е. "восходящий градиент"), что по сути представляет собой вычисление векторно-якобианского произведения (VJP):

Пример

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# Прямой проход: y — вектор
y = x ** 2   # y = [1, 4, 9]

# Для нескалярного выхода нужно передать параметр градиента (той же формы, что и y)
# Градиент можно понимать как "градиент функции потерь по отношению к y"
y.backward(gradient=torch.ones_like(y))   # Предположим, что восходящий градиент состоит из одних единиц

print(x.grad)
# tensor([2., 4., 6.]) ← dy/dx = 2x, вычислено поэлементно

# Если восходящий градиент не состоит из одних единиц (например, взвешенный)
x.grad.zero_()
y.backward(gradient=torch.tensor([1.0, 0.5, 2.0]))  # Разные веса
# Фактическое вычисление: x.grad = 2x * gradient = [2×1, 4×0.5, 6×2]
print(x.grad)
# tensor([2., 2., 12.])

# Более распространённый подход: сначала использовать sum/mean, чтобы преобразовать в скаляр, затем вызвать backward()
x.grad.zero_()
loss = (x ** 2).sum()   # Свести вектор к скаляру
loss.backward()
print(x.grad)
# tensor([2., 4., 6.]) ← Эквивалентно первому подходу

torch.no_grad() Остановка отслеживания градиентов

Во время вывода модели (предсказания) нет необходимости вычислять градиенты. Использованиеtorch.no_grad()позволяет пропустить построение графа вычислений, значительно экономя память и вычислительные ресурсы:

Пример

import torch

x = torch.tensor(3.0, requires_grad=True)

# В контексте no_grad никакие операции не будут отслеживать градиенты
with torch.no_grad():
    y = x ** 2
    print(y.requires_grad)  # False ← Градиенты больше не отслеживаются
    print(y.grad_fn)        # None ← Узел графа вычислений отсутствует

# После выхода из контекста no_grad обычное отслеживание возобновляется
z = x ** 2
print(z.requires_grad)  # True

# Типичное использование: обернуть весь процесс инференса при оценке модели
model = torch.nn.Linear(10, 1)
inputs = torch.randn(32, 10)

with torch.no_grad():
    outputs = model(inputs)   # Граф вычислений не строится; выше скорость и ниже потребление памяти

@torch.no_grad() Синтаксис декоратора

Вы также можете использовать форму декоратора, которая подходит для пометки всей функции инференса как не требующей градиента:

Пример

import torch
import torch.nn as nn

model = nn.Linear(10, 1)

@torch.no_grad()
def predict(model, x):
    """Функция инференса, вычисление градиентов не требуется"""
    return model(x)

x = torch.randn(5, 10)
output = predict(model, x)
print(output.requires_grad)   # False

detach() Отделение от графа вычислений

.detach()Возвращает новый тензор, который разделяет данные с исходным тензором, но не отслеживает градиенты. Обычно используется в следующих сценариях:

Сценарий Описание
Преобразование промежуточных результатов в массивы numpy numpy не поддерживает тензоры с градиентами; сначала нужно вызвать detach()
Запись потерь при обучении (логи) Избегайте сохранения всего графа вычислений, чтобы предотвратить утечки памяти
Заморозка распространения градиента для части сети Сценарии, такие как обучение GAN и трансферное обучение

Пример

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2 + x * 3   # y имеет grad_fn

# Тензор после detach разделяет данные с y, но отсоединён от графа вычислений
y_detached = y.detach()
print(y_detached.requires_grad)  # False
print(y_detached.grad_fn)        # None

# ✅ Преобразование в numpy (тензоры с градиентами нельзя напрямую преобразовать)
# y.numpy() # ❌ Ошибка: RuntimeError
y_detached.numpy()    # ✅ Нормально

# ✅ При записи значений loss используйте detach (чтобы не удерживать вычислительный граф и не тратить память)
losses = []
for i in range(3):
    loss = (x ** 2).sum()
    losses.append(loss.detach().item())  # .item() преобразует скалярный тензор в Python float
    loss.backward()
    x.grad.zero_()

print(losses)   # [14.0, 14.0, 14.0]

retain_graph Сохранение вычислительного графа

По умолчанию,backward()после выполнения вычислительный граф будетавтоматически освобождён(чтобы сэкономить память). Если вам нужно выполнить обратное распространение несколько раз по одному и тому же вычислительному графу (например, при обучении некоторых GAN), нужно передатьretain_graph=True:

Пример

import torch

x = torch.tensor(2.0, requires_grad=True)
y = x ** 3   # y = x³

# Первый backward (сохранить граф вычислений)
y.backward(retain_graph=True)
print(x.grad)    # tensor(12.) ← dy/dx = 3x² = 3×4 = 12

# Второй backward (граф вычислений всё ещё существует)
x.grad.zero_()
y.backward(retain_graph=True)
print(x.grad)    # tensor(12.) ← Тот же результат

# В последний раз сохранять не нужно
x.grad.zero_()
y.backward()     # После этого граф вычислений освобождается
print(x.grad)    # tensor(12.)

# Повторная попытка backward вызовет ошибку (граф вычислений уже освобождён)
# y.backward() # &#x274c; RuntimeError: Попытка выполнить backward по графу второй раз

Ненужное использованиеretain_graph=Trueприведёт к постоянному росту памяти, потому что граф вычислений не может быть освобождён. Используйте его только тогда, когда действительно требуется несколько проходов backward.


Применение градиентов в обучении нейронных сетей

Ниже приведён полный пример ручной реализации градиентного спуска с использованием Autograd, демонстрирующий полный рабочий процесс Autograd в реальном обучении:

Пример

import torch

# Создание обучающих данных: y = 2x + 1 плюс шум
torch.manual_seed(42)
X = torch.randn(100, 1)
y_true = 2 * X + 1 + 0.1 * torch.randn(100, 1)

# Инициализация параметров модели (требуется отслеживание градиента)
w = torch.zeros(1, requires_grad=True)   # Вес
b = torch.zeros(1, requires_grad=True)   # Смещение

lr = 0.1    # Скорость обучения
epochs = 50  # Количество эпох обучения

for epoch in range(epochs):
    # 1. Прямое распространение: вычисление предсказаний
    y_pred = X * w + b

    # 2. Вычисление потерь (среднеквадратичная ошибка)
    loss = ((y_pred - y_true) ** 2).mean()

    # 3. Обратное распространение: автоматическое вычисление d(loss)/dw и d(loss)/db
    loss.backward()

    # 4. Обновить параметры вручную (обернуть с помощью no_grad, чтобы операции обновления не отслеживались в графе вычислений)
    with torch.no_grad():
        w -= lr * w.grad
        b -= lr * b.grad

    # 5. Очистить градиенты (нужно очистить перед следующим обратным проходом)
    w.grad.zero_()
    b.grad.zero_()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1:3d} | Loss: {loss.item():.4f} | w={w.item():.3f}, b={b.item():.3f}")

print(f"\nTraining complete: w ≈ {w.item():.3f} (true value 2.0), b ≈ {b.item():.3f} (true value 1.0))

Результат выполнения приведённого выше кода похож на следующий:

Epoch  10 | Loss: 0.1064 | w=1.587, b=0.805
Epoch  20 | Loss: 0.0281 | w=1.876, b=0.949
Epoch  30 | Loss: 0.0152 | w=1.954, b=0.983
Epoch  40 | Loss: 0.0128 | w=1.978, b=0.993
Epoch  50 | Loss: 0.0122 | w=1.987, b=0.997

训练完成:w ≈ 1.987(真实值 2.0),b ≈ 0.997(真实值 1.0)

Краткая справочная таблица по общему API

API Функция Типовые сценарии
tensor.requires_grad_(True) Включить отслеживание градиентов на месте Включить Autograd для существующего тензора
loss.backward() Обратное распространение, вычислить градиенты всех листовых узлов Каждая итерация в цикле обучения
tensor.grad Доступ к значению градиента тензора Просмотр или ручное обновление параметров
tensor.grad.zero_() Очистить градиенты на месте Необходимо очищать перед каждым backward()
torch.no_grad() Контекстный менеджер, отключить отслеживание градиентов Фаза вывода, ручное обновление параметров
tensor.detach() Вернуть новый тензор, отсоединённый от графа вычислений Преобразовать в numpy, записывать логи, заморозить градиенты
tensor.item() Преобразовать скалярный тензор в число Python Выводить значения потерь, записывать метрики
loss.backward(retain_graph=True) Сохранять граф вычислений, разрешать несколько обратных проходов Сценарии, такие как обучение GAN, требующие нескольких обратных проходов

Часто задаваемые вопросы и меры предосторожности

1. Разница между листовыми и нелистовыми узлами

Тольколистовые узлы(т. е. тензоры, напрямую созданные пользователем, а не результат операций) сохраняют свои градиенты в.grad. Нелистовые узлы, созданные промежуточными операциями, по умолчанию не сохраняют градиенты (для экономии памяти). Если вам нужно просмотреть градиент промежуточного узла, вызовите.retain_grad():

Пример

import torch

x = torch.tensor(2.0, requires_grad=True)   # Листовой узел

# Промежуточный узел (нелистовой узел)
y = x ** 2      # y — промежуточный узел
y.retain_grad() # Явно указать, что нужно сохранить градиент y

z = y * 3       # z — итоговый выход
z.backward()

print(x.grad)   # tensor(12.) ← Листовой узел, сохранён обычным образом
print(y.grad)   # tensor(3.) ← Сохранён благодаря retain_grad()
# Промежуточные узлы без retain_grad(), .grad равен None

2. Операции на месте могут нарушить граф вычислений

Выполнение операций на месте над тензорами, которые требуют отслеживания градиента (например+=、.add_()) может помешать Autograd корректно распространять градиент назад, поэтому их следует по возможности избегать:

Пример

import torch

x = torch.tensor([1.0, 2.0], requires_grad=True)

# Опасно: операция на месте над листовым узлом
# x += 1 # Может вызвать ошибку: листовая Variable, требующая grad, была использована в операции на месте

# Безопасно: используйте не-in-place операцию
y = x + 1   # Создайте новый тензор, не изменяйте x

# Безопасно выполнять операции на месте в контексте no_grad (например, при обновлении параметров)
with torch.no_grad():
    x += 0.01   # Используется для ручного обновления параметров, это безопасно

3. Только тензоры с плавающей точкой поддерживают градиенты

Целочисленные типы (напр.torch.int64) тензоры не поддерживаютrequires_grad=True, только типы с плавающей точкой (float32、float64、float16) могут участвовать в автоматическом дифференцировании.

Другие расширения