Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

11.10 Алгоритм Adam

В этой главе мы уже изучили множество эффективных методов оптимизации. Прежде чем переходить к новому, вспомним их подробно:

  • В разделе 11.4 мы узнали, что стохастический градиентный спуск эффективнее градиентного спуска при решении задач оптимизации.
  • В разделе 11.5 мы узнали, что использование большего набора наблюдений в минибатче даёт дополнительную эффективность за счёт векторизации. Это ключ к эффективному параллелизму на нескольких машинах, нескольких GPU и в целом.
  • В разделе 11.6 мы добавили механизм, который суммирует историю прошлых градиентов, чтобы ускорить сходимость.
  • В разделе 11.7 мы использовали предобусловливатель, который эффективно вычисляется за счёт покоординатного масштабирования.
  • В разделе 11.8 мы отделили масштабирование каждой координаты от управления скоростью обучения.

Алгоритм Adam (Kingma and Ba, 2014) объединяет все эти приёмы в один эффективный алгоритм обучения. Неудивительно, что он стал очень популярным: это один из самых мощных и эффективных оптимизаторов, используемых в глубоком обучении. Однако у него есть и проблемы. В частности, Редди и соавторы (Reddi et al., 2019) показали, что Adam иногда может расходиться из-за плохого контроля дисперсии. В качестве доработки Захир и соавторы (Zaheer et al., 2018) предложили «быстрое исправление» для Adam под названием Yogi. Ниже мы рассмотрим алгоритм Adam.

11.10.1 Алгоритм

Одна из ключевых составляющих алгоритма Adam — использование экспоненциально взвешенных скользящих средних для оценки первого момента (импульса) и второго момента градиента. То есть используются переменные состояния

Здесь и — неотрицательные весовые параметры. Обычно их выбирают равными и . То есть оценка дисперсии смещается гораздо медленнее, чем оценка импульса. Заметим, что если инициализировать , возникает заметное начальное смещение. Его можно устранить, воспользовавшись равенством . Соответственно, нормированные переменные состояния получаются по формулам

Теперь, имея корректные оценки, можно записать уравнения обновления. Сначала мы перемасштабируем градиент очень похожим на RMSProp образом:

В отличие от RMSProp, наше обновление использует импульс , а не сам градиент. Кроме того, из-за масштабирования с помощью , а не , результаты немного различаются. На практике первый вариант работает чуть лучше, поэтому он и отличает алгоритм от RMSProp. Обычно мы выбираем , что обеспечивает хороший баланс между численной устойчивостью и точностью.

Наконец, выполняем простое обновление:

Если взглянуть на алгоритм Adam, его замысел становится очевидным. Во-первых, импульс и масштаб явно выражены в переменных состояния. Их довольно своеобразное определение позволяет убрать смещение (это можно исправить с помощью немного другой инициализации и условий обновления). Во-вторых, комбинация двух слагаемых из RMSProp очень проста. Наконец, явная скорость обучения позволяет управлять размером шага для решения проблем сходимости.

11.10.2 Реализация

Реализовать алгоритм Adam с нуля нетрудно. Для удобства мы храним номер временного шага в словаре hyperparams. Во всём остальном всё просто.

MXNet:

%matplotlib inline
from d2l import mxnet as d2l
from mxnet import np, npx
npx.set_np()

def init_adam_states(feature_dim):
    v_w, v_b = d2l.zeros((feature_dim, 1)), d2l.zeros(1)
    s_w, s_b = d2l.zeros((feature_dim, 1)), d2l.zeros(1)
    return ((v_w, s_w), (v_b, s_b))

def adam(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    for p, (v, s) in zip(params, states):
        v[:] = beta1 * v + (1 - beta1) * p.grad
        s[:] = beta2 * s + (1 - beta2) * np.square(p.grad)
        v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
        s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
        p[:] -= hyperparams['lr'] * v_bias_corr / (np.sqrt(s_bias_corr) + eps)
    hyperparams['t'] += 1

PyTorch:

%matplotlib inline
from d2l import torch as d2l
import torch

def init_adam_states(feature_dim):
    v_w, v_b = d2l.zeros((feature_dim, 1)), d2l.zeros(1)
    s_w, s_b = d2l.zeros((feature_dim, 1)), d2l.zeros(1)
    return ((v_w, s_w), (v_b, s_b))

def adam(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    for p, (v, s) in zip(params, states):
        with torch.no_grad():
            v[:] = beta1 * v + (1 - beta1) * p.grad
            s[:] = beta2 * s + (1 - beta2) * torch.square(p.grad)
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p[:] -= hyperparams['lr'] * v_bias_corr / (torch.sqrt(s_bias_corr)
                                                       + eps)
        p.grad.data.zero_()
    hyperparams['t'] += 1

TensorFlow:

%matplotlib inline
from d2l import tensorflow as d2l
import tensorflow as tf

def init_adam_states(feature_dim):
    v_w = tf.Variable(d2l.zeros((feature_dim, 1)))
    v_b = tf.Variable(d2l.zeros(1))
    s_w = tf.Variable(d2l.zeros((feature_dim, 1)))
    s_b = tf.Variable(d2l.zeros(1))
    return ((v_w, s_w), (v_b, s_b))

def adam(params, grads, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    for p, (v, s), grad in zip(params, states, grads):
        v[:].assign(beta1 * v  + (1 - beta1) * grad)
        s[:].assign(beta2 * s + (1 - beta2) * tf.math.square(grad))
        v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
        s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
        p[:].assign(p - hyperparams['lr'] * v_bias_corr
                    / tf.math.sqrt(s_bias_corr) + eps)

PaddlePaddle:

%matplotlib inline
from d2l import paddle as d2l
import warnings
warnings.filterwarnings("ignore")
import paddle

def init_adam_states(feature_dim):
    v_w, v_b = d2l.zeros((feature_dim, 1)), d2l.zeros((1, ))
    s_w, s_b = d2l.zeros((feature_dim, 1)), d2l.zeros((1, ))
    return ((v_w, s_w), (v_b, s_b))

def adam(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    a = []
    for p, (v, s) in zip(params, states):
        with paddle.no_grad():
            v[:] = beta1 * v + (1 - beta1) * p.grad
            s[:] = beta2 * s + (1 - beta2) * paddle.square(p.grad)
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p[:] -= hyperparams['lr'] * v_bias_corr / (paddle.sqrt(s_bias_corr)
                                                       + eps)
        p.grad.zero_()
        a.append(p)
    hyperparams['t'] += 1
    return a

Теперь обучим модель с помощью описанного алгоритма Adam, используя скорость обучения .

data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
d2l.train_ch11(adam, init_adam_states(feature_dim),
               {'lr': 0.01, 't': 1}, data_iter, feature_dim);

Кроме того, Adam можно использовать из фреймворка глубокого обучения: достаточно передать параметры конфигурации.

MXNet:

d2l.train_concise_ch11('adam', {'learning_rate': 0.01}, data_iter)

PyTorch:

trainer = torch.optim.Adam
d2l.train_concise_ch11(trainer, {'lr': 0.01}, data_iter)

TensorFlow:

trainer = tf.keras.optimizers.Adam
d2l.train_concise_ch11(trainer, {'learning_rate': 0.01}, data_iter)

PaddlePaddle:

trainer = paddle.optimizer.Adam
d2l.train_concise_ch11(trainer, {'learning_rate': 0.01}, data_iter)

11.10.3 Yogi

У Adam есть и свои проблемы: даже в выпуклой постановке он может не сходиться, когда оценка второго момента «взрывается». Захир и соавторы (Zaheer et al., 2018) предложили улучшенное обновление и инициализацию для . В статье предлагается переписать обновление Adam следующим образом:

Всякий раз, когда принимает большие значения или обновления очень разрежены, может слишком быстро «забыть» прошлые значения. Эффективное решение — заменить на . Это и есть обновление Yogi: величина шага теперь не зависит от размера отклонения.

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

MXNet:

def yogi(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-3
    for p, (v, s) in zip(params, states):
        v[:] = beta1 * v + (1 - beta1) * p.grad
        s[:] = s + (1 - beta2) * np.sign(
            np.square(p.grad) - s) * np.square(p.grad)
        v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
        s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
        p[:] -= hyperparams['lr'] * v_bias_corr / (np.sqrt(s_bias_corr) + eps)
    hyperparams['t'] += 1

data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
d2l.train_ch11(yogi, init_adam_states(feature_dim),
               {'lr': 0.01, 't': 1}, data_iter, feature_dim);

PyTorch:

def yogi(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-3
    for p, (v, s) in zip(params, states):
        with torch.no_grad():
            v[:] = beta1 * v + (1 - beta1) * p.grad
            s[:] = s + (1 - beta2) * torch.sign(
                torch.square(p.grad) - s) * torch.square(p.grad)
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p[:] -= hyperparams['lr'] * v_bias_corr / (torch.sqrt(s_bias_corr)
                                                       + eps)
        p.grad.data.zero_()
    hyperparams['t'] += 1

data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
d2l.train_ch11(yogi, init_adam_states(feature_dim),
               {'lr': 0.01, 't': 1}, data_iter, feature_dim);

TensorFlow:

def yogi(params, grads, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    for p, (v, s), grad in zip(params, states, grads):
        v[:].assign(beta1 * v  + (1 - beta1) * grad)
        s[:].assign(s + (1 - beta2) * tf.math.sign(
                   tf.math.square(grad) - s) * tf.math.square(grad))
        v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
        s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
        p[:].assign(p - hyperparams['lr'] * v_bias_corr
                    / tf.math.sqrt(s_bias_corr) + eps)
    hyperparams['t'] += 1

data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
d2l.train_ch11(yogi, init_adam_states(feature_dim),
               {'lr': 0.01, 't': 1}, data_iter, feature_dim);

PaddlePaddle:

def yogi(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-3
    a=[]
    for p, (v, s) in zip(params, states):
        with paddle.no_grad():
            v[:] = beta1 * v + (1 - beta1) * p.grad
            s[:] = s + (1 - beta2) * paddle.sign(
                paddle.square(p.grad) - s) * paddle.square(p.grad)
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p[:] -= hyperparams['lr'] * v_bias_corr / (paddle.sqrt(s_bias_corr)
                                                       + eps)
        p.grad.zero_()
        a.append(p)
    hyperparams['t'] += 1
    return a

data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
d2l.train_ch11(yogi, init_adam_states(feature_dim),
               {'lr': 0.01, 't': 1}, data_iter, feature_dim);

Резюме

  • Adam объединяет множество приёмов оптимизации в довольно мощное правило обновления.
  • Adam создан на основе RMSProp и дополнительно использует экспоненциально взвешенные скользящие средние (EWMA) для минибатчевых стохастических градиентов.
  • При оценке импульса и второго момента Adam использует коррекцию смещения, чтобы компенсировать медленный старт.
  • Для градиентов с заметно различающейся дисперсией могут возникать проблемы со сходимостью. Их можно смягчить, используя больший минибатч или переключившись на улучшенную оценку . Альтернативу предлагает Yogi.

Упражнения

  1. Подберите скорость обучения и понаблюдайте и проанализируйте результаты экспериментов.
  2. Попробуйте переписать обновления импульса и второго момента так, чтобы коррекция смещения не требовалась.
  3. Почему при сходимости нужно уменьшать скорость обучения ?
  4. Постройте пример, в котором Adam расходится, а Yogi сходится.

MXNet: Обсуждение

PyTorch: Обсуждение

TensorFlow: Обсуждение

PaddlePaddle: Обсуждение

Литература

  • Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic optimization. arXiv preprint arXiv:1412.6980.
  • Reddi, S. J., Kale, S., & Kumar, S. (2019). On the convergence of Adam and beyond. arXiv preprint arXiv:1904.09237.
  • Zaheer, M., Reddi, S., Sachan, D., Kale, S., & Kumar, S. (2018). Adaptive methods for nonconvex optimization. Advances in Neural Information Processing Systems, 9793–9803.