本文是对 发表在NIPS 2017 Workshop上面的名为 Automatic differentiation in PyTorch 的 预印论文的一部分的翻译。 只是因为感兴趣,想了解一下 PyTorch 自动求导这方面涉及到的不太深的数学和计算机科学的知识。翻译完成后,我所学习到的有这么几点:
本文中涉及到的计算机科学及数学专业知识及其他的还有如下几点
PyTorch——为了使在机器学习模型领域快速研究成为可能而诞生的一个库(library)。在本文中,我们描述来自 PyTorch 的一个自动微分(automatic differentiation)的模块(module)。它建立在一些项目的基础上,最值得注意地, Lua Torch, Chainer 和HIPS Autograd [4]。它对在不同设备(CPU 和 GPU)上运行的模型的自动微分提供了容易访问的高性能环境。为了简化原型制作,PyTorch 没有遵循在其他许多深度学习框架所使用的符号方法(symbolic approach) ,而是专注于纯指令式程序,重点是可拓展性和低开销。请注意,这个预印本是即将发表的一篇涵盖所有 PyTorch 特性的论文中某些部分的草稿。
PyTorch,如大多数其他深度学习库,支持标量函数(或者向量值函数的 向量-雅可比 积)的 reverse-mode [6] 自动微分 [2]。针对深度学习应用,这是最重要的自动微分的形式,它经常对一个标量损失(loss)微分。
在这个领域,PyTorch 对自动微分的支持跟着 Chainer,HIPS autograd [4] 和 twitter-autograd(twitter-autograd 是,它自己,HIPS autograd 对 Lua 的一个端口)。这些库有两个明显特征:
动态的,define-by-run 运行。动态框架通过运行所需的计算来简单地定义要微分的函数,而不是指定一个静态图(graph)结构,该结构提前通过符号进行微分,然后多次运行。这允许用户使用任意他们想要的宿主语言(host-language)的特性(例如 任意控制流建构),以每次重新微分作为代价。动态运行使得 PyTorch 和 诸如 TensorFlow [1], Caffe 等的静态框架区分开。
立即,迫切执行(Immediate,eager execution)。当遇见张量计算的时候,迫切执行的框架会运行它;这避免了实现 “前向图”(“forward graph”),只记录微分那个计算所需的必要信息。这和 DyNet[5]是相反的,采用 lazy evaluation,DyNet每次训练迭代都要重构前向和后向图(forwards and backwards graph)(DyNet 确实有一个用于调试的立即执行模式,但是默认情况下不启用)。立即执行 允许 CPU 和 GPU 的计算流水线化,但是放弃了全局网络优化和批量处理的机会。
尽管 PyTorch 对自动微分的支持很大程度上受到先驱的启发(尤其是 twitter-autograd 和 Chainer),它引入了一些新的设计和实现选择,这使得它成为在自动微分库中支持这种动态的迫切运行 中是最快的实现:
图1 提供了一个 PyTorch 自动微分的简单例子。你写代码的时候就好像你在直接执行张量运算; 然而,不是对操作Tensors(PyTorch中相当于 Numpy中的 nd-arrays), 用户处理的是 Variables。它存储对 AD 必要的额外的元数据。Variables 支持 backward()方法,该方法计算在计算这个量时涉及的所有输入 Variables 的梯度。
默认情况下,这些梯度在输入变量的梯度字段中累积,这是继承自 Chainer 的设计。然而,PyTorch也为计算梯度提供了一个 HIPS 自动梯度风格的函数接口: 函数 torch.autograd.grad(f(x, y, z), (x, y)) 只计算 f 关于x和y的导数(z不计算梯度)。 相反,它返回一个元组,其中包含关于调用中请求的每个输入的梯度
Variable 是简单地对 Tensor 的打包,它可以对 Function 对象的图有一个引用。此图是被计算的函数的不变的,纯粹的功能性的表示;Variables 只是对此图的可变指针(当原地操作发生,它们是可变的;见 板块 3.1)
Function 可以被认为是拥有所有对计算 向量-雅可比 积必要的闭包。它们接受输出 的梯度,返回输入 的梯度(正式地,左边的积包括了对它们各自操作的项。)Functions 的图是单参数闭包,它吸收一个左积和通过与它包括的所有操作的导数相乘。传递的左积本身就是 Variables,使得图的求值是可微的。
内存管理。PyTorch 的主要用例是在 GPU 上训练机器学习模型。由于 GPU 的最大限制之一是内存容量不足,PyTorch非常小心地确保一旦不需要中间值时就释放它们。 实际上,Python非常适合这个目的,因为默认情况下它是引用计数的 (使用垃圾收集器(garbage collector)只是为了打破循环)。
PyTorch 的 Variable 和 Function 必须设计成在引用计数的情况下工作良好。 例如,一个 Function 记录了使用它的结果的 Function 的指针,所以当一个 Function 子图保留的输出 Variable 死掉时,它将被释放。 这与传统的闭包所有权(ownership for closures)相反,在传统的闭包所有权中,闭包保留它所调用的闭包(一个指向产生其结果的 Function 的指针)。
另一个挑战是避免引用循环。 自动微分的简单实现可以很容易地引入这样的循环(例如,当一个可微函数想要保存对其输出的引用时)。 PyTorch不是记录一个完整的变量,而是记录一个“保存的变量”,在这种情况下,它省略了一个指向 Function 的指针。
C++ 操作符。我们发现,即使可以使用扩展 API 在 Python 中表达所有操作,它们也会受到解释器开销的影响。 将运算符迁移到 C++ 可以减少开销,并将从 Python 调度的单个可微操作的延迟降低到低至 3.4us,而张量运算为 1.7us。另一个好处是可以有多个线程并行执行它们(与 Python 相比,Python 由于 GIL 限制了并行性)。 在使用多个 GPU 的情况下,这一点尤为重要,因为单个 CPU 线程无法满足GPU 的需求。
通常,PyTorch 的用户希望在张量上就地执行操作,以避免在已知不必要时分配新的张量。 直观地说,就地操作等价于其对应的非就地操作,只是就地修改的 Variable 将其计算历史“重新定位”以指向就地操作符的导数,而不是其先前的 Function(计算历史始终保持纯粹的功能)。 然而,这些就地操作以一种微妙的方式与 autograd 交互。
失效。就地操作可以使计算导数所需的数据无效。考虑以下例子:
y = x.tanh()
y.add_(3)
y.backward()
这个程序演示 PyTorch 对 y 执行原地操作; 然而,如果 y (其值为 tanh(x) )被保存,以便可以用于后向计算(回想一下 t a n h ′ ( x ) = 1 − t a n h 2 ( x ) tanh^{'}(x)=1-tanh^{2}(x) tanh′(x)=1−tanh2(x),那么这是不合理的。
在保存 y 时复制它会效率低下,PyTorch 在区分这个程序时反而会在运行时失败。 变量的每个底层存储都与版本计数器相关联,该计数器跟踪已对存储应用了多少就地操作。 当一个变量被保存时,我们记录当时的版本计数器。 尝试使用保存的变量时,如果保存的值与当前值不匹配,则会引发错误。
别名。PyTorch 支持变量之间的非平凡别名; transpose 和 narrow 等操作会产生具有新大小和步幅的新张量,这些张量与原始张量共享存储。 别名的问题在于它可能需要对许多变量的计算历史进行非平凡的转换。 考虑以下示例:
y = x[:2]
x.add_(3)
y.backward()
通常,对 x 的就地操作只会影响 x 的计算历史。 但是,在这种情况下,对 x 的就地添加也会导致 y 的某些元素被更新; 因此,y 的计算历史也发生了变化。 支持这种情况是相当重要的,所以 PyTorch 拒绝了这个程序,使用版本计数器中的一个附加字段(参见前一个失效段落)来确定数据是共享的。
在未来的工作中,我们正在寻求放宽这一限制。 挑战在于一个变量可能有任意多个别名,因此逐个遍历并更新它们的计算历史是不可行的。 但是,可以延迟初始化计算历史,仅在计算产生的结果与原始变量没有别名时才将其具体化。
[1] M. Abadi, A. Agarwal, P. Barham, et al. TensorFlow: Large-scale machine learning on heterogeneous systems, 2015. Software available from tensorflow.org.
[2] A. Griewank. Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation. Society for Industrial and Applied Mathematics, Philadelphia, PA, USA, 2000.
[3] E. Jones, T. Oliphant, P. Peterson, et al. SciPy: Open source scientific tools for Python, 2001.
[4] D. Maclaurin. Modeling, Inference and Optimization with Composable Differentiable Procedures. PhD thesis, 2016.
[5] G. Neubig, C. Dyer, Y. Goldberg, and o. Matthews. DyNet: The Dynamic Neural Network Toolkit. ArXiv e-prints, Jan. 2017.
[6] B. Speelpenning. Compiling Fast Partial Derivatives of Functions Given by Algorithms. PhD thesis, Champaign, IL, USA, 1980. AAI8017989.