构建深度学习模型的基本流程就是:搭建计算图,求得预测值,进而得到损失,然后计算损失对模型参数的导数,再利用梯度下降法等方法来更新参数。
搭建计算图的过程,称为“正向传播”,这个是需要我们自己动手的,因为我们需要设计我们模型的结构。由损失函数求导的过程,称为“反向传播”,求导是件辛苦事儿,所以自动求导基本上是各种深度学习框架的基本功能和最重要的功能之一,PyTorch也不例外。
我们今天来体验一下PyTorch的自动求导吧,好为后面的搭建模型做准备。
1、设置Tensor的自动求导属性
1) 所有的tensor都有.requires_grad
属性,都可以设置成自动求导。具体方法就是在定义tensor的时候,让这个属性为True
,例如:
[1]: import torch
[2]: x = torch.ones(2, 3, requires_grad=True)
[3]: print('x:', x)
x: tensor([[1., 1., 1.],
[1., 1., 1.]], requires_grad=True)
2)只要这样设置了之后,后面由x经过运算得到的其他tensor,就都有requires_grad=True
属性。可以通过x.requires_grad
来查看这个属性。例如:
[4]: y = x + 1
[5]: print(y); print(y.requires_grad)
y: tensor([[2., 2., 2.],
[2., 2., 2.]], grad_fn=<AddBackward0>)
grad: True
3)如果想改变这个属性,就调用x.requires_grad_()
方法:
[6]: x.requires_grad_(False)
[7]: print(x.requires_grad); print(y.requires_grad)
False
True
注意区别:x.requires_grad
和x.requires_grad_()
两个东西,前面是调用变量的属性值,后者是调用内置的函数,来改变属性。
2、来求导吧
下面我们来试试自动求导到底怎么样。
我们首先定义一个计算图(计算的步骤):
[1]: import torch
[2]: x = torch.tensor([[1., 2., 3.], [4., 5., 6.]], requires_grad=True)
[3]: y = x + 1
[4]: z = 2 * y * y
[5]: J = torch.mean(z)
注意:
- 要想使x支持求导,必须让x为浮点类型,否则会报错:
RuntimeError: Only Tensors of floating point dtype can require gradients
; - 求导,只能是【标量】对标量,或者【标量】对向量/矩阵求导,针对这一点的具体分析如下:
x、y、z都是tensor,但是size为(2,3)的矩阵。但是J是对z的每一个元素加起来求平均,所以J是标量。
试图z对x求导:
[6]: z.backward()
# 报错:
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-38-40c0c9b0bbab> in <module>
----> 1 z.backward()
...
...
~/anaconda2/envs/py3/lib/python3.6/site-packages/torch/autograd/__init__.py in _make_grads(outputs, grads)
32 if out.requires_grad:
33 if out.numel() != 1:
---> 34 raise RuntimeError("grad can be implicitly created only for scalar outputs")
35 new_grads.append(torch.ones_like(out))
36 else:
RuntimeError: grad can be implicitly created only for scalar outputs
正确的应该是J对x求导:
- PyTorch里面,求导是调用.backward()方法。直接调用
backward()
方法,会计算对计算图叶节点(允许求导)的导数。 - 获取求得的导数,用
.grad
方法。
[7]: J.backward()
[8]: x.grad
tensor([[1.3333, 2.0000, 2.6667],
[3.3333, 4.0000, 4.6667]])
总结上述过程,构建计算图(正向传播,Forward Propagation)和求导(反向传播,Backward Propagation)的过程就是:
3、关于backward函数的一些关键问题
3.1 一个计算图只能backward一次
一个计算图在进行反向求导之后,为了节省内存,这个计算图就销毁了。如果你想再次求导,就会报错。
例如:
[1]: import torch
[2]: x = torch.tensor([[1., 2., 3.], [4., 5., 6.]], requires_grad=True)
[3]: y = x + 1
[4]: z = 2 * y * y
[5]: J = torch.mean(z)
[6]: J.backward() # 正常运行
[7]: x.grad
tensor([[1.3333, 2.0000, 2.6667],
[3.3333, 4.0000, 4.6667]])
[8]: J.backward() # 报错
RuntimeError: Trying to backward through the graph a second time, but the buffers have already been freed. Specify retain_graph=True when calling backward the first time.
那么,我还想再次求导,该怎么办呢?
遇到这种问题,一般两种情况:
- 你的实际计算,确实需要保留计算图,不让子图释放。
那么,就更改你的backward函数,添加参数retain_graph=True
,重新进行backward,这个时候你的计算图就被保留了,不会报错。但是这样会吃内存!尤其是,你在大量迭代进行参数更新的时候,很快就会内存不足,memory out了。
[1]: import torch
[2]: x = torch.tensor([[1., 2., 3.], [4., 5., 6.]], requires_grad=True)
[3]: y = x + 1
[4]: z = 2 * y * y
[5]: J = torch.mean(z)
[6]: J.backward(retain_graph=True) # 保留图
[7]: x.grad
tensor([[1.3333, 2.0000, 2.6667],
[3.3333, 4.0000, 4.6667]])
[8]: J.backward() # 正常运行
也就是说第8行([8]:J.backward()
)想要正常运行,则需要在第6行增加retain_graph=True
,即第6行改为J.backward(retain_graph=True)
;换句话说,想要再次求导成功,则需要前一次求导中保留图。
- 你实际根本没必要对一个计算图backward多次,而你不小心多跑了一次backward函数。
这种情况在Jupyter中的比较常见,粗暴的解决办法是:重启Jupyter核,重运行一遍所有代码cell。
3.2 不是标量也可以用backward()函数来求导
确实是,不一定只有标量能求导,这里面的玄机在哪里呢?文档中有这么一个例子就不是标量求导:
其中,y是向量,可以对x求导,但同时发现需要传递参数gradients。
那么gradients是什么呢?
从说明中我们可以了解到:
- 如果你要求导的是一个标量,那么gradients默认为None,所以前面可以直接调用
J.backward()
就行了 - 如果你要求导的是一个张量,那么gradients应该传入一个Tensor。那么这个时候是什么意思呢?
在StackOverflow有一个解释很好:
大意就是说,我们有时候需要让loss(loss=[loss1,loss2,loss3]
)的各个分量分别对x求导,这个时候就采用loss.backward(torch.tensor([[1.0,1.0,1.0,1.0]]))
,其中各个分量的权重都为1;
还有一种情况,如果你想让不同的分量有不同的权重,那么就赋予gradients不一样的值即可,比如:loss.backward(torch.tensor([[0.1,1.0,10.0,0.001]]))
。
这样就使得backward()
操作更加灵活。
4 具体实例
4.1 均方误差(MSE)的求导
损失函数公式:
求导公式:
另,那么对的偏导数计算如下:
import torch
import torch.nn.functional as F
x = torch.ones(1)
w = torch.full([1], 2, requires_grad=True) # 允许求导
b = torch.zeros(1)
y = torch.ones(1)
mse = F.mse_loss(y, x * w + b)
torch.autograd.grad(mse, [w])
# =========================== #
OUT:
(tensor([2.]),)
其中求导方式使用torch.autograd.grad(loss, [w1, w2, ..., ])
4.2 Softmax求导
- Softmax过程:
- 求导过程:
1)当
2)当
如果另
则有,
4.3 Pytorch求解梯度的两个API
- torch.autograd.grad(loss, [w1, w2, ...])
提供哪些变量,就对哪些变量求导,当然这些变量可求导,返回[w1.grad, w2.grad, ...] - loss.backward()
返回所有的可求导变量的导数。
网友评论