在本节中,我们更具体地描述了静态声明(§2.1)和动态声明(§2.2)的两种范式。

3.范式编码

3.1 编码范式概述

从用户的角度来看,使用 DyNet 编写程序的目的是创建对应于需要被执行的计算的表达式()。这首先从基本的表达式开始,基本表达式通常是常量输入值或模型参数()。然后,通过进行运算()从其他表达式进一步构建复合表达式,并且运算链(chain of )隐含地为所需的计算定义一个计算图()。该计算图表示了符号计算,并且计算的结果是被动的:仅当用户显式地请求它时(在该点触发「前向()」计算)才执行计算。评估标量(即损失值)的表达式也可以用于触发「后向」计算,其以参数为依据来计算计算的梯度。参数和梯度被保存在模型(Model)对象中,训练器()用于根据梯度和更新规则来更新参数。

我们下面将简要地介绍这些每种组件:

和 : 是表示诸如权重矩阵和偏置向量之类的实数向量、矩阵或张量。 是我们想要查找的参数向量集,例如词嵌入(word )。换句话说,如果我们有一个词汇集 V,我们想要查找其嵌入(),那么就有一个 对象定义一个 | V | ×d 矩阵,其作为一个嵌入矩阵与 0,…,| V | -1 到 d 维向量的项形成映射。 和 被存储在模型中,并可以跨越训练样本(即跨不同的 样本)进行保存。

模型(Model):模型是 和 的集合。用户通过从模型中请求 来获取它们。然后模型会跟踪这些参数(及其梯度)。模型可以保存到磁盘中也可以通过磁盘加载,也可以被下面要讲到的 对象使用。

训练器():训练器实现在线更新规则,比如简单随机梯度下降、 [16] 或 Adam [34]。 有指向 Model 对象的指针,所以同时也有其中的参数,并且还可以根据更新规则的需要保存关于参数的其他信息。

表达式():在 DyNet 项目中,表达式是主要的可以被操作的数据类型。单个表达式代表了一个计算图中的一个子计算。举个例子,一个表示矩阵或者向量的参数对象可以被加进计算图里,这就产生了一个表达式 W 或者 b。同样,一个 对象 E 可以通过查找操作来查询一个专门的嵌入向量(它也是被加在计算图里的),这就产生了一个表达式 E[i]。这些表达式可以被组合成更大的表达式,例如 (E[3], E[4]) 或者 (tanh(W ∗ (E[3], E[4]) +b))。这里的 、tanh、∗、+、 都是运算,下面详细介绍。

运算():运算不是对象,而是在表达式以及返回表达式上运行的函数,它用来在后台构建计算图。DyNet 为很多基本的算术原语(加、乘、点积、、…)和常用的损失函数、激活函数等等都定义了相应的运算。当情况适宜时,运算可以通过运算符重载来定义,这使得图的构建能尽可能地直观和自然。

构造器类( ): 定义了创建各种「标准化」的网络组件(比如循环神经网络、树结构网络和大词汇量 )的接口。这些都工作在表达式和运算之上,并且提供了各种易用的库。 为各种标准算法提供了高效便捷的实现。不过,从代码层次的意义上来说,它并不是「核心」DyNet 库的一部分,因为 是更高层次的,它实现在 DyNet 最核心的自动微分功能之上。 将会在后续的§5 中深入讨论。

计算图():表达式相当于一种隐含的计算图对象的一部分,该计算图定义了需要进行的计算是什么。DyNet 目前假定在任意一个时刻只有一个计算图存在。尽管计算图是 DyNet 内部工作的核心,但从使用者的角度来看,唯一需要负责做的是为每个训练样本创建一个新的计算图。

用 DyNet 中实现并训练一个模型的整体流程可描述如下:

(a) 创建一个新的计算图(),并且建立一个表达式()来填充该计算图,该表达式用来表示针对这个样本想要进行的计算。

(b) 通过调用最终表达式的 value() 或者 () 函数,计算整个图前向计算的结果。

(c) 如果训练的话,计算损失函数的表达式,并使用它的 () 函数来进行反向传播。

(d) 使用训练器对模型的参数进行更新。

与像 和 这样的静态声明库对比可以发现,创建一个图的步骤落在每一个样本的循环里。这有利于使用户为每个实例()灵活地创建新的图结构,并使用他们掌握的编程语言中的流控句法(flow ,比如迭代())来做这些。当然,它也增加了对图结构速度的要求,即它要足够快,不能变成负担,我们会在§4 中进一步阐述。

3.2 高层面的示例

为了在更高层次说明 DyNet 的编码范式,我们用 演示了一个 DyNet 程序的例子,如图 1 所示。这个程序显示了为一个简单分类器进行最大似然训练的过程,这个分类器为每个需要它预测的类计算一个向量分数,然后返回这个得分最高的类 ID 以及这个最高分。我们假定每个训练样本是一个(输入和输出)对,其中输入是一个二词索引的元组,输出是一个指示正确类的数。

图 1:一个使用 DyNet 的 API 进行训练和测试的例子。

在头两行,我们导入()适当的库。在第 3 行,我们初始化 DyNet 模型,并为相关参数分配内存空间,但是不初始化它们。在第 4—6 行,我们向模型里添加我们的参数,这个过程会因为使用的模型不同而不一样。这里我们增加一个 20 × 100 的权重矩阵、一个 20 维的偏置向量和一个查找表(嵌入表)——该查找表的词汇量大小为 20000 项映射到 50 维向量。在第 7 行,我们初始化了一个训练器(在这个例子中是一个简单的随机梯度降(SGD)训练器),这个训练器被用来更新模型参数。在第 8 行中,我们对数据进行多次训练和测试。

从第 9 行开始,我们对训练数据进行迭代。第 10 行,清除当前计算图的内容,开始一个空的计算图,为后续的计算做准备。第 11-13 行,我们创建一个图,这个图会为每个训练实例计算一个分数向量(这个过程会因为模型的不同而不同)。这里我们首先访问模型中的权重矩阵和偏置向量参数(W_p 和 b_p),并把它们加到图中,也就是这个代码例子中用到的表达式中(W 和 b_p)。然后我们根据输入的 id 来查找两个向量,拼接它们,然后做一个线性变换和 ,这样就创建了和计算相对应的表达式。接下来,我们在第 14 行创建一个与损失有关的表达式——对正确记分结果做一次 后的负对数似然估计。在第 15 行,我们计算前向图的结果,在第 16 行,我们计算后向的,并累计模型变量中参数的梯度。在第 17 行,我们根据 SGD 的更新规则更新这些参数,并清掉之前的累计梯度。

接下来,从第 18 和 19 行开始,我们遍历测试数据并测量准确度。在第 20-23 行,我们又一次清除计算图以及定义计算测试数据分数的表达式,方式和我们在训练数据中做的一样。在第 24 行,我们开始计算并把结果数据放到一个 NumPy 的数组里。在第 25 和 26 行,我们检查是否正确的数据是最高分的那个,如果是的话就把它算作是一个正确的结果。最后第 27 行,我们把本次迭代的测试准确度 print 出来。

3.3 动态图构建( Graph )的两个示例

图 2:树结构递归神经网络(tree- )的一个例子

图 3:动态流控制的一个示例。

4 后台工作

如上一节所述,将 DyNet 与其它神经网络工具包相区别的一个主要特性是,它能够为每个训练样本或 有效地创建新的计算图( )。为了保持计算效率,DyNet 使用了细致的内存管理策略来存储前向传播和反向传播的计算过程中的值(§4.2),因此大部分时间都会用在实际的计算上(§4.3)

4.1 计算图( )

图 4:公式 g(x, j) = tanh(W1∗x+b)+tanh(W2∗ej+b) 的计算图的例子,以及相应的代码。

4.2 高效的图构建

4.3 执行计算

5 更高级的抽象结构

如第 3 节所述,DyNet 实现了在张量()上表示基本(子)可微函数的运算。这和 和 库中提供的运算是相似的。除了这些基本运算外,使用可被视为由基本运算组成的更复杂的结构也是很常见的。常见的例子有循环神经网络(RNN)、树结构神经网络(tree- )和更复杂的计算 概率分布的方法。在其它库中,这些更高级别的结构或是通过本地提供,亦或是通过第三方库(如 Keras)提供。在 DyNet 中,循环神经网络的本地支持、树结构神经网络和更复杂的 函数都是通过 提供的;具体细节会在接下来的章节描述,图 5 中也有所总结。

图 5:DyNet 实现的更高级结构的示例,以及它们的规范使用

5.1 循环神经网络的

5.2 树结构神经网络的

5.3 Large-

图 6:各种 RNN 接口

6 效率工具

DyNet 包含许多可以提高计算效率的功能,包括稀疏更新( )、 和跨 CPU 的多处理(multi- CPUs)。

7 实证比较

在本节中,我们将使用 C++ 接口和 接口将 DyNet 和其他三个流行库( [7]、 [1] 和 [62])进行对比。我们选择这些库是因为 和 可以说是目前最受欢迎的深度学习库,而 的 -by-run 哲学和 DyNet 相似。

表 1:各个任务的数据和默认设置。

表 2:每个工具箱在 CPU 上的处理速度。速度是以 RNNLM 与 处理的词/秒和 处理的句/秒进行衡量的。带 + 的行表示 的稀疏更新( ),这是 DyNet 中的默认行为,但与其他工具包的执行密集更新(dense )的实现不可对比。

表 3:每个工具箱在 GPU 上的处理速度。速度是以 RNNLM 与 处理的词/秒和 处理的句/秒进行衡量的。

表 4:从程序启动到为每个工具包处理第一个实例的时间(秒)。

表 5:密集或稀疏更新(dense or )10 分钟后的处理速度和准确度。

表 6:每个工具包的实现的非注释字符数。

8 使用案例

DyNet 已经投入使用,并已被用于各种各样的项目,主要涉及自然语言处理。DyNet 本身包含一些从最小到中等复杂度的示例(在 / 目录下)。我们还列出了一些全面的研究项目,可以让有兴趣的读者找到匹配他们感兴趣的应用程序的参考样例。

句法分析( ):分析是目前使用 DyNet 的最突出的场景,DyNet 是许多方法的开发背后的库,例如 stack LSTMs [17]()、用于依赖性解析的双向 LSTM 特征提取器()、循环神经网络语法 [18](),和 LSTM 层次树 [35]()。

机器翻译( ):DyNet 帮助创造了包括注意偏差( in )[12]()和基于字符的 27 种翻译方法 [42] 等方法。它还为许多机器翻译工具包提供支持,如 ()和 ( //)。

语言建模( ):DyNet 已被用于混合神经/n 元语言模型( /n-gram )的开发 [47]()和生成语法语言模型 [18]()。

标注():DyNet 用于命名实体识别方法的开发 [47]()、POS 标注、语义角色标签 [60]()、标点符号预测 [5]()和序列处理的多任务学习 [37,56] 以及创建新的架构,如段循环神经网络( )[38]( //cpp/-sup)。

形态():DyNet 已被用于形态变化生成 [21, 2]( )。

杂项:DyNet 已被用于开发专门的用于检测协调结构的神经网络 [22];半监督的介词意义消歧 [23]; 和用于识别词汇语义关系 [53,52]()。

总结、致谢和参考文献(略)

©本文为机器之心编译,转载请联系本公众号获得授权。

✄————————————————

加入机器之心(全职记者/实习生):

投稿或寻求报道:

广告&商务合作: