pytorch梯度的自动求取

一、概念

如果将Tensor的属性.requires_grad属性设置为True,它将开始追踪在其上的所有操作(主要目的是利用链式法则进行梯度传播)。完成计算后,可以调用.backward()方法来完成所有梯度计算。此Tensor的梯度将累计到.grad属性中。

注意在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor。这为了避免向量(甚至更高维张量)对张量求导,而转换成标量对张量求导。举个例子,假设形状为 m × n m\times n m×n 的矩阵 X X X 经过运算得到了 p × q p\times q p×q 的矩阵 Y Y Y Y Y Y 又经过运算得到了 s × t s\times t s×t 的矩阵 Z Z Z。那么 d Z d Y \frac{dZ}{dY} dYdZ 应该是 s × t × p × q s\times t\times p\times q s×t×p×q 四维张量, d Y d X \frac{dY}{dX} dXdY 是一个 p × q × m × n p\times q\times m\times n p×q×m×n 的四维张量。反向传播时对四维张量相乘是十分困难的,因此为了避免这种问题,我们不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形式的张量。所以必要时我们要把张量通过将所有张量的元素加权求和的方式转换为标量,举个例子,假设 y y y由自变量 x x x计算而来, w w w是和 y y y同形的张量,则y.backward(w)的含义是:先计算l=torch.sum(y*w),则 l l l 是个标量,然后求 l l l 对自变量 x x x 的导数。

用一个例子说明这一点:

x=torch.tensor([1.0,2.0,3.0,4.0],requires_grad=True)
y=2*x
z=y.view(2,2)
print(z)

输出:
在这里插入图片描述
可以看到现在y并不是一个标量,在调用backward()时需要传入一个和y同形的权重向量进行加权求和得到一个标量:

v=torch.tensor([[1.0,0.1],[0.01,0.001]],dtype=torch.float)
z.backward(v)
print(x.grad)

输出:
在这里插入图片描述
注意,x.grad是和x同形的张量。

如果不想要被继续追踪,可以调用.detach()将张量从追踪记录中分离出来,这样就可以防止将来的计算被追踪(梯度传不过去了)。此外,还可以用with torch.no_grad()将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数(requires_grad=True)的梯度。

Function是另外一个很重要的类。Tensor和Function互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个Tensor都有一个.grad_fn属性,该属性即创建该Tensor的Function,就是说该Tensor是不是通过某些运算得到的,若是,则grad_fn返回一个与这些运算相关的对象,否则是None。

二、Tensor及梯度

2.1 Tensor

创建一个Tensor并设置requires_grad=True

x=torch.ones(2,2,requires_grad=True)
print(x)
print(x.grad_fn)

输出:
在这里插入图片描述
接下来做下加法操作:

y=x+2
print(y)
print(y.grad_fn)

输出如下:
在这里插入图片描述
x是直接创建的,所以它没有grad_fn;y是通过一个加法操作创建的,所以它有一个AddBackward0的grad_fn。像x这种直接创建的称为叶子节点,叶子节点对应的grad_fn是None。

print(x.is_leaf,y.is_leaf)

输出如下:
在这里插入图片描述
我们可以通过.requires_grad_()来用in-place的方式改变requires_grad属性(注意未指定情况下requires_grad默认为False):

a=torch.randn(2,2)
a=((a*3)/(a-1))
print(a.requires_grad)
#False
a.requires_grad_(True)
print(a.requires_grad)
#True
2.2 梯度

计算得到out的过程如下:

import torch
x=torch.ones(2,2,requires_grad=True)
y=x+2
z=y*y*3
out=z.mean()
out.backward()
print(x.grad)

因为out是一个标量,所以调用backward()时不需要指定求导变量:

out.backward()
#等价于out.backward(torch.tensor(1.))

我们可以通过如下的命令查看out关于x的梯度 d ( o u t ) d x \frac{d(out)}{dx} dxd(out)

print(x.grad)

输出结果如下:

在这里插入图片描述
量都为向量的函数 y ⃗ = f ( x ⃗ ) \vec{y}=f(\vec{x}) y =f(x ) ,那么 y ⃗ \vec{y} y 关于 x ⃗ \vec{x} x 的梯度就是一个雅可比矩阵(Jacobian matrix):

J = ( ∂ y 1 ∂ x 1 … ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 … ∂ y m ∂ x n ) J=\begin {pmatrix} {\partial y_1 \over \partial x_1}& \dots & {\partial y_1 \over \partial x_n} \\ \vdots &\ddots & \vdots \\ {\partial y_m \over \partial x_1}& \dots & {\partial y_m \over \partial x_n} \end {pmatrix} J=x1y1x1ymxny1xnym

而torch.autograd这个包就是用来计算一些雅可比矩阵的乘积的。例如,如果 v v v是一个标量函数的 l = g ( y ) ⃗ l=g(\vec{y)} l=g(y) 的梯度:

v = ( ∂ ∂ y 1 … ∂ ∂ y m ) v=\begin{pmatrix}{\partial\over\partial y_1}&\dots&{\partial\over\partial y_m}\end{pmatrix} v=(y1ym)

那么根据链式法则我们有 l l l关于 x ⃗ \vec{x} x 的雅可比矩阵就为:

v J = ( ∂ ∂ y 1 … ∂ ∂ y m ) ( ∂ y 1 ∂ x 1 … ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 … ∂ y m ∂ x n ) = ( ∂ ∂ x 1 … ∂ ∂ x n ) vJ=\begin{pmatrix}{\partial\over\partial y_1}&\dots&{\partial\over\partial y_m}\end{pmatrix}\begin {pmatrix} {\partial y_1 \over \partial x_1}& \dots & {\partial y_1 \over \partial x_n} \\ \vdots &\ddots & \vdots \\ {\partial y_m \over \partial x_1}& \dots & {\partial y_m \over \partial x_n} \end {pmatrix}=\begin{pmatrix}{\partial\over\partial x_1}&\dots&{\partial\over\partial x_n}\end{pmatrix} vJ=(y1ym)x1y1x1ymxny1xnym=(x1xn)

(这里有一个问题,不是m倍的吗?)

注意,grad在反向传播的过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零

#注意这里grad的累加过程
out2=x.sum()
out2.backward()
print(x.grad)

out3=x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)

在这里插入图片描述
接下来看下中断梯度追踪的例子:

x=torch.tensor(1.0,requires_grad=True)
y1=x**2
with torch.no_grad():
	y2=x**3
y3=y1+y2

print(x.requires_grad)
print(y1,y1.requires_grad)
print(y2,y2.requires_grad)
print(y3,y3.requires_grad)

输出:

True
tensor(1., grad_fn=<PowBackward0>) True
tensor(1.) False
tensor(2., grad_fn=<ThAddBackward>) True

可以看到,上面的y2没有grad_fn并且y2.requires_grad=False,而y3是有grad_fn的,此时我们将y3对x求梯度:

y3.backward()
print(x.grad)

输出:

tensor(2.)

如果正常求导,求导结果是 3 x 2 + 2 x = 5 3x^2+2x=5 3x2+2x=5,但由于y2定义时被torch.no_grad():包裹,所以与y2有关的梯度不会回传,只有与y1有关的梯度才会回传,即 x 2 x^2 x2对x的梯度。

上⾯提到, y2.requires_grad=False ,所以不能调⽤ y2.backward() ,会报错:

RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

此外,如果我们想要修改 tensor 的数值,但是⼜不希望被 autograd 记录(即不会影响反向传播),
那么我么可以对 tensor.data 进⾏操作。

x = torch.ones(1,requires_grad=True)
print(x.data) # 还是⼀个tensor
print(x.data.requires_grad) # 但是已经是独⽴于计算图之外
y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
y.backward()
print(x) # 更改data的值也会影响tensor的值
print(x.grad)

输出:

tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])

三、涉及到的具体的API介绍

backward

这个函数的原型是:

Tensor.backward(gradient=None, retain_graph=None, create_graph=False, inputs=None)

整个计算图的梯度通过链式法则进行累计。当调用backward的函数非零时,需要提供gradient参数(具体有关gradient参数的要求在上方一、中进行了介绍)。还需要注意的一点是,在调用这个函数时,我们首先需要将整个模型的梯度清零,因为这个函数会在原来梯度的基础上累加梯度。

参数介绍:

  • gradient
  • retain_graph:如果这个值为False,那么用于计算梯度的整个计算图会被释放。注意在几乎所有情况下都没有必要设置这个值为True并且值为False时整个计算图会以更高效的方式来运行。
  • create_graph:如果这个值为True,那么求导图会被构建,这允许我们计算更高等级的求导结果。
  • Inputs w.r.t. which the gradient will be accumulated into .grad. All other Tensors will be ignored. If not provided, the gradient is accumulated into all the leaf Tensors that were used to compute the attr::tensors. All the provided inputs must be leaf Tensors.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值