PyTorch Autograd Автоматическое дифференцирование
Обучение глубоких нейронных сетей по сути представляет собой процесс многократного вычисления градиентов и обновления параметров.
Ручное вычисление градиентов для каждого слоя — утомительная и чреватая ошибками задача. Движок PyTorchAutograd(автоматическое дифференцирование) был создан для решения этой проблемы — он можетавтоматически вычислять градиенты любого вычислительного графа, позволяя вам сосредоточиться на проектировании модели, а не на выводе формул.
Основные концепции
1. Что такое автоматическое дифференцирование
Автоматическое дифференцирование — это не численное дифференцирование (метод конечных разностей) и не символьное дифференцирование (алгебраический вывод), азапись процесса вычислений и применение правила цепочки в обратном порядке шаг за шагомдля точного вычисления производных.
Autograd в PyTorch используетдинамический граф вычислений(Define-by-Run) подход: каждый прямой проход строит ориентированный ациклический граф (DAG) в реальном времени, записывая каждую операцию и её входы и выходы; во время обратного распространения он обходит граф в обратном порядке и вычисляет градиент каждого узла по очереди.
2. Атрибут requires_grad
Атрибут Tensorrequires_gradуправляет тем, нужно ли отслеживать градиенты для этого тензора:
Пример
# Создайте тензор, который требует отслеживания градиента (по умолчанию 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, который указывает на узел операции, создавший его. Это "скелет" графа вычислений:
Пример
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атрибуте каждого тензора:
Пример
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существующим значениям. Это самая распространённая ошибка в циклах обучения:
Пример
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):
Пример
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()позволяет пропустить построение графа вычислений, значительно экономя память и вычислительные ресурсы:
Пример
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.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 и трансферное обучение |
Пример
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:
Пример
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() # ❌ RuntimeError: Попытка выполнить backward по графу второй раз
Ненужное использование
retain_graph=Trueприведёт к постоянному росту памяти, потому что граф вычислений не может быть освобождён. Используйте его только тогда, когда действительно требуется несколько проходов backward.
Применение градиентов в обучении нейронных сетей
Ниже приведён полный пример ручной реализации градиентного спуска с использованием Autograd, демонстрирующий полный рабочий процесс Autograd в реальном обучении:
Пример
# Создание обучающих данных: 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():
Пример
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 корректно распространять градиент назад, поэтому их следует по возможности избегать:
Пример
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) могут участвовать в автоматическом дифференцировании.