Перейти к содержанию

PyTorch: Шпаргалка

~4 минуты чтения

PyTorch -- доминирующий фреймворк для ML research (90%+ NeurIPS 2025 papers) и production (через TorchScript, ONNX, torch.compile). На интервью PyTorch-вопросы проверяют: training loop (zero_grad/backward/step порядок), model.train() vs model.eval(), autograd механику и device management. Три самые частые ошибки: забыть model.eval() при inference, подать probabilities вместо logits в CrossEntropyLoss, и не использовать torch.no_grad() для validation.

Быстрый старт

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
).to(device) # (1)!

criterion = nn.CrossEntropyLoss() # (2)!
optimizer = optim.Adam(model.parameters(), lr=0.001)

for epoch in range(10):
    for x, y in dataloader:
        x, y = x.to(device), y.to(device)
        optimizer.zero_grad() # (3)!
        loss = criterion(model(x), y)
        loss.backward() # (4)!
        optimizer.step() # (5)!
  1. .to(device) -- обязательно перенести модель на GPU перед обучением. Без этого данные на GPU, модель на CPU = ошибка.
  2. CrossEntropyLoss ожидает logits (raw output), не probabilities! Внутри уже есть LogSoftmax. Подавать softmax(output) -- двойной softmax, потеря качества.
  3. Порядок критичен: zero_grad() ПЕРЕД forward pass. Иначе градиенты накапливаются от предыдущих батчей (иногда это нужно, но обычно -- баг).
  4. backward() вычисляет dL/dw для всех параметров с requires_grad=True через цепочку autograd.
  5. step() обновляет веса: w = w - lr * grad. Вызывать ПОСЛЕ backward(), иначе обновление по старым/нулевым градиентам.

Tensors

Создание

# Из данных
x = torch.tensor([1, 2, 3])
x = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)

# Специальные тензоры
zeros = torch.zeros(3, 4)           # Нули
ones = torch.ones(3, 4)             # Единицы
rand = torch.rand(3, 4)             # Uniform [0, 1)
randn = torch.randn(3, 4)           # Normal(0, 1)
arange = torch.arange(0, 10, 2)     # [0, 2, 4, 6, 8]
linspace = torch.linspace(0, 1, 5)  # 5 точек от 0 до 1
eye = torch.eye(3)                  # Единичная матрица

# Как другой тензор
x_like = torch.zeros_like(x)
x_new = torch.randn_like(x, dtype=torch.float32)

Атрибуты

x.shape          # torch.Size([3, 4])
x.dtype          # torch.float32
x.device         # cpu или cuda:0
x.requires_grad  # True/False
x.ndim           # Количество измерений
x.numel()        # Общее количество элементов

Операции с формой

x.view(2, 6)           # Изменить форму (contiguous required!)
x.reshape(2, 6)        # Изменить форму (безопасно, копирует если надо)
x.squeeze()            # Убрать размерности = 1
x.unsqueeze(0)         # Добавить размерность
x.transpose(0, 1)      # Поменять оси местами
x.permute(2, 0, 1)     # Переставить все оси
x.flatten()            # В одномерный
x.flatten(1)           # Flatten начиная с dim=1
x.contiguous()         # Сделать память непрерывной

# Объединение
torch.cat([x, y], dim=0)    # Конкатенация по оси
torch.stack([x, y], dim=0)  # Новая ось
torch.split(x, 2, dim=0)    # Разбить на части
torch.chunk(x, 3, dim=0)    # Разбить на n частей

Математика

# Поэлементные
x + y, x - y, x * y, x / y
torch.add(x, y)
torch.mul(x, y)
torch.pow(x, 2)
torch.sqrt(x)
torch.exp(x)
torch.log(x)
torch.abs(x)
torch.clamp(x, min=0, max=1)

# Матричные
torch.mm(x, y)          # Матричное умножение 2D
torch.bmm(x, y)         # Батч матричное умножение [B, N, M] @ [B, M, K]
torch.matmul(x, y)      # Универсальное (broadcasting)
x @ y                   # То же что matmul

# Агрегации
x.sum(), x.mean(), x.std(), x.var()
x.min(), x.max()
x.argmin(), x.argmax()
x.sum(dim=1)            # По оси
x.sum(dim=1, keepdim=True)  # Сохранить размерность

Индексация

x[0]                    # Первая строка
x[:, 0]                 # Первый столбец
x[0, 1]                 # Элемент
x[x > 0]                # Boolean indexing
x[[0, 2, 4]]            # Fancy indexing
torch.where(x > 0, x, torch.zeros_like(x))  # Условная замена

# Gather / Scatter
torch.gather(x, dim=1, index=indices)
x.scatter_(dim=1, index=indices, src=values)

GPU

# Перенос на GPU
x = x.to('cuda')
x = x.cuda()

# Перенос на CPU
x = x.to('cpu')
x = x.cpu()

# Конвертация в numpy (только CPU!)
arr = x.cpu().numpy()
x = torch.from_numpy(arr)

view() падает после transpose/permute

view() требует contiguous memory layout. После transpose() или permute() тензор становится non-contiguous -- view() упадёт с ошибкой. Решение: x.transpose(0, 1).contiguous().view(-1) или сразу x.transpose(0, 1).reshape(-1). reshape() безопасен -- сам скопирует если нужно, но view() быстрее когда данные уже contiguous.


Autograd

# Включить градиенты
x = torch.randn(3, requires_grad=True)

# Вычисление
y = x ** 2
z = y.sum()
z.backward()  # Вычислить градиенты

print(x.grad)  # dz/dx = 2x

# Отключить градиенты
with torch.no_grad():
    y = model(x)

# Или через декоратор
@torch.no_grad()
def inference(model, x):
    return model(x)

# Detach от графа
x_detached = x.detach()

# Обнулить градиенты
x.grad.zero_()
optimizer.zero_grad()

Neural Network Layers

Основные слои

# Полносвязный
nn.Linear(in_features, out_features, bias=True)

# Свёрточные
nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0)
nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0)
nn.ConvTranspose2d(...)  # Транспонированная (upsampling)

# Пулинг
nn.MaxPool2d(kernel_size, stride=None, padding=0)
nn.AvgPool2d(kernel_size)
nn.AdaptiveAvgPool2d(output_size)  # Global Average Pooling

# Нормализация
nn.BatchNorm1d(num_features)
nn.BatchNorm2d(num_features)
nn.LayerNorm(normalized_shape)
nn.GroupNorm(num_groups, num_channels)

# Dropout
nn.Dropout(p=0.5)
nn.Dropout2d(p=0.5)  # Для CNN

# Embedding
nn.Embedding(num_embeddings, embedding_dim, padding_idx=None)

Функции активации

nn.ReLU()
nn.LeakyReLU(negative_slope=0.01)
nn.PReLU()
nn.GELU()
nn.Sigmoid()
nn.Tanh()
nn.Softmax(dim=-1)
nn.LogSoftmax(dim=-1)

# Функциональный стиль
import torch.nn.functional as F
F.relu(x)
F.gelu(x)
F.softmax(x, dim=-1)

RNN слои

nn.RNN(input_size, hidden_size, num_layers=1, batch_first=True, bidirectional=False)
nn.LSTM(input_size, hidden_size, num_layers=1, batch_first=True, bidirectional=False)
nn.GRU(input_size, hidden_size, num_layers=1, batch_first=True, bidirectional=False)

# Использование LSTM
lstm = nn.LSTM(input_size=100, hidden_size=256, num_layers=2, batch_first=True)
output, (h_n, c_n) = lstm(x)  # x: [batch, seq_len, input_size]
# output: [batch, seq_len, hidden_size]
# h_n: [num_layers, batch, hidden_size] - последние hidden states

Transformer слои

nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward=2048, dropout=0.1)
nn.TransformerDecoderLayer(d_model, nhead, dim_feedforward=2048, dropout=0.1)
nn.TransformerEncoder(encoder_layer, num_layers)
nn.TransformerDecoder(decoder_layer, num_layers)
nn.MultiheadAttention(embed_dim, num_heads, dropout=0.0, batch_first=True)

Создание моделей

nn.Sequential

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Linear(128, 10)
)

nn.Module (рекомендуется)

class MLP(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super().__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, output_dim)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.5)

    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x

model = MLP(784, 256, 10)

CNN пример

class CNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(64 * 8 * 8, 256)
        self.fc2 = nn.Linear(256, num_classes)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.pool(self.relu(self.conv1(x)))  # [B, 32, 16, 16]
        x = self.pool(self.relu(self.conv2(x)))  # [B, 64, 8, 8]
        x = x.flatten(1)                          # [B, 64*8*8]
        x = self.relu(self.fc1(x))
        x = self.fc2(x)
        return x

Loss Functions

# Классификация
nn.CrossEntropyLoss()           # Multiclass (logits -> softmax -> CE)
nn.BCELoss()                    # Binary (после sigmoid)
nn.BCEWithLogitsLoss()          # Binary (logits, стабильнее)
nn.NLLLoss()                    # После LogSoftmax
nn.FocalLoss  # нет в PyTorch! см. torchvision.ops.sigmoid_focal_loss

# Регрессия
nn.MSELoss()
nn.L1Loss()                     # MAE
nn.SmoothL1Loss()               # Huber loss
nn.HuberLoss(delta=1.0)

# Специальные
nn.CosineEmbeddingLoss()
nn.TripletMarginLoss()
nn.CTCLoss()                    # Для OCR/ASR

# Использование
criterion = nn.CrossEntropyLoss(
    weight=class_weights,       # Для дисбаланса
    label_smoothing=0.1,        # Сглаживание
    ignore_index=-100           # Игнорировать padding
)
loss = criterion(logits, targets)

CrossEntropyLoss принимает logits, НЕ вероятности

nn.CrossEntropyLoss внутри делает log_softmax + nll_loss. Если подать вероятности (после softmax), результат будет неправильным -- loss будет маленьким, но модель не учится. Аналогично nn.BCEWithLogitsLoss внутри делает sigmoid. Частая ошибка: loss = nn.BCELoss()(torch.sigmoid(logits), targets) -- численно нестабильно. Правильно: loss = nn.BCEWithLogitsLoss()(logits, targets).


Optimizers

# Базовые
optim.SGD(params, lr=0.01, momentum=0.9, weight_decay=1e-4)
optim.Adam(params, lr=0.001, betas=(0.9, 0.999), weight_decay=0)
optim.AdamW(params, lr=0.001, weight_decay=0.01)  # Правильный weight decay
optim.RMSprop(params, lr=0.01)

# Использование
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)

# Разный LR для разных частей
optimizer = optim.Adam([
    {'params': model.backbone.parameters(), 'lr': 1e-5},
    {'params': model.head.parameters(), 'lr': 1e-3}
])

Learning Rate Schedulers

from torch.optim.lr_scheduler import (
    StepLR,
    MultiStepLR,
    ExponentialLR,
    CosineAnnealingLR,
    ReduceLROnPlateau,
    OneCycleLR,
    CosineAnnealingWarmRestarts
)

# Step decay
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)

# Cosine annealing
scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)

# One Cycle (для быстрого обучения)
scheduler = OneCycleLR(
    optimizer,
    max_lr=0.01,
    epochs=10,
    steps_per_epoch=len(train_loader)
)

# Reduce on plateau
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10)

# В training loop
for epoch in range(epochs):
    train(...)
    val_loss = validate(...)
    scheduler.step()  # или scheduler.step(val_loss) для ReduceLROnPlateau

Data Loading

Custom Dataset

from torch.utils.data import Dataset, DataLoader

class CustomDataset(Dataset):
    def __init__(self, data, labels, transform=None):
        self.data = data
        self.labels = labels
        self.transform = transform

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        x = self.data[idx]
        y = self.labels[idx]

        if self.transform:
            x = self.transform(x)

        return x, y

# DataLoader
train_loader = DataLoader(
    dataset,
    batch_size=32,
    shuffle=True,
    num_workers=4,
    pin_memory=True,     # Быстрее для GPU
    drop_last=True       # Отбросить неполный последний батч
)

for batch_x, batch_y in train_loader:
    batch_x = batch_x.to(device)
    batch_y = batch_y.to(device)
    ...

Training Loop

model.train() и model.eval() -- НЕ декорация

model.train() и model.eval() переключают поведение BatchNorm и Dropout. В train() -- BatchNorm считает статистику по батчу, Dropout отключает нейроны. В eval() -- BatchNorm использует running statistics, Dropout отключён. Забыть model.eval() перед inference = нестабильные предсказания (особенно заметно при batch_size=1, когда BatchNorm считает mean/std по одному примеру). Забыть model.train() при возобновлении обучения = Dropout не работает.

def train_epoch(model, loader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    correct = 0
    total = 0

    for batch_x, batch_y in loader:
        batch_x = batch_x.to(device)
        batch_y = batch_y.to(device)

        optimizer.zero_grad()
        outputs = model(batch_x)
        loss = criterion(outputs, batch_y)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        _, predicted = outputs.max(1)
        total += batch_y.size(0)
        correct += predicted.eq(batch_y).sum().item()

    return total_loss / len(loader), correct / total

@torch.no_grad()
def evaluate(model, loader, criterion, device):
    model.eval()
    total_loss = 0
    correct = 0
    total = 0

    for batch_x, batch_y in loader:
        batch_x = batch_x.to(device)
        batch_y = batch_y.to(device)

        outputs = model(batch_x)
        loss = criterion(outputs, batch_y)

        total_loss += loss.item()
        _, predicted = outputs.max(1)
        total += batch_y.size(0)
        correct += predicted.eq(batch_y).sum().item()

    return total_loss / len(loader), correct / total

# Main loop
for epoch in range(num_epochs):
    train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device)
    val_loss, val_acc = evaluate(model, val_loader, criterion, device)
    scheduler.step()

    print(f"Epoch {epoch+1}: Train Loss={train_loss:.4f}, Val Acc={val_acc:.4f}")

Saving & Loading

# Сохранить только веса (рекомендуется)
torch.save(model.state_dict(), 'model_weights.pt')

# Загрузить веса
model = MyModel()
model.load_state_dict(torch.load('model_weights.pt'))

# Сохранить checkpoint (для продолжения обучения)
checkpoint = {
    'epoch': epoch,
    'model_state_dict': model.state_dict(),
    'optimizer_state_dict': optimizer.state_dict(),
    'loss': loss,
}
torch.save(checkpoint, 'checkpoint.pt')

# Загрузить checkpoint
checkpoint = torch.load('checkpoint.pt')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
start_epoch = checkpoint['epoch']

Полезные приёмы

Gradient Clipping

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# или
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=1.0)

Mixed Precision Training

# PyTorch 2.4+: новый API (torch.amp)
from torch.amp import autocast, GradScaler

scaler = GradScaler()

for x, y in loader:
    optimizer.zero_grad()

    with autocast(device_type='cuda'):  # FP16 forward
        outputs = model(x)
        loss = criterion(outputs, y)

    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

torch.cuda.amp устарел в PyTorch 2.4+

from torch.cuda.amp import autocast, GradScaler помечен deprecated. Новый импорт: from torch.amp import autocast, GradScaler. Новый autocast требует device_type='cuda' явно. GradScaler тоже переехал в torch.amp. Старый код работает но выдаёт FutureWarning.

Gradient accumulation: забыли поделить loss

При gradient accumulation (имитация большого batch на малом GPU) частая ошибка: loss.backward() без деления на accumulation_steps. Gradients суммируются, не усредняются -- эффективный learning rate в \(N\) раз больше ожидаемого. Правильно: (loss / accumulation_steps).backward() и optimizer.step() каждые \(N\) шагов. Без этого модель diverges, и баг сложно найти потому что loss на экране выглядит нормально.

Freeze layers

# Заморозить все параметры
for param in model.parameters():
    param.requires_grad = False

# Разморозить последний слой
for param in model.fc.parameters():
    param.requires_grad = True

Weight Initialization

def init_weights(m):
    if isinstance(m, nn.Linear):
        nn.init.xavier_uniform_(m.weight)
        if m.bias is not None:
            nn.init.zeros_(m.bias)
    elif isinstance(m, nn.Conv2d):
        nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

model.apply(init_weights)

Model Summary

from torchinfo import summary
summary(model, input_size=(1, 3, 224, 224))

Вопросы на интервью

Q: Объясните порядок операций в training loop PyTorch.

❌ "forward, backward, step" -- неполный ответ, пропущен критический шаг

✅ "1) optimizer.zero_grad() -- обнулить gradients (иначе accumulate от прошлого batch). 2) output = model(x) -- forward pass. 3) loss = criterion(output, y) -- вычислить loss. 4) loss.backward() -- backprop, заполнить .grad. 5) (optional) clip_grad_norm_ -- предотвратить gradient explosion. 6) optimizer.step() -- обновить веса. Порядок zero_grad ПЕРЕД forward критичен -- после backward gradients нужны для step."

Q: В чём разница между torch.no_grad() и model.eval()?

❌ "Одно и то же -- отключают обучение" -- принципиально разные механизмы

✅ "model.eval() меняет поведение МОДУЛЕЙ: BatchNorm переключается на running statistics, Dropout отключается. torch.no_grad() отключает AUTOGRAD -- не строится computation graph, экономит ~50% memory. Для inference нужны ОБА: model.eval() + with torch.no_grad(). Без no_grad() = лишний memory. Без eval() = нестабильные предсказания (BatchNorm по batch=1)."

Q: Как реализовать early stopping в PyTorch?

❌ "Остановить когда val_loss перестал падать" -- нет деталей реализации

✅ "Tracking pattern: сохранять best model state + patience counter. Каждую эпоху: если val_loss < best_loss - min_delta, сбросить patience и сохранить model.state_dict(). Иначе: patience -= 1. При patience == 0 -- загрузить best checkpoint и остановить. Важно: min_delta (обычно 1e-4) предотвращает реакцию на шум. torch.save(model.state_dict()) -- не torch.save(model), для переносимости."


See Also