2.5 Автоматическое дифференцирование
Как говорилось в разделе 2.4, вычисление производных — ключевой шаг почти всех алгоритмов оптимизации в глубоком обучении. Сами по себе вычисления производных просты и требуют лишь базового математического анализа. Но для сложных моделей ручное обновление параметров — очень трудоёмкая работа, и к тому же легко допустить ошибку.
Фреймворки глубокого обучения ускоряют вычисление производных, вычисляя их автоматически, то есть используя автоматическое дифференцирование (automatic differentiation). На практике по заданной модели система строит вычислительный граф (computational graph), в котором отслеживается, какие данные и какие операции комбинируются для получения результата. Автоматическое дифференцирование позволяет системе затем распространять градиенты назад (backpropagate). Здесь обратное распространение (backpropagation) означает обход всего вычислительного графа с заполнением частных производных по каждому параметру.
2.5.1 Простой пример
В качестве демонстрации предположим, что мы хотим продифференцировать функцию по вектору-столбцу .
Сначала создадим переменную x и присвоим ей начальное значение.
MXNet:
from mxnet import autograd, np, npx
npx.set_np()
x = np.arange(4.0)
x
PyTorch:
import torch
x = torch.arange(4.0)
x
TensorFlow:
import tensorflow as tf
x = tf.range(4, dtype=tf.float32)
x
PaddlePaddle:
import warnings
warnings.filterwarnings(action='ignore')
import paddle
x = paddle.arange(4, dtype='float32')
x
Прежде чем вычислять градиент по , нужно место, где он будет храниться. Важно, что мы не будем выделять новую память при каждом дифференцировании по параметру. Поскольку мы часто обновляем одни и те же параметры тысячи раз, выделение новой памяти каждый раз может быстро исчерпать ресурсы. Заметим, что градиент скалярной функции по вектору — это вектор той же формы, что и .
MXNet:
# Выделяем память под градиент тензора вызовом attach_grad
x.attach_grad()
# После вычисления градиента по x он доступен через атрибут 'grad'; изначально его значение равно 0
x.grad
PyTorch:
x.requires_grad_(True) # Эквивалентно x=torch.arange(4.0,requires_grad=True)
x.grad # Значение по умолчанию — None
TensorFlow:
x = tf.Variable(x)
PaddlePaddle:
x = paddle.to_tensor(x, stop_gradient=False)
x.grad # Значение по умолчанию — None
Теперь вычислим .
MXNet:
# Помещаем код внутрь autograd.record, чтобы построить вычислительный граф
with autograd.record():
y = 2 * np.dot(x, x)
y
PyTorch:
y = 2 * torch.dot(x, x)
y
TensorFlow:
# Записываем все вычисления на ленту
with tf.GradientTape() as t:
y = 2 * tf.tensordot(x, x, axes=1)
y
PaddlePaddle:
y = 2 * paddle.dot(x, x)
y
x — вектор длины 4; вычисляя скалярное произведение x и x, мы получаем скаляр, присвоенный y.
Далее вызовем функцию обратного распространения, чтобы автоматически вычислить градиент y по каждой компоненте x и выведем эти градиенты.
MXNet:
y.backward()
x.grad
PyTorch:
y.backward()
x.grad
TensorFlow:
x_grad = t.gradient(y, x)
x_grad
PaddlePaddle:
y.backward()
x.grad
Градиент функции по должен быть равен . Быстро проверим, что градиент вычислен верно.
MXNet, PyTorch, PaddlePaddle:
x.grad == 4 * x
TensorFlow:
x_grad == 4 * x
Теперь вычислим другую функцию от x.
MXNet:
with autograd.record():
y = x.sum()
y.backward()
x.grad # Перезаписывается новым вычисленным градиентом
PyTorch:
# По умолчанию PyTorch накапливает градиенты, поэтому предыдущие значения нужно обнулить
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
TensorFlow:
with tf.GradientTape() as t:
y = tf.reduce_sum(x)
t.gradient(y, x) # Перезаписывается новым вычисленным градиентом
PaddlePaddle:
# По умолчанию PaddlePaddle накапливает градиенты, поэтому предыдущие значения нужно очистить
x.clear_gradient()
y = paddle.sum(x)
y.backward()
x.grad
2.5.2 Обратное распространение для нескалярных переменных
Когда y не является скаляром, наиболее естественной интерпретацией производной вектора y по вектору x будет матрица.
Для тензоров высокого порядка и размерности результатом дифференцирования может быть тензор высокого порядка.
Хотя такие более сложные объекты действительно встречаются в продвинутом машинном обучении (в том числе в глубоком обучении), при вызове обратного распространения для вектора обычно вычисляют производные функции потерь для каждого компонента батча обучающих примеров. Здесь наша цель — не вычислить матрицу производных, а вычислить сумму частных производных для каждого примера в батче по отдельности.
MXNet:
# При вызове backward для векторной переменной y (функции от x) элементы y суммируются,
# образуя новую скалярную переменную. Затем вычисляется градиент этой скалярной переменной по x
with autograd.record():
y = x * x # y — вектор
y.backward()
x.grad # Эквивалентно y=sum(x*x)
PyTorch:
# Для вызова backward у нескаляра нужно передать аргумент gradient, задающий градиент
# дифференцируемой функции по self. В этом примере нам нужна только сумма частных производных,
# поэтому подходит градиент, равный 1
x.grad.zero_()
y = x * x
# Эквивалентно y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
TensorFlow:
with tf.GradientTape() as t:
y = x * x
t.gradient(y, x) # Эквивалентно y=tf.reduce_sum(x*x)
PaddlePaddle:
x.clear_gradient()
y = x * x
paddle.sum(y).backward()
x.grad
2.5.3 Отделение вычислений
Иногда мы хотим вынести некоторые вычисления за пределы записанного вычислительного графа.
Например, пусть y вычисляется как функция x, а z — как функция y и x.
Предположим, мы хотим вычислить градиент z по x, но по какой-то причине хотим рассматривать y как константу и учитывать только то, как x действует после вычисления y.
Здесь можно отделить y, вернув новую переменную u, которая имеет то же значение, что и y, но отбрасывает информацию о том, как y была вычислена в графе.
Иными словами, градиент не будет распространяться назад через u к x.
Поэтому следующая функция обратного распространения вычисляет частную производную z=u*x по x, рассматривая u как константу, а не частную производную z=x*x*x по x.
MXNet:
with autograd.record():
y = x * x
u = y.detach()
z = u * x
z.backward()
x.grad == u
PyTorch:
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
TensorFlow:
# Установите persistent=True, чтобы вызывать t.gradient несколько раз
with tf.GradientTape(persistent=True) as t:
y = x * x
u = tf.stop_gradient(y)
z = u * x
x_grad = t.gradient(z, x)
x_grad == u
PaddlePaddle:
x.clear_gradient()
y = x * x
u = y.detach()
z = u * x
paddle.sum(z).backward()
x.grad == u
Поскольку вычисление y записано, мы можем затем вызвать обратное распространение для y и получить производную y=x*x по x, то есть 2*x.
MXNet:
y.backward()
x.grad == 2 * x
PyTorch:
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
TensorFlow:
t.gradient(y, x) == 2 * x
PaddlePaddle:
x.clear_gradient()
paddle.sum(y).backward()
x.grad == 2 * x
2.5.4 Вычисление градиентов для потока управления Python
Одно из преимуществ автоматического дифференцирования в том, что
даже если вычислительный граф функции строится через управляющие конструкции Python (условия, циклы или произвольные вызовы функций), мы всё равно можем вычислить градиент получившейся переменной.
В приведённом ниже коде число итераций цикла while и результат оператора if зависят от значения входа a.
MXNet:
def f(a):
b = a * 2
while np.linalg.norm(b) < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
PyTorch:
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
TensorFlow:
def f(a):
b = a * 2
while tf.norm(b) < 1000:
b = b * 2
if tf.reduce_sum(b) > 0:
c = b
else:
c = 100 * b
return c
PaddlePaddle:
def f(a):
b = a * 2
while paddle.norm(b) < 1000:
b = b * 2
if paddle.sum(b) > 0:
c = b
else:
c = 100 * b
return c
Вычислим градиент.
MXNet:
a = np.random.normal()
a.attach_grad()
with autograd.record():
d = f(a)
d.backward()
PyTorch:
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
TensorFlow:
a = tf.Variable(tf.random.normal(shape=()))
with tf.GradientTape() as t:
d = f(a)
d_grad = t.gradient(d, a)
d_grad
PaddlePaddle:
a = paddle.to_tensor(paddle.randn(shape=[1]), stop_gradient=False)
d = f(a)
d.backward()
Теперь проанализируем определённую выше функцию f.
Заметим, что по входу a она кусочно-линейна.
Иными словами, для любого a найдётся некоторая константа k, такая что f(a)=k*a, причём значение k зависит от входа a. Поэтому правильность градиента можно проверить, сравнив его с d/a.
MXNet, PyTorch, PaddlePaddle:
a.grad == d / a
TensorFlow:
d_grad == d / a
Резюме
- Фреймворки глубокого обучения могут вычислять производные автоматически: сначала мы присоединяем градиент к переменной, по которой хотим вычислять частные производные, затем записываем вычисление целевого значения, выполняем его обратное распространение и получаем градиент.
Упражнения
- Почему вычисление производной второго порядка обходится дороже, чем производной первого порядка?
- Запустите функцию обратного распространения сразу повторно после её выполнения и посмотрите, что произойдёт.
- В примере с потоком управления мы вычисляем производную
dпоa. Что произойдёт, если изменить переменнуюaна случайный вектор или матрицу? - Придумайте другой пример для вычисления градиента с потоком управления, запустите его и проанализируйте результат.
- Пусть . Постройте графики и , причём производную вычислите без использования равенства .
MXNet: Обсуждение
PyTorch: Обсуждение
TensorFlow: Обсуждение
PaddlePaddle: Обсуждение