Ранее мы с вами
с нуля реализовали алгоритм back propagation для очень
простой НС. И увидели, что в его основе лежит вычисление производной от
выбранной функции потерь по вектору параметров :
которая зависит
от функции ,
описывающей работу НС в целом. И при реализации алгоритма обучения мы самостоятельно
вычисляли все необходимые производные для корректировки весов связей. Но, так
как это важная, ключевая и, в общем-то, распространенная операция, то фреймоврк
PyTorch предоставляет
инструменты автоматизации вычисления производных. Об этом и пойдет речь на
данном занятии.
Автоматическое дифференцирование
Давайте
представим, что у нас есть функция вида:
и мы хотим
посчитать ее значения при разных x, y. Один из
способов сделать это как раз через граф вычислений:
Эффективность
здесь достигается за счет устранения дублирования при вычислениях (если они
возникают), а также за счет распараллеливания независимых операций. Кроме того,
как мы сейчас увидим, такой граф также позволяет эффективно вычислять и
производные функции, что весьма полезно при разработке алгоритмов машинного
обучения и, вообще, при решении различных оптимизационных задач.
Общая идея
вычислительного графа – представить целевую функцию набором элементарных
математических операций (сложение, вычитание, умножение, деление) и
стандартного набора функций (sin, cos, exp, log, sqrt и т.д.).
Существует теорема, доказывающая, что любую непрерывную функцию можно
представить с заданной точностью набором таких математических действий, то
есть, с помощью вычислительного графа.
Давайте теперь
посмотрим, как эта конструкция позволяет вычислять производные. Возьмем граф
для функции:
и рассмотрим
идею расчета производных, которая реализована в PyTorch. Сразу отмечу,
что мы будем искать численные значения производных, а не их аналитический вид.
То есть, PyTorch выполняет
численное дифференцирование функций, используя для этого метод обратного
вычисления производных (reverse mode differentiation), известный также под
названием автоматическое дифференцирование:
https://en.wikipedia.org/wiki/Automatic_differentiation
Это значит, что
вначале нам нужно значениям x, y присвоить
некоторые значения, для которых и будут вычислять частные производные. Пусть, x = 2, y = -4.
Пропускаем эти значения по графу и запоминаем результаты вычислений в каждом
узле.
Затем, PyTorch приступает к
вычислению частных производных сложной функции ,
используя цепное правило. В нашем примере оно реализуется, следующим образом:
где
Фактически, это
правило вычисления производной сложной функции, известное еще со школьной
скамьи. Чтобы было понятнее, применяя его, получим следующие значения частных
производных в аналитическом виде:
Но вернемся к PyTorch. Процесс
вычисления производных начинается с конца (истока), то есть, вычисляется
производная (которая,
очевидно, равна 1). Далее, в соответствии с цепным правилом, нам нужно
вычислить производную:
Так как
функциональные узлы вычислительного графа составлены из элементарных функций,
то PyTorch «знает»
аналитический вид их производных. И, в частности, «понимает», что .
Само значение у
нас было вычислено при прямом проходе по графу с x = 2, y = -4. Значит,
Далее, по
цепному правилу нам нужно вычислить частную производную
Она просто равна
1, поэтому .
Аналогично сразу вычисляем и производную .
Следующим шагом
нам нужно вычислить производную:
И, затем, на
основе частных производных вычислить
их при x = 2, y = -4:
Складываем
полученные величины и получаем частные производные функции по x и y:
Вот так, с
помощью вычислительного графа, мы нашли значения производных функции в
точках x = 2, y = -4.
Аналогичным образом находит значения производных и PyTorch.
Реализация автоматического дифференцирования на PyTorch
Чтобы увидеть,
как вся эта математика реализуется на PyTorch, выполним
вычисления производных этой же функции в той же точках x = 2, y = -4. Для этого
я приведу следующую простую программу:
import torch
x = torch.tensor([2.0], requires_grad=True)
y = torch.tensor([-4.0], requires_grad=True)
f = (x + y) ** 2 + 2 * x * y
f.backward()
print(f)
print(x.data, x.grad)
print(y.data, y.grad)
Давайте детально
разберемся, как она работает. Вначале объявляются два тензора x и y с вещественными
значениями 2 и -4. Причем, у тензоров дополнительно указан аргумент requires_grad=True, который
«указывает» фреймворку PyTorch вычислять для них градиенты. По
умолчанию этот параметр равен False: requires_grad=False. После этого
вычисляется значение функции по
требуемой формуле. А, затем, происходит магия автоматического дифференцирования
путем вызова метода backward. В результате тензоры x и y будут содержать
свойства:
-
x.data, y.data – исходное
значение тензоров (то есть 2.0 и -4.0);
-
x.grad – значение
производной в
точке ;
-
y.grad – значение
производной в
точке .
Причем, программа
будет корректно работать, даже если формулу расписать в несколько строчек с
промежуточными переменными, например:
a = (x + y) ** 2
b = 2 * x * y
f = a + b
f.backward()
Как же это
происходит? На самом деле очень просто. Тензоры x и y в PyTorch – это не просто
наборы чисел и операций с ними, а программные единицы, которые неявно
взаимодействуют между собой. В частности, команды:
a = (x + y) ** 2
b = 2 * x * y
f = a + b
не только вычисляют
итоговое значение f, но и формируют (неявно) граф вычислений, начиная с
начальных значений тензоров x, y. Поэтому метод backward всегда имеет
возможность вычислить численные значения производных для функции f.