自动微分 :label: 正如 :numref: 中所说,求导是几乎所有深度学习优化算法的关键步骤。 虽然求导的计算很简单,只需要一些基本的微积分。 但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。 深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。 实际中,根据设计好的模型,系统会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
🏷sec_autograd
正如 :numref:sec_calculus中所说,求导是几乎所有深度学习优化算法的关键步骤。
虽然求导的计算很简单,只需要一些基本的微积分。
但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。
深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。
实际中,根据设计好的模型,系统会构建一个计算图(computational graph),
来跟踪计算是哪些数据通过哪些操作组合起来产生输出。
自动微分使系统能够随后反向传播梯度。
这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
作为一个演示例子,(假设我们想对函数y=2\mathbf{x}^{\top}\mathbf{x}关于列向量\mathbf{x}求导)。
首先,我们创建变量x并为其分配一个初始值。
from mxnet import autograd, np, npx npx.set_np() x = np.arange(4.0) x
#@tab pytorch import torch x = torch.arange(4.0) x
#@tab tensorflow import tensorflow as tf x = tf.range(4, dtype=tf.float32) x
#@tab paddle import warnings warnings.filterwarnings(action='ignore') import paddle x = paddle.arange(4, dtype='float32') x
[在我们计算y关于\mathbf{x}的梯度之前,需要一个地方来存储梯度。]
重要的是,我们不会在每次对一个参数求导时都分配新的内存。
因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。
注意,一个标量函数关于向量\mathbf{x}的梯度是向量,并且与\mathbf{x}具有相同的形状。
# 通过调用attach_grad来为一个张量的梯度分配内存 x.attach_grad() # 在计算关于x的梯度后,将能够通过'grad'属性访问它,它的值被初始化为0 x.grad
#@tab pytorch x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True) x.grad # 默认值是None
#@tab tensorflow x = tf.Variable(x)
#@tab paddle x = paddle.to_tensor(x, stop_gradient=False) x.grad # 默认值是None
(现在计算y。)
# 把代码放到autograd.record内,以建立计算图 with autograd.record(): y = 2 * np.dot(x, x) y
#@tab pytorch y = 2 * torch.dot(x, x) y
#@tab tensorflow # 把所有计算记录在磁带上 with tf.GradientTape() as t: y = 2 * tf.tensordot(x, x, axes=1) y
#@tab paddle y = 2 * paddle.dot(x, x) y
x是一个长度为4的向量,计算x和x的点积,得到了我们赋值给y的标量输出。
接下来,[通过调用反向传播函数来自动计算y关于x每个分量的梯度],并打印这些梯度。
y.backward() x.grad
#@tab pytorch y.backward() x.grad
#@tab tensorflow x_grad = t.gradient(y, x) x_grad
#@tab paddle y.backward() x.grad
函数y=2\mathbf{x}^{\top}\mathbf{x}关于\mathbf{x}的梯度应为4\mathbf{x}。
让我们快速验证这个梯度是否计算正确。
x.grad == 4 * x
#@tab pytorch x.grad == 4 * x
#@tab tensorflow x_grad == 4 * x
#@tab paddle x.grad == 4 * x
[现在计算x的另一个函数。]
with autograd.record(): y = x.sum() y.backward() x.grad # 被新计算的梯度覆盖
#@tab pytorch # 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值 x.grad.zero_() y = x.sum() y.backward() x.grad
#@tab tensorflow with tf.GradientTape() as t: y = tf.reduce_sum(x) t.gradient(y, x) # 被新计算的梯度覆盖
#@tab paddle # 在默认情况下,PaddlePaddle会累积梯度,我们需要清除之前的值 x.clear_gradient() y = paddle.sum(x) y.backward() x.grad
当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。
对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括[深度学习中]),
但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。
这里(,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。)
# 当对向量值变量y(关于x的函数)调用backward时,将通过对y中的元素求和来创建 # 一个新的标量变量。然后计算这个标量变量相对于x的梯度 with autograd.record(): y = x * x # y是一个向量 y.backward() x.grad # 等价于y=sum(x*x)
#@tab pytorch # 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。 # 本例只想求偏导数的和,所以传递一个1的梯度是合适的 x.grad.zero_() y = x * x # 等价于y.backward(torch.ones(len(x))) y.sum().backward() x.grad
#@tab tensorflow with tf.GradientTape() as t: y = x * x t.gradient(y, x) # 等价于y=tf.reduce_sum(x*x)
#@tab paddle x.clear_gradient() y = x * x paddle.sum(y).backward() x.grad
有时,我们希望[将某些计算移动到记录的计算图之外]。
例如,假设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的偏导数。
with autograd.record(): y = x * x u = y.detach() z = u * x z.backward() x.grad == u
#@tab pytorch x.grad.zero_() y = x * x u = y.detach() z = u * x z.sum().backward() x.grad == u
#@tab 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
#@tab paddle 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。
y.backward() x.grad == 2 * x
#@tab pytorch x.grad.zero_() y.sum().backward() x.grad == 2 * x
#@tab tensorflow t.gradient(y, x) == 2 * x
#@tab paddle x.clear_gradient() paddle.sum(y).backward() x.grad == 2 * x
使用自动微分的一个好处是:
[即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度]。
在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。
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
#@tab 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
#@tab 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
#@tab paddle 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
让我们计算梯度。
a = np.random.normal() a.attach_grad() with autograd.record(): d = f(a) d.backward()
#@tab pytorch a = torch.randn(size=(), requires_grad=True) d = f(a) d.backward()
#@tab tensorflow a = tf.Variable(tf.random.normal(shape=())) with tf.GradientTape() as t: d = f(a) d_grad = t.gradient(d, a) d_grad
#@tab paddle 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验证梯度是否正确。
a.grad == d / a
#@tab pytorch a.grad == d / a
#@tab tensorflow d_grad == d / a
#@tab paddle a.grad == d / a
d关于a的导数,如果将变量a更改为随机向量或矩阵,会发生什么?:begin_tab:mxnet
Discussions
:end_tab:
:begin_tab:pytorch
Discussions
:end_tab:
:begin_tab:tensorflow
Discussions
:end_tab:
:begin_tab:paddle
Discussions
:end_tab: