自动微分
正如微积分中所说,求导是几乎所有深度学习优化算法的关键步骤。 虽然求导的计算很简单,只需要一些基本的微积分。 但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。
度学习框架通过自动计算导数,即 自动微分(automatic differentiation) 来加快求导。 实际中,根据我们设计的模型,系统会构建一个 计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(backpropagate) 意味着跟踪整个计算图,填充关于每个参数的偏导数。
重要理解 requires_grad 梯度参数、backward() 反向传播函数、detach() 分离函数。
一个简单的例子
作为一个演示例子,假设我们想对函数 y = 2 x T x y = 2x^{T}x y=2xTx 关于列向量 x ⃗ \vec{x} x 求导。 首先,我们创建变量x并为其分配一个初始值。
import torch
x = torch.arange(4.0) #生成一个一维张量
x
tensor([0., 1., 2., 3.])
在我们计算 y y y 关于的梯度 x x x 之前,我们需要一个地方(grad)来存储梯度。 重要的是,我们不会在每次对一个参数求导时都分配新的内存。 因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。 注意,一个标量函数关于向量 x x x 的梯度是向量,并且与 x x x 具有相同的形状。
x.requires_grad_(True) #为张量x设置梯度,用来存储计算得出的x梯度向量,等同于torch.arange(4.0, requires_grad=True)
print(x.grad) #输出x默认的梯度值, 为None
None
现在让我们计算 y y y 。
y = 2 * torch.dot(x.T, x) #计算y = 2 * (x.T与x的内积),结果为一个标量
y
tensor(28., grad_fn=<MulBackward0>)
x是一个长度为4的向量,计算x和x的点积,得到了我们赋值给y的标量输出。 接下来,我们通过调用**反向传播函数(backward())**来自动计算y关于x每个分量的梯度,并打印这些梯度。
这里我觉得大家需要明白一个概念。梯度,也就是标量对向量内的每个元素的偏导数组成的向量。
y.backward() #调用反向传播函数,计算之前组成y结果的x向量梯度
x.grad #输出x向量梯度
tensor([ 0., 4., 8., 12.])
它的计算过程如下所示:
其中,函数 y = 2 x T x y = 2x^{T}x y=2xTx 关于 x x x 的梯度应为 4 x 4x 4x (计算原理为二次型对向量的求导)。 如下图(详情请阅读矩阵求导知识):
其中
y = 2 x T x = 2 x T E x = 2 ( E T x + E x ) = 4 x y = 2x^{T}x = 2x^{T}Ex = 2(E^{T}x + Ex) = 4x y=2xTx=2xTEx=2(ETx+Ex)=4x
让我们快速验证这个梯度是否计算正确。
x.grad == 4*x #验证反向传播函数的偏导数计算是否正确
tensor([True, True, True, True])
现在让我们计算x的另一个函数。
#默认情况下,pytorch会累计梯度,我们需要清楚之前的值
x.grad.zero_()
y = x.sum() #此时的y = x_0 + x_1 + x_2 + x_3
y.backward() #反向传播求x向量内分量的偏导数,即x关于y的梯度
x.grad #输出x的梯度
tensor([1., 1., 1., 1.])
非标量变量的反向传播
当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。 对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中), 但当我们调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 在我们的例子中,我们只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward() #y.sum() = x_o的平方 + x_1的平方 + x_2的平方 + x_3的平方
x.grad #其对x的偏导数分别为2x_0,2x_1,2x_2,2x_3, 梯度为(2x_0,2x_1,2x_2,2x_3)
tensor([0., 2., 4., 6.])
分离计算
有时,我们希望将某些计算移动到记录的计算图之外。 例如,假设y是作为x的函数计算的,而z则是作为y和x的函数计算的。 想象一下,我们想计算z关于x的梯度,但由于某种原因,我们希望将y视为一个常数, 并且只考虑到x在y被计算后发挥的作用。
在这里,我们可以分离y来返回一个新变量u,该变量与y具有相同的值, 但丢弃计算图中如何计算y的任何信息。 换句话说,梯度不会向后流经u到x。 因此,下面的反向传播函数计算z=ux关于x的偏导数,同时将u作为常数处理, 而不是z=xx*x关于x的偏导数。
#将x之前的梯度进行清空
x.grad.zero_()
y = x * x #y是关于自变量x的函数
u = y.detach() #detach()函数,即分离。只是将y的值赋值给u,即u仅仅作为普通常数,而不会把y是如何得来的计算图赋值给u
u.requires_grad_(True)
z = u * x #表面上还是z = y * x,但此时的u和x未有任何联系,可以无顾虑地进行偏导数求解
#z.sum() = u * x = u0x0 + u1x1 + u2x2 + u3x3, 对x的偏导数为(u0,u1,u2,u3), 对u的偏导数为(x0,x1,x2,x3)
#故x关于z的梯度为(0,1,4,9), u关于z的梯度为(0,1,2,3)
z.sum().backward() #进行反向传播求出偏导数
x.grad, u.grad, x.grad == u #分别输出x与u的梯度,并判断x的梯度是否等于y的值
(tensor([0., 1., 4., 9.]),
tensor([0., 1., 2., 3.]),
tensor([True, True, True, True]))
由于记录了 y y y 的计算结果,我们可以随后在 y y y 上调用反向传播, 得到 y = x ∗ x y=x*x y=x∗x 关于的 x x x 的导数,即 2 x 2x 2x 。
x.grad.zero_() #将x的梯度清0
y.sum().backward() #其中, y.sum() = x0的平方 + x1的平方 + x2的平方 + x3的平方。计算出x的梯度为2(x0,x1,x2,x3)
x.grad == 2 * x #判断梯度是否正确
tensor([True, True, True, True])
Python控制流的梯度计算
使用自动微分的一个好处是: 即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。 在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = b * 100
return c
让我们计算梯度。
a = torch.randn(size=(),requires_grad=True) #随机生成一个符合正态分布的小数
d = f(a) #调用函数f,计算关于a的函数
d.backward() #调用反向传播函数
我们现在可以分析上面定义的 f f f 函数。 请注意,它在其输入 a a a 中是分段线性的。 换言之,对于任何 a a a ,存在某个常量标量k,使得 f ( a ) = k a f(a)=ka f(a)=ka ,其中 k k k 的值取决于输入 a a a 。 因此,我们可以用 d / a d/a d/a 验证梯度是否正确。
a.grad, a.grad == d / a #计算a的梯度
(tensor(1024.), tensor(True))
小结
深度学习框架可以自动计算导数:
我们首先将梯度附加到想要对其计算偏导数的变量上,然后我们记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。