PyTorch: планировщик скорости обучения
Скорость обучения (Learning Rate) — один из самых важных гиперпараметров при обучении нейронных сетей. Если скорость обучения слишком велика, обучение колеблется или даже расходится; если слишком мала, сходимость крайне медленная, и легко попасть в локальные оптимумы.
Планировщик скорости обученияДинамически регулируя скорость обучения во время обучения, он обеспечивает баланс между быстрой сходимостью на раннем этапе и тонкой настройкой на позднем этапе.
1. Основные понятия и шаблоны использования
Стандартная схема использования
)
import torch.nn as nn
import torch.optim as optim
model = nn.Linear(10, 1)
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 1. Создайте планировщик, передав в него оптимизатор
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, гамма=0.1)
for epoch in range(100):
# 2. Обучение
train(model, optimizer)
# 3. После каждой эпохи вызовите scheduler.step()
scheduler.step()
# 4. Просмотр текущей скорости обучения
current_lr = scheduler.get_last_lr()[0]
print(f"Epoch {epoch+1}, LR: {current_lr:.6f}")
Когда вызывать step()
Пример
optimizer.step()
scheduler.step()
# Неправильно: scheduler.step() до optimizer.step()
# После PyTorch 1.1.0 будет сгенерировано предупреждение, и некоторые планировщики будут работать некорректно
scheduler.step()
optimizer.step()
Примечание:
optimizer.step()Должно бытьscheduler.step()вызван раньше.
Просмотр и сохранение состояния скорости обучения
Пример
current_lr = optimizer.param_groups[0]['lr']
current_lr = scheduler.get_last_lr()[0] # LR после предыдущего step()
# Сохранить чекпоинт (также нужно сохранить состояние планировщика)
torch.save({
'epoch': epoch,
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'scheduler': scheduler.state_dict(), # Не пропустите это
}, 'checkpoint.pth')
# Восстановить контрольную точку
ckpt = torch.load('checkpoint.pth')
model.load_state_dict(ckpt['model'])
optimizer.load_state_dict(ckpt['optimizer'])
scheduler.load_state_dict(ckpt['scheduler'])
2. Планировщики с фиксированным затуханием
2.1 StepLR: ступенчатое затухание
Каждое фиксированноеstep_sizeчисло эпох, умножьте скорость обучения наgamma. Это один из самых простых и наиболее часто используемых планировщиков.
Формула: lr = lr_base * gamma^(floor(step / step_size))
Пример
scheduler = optim.lr_scheduler.StepLR(
optimizer,
step_size=30, # Уменьшать каждые 30 эпох
gamma=0.1 # Умножать на 0.1 каждый раз (т.е. уменьшать до 1/10 от исходного)
)
# Изменения LR:
# Эпоха 0-29: 0.1
# Эпоха 30-59: 0.01
# Эпоха 60-89: 0.001
# Эпоха 90+: 0.0001
Применимые сценарии:Задачи с фиксированным ритмом обучения и чёткими этапами, например обучение ResNet на ImageNet (90 эпох, с уменьшением на 30-й и 60-й эпохах).
2.2 MultiStepLR Многоэтапное снижение
Уменьшайте скорость обучения в нескольких указанных эпохах (контрольных точках), что более гибко, чем StepLR.
Пример
scheduler = optim.lr_scheduler.MultiStepLR(
optimizer,
milestones=[30, 60, 80], # Уменьшение на 30-й, 60-й и 80-й эпохах
гамма=0.1
)
# Изменения LR:
# Эпоха 0-29: 0.1
# Эпоха 30-59: 0.01
# Эпоха 60-79: 0.001
# Эпоха 80+: 0.0001
Применимые сценарии:Когда заранее известно, на каких эпохах модели требуется тонкая настройка, например на этапе точной сходимости в средней и поздней фазах моделей классификации.
2.3 ExponentialLR Экспоненциальное затухание
Скорость обучения снижается на каждой эпохе и продолжает экспоненциально уменьшаться, обеспечивая более плавное снижение.
Формула: lr = lr_base * gamma^epoch
Пример
scheduler = optim.lr_scheduler.ExponentialLR(
optimizer,
gamma=0.95 # Умножить на 0.95 каждую эпоху
)
# Изменения LR (первые 5 эпох):
# Эпоха 1: 0.0100
# Эпоха 2: 0.0095
# Эпоха 3: 0.0090
# Эпоха 4: 0.0086
# Эпоха 5: 0.0081
gammaЕсли задать слишком маленьким (например, 0.5), скорость обучения быстро приблизится к нулю; обычно её задают в диапазоне от 0.9 до 0.99.
3. Адаптивные планировщики
3.1 ReduceLROnPlateau: затухание на основе метрики
Один из самых интеллектуальных планировщиков: отслеживайте определённую метрику (например, потери на валидации) и автоматически снижайте скорость обучения, когда метрика перестаёт улучшаться. Нет необходимости заранее знать, на какой эпохе уменьшать.
Пример
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min', # 'min': чем меньше отслеживаемое значение, тем лучше (потери); 'max': чем больше, тем лучше (точность)
factor=0.1, # При срабатывании lr = lr × factor
patience=10, # Количество эпох, в течение которых метрике разрешено оставаться на плато; при превышении — снижение
threshold=1e-4, # Улучшение меньше этого значения считается отсутствием улучшения
min_lr=1e-6, # Нижняя граница скорости обучения; она не будет ниже этого значения
verbose=True # Выводить информацию о снижении
)
for epoch in range(100):
train_loss = train(model, optimizer)
val_loss = evaluate(model)
# В отличие от других планировщиков, сюда передайте отслеживаемую метрику
scheduler.step(val_loss)
Стиль записи для мониторинга точности:
Пример
optimizer,
mode='max', # Чем выше точность, тем лучше
factor=0.5,
patience=5,
)
scheduler.step(val_accuracy)
Применимые сценарии:Выбор по умолчанию для почти всех задач, особенно когда вы не уверены, сколько эпох обучать, или когда обучение нестабильно.
3.2 CosineAnnealingLR Косинусное затухание
Скорость обучения изменяется покосинусной кривойот начального значения, плавно снижаясь до минимального значения (eta_min), избегая резких изменений ступенчатого затухания.
Формула: lr_t = eta_min + 0.5 * (eta_max - eta_min) * (1 + cos(t * pi / T_max)
Пример
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100, # Длина половины цикла (обычно устанавливается равной общему числу эпох)
eta_min=1e-6 # Минимальная скорость обучения (по умолчанию 0)
)
# Траектория изменения LR (схематично при T_max=10):
# 0.1 -> 0.095 -> 0.079 -> 0.055 -> 0.026 -> 0.001
# (плавное снижение по косинусоидальной кривой)
Применимые сценарии:Сценарии с фиксированным числом эпох обучения, широко используемые в таких работах, как Vision Transformer и ResNet, с хорошим качеством сходимости.
3.3 CosineAnnealingWarmRestarts
Улучшенная версия косинусного отжига поддерживаетпериодические тёплые перезапуски(Warm Restarts): после завершения каждого цикла скорость обучения сбрасывается к начальному значению и начинается новый цикл косинусного затухания. Это позволяет модели выходить из локальных оптимумов.
Пример
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(
optimizer,
T_0=10, # Длина первого цикла (число эпох)
T_mult=2, # Множитель длины цикла после каждого перезапуска (1=равная длина, 2=постепенно увеличивающаяся)
eta_min=1e-6
)
# Длины циклов при T_mult=2: 10 -> 20 -> 40 -> 80 ...
# Изменения LR (T_0=10, T_mult=1):
# 0.1 -> ... -> 0 -> 0.1 -> ... -> 0 -> 0.1 (перезапуск каждые 10 эпох)
Применимые сценарии:При обучении больших моделей или когда вы хотите, чтобы модель выбрала лучшую среди нескольких точек сходимости, это эффективно работает со Snapshot Ensemble.
4. Планировщики разогрева
Разогревозначает постепенное увеличение скорости обучения с очень малого значения до целевого в течение первых нескольких шагов обучения. При обучении с большим размером батча и в Transformer-подобных моделях почти всегда требуется разогрев; иначе начальные обновления градиента слишком агрессивны, и модель трудно стабилизировать.
4.1 LinearLR Линейное планирование
Линейно изменять скорость обучения в течение заданного числа эпох (можно использовать для линейного разогрева или линейного затухания).
Пример
# Линейный разогрев: в течение первых 5 эпох lr линейно увеличивается с 0.01×0.1=0.001 до 0.01
warmup_scheduler = optim.lr_scheduler.LinearLR(
optimizer,
start_factor=0.1, # начальный lr = base_lr × start_factor
end_factor=1.0, # конечный lr = base_lr × end_factor
total_iters=5 # Завершается после 5 эпох
)
# Изменения LR: 0.001 -> 0.003 -> 0.005 -> 0.007 -> 0.009 -> 0.01
4.2 ConstantLR Постоянная фаза
В течение заданного числа эпох зафиксируйте скорость обучения наbase_lr × factor, а затем восстановите исходное значение.
Пример
scheduler = optim.lr_scheduler.ConstantLR(
optimizer,
factor=0.5,
total_iters=5
)
4.3 SequentialLR Комбинированное планирование
Объедините несколько планировщиковпоследовательно, что является стандартным подходом для реализации комбинированной стратегии "разогрев + затухание".
Пример
# Фаза 1: разогрев (первые 5 эпох, LR линейно увеличивается с 0.001 до 0.01)
warmup = optim.lr_scheduler.LinearLR(
optimizer, start_factor=0.1, end_factor=1.0, total_iters=5
)
# Фаза 2: косинусное затухание (оставшиеся 95 эпох)
cosine = optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=95, eta_min=1e-6
)
# Комбинированный: сначала выполнить warmup, затем переключиться на cosine после 5-й эпохи
scheduler = optim.lr_scheduler.SequentialLR(
optimizer,
schedulers=[warmup, cosine],
milestones=[5] # Переключение на 5-й эпохе
)
# Использование точно такое же, как у обычного планировщика
for epoch in range(100):
train(...)
scheduler.step()
Применимые сценарии:Стандартная конфигурация для обучения Transformer (warmup + cosine annealing); предобучение BERT, GPT и ViT — все используют эту стратегию.
5. Циклические планировщики
5.1 CyclicLR Циклическая скорость обучения
Скорость обученияbase_lrиmax_lrмеждуциклами периодически, что помогает модели исследовать более широкое пространство параметров и выходить из седловых точек.
Пример
scheduler = optim.lr_scheduler.CyclicLR(
optimizer,
base_lr=0.001, # Нижняя граница скорости обучения
max_lr=0.01, # Верхняя граница скорости обучения
step_size_up=2000, # Число итераций для роста от base_lr до max_lr
step_size_down=2000, # Число итераций для спада от max_lr до base_lr (по умолчанию равно step_size_up)
mode='triangular', # Треугольный цикл (постоянная амплитуда)
# mode='triangular2' # амплитуда уменьшается вдвое каждый цикл
# mode='exp_range' # экспоненциальное затухание амплитуды
)
# CyclicLR вызывается на каждом шаге (батче), а не на каждой эпохе
for epoch in range(num_epochs):
for inputs, labels in train_loader:
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
scheduler.step() # Вызывать после каждого батча
Сравнение трёх режимов:
| mode | Изменение амплитуды | Характеристика |
|---|---|---|
triangular | Константа | Стабильное исследование, подходит для ранних этапов |
triangular2 | Уменьшается вдвое каждый цикл | Сначала исследование, затем сходимость |
exp_range | Экспоненциальное затухание | В итоге плавно сходится |
5.2 OneCycleLR: стратегия одного цикла
Один из лучших по эффективности планировщиков, предложенный fastai как 1-Cycle Policy. Всё обучение имеет только один цикл: скорость обучения сначала возрастает, затем убывает, а моментум меняется в противоположном направлении.
Обучение быстрее, обычно требует лишь1/5~1/10число эпох традиционного обучения.
Пример
steps_per_epoch = len(train_loader)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer,
max_lr=0.1, # Максимальная скорость обучения
steps_per_epoch=steps_per_epoch, # Количество шагов за эпоху
epochs=10, # Общее количество эпох
pct_start=0.3, # Первые 30% используются для роста на этапе warmup
anneal_strategy='cos', # Стратегия затухания ('cos' или 'linear')
div_factor=25, # Начальный lr = max_lr / div_factor
final_div_factor=1e4 # Конечный lr = max_lr / final_div_factor
)
# Начальный lr = 0.1 / 25 = 0.004
# Пиковый lr = 0.1 (на 30%)
# Конечный lr = 0.1 / 10000 = 0.00001
# Также называется per batch
for epoch in range(10):
for inputs, labels in train_loader:
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
scheduler.step() # Вызывать после каждого батча
Сценарии применения:Первый выбор, когда вычислительные ресурсы ограничены и нужно быстро проверить идеи;
max_lrРекомендуется использовать инструмент LR Finder для определения оптимального значения.
6. Пользовательские планировщики
6.1 Функциональная настройка LambdaLR
Полностью настройте стратегию планирования, передав Lambda-функцию (которая принимает номер эпохи и возвращает множитель скорости обучения).
Пример
# Пример 1: классическая стратегия разогрева Transformer
# lr ∝ min(step^-0.5, step × warmup_steps^-1.5)
def transformer_lr(epoch, warmup_epochs=10, d_model=512):
if epoch == 0:
return 1e-7 / 0.01 # Избегаем деления на ноль
step = epoch + 1
warmup = warmup_epochs
return (d_model ** -0.5) * min(step ** -0.5, step * warmup ** -1.5) / 0.01
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=transformer_lr)
# Пример 2: полиномиальное затухание (lr линейно/полиномиально уменьшается от начального значения до 0)
def polynomial_decay(epoch, total_epochs=100, power=1.0):
return max((1 - epoch / total_epochs) ** power, 0.0)
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=polynomial_decay)
# Пример 3: сгруппированные скорости обучения (разные стратегии для разных групп параметров)
optimizer = optim.Adam([
{'params': model.backbone.parameters(), 'lr': 1e-4},
{'params': model.head.parameters(), 'lr': 1e-3},
])
# Одна лямбда-функция на группу параметров
scheduler = optim.lr_scheduler.LambdaLR(
optimizer,
lr_lambda=[
lambda epoch: 0.95 ** epoch, # backbone: медленное затухание
lambda epoch: 0.85 ** epoch, # head: быстрое затухание
]
)
6.2 Наследование LRScheduler
Когда нужна более сложная логика, наследуйтеLRScheduler(PyTorch >= 2.0, в старых версиях_LRScheduler) для реализации полной настройки:
Пример
import math
class WarmupCosineScheduler(LRScheduler):
"""
Комбинированный планировщик разогрева + косинусного отжига (версия с ручной реализацией)
- Линейный разогрев в течение первых warmup_epochs эпох
- Затем косинусный отжиг до min_lr
"""
def __init__(self, optimizer, warmup_epochs, total_epochs,
min_lr=1e-6, last_epoch=-1):
self.warmup_epochs = warmup_epochs
self.total_epochs = total_epochs
self.min_lr = min_lr
super().__init__(optimizer, last_epoch)
def get_lr(self):
epoch = self.last_epoch
# Фаза разогрева: линейное увеличение
if epoch < self.warmup_epochs:
warmup_factor = (epoch + 1) / self.warmup_epochs
return [base_lr * warmup_factor for base_lr in self.base_lrs]
# Фаза косинусного отжига
progress = (epoch - self.warmup_epochs) / (
self.total_epochs - self.warmup_epochs
)
cosine_factor = 0.5 * (1 + math.cos(math.pi * progress))
return [
self.min_lr + (base_lr - self.min_lr) * cosine_factor
for base_lr in self.base_lrs
]
optimizer = optim.Adam(model.parameters(), lr=0.01)
scheduler = WarmupCosineScheduler(
optimizer,
warmup_epochs=10,
total_epochs=100,
min_lr=1e-6
)
7. Сравнение визуализации планировщиков
Следующий код может построить кривые изменения LR каждого планировщика для наглядного сравнения:
Пример
import torch.optim as optim
import matplotlib.pyplot as plt
def simulate_lr(scheduler_fn, epochs=100, steps_per_epoch=None):
"""Симулировать и записывать изменения скорости обучения планировщика"""
model = torch.nn.Linear(1, 1)
optimizer = optim.SGD(model.parameters(), lr=0.1)
scheduler = scheduler_fn(optimizer)
lrs = []
if steps_per_epoch:
# Планировщик вызывается на каждом шаге
for _ in range(epochs):
for _ in range(steps_per_epoch):
optimizer.step()
scheduler.step()
lrs.append(optimizer.param_groups[0]['lr'])
else:
# Планировщик вызывается каждую эпоху
for _ in range(epochs):
optimizer.step()
scheduler.step()
lrs.append(optimizer.param_groups[0]['lr'])
return lrs
schedulers = {
'StepLR(step=30, gamma=0.1)':
lambda opt: optim.lr_scheduler.StepLR(opt, 30, 0.1),
'CosineAnnealingLR':
lambda opt: optim.lr_scheduler.CosineAnnealingLR(opt, T_max=100),
'ExponentialLR(gamma=0.95)':
lambda opt: optim.lr_scheduler.ExponentialLR(opt, 0.95),
'CosineWarmRestarts(T0=25)':
lambda opt: optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=25),
}
plt.figure(figsize=(12, 5))
for name, fn in schedulers.items():
plt.plot(simulate_lr(fn), label=name)
plt.xlabel('Эпоха')
plt.ylabel('Скорость обучения')
plt.title('Сравнение планировщиков LR в PyTorch')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('lr_schedulers.png', dpi=150)
plt.show()
8. Полный шаблон обучения
Шаблон обучения промышленного уровня, объединяющий разогрев + косинусный отжиг, сохранение и восстановление модели:
Пример
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR
# Гиперпараметры
EPOCHS = 100
WARMUP_EPOCHS = 5
BASE_LR = 1e-3
MIN_LR = 1e-6
SAVE_PATH = 'best_model.pth'
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Модель / Оптимизатор / Планировщик
model = MyModel().to(device)
optimizer = optim.AdamW(model.parameters(), lr=BASE_LR, weight_decay=1e-4)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# Разогрев 5 эпох + косинусное затухание 95 эпох
warmup_sched = LinearLR(optimizer, start_factor=0.1, total_iters=WARMUP_EPOCHS)
cosine_sched = CosineAnnealingLR(optimizer, T_max=EPOCHS - WARMUP_EPOCHS, eta_min=MIN_LR)
scheduler = SequentialLR(optimizer, [warmup_sched, cosine_sched], milestones=[WARMUP_EPOCHS])
# Восстановить контрольную точку
start_epoch = 0
best_acc = 0.0
try:
ckpt = torch.load(SAVE_PATH, map_location=device)
model.load_state_dict(ckpt['model'])
optimizer.load_state_dict(ckpt['optimizer'])
scheduler.load_state_dict(ckpt['scheduler'])
start_epoch = ckpt['epoch'] + 1
best_acc = ckpt['best_acc']
print(f"Возобновлено с эпохи {start_epoch}, лучшая точность {best_acc:.4f}")
except FileNotFoundError:
print("Обучение с нуля")
# Цикл обучения
history = {'train_loss': [], 'val_acc': [], 'lr': []}
for epoch in range(start_epoch, EPOCHS):
# Обучить
model.train()
total_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # Отсечение градиентов
optimizer.step()
total_loss += loss.item()
# Валидация
model.eval()
correct = 0
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
correct += (model(inputs).argmax(1) == labels).sum().item()
avg_loss = total_loss / len(train_loader)
val_acc = correct / len(val_loader.dataset)
cur_lr = scheduler.get_last_lr()[0]
# Запись и вывод
history['train_loss'].append(avg_loss)
history['val_acc'].append(val_acc)
history['lr'].append(cur_lr)
print(f"Эпоха {epoch+1:3d}/{EPOCHS} | "
f"Потери: {avg_loss:.4f} | Точность: {val_acc:.4f} | LR: {cur_lr:.2e}")
# Обновление планировщика
scheduler.step()
# Сохранить лучшую модель
if val_acc > best_acc:
best_acc = val_acc
torch.save({
'epoch': epoch,
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'scheduler': scheduler.state_dict(),
'best_acc': best_acc,
}, SAVE_PATH)
print(f" Сохранение лучшей модели, Acc: {best_acc:.4f}")
print(f"\n"Обучение завершено, лучшая точность валидации: {best_acc:.4f}")
9. Руководство по выбору планировщика
Рекомендации по типу задачи
| Сценарий | Рекомендуемый планировщик | Причина |
|---|---|---|
| Быстрые эксперименты / проверка прототипа | ReduceLROnPlateau | Не требуется настройка гиперпараметров, адаптивное затухание |
| Классификация изображений (фиксированное число эпох) | CosineAnnealingLR | Плавное затухание, хорошее качество сходимости |
| Transformer / BERT | LinearLR + CosineAnnealingLR | Разогрев обязателен |
| Ограниченные ресурсы, быстрое обучение | OneCycleLR | Достичь той же точности за 1/5 времени |
| Дообучение больших моделей (fine-tune) | LinearLR(разогрев) +ConstantLR | Низкая скорость обучения для стабильного дообучения |
| Неопределённое количество эпох | ReduceLROnPlateau | Автоматически реагирует, не зависит от фиксированного расписания |
| Обнаружение объектов (YOLO/Faster-RCNN) | MultiStepLR / OneCycleLR | Имеет чёткие точки снижения или нацелен на скорость |
| Хотите выйти из локальных оптимумов | CosineAnnealingWarmRestarts | Периодический перезапуск для исследования пространства параметров |
Типичные комбинации конфигураций
Пример
SequentialLR([LinearLR(5 epochs), CosineAnnealingLR(95 epochs)])
# Комбинация 2: мониторинг валидационного набора + автоматическое снижение (универсально предпочтительно)
ReduceLROnPlateau(mode='min', patience=5, factor=0.5)
# Комбинация 3: OneCycleLR (предпочтительно для быстрого обучения)
OneCycleLR(max_lr=0.1, total_steps=total_steps, pct_start=0.3)
# Комбинация 4: традиционное ступенчатое снижение (классика CV)
MultiStepLR(milestones=[30, 60, 90], gamma=0.1)
Краткий справочник по заметкам
| Проблема | Решение |
|---|---|
| Планировщик не срабатывает | Проверитьscheduler.step()является лиoptimizer.step() послевызова |
| CyclicLR / OneCycleLR не меняется | Эти два основаны наbatchвызовах, а не по эпохам |
| ReduceLROnPlateau не срабатывает | Проверитьmodeправильно ли он задан (используйте loss для'min', используйте accuracy для'max') |
| LR некорректен после возобновления обучения | Проверьте, было ли это сохранено и загруженоscheduler.state_dict() |
| Настройки LR для нескольких групп параметров | ИспользуйтеLambdaLRПередайте список, по одной lambda на каждую группу параметров |