正向/反向自差分包
Dotua的Python项目详细描述
Dotua文档
尼克·斯特恩、文森特·维戈、夏媛、扎克·韦尔文
简介
根据美国数学家迈克尔斯皮瓦克在其著名的教科书中所说,微积分从根本上讲是对"无穷小变化"的研究。对这些微小变化的研究是对变化关系的研究,而不是对变化本身的计算。当导数接近0时,它是一个点的极限的函数,我们关心的是变化的关系,而不是变化本身的计算。
导数的一个非常重要的应用是各种优化问题。机器能够通过计算导数迭代地遍历梯度。然而,在机器学习的应用中,一个给定的神经网络可能有数百万个参数,这意味着要进行解析计算的导数的组合数目是非常繁重的。数值牛顿方法(通过对极限的猜测迭代计算)同样不是一个明智的选择,因为即使对于"非常小的" ,最终结果可能是相对于机器精度的数量级误差。
因此,人们可能会认为,在ML的职业生涯需要广泛的微积分背景,但是,瑞安P亚当斯,以前的Twitter(和哈佛IACS),现在的普林斯顿CS,描述自动差异化为"摆脱妨碍解决[ml]问题的数学。"我们最终关心的是调整机器学习算法的超参数,因此,如果我们能让机器为我们这样做,那就是我们最终关心的。在这个软件包中实现的是自动微分,它允许我们计算复杂函数的导数以达到机器精度,而无需数学运算。
自微分的一个重要应用是神经网络中的反向传播。反向传播是相对于损失函数优化权重的过程。这些步骤通过一个简单的神经网络示例加以说明。
背景
自动微分最重要的微积分导数规则是多元链式规则。
基本链式规则规定,函数组成的导数为:
也就是说,导数是应用于内部函数的外部函数增量变化乘以内部函数变化的函数。
在多元情况下,我们可以应用链式法则和全微分法则。例如,如果我们有一个简单的方程:
那么,
偏导数:
IMG SRC=https://warehouse camo.cmh1.psfhosted.org/da8588130e5c49c36ac91b506d5f1fbf74e895/687470733a2f2f6c617465782e636f6465636f67732e636f6d2f706e672e676c617465783f5c6672261637b5c7061727469616c26727469616c266737b5c70616737061163653b797d7b5c706169616c26763653b757d267706163653b36d26737061163653b76" />这个简单的例子说明了多元函数的导数最终是偏导数的加法和它的分量变量的计算。如果机器可以计算任何给定的子函数以及任何子函数之间的偏导数,那么机器只需要将函数及其导数的乘积相加即可计算出总导数。
理解自动微分的一种直观方法是将任何复杂的函数看作最终的复合函数图。每个节点都是一个原始操作,其中的导数是rea。众所周知,这个图的边,任何两个变量之间的变化关系,都是偏导数。因此,任意两个节点之间的路径之和就是这两个函数之间的偏导数(这是通过链式规则对总导数的图形重述)。
前进模式
因此,正向模式自动微分从图形的输入开始,并对源路径求和。下面的图表(来自christopher olah的博客)提供了这个过程的直觉。三个变量(x,y,z)之间的关系由多个路径定义()。前进模式从1的种子开始,然后在每个节点中,导数是前面步骤总和的乘积。
因此,只要每个节点内都有一个基本函数,机器就可以通过计算图跟踪导数。
最后一个难题是:通过将每个实数重新命名为,其中。在一台机器中,符号计算很快就会变得无法计算,因为在微积分规则的连续展开中,机器必须在内存中保存变量及其导数。对偶数允许我们追踪一个甚至是一个复杂函数的导数,作为一种既能追踪导数又能追踪原始数的数据结构。
在我们的链式规则方程中,有两个部分需要计算:应用于内函数的外函数的导数和该值乘以内函数的导数。这意味着一个极其复杂的函数的完整符号表示可以增长成指数级的许多项。然而,对偶数允许我们解析可以解析计算的位化片段中的符号表示。原因是函数的泰勒级数展开:
当评估时,假设,然后所有更高阶的术语都会退出(它们是0),剩下一个带有
倒车模式
逆模自微分的一个直觉是将我们的链式规则看作图的概念。
一个直观的动机(h/trufflewind)是将reverse-mode看作链式规则的反转。
在这个符号中,某个输出变量w到某个变量t的导数是w连接到的每个u的导数的线性组合。
通过翻转分子和分母,这是新参数s相对于输入变量u的偏导数的表示法。
图论直觉也许是最直接的:就像机器在一系列步骤中计算一个方程一样,逆向模式是计算图的逆向遍历。在神经网络中,我们希望相对于某个损失函数最小化权重,这使得我们能够有效地回溯我们的路径,而无需重新评估可能是一个极其复杂的网络的权重。
概括地说:自动微分是一种计算复杂导数的算法,它将这些函数解析为要遍历的图结构。对偶数被用作一种数学数据结构,它允许机器解析地计算任意给定节点的导数。它优于解析或符号微分,因为它实际上是计算f在现代机器上是可行的!而且它比数值方法优越,因为自动微分更精确(它达到机器精度)。因此,它对于中性网络上的反向传播等应用非常有用。
如何使用dotua
如何安装
要安装我们的软件包,只需像这样使用pip install即可:
$ pip install Dotua
导入和使用示例
前进模式
为了从我们的包中实例化前向模式自动差异化对象,用户应首先从dotua库导入autodiff函数,如下所示:
fromDotua.autodiffimportAutoDiffasad
用户的一般工作流程如下:
- 将所有变量实例化为autodiff对象。
- 将这些变量输入到 dotua库创建更复杂的表达式来传播派生 使用前进模式自动区分。
autodiff类是函数中所有变量的核心构造函数 这需要区别对待。实例化有两个选项 变量:标量和矢量,分别由create_scalar()和create_vector()生成。标量变量 变量,而向量变量可以有多个关联值。这个 用户如何实例化autodiff对象的一般示意图是 概述如下:
- 创建标量或矢量autodiff对象以生成种子 变量,以便以后构建要区分的函数。初始化 工作方式如下:
x,y=ad.create_scalar(vals=[1,2])z=ad.create_vector(vals=[1,2,3])
- 接下来,用户应导入运算符类并传入这些 变量转换成初等函数如下:
fromDotua.operatorimportOperatorasopresult=op.sin(x*y)results=op.sin(z)
简单的运算符,如和和和积,可以正常使用:
result=6*xresults=z+4
- 最后,(作为前一示例的延续),用户可以使用eval()和partial()方法访问函数的值和导数:
print(result.eval())# 6print(result.partial(x))# 6print(result.partial(y))# 0
对于标量变量,result.eval()将返回函数的值, 而result.partial(v)将返回任何变量的偏导数,v。对于向量变量,results.eval()返回元组列表 (值,jacobian),向量中每个函数有一个元组。jacobian是一本字典,它表示 该元素相对于矢量中所有元素的导数。
倒车模式
反向模式变量的初始化与正向模式非常相似。 唯一的区别是模块名前面有一个"r"。另外,对于反向模式的初始化 变量,用户必须实例化初始化器对象。这不同 可以使用静态方法初始化的前向模式变量。 可以按如下方式初始化反向模式标量对象:
fromDotua.rautodiffimportrAutoDiffrad=rAutoDiff()x,y,z=rad.create_rscalar([1,2,3])
在反向模式下,当用户调用渐变函数时,必须指定 他们想要区分的变量。这一次, 梯度函数只返回一个数值常量。下面是一个例子 以下:
f=x+y+zf_gradx=rad.partial(f,x)# f_gradx = 1
下面的代码展示了用户如何与rvector交互。注意,rvector操作 不同的是在反向模式下,因为它主要是允许计算rscalar的扩展 值向量的函数。
v=rad.create_rvector([1,2,3])g=2*vg_grad=rad.partial(g,v)# g_grad = [2, 2, 2]
示例
dotua包的顶层目录中有几个文件演示了包的用法。
第一个文件是一个交互式的jupyter笔记本,其中包含一个使用示例 dotua包执行良好的情况,即逼近函数根的newton-raphson方法。这个便条ok名为"newton_demo.ipynb",位于包的顶层目录中的"examples"文件夹中。为了方便起见,此演示的输出将在此处复制:
第二个文件是一个例子,说明如何使用我们的反向模式自微分包在神经网络中进行反向传播。文件名为"neuralnet_demo.ipynb"
对于某些输出y_hat和一组输入x,神经网络的任务是找到一个最小化损失函数的函数,如mse:
其中n是数据点的个数,f_i是模型返回的值,y_i是给定观测值i下y的真值。一个"密集"神经网络将涉及每个参数和这些边之间的连接,每个边都包含一个权重值。任务是找到使y与y之间的距离最小化的权重。由于每个节点彼此连接,这会捕获非线性,因为某些参数可能比其他参数更具表现力。但是,这也会带来计算负担。
向后自动微分是遍历图形并重新调整这些权重的特别有用的方法。
这里我们实现了一个只有一个隐藏层的玩具神经网络。利用R.A.Fischer著名的鸢尾花数据集——一组对花卉及其各自物种的测量数据——我们根据花卉的测量数据来预测物种。正如第一个散点图所示,刚毛植物和另外两个物种,花斑羚和维吉尼亚之间有一个清晰的分离界限。捕捉每个物种之间的边界需要更多的复杂性,因此我们简单的神经网络只能捕捉最大的边界——这是我们使用反向自微分来拟合适当梯度的结果。这不是一个神经网络包,而是一个自动微分包,这只是一个非常有用的应用程序的说明。
软件组织
目录结构
我们的项目将遵循python打包文档中概述的目录结构。在较高层次上,该项目具有以下结构:
$ pip install Dotua0
模块
dotua/
dotua模块包含正向模式实现和反向模式实现的代码。
它包含autodiff(autodiff.py),它是前向模式自微分的驱动程序。驱动程序帮助用户访问节点文件中的节点超类(node.py)和相关子类(即向量(vector.py)和标量(scalar.py))以及运算符类(operator.py)。
它还包含rautodiff(rautodiff.py),它是反向模式自微分的驱动程序。驱动程序帮助用户访问nodes文件和roperator类(roperator.py)中的rscalar类(rscalar.py)和rvector类(rvector.py)。
示例/
examples模块有python文件和库的文档化用例。例子包括牛顿逼近非线性函数根的方法的实现和计算局部极值的模块,以及neu的实现。用于预测问题的RAL网络。
测试/
测试模块包含项目的测试套件,并根据自动测试发现的pytest要求格式化。
测试
概述
dotua测试套件中的大多数测试都由单元测试组成。 目的是用完整的单元验证应用程序的正确性 测试前进和后退模式的所有简单用法 自动区分。本质上,这涉及到验证我们的应用程序为所有基本函数生成正确的计算(求值和求导)。此外,一系列更复杂的单元测试涵盖了高级场景,如具有多维域和共域的函数。 (用于前向模式)以及由 初等函数的组成。
测试自动化
Dotua通过travis ci使用持续集成测试来执行 自动化,独立于机器的测试。此外,Dotua还使用工作服。 验证测试套件的高代码覆盖率(目前为100%)。 travis ci和工作服徽章嵌入到项目自述文件中,为通过github与我们的项目交互的用户提供透明度。
安装
要安装我们的软件包,您可以简单地使用pip install,就像这样:
$ pip install Dotua
用户验证
整个dotua测试套件都包含在项目发行版中。因此, 用户可以在 安装Dotua软件包。
分布
许可
dotua是在gnu gplv3许可下发布的,允许"按原样"使用 同时要求所有扩展保持开源。
实施
Dotua库的目的是执行自动差异 在用户定义的函数中,域和余域可以是单个的或 多维(n.b.这个库为两个forward提供支持 和自动差分的反转模式,但仅适用于反转模式 支持具有一维共域的函数)。在高层, dotua是numpy的部分替代品 dotua提供了许多数学函数的方法 (如三角、反三角、双曲线等) 实现;但是,虽然这些方法的numpy版本只能提供函数求值,但dotua等价物同时提供求值和求导。
为了实现这一点,dotua库实现了以下抽象 想法:
- 通过提供 我们自己的二进制和一元运算符版本。
- 转发广告:跟踪用户定义的值和导数 表达式和函数。
- 逆向ad:从用户定义函数构造计算图 可用于快速计算梯度。
考虑到这些目标,dotua前进模式的实现依赖于 节点模块和运算符类并允许用户界面 通过autodiff类,该类充当节点工厂,用于初始化标量和向量的实例。类似地,dotua反转模式 实现依赖于节点模块和属性器类 并通过充当 初始化rscalar实例的工厂
节点
节点模块包含具有以下基本设计的节点超类:
$ pip install Dotua2
从本质上讲,节点类的作用(在抽象意义上是指 表示计算图und中的节点二进模式用户定义表达式的自动区分)用作两个 节点包中的其他类:标量,向量,rscalar,rvector。这些子类中的每一个子类都实现标量所需的运算符重载。 和向量函数(即加法、乘法、减法、除法、幂等)。这个逻辑被分成四个不同的类 为更高维度的功能提供更多的组织,并允许 使用标量和向量的特定属性假设的类方法 以降低实现的复杂性。
标量类和向量类都具有和类属性,通过跟踪 每个节点的值和导数。rscalar类和rvector类都具有和类属性,这些属性允许 通过存储计算图和中间梯度值来反转自微分。
标量
标量类用于用户定义的一维变量。 具体来说,用户可以定义标量变量的函数(即函数 在多个标量变量上用一维共域定义 同时计算函数值的标量实例 在预先选择的评估点使用正向模式进行一阶导数 自动区分。标量类的对象用 存储在类属性self中的值(即求值点)(注意,正如单下划线所示,该属性应该 不能被用户直接访问或修改)。另外,标量 对象-可以是单个标量变量或 标量变量–在类属性self中跟踪它们自己的导数。这个导数被实现为一个字典,其中标量对象作为键,实数作为值。注意,每个标量对象的_jacobian属性都有一个用于所有标量变量的条目,其中 对象可能与交互(有关详细信息,请参阅autodiff初始值设定项部分 信息)。
用户通过两种方式与标量对象进行交互:
- eval(self):此方法允许用户获取值 当用户第一次 初始化它们的标量对象。具体来说,该方法返回floatself.\u val
- 部分(self,var):此方法允许用户获取部分 给定标量对象相对于var的导数。如果自我 是由用户直接初始化的标量对象之一(请参阅autodiff初始化项部分),如果var==self和0 否则。如果self是由 scalar对象(例如,self=scalar(1)+scalar(2)),则此方法返回正确的部分 自相对于var的导数
注意,这些是用户应该调用标量的唯一方法。 对象,并且用户不应该直接访问对象的 类属性。
标量对象支持左侧和右侧的加法、减法, 乘法、除法、求幂和求反。
rscalar
rscalar类是 标量类。也就是说,rscalar用于用户定义 一元用户可以定义标量函数的变量(即, 在一维多个标量变量上定义的函数 (CODOMAIN)以便计算函数值并随后轻松确定 函数的梯度反转模式自动微分(参见 有关更多详细信息,请参阅rautodiff初始值设定项部分)。rscalar 类是用一个值(即求值点)初始化的,该值是 存储在类属性中 单下划线表示,不应直接访问此属性或 由用户修改)。此外,rscalar对象(可以是单个标量变量或标量变量的表达式)显式地 构造用于类中自动微分的计算图 属性self.roots()。此属性表示计算图 作为一个字典,其中键表示子项,值表示子项的派生项 关于rscalarself。这本词典 当用户 使用rscalar对象定义函数。
用户以一种方式与rscalar对象交互:
- eval(self):此方法允许用户获取值 对于用户第一次定义的计算点处的rscalar对象 初始化rscalar对象。具体来说,此方法返回值 属性的值。
注意,这是用户应该调用的唯一方法rscalar 对象,并且用户不应该直接访问对象的 类属性。当rscalar类包含gradient()方法时, 此方法仅供内部使用。用户应该只获取 通过 在初始化器类中提供的partial()方法(参见rautodiff 初始化项部分了解更多详细信息)。
rscalar对象支持左侧和右侧的加减法, 乘法、除法、求幂和求反。
矢量
vector是node的一个子类。每个向量变量由一个一维numpy数组存储值和一个二维numpy数组存储jacobian矩阵组成。 用户可以使用索引访问向量实例中的特定元素。同一向量实例中的元素之间的运算和向量之间的运算是通过重载类的运算符来实现的。
autodiff初始值设定项
autodiff类作为节点工厂运行,允许用户初始化 用于构造任意函数的变量。因为节点 类仅用作标量和向量类的接口,用户 不应直接实例化节点类的对象。因此,我们 按以下方式定义autodiff类,以允许用户初始化 标量和向量变量:
$ pip install Dotua3
使用create_scalar和create_vector方法,用户可以 初始化用于构造任意函数的变量。另外, 用户可以为这些变量指定初始值。创建变量 这样将确保用户能够使用定义的dotua 用于计算函数及其导数的运算符。
可变宇宙
autodiff库的实现做出以下假设: 对于用户使用自导变量的每个环境 (即标量和矢量对象),用户初始化所有这些变量 只需调用一次create_scalar或create_vector这个假设 允许标量和矢量对象在 被用户使用。这大大降低了实现的复杂性。
这种设计选择不应限制用户在其构造中的任意性 函数,因为要定义函数,用户必须 知道他们需要预先使用多少原始标量变量。实现 这并不意味着用户不能定义新的python 变量作为先前创建的标量对象的函数,但只有 用户在定义数学函数时必须初始化 x、y和z只需调用一次"创建标量"。它是完美的 在f(x,y,z)的定义中,python变量如 a=x+y被创建。用户保证a.eval()和 a.partial(x)、a.partial(y)和a.partial(z)都定义良好且正确,因为在本例中a是scalar的实例;但是,它不是 "原始"标量变量,因此用户不能接受部分 关于a的导数
rautodiff初始值设定项
rautodiff类作为rscalar工厂运行,允许用户 为了构造任意函数而初始化变量 他们想以后用自动反转模式来确定导数 区别。因为同样的rscalar变量可以用来定义 多个函数,用户必须实例化一个rautodiff对象来管理 他们创建并计算不同 相同变量的函数。因此,我们用以下方式定义rautodiff类:
$ pip install Dotua4
通过实例化一个rautodiff对象并使用create\u rscalar方法, 用户可以初始化变量以用于构造任意 功能。此外,用户还可以为这些 变量。以这种方式创建变量将确保用户能够 使用dotua定义的运算符对函数求值并计算其 衍生品。此外,使用partial方法,用户可以 确定其构造函数相对于 指定的rscalar变量。
操作员
运算符类定义初等数学的静态方法 函数和运算符(特别是那些不能在 标量和向量类),用户可以在构造任意函数时调用它们。运算符类将导入nodes模块,以便 根据需要返回新的标量或向量变量。设计 运算符类如下:
$ pip install Dotua5
对于运算符类中定义的每个方法,我们的实现使用 ducktyping返回必要的对象。如果用户传递标量对象 对于其中一个方法,会向用户返回一个新的标量对象 具有正确的值和雅可比。如果用户传递一个向量对象 对于其中一个方法,会向用户返回一个新的向量对象 具有正确的值和雅可比。另一方面,如果用户通过 一个python数值类型,然后该方法返回 给定参数的相应numpy方法 (例如,操作sin(1)=np.sin(1))。
转子
类似地,roperator类定义基本数学的静态方法 函数和运算符(特别是那些不能在 rscalar类),可由用户在构造任意函数时调用。设计 roperator类如下:
$ pip install Dotua6 <P>同样,对于在ropertor类中定义的每个方法,我们的实现使用 ducktyping返回必要的对象。如果用户传递rscalar对象 对于其中一个方法,则会向用户返回一个新的rscalar对象 具有正确的值和自我/子链接。如果用户通过一个rvector对象 对于其中一个方法,则会向用户返回一个新的rvector对象 具有正确的值和父/子链接。另一方面,如果用户通过 一个python数值类型,然后该方法返回 给定参数的相应numpy方法 (例如,rop.sin(1)=np.sin(1))。
关于比较的说明
需要注意的是,dotua库故意不重载 变量类的比较运算符(即,标量,rscalar, 向量,和rvector)。用户应该只使用相等和不相等运算符==和!= 以确定对象等价性。对于希望执行比较的用户 由标量,rscalar组成的函数或变量的值, 向量,或具有相同类型的函数或变量值的变量, 他们可以通过使用eval()函数访问值来完成此操作。
外部依赖性
Dotua将对第三方库的依赖限制为 最小值。因此,唯一的外部依赖是numpy和scipy numpy在库中用于数学计算(例如三角函数)。 scipy在newton-raphson演示中用作比较。