【Deep Learning with PyTorch 中文手册】(三)Execution Mode
Immediate versus Deferred Execution
深度学习库的关键区别在于是即时执行(Immediate Execution)还是延迟执行(Deferred Execution)。PyTorch的易用性很大程度上取决于它如何实现即时执行,因此我们在这里简要介绍一下。
首先看一个例子,如果需要执行勾股定理表达式,需要定义两个变量a和b,Python实现过程如下:
>>> a = 3
>>> b = 4
>>> c = (a**2 + b**2) ** 0.5
>>> c
5.0
像这样获取输入值(a、b)后得到输出值(c)的过程就是即时执行。PyTorch就像普通的Python程序一样,默认执行方式为即时执行(在PyTorch文档中称为Eager Mode)。即时执行非常有用,如果在执行某个表达式时出现错误,Python的解释器、调试器或者其他类似的工具都能够直接访问到相关的Python对象,并且在执行出错的地方会直接抛出异常。
现在讨论另一种情况,我们可以先行定义勾股定理表达式而不必关心具体的输入是什么,随后我们在获取到具体的输入时再使用该表达式得到输出。我们定义的函数可以根据不同的输入而随时调用。比如像这样:
>>> p = lambda a,b: (a**2 + b**2) ** 0.5
>>> p(1,2)
2.23606797749979
>>> p(3,4)
5.0
在第二个例子中,我们定义了一系列待执行的操作,并返回一个输出函数(p)。然而直到我们给定了具体的输入,这些操作才会执行。这个例子就是延迟执行。延迟执行意味着大多数异常是在函数调用时(而不是在函数定义时)抛出的。对于普通的Python程序(第二个例子)来说,这种实现方式很不错。这是因为在程序发生错误时,解释器和调试器能够完全访问到Python的状态。
然而,如果我们使用具有大量运算符重载的专用类,允许那些即时执行的操作推迟到后台运行,情况就会变得复杂棘手。这些专用类看上去可能长这个样子:(作者你又开始暗指某厂了 - -)
>>> a = InputParameterPlaceholder()
>>> b = InputParameterPlaceholder()
>>> c = (a**2 + b**2) ** 0.5
>>> callable(c)
True
>>> c(3, 4)
5.0
通常,在采用上述这种函数定义形式的库中,对a和b求平方、相加再取平方根的操作不会被记录为高级的Python字节码。相反,这些库更倾向于将表达式编译成静态计算图(包含基本操作的图),这种方式相比于纯Python代码的形式可能会有一些优势(例如出于性能原因将数学公式直接编译为机器代码)。
计算图在某处定义,却在别处执行,这会使得调试变得更加困难。因为异常通常会缺失关于错误的具体信息,并且Python的调试工具也看不到数据的中间状态。另外,静态图通常不能很好地与标准Python控制流融合:它们实际上是基于宿主语言(本例中是Python)而实现的针对特定领域的语言。
接下来,我们将更具体地研究即时执行和延迟执行之间的区别,特别是在与神经网络相关的情况下。在这里我们不会深入地探索这些概念,而是在宏观角度上给读者介绍一下这些概念中的术语及其关系。理解这些概念及其关系可以帮助我们理解像PyTorch这种使用即时执行的框架与使用延迟执行的框架之间的区别,尽管这两种类型框架的底层数学原理是相同的。
神经网络的基本组成单元是神经元。大量的神经元串在一起构成神经网络。
下图中第一行展示的是单个神经元典型的数学表达式:
o = tanh(w * x + b)
在我们解释下图中的执行模式前,有以下几点需要说明:
x 是单个神经元计算的输入。
w 和 b 是神经元的参数或者说是权重,根据需求它们的值可以被更新。
为了更新参数(生成更接近我们预期的输出),我们通过反向传播将误差分配给每个权重,然后相应地对每个权重进行调整。
反向传播需要计算输出相对于权重的梯度。
我们使用自动微分来自动计算梯度,节省了手工编写微分计算的麻烦。
在上图中,神经元被编译进一个符号图,其中每个节点表示一个独立的操作(第二行),并对输入和输出使用占位符。然后,在将具体数字放入占位符中后(在本例中,放入占位符中的数字是存储在变量w、x和b中的值),该计算图便进行数值运算(第三行)。输出结果相对于权重的梯度是通过自动微分构造的,该自动微分反向遍历计算图并在各个节点处乘以梯度(第四行)。第五行展示了相应的数学表达式。
TensorFlow是深度学习框架中主要的竞争对手之一(作者表示我不想暗指了,没错就是你,hiahia),采用类似延迟执行的静态图模式。静态图模式是TensorFlow 1.0中的默认执行模式。相比之下,PyTorch使用了一个按运行定义的动态图引擎,其中计算图是逐节点构建的,同时代码是即时执行的。
下图中的上半部分展示了在动态图引擎下的计算操作,这一部分和静态图模式是相同的。计算操作被分解成独立的表达式,当执行到这些表达式时,它们会即时运算。该程序对这些计算之间的内在联系没有预先的概念。下图中的下半部分展示了表达式的动态计算图的后台运行场景。表达式仍然被分解为独立的操作,但是这些操作会即时运算求值,进而促使动态图逐步建立。自动微分是通过反向遍历运算结果图来实现的,类似于静态计算图。注意,这并不意味着动态图自身就比静态图更有能力,只是动态图通常更容易完成循环或条件判断这些操作。
动态图在连续的前向传递过程中可能会发生变化。例如,可以根据前面节点的输出情况调用不同的节点,而不需要在图本身中表示出这些情况,这与静态图模式相比有明显的优势(有理有据,加十分)。
目前主要的一些框架都趋向于支持以上两种操作模式。PyTorch 1.0能够在静态计算图中记录模型的执行情况,或通过预编译的脚本语言对其进行定义,从而提高了性能并易于将模型部署到工业生产。TensorFlow也增加了"eager mode",一种新的按运行定义的API,它增加了TensorFlow库的灵活性。