Глубина
~25 мин

Оптимизация и регуляризация

SGD, Adam, LR schedulers, dropout, batch norm — как обучать нейросети эффективно.

Оптимизация нейросетей — от SGD до AdamW

Ты написал модель, выбрал loss, запустил обучение — а loss не падает, или падает, но медленно, или скачет как сумасшедший. Проблема почти всегда в оптимизаторе или learning rate. Оптимизатор — это алгоритм, который решает, как именно обновлять веса на каждом шаге. Выбор оптимизатора и его гиперпараметров — одно из самых важных решений при обучении нейросети.

SGD — стохастический градиентный спуск

Самый простой оптимизатор. На каждом шаге: вычисли градиент loss по весам, сдвинь веса в сторону антиградиента. Размер шага определяется learning rate (lr).

θ — веса, η — learning rate, ∇L — градиент loss. Простая и элегантная формула.

Проблемы чистого SGD: шумные градиенты (каждый mini-batch даёт немного разное направление), одинаковый lr для всех параметров (а некоторые параметры нуждаются в более агрессивном обновлении), застревание в седловых точках (градиент маленький, но это не минимум).

Momentum — добавляем инерцию

Идея: представь шарик, катящийся по поверхности loss. Чистый SGD телепортирует шарик на каждом шаге. Momentum даёт ему инерцию — шарик накапливает скорость в направлении, куда его постоянно толкают, и игнорирует кратковременные колебания.

v — «скорость» (экспоненциально взвешенная сумма градиентов), β ≈ 0.9 — коэффициент инерции.

Momentum ускоряет сходимость в 2-10 раз. Если 10 батчей подряд градиент указывает «вправо» — скорость растёт. Если направление меняется — скорость затухает. На практике SGD + Momentum — сильный базелайн, особенно для CNN. Часто финальная точность выше, чем у Adam, но нужно дольше подбирать lr.

RMSProp → Adam → AdamW — адаптивные методы

RMSProp решает другую проблему: одинаковый lr для всех параметров. Идея: отслеживай среднеквадратичную величину градиентов для каждого параметра. Если градиенты для параметра большие — уменьшай шаг. Если маленькие — увеличивай. Каждый параметр получает свой эффективный lr.

Adam (Adaptive Moment Estimation) = Momentum + RMSProp. Отслеживает и первый момент (среднее направление, как Momentum) и второй момент (среднеквадратичная величина, как RMSProp).

m — первый момент (среднее градиентов), v — второй момент (среднее квадратов). β₁ = 0.9, β₂ = 0.999.

Bias correction (m̂, v̂) компенсирует нулевую инициализацию. ε = 1e-8 предотвращает деление на 0.

AdamW — исправленная версия Adam. В обычном Adam weight decay (L2-регуляризация) работает некорректно: адаптивный lr «поглощает» штраф. AdamW декаплирует weight decay от адаптивного lr — штраф применяется напрямую к весам, а не через градиент. На практике AdamW даёт лучшую генерализацию, и это дефолтный выбор для трансформеров.

Какой оптимизатор выбрать?

AdamW (lr=1e-3 → 3e-4) — дефолт для трансформеров, NLP, и вообще «если не знаешь — бери AdamW». • SGD + Momentum (lr=0.1, momentum=0.9) — дефолт для CNN (ResNet, EfficientNet). Часто лучшая финальная accuracy, но требует тюнинга lr. • Adam — если нет weight decay (например, GAN дискриминатор). • Не используй: чистый SGD без momentum, Adagrad (устарел), «экзотику» (LAMB, LARS) без причины.
SGD vs Momentum vs Adam: траектории оптимизации на loss surface

Загрузка интерактивного виджета...

Загрузка интерактивного виджета...

Learning Rate Schedulers — управляем скоростью обучения

Фиксированный lr — плохая идея. В начале обучения нужен большой lr (быстро двигаемся к минимуму), в конце — маленький (точно настраиваемся). LR scheduler автоматически меняет lr по расписанию.

  • StepLR — умножает lr на gamma каждые N эпох. Пример: lr×0.1 каждые 30 эпох. Просто, но грубо. Подходит для SGD на ResNet.
  • CosineAnnealingLR — lr плавно убывает по косинусоиде от max до min. Гладкий, без резких скачков. Самый популярный для трансформеров.
  • OneCycleLR — сначала lr РАСТЁТ (warmup), потом убывает. Одна «волна» за всё обучение. Позволяет использовать очень высокий peak lr. Часто даёт лучший результат за меньшее число эпох.
  • Warmup — первые N шагов lr линейно растёт от 0 до целевого. Критичен для Adam/AdamW: без warmup начальные обновления слишком агрессивны (bias correction ещё не стабилизировалась). Типичный warmup = 5-10% от общего числа шагов.
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR, OneCycleLR

# Типичный сетап: AdamW + Cosine Annealing + Warmup
optimizer = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)

# Вариант 1: CosineAnnealing (нужен ручной warmup)
scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs, eta_min=1e-6)

# Вариант 2: OneCycleLR (warmup встроен)
scheduler = OneCycleLR(
    optimizer,
    max_lr=3e-4,
    epochs=num_epochs,
    steps_per_epoch=len(train_loader),  # шагаем КАЖДЫЙ батч!
    pct_start=0.1,   # 10% warmup
    anneal_strategy='cos'
)

# В training loop:
for epoch in range(num_epochs):
    for batch in train_loader:
        loss = train_step(batch)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        scheduler.step()  # OneCycleLR — после КАЖДОГО шага!
    # scheduler.step()  # CosineAnnealing — после КАЖДОЙ эпохи!

Частая ошибка

Забыть вызвать scheduler.step(). Модель обучается с постоянным lr, результаты хуже, а ты неделю ищешь баг в модели. Ещё одна ошибка: вызвать scheduler.step() не в том месте — OneCycleLR шагает после каждого батча, CosineAnnealing — после каждой эпохи.

Регуляризация — не даём модели переобучиться

Оптимизатор минимизирует train loss. Но нас волнует val loss — качество на новых данных. Регуляризация ограничивает модель, чтобы она не запоминала обучающую выборку.

Dropout (p = 0.1–0.5) — случайно зануляет нейроны при обучении. При инференсе выключается (model.eval()). Для трансформеров стандарт p = 0.1. Для маленьких датасетов можно увеличить до 0.3–0.5.

BatchNorm — нормализует выход слоя по статистикам батча (среднее=0, дисперсия=1), потом масштабирует и сдвигает обучаемыми γ и β. Ускоряет обучение, действует как лёгкий регуляризатор (шум из-за mini-batch статистик). Стандарт для CNN.

LayerNorm — нормализует по признакам (а не по батчу). Не зависит от batch size, работает при batch_size=1. Стандарт для трансформеров. Ставится внутри каждого attention-блока.

Weight Decay — штраф за большие веса. В AdamW: θ = θ − η·wd·θ на каждом шаге. Типичные значения: 0.01–0.1. Не применяй к bias и LayerNorm параметрам (они не должны быть маленькими).

Label Smoothing — вместо жёстких one-hot меток [0, 0, 1, 0] используем «размытые» [0.025, 0.025, 0.925, 0.025]. Предотвращает overconfidence модели, улучшает calibration. В PyTorch: nn.CrossEntropyLoss(label_smoothing=0.1).

# Правильный weight decay: не применять к bias и norm слоям
def get_param_groups(model, weight_decay=0.01):
    decay, no_decay = [], []
    for name, param in model.named_parameters():
        if not param.requires_grad:
            continue
        # bias, LayerNorm, BatchNorm — без weight decay
        if 'bias' in name or 'norm' in name or 'bn' in name:
            no_decay.append(param)
        else:
            decay.append(param)
    return [
        {'params': decay, 'weight_decay': weight_decay},
        {'params': no_decay, 'weight_decay': 0.0},
    ]

optimizer = optim.AdamW(get_param_groups(model), lr=3e-4)
Dropout: случайное выключение нейронов при обучении

Практические советы

LR Finder — метод Лесли Смита: начни с очень маленького lr (1e-7), экспоненциально увеличивай, записывай loss. Оптимальный lr — там, где loss падает быстрее всего (примерно в 10 раз меньше точки, где loss начинает расти). В PyTorch Lightning: tuner.lr_find(model). Экономит часы тюнинга.

Мониторинг gradient norms. Если норма градиентов взрывается (> 100) — нужен gradient clipping: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0). Если градиенты затухают (< 1e-7) — lr слишком маленький или архитектура плохая (vanishing gradients).

Топ-5 ошибок в оптимизации

1. Adam + L2 weight decay вместо AdamW — L2 в Adam работает некорректно, используй AdamW. 2. Забыл scheduler.step() — lr не меняется, результат хуже. 3. Слишком большой lr → loss NaN или взрыв. Начинай с 3e-4 для AdamW, 0.1 для SGD. 4. Нет warmup для трансформеров — первые шаги слишком агрессивные, модель «ломается». 5. Weight decay на всех параметрах — bias и norm слои не нуждаются в регуляризации.

Визуализация: читаем loss curves

Loss curves — главный диагностический инструмент. Логируй train_loss и val_loss каждую эпоху (или чаще). Что они говорят:

  • Train и val падают параллельно → всё хорошо, модель учится и обобщает.
  • Train падает, val растёт → переобучение. Добавь регуляризацию, увеличь данные, уменьши модель.
  • Обе не падают → lr слишком маленький, или модель слишком маленькая, или баг в коде.
  • Loss скачет (spiking) → lr слишком большой. Уменьши lr или добавь gradient clipping.
  • Loss = NaN → lr слишком большой, или числовая нестабильность. Проверь данные на NaN, уменьши lr.
  • Train и val одинаковые → модель underfitting. Увеличь модель, уменьши регуляризацию, обучай дольше.

Помимо loss, логируй gradient norm (среднеквадратичная норма градиентов), learning rate (убедись что scheduler работает), accuracy / метрику на val. Используй Weights & Biases (wandb) или TensorBoard — без визуализации ты летишь вслепую.

# Логирование gradient norm — встраивай в training loop
total_norm = 0.0
for p in model.parameters():
    if p.grad is not None:
        total_norm += p.grad.data.norm(2).item() ** 2
total_norm = total_norm ** 0.5
print(f"Gradient norm: {total_norm:.4f}")

# Gradient clipping — ставь ПЕРЕД optimizer.step()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
Переобучение: train loss падает, validation loss растёт

🎯 На собеседовании

Junior

Чем Adam отличается от SGD? Adam адаптирует lr для каждого параметра + использует momentum. SGD — один lr для всех. Adam сходится быстрее, но SGD может дать лучшую финальную accuracy. • Что такое learning rate? Размер шага при обновлении весов. Слишком большой — loss взрывается. Слишком маленький — обучение застревает. • Зачем нужен scheduler? Уменьшает lr во время обучения. В начале — большие шаги (быстрая сходимость), в конце — маленькие (точная настройка).

Middle

AdamW vs Adam — в чём разница? В Adam weight decay (L2) проходит через адаптивный lr и работает некорректно. AdamW декаплирует weight decay от lr — правильная регуляризация. • BatchNorm vs LayerNorm? BatchNorm нормализует по батчу (стандарт CNN), зависит от batch_size. LayerNorm — по признакам (стандарт трансформеров), работает при batch_size=1. • Что такое warmup и зачем? Первые N шагов lr линейно растёт от 0. Для Adam/AdamW: bias correction не стабилизировалась, без warmup начальные обновления слишком агрессивны.

Senior

Когда SGD лучше Adam? Для CNN (ResNet и др.) SGD+Momentum часто даёт лучшую финальную accuracy и генерализацию. Adam быстрее сходится, но может застрять в более «острых» минимумах. • Как работает gradient clipping? Если норма градиента > max_norm, все градиенты масштабируются (умножаются на max_norm/norm). Предотвращает взрыв градиентов, особенно в RNN/Transformer. • Почему weight decay не применяют к bias и norm? Bias — смещение, его величина не связана с переобучением. Norm параметры (γ, β) — обучаемые масштаб и сдвиг нормализации, их ограничение вредит обучению.

Собираем всё вместе

Рецепт по умолчанию: AdamW (lr=3e-4, wd=0.01) + CosineAnnealing или OneCycleLR + warmup (5-10% шагов) + gradient clipping (max_norm=1.0). Для CNN: SGD + Momentum (lr=0.1, momentum=0.9) + StepLR. Регуляризация: Dropout (0.1-0.3) + weight decay (без bias/norm).

Главное: логируй всё (loss, lr, gradient norm), начинай с проверенных дефолтов, и только потом тюнь. LR finder экономит часы. Loss curves — твой главный диагностический инструмент. Если loss не падает — 80% что проблема в lr или баге, а не в архитектуре.

Дальше: Training Loop — как собрать полный цикл обучения на PyTorch (Dataset, DataLoader, train/eval loop, checkpoints, mixed precision).