Мы привыкли использовать 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 не тогда, когда скорость упала до нуля, а тогда, когда ускорение стало отрицательным (мы начали замедляться).

  1. Наблюдаем за скоростью: На каждой эпохе мы вычисляем, насколько val_loss улучшился по сравнению с предыдущей эпохой. Назовем это improvement.

  2. Анализируем тренд улучшений: Мы собираем историю improvement за последние, скажем, 10-15 эпох.

  3. Ищем замедление: Если мы видим, что среднее improvement за последние эпохи стабильно уменьшается (т.е. мы все еще спускаемся, но все медленнее и медленнее), это сигнал к упреждающему снижению learning_rate. Мы притормаживаем перед входом в крутой поворот.

  4. Ищем ускорение: А что, если после снижения 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 становится сильно положительным)

Заключение

Возможно, этот подход не всегда будет лучше стандартного, но он точно является шагом к более умным и адаптивным инструментам, которые чувствуют процесс обучения, а не просто следуют жестким правилам.

Комментарии (0)