PyTorch 自动微分论文翻译及解释

绪论

本文是对 发表在NIPS 2017 Workshop上面的名为 Automatic differentiation in PyTorch 的 预印论文的一部分的翻译。 只是因为感兴趣,想了解一下 PyTorch 自动求导这方面涉及到的不太深的数学和计算机科学的知识。翻译完成后,我所学习到的有这么几点:

  • PyTorch 自动微分抛弃了符号方法。PyTorch 是动态运行的,TensorFlow 和 Caffe 是静态运行的,静态运行的需要制定一个静态图和提前通过符号进行微分。
  • PyTorch 大部分使用 C++ 写的,而且正在将核心操作定义转移到 C++ 上。与其他框架相比,精心调优的 C++ 代码是 PyTorch 能够实现更低开销的主要原因之一。 将运算符迁移到 C++ 可以减少开销,并将从 Python 调度的单个可微操作的延迟降低到低至 3.4us,而张量运算为 1.7us。另一个好处是可以有多个线程并行执行它们(与 Python 相比,Python 由于 GIL 限制了并行性)。 在使用多个 GPU 的情况下,这一点尤为重要,因为单个 CPU 线程无法满足GPU 的需求。
  • PyTorch 支持原地操作,但是比较危险。

本文中涉及到的计算机科学及数学专业知识及其他的还有如下几点

  • 原地操作
  • 计算机程序中的求值策略,迫切求值还是懒惰求值
  • 自动求导,含正向累积与反向累积方法
  • 向量-雅可比 积
    本人在这篇文章做了这些点的初步详细阐述。

摘要

PyTorch——为了使在机器学习模型领域快速研究成为可能而诞生的一个库(library)。在本文中,我们描述来自 PyTorch 的一个自动微分automatic differentiation)的模块(module)。它建立在一些项目的基础上,最值得注意地, Lua Torch, Chainer 和HIPS Autograd [4]。它对在不同设备(CPU 和 GPU)上运行的模型的自动微分提供了容易访问的高性能环境。为了简化原型制作,PyTorch 没有遵循在其他许多深度学习框架所使用的符号方法(symbolic approach) ,而是专注于纯指令式程序,重点是可拓展性和低开销。请注意,这个预印本是即将发表的一篇涵盖所有 PyTorch 特性的论文中某些部分的草稿。

1 背景

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),它引入了一些新的设计和实现选择,这使得它成为在自动微分库中支持这种动态的迫切运行 中是最快的实现:

  • 原地操作。原地操作对自动微分构成危险,因为原地操作可以让在微分阶段需要的数据无效。另外,它们需要执行非平凡的 tape 变换。PyTorch 利用简单有效的机制,可以解决这些问题。本文在 板块 3.1详细描述。
  • 无 tape。传统的 反向模式(reverse-mode) 微分 记录 tape(也被称之为 Wengert 列表),用来描述操作最初被执行的顺序;这种优化允许实现避免拓扑排序。PyTorch(和 Chainer)避开了 tape。 相反地,每个 intermediate 结果只记录与它们的计算相关的计算图的子图。这意味着 PyTorch 用户可以在任何他们喜欢的线程中混合和匹配独立的图(没有显式的同步)。以这种方式构造图的另一个好处是,当图的一部分死时,它会自动释放; 当我们想要尽可能快地释放大内存块时,一个重要的考虑因素。
  • 核心逻辑是用 C++ 的PyTorch 在最初是一个 Python 库;然而,很快发现,解释器开销对核心 AD 逻辑太高了。如今,大部分使用 C++ 写的,而且我们正在将核心操作定义转移到 C++ 上。与其他框架相比,精心调优的 C++ 代码是PyTorch能够实现更低开销的主要原因之一。

PyTorch 自动微分论文翻译及解释_第1张图片

图1. 一个使用 PyTorch 的自动求导模块的例子(torch.autograd)

2 接口

图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 标志 。当导数不需要被计算时,它们应该不被计算,这很重要。继 Chainer 之后,PyTorch 提供了两个工具来将子图从导数计算中排除 :“requires grad” 和 “volatile” 标志。 这些标志遵循以下规则: 如果任何一个输入变量是 volatile,那么输出也是 volatile。 否则,如果任何输入变量需要 grad, 则输出也需要 grad。 此外,设置 volatile 意味着没有设置 “require grad”。如果一个操作不需要 grad,派生闭包甚至不会被实例化。Volatile 标志可以很容易地禁用微分(例如,当一个模型执行推断时,用户不一定要在每个参数将 “requires grad”设置为 false)
  • 钩子(Hooks)。 自动微分的一个缺点是微分对用户是相对不透明的:不像被用户写的 Python 代码调用的前向传播(forward pass),微分是从三方库代码实现的,用户对这第三方库代码有很小的可视性。为了允许用户检查梯度,当函数的后向(backwards)被调用:x.register_hoook(lambda grad: print(grad)), 我们提供钩子机制来允许用户观察。这条代码在 x 上注册了一个钩子,这打印 x 的 梯度无论何时它被计算。钩子回调(callback)也可以返回新的梯度,它被用来替换原先的梯度;这个能力被证实在元学习和增强学习是有用的。
  • 拓展。 PyTorch 用户可以创造传统的微分操作,通过用 Python 指定一对前向和后向函数。前向函数计算操作,而后向函数拓展 向量-雅可比 积。这可以被用来制作任意的 Python 库。(例如,Scipy[3] 微分(关键地学习了 PyTorch 的 零-拷贝 NumPy 传统))。

3 实现

Variable 是简单地对 Tensor 的打包,它可以对 Function 对象的图有一个引用。此图是被计算的函数的不变的,纯粹的功能性的表示;Variables 只是对此图的可变指针(当原地操作发生,它们是可变的;见 板块 3.1

Function 可以被认为是拥有所有对计算 向量-雅可比 必要的闭包。它们接受输出 的梯度,返回输入 的梯度(正式地,左边的积包括了对它们各自操作的项。)Functions 的图是单参数闭包,它吸收一个左积和通过与它包括的所有操作的导数相乘。传递的左积本身就是 Variables,使得图的求值是可微的。

  • 内存管理。PyTorch 的主要用例是在 GPU 上训练机器学习模型。由于 GPU 的最大限制之一是内存容量不足PyTorch非常小心地确保一旦不需要中间值时就释放它们。 实际上,Python非常适合这个目的,因为默认情况下它是引用计数的 (使用垃圾收集器(garbage collector)只是为了打破循环)。

    PyTorchVariableFunction 必须设计成在引用计数的情况下工作良好。 例如,一个 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 的需求。

3.1 对原地操作的支持

通常,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)=1tanh2(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.

你可能感兴趣的:(PyTorch,pytorch,人工智能,python)