深度学习——动态计算图与梯度下降入门

Lesson 6.动态计算图与梯度下降入门

  在《Lesson 5.基本优化思想与最小二乘法》的结尾,我们提到PyTorch中的AutoGrad(自动微分)模块,并简单尝试使用该模块中的autograd.grad进行函数的微分运算,我们发现,autograd.grad函数可以灵活进行函数某一点的导数或偏导数的运算,但微分计算其实也只是AutoGrad模块中的一小部分功能。本节课,我们将继续讲解AutoGrad模块中的其他常用功能,并在此基础上介绍另一个常用优化算法:梯度下降算法。

import numpy as np
import torch

一、AutoGrad的回溯机制与动态计算图

1.可微分性相关属性

  在上一节中我们提到,新版PyTorch中的张量已经不仅仅是一个纯计算的载体,张量本身也可支持微分运算。这种可微分性其实不仅体现在我们可以使用grad函数对其进行求导,更重要的是这种可微分性会体现在可微分张量参与的所有运算中。

  • requires_grad属性:可微分性
# 构建可微分张量
x = torch.tensor(1.,requires_grad = True)
x
#tensor(1., requires_grad=True)

# 构建函数关系
y = x ** 2

'''grad_fn属性:存储Tensor微分函数'''
y
#tensor(1., grad_fn=<PowBackward0>)

'''我们发现,此时张量y具有了一个grad_fn属性,并且取值为<PowBackward0>,我们可以查看该属性'''
y.grad_fn
#<PowBackward0 at 0x200a2047208>
'''grad_fn其实是存储了Tensor的微分函数,或者说grad_fn存储了可微分张量在进行计算的过程中函数关系,此处x到y其实就是进行了幂运算'''

# 但x作为初始张量,并没有grad_fn属性
x.grad_fn

'''这里值得主要的是,y不仅和x存在幂运算关系(y = x**2),更重要的是,y本身还是一个有x张量计算得出的一个张量'''
y
#tensor(1., grad_fn=<PowBackward0>)

'''而对于一个可微分张量生成的张量,也是可微分的'''
y.requires_grad
#True

'''也就是相比于x,y不仅同样拥有张量的取值,并且同样可微,还额外存储了x到y的函数计算信息。我们再尝试围绕y创建新的函数关系,z = y + 1'''
z = y + 1
z
#tensor(2., grad_fn=<AddBackward0>)

z.requires_grad
#True

z.grad_fn
#<AddBackward0 at 0x200a2037648>

不难发现,z也同时存储了张量计算数值、z是可微的,并且z还存储了和y的计算关系(add)。据此我们可以知道,在PyTorch的张量计算过程中,如果我们设置初始张量是可微的,则在计算过程中,每一个由原张量计算得出的新张量都是可微的,并且还会保存此前一步的函数关系,这也就是所谓的回溯机制。而根据这个回溯机制,我们就能非常清楚掌握张量的每一步计算,并据此绘制张量计算图。

2.张量计算图

  借助回溯机制,我们就能将张量的复杂计算过程抽象为一张图(Graph),例如此前我们定义的x、y、z三个张量,三者的计算关系就可以由下图进行表示。

深度学习——动态计算图与梯度下降入门

  • 计算图的定义

  上图就是用于记录可微分张量计算关系的张量计算图,图由节点和有向边构成,其中节点表示张量,边表示函数计算关系,方向则表示实际运算方向,张量计算图本质是有向无环图。

  • 节点类型

  在张量计算图中,虽然每个节点都表示可微分张量,但节点和节点之间却略有不同。就像在前例中,y和z保存了函数计算关系,但x没有,而在实际计算关系中,我们不难发现z是所有计算的终点,因此,虽然x、y、z都是节点,但每个节点却并不一样。此处我们可以将节点分为三类,分别是:
a):叶节点,也就是初始输入的可微分张量,前例中x就是叶节点;
b):输出节点,也就是最后计算得出的张量,前例中z就是输出节点;
c):中间节点,在一张计算图中,除了叶节点和输出节点,其他都是中间节点,前例中y就是中间节点。
当然,在一张计算图中,可以有多个叶节点和中间节点,但大多数情况下,只有一个输出节点,若存在多个输出结果,我们也往往会将其保存在一个张量中。

3.计算图的动态性

  值得一提的是,PyTorch的计算图是动态计算图,会根据可微分张量的计算过程自动生成,并且伴随着新张量或运算的加入不断更新,这使得PyTorch的计算图更加灵活高效,并且更加易于构建,相比于先构件图后执行计算的部分框架(如老版本的TensorFlow),动态图也更加适用于面向对象编程。

二、反向传播与梯度计算

1.反向传播的基本过程

  在《Lesson 5.》中,我们曾使用autograd.grad进行函数某一点的导数值得计算,其实,除了使用函数以外,我们还有另一种方法,也能进行导数运算:反向传播。当然,此时导数运算结果我们也可以有另一种解读:计算梯度结果。

注:此处我们暂时不区分微分运算结果、导数值、梯度值三者区别,目前位置三个概念相同,后续讲解梯度下降时再进行区分。

首先,对于某一个可微分张量的导数值(梯度值),存储在grad属性中。

x.grad

在最初,x.grad属性是空值,不会返回任何结果,我们虽然已经构建了x、y、z三者之间的函数关系,x也有具体取值,但要计算x点导数,还需要进行具体的求导运算,也就是执行所谓的反向传播。所谓反向传播,我们可以简单理解为,在此前记录的函数关系基础上,反向传播函数关系,进而求得叶节点的导数值。在必要时求导,这也是节省计算资源和存储空间的必要规定。

z
#tensor(2., grad_fn=<AddBackward0>)

z.grad_fn
#<AddBackward0 at 0x7fad381971c0>

# 执行反向传播
z.backward()
'''反向传播结束后,即可查看叶节点的导数值'''

x
#tensor(1., requires_grad=True)

# 在z=y+1=x**2+1函数关系基础上,x取值为1时的导数值
x.grad
#tensor(2.)

'''注意,在默认情况下,在一张计算图上执行反向传播,只能计算一次,再次调用backward方法将报错'''
z.backward()
#---------------------------------------------------------------------------
#RuntimeError                              Traceback (most recent call last)
#<ipython-input-52-40c0c9b0bbab> in <module>
#----> 1 z.backward()

当然,在y上也能执行反向传播

x = torch.tensor(1.,requires_grad = True)
y = x ** 2
z = y + 1

y.backward()

x.grad
#tensor(2.)

'''第二次执行时也会报错'''
y.backward()
#---------------------------------------------------------------------------
#RuntimeError                              Traceback (most recent call last)
#<ipython-input-60-ab75bb780f4c> in <module>
#----> 1 y.backward()
z.backward()
#---------------------------------------------------------------------------
#RuntimeError                              Traceback (most recent call last)
#<ipython-input-61-40c0c9b0bbab> in <module>
#----> 1 z.backward()

'''无论何时,我们只能计算叶节点的导数值'''
y.grad
#D:\Users\ASUS\anaconda3\lib\site-packages\ipykernel_launcher.py:1: UserWarning: #The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its #.grad attribute won't be populated during autograd.backward(). If you indeed want #the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If #you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor #instead. See github.com/pytorch/pytorch/pull/30531 for more informations.
#  """Entry point for launching an IPython kernel.

至此,我们就了解了反向传播的基本概念和使用方法:

  • 反向传播的本质:函数关系的反向传播(不是反函数);
  • 反向传播的执行条件:拥有函数关系的可微分张量(计算图中除了叶节点的其他节点);
  • 反向传播的函数作用:计算叶节点的导数/微分/梯度运算结果;

2.反向传播运算注意事项

  • 中间节点反向传播和输出节点反向传播区别

  尽管中间节点也可进行反向传播,但很多时候由于存在复合函数关系,中间节点反向传播的计算结果和输出节点反向传播输出结果并不相同。

x = torch.tensor(1.,requires_grad = True)
y = x ** 2
z = y ** 2
z.backward()
x.grad
#tensor(4.)

x = torch.tensor(1.,requires_grad = True)
y = x ** 2
z = y ** 2
y.backward()
x.grad
#tensor(2.)
  • 中间节点的梯度保存

  默认情况下,在反向传播过程中,中间节点并不会保存梯度

x = torch.tensor(1.,requires_grad = True)
y = x ** 2
z = y ** 2
z.backward()
y.grad
#D:\Users\ASUS\anaconda3\lib\site-packages\ipykernel_launcher.py:2: UserWarning: #The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its #.grad attribute won't be populated during autograd.backward(). If you indeed want #the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If #you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor #instead. See github.com/pytorch/pytorch/pull/30531 for more informations.
x.grad
#tensor(4.)

'''若想保存中间节点的梯度,我们可以使用retain_grad()方法'''
x = torch.tensor(1.,requires_grad = True)
y = x ** 2
y.retain_grad()
z = y ** 2
z.backward()
y
#tensor(1., grad_fn=<PowBackward0>)
y.grad
#tensor(2.)
x.grad
#tensor(4.)

3.阻止计算图追踪

  在默认情况下,只要初始张量是可微分张量,系统就会自动追踪其相关运算,并保存在计算图关系中,我们也可通过grad_fn来查看记录的函数关系,但在特殊的情况下,我们并不希望可微张量从创建到运算结果输出都被记录,此时就可以使用一些方法来阻止部分运算被记录。

  • with torch.no_grad():阻止计算图记录

  例如,我们希望x、y的函数关系被记录,而y的后续其他运算不被记录,可以使用with torch.no_grad()来组织部分y的运算不被记录。

x = torch.tensor(1.,requires_grad = True)
y = x ** 2

with torch.no_grad():
    z = y ** 2

'''with相当于是一个上下文管理器,with torch.no_grad()内部代码都“屏蔽”了计算图的追踪记录'''
z
#tensor(1.)

z.requires_grad
#False

y
#tensor(1., grad_fn=<PowBackward0>)
  • .detach()方法:创建一个不可导的相同张量

在某些情况下,我们也可以创建一个不可导的相同张量参与后续运算,从而阻断计算图的追踪

x = torch.tensor(1.,requires_grad = True)
y = x ** 2
y1 = y.detach()
z = y1 ** 2

y
#tensor(1., grad_fn=<PowBackward0>)

y1
#tensor(1.)

z
#tensor(1.)

4.识别叶节点

  由于叶节点较为特殊,如果需要识别在一个计算图中某张量是否是叶节点,可以使用is_leaf属性查看对应张量是否是叶节点。

x.is_leaf
#True

y.is_leaf
#False

'''但is_leaf方法也有容易混淆的地方,对于任何一个新创建的张量,无论是否可导、是否加入计算图,都是可以是叶节点,这些节点距离真正的叶节点,只差一个requires_grad属性调整。'''
torch.tensor([1]).is_leaf
#True

# 经过detach的张量,也可以是叶节点
y1
#tensor(1.)

y1.is_leaf
#True

三、梯度下降基本思想

  有了AutoGrad模块中各函数方法的支持,接下来,我们就能尝试手动构建另一个优化算法:梯度下降算法。

1.最小二乘法的局限与优化

  在《Lesson 5.》中,我们尝试使用最小二乘法求解简单线性回归的目标函数,并顺利的求得了全域最优解。但正如上节所说,在所有的优化算法中最小二乘法虽然高效并且结果精确,但也有不完美的地方,核心就在于最小二乘法的使用条件较为苛刻,要求特征张量的交叉乘积结果必须是满秩矩阵,才能进行求解。而在实际情况中,很多数据的特征张量并不能满足条件,此时就无法使用最小二乘法进行求解。

最小二乘法结果:

深度学习——动态计算图与梯度下降入门

  当最小二乘法失效的情况时,其实往往也就代表原目标函数没有最优解或最优解不唯一。针对这样的情况,有很多中解决方案,例如,我们可以在原矩阵方程中加入一个扰动项

上一篇:Pytorch Chain-Rules


下一篇:平价有机蔬菜亮相天猫双11,阿里云IoT成助攻神器