【d2l】2.5.自动微分
【d2l】2.5.自动微分
这一节看似只讲了torch的反向传播接口,但真正想理解还是得有一个计算图的概念
简单的例子
现在有一个向量\(x\),我需要计算\(y = 2x^\top x\)关于\(x\)求导,实现如下
import torch
x = torch.arange(4., requires_grad = True)
x, x.grad
y = 2 * torch.dot(x, x)
y
y.backward()
x.grad
结果如下
(tensor([0., 1., 2., 3.], requires_grad=True), None)
tensor(28., grad_fn=<MulBackward0>)
tensor([ 0., 4., 8., 12.])
结果的梯度应当是\(4x\),验证一下
x.grad == 4 * x
tensor([True, True, True, True])
结果看着简单,但其实要有一点意识,中间的计算图发生了什么
首先\(y\)是这样子算出来的:
x ──► dot(x, x) ──► *2 ──► y
反向传播是通过链式法则对于这些原子操作求梯度
由于整个计算图的起点是\(x\),并且梯度是作用在\(x\)上面的,因而最终是x.grad
下面给一个稍微复杂的例子
z = torch.dot(x, torch.exp(x))
z
结果如下
tensor(77.7530, grad_fn=<DotBackward0>)
计算梯度
x.grad.zero_() # 重新计算梯度需要把原先的计算图清零
z.backward()
x.grad
tensor([ 1.0000, 5.4366, 22.1672, 80.3421])
这个结果是
这是个哈达玛积而非点乘,我一开始写成了点乘没有意识到问题在哪
- 首先这个结果是个向量而非标量,因而从代数对象来看肯定是有问题的
- 另外偏导的过程中,只有对应项有贡献,其他都是常数,因此不会参与求导,即
所以验证的逻辑表达式应当为
# 错误写法 x.grad == torch.dot(x + 1, torch.exp(x))
x.grad == (x + 1) * torch.exp(x)
tensor([True, True, True, True])
接下来是一个衔接的函数,.sum()
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
tensor([1., 1., 1., 1.])
非标量变量的反向传播
前面得到的结果都是标量,反向传播的过程很自然
如果结果是一个向量,想要反向传播需要先压扁
x.grad.zero_()
y = x * x
y.sum().backward()
x.grad
这里没有直接y.backward()而是先求和,就是因为标量无法直接反向传播,需要先另外处理
分离计算
接下来的问题是,在某种情况下,我们需要把某个过程量常数化,而不是沿着这个过程量进行反向传播,因而要采用一个detach()接口
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
tensor([True, True, True, True])
本来\(z = x^3\),但是由于中间变量\(y = x^2\)被常数化,让\(z = u x\),进而梯度为\(u\)
在计算图中发生了这样的事情:
x ──► y = x*x (图在这里断了)
│
└──► u = y.detach() ──► z = u*x
↑
视为常量
由于\(y\)的计算结果还是存在的,因此还是可以从\(y\)开始反向传播
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
tensor([True, True, True, True])
Python控制流的梯度计算
这个部分展示了自动微分的高度可能性,对于Python的条件、循环、任意函数调用,自动微分都可以进行
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
接下来计算梯度
a = torch.randn(size = (), requires_grad = True)
d = f(a)
d.backward()
这个函数可以知道对于任何\(a\),最终的\(f(a)\)都是关于\(a\)的线性函数
因此验证可以用
a.grad == d / a
tensor(True)
可以得知,自动微分可以从计算上让任何对象都得到对应的偏导,哪怕可能是不可导的,这在计算上提供了巨大的可能性
小练习
题目是对于\(f(x) = \sin x\)求导并画出来,需要采用自动微分
\(f(x)\)之后的结果显然是一个向量,求和即可
import sys
sys.path.append("..")
import torch
import math
from d2l_local import torch as d2l
x = torch.linspace(-2 * math.pi, 2 * math.pi, 400, requires_grad = True)
y = torch.sin(x)
# 由于y此时是个向量,而且每个x视作等效,直接求和即可,本处采用另一种写法
y.backward(torch.ones_like(y))
dy_dx = x.grad
# 此处的detach()仅仅是为了表示不需要考虑梯度的问题,事实上是可以去掉的
d2l.plot(
x.detach(),
[y.detach(), dy_dx.detach()],
legend = ['f(x) = sin(x)', 'df(x)/dx (autograd)'],
xlabel = 'x',
ylabel = 'value'
)
.png)

浙公网安备 33010602011771号