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实现全局变量的两个解决方法
Jul 03 Python
Python 爬虫多线程详解及实例代码
Oct 08 Python
Python使用defaultdict读取文件各列的方法
May 11 Python
Python实现简单遗传算法(SGA)
Jan 29 Python
解决pandas无法在pycharm中使用plot()方法显示图像的问题
May 24 Python
Python参数解析模块sys、getopt、argparse使用与对比分析
Apr 02 Python
python 并发编程 非阻塞IO模型原理解析
Aug 20 Python
pytorch中的上采样以及各种反操作,求逆操作详解
Jan 03 Python
python GUI库图形界面开发之PyQt5工具栏控件QToolBar的详细使用方法与实例
Feb 28 Python
Selenium环境变量配置(火狐浏览器)及验证实现
Dec 07 Python
python 实现mysql自动增删分区的方法
Apr 01 Python
python+opencv实现视频抽帧示例代码
Jun 11 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
深入PHP5中的魔术方法详解
2013/06/17 PHP
destoon复制新模块的方法
2014/06/21 PHP
PHP6新特性分析
2016/03/03 PHP
PHP INT类型在内存中占字节详解
2019/07/20 PHP
图像替换新技术 状态域方法
2010/01/28 Javascript
input链接页面、打开新网页等等的具体实现
2013/12/30 Javascript
javascript学习笔记(七)Ajax和Http状态码
2014/10/08 Javascript
javascript字符串循环匹配实例分析
2015/07/17 Javascript
BootStrap+Mybatis框架下实现表单提交数据重复验证
2017/03/23 Javascript
Javascript实现跨域后台设置拦截的方法详解
2017/08/04 Javascript
Angular4学习教程之HTML属性绑定的方法
2018/01/04 Javascript
vue-router中scrollBehavior的巧妙用法
2018/07/09 Javascript
jQuery实现输入框的放大和缩小功能示例
2018/07/21 jQuery
echarts实现词云自定义形状的示例代码
2019/02/20 Javascript
javascript关于“时间”的一次探索
2019/07/24 Javascript
layui表格数据复选框回显设置方法
2019/09/13 Javascript
微信小程序图片加载失败时替换为默认图片的方法
2019/12/09 Javascript
JS关闭子窗口并且刷新上一个窗口的实现示例
2020/03/10 Javascript
[02:51]DOTA2战队出征照拍摄花絮 TI3明星化身时尚男模
2013/07/22 DOTA
Python编程中实现迭代器的一些技巧小结
2016/06/21 Python
梯度下降法介绍及利用Python实现的方法示例
2017/07/12 Python
使用python3+xlrd解析Excel的实例
2018/05/04 Python
pytorch 调整某一维度数据顺序的方法
2018/12/08 Python
python子线程退出及线程退出控制的代码
2019/10/16 Python
Pandas聚合运算和分组运算的实现示例
2019/10/17 Python
python通过安装itchat包实现微信自动回复收到的春节祝福
2020/01/19 Python
Python如何向SQLServer存储二进制图片
2020/06/08 Python
详解background属性的8个属性值(面试题)
2020/11/02 HTML / CSS
德国街头和运动文化高品质商店:BSTN Store
2017/08/26 全球购物
英国时尚优质的女装:Hope Fashion
2018/08/14 全球购物
Reebok官方旗舰店:美国知名健身品牌锐步
2019/01/07 全球购物
Big Green Smile法国:领先的英国有机和天然产品在线商店
2021/01/02 全球购物
总经理职责范文
2013/11/08 职场文书
新闻专业个人求职信
2013/12/19 职场文书
安全生产奖惩制度
2015/08/06 职场文书
Nginx同一个域名配置多个项目的实现方法
2021/03/31 Servers