Посмотреть на 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')
Поток управления
Поскольку градиентная лента записывает операции по мере их выполнения, поток управления 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)