Умный Learning Rate Scheduler: Управляем скоростью обучения, анализируя ускорение. ai.. ai. deep learning.. ai. deep learning. learning rate.. ai. deep learning. learning rate. machine learning.. ai. deep learning. learning rate. machine learning. optimization.. ai. deep learning. learning rate. machine learning. optimization. PyTorch.

Мы привыкли использовать 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 становится сильно положительным)

Заключение

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

Автор: YH7H22

Источник

Rambler's Top100