.. _sec_adam: Адам ==== В разделах, предшествующих этому, мы рассмотрели ряд методов эффективной оптимизации. Давайте подробно остановимся на них здесь: - Мы видели, что :numref:`sec_sgd` более эффективен, чем градиентный спуск, при решении задач оптимизации, например, благодаря его устойчивости к избыточным данным. - Мы видели, что :numref:`sec_minibatch_sgd` обеспечивает значительное повышение эффективности за счёт векторизации, позволяя использовать более крупные наборы наблюдений в одном мини-батче. Это ключ к эффективной многомашинной, многопроцессорной (GPU) и общей параллельной обработке. - :numref:`sec_momentum` добавил механизм агрегирования истории прошлых градиентов для ускорения сходимости. - :numref:`sec_adagrad` использовал масштабирование по каждой координате для обеспечения вычислительно эффективного предобусловливателя. - :numref:`sec_rmsprop` отделил масштабирование по координатам от корректировки скорости обучения. Adam :cite:`Kingma.Ba.2014` объединяет все эти методы в один эффективный алгоритм обучения. Как и следовало ожидать, он стал довольно популярным как один из наиболее надёжных и эффективных алгоритмов оптимизации для глубокого обучения. Однако у него есть и недостатки. В частности, :cite:`Reddi.Kale.Kumar.2019` показывают, что существуют ситуации, в которых Adam может расходиться из-за плохого контроля дисперсии. В последующей работе :cite:t:`Zaheer.Reddi.Sachan.ea.2018` предложен «горячий фикс» для Adam под названием Yogi, который решает эти проблемы. Об этом мы поговорим позже. А пока давайте рассмотрим алгоритм Adam. Алгоритм -------- Одним из ключевых компонентов Adam является использование экспоненциально взвешенных скользящих средних (также известных как затухающее усреднение) для получения оценки как импульса, так и второго момента градиента. То есть он использует переменные состояния .. math:: \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} Здесь :math:`\beta_1` и :math:`\beta_2` — неотрицательные параметры взвешивания. Обычно выбирают значения :math:`\beta_1 = 0.9` и :math:`\beta_2 = 0.999`. То есть оценка дисперсии изменяется *намного медленнее*, чем член импульса. Обратите внимание, что если инициализировать :math:`\mathbf{v}_0 = \mathbf{s}_0 = 0`, то изначально возникает значительное смещение в сторону меньших значений. Это можно исправить, используя тот факт, что :math:`\sum_{i=0}^{t-1} \beta^i = \frac{1 - \beta^t}{1 - \beta}`, для перенормировки членов. Соответственно, нормализованные переменные состояния задаются формулами .. math:: \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, чтобы получить .. math:: \mathbf{g}_t' = \frac{\eta \hat{\mathbf{v}}_t}{\sqrt{\hat{\mathbf{s}}_t} + \epsilon}. В отличие от RMSProp, наше обновление использует импульс :math:`\hat{\mathbf{v}}_t`, а не сам градиент. Кроме того, есть небольшое косметическое отличие: перемасштабирование происходит с использованием :math:`\frac{1}{\sqrt{\hat{\mathbf{s}}_t} + \epsilon}` вместо :math:`\frac{1}{\sqrt{\hat{\mathbf{s}}_t + \epsilon}}`. Первый вариант, как правило, работает немного лучше на практике, поэтому мы отклонились от RMSProp. Обычно мы выбираем :math:`\epsilon = 10^{-6}` для хорошего баланса между численной стабильностью и точностью. Теперь у нас есть все необходимые компоненты для вычисления обновлений. Это несколько неожиданно, и мы получаем простое обновление вида .. math:: \mathbf{x}_t \leftarrow \mathbf{x}_{t-1} - \mathbf{g}_t'. Рассматривая конструкцию алгоритма Adam, мы видим, что его идея проста. Переменные состояния явно отражают моменты и масштаб. Их довольно необычное определение вынуждает нас использовать корректирующие члены (это можно было бы избежать, применив немного другую инициализацию и условие обновления). Во-вторых, объединение обоих членов достаточно прямолинейно, если опираться на RMSProp. Наконец, явная скорость обучения :math:`\eta` позволяет нам контролировать длину шага для решения проблем сходимости. Реализация ---------- Реализация алгоритма Adam с нуля не представляет особой сложности. Для удобства мы сохраняем счётчик шагов времени :math:`t` в словаре ``hyperparams``. В остальном всё достаточно просто. .. raw:: latex \diilbookstyleinputcell .. code:: python %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 для обучения модели. Мы используем скорость обучения :math:`\eta = 0.01`. .. raw:: latex \diilbookstyleinputcell .. code:: python 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. .. raw:: latex \diilbookstyleinputcell .. code:: python trainer = torch.optim.Adam d2l.train_concise_ch11(trainer, {'lr': 0.01}, data_iter) Yogi ---- Одной из проблем Adam является то, что он может не сходиться даже в выпуклых задачах, когда оценка второго момента в :math:`\mathbf{s}_t` уходит в бесконечность. В качестве решения :cite:t:`Zaheer.Reddi.Sachan.ea.2018` предложили уточнённое обновление (и инициализацию) для :math:`\mathbf{s}_t`. Чтобы понять, что именно происходит, переписываем обновление Adam следующим образом: .. math:: \mathbf{s}_t \leftarrow \mathbf{s}_{t-1} + (1 - \beta_2) \left(\mathbf{g}_t^2 - \mathbf{s}_{t-1}\right). Когда :math:`\mathbf{g}_t^2` имеет высокую дисперсию или обновления являются разреженными, :math:`\mathbf{s}_t` может слишком быстро забывать о прошлых значениях. Возможным решением этой проблемы является замена :math:`\mathbf{g}_t^2 - \mathbf{s}_{t-1}` на :math:`\mathbf{g}_t^2 \odot \mathop{\textrm{sgn}}(\mathbf{g}_t^2 - \mathbf{s}_{t-1})`. Теперь величина обновления больше не зависит от степени отклонения. Это приводит к обновлениям Yogi .. math:: \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}). Авторы дополнительно рекомендуют инициализировать моментум на основе более крупной начальной выборки, а не просто начальной поэлементной оценки. Мы опускаем детали, поскольку они не существенны для обсуждения, и даже без этого сходимость остаётся довольно хорошей. .. raw:: latex \diilbookstyleinputcell .. code:: python 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); Резюме ------ - Алгоритм Adam объединяет особенности многих методов оптимизации в достаточно устойчивое правило обновления. - Созданный на основе RMSProp, алгоритм Adam также использует экспоненциальное скользящее среднее (EWMA) для стохастического градиента мини-батча. - Adam использует коррекцию смещения для компенсации медленного старта при оценке импульса и второго момента. - Для градиентов с высокой дисперсией могут возникать проблемы со сходимостью. Их можно исправить, используя более крупные мини-выборки или перейдя к улучшенной оценке :math:`\mathbf{s}_t`. Yogi предлагает такую альтернативу. Упражнения ---------- 1. Измените скорость обучения и наблюдайте за результатами эксперимента, а также проанализируйте их. 2. Можно ли переписать обновления импульса и второго момента так, чтобы они не требовали коррекции смещения? 3. Почему необходимо уменьшать скорость обучения :math:`\eta` по мере сходимости? 4. Попробуйте привести пример, в котором Adam расходится, а Yogi сходится. `Обсуждения `__