12.10. Адам

В разделах, предшествующих этому, мы рассмотрели ряд методов эффективной оптимизации. Давайте подробно остановимся на них здесь:

  • Мы видели, что Section 12.4 более эффективен, чем градиентный спуск, при решении задач оптимизации, например, благодаря его устойчивости к избыточным данным.

  • Мы видели, что Section 12.5 обеспечивает значительное повышение эффективности за счёт векторизации, позволяя использовать более крупные наборы наблюдений в одном мини-батче. Это ключ к эффективной многомашинной, многопроцессорной (GPU) и общей параллельной обработке.

  • Section 12.6 добавил механизм агрегирования истории прошлых градиентов для ускорения сходимости.

  • Section 12.7 использовал масштабирование по каждой координате для обеспечения вычислительно эффективного предобусловливателя.

  • Section 12.8 отделил масштабирование по координатам от корректировки скорости обучения.

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

12.10.1. Алгоритм

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

(12.10.1)\[\begin{split}\begin{aligned} \mathbf{v}_t & \leftarrow \beta_1 \mathbf{v}_{t-1} + (1 - \beta_1) \mathbf{g}_t, \\ \mathbf{s}_t & \leftarrow \beta_2 \mathbf{s}_{t-1} + (1 - \beta_2) \mathbf{g}_t^2. \end{aligned}\end{split}\]

Здесь \(\beta_1\) и \(\beta_2\) — неотрицательные параметры взвешивания. Обычно выбирают значения \(\beta_1 = 0.9\) и \(\beta_2 = 0.999\). То есть оценка дисперсии изменяется намного медленнее, чем член импульса. Обратите внимание, что если инициализировать \(\mathbf{v}_0 = \mathbf{s}_0 = 0\), то изначально возникает значительное смещение в сторону меньших значений. Это можно исправить, используя тот факт, что \(\sum_{i=0}^{t-1} \beta^i = \frac{1 - \beta^t}{1 - \beta}\), для перенормировки членов. Соответственно, нормализованные переменные состояния задаются формулами

(12.10.2)\[\hat{\mathbf{v}}_t = \frac{\mathbf{v}_t}{1 - \beta_1^t} \textrm{ and } \hat{\mathbf{s}}_t = \frac{\mathbf{s}_t}{1 - \beta_2^t}.\]

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

(12.10.3)\[\mathbf{g}_t' = \frac{\eta \hat{\mathbf{v}}_t}{\sqrt{\hat{\mathbf{s}}_t} + \epsilon}.\]

В отличие от RMSProp, наше обновление использует импульс \(\hat{\mathbf{v}}_t\), а не сам градиент. Кроме того, есть небольшое косметическое отличие: перемасштабирование происходит с использованием \(\frac{1}{\sqrt{\hat{\mathbf{s}}_t} + \epsilon}\) вместо \(\frac{1}{\sqrt{\hat{\mathbf{s}}_t + \epsilon}}\). Первый вариант, как правило, работает немного лучше на практике, поэтому мы отклонились от RMSProp. Обычно мы выбираем \(\epsilon = 10^{-6}\) для хорошего баланса между численной стабильностью и точностью.

Теперь у нас есть все необходимые компоненты для вычисления обновлений. Это несколько неожиданно, и мы получаем простое обновление вида

(12.10.4)\[\mathbf{x}_t \leftarrow \mathbf{x}_{t-1} - \mathbf{g}_t'.\]

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

12.10.2. Реализация

Реализация алгоритма Adam с нуля не представляет особой сложности. Для удобства мы сохраняем счётчик шагов времени \(t\) в словаре hyperparams. В остальном всё достаточно просто.

%matplotlib inline
import torch
from d2l import torch as d2l


def init_adam_states(feature_dim):
    v_w, v_b = torch.zeros((feature_dim, 1)), torch.zeros(1)
    s_w, s_b = torch.zeros((feature_dim, 1)), torch.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

Мы готовы использовать Adam для обучения модели. Мы используем скорость обучения \(\eta = 0.01\).

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 является одним из алгоритмов, предоставляемых в составе библиотеки оптимизации Gluon trainer. Следовательно, нам необходимо лишь передать параметры конфигурации для реализации в Gluon.

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

12.10.3. Yogi

Одной из проблем Adam является то, что он может не сходиться даже в выпуклых задачах, когда оценка второго момента в \(\mathbf{s}_t\) уходит в бесконечность. В качестве решения Zaheer et al. (2018) предложили уточнённое обновление (и инициализацию) для \(\mathbf{s}_t\). Чтобы понять, что именно происходит, переписываем обновление Adam следующим образом:

(12.10.5)\[\mathbf{s}_t \leftarrow \mathbf{s}_{t-1} + (1 - \beta_2) \left(\mathbf{g}_t^2 - \mathbf{s}_{t-1}\right).\]

Когда \(\mathbf{g}_t^2\) имеет высокую дисперсию или обновления являются разреженными, \(\mathbf{s}_t\) может слишком быстро забывать о прошлых значениях. Возможным решением этой проблемы является замена \(\mathbf{g}_t^2 - \mathbf{s}_{t-1}\) на \(\mathbf{g}_t^2 \odot \mathop{\textrm{sgn}}(\mathbf{g}_t^2 - \mathbf{s}_{t-1})\). Теперь величина обновления больше не зависит от степени отклонения. Это приводит к обновлениям Yogi

(12.10.6)\[\mathbf{s}_t \leftarrow \mathbf{s}_{t-1} + (1 - \beta_2) \mathbf{g}_t^2 \odot \mathop{\textrm{sgn}}(\mathbf{g}_t^2 - \mathbf{s}_{t-1}).\]

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

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);

12.10.4. Резюме

  • Алгоритм Adam объединяет особенности многих методов оптимизации в достаточно устойчивое правило обновления.

  • Созданный на основе RMSProp, алгоритм Adam также использует экспоненциальное скользящее среднее (EWMA) для стохастического градиента мини-батча.

  • Adam использует коррекцию смещения для компенсации медленного старта при оценке импульса и второго момента.

  • Для градиентов с высокой дисперсией могут возникать проблемы со сходимостью. Их можно исправить, используя более крупные мини-выборки или перейдя к улучшенной оценке \(\mathbf{s}_t\). Yogi предлагает такую альтернативу.

12.10.5. Упражнения

  1. Измените скорость обучения и наблюдайте за результатами эксперимента, а также проанализируйте их.

  2. Можно ли переписать обновления импульса и второго момента так, чтобы они не требовали коррекции смещения?

  3. Почему необходимо уменьшать скорость обучения \(\eta\) по мере сходимости?

  4. Попробуйте привести пример, в котором Adam расходится, а Yogi сходится.

Обсуждения