笔记:自动求导【动手学深度学习v2】

沐神 视频链接

自动求导

自动求导是指计算一个函数在指定值上的导数

注意是求导数的具体值,而不是求求导公式

计算图

计算图

就是一个DAG
分为隐式构造和显式构造:
显式是指用公式定义的计算图(静态),隐式是指用python控制流定义的计算图(动态)

自动求导的方式

有两种计算导数的方法,因为链式求导只是将导数乘起来,顺序可以自定义,下面将的都是采用反向计算导数

反向计算导数的过程

首先正向计算值,并把中间结果存在对应节点
然后反向传递计算导数的值,可能需要用到节点的值,例如下面的 \(\frac{\partial z}{\partial b}\)\(\frac{\partial z}{\partial \mathbf{w}}\)

其中,反向传递时可以不计算没用到的分支

总结

正向(forward)和反向(backword)的时间复杂度差不多,但是正向需要记录所有的中间值,空间复杂度会高一些,
存起来,这也是深度神经网络特别耗GPU内存的祸源

与正向求导相比,反向求导可以一次性计算完梯度,而正向求导每次求梯度都需要将后面的图扫一遍

复杂度

Hands on try

反向传播才会计算梯度

点击查看代码
import torch

x = torch.arange(4.0)
print(x)
x.requires_grad_(True)
x.grad # 默认值为None,存梯度的地方

y =  2 * torch.dot(x, x)   # y = 2x^2, y.grad = 4x

y.backward()   # 通过调用反向传播来自动计算y对x每个元素的梯度
print(x.grad)

print(x.grad == 4 * x)

Out:

tensor([0., 1., 2., 3.])
tensor([ 0.,  4.,  8., 12.])
tensor([True, True, True, True])

如果将 y 定义为 x.sum,梯度会变成多少呢

点击查看代码
x.grad.zero_() # 清空梯度
y = x.sum()   # y = x1 + x2 + x3 + x4
y.backward()
print(x.grad)  # 所以对每个分量的梯度都是1

Out:

tensor([1., 1., 1., 1.])

如果将 x.grad.zero_() 注释掉呢?会发现梯度累加了

点击查看代码

Out:

tensor([ 1.,  5.,  9., 13.])

如果是对非标量求导呢?
在深度学习中,我们的目的不是计算微分矩阵,而是批量中每个样本单独计算的偏导数之和
深度学习中很少到非标量求和,对非标量求导需要加 gradient 参数
如果出现这种情况,通常会对非标量求一个 sum

点击查看代码
# 对非标量直接调用backward()会报错
# 需要给backward()传入一个gradient参数,参数的形状和x一样,可以是标量
# 或者用sum()将非标量转换为标量
x.grad.zero_()
y = x * x
y.sum().backward() # 等价于 y.backward(torch.ones_like(y))
print(x.grad)  # 所以对每个分量的梯度都是2

Out:

tensor([0., 2., 4., 6.])

使用 detach 将某些操作移到计算图外,被 detach 的向量会变成一个标量,不再有梯度了
这在固定部分参数的时候很有用

点击查看代码
x.grad.zero_()
y = x * x
u = y.detach()  # u是一个标量,不会被求导
z = u * x
z.sum().backward()
print(x.grad == u)

x.grad.zero_()
y.sum().backward()   # y还是x的导数,但u不是了
print(x.grad == 2 * x)  

Out:

tensor([True, True, True, True])

即使计算图是通过python控制流构建的,我们仍然可以计算变量的梯度

这是一个动态图,图的结构与输入有关

因为在正向执行图的时候,Pytorch会将计算图存下来,因此反向的时候知道怎么算

点击查看代码
def f(a):
    b = a * 2
    while b.norm() < 1:
        b = b * 2
    if b.sum() > 1:
        c = b / b.sum()
    else:
        c = b
    return c

a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
print(a.grad)

在运行反向传播函数之后,立即再次运行它,看看会发生什么?
会出错
因为pytorch默认backward()后回将节点的值free(清空),前面我们知道计算梯度需要用到节点的值

点击查看代码
d.backward()
d.backward()

Out:

RuntimeError: Trying to backward through the graph a second time (or directly access saved variables after they have already been freed). Saved intermediate values of the graph are freed when you call .backward() or autograd.grad(). Specify retain_graph=True if you need to backward through the graph a second time or if you need to access saved variables after calling backward.
posted @ 2021-11-26 00:12  Rogn  阅读(752)  评论(0编辑  收藏  举报