深度学习Pytorch中的自动微分autograd

贴一个讲的十分详细的关于Pytorch autograd,backward的文章

1. 自动微分

深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。实际中,根据我们设计的模型,系统会构建一个计算图(computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。

一个简单的例子

作为一个演示例子,(假设我们想对函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2xx关于列向量 x \mathbf{x} x求导)。
首先,我们创建变量x并为其分配一个初始值。

import torch

x = torch.arange(4.0)
x
tensor([0., 1., 2., 3.])

[在我们计算 y y y关于 x \mathbf{x} x的梯度之前,我们需要一个地方来存储梯度。]

重要的是,我们不会在每次对一个参数求导时都分配新的内存。

因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。
注意,一个标量函数关于向量 x \mathbf{x} x的梯度是向量,并且与 x \mathbf{x} x具有相同的形状。

x.requires_grad_(True)  # 等价于x=torch.arange(4.0,requires_grad=True)
x.grad  # 默认值是None

(现在让我们计算 y y y)

y = 2 * torch.dot(x, x)
y
tensor(28., grad_fn=<MulBackward0>)
#输出有一个是grad_fn(grad function),因为pytorch是隐式地构造了计算图,所以这里有一个求梯度的函数存在,表明y是通过x计算过来的

x是一个长度为4的向量,计算xx的点积,得到了我们赋值给y的标量输出。
接下来,我们[通过调用反向传播函数来自动计算y关于x每个分量的梯度],并打印这些梯度。

y.backward()#调用反向传播函数来自动计算y关于x每个分量的梯度
x.grad
tensor([ 0.,  4.,  8., 12.])

函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2xx关于 x \mathbf{x} x的梯度应为 4 x 4\mathbf{x} 4x
让我们快速验证这个梯度是否计算正确。

x.grad == 4 * x
tensor([True, True, True, True])

[现在让我们计算x的另一个函数。]

# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
#如果不清除的话,输出的x的grad就是1,5,9,13
x.grad.zero_()
y = x.sum()
y = x.sum()
y.backward()
x.grad
tensor([1., 1., 1., 1.])

Tips:在pytorch中,对一个tensor进行操作时我们应该要知道我们是
(1)直接修改这个tensor;
(2)返回一个新的tensor,而旧的tensor并不修改。

pytorch中对tensor的操作的函数后加上了下划线 ‘_’ ,则表明这是一个in-place类型,也就是(1)这种情况。
具体例子见:碎片篇——pytorch函数名有下划线和没有下划线区别


非标量变量的反向传播

y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。
对于高阶和高维的yx,求导的结果可以是一个高阶张量。

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括[深度学习中]),
但当我们调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。
这里(,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。)

# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 在我们的例子中,我们只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
#此时y.sum()是个标量
y.sum().backward()
x.grad
tensor([0., 2., 4., 6.])

分离计算

有时,我们希望[将某些计算移动到记录的计算图之外]。
例如,假设y是作为x的函数计算的,而z则是作为yx的函数计算的。想象一下,我们想计算z关于x的梯度,但由于某种原因,我们希望将y视为一个常数,并且只考虑到xy被计算后发挥的作用。

在这里,我们可以分离y来返回一个新变量u,该变量与y具有相同的值,但丢弃计算图中如何计算y的任何信息。换句话说,梯度不会向后流经ux。因此,下面的反向传播函数计算z=u*x关于x的偏导数,同时将u作为常数处理,而不是z=x*x*x关于x的偏导数。

x.grad.zero_()
y = x * x
#Returns a new Tensor, detached from the current graph.
u = y.detach()#将y当作一个常数,而不是关于x的函数
z = u * x

z.sum().backward()
x.grad == u
tensor([True, True, True, True])

由于记录了y的计算结果,我们可以随后在y上调用反向传播,
得到y=x*x关于的x的导数,即2*x

x.grad.zero_()
y.sum().backward()
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 = 100 * b
    return c

让我们计算梯度。

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

我们现在可以分析上面定义的f函数。
请注意,它在其输入a中是分段线性的。
换言之,对于任何a,存在某个常量标量k,使得f(a)=k*a,其中k的值取决于输入a
因此,我们可以用d/a验证梯度是否正确。

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

更深层次理解

import torch
from torchviz import make_dot
x = torch.arange(4.0, requires_grad=True)
x_1 = x.reshape(4,1)
x_1.retain_grad()
x_1_T = x_1.T
x_1_T.retain_grad()#对非叶节点(即中间节点张量)张量启用用于保存梯度的属性(.grad).
y = torch.mm(x_1_T, x_1)
y.sum().backward()
print(y.sum())
x,x.grad,x_1_T.grad,x_1.grad
tensor(14., grad_fn=<SumBackward0>)





(tensor([0., 1., 2., 3.], requires_grad=True),
 tensor([0., 2., 4., 6.]),
 tensor([[0., 1., 2., 3.]]),
 tensor([[0.],
         [2.],
         [4.],
         [6.]]))
make_dot(y.sum())

在这里插入图片描述

为什么y.sum()对x和x_1_T求导的结果不同?

  • 对x求导就是整个的 x 1 2 + x 2 2 + x 3 2 + x 4 2 x_1^2+x_2^2+x_3^2+x_4^2 x12+x22+x32+x42 [ x 1 , x 2 , x 3 , x 4 ] [x_1,x_2,x_3,x_4] [x1,x2,x3,x4]求导,其结果就是 2 [ x 1 , x 2 , x 3 , x 4 ] 2[x_1,x_2,x_3,x_4] 2[x1,x2,x3,x4],所以结果为[0,2,4,6]
  • 对x_1_T求导,因为从上图可以看出,x_1_T和x_1都一样是根据x新创建的tensor,本来直接求x_1_T的导数是会报一个userwarning的:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QROfgdoc-1662460693204)(attachment:image.png)]
    因为正在访问非叶张量的梯度,也就是说 x_1_T和x_1不是自动求导过程中梯度最后汇总的叶张量,而是于叶张量的一个操作后的结果。但我们在代码中添加了x_1_T.retain_grad(),这使得我们可以对非叶节点(即中间节点张量)张量启用用于保存梯度的属性(.grad)。所以y.sum()对x_1_T求导实际上就是一个标量对x_1_T(4 × \times × 1的向量)求导,而 y . s u m ( ) = x _ 1 _ T × x _ 1 y.sum() = x \_1 \_T \times x \_1 y.sum()=x_1_T×x_1,所以求导的结果就是x_1,即[0., 1., 2., 3.];
  • 同理,如果要求x_1的导数,只需在上面的代码中添加x_1.retain_grad()即可,其结果为:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eXKZDG9m-1662460693204)(attachment:image-2.png)]
    因为从上图中可以看出x_1相对于x_1_T,其还有一步是来自于ReshapeAliasBackward,所以结果是2 *x_1_T (需要再考虑

小结

  • 深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上。然后我们记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。

2. 补充问题

1. 为什么Pytorch会默认累积梯度?

反向传播的时候需要把内存结果存起来,因此耗费内存比较多

PyTorch对内存的管理不太好,如果对于一个很大的批量,如果无法一次计算全计算出来,则可以将其划分成多 次计算,然后进行累加,从而得到正确的结果

当weight在不同的模型之间进行共享时也是有好处的(CNN)

2. 梯度需要正向和反向都要算一遍吗?

在神经网络中求梯度的时候,需要正着算一遍,然后再反着算一遍

自动求导的时候只需要反着算一遍,不需要正着再算一遍

3. 为什么获取.grad前需要backward?

不去backward的话就不会去计算梯度,这件事情占用很多的内存,所以需要手动backward计算梯度

4. 为什么深度学习中一般对标量求导而不是对矩阵或者向量,如果我的loss是包含向量或者矩阵,那求导之前是不是要把它们变成标量?

因为loss通常是一个标量

如果loss是一个向量的话,随着神经网络深度的增加,它会变成一个很大的张量,无法进行计算

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
PyTorch是一个基于Python的科学计算库,它是一个用于深度学习的开源框架,具有高度灵活性和可扩展性。PyTorch的一个关键特性是它的自动微分功能,也称为自动求导功能。自动微分PyTorch非常强大的功能,它可以自动计算函数的导数,因此可以为深度学习的反向传播算法提供支持。 在PyTorch,我们可以使用torch.autograd包实现自动微分。该包提供了Variable类,它是一个包装Tensor的类,它不仅保存了Tensor的值,还保存了梯度信息。我们可以在Variable上执行操作,并使用.backward()方法计算梯度。 下面是PyTorch自动微分的基本示例代码: ```python import torch x = torch.tensor([3.0], requires_grad=True) y = x ** 2 + 2 * x + 1 # 计算梯度 y.backward() # 输出梯度 print(x.grad) ``` 在这个例子,我们定义了一个变量x,并将requires_grad设置为True,以指示PyTorch需要计算x的梯度。然后我们定义了一个函数y,该函数对x进行操作。我们使用backward()方法计算y相对于x的梯度,并使用x.grad输出梯度。 这里需要注意的是,只有requires_grad=True的变量才会被计算梯度。如果我们想要计算多个变量的梯度,可以将它们放在一个元组,然后调用backward()方法。 ```python import torch x = torch.tensor([3.0], requires_grad=True) y = torch.tensor([4.0], requires_grad=True) z = x ** 2 + y ** 2 # 计算梯度 z.backward() # 输出梯度 print(x.grad) print(y.grad) ``` 在这个例子,我们定义了两个变量x和y,并将requires_grad设置为True,以指示PyTorch需要计算它们的梯度。然后我们定义了一个函数z,该函数对x和y进行操作。我们使用backward()方法计算z相对于x和y的梯度,并使用x.grad和y.grad输出它们的梯度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值