当涉及到大型神经网络时,我们都不擅长微积分。通过明确解决数学方程来计算这种大型复合函数的梯度是不切实际的,尤其是这些曲线存在于大量的维度中,不可能被摸透。
要处理14维空间中的超平面,可以想象一个三维空间,然后非常大声地对自己说 "14"。每个人都会这样做--Geoffrey Hinton
这就是PyTorch的autograd发挥作用的地方。它将复杂的数学抽象化,帮助我们 "神奇地 "计算高维曲线的梯度,只需几行代码。这篇文章试图描述autograd的神奇之处。
PyTorch基础知识
在我们进一步讨论之前,我们需要了解一些PyTorch的基本概念。
Tensors:简单地说,它只是PyTorch中的一个n维数组。张量支持一些额外的增强功能,这使得它们独一无二。除了CPU之外,它们还可以被加载到GPU上,以加快计算速度。当设置.requires_grad = True时,它们会开始形成一个后向图,跟踪应用于它们的每一个操作,以使用称为动态计算图(DCG)的东西计算梯度(在帖子中进一步解释)。
在PyTorch的早期版本中,torch.autograd.Variable类被用来创建支持梯度计算和操作跟踪的张量,但* 从PyTorch v0.4.0开始,这个变量类已经被废弃了。 torch.Tensor和torch.autograd.Variable现在是同一个类。更确切地说,torch.Tensor能够跟踪历史,其行为与旧的Variable类似
备注:根据PyTorch的设计,梯度只能针对浮点张量进行计算,这就是为什么我在使其成为支持梯度的PyTorch张量之前创建了一个浮点类型的numpy数组。*
Autograd: 这个类是一个计算导数的引擎(更准确地说,是雅各布向量乘积)。它记录了对启用梯度的张量进行的所有操作的图,并创建了一个非循环图,称为动态计算图。这个图的叶子是输入张量,根部是输出张量。梯度的计算是通过从根到叶的图形追踪,并在途中使用连锁规则乘以每一个梯度。
神经网络和反向传播
神经网络只不过是复合数学函数,经过微妙的调整(训练)后,输出所需的结果。这种调整或训练是通过一种叫做反向传播的卓越算法完成的。逆向传播是用来计算相对于输入权重的损失梯度,然后更新权重,最终减少损失。
在某种程度上,反向传播只是链式规则的花哨名称。
创建和训练一个神经网络包括以下几个基本步骤:
- 定义架构
- 使用输入数据对架构进行正向传播
- 计算损失
- 反向传播以计算每个权重的梯度
- 使用学习率更新权重
一个输入权重的微小变化所带来的损失变化被称为该权重的梯度,并使用反向传播计算。然后,梯度被用来使用学习率更新权重,以全面减少损失并训练神经网络。
这是以迭代的方式进行的。对于每个迭代,都要计算几个梯度,并建立一个称为计算图的东西来存储这些梯度函数。PyTorch通过构建动态计算图(DCG)来完成这一工作。这个图在每次迭代中都会从头开始建立,为梯度计算提供最大的灵活性。例如,对于一个前向操作(函数),一个名为MulBackward的后向操作(函数)被动态地集成到后向图中以计算梯度。
动态计算图
梯度启用的张量(变量)与函数(操作)相结合,形成动态计算图。数据流和应用于数据的操作是在运行时定义的,因此动态地构建了计算图。这个图是由引擎盖下的autograd类动态生成的。你不需要在启动训练之前对所有可能的路径进行编码--你所运行的就是你所区分的。
一个简单的两个张量的乘法的DCG看起来是这样的。
DCG with requires_grad = False (Diagram created using draw.io)
图中每个虚线框是一个变量,紫色矩形框是一个操作。
每个变量对象都有几个成员,其中一些是。
数据:它是一个变量所持有的数据。 x持有一个1x1的张量,其值等于1.0,而2.0。 y持有2.0。z持有两个的乘积,即2.0
requires_grad:这个成员,如果为真,则开始跟踪所有的操作历史,并形成一个用于梯度计算的后向图。对于一个任意的张量 a 它可以被就地操作,如下:a.requires_grad_(True)。
grad: grad持有梯度的值。如果requires_grad为False,它将持有一个None值。即使 requires_grad 是 True,它也将持有一个 None 值,除非从其他节点调用 .backward() 函数。例如,如果你为某个变量调用out.backward() 出涉及到 x在它的计算中,那么x.grad将保持∂out/∂x。
grad_fn: 这是用于计算梯度的后向函数。
is_leaf:一个节点是叶子,如果:
- 它被一些函数明确地初始化了,比如x = torch.tensor(1.0)或者x = torch.randn(1, 1)(基本上是本帖开头讨论的所有张量初始化方法)。
- 它是在对张量的操作之后创建的,这些操作都有requires_grad = False。
- 它是通过在某个张量上调用.detach()方法创建的。
在调用backward()时,梯度只对那些同时具有require_grad和is_leaf True的节点进行填充。梯度是调用.backward()的输出节点的梯度,而不是其他叶子节点。
当requires_grad = True时,PyTorch将开始跟踪操作,并在每一步存储梯度函数 要求grad = True的DCG(使用draw.io创建的图表)
在PyTorch的引擎盖下生成上述图形的代码是:
为了阻止PyTorch跟踪历史并形成落后的图形,可以用torch.no_grad()将代码包裹在里面。只要不需要梯度跟踪,它就会使代码运行得更快。
Backward()函数
Backward是一个实际计算梯度的函数,它将参数(默认为1x1单位的张量)通过后向图一直传递到从调用的根张量可追踪的每个叶子节点。计算出的梯度被存储在每个叶子节点的.grad中。记住,后向图已经在前向过程中动态生成。后向函数只使用已经制作好的图来计算梯度,并将它们存储在叶子节点中。
让我们分析一下下面的代码
需要注意的是,当z.backward()被调用时,一个张量被自动传递为z.backward(torch.tensor(1.0))。torch.tensor(1.0)是为终止连锁规则梯度乘法而提供的外部梯度。这个外部梯度被作为输入传给MulBackward函数,以进一步计算以下梯度 x.传入.backward()的张量尺寸必须与计算梯度的张量尺寸相同。例如,如果梯度启用的张量x和y如下所示:
x = torch.tensor([0.0, 2.0, 8.0], requires_grad = True)
y = torch.tensor([5.0 , 1.0 , 7.0], requires_grad = True)
而z = x * y
然后,为了计算z(1x3张量)相对于x或y的梯度,需要向z.backward()函数传递一个外部梯度,如下所示。z.backward(Torch.FloatTensor([1.0, 1.0, 1.0])
z.backward()会给出一个 RuntimeError: grad只能为标量输出隐式创建
传入backward函数的张量就像梯度的加权输出的权重。在数学上,这是一个向量乘以非标量张量的雅各布矩阵(在这篇文章中进一步讨论),因此它几乎总是一个与被调用的张量相同维度的单位张量,除非需要计算加权的输出。
tldr : 后向图是由autograd类在前向过程中自动动态地创建的。Backward()只是通过将其参数传递给已经生成的后向图来计算梯度。
数学 - 雅各布和向量
在数学上,autograd类只是一个Jacobian-vector product计算引擎。用非常简单的话来说,雅各布矩阵是一个代表两个向量所有可能的偏导数的矩阵。它是一个向量相对于另一个向量的梯度。
注意:在这个过程中,PyTorch从不明确地构造整个雅各布矩阵。通常情况下,直接计算JVP(雅各布向量乘积)会更简单、更高效。
如果一个向量X=[x1, x2,....xn]被用来通过一个函数f计算其他一些向量f(X)=[f1, f2, .... fn] ,那么雅各布矩阵**(**J)只是包含所有的偏导组合,如下所示。
雅各布矩阵 (来源: 维基百科)
以上矩阵表示f(X)相对于X的梯度
假设一个PyTorch梯度启用的张量X为。
X = [x1, x2, ..... xn] (让这是一些机器学习模型的权重)
X经过一些运算,形成一个向量Y
Y = f(X) = [y1, y2, .... ym]
然后Y 被用来计算一个标量损失 l. 假设一个向量 v恰好是标量损失的梯度 l相对于向量Y,如下所示
这个向量v被称为 grad_tensor,并作为参数传递给 backward()函数的一个参数
为了得到损失的梯度 l的梯度,雅各布矩阵J 与 矢量进行矢量相乘。
这种计算雅各布矩阵并将其与向量相乘的方法 v使得PyTorch可以轻松地对非标量输出输入外部梯度。
进一步阅读
PyTorch:自动分化包 - torch.autograd
视频:PyTorch Autograd详解-- Elliot Waite的深度教程
谢谢您的阅读!欢迎在回复中表达任何疑问。