深度强化学习实践(原书第2版)
上QQ阅读APP看书,第一时间看更新

3.2 梯度

即便对GPU的支持是透明的,如果没有“杀手锏”——梯度的自动计算——所有与张量有关的计算都将变得很复杂。这个功能最初是在Caffe工具库中实现的,然后成为DL库中约定俗成的标准。

手动计算梯度实现和调试起来都非常痛苦,即使是最简单的神经网络(Neural Network,NN)。你必须计算所有函数的导数,应用链式法则,然后计算结果,并祈祷计算准确。对于理解DL的具体细节来说,这可能是一个非常有用的练习,但你肯定不想一遍又一遍地在不同的NN架构中重复计算。

幸运的是,那些日子已经过去了,就像使用烙铁和真空管编写硬件程序一样,都过去了!现在,定义一个数百层的NN只需要从预先定义好的模块中组装即可,在一些极端情况下,也可以手动定义转换表达式。

所有的梯度都会仔细计算好,通过反向传播应用于网络。为了能够做到这一点,需要根据所使用的DL库来定义网络架构,它可能在细节上有所不同,但大体是相同的——就是必须定义好网络输入输出的顺序(见图3.2)。

058-01

图3.2 流经NN的数据和梯度

最根本的区别是如何计算梯度。有两种方法:

  • 静态图:在这种方法中,需要提前定义计算,并且以后也不能更改。在进行任何计算之前,DL库将对图进行处理和优化。此模型在TensorFlow(<2的版本)、Theano和许多其他DL工具库中均已实现。
  • 动态图:不需要预先精确地定义将要执行的图;只需要在实际数据上执行希望用于数据转换的操作。在此期间,库将记录执行的操作的顺序,当要求它计算梯度时,它将展开其操作历史,积累网络参数的梯度。这种方法也称为notebook gradient,它已在PyTorch、Chainer和一些其他库中实现。

两种方法各有优缺点。例如,静态图通常更快,因为所有的计算都可以转移到GPU,从而最小化数据传输开销。此外,在静态图中,库可以更自由地优化在图中执行计算的顺序,甚至可以删除图的某些部分。

另一方面,虽然动态图的计算开销较大,但它给了开发者更多的自由。例如,开发者可以说“对这部分数据,可以将这个网络应用两次,对另一部分数据,则使用一个完全不同的模型,并用批的均值修剪梯度。”动态图模型的另一个非常吸引人的优点是,它可以通过一种更Pythonic的方式自然地表达转换。最后,它只是一个有很多函数的Python库,所以只需调用它们,让库发挥作用就可以了。

007-03兼容性说明

PyTorch从1.0版本起就已经支持即时(Just-In-Time,JIT)编译器,该编译器支持PyTorch代码并将其导入所谓的TorchScript中。这是一种中间表示形式,它可以在生产环境中更快地执行,并且不需要Python依赖。

张量和梯度

PyTorch张量有内置的梯度计算和跟踪机制,因此你所需要做的就是将数据转换为张量,并使用torch提供的张量方法和函数执行计算。当然,如果要访问底层的详细信息,也是可以的,不过在大多数情况下PyTorch可以满足你的期望。

每个张量都有几个与梯度相关的属性:

  • grad:张量的梯度,与原张量形状相同。
  • is_leaf:如果该张量是由用户构造的,则为True;如果是函数转换的结果,则为False
  • requires_grad:如果此张量需要计算梯度,则为True。此属性是从叶张量继承而来,叶张量从张量构建过程(torch.zeros()torch.tensor()等)中获得此值。默认情况下,构造函数的requires_grad = False,如果要计算张量梯度,则需明确声明。

为了更清楚地展示梯度机制,我们来看下面的例子:

059-01

上面的代码创建了两个张量。第一个要求计算梯度,第二个则不需要。

059-02

因此,现在我们将两个向量逐个元素相加(向量[3, 3]),然后每个元素翻倍,再将它们求和。结果是零维张量,值为12。到目前依然很简单。现在我们来看表达式创建的底层图(见图3.3)。

060-01

图3.3 表达式的图形表示

如果查看张量的属性,会发现v1v2是仅有的叶节点,并且每个变量(v2除外)都需要计算梯度:

060-02

现在,让PyTroch来计算图中的梯度:

060-03

通过调用backward函数,PyTorch计算了v_res变量相对于图中变量的数值导数。换句话说,图中变量的变化会对v_res变量产生什么样的影响?在上面的例子中,v1的梯度值为2,这意味v1的任意元素增加1,v_res的值将增加2。

如前所述,PyTorch仅针对requires_grad = True的叶张量计算梯度。的确,如果查看v2的梯度,会发现v2没有梯度:

060-04

这样做主要是考虑计算和存储方面的效率。实际情况下,网络可以拥有数百万个优化参数,并需要对它们执行数百个中间操作。在梯度下降优化过程中,我们对任何中间矩阵乘法的梯度都不感兴趣。我们要在模型中调整的唯一参数,是与模型参数(权重)有关的损失的梯度。当然,如果你要计算输入数据的梯度(如果想生成一些对抗性示例来欺骗现有的NN或调整预训练的文本嵌入层,可能会很有用),可以简单地通过在张量创建时传递requires_grad = True来实现。

基本上,你现在已经拥有实现自己NN优化器所需的一切。本章的其余部分是关于额外的便捷函数的,提供NN结构中更高级的构建块、流行的优化算法以及常见的损失函数。但是,请不要忘记,你可以按照自己喜欢的任何方式轻松地重新实现所有功能。这就是PyTorch在DL研究人员中如此受欢迎的原因,因为它优雅且灵活。

007-03兼容性说明

支持张量的梯度计算是PyTorch 0.4.0版本的主要变化之一。在以前的版本中,图形跟踪和梯度计算是在非常轻量级的Variable类中分别完成的。它用作张量的包装器,自动保存了计算历史以便能够反向传播。该类仍存在于0.4.0中,但已过时,它将很快消失,因此新代码应避免使用。在我看来,这种变化是很好的,因为Variable的逻辑确实很简单,但是它仍然需要额外的代码,并且开发人员还需要注意包装和反包装张量。现在,梯度变成张量的内置属性,使得API更加整洁。