Оптимизаторы являются важными инструментами в стеке моделирования. Одним из наиболее широко используемых оптимизаторов является оптимизатор Adam, представленный Kingma и Ba [бумага]. Этот оптимизатор отслеживает скользящие средние градиента (термин импульса) и второй момент (термин энергии) градиента с использованием фильтров экспоненциального скользящего среднего (EMA) и использует квадратный корень из термина энергии для нормализации члена импульса. прежде чем сделать шаг.
Это кажется хорошей идеей, верно? Во-первых, это выглядит как диагональное приближение оптимизации второго порядка, а во-вторых, когда градиенты зашумлены, знаменатель (квадратный корень из энергетического члена) велик по отношению к числителю (импульсный член), а шаги малы. С другой стороны, когда градиенты согласованы, знаменатель приблизительно равен числителю, и мы делаем шаги постоянного размера, равные скорости обучения γ. Вот почему этот оптимизатор де-факто является выбором исследователей и практиков машинного обучения.
Было предложено и изучено множество вариаций оптимизатора Adam (см. AdamW из PyTorch docs и оптимизаторы типа QHAdam из пакета torch_optimizer). Здесь мы предлагаем вариант, который существенно не отклоняется от Адама для зашумленных недавних градиентов, но значительно увеличивает фактический размер шага для параметров с последовательной недавней историей градиента (насколько недавний зависит от параметров EMA β1 и β2).
Наша модификация проста, но эффективна: мы заменяем EMA-фильтр члена энергии градиента на EMA-фильтр члена дисперсии градиента. Это означает окончательное уравнение обновления θ(t) = θ(t-1) - γ * sqrt(SNR) * sign(импульс), где SNR относится к отношению сигнал-шум в «недавней» истории градиента (SNR равно термин, заимствованный из литературы по обработке сигналов и обозначающий отношение средней мощности бесшумного сигнала к дисперсии шума в сигнале). Таким образом, параметры с высоким SNR, то есть с согласованными «недавними» историями градиента, будут иметь гораздо больший размер шага (до бесконечности, если градиент постоянный), чем те, которые имеют зашумленные «недавние» истории градиента (или низкие значения SNR градиента). . Реализация этого оптимизатора проста и приведена ниже для полноты картины.
from typing import Tuple import torch from torch.optim.optimizer import Optimizer class SNRAdam(Optimizer): r"""Implements the SNRAdam optimization algorithm, which uses std deviation for the denominator rather than sqrt(energy) term used in conventional Adam. Why is this a good idea? If gradient stddev for a param is small, we should take larger steps as it means the gradient is consistent over time. Arguments: params: iterable of parameters to optimize or dicts defining parameter groups lr: learning rate (default: 1e-3) betas: coefficients used for computing running averages of gradient and its variance (default: (0.9, 0.999)) eps: term added to the denominator to improve numerical stability (default: 1e-8) weight_decay: weight decay (L2 penalty) (default: 0) """ def __init__( self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.9, 0.999), weight_decay: float = 0.0, eps: float = 1e-8, ): if lr <= 0.0: raise ValueError('Invalid learning rate: {}'.format(lr)) if eps < 0.0: raise ValueError('Invalid epsilon value: {}'.format(eps)) if not 0.0 <= betas[0] < 1.0: raise ValueError( 'Invalid beta parameter at index 0: {}'.format(betas[0]) ) if not 0.0 <= betas[1] < 1.0: raise ValueError( 'Invalid beta parameter at index 1: {}'.format(betas[1]) ) if weight_decay < 0: raise ValueError( 'Invalid weight_decay value: {}'.format(weight_decay) ) defaults = { 'lr': lr, 'betas': betas, 'weight_decay': weight_decay, 'eps': eps, } super().__init__(params, defaults) def step(self, closure=None): """Performs a single optimization step. Arguments: closure: A closure that reevaluates the model and returns the loss. """ loss = None if closure is not None: loss = closure() for group in self.param_groups: lr = group['lr'] beta1, beta2 = group['betas'] weight_decay = group['weight_decay'] eps = group['eps'] for p in group['params']: if p.grad is None: continue d_p = p.grad.data if d_p.is_sparse: raise RuntimeError( 'SNRAdam does not support sparse gradients, ' 'please consider SparseAdam instead' ) state = self.state[p] if weight_decay != 0: p.data.mul_(1 - lr * weight_decay) if len(state) == 0: state['iter_'] = 1 state['exp_avg'] = torch.zeros_like( p.data, memory_format=torch.preserve_format ) state['exp_avg_sq'] = torch.zeros_like( p.data, memory_format=torch.preserve_format ) iter_ = state['iter_'] exp_avg = state['exp_avg'] if iter_ == 1: d_sub_p_sq = d_p - exp_avg else: d_sub_p_sq = d_p - exp_avg.mul(1.0 / (1 - beta1 ** (iter_ - 1))) d_sub_p_sq.mul_(d_sub_p_sq) exp_avg_sq = state['exp_avg_sq'] exp_avg.mul_(beta1).add_(d_p, alpha=1.0 - beta1) exp_avg_sq.mul_(beta2).add_(d_sub_p_sq, alpha=1.0 - beta2) p.data.addcdiv_(exp_avg.mul(1.0 / (1 - beta1 ** iter_)), exp_avg_sq.mul(1.0 / (1 - beta2 ** iter_)).sqrt() + eps, value=-lr) state['iter_'] += 1 return loss
Мы провели эксперименты, используя простую модель Vision Transformer со 100 000 параметрами (во многом вдохновленную этим средним постом) в наборе данных MNIST с размером партии 4096 в течение 20 эпох со скоростью обучения 1e-3. Результаты указывают на быструю сходимость предложенного оптимизатора по сравнению с оптимизатором Адама (мы построили один из прогонов, но заметили одно и то же поведение последовательно для этой комбинации модели и набора данных):
Чтобы распутать источник выигрыша, мы делаем размер партии = ∞ и сравниваем два алгоритма. Это показывает, происходит ли выигрыш от (i) корректировки стохастики в «стохастическом» градиентном спуске, т. Е. Размер партии меньше, чем полный размер набора данных, или (ii) шума в градиенте, возникающего из траектории оптимизации (часть градиентного спуска ). Мы видим, что выигрыш происходит от компенсации (ii):