DL Заметки Градиентный спуск

DL Заметки Градиентный спуск в мире моды

Как нейронные сети “узнают”

Фото Рохит Тандон / Нескользящий фотообои

Искусственные нейронные сети (ИНС) являются универсальными аппроксиматорами функции. Они могут приближать любые сложные функции, если им предоставить достаточное количество данных, правильную архитектуру и обучить их достаточное время.

Но что означает “обучение” сети?

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

В этой статье я расскажу об алгоритме градиентного спуска, который используется для настройки весов ИНС.

Давайте начнем с основных понятий.

Спуск с горы

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

У нас нет карты, туманно и стемнеет, мы потеряли маршруты и нам нужно быстро добраться до низа. Не самая приятная ситуация, но это показывает “границы” проблемы.

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

Спуск с вершины Монвизо. Маленькая долина рядом с Ончино, Кунео. Изображение автора.

Когда становится темно, мы не видим, в каком направлении движемся. Единственный способ спуститься – сделать небольшие шаги и проверить, находится ли мы на меньшей высоте или нет.

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

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

Это иллюстрирует основную рабочую принцип градиентного спуска и его главные проблемы. Мы вернемся к этому примеру, но сначала рассмотрим более формальное объяснение.

Что такое градиент?

Градиент – это представление скорости изменения функции. Он указывает направление наибольшего увеличения или уменьшения. Интуитивно это означает, что градиент равен нулю в локальном максимуме или локальном минимуме.

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

Давайте посмотрим на это в математической записи. Предположим, у нас есть n-мерная функция f:

Градиент этой функции в точке p (определяемой n координатами) задается следующим образом:

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

Метод градиентного спуска

Для множества задач оптимизации мы стремимся минимизировать функцию потерь для достижения наиболее точного результата.

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

Это позволяет нам использовать производные функции потерь по отношению к независимым переменным в качестве указателя того, двигаемся ли мы к решению (глобальному минимуму).

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

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

Например, посмотрим на следующую картину, чтобы проиллюстрировать, как это работает при оптимизации функции одной переменной:

Упрощенный пример градиентного спуска в задаче с одной переменной. Картинка от автора.

Как видите, мы начинаем нашу “поисковую” процедуру для минимума в произвольной точке (я изобразил два примера, A и B). Постепенно мы делаем шаги в сторону ближайшего минимума, изменяя θ пропорционально наклону.

Иллюстрация показывает, что делает следующий алгоритм (псевдокод) [1]:

θ(0) = θ_init       # Инициализация переменной оптимизацииη = 0.02            # Произвольный параметр размера шагаЕ = 0.01            # Точность оптимизацииk = 0               # Счетчик итерацийwhile |f(θ(k+1) - f(θ(k))| > Є: θ(k+1) = θ(k) - η ∇f(θ(k)) k = k + 1

Функция потерь здесь похожа на наши примеры гор, когда темно и у нас нет карты: мы не знаем, как они выглядят. Мы хотим знать значение θ, при котором достигается минимум для J, но алгоритм оптимизации не знает, каким будет значение J для всех возможных значений θ .

Вот почему мы начинаем наш алгоритм оптимизации с любого произвольного значения θ. Например, точки A и B на картинке представляют два разных начальных значения.

Потенциальные проблемы с градиентным спуском

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

Если функция, которую мы пытаемся оптимизировать, выпукла, то для любого значения ϵ найдется такой размер шага η, что градиентный спуск сойдется к θ* в пределах ϵ от истинного оптимального θ. [1]

Однако, как вы могли догадаться, это не идеально. Алгоритм может сойтись, но это не гарантирует, что мы найдем глобальный минимум.

Основные проблемы градиентного спуска следующие:

Произвольная начальная инициализация оказывает влияние на результаты

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

Например, начиная с точки B вместо точки A на предыдущей картинке выше.

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

Градиентный спуск по седловой поверхности. Анимация показывает, как разные начальные точки могут привести к разным результатам. Картинка от автора.

Выбор подходящего размера шага требует компромисса между скоростью сходимости и стабильностью

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

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

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

Изображения из онлайн-курса Глубокое понимание глубокого обучения, Майк Коэн.

Истинный глобальный минимум функции находится около -1.4. Можно видеть, что для более маленьких скоростей обучения требуется большее количество эпох обучения для сходимости к этому результату. Поэтому кажется, что просто использование более большой скорости обучения может помочь нам сократить вычислительное время.

Но на практике это не так просто с точки зрения скорости сходимости.

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

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

Пример стохастического градиентного спуска, примененного к f(x) = x^2 с разными размерами шага. Во всех случаях алгоритм инициализируется в x = 2. Изображение автора, с базовым кодом, адаптированным из блокнота Сарадж Ривала.

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

Увеличение скорости обучения на порядок привело к застою алгоритма. Результаты для η = 1 осциллируют между x = 2 и x = -2, и это показано только синей горизонтальной линией на левой фигуре.

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

С другой стороны, слишком маленькие размеры шага могут создать очень медленную сходимость или совсем никакой сходимости.

Градиентный спуск для обучения нейронной сети

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

Где Dtrain – количество образцов в нашем тренировочном наборе данных.

Таким образом, мы можем реализовать градиентный спуск на основе следующего алгоритма, в котором мы вычисляем градиент тренировочных потерь по отношению к весам в течение определенного числа эпох для обновления весов модели [2]

w = [0, ... ,0]    # Инициализация весовfor k = 1,..., num_epochs:       # Повторяем для желаемого числа итераций  grad = ∇w TrainLoss(w)         # Градиент тренировочных потерь   w[k] <-- w[k-1] - η * grad     # Обновление весов модели

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

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

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

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

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

Чтобы преодолеть проблему медленной сходимости “ванильного” градиентного спуска, мы можем обновлять веса модели на основе каждого образца тренировочного набора. Преимущество заключается в том, что нам не нужно ждать, пока мы пройдем через весь набор, чтобы выполнить только одно обновление весов.

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

Вот как выглядит алгоритм стохастического градиентного спуска (SGD)

w = [0, ... ,0]    # Инициализация весовfor k = 1,..., num_epochs:       for (x, y) ∈ Dtrain:                 grad = ∇w J(x,y,w)                w[k] <--  w[k-1] - η(k) * grad 

Обратите внимание, что шаг является функцией итераций обучения. Это потому, что для сходимости алгоритма, η должно уменьшаться по мере продвижения числа итераций. В лекционных заметках MIT 6.036 [1] упоминается следующая теорема:

Теорема 4.1. Если J является выпуклым, и η(t) – последовательность, удовлетворяющая

тогда SGD сходится с вероятностью один к оптимальному θ.

Люди используют разные подходы для уменьшения значения η с прогрессом обучения, и это часто называется “охлаждение”:

  • Изменение скорости обучения пропорционально эпохам обучения (например, η(t) = 1/t), или установка ее на меньшее значение после достижения определенной эпохи обучения. Этот метод дает хорошие результаты, но не связан с производительностью модели. Это называется “затухание скорости обучения” и является стандартом в индустрии для решения этой проблемы.
  • Умножение скорости обучения на градиент функции потерь: этот метод является хорошим, потому что он адаптивен к проблеме, но требует тщательного масштабирования. Это включено в RMSprop и Adam модификации градиентного спуска.
  • Умножение скорости обучения на потери: преимущество заключается в том, что оно также адаптивно к проблеме, но также требует масштабирования.

SGD может работать хорошо после посещения только части данных. Это поведение может быть полезным для относительно больших наборов данных, потому что мы можем уменьшить объем необходимой памяти и общее время выполнения по сравнению со “ванильной” реализацией градиентного спуска.

Можно сказать, что “ванильная” реализация медленнее, потому что ей необходимо пройти через всю “партию” образцов, чтобы выполнить одно обновление весов. SGD выполняет обновление для каждого образца, но качество обновлений ниже. У нас может быть шумные данные или очень сложная функция, которую мы пытаемся подогнать к нашей ИНС. Это похоже на то, что использование партии размера Dtrain – это медленно, но точно, а использование партии размера 1 – это быстро, но немного менее точно.

Есть термин посреди, и он называется “мини-партия” градиентный спуск.

Мини-партия градиентного спуска

Фото Себастьяна Херрмана на Unsplash

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

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

Преимущество этого заключается в том, что мы обновляем параметры модели после каждого мини-батча, а не после просмотра всего набора данных. Подход, на который я ссылаюсь как “обычный” градиентный спуск, также известен как пакетный градиентный спуск, где размер пакета – это общее количество образцов в тренировочном наборе данных. Для градиентного спуска с мини-пакетами, мини-пакеты обычно являются степенями двойки: 32 образца, 64, 128, 256 и так далее.

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

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

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

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

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

Тем временем вам может быть интересно прочитать мою предыдущую статью о передающих искусственных нейронных сетях:

Передающие искусственные нейронные сети

Основной концепт, объясненный

VoAGI.com

Ссылки

[1] МИТ Открытая библиотека обучения: 6.036 Введение в машинное обучение. Глава 6: Градиентный спуск

[2] Онлайн-курс Стэнфордского университета: Искусственный интеллект и машинное обучение 4 — Стохастический градиентный спуск | CS221 Стэнфорд (2021)

[3] Онлайн-курс Глубокое понимание глубокого обучения, Майк X Коэн (sincxpress.com)

[4] Онлайн-курс Стэнфордского университета: CS231 Сверточные нейронные сети для визуального распознавания

Опубликовано на сайте https://www.makerluis.com 4 ноября 2023 года.