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 相关文章推荐
详解Python3.1版本带来的核心变化
Apr 07 Python
详解python中的 is 操作符
Dec 26 Python
Python学习之Django的管理界面代码示例
Feb 10 Python
Python中flatten( )函数及函数用法详解
Nov 02 Python
django中使用Celery 布式任务队列过程详解
Jul 29 Python
使用Python自动生成HTML的方法示例
Aug 06 Python
PyTorch之图像和Tensor填充的实例
Aug 18 Python
Pytorch之contiguous的用法
Dec 31 Python
Python tkinter常用操作代码实例
Jan 03 Python
python opencv 实现对图像边缘扩充
Jan 19 Python
python迷宫问题深度优先遍历实例
Jun 20 Python
Python实现信息管理系统
Jun 05 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&amp;MYSQL分页原理及实现
2007/01/02 PHP
深入掌握include_once与require_once的区别
2013/06/17 PHP
ThinkPHP基本的增删查改操作实例教程
2014/08/22 PHP
WordPress中查询文章的循环Loop结构及用法分析
2015/12/17 PHP
简单理解PHP的面向对象编程方式
2016/05/17 PHP
PHP简单实现二维数组的矩阵转置操作示例
2017/11/24 PHP
Laravel实现ApiToken认证请求
2019/10/14 PHP
禁止直接访问php文件代码分享
2020/05/05 PHP
js最简单的拖拽效果实现代码
2010/09/24 Javascript
jquery操作select详解(取值,设置选中)
2014/02/07 Javascript
Node.js与PHP、Python的字符处理性能对比
2014/07/06 Javascript
js中一维数组和二位数组中的几个问题示例说明
2014/07/17 Javascript
浅谈JSON中stringify 函数、toJosn函数和parse函数
2015/01/26 Javascript
JS实现进入页面时渐变背景色的方法
2015/02/25 Javascript
AngularJS中$interval的用法详解
2016/02/02 Javascript
JavaScript中获取时间的函数集
2016/08/16 Javascript
ionic2 tabs 图标自定义实例
2017/03/08 Javascript
javascript实现最长公共子序列实例代码
2018/02/05 Javascript
使用 JavaScript 创建并下载文件(模拟点击)
2019/10/25 Javascript
搭建Vue从Vue-cli到router路由护卫的实现
2019/11/14 Javascript
分享JS表单验证源码(带错误提示及密码等级)
2020/01/05 Javascript
JavaScript日期库date-fn.js使用方法解析
2020/09/09 Javascript
详解Python中最难理解的点-装饰器
2017/04/03 Python
tensorflow saver 保存和恢复指定 tensor的实例讲解
2018/07/26 Python
在Pandas中给多层索引降级的方法
2018/11/16 Python
pytorch中的上采样以及各种反操作,求逆操作详解
2020/01/03 Python
浅谈Pytorch torch.optim优化器个性化的使用
2020/02/20 Python
tensorflow使用L2 regularization正则化修正overfitting过拟合方式
2020/05/22 Python
Django框架安装及项目创建过程解析
2020/09/14 Python
Jupyter Notebook 远程访问配置详解
2021/01/11 Python
机械机修工岗位职责
2014/08/03 职场文书
干部职工纪律作风整改措施思想汇报
2014/10/11 职场文书
担保书范本
2015/01/20 职场文书
2016消防宣传标语口号
2015/12/26 职场文书
神州牡丹园的导游词
2019/11/20 职场文书
纯html+css实现Element loading效果
2021/08/02 HTML / CSS