Pytorch-自动求导

构建深度学习模型的基本流程就是:搭建计算图,求得预测值,进而得到损失,然后计算损失对模型参数的导数,再利用梯度下降法等方法来更新参数。

搭建计算图的过程,称为“正向传播”,这个是需要我们自己动手的,因为我们需要设计我们模型的结构。由损失函数求导的过程,称为“反向传播”,求导是件辛苦事儿,所以自动求导基本上是各种深度学习框架的基本功能和最重要的功能之一,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_gradx.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)的求导

损失函数公式:
\text{loss} = \sum [y - f_{w} (x)]^2,f_{w}(x) = x * w + b

求导公式:
\frac{\triangledown loss}{\triangledown w} = 2 \sum [y - f_{w}(x)] * \frac{(-1) * \triangledown f_{w}(x)}{\triangledown w}

x = 1, w = 2, b = 0, y = 1,那么lossw的偏导数计算如下:
\frac{\triangledown loss}{\triangledown w} = 2 * [y - (x * w + b)] * (-1) * x = 2 * [1 - (1 * 2 + 0)] * (-1) = 2.

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)当i=j

2)当i != j

\frac{\partial p_i}{\partial {a_j}} = \left\{\begin{matrix} p_i (1 - p_j)& if \; i = j\\ -p_j p_i& if \; i \neq j \end{matrix}\right.

如果另
\delta{ij} = \left\{\begin{matrix} 1& if \; i = j\\ 0& if \; i \neq j \end{matrix}\right.

则有,
\frac{\partial p_i}{\partial {a_j}} = p_i (\delta{ij} - p_j)

4.3 Pytorch求解梯度的两个API

  • torch.autograd.grad(loss, [w1, w2, ...])
    提供哪些变量,就对哪些变量求导,当然这些变量可求导,返回[w1.grad, w2.grad, ...]
  • loss.backward()
    返回所有的可求导变量的导数。

参考文献

PyTorch简明笔记[2]-Tensor的自动求导(AoutoGrad)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  •   自动求导应该是Torch、Tensorflow等基础框架最核心的部分,属于任督二脉性质的,一通百通;本主题主要...
    杨强AT南京阅读 2,818评论 0 3
  • 原版英文链接:Edward Z. Yang's PyTorch internals : Inside 245-5D...
    _soaroc_阅读 924评论 0 0
  • 概述 在新版本中,PyTorch引入了许多令人兴奋的新特性,主要的更新在于 Variable和Tensor的合并 ...
    古de莫宁阅读 6,173评论 0 1
  • 闲翻史书,看到很多地名,需要一本历史地图。为什么这些史书不带地图呢。 在网上搜了下,找到不少春秋时的地图,上面这个...
    小岛毅阅读 538评论 0 0
  • 这本是放飞风筝的季节 今日却无风 刮起多年的愿望 并一心一意地牵引 童年扎的风筝 在角落里数着灰尘 ...
    红尘之土阅读 150评论 0 0