Введение в градиенты и автоматическое различение

Посмотреть на TensorFlow.org Запустить в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

Автоматическое дифференцирование и градиенты

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

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

Настраивать

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

Вычисление градиентов

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

Градиентные ленты

TensorFlow предоставляет API tf.GradientTape для автоматической дифференциации; то есть вычисление градиента вычисления относительно некоторых входных данных, обычно tf.Variable s. TensorFlow «записывает» соответствующие операции, выполняемые внутри контекста tf.GradientTape , на «ленту». Затем TensorFlow использует эту ленту для вычисления градиентов «записанного» вычисления с использованием дифференцирования в обратном режиме .

Вот простой пример:

x = tf.Variable(3.0)

with tf.GradientTape() as tape:
  y = x**2

После того, как вы записали некоторые операции, используйте GradientTape.gradient(target, sources) для вычисления градиента некоторой цели (часто потери) относительно некоторого источника (часто переменных модели):

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
dy_dx.numpy()
6.0

В приведенном выше примере используются скаляры, но tf.GradientTape так же легко работает с любым тензором:

w = tf.Variable(tf.random.normal((3, 2)), name='w')
b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')
x = [[1., 2., 3.]]

with tf.GradientTape(persistent=True) as tape:
  y = x @ w + b
  loss = tf.reduce_mean(y**2)

Чтобы получить градиент loss по обеим переменным, вы можете передать их в качестве источников методу gradient . Лента гибка в отношении того, как передаются источники, и будет принимать любую вложенную комбинацию списков или словарей и возвращать градиент, структурированный таким же образом (см. tf.nest ).

[dl_dw, dl_db] = tape.gradient(loss, [w, b])

Градиент относительно каждого источника имеет форму источника:

print(w.shape)
print(dl_dw.shape)
(3, 2)
(3, 2)

Вот снова расчет градиента, на этот раз с передачей словаря переменных:

my_vars = {
    'w': w,
    'b': b
}

grad = tape.gradient(loss, my_vars)
grad['b']
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-1.6920902, -3.2363236], dtype=float32)>

Градиенты относительно модели

Обычно tf.Variables собирают в tf.Module или один из его подклассов ( layers.Layer , keras.Model ) для создания контрольных точек и экспорта .

В большинстве случаев вам потребуется рассчитать градиенты по отношению к обучаемым переменным модели. Поскольку все подклассы tf.Module агрегируют свои переменные в свойстве Module.trainable_variables , вы можете вычислить эти градиенты в несколько строк кода:

layer = tf.keras.layers.Dense(2, activation='relu')
x = tf.constant([[1., 2., 3.]])

with tf.GradientTape() as tape:
  # Forward pass
  y = layer(x)
  loss = tf.reduce_mean(y**2)

# Calculate gradients with respect to every trainable variable
grad = tape.gradient(loss, layer.trainable_variables)
for var, g in zip(layer.trainable_variables, grad):
  print(f'{var.name}, shape: {g.shape}')
dense/kernel:0, shape: (3, 2)
dense/bias:0, shape: (2,)

Контроль того, что смотрит лента

Поведение по умолчанию — записывать все операции после доступа к обучаемому tf.Variable . Причинами этого являются:

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

Например, следующее не может рассчитать градиент, потому что tf.Tensor по умолчанию не «отслеживается», а tf.Variable не обучаем:

# A trainable variable
x0 = tf.Variable(3.0, name='x0')
# Not trainable
x1 = tf.Variable(3.0, name='x1', trainable=False)
# Not a Variable: A variable + tensor returns a tensor.
x2 = tf.Variable(2.0, name='x2') + 1.0
# Not a variable
x3 = tf.constant(3.0, name='x3')

with tf.GradientTape() as tape:
  y = (x0**2) + (x1**2) + (x2**2)

grad = tape.gradient(y, [x0, x1, x2, x3])

for g in grad:
  print(g)
tf.Tensor(6.0, shape=(), dtype=float32)
None
None
None

Вы можете перечислить переменные, отслеживаемые лентой, используя метод GradientTape.watched_variables :

[var.name for var in tape.watched_variables()]
['x0:0']

tf.GradientTape предоставляет хуки, которые дают пользователю контроль над тем, что просматривается, а что нет.

Чтобы записать градиенты относительно tf.Tensor , вам нужно вызвать GradientTape.watch(x) :

x = tf.constant(3.0)
with tf.GradientTape() as tape:
  tape.watch(x)
  y = x**2

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
print(dy_dx.numpy())
6.0

И наоборот, чтобы отключить поведение по умолчанию для просмотра всех tf.Variables , установите watch_accessed_variables=False при создании ленты градиента. Этот расчет использует две переменные, но связывает градиент только для одной из переменных:

x0 = tf.Variable(0.0)
x1 = tf.Variable(10.0)

with tf.GradientTape(watch_accessed_variables=False) as tape:
  tape.watch(x1)
  y0 = tf.math.sin(x0)
  y1 = tf.nn.softplus(x1)
  y = y0 + y1
  ys = tf.reduce_sum(y)

Поскольку GradientTape.watch не вызывался для x0 , по отношению к нему не вычисляется градиент:

# dys/dx1 = exp(x1) / (1 + exp(x1)) = sigmoid(x1)
grad = tape.gradient(ys, {'x0': x0, 'x1': x1})

print('dy/dx0:', grad['x0'])
print('dy/dx1:', grad['x1'].numpy())
dy/dx0: None
dy/dx1: 0.9999546

Промежуточные результаты

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

x = tf.constant(3.0)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = x * x
  z = y * y

# Use the tape to compute the gradient of z with respect to the
# intermediate value y.
# dz_dy = 2 * y and y = x ** 2 = 9
print(tape.gradient(z, y).numpy())
18.0

По умолчанию ресурсы, удерживаемые GradientTape , освобождаются, как только вызывается метод GradientTape.gradient . Чтобы вычислить несколько градиентов по одному и тому же вычислению, создайте градиентную ленту с параметром persistent=True . Это позволяет многократно вызывать метод gradient , поскольку ресурсы высвобождаются при сборке мусора для объекта ленты. Например:

x = tf.constant([1, 3.0])
with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  y = x * x
  z = y * y

print(tape.gradient(z, x).numpy())  # [4.0, 108.0] (4 * x**3 at x = [1.0, 3.0])
print(tape.gradient(y, x).numpy())  # [2.0, 6.0] (2 * x at x = [1.0, 3.0])
[  4. 108.]
[2. 6.]
del tape   # Drop the reference to the tape

Примечания по производительности

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

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

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

Градиенты нескалярных целей

Градиент - это, по сути, операция над скаляром.

x = tf.Variable(2.0)
with tf.GradientTape(persistent=True) as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient(y0, x).numpy())
print(tape.gradient(y1, x).numpy())
4.0
-0.25

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

  • Градиент суммы целей или эквивалентно
  • Сумма градиентов каждой цели.
x = tf.Variable(2.0)
with tf.GradientTape() as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient({'y0': y0, 'y1': y1}, x).numpy())
3.75

Точно так же, если цель(и) не являются скалярными, вычисляется градиент суммы:

x = tf.Variable(2.)

with tf.GradientTape() as tape:
  y = x * [3., 4.]

print(tape.gradient(y, x).numpy())
7.0

Это упрощает получение градиента суммы набора потерь или градиента суммы поэлементного расчета потерь.

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

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

x = tf.linspace(-10.0, 10.0, 200+1)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = tf.nn.sigmoid(x)

dy_dx = tape.gradient(y, x)
plt.plot(x, y, label='y')
plt.plot(x, dy_dx, label='dy/dx')
plt.legend()
_ = plt.xlabel('x')

png

Поток управления

Поскольку градиентная лента записывает операции по мере их выполнения, поток управления Python обрабатывается естественным образом (например, операторы if и while ).

Здесь в каждой ветви if используется другая переменная. Градиент подключается только к той переменной, которая использовалась:

x = tf.constant(1.0)

v0 = tf.Variable(2.0)
v1 = tf.Variable(2.0)

with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  if x > 0.0:
    result = v0
  else:
    result = v1**2 

dv0, dv1 = tape.gradient(result, [v0, v1])

print(dv0)
print(dv1)
tf.Tensor(1.0, shape=(), dtype=float32)
None

Просто помните, что операторы управления сами по себе не дифференцируемы, поэтому они невидимы для оптимизаторов на основе градиента.

В зависимости от значения x в приведенном выше примере на ленту записывается либо result = v0 , либо result = v1**2 . Градиент по x всегда равен None .

dx = tape.gradient(result, x)

print(dx)
None

Получение градиента None

Когда цель не подключена к источнику, вы получите градиент None .

x = tf.Variable(2.)
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y * y
print(tape.gradient(z, x))
None

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

1. Заменил переменную на тензор

В разделе «управление просмотром ленты» вы видели, что лента будет автоматически отслеживать tf.Variable , но не tf.Tensor .

Одной из распространенных ошибок является непреднамеренная замена tf.Variable на tf.Tensor вместо использования Variable.assign для обновления tf.Variable . Вот пример:

x = tf.Variable(2.0)

for epoch in range(2):
  with tf.GradientTape() as tape:
    y = x+1

  print(type(x).__name__, ":", tape.gradient(y, x))
  x = x + 1   # This should be `x.assign_add(1)`
ResourceVariable : tf.Tensor(1.0, shape=(), dtype=float32)
EagerTensor : None

2. Делал расчеты вне TensorFlow

Лента не может записать путь градиента, если вычисление выходит за пределы TensorFlow. Например:

x = tf.Variable([[1.0, 2.0],
                 [3.0, 4.0]], dtype=tf.float32)

with tf.GradientTape() as tape:
  x2 = x**2

  # This step is calculated with NumPy
  y = np.mean(x2, axis=0)

  # Like most ops, reduce_mean will cast the NumPy array to a constant tensor
  # using `tf.convert_to_tensor`.
  y = tf.reduce_mean(y, axis=0)

print(tape.gradient(y, x))
None

3. Взял градиенты через целое число или строку

Целые числа и строки не дифференцируемы. Если путь расчета использует эти типы данных, градиента не будет.

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

x = tf.constant(10)

with tf.GradientTape() as g:
  g.watch(x)
  y = x * x

print(g.gradient(y, x))
WARNING:tensorflow:The dtype of the watched tensor must be floating (e.g. tf.float32), got tf.int32
WARNING:tensorflow:The dtype of the target tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
WARNING:tensorflow:The dtype of the source tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
None

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

4. Взял градиенты через объект с состоянием

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

tf.Tensor неизменяем. Вы не можете изменить тензор после его создания. У него есть значение , но нет состояния . Все рассмотренные до сих пор операции также не имеют состояния: выходные tf.matmul зависят только от его входных данных.

У tf.Variable есть внутреннее состояние — его значение. Когда вы используете переменную, считывается состояние. Вычисление градиента относительно переменной является нормальным явлением, но состояние переменной блокирует вычисление градиента от более глубокого просчета. Например:

x0 = tf.Variable(3.0)
x1 = tf.Variable(0.0)

with tf.GradientTape() as tape:
  # Update x1 = x1 + x0.
  x1.assign_add(x0)
  # The tape starts recording from x1.
  y = x1**2   # y = (x1 + x0)**2

# This doesn't work.
print(tape.gradient(y, x0))   #dy/dx0 = 2*(x1 + x0)
None

Точно так же итераторы tf.data.Dataset и tf.queue s имеют состояние и останавливают все градиенты на тензорах, которые проходят через них.

Градиент не зарегистрирован

Некоторые tf.Operation зарегистрированы как недифференцируемые и будут возвращать None . У других градиент не зарегистрирован .

Страница tf.raw_ops показывает, для каких низкоуровневых операций зарегистрированы градиенты.

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

Например, функция tf.image.adjust_contrast оборачивает raw_ops.AdjustContrastv2 , который может иметь градиент, но градиент не реализован:

image = tf.Variable([[[0.5, 0.0, 0.0]]])
delta = tf.Variable(0.1)

with tf.GradientTape() as tape:
  new_image = tf.image.adjust_contrast(image, delta)

try:
  print(tape.gradient(new_image, [image, delta]))
  assert False   # This should not happen.
except LookupError as e:
  print(f'{type(e).__name__}: {e}')
LookupError: gradient registry has no entry for: AdjustContrastv2

Если вам нужно дифференцировать эту операцию, вам нужно либо реализовать градиент и зарегистрировать его (используя tf.RegisterGradient ), либо повторно реализовать функцию, используя другие операции.

Нули вместо None

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

x = tf.Variable([2., 2.])
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y**2
print(tape.gradient(z, x, unconnected_gradients=tf.UnconnectedGradients.ZERO))
tf.Tensor([0. 0.], shape=(2,), dtype=float32)