《动手学深度学习 Pytorch版》 2.5 自动微分

2.5.1 一个简单的例子

import torch

假设我们函数 y=2xTx 关于列向量 x 求导。

x = torch.arange(4.0)
x.requires_grad_(True)  # 自动求导机制的必要参数 此处两句等效于 x=torch.arange(4.0,requires_grad=True)
x, x.grad
(tensor([0., 1., 2., 3.], requires_grad=True), None)
y = 2 * torch.dot(x, x)  # 计算y
y
tensor(28., grad_fn=<MulBackward0>)
y.backward()  # 调用反向传播函数自动计算关于 x 的每个分量的梯度
x.grad
tensor([ 0.,  4.,  8., 12.])
x.grad == 4 * x   # 快速验证一下上面计算的梯度是否正确
tensor([True, True, True, True])

我对此的理解是 y=2xTx 关于列向量 x=tensor([0.0, 1.0, 2.0, 3.0]) 求导,实际上就是求 y=2x2 的导函数 y=4xx=0.0x=1.0x=2.0x=3.0处的值,梯度为 y=[4x]T

不同于手工计算,先利用求导规则算出导函数 y=4x,然后再分别将上述 x 值代入。

自动求导相当于把一个自变量 x 的不同值 x=0.0x=1.0x=2.0x=3.0 作为不同的自变量 x1x2x3x4 分别代入函数中再相加形成一个多元函数,也就是 y=2xTx=2(x12+x22++xn2),再借助求偏导时可以把其他自变量当常数这一点去分别求偏导,又因为是加法,因此对当前变量求偏导时其他自变量求导为0。对于这个由加法生成的多元函数的梯度则为 y=[4x1,4x2,4xn]T,刚好是一元函数的梯度组成的向量。

因此 y 最后一般都采取加法,例如本例和2.5.2中的例子实际上时一样的(就差个系数),因为2.5.2中的例子最后也要调用 sum 函数或者与全一同行矩阵做哈达玛积再求和,使非标量向量转换为标量向量。

'''再试一下另一个函数'''
x.grad.zero_()  # 默认情况下会累积梯度,在求新梯度之前需要进行清除
y = x.sum()  # 显然它的梯度应该是一串1
y.backward()
x.grad
tensor([1., 1., 1., 1.])

2.5.2 非标量变量的反向传播

PyTorch不让张量对张量求导,只允许标量对张量求导。因此,目标量对一个非标量调用 backward() 时需要传入一个 gradient 参数(该参数指定微分函数关于self的梯度,实际上就是 y 和 gradient 做一个点积,这里gradient 参数用的是全一向量,因此用sum是一样的),以使张量对张量的求导转换为标量对张量的求导。

x.grad.zero_()
y = x * x
y.sum().backward()  # 等价于 y.backward(torch.ones(len(x)))
y, y.sum(), x.grad
(tensor([0., 1., 4., 9.], grad_fn=<MulBackward0>),
 tensor(14., grad_fn=<SumBackward0>),
 tensor([0., 2., 4., 6.]))

2.5.1已经论述了为何使用加法,在此参照此文写个例子实验一下。

此例分别计算 n1(m1,m2)=m12+3m2n2(m1,m2)=2m1+m22 在点 (m1,m2)=(2.0,3.0)(m1,m2)=(6.0,7.0)(m1,m2)=(4.0,1.0) 处的偏导数。

手工计算易得二者的梯度表达式分别是:n1=[2m1,3]Tn2=[2,2m2]T

代入数据得:

n1 在三个点的梯度分别为:(4.0,3.0)T(12.0,3.0)T(8.0,3.0)T.

n2 在三个点的梯度分别为:(2.0,6.0)T(2.0,14.0)T(2.0,2.0)T

以下是两种计算方式。

m = torch.tensor([[2., 3.], [6., 7.], [4., 1.]], requires_grad=True)  # 两个自变量的值列表
n = torch.zeros(m.shape[0], m.shape[1])  # 初始化目标张量
for i in range(m.shape[0]):
    n[i][0] = m[i][0] ** 2 + 3 * m[i][1]  # 定义映射关系1
    n[i][1] = 2 * m[i][0] + m[i][1] ** 2  # 定义映射关系2

# retain_graph=True是为了方便多次反向传播
n.backward(torch.Tensor([[1, 0], [1, 0], [1, 0]]), retain_graph=True)  # 此处相当于(n * torch.Tensor([[1, 0], [1, 0], [1, 0]])).sum().backward()
grad1 = m.grad.clone()  # 暂存函数1的求导结果
m.grad.zero_()
n.backward(torch.Tensor([[0, 1], [0, 1], [0, 1]]), retain_graph=True)  # 此处相当于(n * torch.Tensor([[0, 1], [0, 1], [0, 1]])).sum().backward()
torch.stack((grad1, m.grad))  # 扩维拼接
tensor([[[ 4.,  3.],
         [12.,  3.],
         [ 8.,  3.]],

        [[ 2.,  6.],
         [ 2., 14.],
         [ 2.,  2.]]])
m = torch.tensor([[2., 6., 4.], [3., 7., 1.]], requires_grad=True)  # 两个自变量的值列表
n = torch.zeros(m.shape[0], m.shape[1])  # 初始化目标张量
n[0] = m[0] ** 2 + 3 * m[1]  # 定义映射关系1
n[1] = 2 * m[0] + m[1] ** 2  # 定义映射关系2

# retain_graph=True是为了方便多次反向传播
n.backward(torch.Tensor([[1, 1, 1], [0, 0, 0]]), retain_graph=True)  # 此处相当于(n * torch.Tensor([[1, 1, 1], [0, 0, 0]])).sum().backward()
grad1 = m.grad.clone()  # 暂存函数1的求导结果
m.grad.zero_()
n.backward(torch.Tensor([[0, 0, 0], [1, 1, 1]]), retain_graph=True)  # 此处相当于(n * torch.Tensor([[0, 0, 0], [1, 1, 1]])).sum().backward()
torch.stack((grad1, m.grad), dim=0)  # 扩维拼接
tensor([[[ 4., 12.,  8.],
         [ 3.,  3.,  3.]],

        [[ 2.,  2.,  2.],
         [ 6., 14.,  2.]]])

2.5.3 分离计算

假设 y 是关于 x 的函数,而 z 是关于 xy 的函数,如果希望在计算 z 是关于 x 的梯度时将 y 视为一个常数且只考虑 xy 计算后发挥作用,则可以分离 y 来返回一个新变量 u,该变量与 y 具有相同的值,但是丢弃了计算图中如何计算 y 的任何信息,梯度不会向后流经 ux.

如下例,计算 z=ux 关于 x 的偏导数时将 u 作为常数处理,所以偏导数 z(u,x)x=u,而不是计算 z=xxx 关于 x 的导数 dzdx=3x2

x.grad.zero_()
y = x * x
u = y.detach()
z= u * x

z.sum().backward()
x.grad == u
tensor([True, True, True, True])
# 随后再算y也不影响
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
tensor([True, True, True, True])

2.5.4 Python控制流的梯度计算

自动微分可以兼容一些需要使用Python控制流的复杂函数。

def f(a):
    b = a * 2
    while b.norm()  < 1000:
        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, d, a.grad
(tensor(1.4756, requires_grad=True),
 tensor(1510.9863, grad_fn=<MulBackward0>),
 tensor(1024.))

虽然上述函数很复杂,但是我们至少知道它是线性的,也就是说 f(a)=ka,所以可以使用 d/a 验证梯度是否正确。

a.grad == d / a
tensor(True)

练习

(1)为什么计算二阶导数比一阶导数的开销要更大?

二阶那不得多导一遍嘛,肯定开销大。


(2)在运行反向传播函数之后,立即再次运行它,看看会发生什么。

# d.backward() # 会报 Trying to backward through the graph a second time 错,需要加 retain_graph=True

(3)在控制流的例子中,我们计算 d 关于 a 的导数,如果将变量 a 更改为随机向量或矩阵,会发生什么?

a = torch.randn(size=(1, 4), requires_grad=True)  # 换向量的话给 d 调用一下 sum() 即可
d = f(a)
d.sum().backward()
a, d, a.grad, a.grad == d / a  # 每个梯度都一样诶
(tensor([[ 0.4611, -1.3128,  0.9323,  0.0999]], requires_grad=True),
 tensor([[  472.1350, -1344.2858,   954.7211,   102.3088]],
        grad_fn=<MulBackward0>),
 tensor([[1024., 1024., 1024., 1024.]]),
 tensor([[True, True, True, True]]))
a = torch.randn(size=(3, 4), requires_grad=True)  # 换矩阵也是给 d 调用一下 sum() 即可
d = f(a)
d.sum().backward()
a, d, a.grad, a.grad == d / a  # 每个梯度都一样诶
(tensor([[-0.6832, -0.9762, -0.9894, -1.1655],
         [ 0.6858,  0.7037, -0.7619, -0.5135],
         [ 0.0172,  1.4394,  1.6180,  0.8451]], requires_grad=True),
 tensor([[-349.7996, -499.8079, -506.5559, -596.7241],
         [ 351.1539,  360.2910, -390.0962, -262.9074],
         [   8.7919,  736.9765,  828.3993,  432.6812]], grad_fn=<MulBackward0>),
 tensor([[512., 512., 512., 512.],
         [512., 512., 512., 512.],
         [512., 512., 512., 512.]]),
 tensor([[True, True, True, True],
         [True, True, True, True],
         [True, True, True, True]]))

(4)重新设计一个求控制流梯度的例子,运行并分析结果。

试一下导数不存在点求导会怎样。

def g(x):
    if x < 1:  # 可以换 <= 试试
        return x+1
    else:
        return 2*x

x = torch.tensor(1., requires_grad=True)
g1 = g(x)
g1.backward()
x.grad  # 看来是不在意什么分段不分段的
tensor(2.)
def h(x):  # y=|x|
    return abs(x)

x1 = torch.tensor(0., requires_grad=True)
x2 = torch.tensor(1., requires_grad=True)
h1 = h(x1)
h2 = h(x2)
h1.backward()
h2.backward()
x1.grad, x2.grad  # 在不可导点能求出来一个无意义值
(tensor(0.), tensor(1.))

(5)使 f(x)=sin(x),绘制 f(x)df(x)dx 的图像,其中后者不使用 f(x)=cos(x)

from d2l import torch as d2l  # 方便调用前一节那个 plot 函数
x = torch.arange(-6.5, 6.5, 0.1, requires_grad=True)
y = torch.sin(x)
y.sum().backward()

# 注意,需要先用 tensor.detach().numpy() 把 tensor 转 array 才能使用
d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.detach().numpy()], 'x', 'f(x)', legend=['f(x)', "f'(x)"])


image

posted @   AncilunKiang  阅读(363)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示