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中一些自然语言工具的使用的入门教程
Apr 13 Python
pandas DataFrame数据转为list的方法
Apr 11 Python
使用Python更换外网IP的方法
Jul 09 Python
python 批量修改/替换数据的实例
Jul 25 Python
Python八皇后问题解答过程详解
Jul 29 Python
对Python中一维向量和一维向量转置相乘的方法详解
Aug 26 Python
Python流程控制 while循环实现解析
Sep 02 Python
python入门之基础语法学习笔记
Feb 08 Python
Python object类中的特殊方法代码讲解
Mar 06 Python
python不同系统中打开方法
Jun 23 Python
python时间序列数据转为timestamp格式的方法
Aug 03 Python
Python数据清洗工具之Numpy的基本操作
Apr 22 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
常用表单验证类,有了这个,一般的验证就都齐了。
2006/12/06 PHP
thinkphp模板的包含与渲染实例分析
2014/11/26 PHP
利用PHP获取网站访客的所在地位置
2017/01/18 PHP
JavaScript 变量作用域分析
2011/07/04 Javascript
从URL中提取参数与将对象转换为URL查询参数的实现代码
2012/01/12 Javascript
jQuery easyui datagrid动态查询数据实例讲解
2013/02/26 Javascript
JS写的贪吃蛇游戏(个人练习)
2013/07/08 Javascript
JQuery操作单选按钮以及复选按钮示例
2013/09/23 Javascript
jquery比较简洁的软键盘特效实现方法
2015/03/19 Javascript
javascript检测两个数组是否相似
2015/05/19 Javascript
jQuery插件jRumble实现网页元素抖动
2015/06/05 Javascript
javascript弹出拖动窗口
2015/08/11 Javascript
轻松实现JavaScript图片切换
2016/01/12 Javascript
window.close(); 关闭浏览器窗口js代码的总结介绍
2016/07/14 Javascript
在localStorage中存储对象数组并读取的方法
2016/09/24 Javascript
Bootstrap滚动监听组件scrollspy.js使用方法详解
2017/07/20 Javascript
vue与TypeScript集成配置最简教程(推荐)
2017/10/17 Javascript
vue项目中使用fetch的实现方法
2019/04/25 Javascript
vue 组件中使用 transition 和 transition-group实现过渡动画
2019/07/09 Javascript
vue 解决异步数据更新问题
2019/10/29 Javascript
微信小程序实现列表的横向滑动方式
2020/07/15 Javascript
Python计算时间间隔(精确到微妙)的代码实例
2019/02/26 Python
Python基本数据结构与用法详解【列表、元组、集合、字典】
2019/03/23 Python
numpy.array 操作使用简单总结
2019/11/08 Python
python+opencv边缘提取与各函数参数解析
2020/03/09 Python
python属于跨平台语言码
2020/06/09 Python
Django Session和Cookie分别实现记住用户登录状态操作
2020/07/02 Python
python利用proxybroker构建爬虫免费IP代理池的实现
2021/02/21 Python
Web页面中八种创建多列等高(等高列布局)的实现技术
2012/12/24 HTML / CSS
幼儿园中班教师寄语
2014/04/03 职场文书
平安建设汇报材料
2014/12/29 职场文书
英语辞职信范文
2015/02/28 职场文书
联谊会开场白
2015/06/01 职场文书
2015大学迎新晚会主持词
2015/07/16 职场文书
2016年清明节期间群众祭祀活动工作总结
2016/04/01 职场文书
JavaScript继承的三种方法实例
2021/05/12 Javascript