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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

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