Pytorch入门之一文看懂自动求梯度

    科技2022-08-12  102

    PyTorch由4个主要包装组成: 1.Torch:类似于Numpy的通用数组库,可以在将张量类型转换为(torch.cuda.TensorFloat)并在GPU上进行计算。 2.torch.autograd:用于构建计算图形并自动获取渐变的包 3.torch.nn:具有共同层和成本函数的神经网络库 4.torch.optim:具有通用优化算法(如SGD,Adam等)的优化包

     

    Pytorch的autograd包会根据输入和前向传播(推理)过程自动构建计算图,并执行反向传播求参数的梯度。

    关于autpgrad流程机制原理,我觉得这个博主讲的挺详细的,推荐看这个:

    https://blog.csdn.net/MR_kdcon/article/details/108937374中的链接

     

    这里顺带提一下,现在很多资料都有Variable这个变量,这个是老版本了。新版本中,torch.autograd.Variable 和 torch.Tensor 将同属一类。更确切地说,torch.Tensor 能够追踪日志并像旧版本的 Variable 那样运行; Variable 封装仍旧可以像以前一样工作,但返回的对象类型是 torch.Tensor。这意味着你的代码不再需要变量封装器。

    所以我们想要导入autograd包的话只需要:import torch

     

    我们之前已经知道了Tensor的某些属性,比如dtype、device,那么现在我们又可以继续学到4个属性

    1、tensor.requires_grad

    2、tensor.grad

    3、tensor.grad_fn

    4、tensor.data

    说明如下:

    requires_grad有True和False(默认值),True代表这个tensor将在计算图中被追踪其所有操作行为(加、乘、求平均、logistic、softmax.....),这么做的目的就是便于之后通过反向梯度传播计算梯度。tensor必须为浮点型

    两者设置跟踪的方法

    x = torch.ones(2, 3, requires_grad=True) y = torch.ones(6) print(y.requires_grad) # False y.requires_grad_(True) print(y.requires_grad) # True

     

    note:

    1、x.requires_grad_(True/False) 设置tensor的可导与不可导。但是需要注意的是,我只能够设置叶子变量,即leaf variable的这个方法,否则会出现以下错误:

    RuntimeError: you can only change requires_grad flags of leaf variables.

    2、只有当所有的“叶子变量”,即所谓的leaf variable都是不可求导的,那函数y才是不能求导的。

     

    那么如果不想被跟踪咋办?

    法1:

    方法.detach可以将tensor从计算图中分离出来,切断后面的计算跟踪,这样反向传播的时候,梯度就传不过去了。但不影响正向传播。

    法2:

    with torch.no_grad: #包裹不想被跟踪的操作 ... ...

    可以将不想被跟踪的操作代码块包裹起来。但不影响正向传播。

     

    那么如何计算梯度呢?

    很简单,一句话就解决:loss.backward()

    loss是损失函数,是你前向传播的最后计算结果,也是反向传播的开端。方法.backward()是用来完成计算图中所有的梯度计算。以一己之力解决所有梯度计算。

    这里需要注意一下:loss.backward()中loss如果是0维张量(标量)则backward不需要任何实参。但如果loss是个非标量(向量或者多维张量),则backward()需要传入一个同样规模的Tensor,即loss.backward(loss_like),等价于先计算y = torch,sum(loss*loss_like),然后loss对y求偏导。

    为啥要这么做呢?主要是为了避免张量对张量求导时产生的维数扩张,从而使得链式传播过程中难以匹配合适的相乘项,因此我们最希望是用标量去对张量求导,这样可以使得参数的求导结果和参数是同规模的,也便于后期的计算,如SGD、Adam等等。

     

    梯度保存在哪?

    所以接下来说第二个属性tensor.grad,这个属性用来保存计算好的梯度值

    note:

    grad在反向传播过程中是累加的,因此在每次loss.backward()之前,都需要把有关上一轮batch产生的梯度清零,即x.grad.data.zeros_()。然后再调用loss.backward()来计算这一轮batch的新梯度。

    举个栗子:

    a = torch.tensor(2,dtype=torch.float32, requires_grad=True) b = torch.tensor(3, dtype=torch.float32, requires_grad=True) d = torch.tensor(4, dtype=torch.float32, requires_grad=True) e = torch.tensor(5, dtype=torch.float32, requires_grad=True) c = a * b f = c * d g = e * c f.backward() print(a.grad) # tensor(12) print(e.grad) # None

    从这个例子我们可以看出,c节点有2个子分支,一路和e产生g,一路和d产生f,但是我只对f求反向传播,因此g那一路相当于不存在,因此最后对a的梯度只有d*b=3*4=12。

     

    第三个属性tensor.grad_fn,这个属性就是保存tensor所经过的某些计算相关的对象,比如

    x = torch.tensor([1, 2], dtype=torch.float32, requires_grad=True) y = x + 2 print(y) # tensor([3., 4.], grad_fn=<AddBackward0>)

    grad_fn就显示了经过了Add加法计算,返回这么个对象。另外自己创建的Tensor,这种叫叶子节点,叶子节点的grad_fn为None,后期经过计算得到的Tensor就会有类似上述例子的值。

    note:叶子节点不能进行in-place操作,但是除了requires_grad_()

     

    最后说一下最后一个属性 tensor.data

    x.data 返回和 x的相同数据 Tensor,只是保存原张量的值,而且这个新的Tensor和原来的Tensor是共用数据的,一者改变,另一者也会跟着改变,而且新分离得到的Tensor的require s_grad = False, 即不可求导的,即脱离了计算图,切断被跟踪

    note:

    x.data 不能被 autograd 追踪求微分 。改变x.data,导致原来的张量x的值也跟着改变了,但是这种改变对于autograd是没有察觉的,它依然按照求导规则来求导,导致得出完全错误的导数值却浑然不知。它的风险性就是如果我再任意一个地方更改了某一个张量,求导的时候也没有通知我已经在某处更改了,导致得出的导数值完全不正确,可怕的是,你还不知道是错误的答案。这个问题可以用x.detach来解决,使用detach会产生RunTimeError,提示你x已经被改变了,接下去求导会出现错误。

    x = torch.ones(1, requires_grad=True) xx = x.detach() print(xx) # tensor([1.]) print(xx.requires_grad) # False #前向传播 y = x * x * 4 xx *= 100 #后向传播 y.backward() print(x) print(x.grad)

     

    但话又说回来了,如果我们想要修改tensor的值,但又不希望被autograd记录在计算图中(即不影响反向传播),就对tensor.data操作吧。下面来看2个例子进一步加深对data的理解

    x = torch.ones(1, requires_grad=True) print(x.data) # tensor([1.]) print(x.data.requires_grad) # False #前向传播 y = x * x * 4 x.data *= 100 #后向传播 y.backward() print(x) # tensor([100.], requires_grad=True) print(x.grad) # tensor([800.])

    再看下面这个

    x = torch.ones(1, requires_grad=True) print(x.data) # tensor([1.]) print(x.data.requires_grad) # False #前向传播 y = x * x * 4 x = x*100 #后向传播 y.backward() print(x) # tensor([100.], requires_grad=True, grad_fn=<MulBackward0>) print(x.grad) # None

    这里我们想要去改变x的值,但我们从x.grad看出反向梯度计算出了错误,这一点从x.grad_fn可以看出,x进行了Mul乘法运算,我们在前向传播完成之后,即y=x*x*4,后向传播其实以完成了,就等着我们去调用backward调用,但这时候x又进行了Mul运算,使得整个后向传播收到了影响,因此产生了x.grad=None的结果。

    Processed: 0.010, SQL: 8