Я читал прекрасную книгу «Глубокое обучение для R» Франсуа Шолле и Дж. Дж. Аллера. Как и в большинстве практических руководств по глубокому обучению, в книге представлены 3 элемента, необходимые для обучения DNN:

  1. Модель: ŷ = f(x; w), где w — набор весов, параметризующих функцию, а ŷ — прогноз модели. Обычно функция f выглядит следующим образом:
    ∑g(wx+b), где g — нелинейная функция активации (relu, сигмовидная и т. д.).
  2. Потери: целевая функция, которая сравнивает прогнозы модели с правдой, т. е. ||y-ŷ||²
  3. Оптимизатор: обновляет веса w, чтобы улучшить потери. Оптимизатор использует градиентный спуск и друзей (Adam, RMSprop) для обновления весов на основе «градиента функции потерь относительно весов» (функция, которая описывает, как меняются потери при изменении весов) и скорости обучения. w(t+1) = w(t)-ℓ(dL/dW). Вычисление градиента выполняется с помощью обратного распространения.

В книге представлен отличный практический пример определения модели, потерь и оптимизатора. Однако мне стало очень интересно узнать о частях 2 и 3 — как волшебный «оптимизатор» обновляет веса модели, чтобы улучшить потери? Почему авторы книги уклонились от описания математики шага обновления?

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

  • Почему слово «градиент» звучит так знакомо?
    Подсказка: Calc III.
  • Почему мы аппроксимируем производные с помощью загадочного «обратного распространения», а не моего старого знакомого с конечными разностями?
    Подсказка: у страшного цепного правила наконец-то появилась цель!
  • Являются ли нелинейные функции активации похожими на функции связи в обобщенных моделях?
    Подсказка: Да.

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

|----------------|---------------------|------------------------|
buzzwords    using Keras          this post     implementing a DNN    
                                                       (CS231)

Понимание волшебства также помогает объяснить, почему такие инструменты, как TensorFlow, такие мощные и популярные.

В ходе ответов на мои вопросы я нашел ресурсы, которые попали в несколько ведер: сообщения, которые были в основном модными словечками, сообщения SO, в которых выгружался код рядом с уравнениями, статьи в Википедии, где доказательства были оставлены в качестве упражнения, и полезные сообщения / конспекты лекций, которые сбалансированная интуиция с примерами. Последняя группа была малонаселенной, и я надеюсь, что этот пост будет долгожданным дополнением. В частности, я во многом полагался на заметки о курсе из Stanford’s CS231 и отличный пост Sebastian Ruder.

Как обновляются веса?

Фундаментальный вопрос для обучения DNN заключается в том, как обновить веса в модели, чтобы сделать более точный прогноз. Математический ответ*:

w(t+1) = w(t) - ℓ(dL/dw)

Формула гласит: вес на шаге t+1 равен предыдущему весу за вычетом небольшого изменения: ℓ(dL/dw)

Небольшое изменение состоит из скорости обучения ℓ, умноженной на «градиент потери по отношению к весам, w». Вы можете спросить: «Что, черт возьми, это значит»? (Я сделал). Чтобы ответить, сделайте шаг назад:

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

Всякий раз, когда мы хотим знать, как изменится функция, мы используем производную. Вспомните расчет 1. Человек идет со скоростью v. Со временем человек ускоряется и замедляется. Как мы узнаем, ускоряется или замедляется человек и насколько? Ускорение! а = дв/дт.

Как узнать, увеличиваются или уменьшаются потери и насколько? dL/dw — производная от L по w! Поскольку w может быть многомерным, мы используем градиент вместо производной.

Как именно мы используем скорость изменения dL/dw для обновления w?

Представьте, что мы редактируем фильм о человеке. Фильм начинается с бегущего человека, затем он замедляется до шага, а затем останавливается. Затем человек снова начинает идти и ускоряется до тех пор, пока не побежит. Нас помещают в случайный кадр фильма и просят найти кадр, на котором человек остановился. Как мы могли это сделать? Что ж, мы можем начать с того, что немного сдвинем кадр вперед в фильме. Если человек ускоряется, то мы знаем, что идем в неправильном направлении, и нам нужно идти в другую сторону. Если человек замедляется, мы движемся в правильном направлении и должны продолжать идти. Кроме того, если человек сильно ускоряется, то мы знаем, что нам нужно идти в другую сторону. И наоборот, если человек немного ускоряется, то мы знаем, что нам нужно только немного вернуться назад. Математически мы могли бы выбрать новый кадр по формуле:

новый t = старый t - dv/dt

Если человек ускоряется (dv/dt >0), то мы идем назад во времени. Если они замедляются (dv/dt ‹0), то идем вперед. Скорость, с которой мы идем, пропорциональна тому, как быстро человек меняет скорость.

Именно так мы используем градиент для обновления весов. Если dL/dw становится больше, мы уменьшаем w, вычитая dL/dw. Насколько сильно изменить w, зависит от того, как быстро меняются потери.

К настоящему времени я победил дохлую лошадь, но, если вкратце, градиенты говорят нам, как изменить веса. Этот подход называется «градиентным спуском».

Если вы теперь полностью помните Расчет 1, вы также можете вспомнить, что функция имеет минимум, когда производная равна 0. Когда потери минимальны, dL/dw = 0, мы перестанем изменять наши веса. Хороший.

Я проигнорировал объяснение значения ℓ, подробнее о скорости обучения позже.

Как рассчитываются градиенты?

Чтобы обновить веса, мы остановились на w(t+1) = w(t) — ℓ(dL/dw), что означает, что нам нужно рассчитать (dL/dw). Возможно, вы помните, что вычисление производной сложной функции — непростая задача. Как это делает Tensorflow?

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

Числовое дифференцирование — конечные различия

Численное дифференцирование — это хлеб с маслом большинства классов научных вычислений. Численное дифференцирование — это подход, который говорит, что при заданном f(x) вы можете аппроксимировать f’(x), используя конечную разницу. f’(x) = dy/dx ≅ (f(x+h)-f(x))/h. По сути, вы следуете определению и аппроксимируете производную, делая небольшое изменение x! Довольно просто, и вы можете доказать много интересного о точности вашего приближения. В основе числовой дифференциации f’ никогда не известно, вместо этого f’ заменяется серией оценок f. Это прекрасно работает, если вы понятия не имеете, что такое f’, И вы можете быстро вычислить f. Но для нейронных сетей вычисление f для значения x обходится дорого (весь прямой проход). Делать так много раз — плохой способ аппроксимировать f’, потому что оказывается, что мы знаем, как вычислить f’! Как? Использование автоматического дифференцирования.

Автоматическое дифференцирование — обратное распространение

В отличие от численного дифференцирования, автоматическое дифференцирование основано на вычислении производной f' напрямую, а НЕ путем аппроксимации f' с оценками f. Автоматическое дифференцирование решает f’ с помощью хитрого трюка.

Представьте себе функцию f = (x+y)z. Есть несколько разных способов найти частные производные этой функции. Автоматическое дифференцирование использует очень специфический подход, разбивая функцию на 2 части:

  1. q = x+y
  2. f = qz

Частная производная df/dx может быть вычислена с помощью цепного правила:

df/dx = (df/dq)(dq/dx)

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

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

Обратное распространение

Я сказал, что автоматическое дифференцирование работает, разбивая части формулы и вычисляя производную каждой части. Чтобы сделать это вычисление эффективно, используется обратное распространение. Обратное распространение работает в процессе из 3 частей:

  1. Разбейте уравнение на маленькие упорядоченные шаги
  2. Вычислите прямой проход, который сохраняет каждую часть уравнения, оцененную в определенной точке.
  3. Вычислите обратный проход, который начинается с последнего шага и постепенно вычисляет производную, используя цепное правило для работы в обратном направлении по шагам.

Например, f = (x+y)z. Чтобы найти df/dx, df/dy и df/dz, начнем с записи:

  1. q = (x+y)
  2. f = qz

Прямой проход оценивает q и f в определенной точке x, y, z. Затем обратный проход начинается с вычисления df/dq и df/dz для конкретных значений x,y,z, используемых в прямом проходе. Затем вычисляются df/dx и df/dy с использованием цепного правила для вычисления dq/dx и dq/dy, умноженных на уже вычисленное значение df/dq.

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

Обновление весов:

Помните, уравнение, которое нас интересует, это:

w(t+1) = w(t) — ℓ(dL/dw)

за целую кучу весов!

Используя обратное распространение для решения dL/dw, мы начинаем с прямого прохода для вычисления L при определенном наборе весов, w. Затем обратный проход начинается с вычисления частной производной (dL/dw) для каждого веса. Как следует из названия, обратный проход сначала вычисляет dL/dw для весов, ближайших к выходному слою (аналогично тому, как df/dz вычислялось до df/dx в нашем примере).

А как насчет функций активации?

Вычисление градиента требует знания производной небольших частей сети. Одной из важнейших частей современных нейронных сетей является функция активации. Каждый нейрон в сети вычисляется как g(wx +b), где g — нелинейная функция. Эти функции невероятно важны. Например, если бы g(x) было разрешено использовать только в качестве функции тождества, то несколько слоев и даже несколько нейронов можно было бы свести к одному вычислению wx +b (линейные преобразования транзитивны)! Ничего глубокого в этом нет!

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

Что такого интересного в TensorFlow?

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

Как насчет скорости обучения?

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

w(t+1) = w(t) — ℓ(dL/dw)

ℓ — это скорость обучения, которая определяет, в дополнение к dL/dw, насколько корректировать вес. Почему бы ℓ не = 1? В предыдущем разделе мы рассмотрели, как dL/dw влияет как на направление шага, так и на его размер. Однако оказывается, что полагаясь исключительно на dL/dw для определения величины обновления, могут возникнуть проблемы — представьте себе скейтбордиста, который ходит туда-сюда по хафпайпу, но никогда не останавливается на плоском дне. Скорость обучения (обычно ℓ ‹ 1) может представлять «трение» и гарантирует, что скейтбордист в конце концов перестанет переключаться.

Хотя допустимо использовать скорость обучения ℓ = c, где c — некоторая константа, часто нейронная сеть может сходиться быстрее, если ℓ сама по себе является функцией. Скорость обучения также помогает предотвратить сходимость весов к локальному минимуму, пропуская при этом глобальный минимум (представьте, что вы спускаетесь с горы с ложной вершиной, вы не хотите останавливаться у подножия ложной вершины, вы хотите пройти весь путь до конца). вниз!).

Существует множество вариантов функций ℓ, которые можно использовать в качестве адаптации к градиентному спуску: RMSprop, Adam, Adagrad, импульс Нестерова. Очень рекомендую прочитать Пост Рудера.

Человек за занавеской…

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

*Уравнения в этом посте не претендуют на точность. Например, уравнение w(t+1) = w(t) — ℓ(dL/dw) не имеет правильной формы, учитывая, что w является многомерным. Вместо этого уравнения предназначены для использования в качестве краткого справочника, так же как псевдокод может быть полезен для программистов, решающих задачу, не беспокоясь о специфике обозначений.