Мы привыкли использовать ReduceLROnPlateau если val_loss не улучшается N эпох подряд - режем learning_rate. Это работает. Мы ждем, пока обучение врежется в стену, и только потом реагируем.
А что, если мы могли бы увидеть эту стену заранее? Что, если бы мы могли сбросить скорость плавно, еще на подходе к плато, и снова нажать на газ, если впереди откроется новый спуск?
Я хочу поделиться концепцией умного LR шедулера, который управляет скоростью обучения, анализируя не сам loss, а скорость его изменения.
Проблема ReduceLROnPlateau: Мы реагируем на симптом, а не на причину
ReduceLROnPlateau срабатывает, когда val_loss перестает падать. Это уже финальная стадия замедления. В этот момент оптимизатор уже блуждает по плоскому дну долины, и резкое снижение LR - это скорее мера отчаяния.
Мы теряем драгоценные эпохи, пока ждем срабатывания patience, и упускаем момент, когда можно было бы вмешаться.
"Шедулер, который смотрит на вторую производную"
Давайте рассуждать как физики, а не как программисты.
val_loss это наша позиция на вертикальной оси ландшафта потерь.
Изменение val_loss за эпоху (loss_t - loss_{t-1}) это наша скорость спуска.
Изменение этой скорости это наше ускорение (или замедление). Это и есть вторая производная loss по времени (эпохам).
Идея проста: Мы будем менять learning_rate не тогда, когда скорость упала до нуля, а тогда, когда ускорение стало отрицательным (мы начали замедляться).
Наблюдаем за скоростью: На каждой эпохе мы вычисляем, насколько val_loss улучшился по сравнению с предыдущей эпохой. Назовем это improvement.
Анализируем тренд улучшений: Мы собираем историю improvement за последние, скажем, 10-15 эпох.
Ищем замедление: Если мы видим, что среднее improvement за последние эпохи стабильно уменьшается (т.е. мы все еще спускаемся, но все медленнее и медленнее), это сигнал к упреждающему снижению learning_rate. Мы притормаживаем перед входом в крутой поворот.
Ищем ускорение: А что, если после снижения LR мы внезапно нашли новый крутой спуск? Скорость улучшений (improvement) снова начнет расти. Наш умный шедулер это заметит и может вернуть learning_rate обратно на более высокое значение, чтобы быстрее пройти этот новый участок
Реализация на Python и PyTorch
Давайте набросаем класс, реализующий эту логику.
import numpy as np
class ProactiveLRScheduler:
def __init__(self, optimizer, factor=0.1, patience=10,
window_size=20, min_lr=1e-8, cooldown=0, verbose=True):
self.optimizer = optimizer
self.factor = factor
self.patience = patience
self.window_size = window_size
self.min_lr = min_lr
self.cooldown = cooldown
self.verbose = verbose
self.history = []
self.bad_trend_counter = 0
self.cooldown_counter = 0
self.last_lr_change_epoch = 0
def step(self, current_loss, epoch):
self.history.append(current_loss)
if len(self.history) < self.window_size:
return # Накапливаем историю
if self.cooldown_counter > 0:
self.cooldown_counter -= 1
return # Находимся в периоде охлаждения после изменения LR
# Вычисляем скорости улучшений за последние N-1 эпох
improvements = -np.diff(self.history[-self.window_size:])
# Если среднее улучшение стало очень маленьким или отрицательным - это плохой знак
# Мы можем анализировать тренд этих улучшений
x = np.arange(len(improvements))
slope, _ = np.polyfit(x, improvements, 1) # Наклон тренда улучшений
if self.verbose:
print(f"[Scheduler] Наклон тренда улучшений: {slope:.6f}")
# Если наклон < 0, значит, улучшения замедляются
if slope < 0:
self.bad_trend_counter += 1
else:
# Если улучшения снова начали ускоряться, сбрасываем счетчик
self.bad_trend_counter = 0
if self.bad_trend_counter >= self.patience:
self._reduce_lr(epoch)
self.bad_trend_counter = 0 # Сбрасываем после срабатывания
def _reduce_lr(self, epoch):
for i, param_group in enumerate(self.optimizer.param_groups):
old_lr = float(param_group['lr'])
new_lr = max(old_lr * self.factor, self.min_lr)
if old_lr - new_lr > 1e-8: # Если изменение значимо
param_group['lr'] = new_lr
if self.verbose:
print(f"Эпоха {epoch}: снижаю learning rate группы {i} с {old_lr:.2e} до {new_lr:.2e}.")
self.cooldown_counter = self.cooldown
self.last_lr_change_epoch = epoch
(Примечание: это концептуальная реализация. Логику возврата LR можно добавить как отдельное условие, если slope становится сильно положительным)
Заключение
Возможно, этот подход не всегда будет лучше стандартного, но он точно является шагом к более умным и адаптивным инструментам, которые чувствуют процесс обучения, а не просто следуют жестким правилам.