PyTorch 如何自动计算梯度


Posted in Python onMay 23, 2021

在PyTorch中,torch.Tensor类是存储和变换数据的重要工具,相比于Numpy,Tensor提供GPU计算和自动求梯度等更多功能,在深度学习中,我们经常需要对函数求梯度(gradient)。

PyTorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。

本篇将介绍和总结如何使用autograd包来进行自动求梯度的有关操作。

1. 概念

Tensor是这个pytorch的自动求导部分的核心类,如果将其属性.requires_grad=True,它将开始追踪(track) 在该tensor上的所有操作,从而实现利用链式法则进行的梯度传播。完成计算后,可以调用.backward()来完成所有梯度计算。此Tensor的梯度将累积到.grad属性中。

如果不想要被继续对tensor进行追踪,可以调用.detach()将其从追踪记录中分离出来,接下来的梯度就传不过去了。此外,还可以用with torch.no_grad()将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为此时并不需要继续对梯度进行计算。

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

2. 具体实现

2.1. 创建可自动求导的tensor

首先我们创建一个tensor,同时设置requires_grad=True:

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

输出:

tensor([[1., 1.],

[1., 1.]], requires_grad=True)

None

'''

像x这种直接创建的tensor 称为叶子节点,叶子节点对应的grad_fn是None。如果进行一次运算操作:

y = x + 1
print(y)
print(y.grad_fn)
'''
tensor([[2., 2.],
        [2., 2.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>

'''

而y是通过一个加法操作创建的,所以它有一个为操作的grad_fn。

尝试进行更复杂的操作:

z = y ** 2
out = z.mean()
print(z, out)
'''
tensor([[4., 4.],
        [4., 4.]], grad_fn=<PowBackward0>) tensor(4., grad_fn=<MeanBackward0>)
'''

上面的out是一个标量4,通常对于标量直接使用out.backward()进行求导,不需要指定求导变量,后面进行详细说明。

也可以通过.requires_grad_()改变requires_grad属性:

a = torch.randn(3, 2) # 缺失情况下默认 requires_grad = False
a = (a ** 2)
print(a.requires_grad) # False

a.requires_grad_(True) #使用in-place操作,改变属性
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)
'''
False
True
<SumBackward0 object at 0x7fd8c16edd30>
'''

2.2. 梯度计算

torch.autograd实现梯度求导的链式法则,用来计算一些雅克比矩阵的乘积,即函数的一阶导数的乘积。

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

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

# 注意grad是累加的
out2 = x.sum()
out2.backward()
print(out2)
print(x.grad)

out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(out3)
print(x.grad)
'''
tensor([[4., 4.],
        [4., 4.]], grad_fn=<PowBackward0>) tensor(4., grad_fn=<MeanBackward0>)
tensor([[1., 1.],
        [1., 1.]])
tensor(4., grad_fn=<SumBackward0>)
tensor([[2., 2.],
        [2., 2.]])
tensor(4., grad_fn=<SumBackward0>)
tensor([[1., 1.],
        [1., 1.]])
'''

Tensor的自动求导对于标量比如上面的out.backward()十分方便,但是当反向传播的对象不是标量时,需要在y.backward()种加入一个与out同形的Tensor,不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量。

这是为了避免向量(甚至更高维张量)对张量求导,而转换成标量对张量求导。

x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
z = y.view(2, 2)
print(z)
'''
tensor([[2., 4.],
        [6., 8.]], grad_fn=<ViewBackward>)
'''

显然上面的tensor z不是一个标量,所以在调用 z.backward()时需要传入一个和z同形的权重向量进行加权求和得到一个标量。

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

'''
tensor([[2., 4.],
        [6., 8.]], grad_fn=<ViewBackward>)
tensor([2.0000, 0.2000, 0.0200, 0.0020])
'''

2.3 停止梯度追踪

我们可以使用detach()或者torch.no_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) # True
print(y2, y2.requires_grad) # False
print(y3, y3.requires_grad) # True
'''
True
tensor(1., grad_fn=<PowBackward0>) True
tensor(1.) False
tensor(2., grad_fn=<ThAddBackward>) True
'''

我们尝试计算梯度:

y3.backward()
print(x.grad)
# y2.backward() #这句会报错,因为此时 y2.requires_grad=False,,无法调用反向传播
'''
tensor(2.)
'''

这里结果为2,是因为我们没有获得y2的梯度,仅仅是对y1做了一次反向传播,作为最后的梯度输出。

2.4. 修改tensor的值

如果我们想要修改tensor的数值,但是不希望保存在autograd的记录中,require s_grad = False, 即不影响到正在进行的反向传播,那么可以用tensor.data进行操作。但是这种操作需要注意可能会产生一些问题,比如标量为0

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)

pytorch0.4以后保留了.data() 但是官方文档建议使用.detach(),因为使用x.detach时,任何in-place变化都会使backward报错,因此.detach()是从梯度计算中排除子图的更安全方法。

如下面的例子:

torch.tensor([1,2,3.], requires_grad = True)
out = a.sigmoid()
c = out.detach()
c.zero_()  # in-place为0 ,tensor([ 0.,  0.,  0.])
print(out) # modified by c.zero_() !! tensor([ 0.,  0.,  0.])
out.sum().backward()  # Requires the original value of out, but that was overwritten by c.zero_()
'''
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation
'''
a = torch.tensor([1,2,3.], requires_grad = True)
out = a.sigmoid()
c = out.data
c.zero_() # tensor([ 0.,  0.,  0.])
print(out)  # out  was modified by c.zero_() tensor([ 0.,  0.,  0.])
out.sum().backward()
a.grad  # 这么做不会报错,但是a已经被改变,最后计算的梯度实际是错误的
'''
tensor([ 0.,  0.,  0.])
'''

补充:pytorch如何计算导数_Pytorch 自动求梯度(autograd)

深度学习其实就是一个最优化问题,找到最小的loss值,因为自变量过多,想要找到最小值非常困难。所以就出现了很多最优化方法,梯度下降就是一个非常典型的例子。本文针对python的pytorch库中的自动求梯度进行了详细的解释

Tensor

pytorch里面的tensor可以用来存储向量或者标量。

torch.tensor(1) # 标量
torch.tensor([1]) # 1*1 的向量

tensor还可以指定数据类型,以及数据存储的位置(可以存在显存里,硬件加速)

torch.tensor([1,2], dtype=torch.float64)

梯度

在数学里,梯度的定义如下:

PyTorch 如何自动计算梯度

可以看出,自变量相对于因变量的每一个偏导乘以相应的单位向量,最后相加,即为最后的梯度向量。

在pytorch里面,我们无法直接定义函数,也无法直接求得梯度向量的表达式。更多的时候,我们其实只是求得了函数的在某一个点处相对于自变量的偏导。

我们先假设一个一元函数:y = x^2 + 3x +1,在pytorch里面,我们假设x = 2, 那么

>>> x = torch.tensor(2, dtype=torch.float64, requires_grad=True)
>>> y = x * x + 3 * x + 1
>>> y.backward()
>>> x.grad
tensor(7., dtype=torch.float64)

可以看出,最后y相对于x的导数在x=2的地方为7。在数学里进行验证,那么就是

y' = 2*x + 3, 当x=2时,y' = 2 * 2 + 3 = 7, 完全符合torch自动求得的梯度值。

接下来计算二元函数时的情况:

>>> x1 = torch.tensor(1.0)
>>> x2 = torch.tensor(2.0, requires_grad=True)
>>> y = 3*x1*x1 + 9 * x2
>>> y.backward()
tensor(6.)
>>> x2.grad
tensor(9.)

可以看出,我们可以求得y相对于x2的偏导数。

以上讨论的都是标量的情况,接下来讨论自变量为向量的情况。

mat1 = torch.tensor([[1,2,3]], dtype=torch.float64, requires_grad=True)
>>> mat2
tensor([[1.],
        [2.],
        [3.]], dtype=torch.float64, requires_grad=True)

mat1是一个1x3的矩阵,mat2是一个3x1的矩阵,他们俩的叉乘为一个1x1的矩阵。在pytorch里面,可以直接对其进行backward,从而求得相对于mat1或者是mat2的梯度值。

>>> y = torch.mm(mat1, mat2)
>>> y.backward()
>>> mat1.grad
tensor([[1., 2., 3.]], dtype=torch.float64)
>>> mat2.grad
tensor([[1.],
        [2.],
        [3.]], dtype=torch.float64)

其实可以把mat1中的每一个元素当成一个自变量,那么相对于mat1的梯度向量,就是分别对3个x进行求偏导。

相当于是y = mat1[0] * mat2[0] + mat1[1] * mat2[1] + mat1[2] * mat2[2]

然后分别求y对于mat1,mat2每个元素的偏导。

另外,如果我们最后输出的是一个N x M 的一个向量,我们要计算这个向量相对于自变量向量的偏导,那么我们就需要在backward函数的参数里传入参数。

PyTorch 如何自动计算梯度

如上图所述,其实pytorch的autograd核心就是计算一个 vector-jacobian 乘积, jacobian就是因变量向量相对于自变量向量的偏导组成的矩阵,vector相当于是因变量向量到一个标量的函数的偏导。最后就是标量相对于一个向量的梯度向量。

总结

最后,其实神经网络就是寻求一个拟合函数,但是因为参数过多,所以不得不借助每一点的梯度来一点一点的接近最佳的LOSS值,pytorch拥有动态的计算图,存储记忆对向量的每一个函数操作,最后通过反向传播来计算梯度,这可以说是pytorch的核心。

所以深入了解如果利用pytorch进行自动梯度计算非常重要。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
用Python编写一个国际象棋AI程序
Nov 28 Python
在Heroku云平台上部署Python的Django框架的教程
Apr 20 Python
编写Python脚本把sqlAlchemy对象转换成dict的教程
May 29 Python
python搭建微信公众平台
Feb 09 Python
python对DICOM图像的读取方法详解
Jul 17 Python
详细分析python3的reduce函数
Dec 05 Python
python 读取视频,处理后,实时计算帧数fps的方法
Jul 10 Python
python 通过麦克风录音 生成wav文件的方法
Jan 09 Python
python利用跳板机ssh远程连接redis的方法
Feb 19 Python
详解python中递归函数
Apr 16 Python
python使用sklearn实现决策树的方法示例
Sep 12 Python
浅析python内置模块collections
Nov 15 Python
解决numpy和torch数据类型转化的问题
May 23 #Python
Python 用户输入和while循环的操作
May 23 #Python
解决Tkinter中button按钮未按却主动执行command函数的问题
May 23 #Python
python tkinter Entry控件的焦点移动操作
May 22 #Python
python3.7.2 tkinter entry框限定输入数字的操作
May 22 #Python
tensorboard 可视化之localhost:6006不显示的解决方案
pytorch训练神经网络爆内存的解决方案
You might like
php 随机生成10位字符代码
2009/03/26 PHP
php实现获取文件mime类型的方法
2015/02/11 PHP
PHP开发中AJAX技术的简单应用
2015/12/11 PHP
基于Laravel实现的用户动态模块开发
2017/09/21 PHP
弹出层之1:JQuery.Boxy (一) 使用介绍
2011/10/06 Javascript
JavaScript Promise启示录
2014/08/12 Javascript
原生js实现复制对象、扩展对象 类似jquery中的extend()方法
2014/08/30 Javascript
JavaScript必知必会(二) null 和undefined
2016/06/08 Javascript
一个极为简单的requirejs实现方法
2016/10/20 Javascript
NodeJs模拟登陆正方教务
2017/04/28 NodeJs
[03:03]2014DOTA2国际邀请赛 EG战队专访
2014/07/12 DOTA
[49:54]Ti4 循环赛第三日 LGD vs Titan
2014/07/12 DOTA
Python字符遍历的艺术
2008/09/06 Python
python实现文件快照加密保护的方法
2015/06/30 Python
Python函数式编程
2017/07/20 Python
Selenium的使用详解
2018/10/19 Python
在IPython中进行Python程序执行时间的测量方法
2018/11/01 Python
pandas 透视表中文字段排序方法
2018/11/16 Python
python json.loads兼容单引号数据的方法
2018/12/19 Python
python列表每个元素同增同减和列表元素去空格的实例
2019/07/20 Python
Python 使用 prettytable 库打印表格美化输出功能
2019/12/26 Python
python Plotly绘图工具的简单使用
2020/03/03 Python
Python+Selenium实现自动化的环境搭建的步骤(图文)
2020/09/01 Python
Biblibili视频投稿接口分析并以Python实现自动投稿功能
2021/02/05 Python
养殖人员的创业计划书范文
2013/12/26 职场文书
五年级科学教学反思
2014/02/05 职场文书
公司周年庆典策划方案
2014/05/17 职场文书
酒店办公室主任岗位职责
2015/04/01 职场文书
在职证明书模板
2015/06/15 职场文书
总经理2015中秋节致辞
2015/07/29 职场文书
前端学习——JavaScript原生实现购物车案例
2021/03/31 Javascript
JavaScript的Set数据结构详解
2022/02/18 Javascript
vue使用echarts实现折线图
2022/03/21 Vue.js
MySQL Server层四个日志的实现
2022/03/31 MySQL
iOS 16进一步确认,一共支持16款iPhone
2022/04/28 数码科技
vue如何在data中引入图片的正确路径
2022/06/05 Vue.js