Дифференцируемое программирование движет миром машинного обучения. PyTorch, TensorFlow и любой другой современный фреймворк машинного обучения полагаются на него, когда дело доходит до фактического обучения. Достаточная причина для того, чтобы любой, кто хотя бы отдаленно интересуется ИИ, познакомился с дифференцируемым программированием. Мы приглашаем вас ознакомиться с двумя частями объяснения концептуальной основы, лежащей в основе большей части современной индустрии искусственного интеллекта. Нет доктора философии. обязательный!

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

Чтобы обучить даже глубокую нейронную сеть (DNN) среднего размера с помощью градиентного спуска, вам нужно вычислить множество производных. Вот почему эффективность обучения зависит от умения эффективно дифференцировать. Входит в автоматическое дифференцирование (AD). AD - это то, как компьютеры вычисляют производные. AD, и в частности его так называемый обратный режим (также известный как обратное распространение), является алгоритмической движущей силой нынешней золотой лихорадки AI. Без него серьезные приложения для машинного обучения были бы невозможны - не говоря уже о прибылях - даже в современной облачной инфраструктуре. Фактически, если вы видите глубокую нейронную сеть, работающую где угодно, значит, она была обучена с помощью AD в обратном режиме.

Сегодняшний пост ответит на следующие вопросы:

  • Что такое программное обеспечение 2.0 и почему идея дифференцируемого программирования может быть даже шире, чем машинное обучение?
  • Как мой класс по математике в старшей школе связан с градиентным спуском, рабочей лошадкой машинного обучения?
  • Что значит различать код? Как можно вычислить производную от части, скажем, кода Python, Scala или C?
  • Почему можно и нужно автоматизировать дифференциацию?

Программное обеспечение 2.0, или программирование с заполнением пробелов

Когда в конце 2010-х машинное обучение стало нормой, знаменитости в области искусственного интеллекта, такие как Ян Лекун или Андрей Карпати, популяризировали идею программного обеспечения 2.0: если нейронные сети, особый вид компьютерных программ, могут изучать свои оптимальные параметры на основе достаточного количества обучающих данных, тогда почему бы нам не разработать все программы так, чтобы они оптимизировались на основе данных, описывающих их правильное поведение?

Чтобы проиллюстрировать эту идею, представьте себе пространство всех возможных программ - огромное пространство программ, где каждая возможная компьютерная программа представлена ​​одной точкой. Программное обеспечение 2.0 - это когда вы не исправляете всю свою программу, а скорее выбираете подмножество программного пространства - параметризуемый класс возможных программ - и оставляете его на усмотрение компьютера для поиска оптимального выбора в этом классе. Это то, что Андрей Карпатый называет программированием с заполнением пробелов. Вы определяете рамку, Программное обеспечение 2.0 рисует картину.

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

Чтобы этот подход работал, мы должны немного помочь компьютеру с помощью специального вида тестов, так называемых мягких модульных тестов (SUT). SUT - это в основном ваш обычный модульный тест с той лишь разницей, что он не дает вам двоичного ответа (зеленый свет при успехе, красный свет при неудаче), а вместо этого вычисляет балл, чтобы количественно определить, насколько вы далеки от ожидаемого. Например, при проверке сложения SUT скажет вам, что 2 + 2 = 4,1 меньше неверно, чем 2 + 2 = -11.

С точки зрения тестировщика программа представляет собой черный ящик, преобразующий входные данные в выходные. При некотором вводе x он производит вывод f (x). Конечно, если вы разрежете черный ящик, вы обнаружите (возможно, огромное) количество внутренних параметров. В обычной программе все эти параметры были бы зафиксированы на определенных значениях. В ПО 2.0, однако, путем настройки этих параметров программа «перемещается» по программному пространству.

Каждый из SUT содержит входной x и выходной y. Входные данные вставляются в программу, которая производит на их основе выходные данные f (x). Этот результат сравнивается с ожидаемым результатом и получает хорошую оценку, когда оба значения примерно равны. Результат сравнения передается обратно в алгоритм программного обеспечения 2.0, где он используется для настройки параметров для постепенного улучшения результатов теста. г

После относительно недолгой шумихи термин «программное обеспечение 2.0» не стал тем модным словом, на которое надеялись его изобретатели. Однако, если вы в последнее десятилетие были где-то рядом с машинным обучением, вы, вероятно, слышали о наиболее подходящем подклассе программного обеспечения 2.0: дифференцируемый код, иногда записываемый как ∂Code («∂» - это символ частичной дифференциации).

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

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

Чтобы проиллюстрировать эти проблемы и их решение, давайте сначала рассмотрим дифференцируемое программирование.

Первая дифференцированная программа

(Идея этого раздела заимствована из Дифференцируемого функционального программирования, N. Welsh, 2018)

Представьте, что вам нужно написать довольно простую программу. Требования следующие: Программа должна принимать одно число с плавающей запятой в качестве входного параметра x. Из этого ввода он должен произвести вывод y, еще одно число с плавающей запятой. Вам предлагается 200 мягких модульных тестов, описывающих требуемое поведение. Каждый тест определяется парой (x, y) из одного входа x и одного ожидаемого выхода ŷ.

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

Показатель 𝓛⁽ⁱ⁾ - это так называемый проигрыш i -го теста. Оптимально, это будет ноль, потому что тогда ввод x ⁽ⁱ⁾ будет точно соответствовать ожидаемому результату ŷ ⁽ⁱ⁾. В таком случае общая метрика производительности набора тестов 𝓛 нашей программы представляет собой просто сумму всех 200, или, выражаясь словами математический цикл for,

Чем меньше 𝓛, тем лучше наша общая производительность. Поскольку тесты представляют собой пары чисел, мы можем визуализировать все 200 на одном 2-мерном графике с входами на оси x и выходами на оси y. . Предположим, график ваших тестовых данных выглядит следующим образом:

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

Учитывая синусоидальную природу отношения ввода-вывода в вашем наборе тестов, у нас есть подозрение, что

может быть разумно хорошим выбором. В этой функции есть только один внутренний параметр: ω (это строчная «омега»), обозначающий частоту синуса. Следовательно, мы рассматриваем одномерное подмножество программного пространства, которое вы можете визуализировать в виде линии от ω = -∞ до ω = ∞, как и x - ось изменяется от x = -∞ до x = ∞.

Каждое возможное значение ω- соответствует другой версии нашей программы f (x). Допустим, мы установили ω на 12,345, тогда наша программа станет

Итак, давайте попробуем! Давайте выберем некоторые ω -значения и посмотрим, сможем ли мы удовлетворить все эти модульные тесты.

Судя по анимации выше, ω = 1,5 - безусловно, лучший результат. Но может ли быть лучшее решение, скажем, на уровне 1.54321? В этом примере нет. На самом деле, по случайному выбору, вам могли потребоваться десятки или сотни попыток, чтобы найти достаточно хорошее решение, и вы, вероятно, все еще не подошли очень близко к правильному 1,5.

Как нам выйти из этой дилеммы? Подумайте о метрике потерь 𝓛. Каковы параметры, то есть неизвестные, в этой функции? Несмотря на наличие пар 200 x - y, структура 𝓛 очень проста. По общему признанию, для человека было бы немного громоздко выписать полную сумму из 200 терминов, по одному для каждой ТРИ. Однако для компьютера сумма из 200 членов не страшнее, чем сумма из двух. Остается простая однопараметрическая функция 𝓛 (ω). Если вы посмотрите на его определение выше, то увидите, что оно полностью определяется x⁽ⁱ⁾- и ŷ ⁽ⁱ⁾-значениями набора тестов. Это означает, что нет необходимости в значениях параметров рандомизированной выборки вообще. Вы уже знаете значение функции в каждой отдельной точке! Вы можете легко построить график 𝓛 (ω), как и любую другую функцию:

Итак, чтобы найти оптимальное ω, вам просто нужно найти минимум функции 𝓛 (ω). Нахождение минимумов - подождите, разве мы не занимались этим все время на уроках математики в старшей школе? Верно. И это как-то связано с производными. К счастью, 𝓛 (ω) не только структурно простой, но и дифференцируемый. Термин дифференцируемый означает, что вы можете вычислить производную от 𝓛 (ω). Фактически, вся причина, по которой наш небольшой пример здесь называется дифференцируемым программированием, заключается в том, что 𝓛 (ω), функция, для которой мы хотим оптимизировать, дифференцируема. Давайте обновим наши вычислительные воспоминания, чтобы увидеть, как производная может помочь нам систематически оптимизировать.

Резюме исчисления

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

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

Чтобы увидеть, как производная связана с наклоном, давайте взглянем на ее определение. Обычно производная записывается как f ′ (читается: «eff prime») и определяется как математический предел формы

Вы можете визуализировать производную как наклон прямой линии, идущей от точки (x, f (x)) до ( x + Δx, f (x + Δx)) при уменьшении горизонтального расстояния Δx, сближая точки. В математическом пределе x, f (x)) до (x + Δx , f (x + Δx)) при уменьшении горизонтального расстояния Δx, Δx → 0 точек практически сливаются.

Тогда наклон вполне естественно определяется как

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

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

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

Что именно это означает сейчас? Предположим, у вас сейчас значение параметра x₀. Здесь локальная производная - f ′ (x₀). Если производная больше нуля, это означает, что, увеличивая x, вы поднимаетесь, а уменьшая x, вы опускаетесь. Итак, что вы делаете, чтобы приблизиться к своей цели, наивысшей точке? Вы увеличиваете x. Говоря более математически, вы выбираете новое значение параметра x, делая шаг в направлении положительного наклона:

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

Эта формула автоматически охватывает противоположный случай, когда производная меньше нуля. Отрицательное значение f ′ означает, что для большего x вы опускаетесь, дальше от своей цели, на самую высокую точку. Следовательно, при отрицательном f ′ (x₀) движение вверх означает шаг к меньшему x -значению. . Если вы хотите минимизировать значение функции, действуйте аналогично, только теперь вы делаете ɛ -шаги в направлении отрицательного наклона (под гору):

Процесс представлен ниже.

Проницательный читатель заметит несколько проблем с этим подходом. Во-первых, вы всегда найдете минимум или максимум, ближайший к вашей отправной точке. В приведенном выше примере, если вы случайно начали ближе к x ₘᵢₙ, ₂, чем к x ₘᵢₙ, ₁, минимизация с использованием описанного метода всегда приведет вас к x ₘᵢₙ, ₂ и никогда - до x ₘᵢₙ, ₁, хотя это будет еще более низкий, то есть лучший, пункт.

Это хорошо известная проблема, возникающая из-за локальной природы самого производного инструмента, и это очень сложная проблема. Каждый метод, основанный на производных инструментах, всегда будет иметь этот встроенный недостаток. Проблема еще больше, особенно в более высоких измерениях, потому что помимо минимумов и максимумов есть также седловые точки, другие сущности со свойством f ′ = 0, которые будут мешать вашему поиску оптимума.

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

Вторая проблема - это выбор размера шага ɛ. Если он слишком большой, вы рискуете перескочить, то есть вы можете перепрыгнуть через оптимум вместо того, чтобы приближаться к нему. Если, с другой стороны, он слишком мал, вам, возможно, придется предпринять много шагов, чтобы приблизиться к оптимальному значению. Хуже того, размер шага на самом деле не ɛ, а ɛf ′ и, следовательно, пропорционален значению производной. Поскольку в экстремуме производная становится равной нулю, когда вы приближаетесь к нему, производная будет становиться все меньше и меньше, что, в свою очередь, приведет к тому, что вы будете делать все меньшие и меньшие шаги. Чтобы бороться как с этим замедлением, так и с проблемой выхода за пределы, на практике ɛ выбирается адаптивно.

Экскурсия в высшие измерения

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

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

Мы пытаемся сделать то же самое, что и раньше, то есть найти локальный оптимум. Представьте, что мы ищем локальный минимум. Наша логика из одномерного случая по-прежнему отлично работает: чтобы приблизиться к следующему локальному минимуму, нужно спуститься вниз. Но для этого нам нужно знать наклон. Как рассчитать уклон в 2-D?

Например, прямую, параллельную оси y в точке xx = -0,5, можно рассматривать как функцию y g (y) = f (-0,5, y) с константой x = -0,5. Это означает, что в любой точке (x, y) мы можем разделить двумерную функцию на две одномерные функции. И мы уже знаем, как получить наклон одномерной функции! Мы просто вычисляем его производную.

Но сначала нам нужно немного уточнить наши обозначения. Теперь, когда существует более одной переменной, обозначение f ′ стало неоднозначным, поскольку неясно, является ли производная по x или y . Чтобы устранить неоднозначность, есть более общие обозначения. Производная по x обозначается

а производная по y равна

Символ ∂ имеет много имен, таких как умереть, до или - мой любимый - дабба, и это лишь некоторые из них. Но поскольку это стилизованная буква d, вы можете просто прочитать ее dee и производную от x как dee eff dee ex. Если вы не привыкли к многомерному исчислению, вас может сбить с толку, как внезапно дробь используется для обобщения простого символа простого числа ('). Это происходит вполне естественно, потому что, как мы видели в предыдущем разделе, производная является дробью:

Числитель ∂f можно интерпретировать как разницу в f, а знаменатель ∂x как разницу в x , в рамках предельного процесса.

Определившись с обозначениями, как нам объединить две одномерные производные обратно в двухмерный объект, чтобы мы могли сказать, в каком направлении изменять наши параметры (x, y )?

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

и еще один небольшой шаг в направлении x, опять же, точно так же, как и в 1-D, то есть

вы фактически сделали двумерное движение в правильном направлении, прямо к минимуму. Что же теперь достаточно маленького с точки зрения размера шага? Строго говоря, два одномерных шага аналогичны одному двумерному шагу только в том случае, если эти шаги математики называют бесконечно малыми, то есть бесконечно маленькими. Однако здесь мы в значительной степени в безопасности, поскольку ошибка из-за игнорирования взаимодействия x - и y -измерений с конечными шагами мягко масштабируется с размером шага . Это означает, что при увеличении размера шага вы почувствуете ошибки однопараметрических производных задолго до того, как ошибки смешанной размерности станут значительными (поскольку первые имеют порядок 𝒪 (ɛ), а последние имеют порядок 𝒪 (ɛ ²) и ɛ маленькие).

Конечно, есть лучший и более компактный способ выразить, что вы на самом деле делаете то же самое для каждого измерения. Во-первых, вы можете определить сокращение для x и y:

Во-вторых, набор всех одномерных производных функции, градиент, обозначается ∇ f, который в двухмерном определяется как

Таким образом, вы можете записать два шага одномерной оптимизации вместе как один двухмерный шаг:

Теперь, когда мы увидели, как идеально разложить двумерную проблему на две одномерные проблемы, что остановит нас от перехода к трехмерным, 42-мерным или любому другому мыслимому количеству измерений? Ничего не будет!

Рассмотрим любое положительное целочисленное измерение N. Снова используя сокращение для переменных

уклон в N размерах можно записать как комбинацию N одномерных уклонов:

Более того, записанный таким образом шаг оптимизации в N -D выглядит точно так же, как в 2-D:

На данный момент, возможно, вы думаете, что механика вычисления многомерных производных в конце концов не так уж и сложна, но вы все еще не совсем уверены в собственном мышлении в N -мерные пространства. Если так, не волнуйтесь, вы в хорошей компании! Просто сделайте так, как рекомендует Джеффри Хинтон, один из старейшин искусственного интеллекта:

Чтобы иметь дело с гиперплоскостями в 14-мерном пространстве, визуализируйте трехмерное пространство и очень громко скажите себе «четырнадцать». Все так делают.

Джеффри Хинтон (в его архиве на Coursera MOOC, Лекция 2c: Геометрический взгляд на восприятие, около 1 минуты)

Чтобы понять все о градиентах, вы всегда можете подумать о трехмерном ландшафте, таком как в нашем примере выше, а градиентный спуск - это не что иное, как прогулка по холмам этого ландшафта. Если вам все еще трудно визуализировать градиентный спуск даже в 3-D, люди с losslandcape.com сделали для вас красивую анимацию (с дурацким закадровым голосом).

Дифференциация кода

Сейчас мы довольно много говорили о производных математических функций в одном или многих измерениях. Но что является производным от компьютерного кода? Давайте посмотрим на некоторые конкретные языки программирования и вычислим некоторые фактические производные кода. Чтобы следовать, вы должны либо быть знакомы с производными элементарных функций, таких как x², sin (x) и exp (x), либо иметь под рукой ссылку.

Рассмотрим, например, эту простую функцию, реализованную на Python:

def f(x: float) -> float:
    return 3.0 * x

Эта функция реализует математическую функцию f (x) = 3 x. В классе исчисления было бы проще всего вычислить его производную: f ′ (x) = 3. Итак, как будет выглядеть наилучшая реализация производной в Python? Как насчет этого:

def fprime(x: float) -> float:
    return 3.0

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

Давайте посмотрим на другой пример, на этот раз на Scala:

def g(x: Double): Double =
    math.sin(x)

Опять же, рассмотрим реализованную здесь математическую функцию: g (x) = sin (x). Производная этой важной, но элементарной функции - это просто g ′ (x) = cos (x). Следовательно, производная нашей функции Scala g (x: Double): Double должна быть такой:

def gprime(x: Double): Double =
    math.cos(x)

Вот еще один последний пример, на этот раз на C, чтобы убедиться, что мы все правильно поняли:

float h(float x) {
    return exp(x) + 1.0/x;
}

Нам нужна функция float hprime (float x). Как бы она выглядела? Начнем снова с реальной математической функции, реализованной в коде

и применяя основные правила дифференцирования, получаем

Это может быть легко реализовано на C снова!

float hprime(float x) {
    return exp(x) - 1.0/powf(x, 2);
}

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

Сложные функции - это цепочки простых

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

Функцию h (x) можно рассматривать как результат последующего применения двух функций

как можно увидеть здесь:

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

Давайте сделаем еще один шаг в нашей композиции и подключим h (x) к еще одной функции.

Результат будет

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

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

Если g (x) сам по себе является композицией, правило цепочки может применяться рекурсивно, чтобы различать произвольно длинные цепочки составов.

Следовательно, если мы знаем, как различать относительно небольшой набор элементарных функций, таких как sin (x), exp (x), log (x ) и xⁿ, а также базовых операций, таких как суммы и произведения функций, правило цепочки дает нам простой рецепт для вычисления производных любой функции, какой бы сложной она ни была.

Теперь вы можете использовать этот рецепт для вычисления производной любой функции вручную, например, на листе бумаги, а затем реализовать результат в коде. С математической точки зрения в таком подходе нет ничего плохого. На практике, однако, даже если у вас есть только умеренно сложная функция, например, продукт нескольких десятков факторов, каждый из которых состоит из нескольких связанных элементарных функций, этот процесс становится утомительным и подверженным ошибкам. Для серьезных приложений, таких как, например, обучение DNN, состоящего из тысяч элементарных операций, ручной подход был бы нелепым. Возникает извечный вопрос, который всегда возникает, когда приходится сталкиваться с огромным объемом утомительной работы: А кто-нибудь другой не может этого сделать?

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

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

Заключение

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

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

  • Как работает автоматическое дифференцирование (AD)
  • Что такое автоматическая дифференциация (AD) в прямом и обратном (обратном) режимах
  • Зачем градиентному спуску нужен обратный режим AD
  • Как вычислять производные циклов for, условных выражений и других управляющих структур
  • Для чего дифференцируемое программирование можно использовать помимо машинного обучения

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

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

Первоначально опубликовано на https://consilica.de 11 мая 2020 г.