Почувствуйте разницу между оптимизаторами SGD, RMSprop и Adam

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

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

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

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

Оглавление

  1. Быстрое напоминание о градиентном спуске (SGD)
  2. SGD с импульсом
  3. RMSprop
  4. Адам

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

Мы начнем с быстрого освежения знаний о градиентном спуске - не стесняйтесь пропустить, если вы знакомы.

Стохастический градиентный спуск

Объяснение этого раздела взято из записной книжки SGD, которая является частью второго урока курса fast.ai v3 на Kaggle (имя блокнота поиска: fastai-v3-lesson-2-sgd)

Учитывая эту диаграмму рассеяния, наша цель состоит в том, чтобы модель машинного обучения восстановила два параметра нашей регрессионной модели: m и b.

Первое, что нам нужно сделать, это определить функцию потерь. Эта функция потерь позволяет нам определить, насколько точны прогнозы нашей модели → Следовательно, мы можем вносить изменения в параметры модели на основе значений ошибок.

Теперь, как нам на самом деле внести эти изменения в наши значения потерь? Сквозной градиентный спуск.

градиент - это производная ошибки по рассматриваемому параметру. Давайте разберемся с этим.

По сути, мы пытаемся измерить, насколько наша ошибка изменяется при бесконечно малом увеличении интересующего параметра. Если MSE растет, то производная положительна. Это означает, что мы должны уменьшить параметр. Если градиент отрицательный, это означает, что увеличение параметра приведет к уменьшению ошибки, поэтому мы увеличиваем параметр.

Теперь наша цель - получить нулевой градиент, то есть при бесконечно малом увеличении нашего параметра ошибка не изменится.

Вот и все! Это градиентный спуск. Давайте возьмем все, что мы обсуждали, и представим это в небольшой краткой обобщаемой формуле (на ситуации, выходящие за рамки простой линейной регрессии).

Теперь давайте попробуем реализовать это в таблице Excel с градиентным спуском и посмотрим, насколько хорошо это работает! Чтобы работать вместе со мной, получите электронную таблицу градиентного спуска по этой ссылке: https://course.fast.ai/videos/?lesson=5.

Наши новые параметры: m = 30 и b = 2.

Давайте пробежимся по этим данным за 4 эпохи. Эпоха определяется периодом времени, по истечении которого наша модель наблюдала все точки данных.

Что-то не так.

Как насчет увеличения скорости обучения альфа? Вы могли подумать, что это сработает, но это приводит к ошибкам в электронной таблице из-за того, что известно как проблема взрывающегося градиента - градиенты становятся слишком большими, и внезапно мы достигаем значений ошибки 10¹⁵³ .

Импульс

Давайте попробуем SGD с так называемым импульсом. Momentum говорит нам немного иначе рассчитать шаг (значение, которое мы вычитаем из параметров). Вместо того, чтобы вычитать производную по параметру, умноженную на скорость обучения, мы вычтем (0,9 * предыдущий шаг) + (0,1 * градиент).

По сути, мы говорим, что наша модель придает только 10% важность градиенту, который мы только что вычислили, и 90% важности предыдущему шагу, который модель сделала для параметров.

Думайте об этом как об инерции: если параметр уменьшился на тонну на последнем этапе, он будет продолжать уменьшаться из-за полученного импульса - новый градиент может замедлить его, но только немного. Это может помочь нам намного быстрее сойтись на глобальном минимуме.

Назовем наш новый параметр импульсом β.

Теперь наш новый шаг выглядит примерно так…

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

RMSprop

V (t) - это так называемая экспоненциально взвешенная скользящая средняя. Причина, по которой он называется экспоненциально взвешенным, заключается в том, что коэффициент квадрата градиента (1-бета) * позволяет ему вести учет всех предыдущих значений градиента в квадрате.

Обратите внимание, что v (t) находится в знаменателе ступенчатой ​​функции. Это создает обратную зависимость: если экспоненциально взвешенное скользящее среднее больше, то шаг меньше, а если среднее меньше, шаг больше.

Помните, когда наши параметры обновлялись очень медленно в первой таблице SGD? Теперь, с RMSprop, наши параметры будут обновляться намного быстрее из-за обратной зависимости между градиентом и размером шага.

Посмотри на это! С 2 эпохами мы получаем наклон 4,72 - не совсем к наклону 30, к которому мы стремимся, но в три раза лучше, чем наклон 1,5, который мы получили через SGD.

Ожидается еще одно улучшение ...

Адам

Теперь мы собираемся объединить идеи импульса с принципом RMSprop, включающим обратную зависимость.

Вот и сюрприз - в Адаме на самом деле нет ничего нового. Думайте об этом как о комбинации импульса и реализации RMSprop.

Чтобы упростить себе задачу, сначала игнорируйте m (t) вверху - обратите внимание на обратную связь, сохраненную с v (t). Когда экспоненциально взвешенное скользящее среднее градиентов уменьшается, шаг увеличивается, чтобы быстрее сходиться. Теперь игнорируйте v (t) и посмотрите на m (t). Это прямая связь - если предыдущий шаг большой, то mt также будет большим, увеличивая значение шага, поскольку оно указано в числителе.

Вот и все - маленькие v (t) и большие (m (t)) позволяют вносить изменения в наши параметры гораздо быстрее, чем любые другие оптимизаторы, на которые мы смотрели до сих пор.

Мы находимся в 2 эпохах и ...

б = 29,25! - это очень близко к нашей цели m = 30.

a имеет небольшой переворот, но также примерно близок к 2.

Реализации кода и графики потерь в PyTorch для набора данных MNIST

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

Вот три реализации кода для импульса, RMSprop и Adam, расположенные рядом соответственно. Обратите внимание на элемент уменьшения веса, присутствующий во всех примерах кода. Снижение веса также можно назвать регуляризацией L2. Применение штрафных санкций к нашей функции потерь для больших значений веса позволяет нам лучше обобщить воображаемый набор тестов.

Во-первых, давайте познакомимся с моделью - здесь мы используем простую сеть прямого распространения с одним линейным слоем, унаследованным от модуля nn.Linear. Есть два параметра. Первый - это весовая матрица размером 784 на 10. Второй - это вектор смещения размером 1 на 10. Наша цель - протестировать различные оптимизаторы, чтобы увидеть, какой из них может минимизировать потери в кратчайший интервал времени (наши потери функция - потеря MSE)

Перед написанием кода оптимизаторов я инициализировал три параметра, которые участвуют в оптимизации. Первый - это prev_step - это параметр, который умножается на постоянную бета импульса во время SGD с импульсом. Затем у нас есть prev_mo_step -, который используется для вычисления нашего нового значения m (t) для оптимизатора adam. И, наконец, у нас есть prev_exp_avg_step - это значение сохраненного предыдущего экспоненциально взвешенного скользящего среднего квадратов градиентов (представленного v (t)).

SGD

Вот код для SGD. Во-первых, мы вычисляем предсказанные значения y и реализуем уменьшение веса, где мы добавляем то же значение квадрата градиентов к нашей переменной w2. Затем мы используем loss.backward для вычисления градиентов (p.grad).

После установки torch.no_grad () == true мы обновляем наши параметры p.

Расчет шага [i] - самая важная строка в этом коде. Обратите внимание, как мы умножаем p.grad на (1-импульс) и умножаем предыдущий шаг на импульс.

После вычисления шага мы вычитаем скорость обучения, умноженную на шаг с оператором .sub_ ().

RMSprop

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

Единственное, что различается в коде каждого из этих оптимизаторов, - это входные аргументы и способ вычисления step [i].

В RMSprop наш шаг рассчитывается в два этапа. Сначала мы должны получить экспоненциально взвешенное скользящее среднее v (t). Как видно из кода, мы возводим наш градиент в квадрат, умножаем его на (1-импульс), а затем берем наше предыдущее средневзвешенное значение и умножаем на импульс.

Чтобы вычислить шаг, мы берем градиент и делим его на корень вновь вычисленного v (t), и, наконец, обновляем наши параметры. Подобно SGD, мы должны установить переменную, инициализирующую наш предыдущий шаг (prev_exp_avg_step), прежде чем возвращать значения потерь.

Адам

Опять же - никакой разницы, кроме того, как рассчитывается шаг, только на этот раз мы добавляем еще один параметр mo_step. Значения step [i] для adam рассчитываются аналогично RMSprop - мы по-прежнему делим на корень exp_avg_step. Однако на этот раз дивиденд - это наш mo_step [i]. Здесь вы можете ясно увидеть, как Адам учитывает как наши m (t), так и v (t).

Аргументы momentum1 и 2 используются для установки коэффициентов для линейных комбинаций, используемых для создания шагов импульса и exp_avg (в этом случае я использовал 0,9 для обоих).

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

Графики убытков

Вот упражнение - попытайтесь угадать, к какому оптимизатору принадлежит каждый из приведенных ниже графиков потерь, используя свое понимание скорости, с которой каждый оптимизатор спускается.

Ниже приведены ответы с пометками:

Обратите внимание на то, что график потерь для SGD действительно не сходится к оптимуму до ~ 800 обновлений. Адам и RMSprop сходятся примерно в одно и то же время (~ 250 обновлений), но обратите внимание, как спуск Адама немного круче.

Почему это важно

Есть много нюансов, связанных с производительностью моделей глубокого обучения, от количества скрытых слоев до оптимизатора, используемого для набора скорости обучения. Большинство новичков могут не посчитать важным изучать их на начальном этапе, но чем быстрее вы усвоите эти нюансы - тем быстрее вы удивитесь современным результатам.

Следите за статьей о оптимизации скорости обучения 👀.

Привет Джереми Ховарду, основателю библиотеки и курса fast.ai, на которых основана большая часть этой статьи. Я очень благодарен за ваше стремление сделать современные результаты глубокого обучения доступными для всех.

Привет! Меня зовут Мукундх Мурти, 16 лет, я страстно увлечен взаимосвязью между машинным обучением и открытием новых лекарств. Спасибо, что прочитали эту статью! Надеюсь, вы нашли это полезным :)

Не стесняйтесь проверять мои другие статьи на Medium и связываться со мной в LinkedIn!

Если вы хотите обсудить любую из вышеперечисленных тем, я хотел бы связаться с вами! (отправьте мне письмо по адресу [email protected] или напишите мне в LinkedIn). не стесняйтесь заходить на мой сайт mukundhmurthy.com.

Подпишитесь на мою ежемесячную новостную рассылку здесь, если вы хотите следить за моими успехами :)