理解深度学习中的自动微分

2024-03-23 15:59

本文主要是介绍理解深度学习中的自动微分,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、自动微分定义

引用自 李沐《动手学深度学习》
深度学习框架可以通过自动计算导数,即自动微分(automatic differentiation)来加快求导。具体的,根据设计好的模型,torch会构建一个计算图(computational graph), 来跟踪计算是哪些数据通过哪些操作组合起来产生输出。 自动微分使系统能够随后反向传播梯度。 这里,反向传播(back propagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。

以上这段话总体介绍了自动微分的定义及其各类概念,我们将在下面的讲解中进行不断的分点理解。

阅读以下内容时博主在这里斗胆将自动微分与反向传播画上等号,即以下文章中,反向传播与自动微分意义相等。

二、反向传播中的概念

1. 计算图(dynamic graph)

上来先入为主,PyTorch在进行反向传播时是通过图的形式进行计算的,这个图称为计算图。这句话的意思是,反向传播这一个过程不是凭空产生的说传播就传播,而是借助图来传播的,这图叫计算图。
下图即是一个简单的计算图,其中蓝色代表叶子节点,绿色代表根节点,灰色代表要进行的运算,箭头方向为前向传播的方向。
在这里插入图片描述

初看这张图时可能有很多疑问,什么是叶子节点,什么又是运算,什么是前向传播,标题中的反向传播又是什么。不要担心,相信你阅读完这篇文章后都会明白的。

首先,在pytorch的计算图里只有两种东西:数据(tensor)运算(operation),其中:

  • 运算包括了:加减乘除、开方、幂指对、三角函数等可求导运算
  • 数据包括了:叶子节点非叶子节点 两种数据类型(所以数据就是节点,节点就是数据),其中,数据为Tensor(张量)格式。每个Tensor(每个节点、每个数据)都具有如下属性
  1. 是否可以(需要)求(计算)梯度 :.requires_grad

  2. 创建出(运算出)该节点的方式:.grad_fn

  3. 是否为叶子节点 :.is_leaf

  4. 梯度值:.grad

在这里插入图片描述

也就是说,计算图中的每个节点(数据)都有四个属性围绕着他,这四个属性存储了这个数据的一些基本信息,可以表明这个节点的身份信息,同时也决定了它是否是我们反向传播的最终目标(后续会讲)。
既然我们已经知道了计算图的组织结构,即 数据 + 运算,而且我们也知道了数据分两种,分别是叶子节点和非叶子节点,那我们怎么来判断某个数据是否是叶子节点呢?

2. 是否是叶子节点

我们判断计算图中的数据是否是叶子节点时,我们是有判断规律的。
两种情况下的tensor会是叶子节点:

  1. 它的属性 .requires_grad为False
    也就是说,当一个张量的 requires_grad= False,那它就是叶子节点。
X = torch.tensor([1.0, 2.0, 3.0]) #没有手动指明requires_grad=True,则默认为False
print(X.requires_grad)
print(X.is_leaf)>>>False #不可求梯度
>>>True #是叶子节点
  1. 它的属性 .requires_grad为True,但不是经由运算得到的。
    也就是说,当一个张量时的 requires_grad=True,并且此该张量是由用户自定义出来的,而不是经过运算得到,它是叶子节点。
X = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) #手动指明requires_grad=True
print(X.requires_grad)
print(X.is_leaf)>>>True #可求梯度
>>>True #是叶子节点

等一下等一下,我知道你们有点迷,因为我也迷了,我们捋一捋:

假如一个张量的requires_grad为False,它一定是叶子节点,也就是说,不管你是定义出来的张量,还是通过运算得出来的张量,只要你的requires_grad为False,那你就是叶子节点。我们来拿代码看一下:

x = torch.tensor([1.0, 2.0, 3.0]) #定义出来的张量x,默认requires_grad为False
y = torch.tensor([1.0, 2.0, 3.0]) #定义出来的张量y,默认requires_grad为False
z1 = x + 1    #通过x+1运算出来的张量z1,requires_grad为False
z2 = torch.mul(x, y)  #通过x*y运算出来的张量z2,requires_grad为False
print(z1)
print(z2)
#验证他们的属性
print(x.requires_grad, x.is_leaf)
print(y.requires_grad, y.is_leaf)
print(z1.requires_grad, z1.is_leaf)
print(z2.requires_grad, z2.is_leaf)>>>tensor([2., 3., 4.])
>>>tensor([1., 4., 9.])
>>>False True
>>>False True
>>>False True
>>>False True

我们发现,只要是requires_grad=False的Tensor,它都是叶子节点。

看到这里可能有一部分人有疑问了,既然我们自定义的x和y的requires_grad是默认为False的,那对他们进行运算得出来的z1和z2的requires_grad为啥也是False?

那么我们就引出来一个知识:如果一个张量z的形成中有requires_grad=True的张量x的参与(我们将这个requires_grad=True的张量x称作源节点),那么这个张量zrequires_grad属性也会变为True,否则为False。也就是说,既然你x(源节点)不需要计算梯度,我z是用你运算出来的,那我也不计算(我不管闲事),但如果你需要梯度计算,那我也勉强计算一下梯度吧(话是这么说,这里面是有原因的,原因就是求导的链式法则必须要求这样)。

我们来拿个代码验证一下:

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = torch.tensor([1.0, 2.0, 3.0])
z1 = x + 1
z2 = torch.mul(x, y)
print(z1)
print(z2)
print(x.requires_grad, x.is_leaf)
print(y.requires_grad, y.is_leaf)
print(z1.requires_grad, z1.is_leaf)
print(z2.requires_grad, z2.is_leaf)>>>tensor([2., 3., 4.])
>>>tensor([1., 4., 9.])
>>>True True
>>>False True
>>>True False
>>>True False

我们只将x手动指明requires_grad=True,就发现,跟x相关的z1,z2都requires_grad=True了,但是由于z1,z2是运算而来的,他们就不是叶子节点了!
在这里插入图片描述

看来要不是有requires_grad=True的Tensor参与进来,将运算得出来的张量也变成可求梯度,那上面的张量就只能永远参加全是叶子节点的party了。

3. 是否可以求梯度

经过上面的对叶子节点的讲解,我们现在也大致知道了requires_grad属性是干嘛的了。当我们想要对某个tensor变量求梯度时,一定需要先保证其requires_grad属性为True(即可求梯度或者说是需要求梯度),而要保证这个tensor的requires_grad为True的话,要么你是自定义的requires_grad=True叶子节点,要么你是自定义的requires_grad=True叶子节点运算而来的非叶子节点。

这里插一嘴,指定自定义叶子节点可求梯度的方式主要有两种:

x = torch.tensor(1.).requires_grad_() # 第一种
x = torch.tensor(1., requires_grad=True) # 第二种

可以看到我们刚刚一直是使用的第二种来指定我们定义的叶子节点为可求梯度的(反正我喜欢用第二种)

好的!到此,我们即将引出我们此次文章的终极目标:求梯度!!!!!

在这里,允许我强调一点,我们的终极目标其实求的是叶子节点的梯度,虽然非叶子节点的梯度也需要求,但他们只是我们达到终极目标的垫脚石而已,不能没有,但却不那么重要。所以说,终极目标很好的解释了我为什么花了这么大的功夫去区分叶子节点和非叶子节点。

4.如何求叶子节点的梯度

我们来看一个例子,假如有以下一个式子:
y = ( x + 1 ) ∗ ( x + 2 ) (1) \pmb{y}=(\pmb{x}+1)*(\pmb{x}+2) {\tag {1}} y=(x+1)(x+2)(1)
我们能不能想一下把其中出现过的元素(数据和运算)放到torch的计算图中,并求一下:
∂ y ∂ x (2) \frac{\partial {\pmb y}}{\partial {\pmb x}} {\tag {2}} xy(2)

这时我们定义的x一定是requires-grad=True了,并且我们假设x的值为2,即:

x = torch.tensor([2.0], requires_grad=True)

来画一下这个计算图吧:
在这里插入图片描述

我们令(x+1)为运算出来的非叶子节点a,(x+2)为运算出来的非叶子节点b,理所应当的,aby也一定是requires_grad=True

这里我们说明一下前向传播的概念:

前向传播就是将已知的叶子节点数据带入函数中顺序得出非叶子节点的结构及值的过程。也就是上图中的将x的值丢进函数里依次得出节点a、节点b、节点c的结构及值。

好家伙,这就是函数求值嘛,我小学就学过。真没错,前向传播就是函数求值,不过我们在函数求值中引入了我们计算图的概念,包括节点的结构、节点的值、运算符。

那我们不妨先用人类的思维手算一下yx的梯度:
∂ y ∂ x = ∂ y ∂ a ∂ a ∂ x + ∂ y ∂ b ∂ b ∂ x = ( x + 2 ) ∗ 1 + ( x + 1 ) ∗ 1 = 7 (3) \frac{\partial{{\pmb y}}}{\partial{{\pmb x}}} = \frac{\partial{\pmb y}}{\partial{{\pmb a}}}\frac{\partial{{\pmb a}}}{\partial{{\pmb x}}} + \frac{\partial{\pmb y}}{\partial{{\pmb b}}}\frac{\partial{{\pmb b}}}{\partial{{\pmb x}}} = ({\pmb x} +\pmb 2)*\pmb 1 + ({\pmb x}+\pmb 1)*\pmb 1 =\pmb 7 {\tag 3} xy=ayxa+byxb=(x+2)1+(x+1)1=7(3)
可以,这不就是 求偏导+链式法则 嘛,我们要求yx的梯度就得先求ya的梯度和ax的梯度以及yb的梯度和bx的梯度,那我们求一下这其中出现过的梯度表达式都有哪些吧:
∂ y ∂ a = ∂ a ∗ b ∂ a = b = ( x + 2 ) ∂ y ∂ b = ∂ a ∗ b ∂ b = a = ( x + 1 ) ∂ a ∂ x = ∂ x + 1 ∂ x = 1 ∂ b ∂ x = ∂ x + 2 ∂ x = 1 \begin{align} \frac{\partial{{\pmb y}}}{\partial{{\pmb a}}} &= \frac{\partial{\pmb a*\pmb b}}{\partial{{\pmb a}}} = \pmb b =({\pmb x} +\pmb 2) \tag{4-1}\\ \frac{\partial{{\pmb y}}}{\partial{{\pmb b}}} &= \frac{\partial{\pmb a*\pmb b}}{\partial{{\pmb b}}} = \pmb a =({\pmb x} +\pmb 1) \tag{4-2}\\ \frac{\partial{{\pmb a}}}{\partial{{\pmb x}}} &= \frac{\partial{\pmb x+ \pmb 1}}{\partial{{\pmb x}}} = \pmb 1 \tag{4-3}\\ \frac{\partial{{\pmb b}}}{\partial{{\pmb x}}} &= \frac{\partial{\pmb x+ \pmb 2}}{\partial{{\pmb x}}} = \pmb 1 \tag{4-4} \end{align} aybyxaxb=aab=b=(x+2)=bab=a=(x+1)=xx+1=1=xx+2=1(4-1)(4-2)(4-3)(4-4)

既然我们的脑子可以手算出上面(4-1)到(4-4)的表达式,那么pytorch一定也可以帮我们算出来,因为函数结构是你知我知计算机也知的。

所以我们不妨假设一下,如果pytorch按照我们人脑的方式从后往前计算出梯度表达式,即依次计算出(4-1)(4-2)(4-3)(4-4)的表达式,再将这4个表达式按照(3)的链式法则一相乘,那我们需要求的yx的梯度表达式不就出来了吗?

对于我们人来说,我们是通过眼睛来看这个函数,了解了这个函数的结构,加上我们所学的数学知识,我们方可以写出上面的求偏导表达式。

而对于pytroch,它则是使用了我们介绍过的计算图,将函数映射到计算图中,先保存最开始的叶子节点x的结构,然后不断的保存由x运算所得的非叶子节点的结构,这样对pytorch来说,根据数学知识,求每一个梯度表达式也是易如反掌啦,即:

由最后的节点开始,依次调用后向节点的表达式,分别计算(4-1)(4-2)(4-3)(4-4)的梯度表达式及其值的过程,就是反向传播!

下图计算图中绿色的部分即为pytorch计算出来的各个节点的梯度表达式:
在这里插入图片描述

等等,等等,pytorch是怎么知道最终要求的是yx的梯度的?我们一直在先入为主的计算yx的梯度,认为pytorch最终计算yx的梯度也是理所应当,那到底是什么原因能够让pytorch专门去计算yx的梯度呢。记得我之前说过非叶子节点都是垫脚石吗?正是因为x是叶子节点,pytorch才会从最后一个节点去对它求偏导,也就是说,pytorch在一个完整的计算图中,反向传播时会对所有叶子节点求出梯度,计算图保存数据的时候同时保存了is_leaf属性信息,知道谁是叶子节点不过分吧?

所以这就是为什么 反向传播是基于计算图的,因为图这个东西是真能保存结构,真能保存运算信息,真好用啊。

以上就是自动微分的原理,其实自动微分挺简单的,无非就是你给我个函数,再给我几个requires_grad=True的源数据,我把函数转为一个数据+运算的计算图,再把源数据放到计算图的叶子节点上,根据运算自动填充后面的非叶子节点(前向传播过程)。当我填充好了之后,你要是想求源数据的梯度,我就通过最后一个节点的结构一点点去调用后方的节点结构求偏导,最后再利用一下链式法则串起来,不就求到了对叶子节点的梯度。归根到底,可以反向传播的根本原因是,通过计算图,可以在前向传播的时候保留各个节点的结构信息及过程中的运算信息。

tips:

  • 我们可以将前向传播的过程看成计算图构建的过程,因为在前向传播的过程中,不断地构建出每个节点的结构和值并保存到计算图中。
  • 当源数据不需要求梯度(requires_grad=False)时,由源数据运算得出的数据也不需要求梯度,因为没有保存梯度的必要了,这时,计算图也就没有存在的意义,所以计算图并不会被构建出来,所以每个数据都当叶子节点去吧。

相比于我们人类计算梯度时要列出链式法则的式子然后带入值算出结果,pytorch计算梯度非常方便,我们只需要使用反向传播函数。

5.反向传播函数

  1. PyTorch提供两种求梯度的方法:backward() 和 torch.autograd.grad()
  2. 这两种求梯度的区别在于前者是给叶子节点填充grad字段(导数值字段)(也就是前者可以通过 a.grad (a为叶子节点) 来获得该叶子节点的梯度值,相当于该叶子节点的grad部分被赋值了),而后者是直接返回梯度给你(后者只得出导数值而不将这个梯度存到叶子节点的grap字段中)
  3. y.backward() 差不多等同于 torch.autograd.backward(y),只有第 2) 点的区别
  4. 可以看到在第二条,grad的前方都是__叶子节点__,而不是__非叶子节点__,也就是说,一般只有叶子节点才有实际意义上的grad属性(具体原因见下方)。
    下面给出在可以求导的前提下,也就是对要求梯度的tenser的requires_grad=True时,如何对其自动微分

我们通过两种自动微分方式来获得叶子节点的梯度:

1) 使用backward()

x = torch.tensor(2., requires_grad=True)a = torch.add(x, 1)
b = torch.add(x, 2)
y = torch.mul(a, b)y.backward()
print(x.grad)
>>>tensor(7.)

看一下这几个tensor的属性:

print("requires_grad: ", x.requires_grad, a.requires_grad, b.requires_grad, y.requires_grad)
print("is_leaf: ", x.is_leaf, a.is_leaf, b.is_leaf, y.is_leaf)
print("grad: ", x.grad, a.grad, b.grad, y.grad)>>>requires_grad: True True True True
>>>is_leaf: True False False False
>>>grad: tensor(7.) None None None

可以看到,使用backward() 函数反向传播计算tensor的梯度时,并不保存所有tensor的梯度,而是只保存满足这几个条件的tensor的梯度:

I. 类型为叶子节点
II. requires_grad=True
III. 依赖该tensor的所有tensor的requires_grad=True。

所有满足条件的tensor梯度会自动保存到对应的grad属性里,而你需要求梯度的叶子节点一定是满足上述条件的,而计算过程中的非叶子节点的 .grad 属性默认情况下是不会被保存的,因为它们通常不是计算梯度的终点(这是torch框架的考虑设定)。

tips:

  • 如果需要保存非叶子节点的grad,则可以使用a.retain_grad(),这样即可保存该非叶子节点a的grad(前提是该节点的requires_grad属性为True)。

2) 使用autograd.grad()

x = torch.tensor(2., requires_grad=True)a = torch.add(x, 1)
b = torch.add(x, 2)
y = torch.mul(a, b)grad = torch.autograd.grad(outputs=y, inputs=x)
print(grad[0])
Print(x.grad)>>>tensor(7.)
>>>None

因为指定了输出为y,输入为x,所以返回值就是 y关于x的梯度,完整的返回值其实是一个元组,保留第一个元素就行,后面元素是空白。

此外,我们发现x.grad是None,说明使用 autograd.grad() 得到的grad并不会存入x.gard中。

注意,使用 autograd.grad() 得到的是一个元组,我们只需要保留第一个元素即可,也就是grad[0]。

有任何问题请联系作者邮箱,位于作者简介处。

参考

  1. https://www.bilibili.com/video/BV1yG411x7Cc/?spm_id_from=333.337.search-card.all.click&vd_source=c8e5d5163f5fc51afd4d9b45ec8c57d9
  2. https://blog.csdn.net/cendrier/article/details/129045868
  3. https://zhuanlan.zhihu.com/p/279758736

这篇关于理解深度学习中的自动微分的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/838835

相关文章

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

Go Mongox轻松实现MongoDB的时间字段自动填充

《GoMongox轻松实现MongoDB的时间字段自动填充》这篇文章主要为大家详细介绍了Go语言如何使用mongox库,在插入和更新数据时自动填充时间字段,从而提升开发效率并减少重复代码,需要的可以... 目录前言时间字段填充规则Mongox 的安装使用 Mongox 进行插入操作使用 Mongox 进行更

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

深入理解Apache Airflow 调度器(最新推荐)

《深入理解ApacheAirflow调度器(最新推荐)》ApacheAirflow调度器是数据管道管理系统的关键组件,负责编排dag中任务的执行,通过理解调度器的角色和工作方式,正确配置调度器,并... 目录什么是Airflow 调度器?Airflow 调度器工作机制配置Airflow调度器调优及优化建议最

IDEA如何让控制台自动换行

《IDEA如何让控制台自动换行》本文介绍了如何在IDEA中设置控制台自动换行,具体步骤为:File-Settings-Editor-General-Console,然后勾选Usesoftwrapsin... 目录IDEA如何让控制台自http://www.chinasem.cn动换行操作流http://www

vscode保存代码时自动eslint格式化图文教程

《vscode保存代码时自动eslint格式化图文教程》:本文主要介绍vscode保存代码时自动eslint格式化的相关资料,包括打开设置文件并复制特定内容,文中通过代码介绍的非常详细,需要的朋友... 目录1、点击设置2、选择远程--->点击右上角打开设置3、会弹出settings.json文件,将以下内

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.