此页面由 Cloud Translation API 翻译。
Switch to English

渐变和自动微分简介

在TensorFlow.org上查看 在Google Colab中运行 在GitHub上查看源代码 下载笔记本

自动微分和梯度

自动微分对于实现机器学习算法(例如用于训练神经网络的反向传播 )非常有用。

在本指南中,我们将讨论使用TensorFlow计算梯度的方法,尤其是在急切执行中。

建立

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

计算梯度

为了自动区分,TensorFlow需要记住在正向传递期间哪些操作以什么顺序发生。然后,在向后传递期间,TensorFlow以相反的顺序遍历此操作列表以计算梯度。

渐变胶带

TensorFlow提供了tf.GradientTape API以进行自动区分;也就是说,计算相对于某些输入(通常为tf.Variable s)的计算的梯度。 TensorFlow将在tf.GradientTape上下文内执行的相关操作“记录”到“ tape”上。然后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)

要获得两个变量的y梯度,可以将这两个变量都作为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': tf.Variable(tf.random.normal((3, 2)), name='w'),
    'b': tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')
}

grad = tape.gradient(loss, my_vars)
grad['b']

关于模型的渐变

通常将tf.Variables收集到tf.Module或其子类之一( layers.Layerkeras.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)

由于未在x0调用GradientTape.watch因此不会对其进行任何梯度计算:

# dy = 2x * dx
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_dx = 2 * y, where y = x ** 2
print(tape.gradient(z, y).numpy())
18.0

默认情况下,只要调用GradientTape.gradient()方法,就会释放GradientTape拥有的资源。要在同一计算上计算多个梯度,请创建一个persistent梯度带。当磁带对象被垃圾回收时,释放资源时,这允许多次调用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())  # 108.0 (4 * x**3 at x = 3)
print(tape.gradient(y, x).numpy())  # 6.0 (2 * x)
[  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

这使得简单地采用损失集合之和的梯度或按元素进行损失计算之和的梯度。

如果每个项目都需要单独的渐变,请参阅Jacobians

在某些情况下,您可以跳过雅可比行列式。对于逐元素计算,总和的梯度给出了每个元素相对于其输入元素的导数,因为每个元素都是独立的:

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控制流(例如,使用ifwhile )。

这里,在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 = v0result = 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.Variabletf.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.通过整数或字符串进行渐变

整数和字符串不可区分。如果计算路径使用这些数据类型,则不会出现梯度。

没有人期望字符串是可区分的,但是如果不指定dtype ,很容易意外地创建一个int常量或变量。

# The x0 variable has an `int` dtype.
x = tf.Variable([[2, 2],
                 [2, 2]])

with tf.GradientTape() as tape:
  # The path to x1 is blocked by the `int` dtype here.
  y = tf.cast(x, tf.float32)
  y = tf.reduce_sum(x)

print(tape.gradient(y, x))
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 + x2)
None

类似地, tf.data.Dataset迭代器和tf.queue是有状态的,并将停止通过它们的张量上的所有梯度。

没有注册渐变

一些tf.Operation注册为不可微分的,并且将返回None 。其他人没有注册梯度

tf.raw_ops页面显示哪些低级操作已注册了渐变。

如果您尝试通过未注册渐变的float操作进行渐变,则磁带将抛出错误,而不是None提示地返回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 ),或使用其他操作重新实现该功能。

零而不是无

在某些情况下,对于未连接的渐变,获得0而不是None将很方便。您可以使用unconnected_gradients参数确定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)