3.2 梯度
即便对GPU的支持是透明的,如果没有“杀手锏”——梯度的自动计算——所有与张量有关的计算都将变得很复杂。这个功能最初是在Caffe工具库中实现的,然后成为DL库中约定俗成的标准。
手动计算梯度实现和调试起来都非常痛苦,即使是最简单的神经网络(Neural Network,NN)。你必须计算所有函数的导数,应用链式法则,然后计算结果,并祈祷计算准确。对于理解DL的具体细节来说,这可能是一个非常有用的练习,但你肯定不想一遍又一遍地在不同的NN架构中重复计算。
幸运的是,那些日子已经过去了,就像使用烙铁和真空管编写硬件程序一样,都过去了!现在,定义一个数百层的NN只需要从预先定义好的模块中组装即可,在一些极端情况下,也可以手动定义转换表达式。
所有的梯度都会仔细计算好,通过反向传播应用于网络。为了能够做到这一点,需要根据所使用的DL库来定义网络架构,它可能在细节上有所不同,但大体是相同的——就是必须定义好网络输入输出的顺序(见图3.2)。
图3.2 流经NN的数据和梯度
最根本的区别是如何计算梯度。有两种方法:
- 静态图:在这种方法中,需要提前定义计算,并且以后也不能更改。在进行任何计算之前,DL库将对图进行处理和优化。此模型在TensorFlow(<2的版本)、Theano和许多其他DL工具库中均已实现。
- 动态图:不需要预先精确地定义将要执行的图;只需要在实际数据上执行希望用于数据转换的操作。在此期间,库将记录执行的操作的顺序,当要求它计算梯度时,它将展开其操作历史,积累网络参数的梯度。这种方法也称为notebook gradient,它已在PyTorch、Chainer和一些其他库中实现。
两种方法各有优缺点。例如,静态图通常更快,因为所有的计算都可以转移到GPU,从而最小化数据传输开销。此外,在静态图中,库可以更自由地优化在图中执行计算的顺序,甚至可以删除图的某些部分。
另一方面,虽然动态图的计算开销较大,但它给了开发者更多的自由。例如,开发者可以说“对这部分数据,可以将这个网络应用两次,对另一部分数据,则使用一个完全不同的模型,并用批的均值修剪梯度。”动态图模型的另一个非常吸引人的优点是,它可以通过一种更Pythonic的方式自然地表达转换。最后,它只是一个有很多函数的Python库,所以只需调用它们,让库发挥作用就可以了。
兼容性说明
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
,如果要计算张量梯度,则需明确声明。
为了更清楚地展示梯度机制,我们来看下面的例子:
上面的代码创建了两个张量。第一个要求计算梯度,第二个则不需要。
因此,现在我们将两个向量逐个元素相加(向量[3, 3]),然后每个元素翻倍,再将它们求和。结果是零维张量,值为12。到目前依然很简单。现在我们来看表达式创建的底层图(见图3.3)。
图3.3 表达式的图形表示
如果查看张量的属性,会发现v1
和v2
是仅有的叶节点,并且每个变量(v2
除外)都需要计算梯度:
现在,让PyTroch来计算图中的梯度:
通过调用backward
函数,PyTorch计算了v_res
变量相对于图中变量的数值导数。换句话说,图中变量的变化会对v_res
变量产生什么样的影响?在上面的例子中,v1
的梯度值为2,这意味v1
的任意元素增加1,v_res
的值将增加2。
如前所述,PyTorch仅针对requires_grad = True
的叶张量计算梯度。的确,如果查看v2
的梯度,会发现v2
没有梯度:
这样做主要是考虑计算和存储方面的效率。实际情况下,网络可以拥有数百万个优化参数,并需要对它们执行数百个中间操作。在梯度下降优化过程中,我们对任何中间矩阵乘法的梯度都不感兴趣。我们要在模型中调整的唯一参数,是与模型参数(权重)有关的损失的梯度。当然,如果你要计算输入数据的梯度(如果想生成一些对抗性示例来欺骗现有的NN或调整预训练的文本嵌入层,可能会很有用),可以简单地通过在张量创建时传递requires_grad = True
来实现。
基本上,你现在已经拥有实现自己NN优化器所需的一切。本章的其余部分是关于额外的便捷函数的,提供NN结构中更高级的构建块、流行的优化算法以及常见的损失函数。但是,请不要忘记,你可以按照自己喜欢的任何方式轻松地重新实现所有功能。这就是PyTorch在DL研究人员中如此受欢迎的原因,因为它优雅且灵活。
兼容性说明
支持张量的梯度计算是PyTorch 0.4.0版本的主要变化之一。在以前的版本中,图形跟踪和梯度计算是在非常轻量级的Variable
类中分别完成的。它用作张量的包装器,自动保存了计算历史以便能够反向传播。该类仍存在于0.4.0中,但已过时,它将很快消失,因此新代码应避免使用。在我看来,这种变化是很好的,因为Variable
的逻辑确实很简单,但是它仍然需要额外的代码,并且开发人员还需要注意包装和反包装张量。现在,梯度变成张量的内置属性,使得API更加整洁。