PyTorch基础(二)-----自动求导Autograd

一、前言

上一篇文章中提到PyTorch提供了两个重要的高级功能,分别是:

  • 具有强大的GPU加速的张量计算(如NumPy)
  • 包含自动求导系统的的深度神经网络

第一个特性我们会在之后的学习中用到,这里暂且按下不表,我们首先来探讨研究一下什么是自动求导Autograd、自动求导Autograd的原理是怎样的等问题。

  • 当然首先我们要保证正确导入了torch包
import torch
# 打印一下PyTorch的版本
torch.__verson__

PyTorch的Autograd模块实现了深度学习算法中的反向传播求梯度,在张量(Tensor类)上的所有操作,Autograd都能为它们自动提供微分,简化了手动计算导数的复杂过程。

在张量创建时,通过设置 requires_grad 标识为Ture来告诉Pytorch需要对该张量进行自动求导,PyTorch会记录该张量的每一步操作历史并自动计算。

  • 首先我们新建一个张量x
# requires_grad = True表示告诉Pytorch需要对该张量进行自动求导,
# 并且PyTorch会记录该张量的每一步操作历史并自动计算。
x = torch.rand(5,5,requires_grad = True)
x

在这里插入图片描述

PyTorch会自动追踪和记录对于张量的所有操作,当计算完成后调用.backward()方法自动计算梯度并且将计算结果保存到grad属性中。

  • 然后我们另建一个张量y
y = torch.rand(5,5,requires_grad = True)
y

在这里插入图片描述

二、什么是自动求导Autograd

  • 1.当涉及到大型神经网络时,我们都不擅长微积分,这是事实,我们不可能随时随地的高效的计算一个很复杂的微积分。因此通过显式求解数学方程来计算这样大的复合函数的梯度是不现实的,特别是这些曲线存在于大量的维数中,是无法理解的。这就是PyTorch的Autograd发挥作用的地方。它抽象了复杂的数学,帮助我们“神奇地”计算高维曲线的梯度,只需要几行代码。
  • 2.每个张量都有一个.grad_fn属性,如果这个张量是用户手动创建的那么这个张量的grad_fn是None
  • 3.在张量进行操作后,grad_fn已经被赋予了一个新的函数,这个函数引用了一个创建了这个Tensor类的Function对象。 Tensor和Function互相连接生成了一个非循环图,它记录并且编码了完整的计算历史。

2.1 简单的自动求导

  • 案例1
z = torch.sum(x+y)
z
  • 自动求导
z.backward()
print(x.grad,y.grad)

在这里插入图片描述

如果Tensor类表示的是一个标量(即它包含一个元素的张量),则不需要为backward()指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,它是形状匹配的张量。 以上的 z.backward()相当于是z.backward(torch.tensor(1.))的简写。 这种参数常出现在图像分类中的单标签分类,输出一个标量代表图像的标签。

2.2 复杂的自动求导

  • 案例2
x = torch.rand(5, 5, requires_grad=True)
y = torch.rand(5, 5, requires_grad=True)
z= x**2+y**3
z

在这里插入图片描述

  • 自动求导
#我们的返回值不是一个标量,所以需要输入一个大小相同的张量作为参数,这里我们用ones_like函数根据x生成一个张量
z.backward(torch.ones_like(x))
print(x.grad)

在这里插入图片描述
我们可以使用with torch.no_grad()上下文管理器临时禁制对已设置requires_grad = True的张量进行自动求导,这个方法在测试集计算准确率的时候会经常用到。

扫描二维码关注公众号,回复: 12811519 查看本文章
with torch.no_grad():
    print((x + y**2).requires_grad)
    # 会打印False,说明临时禁制对已设置requires_grad = True的张量进行自动求导

三、自动求导Autograd过程的简单解析

为了说明Pytorch的自动求导原理,我们来尝试分析一下PyTorch的源代码,虽然Pytorch的 Tensor和 TensorBase都是使用CPP来实现的,但是可以使用一些Python的一些方法查看这些对象在Python的属性和状态。 Python的 dir() 函数返回参数的属性、方法列表。上述案例2中的z是一个Tensor变量,看看里面有哪些成员变量。

dir(z)

会打印处一大串东西来,返回很多,我们直接排除掉一些Python中特殊方法(以__开头和结束的)和私有方法(以_开头的,直接看几个比较主要的属性:

  • .is_leaf:记录是否是叶子节点。通过这个属性来确定这个变量的类型 在官方文档中所说的“graph leaves”,“leaf variables”,都是指像x,y这样的手动创建的、而非运算得到的变量,这些变量称为创建变量。 像z这样的,是通过计算后得到的结果称为结果变量
  • 判断一个Tensor变量是创建变量还是结果变量可以通过.is_leaf
print('x.is_leaf=' + str(x.is_leaf)) # 打印True
print('z.is_leaf=' + str(z.is_leaf)) # 打印False

x是手动创建的没有通过计算,所以他被认为是一个叶子节点也就是一个创建变量,而z是通过x与y的一系列计算得到的,所以不是叶子结点也就是结果变量。

  • grad_fn:每个张量都有一个grad_fn属性用于保存张量的操作,如果这个张量为用户自己创建的,则grad_fn为None。
    为什么我们执行z.backward()方法的时候会更新x.grad和y.grad呢? .grad_fn属性记录的就是这部分的操作,虽然.backward()方法也是CPP实现的,但是可以通过Python来进行简单的探索。
z.grad_fn

在这里插入图片描述
grad_fn是一个AddBackward0类型的变量 AddBackward0这个类也是用Cpp来写的,但是我们从名字里就能够大概知道,他是加法(ADD)的反反向传播(Backward),看看里面有些什么东西。

dir(z.grad_fn)
  • 输出结果
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '_register_hook_dict',
 'metadata',
 'name',
 'next_functions',
 'register_hook',
 'requires_grad']

next_functions就是grad_fn的精华

z.grad_fn.next_functions

在这里插入图片描述
next_functions是一个tuple of tuple of PowBackward0 and int。
为什么是2个tuple ? 因为我们的操作是z= x**2+y**3 刚才的AddBackward0是相加,而前面的操作是乘方 PowBackward0。tuple第一个元素就是x相关的操作记录

  • 查看z的grad_fn属性next_functions的第一个元组的第一个元素
xg = z.grad_fn.next_functions[0][0]
dir(xg)
  • 输出结果
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '_register_hook_dict',
 'metadata',
 'name',
 'next_functions',
 'register_hook',
 'requires_grad']
  • 继续剖析,查看xg的next_functions
x_leaf=xg.next_functions[0][0]
type(x_leaf)
  • 输出结果
AccumulateGrad

在PyTorch的反向图计算中,AccumulateGrad类型代表的就是叶子节点类型,也就是计算图终止节点。AccumulateGrad类中有一个.variable属性指向叶子节点。

x_leaf.variable

这个.variable的属性就是我们的生成的变量x

这样整个规程就很清晰了:

  • 1.当我们执行z.backward()的时候。这个操作将调用z里面的grad_fn这个属性,执行求导的操作。

  • 2.这个操作将遍历grad_fn的next_functions,然后分别取出里面的Function(AccumulateGrad),执行求导操作。这部分是一个递归的过程直到最后类型为叶子节点。

  • 3.计算出结果以后,将结果保存到他们对应的variable 这个变量所引用的对象(x和y)的 grad这个属性里面。

  • 4.求导结束。所有的叶节点的grad变量都得到了相应的更新
    最终当我们执行完c.backward()之后,a和b里面的grad值就得到了更新。

  • 首先我们来剖析一下这个案例,通过这个案例来进一步理解Autograd的工作流程

# 导入相应的包
import torch
import torch.autograd

# 创建张量x、y
x = torch.tensor([1.0],requires_grad = True)
y  =torch.tensor([2.0],requires_grad = False)
z = x * y

# 求解x的梯度
z.backward(torch.ones_like(x))
x.grad

  • 输出结果
x.grad =  tensor([2.])

在这里插入图片描述
图片来源:https://zhuanlan.zhihu.com/p/148669484

从计算图中可以看出,一个张量中:

  • data中保存着所存有的数据
  • grad中保存这梯度
  • requires_grad表示是否开始追踪所有的操作历史
    在这里插入图片描述

Autograd具体工作流程:
x、y是我们自己创建的张量,让x、y执行相乘操作,生成张量z。

  • 1.当我们调用.backward()方法时,该操作会调用张量z中的grad_fn属性,前面我们说过,grad_fn属性用来保存张量的操作。如果张量是创建张量,那么该张量的grad_fn属性为None。
  • 2.这个操作将遍历grad_fn的next_functions,然后分别取出里面的Function(AccumulateGrad),执行求导操作。这部分是一个递归的过程直到最后类型为叶子节点。
  • 3.计算出结果以后,将结果保存到他们对应的variable 这个变量所引用的对象(x)的 grad属性里面。
    求导结束。所有的叶节点的grad变量都得到了相应的更新。
  • 4.最终当我们执行完z.backward()之后,x中的grad值就得到了更新。

四、扩展Autograd

如果需要自定义autograd扩展新的功能,就需要扩展Function类。因为Function使用autograd来计算结果和梯度,并对操作历史进行编码。 在Function类中最主要的方法就是forward()和backward()他们分别代表了前向传播和反向传播。

一个自定义的Function需要一下三个方法:

  • __init__ (optional):如果这个操作需要额外的参数则需要定义这个Function的构造函数,不需要的话可以忽略。

  • forward():执行前向传播的计算代码

  • backward():反向传播时梯度计算的代码。 参数的个数和forward返回值的个数一样,每个参数代表传回到此操作的梯度。

# 引入Function便于扩展
from torch.autograd.function import Function
# 定义一个乘以常数的操作(输入参数是张量)
# 方法必须是静态方法,所以要加上@staticmethod 
class MulConstant(Function):
    @staticmethod 
    def forward(ctx, tensor, constant):
        # ctx 用来保存信息这里类似self,并且ctx的属性可以在backward中调用
        ctx.constant=constant
        return tensor *constant
    @staticmethod
    def backward(ctx, grad_output):
        # 返回的参数要与输入的参数一样.
        # 第一个输入为3x3的张量,第二个为一个常数
        # 常数的梯度必须是 None.
        return grad_output, None

定义完我们的新操作后,我们来进行测试

a=torch.rand(3,3,requires_grad=True)
b=MulConstant.apply(a,5)
print("a:"+str(a))
print("b:"+str(b)) # b为a的元素乘以5

反向传播,返回值不是标量,所以backward方法需要参数

b.backward(torch.ones_like(a))
a.grad

参考文献

猜你喜欢

转载自blog.csdn.net/dongjinkun/article/details/113791715