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 является использование экспоненциально взвешенных скользящих средних (также известных как затухающее усреднение) для получения оценки как импульса, так и второго момента градиента. То есть он использует переменные состояния
Здесь \(\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}\), для перенормировки членов. Соответственно, нормализованные переменные состояния задаются формулами
Имея правильные оценки, мы можем теперь записать уравнения обновления. Сначала мы перемасштабируем градиент способом, очень похожим на тот, что используется в RMSProp, чтобы получить
В отличие от 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}\) для хорошего баланса между численной стабильностью и точностью.
Теперь у нас есть все необходимые компоненты для вычисления обновлений. Это несколько неожиданно, и мы получаем простое обновление вида
Рассматривая конструкцию алгоритма 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 следующим образом:
Когда \(\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
Авторы дополнительно рекомендуют инициализировать моментум на основе более крупной начальной выборки, а не просто начальной поэлементной оценки. Мы опускаем детали, поскольку они не существенны для обсуждения, и даже без этого сходимость остаётся довольно хорошей.
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. Упражнения¶
Измените скорость обучения и наблюдайте за результатами эксперимента, а также проанализируйте их.
Можно ли переписать обновления импульса и второго момента так, чтобы они не требовали коррекции смещения?
Почему необходимо уменьшать скорость обучения \(\eta\) по мере сходимости?
Попробуйте привести пример, в котором Adam расходится, а Yogi сходится.