真实世界的自然语言处理(一)(1)https://developer.aliyun.com/article/1519724
1.3 构建 NLP 应用程序
在本节中,我将向您展示 NLP 应用程序通常是如何开发和构建的。尽管具体细节可能会因案例而异,但了解典型的过程有助于您在开始开发应用程序之前进行规划和预算。如果您事先了解开发 NLP 应用程序的最佳实践,这也会有所帮助。
1.3.1 NLP 应用程序的开发
NLP 应用的开发是一个高度迭代的过程,包括许多研究、开发和运营阶段(见图 1.9)。大多数学习材料,如书籍和在线教程,主要关注训练阶段,尽管应用开发的所有其他阶段对于实际 NLP 应用同样重要。在本节中,我简要介绍了每个阶段涉及的内容。请注意,这些阶段之间没有明确的界限。应用开发者(研究人员、工程师、经理和其他利益相关者)经常会在一些阶段之间反复试验。
图 1.9 NLP 应用的开发循环
数据收集
大多数现代自然语言处理(NLP)应用都是基于机器学习的。根据定义,机器学习需要训练 NLP 模型的数据(记住我们之前讨论过的 ML 的定义——它是通过数据来改进算法的)。在这个阶段,NLP 应用开发者讨论如何将应用构建为一个 NLP/ML 问题,以及应收集哪种类型的数据。数据可以从人类那里收集(例如,通过雇佣内部注释者并让他们浏览一堆文本实例),众包(例如,使用亚马逊机械土耳其等平台),或自动机制(例如,从应用程序日志或点击流中收集)。
你可能首先选择不使用机器学习方法进行你的 NLP 应用,这完全可能是正确的选择,这取决于各种因素,比如时间、预算、任务的复杂性以及你可能能够收集的数据量。即使在这种情况下,收集少量数据进行验证也可能是一个好主意。我将在第十一章更详细地讨论 NLP 应用的训练、验证和测试。
分析和实验
收集数据后,您将进入下一个阶段,进行分析和运行一些实验。对于分析,您通常寻找诸如:文本实例的特征是什么?训练标签的分布情况如何?您能否提出与训练标签相关的信号?您能否提出一些简单的规则,以合理的准确性预测训练标签?我们甚至应该使用 ML 吗?这个清单不胜枚举。这个分析阶段包括数据科学的方面,各种统计技术可能会派上用场。
你运行实验来快速尝试一些原型。这个阶段的目标是在你全力投入并开始训练庞大模型之前将可能的方法集缩小到几个有前途的方法。通过运行实验,你希望回答的问题包括:哪些类型的自然语言处理任务和方法适用于这个自然语言处理应用?这是一个分类、解析、序列标记、回归、文本生成还是其他一些问题?基线方法的性能如何?基于规则的方法的性能如何?我们是否应该使用机器学习?有关有前途方法的训练和服务时间的估计是多少?
我把这两个阶段称为“研究”阶段。这个阶段的存在可以说是自然语言处理应用与其他通用软件系统之间最大的区别。由于其特性,很难预测机器学习系统或自然语言处理系统的性能和行为。在这一点上,你可能还没有写一行生产代码,但完全没问题。这个研究阶段的目的是防止你在以后的阶段浪费精力编写后来证明是无用的生产代码。
训练
在这一点上,你已经对你的自然语言处理应用的方法有了相当清晰的想法。这时你开始增加更多的数据和计算资源(例如,GPU)来训练你的模型。现代自然语言处理模型通常需要花费几天甚至几周的时间进行训练,尤其是基于神经网络模型的模型。逐渐增加你训练的数据量和模型的大小是一种很好的实践。你不想花几周的时间训练一个庞大的神经网络模型,只是发现一个更小、更简单的模型效果一样好,甚至更糟糕的是,你在模型中引入了一个 bug,而你花了几周时间训练的模型根本没用!
在这个阶段,保持你的训练流水线可复制是至关重要的。很可能你需要用不同的超参数集合运行这个流水线多次,超参数是在启动模型学习过程之前设置的调整值。很可能几个月甚至几年后你还需要再次运行这个流水线。我会在第十章讨论一些训练自然语言处理/机器学习模型的最佳实践。
实施
当你有一个表现良好的模型时,你就会进入实施阶段。这是你开始使你的应用“投入生产”的时候。这个过程基本上遵循软件工程的最佳实践,包括:为你的自然语言处理模块编写单元和集成测试,重构你的代码,让其他开发人员审查你的代码,提高你的自然语言处理模块的性能,并将你的应用程序打包成 Docker 镜像。我将在第十一章更详细地讨论这个过程。
部署
你的 NLP 应用程序终于准备好部署了。你可以以多种方式部署你的 NLP 应用程序 —— 它可以是一个在线服务、一个定期批处理作业、一个离线应用程序,或者是一个离线一次性任务。如果这是一个需要实时提供预测的在线服务,将其打造成一个微服务以使其与其他服务松耦合是个好主意。无论如何,对于你的应用程序来说,使用持续集成(CI)是一个很好的实践,在这种情况下,你在每次对应用程序进行更改时都会运行测试,并验证你的代码和模型是否按预期工作。
监控
开发 NLP 应用程序的一个重要的最终步骤是监控。这不仅包括监控基础架构,比如服务器 CPU、内存和请求延迟,还包括更高级别的 ML 统计信息,比如输入和预测标签的分布。在这个阶段要问一些重要的问题是:输入实例是什么样子的?它们是否符合你构建模型时的预期?预测的标签是什么样子的?预测的标签分布是否与训练数据中的分布相匹配?监控的目的是检查你构建的模型是否按预期运行。如果传入的文本或数据实例或预测的标签与你的期望不符,那么你可能遇到了一个领域外的问题,这意味着你收到的自然语言数据的领域与你的模型训练的领域不同。机器学习模型通常不擅长处理领域外数据,预测精度可能会受到影响。如果这个问题变得明显,那么重新开始整个过程可能是一个好主意,从收集更多的领域内数据开始。
1.3.2 NLP 应用程序的结构
现代基于机器学习的 NLP 应用程序的结构出人意料地相似,主要有两个原因——一个是大多数现代 NLP 应用程序在某种程度上依赖于机器学习,并且它们应该遵循机器学习应用程序的最佳实践。另一个原因是,由于神经网络模型的出现,一些 NLP 任务,包括文本分类、机器翻译、对话系统和语音识别,现在可以端到端地进行训练,正如我之前提到的。其中一些任务过去是复杂的、包含数十个组件且具有复杂管道的庞然大物。然而,现在,一些这样的任务可以通过不到 1000 行的 Python 代码来解决,只要有足够的数据来端到端地训练模型。
图 1.10 展示了现代 NLP 应用程序的典型结构。有两个主要基础设施:训练基础设施和服务基础设施。训练基础设施通常是离线的,用于训练应用程序所需的机器学习模型。它接收训练数据,将其转换为可以由管道处理的某种数据结构,并通过转换数据和提取特征进一步处理数据。这一部分因任务而异。最后,如果模型是神经网络,将数据实例分批处理并馈送到模型中,该模型经过优化以最小化损失。如果你不理解我在最后一句说的是什么,不要担心,我们将在第二章讨论与神经网络一起使用的技术术语。训练好的模型通常是序列化并存储以传递给服务基础设施。
图 1.10 典型 NLP 应用程序的结构
服务基础设施的任务是在给定新实例的情况下生成预测,例如类别、标签或翻译。这个基础设施的第一部分,读取实例并将其转换为一些数字,与训练的部分类似。事实上,你必须保持数据集读取器和转换器相同。否则,这两个过程数据的方式将产生差异,也被称为训练 - 服务差异。在处理实例后,它被馈送到预训练模型以生成预测。我将在第十一章更多地讨论设计 NLP 应用程序的方法。
概述
- 自然语言处理(NLP)是人工智能(AI)的一个子领域,指的是处理、理解和生成人类语言的计算方法。
- NLP 面临的挑战之一是自然语言中的歧义性。有句法和语义歧义。
- 有文本的地方就有 NLP。许多技术公司使用 NLP 从大量文本中提取信息。典型的 NLP 应用包括机器翻译、语法错误纠正、搜索引擎和对话系统。
- NLP 应用程序以迭代方式开发,更多注重研究阶段。
- 许多现代自然语言处理(NLP)应用程序严重依赖于机器学习(ML),并且在结构上与 ML 系统相似。
第二章:您的第一个 NLP 应用程序
本章内容包括:
- 使用 AllenNLP 构建情感分析器
- 应用基本的机器学习概念(数据集,分类和回归)
- 应用神经网络概念(词嵌入,循环神经网络,线性层)
- 通过减少损失训练模型
- 评估和部署您的模型
在 1.1.2 节中,我们看到了如何使用 NLP。在本章中,我们将讨论如何以更有原则性和现代化的方式进行 NLP。具体而言,我们希望使用神经网络构建一个情感分析器。尽管我们要构建的情感分析器是一个简单的应用程序,并且该库(AllenNLP)会处理大部分工作,但它是一个成熟的 NLP 应用程序,涵盖了许多现代 NLP 和机器学习的基本组件。我将沿途介绍重要的术语和概念。如果您一开始不理解某些概念,请不要担心。我们将在后面的章节中再次讨论在此处介绍的大部分概念。
2.1 介绍情感分析
在 1.1.2 节中描述的情景中,您希望从在线调查结果中提取用户的主观意见。您拥有对自由回答问题的文本数据集合,但缺少对“您对我们的产品有何评价?”问题的答案,您希望从文本中恢复它们。这个任务称为情感分析,是一种在文本中自动识别和分类主观信息的文本分析技术。该技术广泛应用于量化以非结构化方式书写的意见、情感等方面的文本资源。情感分析应用于各种文本资源,如调查、评论和社交媒体帖子。
在机器学习中,分类意味着将某样东西归类为一组预定义的离散类别。情感分析中最基本的任务之一就是极性的分类,即将表达的观点分类为正面、负面或中性。您可以使用超过三个类别,例如强正面、正面、中性、负面或强负面。如果您使用过可以使用五级评分表达的网站(如亚马逊),那么这可能听起来很熟悉。
极性分类是一种句子分类任务。另一种句子分类任务是垃圾邮件过滤,其中每个句子被分类为两类——垃圾邮件或非垃圾邮件。如果只有两个类别,则称为二元分类。如果有超过两个类别(前面提到的五星级分类系统,例如),则称为多类分类。
相反,当预测是连续值而不是离散类别时,称之为 回归。如果你想根据房屋的属性来预测房屋的价格,比如它的社区、卧室和浴室的数量以及平方英尺,那就是一个回归问题。如果你尝试根据从新闻文章和社交媒体帖子中收集到的信息来预测股票价格,那也是一个回归问题。(免责声明:我并不是在建议这是预测股价的适当方法。我甚至不确定它是否有效。)正如我之前提到的,大多数语言单位,如字符、单词和词性标签,都是离散的。因此,自然语言处理中大多数使用的机器学习都是分类,而不是回归。
注意 逻辑回归,一种广泛使用的统计模型,通常用于分类,尽管它的名字中有“回归”一词。是的,我知道这很令人困惑!
许多现代自然语言处理应用,包括我们将在本章中构建的情感分析器(如图 2.1 所示),都是基于 监督式机器学习 范式构建的。监督式机器学习是一种机器学习类型,其中算法是通过具有监督信号的数据进行训练的——对于每个输入都有期望的结果。该算法被训练成尽可能准确地重现这些信号。对于情感分析,这意味着系统是在包含每个输入句子的所需标签的数据上进行训练的。
图 2.1 情感分析流水线
2.2 处理自然语言处理数据集
正如我们在上一节中讨论的,许多现代自然语言处理应用都是使用监督式机器学习开发的,其中算法是从标有期望结果的数据中训练出来的,而不是使用手写规则。几乎可以说,数据是机器学习的关键部分,因此了解它是如何结构化并与机器学习算法一起使用的至关重要。
2.2.1 什么是数据集?
数据集 简单地意味着一组数据。如果你熟悉关系型数据库,你可以将数据集想象成一个表的转储。它由符合相同格式的数据片段组成。在数据库术语中,数据的每个片段对应一个记录,或者表中的一行。记录可以有任意数量的字段,对应数据库中的列。
在自然语言处理中,数据集中的记录通常是某种类型的语言单位,比如单词、句子或文档。自然语言文本的数据集称为 语料库(复数形式为 语料库)。举个例子,我们来想象一个(假想的)用于垃圾邮件过滤的数据集。该数据集中的每条记录都是一对文本和标签,其中文本是一句话或一段文字(例如,来自一封电子邮件),而标签指定文本是否是垃圾邮件。文本和标签都是记录的字段。
一些自然语言处理数据集和语料库具有更复杂的结构。例如,一个数据集可能包含一系列句子,其中每个句子都用详细的语言信息进行了注释,例如词性标签、句法树、依存结构和语义角色。如果一个数据集包含了一系列句子,并且这些句子带有它们的句法树注释,那么这个数据集被称为树库。最著名的例子是宾夕法尼亚树库(Penn Treebank,PTB)(realworldnlpbook.com/ch2.html#ptb
),它一直作为培训和评估自然语言处理任务(如词性标注和句法分析)的事实标准数据集。
与记录密切相关的术语是实例。在机器学习中,实例是进行预测的基本单位。例如,在前面提到的垃圾邮件过滤任务中,一个实例是一段文本,因为对单个文本进行预测(垃圾邮件或非垃圾邮件)。实例通常是从数据集中的记录创建的,就像在垃圾邮件过滤任务中一样,但并非总是如此——例如,如果您拿一个树库来训练一个 NLP 任务,该任务检测句子中的所有名词,那么每个单词,而不是一个句子,就成为一个实例,因为对每个单词进行预测(名词或非名词)。最后,标签是附加到数据集中某些语言单位的信息片段。一个垃圾邮件过滤数据集有与每个文本是否为垃圾邮件相对应的标签。一个树库可能具有每个词的词性标签的标签。标签通常在监督式机器学习环境中用作训练信号(即训练算法的答案)。请参见图 2.2,了解数据集的这些部分的描绘。
图 2.2 数据集、记录、字段、实例和标签
2.2.2 斯坦福情感树库
为了构建情感分析器,我们将使用斯坦福情感树库(SST;nlp.stanford.edu/sentiment/
),这是截至目前最广泛使用的情感分析数据集之一。前往链接中的 Train, Dev, Test Splits in PTB Tree Format 下载数据集。SST 与其他数据集的一个不同之处在于,情感标签不仅分配给句子,而且分配给句子中的每个单词和短语。例如,数据集的一些摘录如下:
(4 (2 (2 Steven) (2 Spielberg)) (4 (2 (2 brings) (3 us)) (4 (2 another) (4 masterpiece)))) (1 (2 It) (1 (1 (2 (2 's) (1 not)) (4 (2 a) (4 (4 great) (2 (2 monster) (2 movie))))) (2 .)))
现在不用担心细节——这些树以人类难以阅读的 S 表达式编写(除非你是 Lisp 程序员)。请注意以下内容:
- 每个句子都带有情感标签(4 和 1)。
- 每个单词也被注释了,例如,(4 masterpiece)和(1 not)。
- 每个短语也被注释了,例如,(4(2 another)(4 masterpiece))。
数据集的这种属性使我们能够研究单词和短语之间的复杂语义交互。 例如,让我们将以下句子的极性作为一个整体来考虑:
这部电影实际上既不是那么有趣,也不是非常机智。
上面的陈述肯定是一个负面的,尽管,如果你专注于单词的个别词语(比如有趣、机智),你可能会被愚弄成认为它是一个积极的。 如果您构建一个简单的分类器,它从单词的个别“投票”中获取结果(例如,如果其大多数单词为积极,则句子为积极),这样的分类器将难以正确分类此示例。 要正确分类此句子的极性,您需要理解否定“既不…也不”的语义影响。 为了这个属性,SST 已被用作可以捕获句子的句法结构的神经网络模型的标准基准(realworldnlpbook.com/ch2.html#socher13
)。 但是,在本章中,我们将忽略分配给内部短语的所有标签,并仅使用句子的标签。
2.2.3 训练、验证和测试集
在我们继续展示如何使用 SST 数据集并开始构建我们自己的情感分析器之前,我想简要介绍一些机器学习中的重要概念。 在 NLP 和 ML 中,通常使用几种不同类型的数据集来开发和评估模型是常见的。 一个广泛使用的最佳实践是使用三种不同类型的数据集拆分——训练、验证和测试集。
训练(或训练)集是用于训练 NLP/ML 模型的主要数据集。 通常将来自训练集的实例直接馈送到 ML 训练管道中,并用于学习模型的参数。 训练集通常是这里讨论的三种类型的拆分中最大的。
验证集(也称为开发或开发集)用于模型选择。 模型选择是一个过程,在这个过程中,从所有可能使用训练集训练的模型中选择适当的 NLP/ML 模型,并且这是为什么它是必要的。 让我们想象一种情况,在这种情况下,您有两种机器学习算法 A 和 B,您希望用它们来训练一个 NLP 模型。 您同时使用这两个算法,并获得了模型 A 和 B。 现在,您如何知道哪个模型更好呢?
“那很容易,”您可能会说。“在训练集上评估它们两个。”乍一看,这似乎是个好主意。 您在训练集上运行模型 A 和 B,并查看它们在准确度等度量方面的表现。 为什么人们要费心使用单独的验证集来选择模型?
答案是 过拟合 —— 自然语言处理和机器学习中另一个重要概念。过拟合是指训练模型在训练集上拟合得非常好,以至于失去了其泛化能力的情况。让我们想象一个极端情况来说明这一点。假设算法 B 是一个非常非常强大的算法,可以完全记住所有东西。可以把它想象成一个大的关联数组(或 Python 中的字典),它可以存储它曾经遇到过的所有实例和标签对。对于垃圾邮件过滤任务来说,这意味着模型会以训练时呈现的确切文本及其标签的形式进行存储。如果在评估模型时呈现相同的文本,它将返回存储的标签。另一方面,如果呈现的文本与其记忆中的任何其他文本略有不同,模型就一无所知,因为它以前从未见过。
你认为这个模型在训练集上进行评估时会表现如何?答案是……是的,100%!因为模型记住了训练集中的所有实例,所以它可以简单地“重播”整个数据集并进行完美分类。现在,如果你在电子邮件软件上安装了这个算法,它会成为一个好的垃圾邮件过滤器吗?绝对不会!因为无数的垃圾邮件看起来与现有邮件非常相似,但略有不同,或者完全是新的,所以如果输入的电子邮件与存储在内存中的内容只有一个字符的不同,模型就一无所知,并且在投入生产时将毫无用处。换句话说,它的泛化能力非常差(事实上是零)。
你如何防止选择这样的模型呢?通过使用验证集!验证集由与训练集类似的独立实例组成。因为它们与训练集独立,所以如果你在验证集上运行训练过的模型,你就可以很好地了解模型在训练集之外的表现。换句话说,验证集为模型的泛化能力提供了一个代理。想象一下,如果之前的“记住所有”算法训练的模型在验证集上进行评估。因为验证集中的实例与训练集中的实例类似但独立,所以你会得到非常低的准确率,知道模型的性能会很差,甚至在部署之前。
验证集还用于调整超参数。超参数是关于机器学习算法或正在训练的模型的参数。例如,如果你将训练循环(也称为epoch,关于更多解释请见后文)重复N次,那么这个N就是一个超参数。如果你增加神经网络的层数,你就改变了关于模型的一个超参数。机器学习算法和模型通常有许多超参数,调整它们对模型的性能至关重要。你可以通过训练多个具有不同超参数的模型并在验证集上评估它们来做到这一点。事实上,你可以将具有不同超参数的模型视为不同的模型,即使它们具有相同的结构,超参数调整可以被视为一种模型选择。
最后,测试集用于使用新的、未见过的数据对模型进行评估。它包含的实例与训练集和验证集是独立的。它可以让你很好地了解模型在“野外”中的表现。
你可能会想知道为什么需要另外一个独立的数据集来评估模型的泛化能力。难道你不能只使用验证集吗?再次强调,你不应该仅仅依赖于训练集和验证集来衡量你的模型的泛化能力,因为你的模型也可能以微妙的方式对验证集进行过拟合。这一点不太直观,但让我举个例子。想象一下,你正在疯狂地尝试大量不同的垃圾邮件过滤模型。你编写了一个脚本,可以自动训练一个垃圾邮件过滤模型。该脚本还会自动在验证集上评估训练好的模型。如果你用不同的算法和超参数组合运行此脚本 1,000 次,并选择在验证集上性能最好的一个模型,那么它是否也会在完全新的、未见过的实例上表现最好呢?可能不会。如果你尝试大量的模型,其中一些可能纯粹是由于偶然性而在验证集上表现相对较好(因为预测本质上存在一些噪音,和/或者因为这些模型恰好具有一些使它们在验证集上表现更好的特性),但这并不能保证这些模型在验证集之外表现良好。换句话说,可能会将模型过度拟合到验证集上。
总之,在训练 NLP 模型时,使用一个训练集来训练你的模型候选者,使用一个验证集来选择好的模型,并使用一个测试集来评估它们。用于 NLP 和 ML 评估的许多公共数据集已经分成了训练/验证/测试集。如果你只有一个数据集,你可以自己将其分成这三个数据集。常用的是 80:10:10 分割。图 2.3 描绘了训练/验证/测试分割以及整个训练流水线。
图 2.3 训练/验证/测试分割和训练流水线
2.2.4 使用 AllenNLP 加载 SST 数据集
最后,让我们看看如何在代码中实际加载数据集。在本章的其余部分,我们假设你已经安装了 AllenNLP(版本 2.5.0)和相应版本的 allennlp-models 包,通过运行以下命令:
pip install allennlp==2.5.0 pip install allennlp-models==2.5.0
并导入了如下所示的必要类和模块:
from itertools import chain from typing import Dict import numpy as np import torch import torch.optim as optim from allennlp.data.data_loaders import MultiProcessDataLoader from allennlp.data.samplers import BucketBatchSampler from allennlp.data.vocabulary import Vocabulary from allennlp.models import Model from allennlp.modules.seq2vec_encoders import Seq2VecEncoder, PytorchSeq2VecWrapper from allennlp.modules.text_field_embedders import TextFieldEmbedder, BasicTextFieldEmbedder from allennlp.modules.token_embedders import Embedding from allennlp.nn.util import get_text_field_mask from allennlp.training import GradientDescentTrainer from allennlp.training.metrics import CategoricalAccuracy, F1Measure from allennlp_models.classification.dataset_readers.stanford_sentiment_tree_bank import \ StanfordSentimentTreeBankDatasetReader
很遗憾,截至目前为止,AllenNLP 并不官方支持 Windows。但别担心——本章节中的所有代码(实际上,本书中的所有代码)都可以作为 Google Colab 笔记本 (www.realworldnlpbook.com/ch2.html#sst-nb
) 使用,你可以在那里运行和修改代码并查看结果。
你还需要定义以下两个在代码片段中使用的常量:
EMBEDDING_DIM = 128 HIDDEN_DIM = 128
AllenNLP 已经支持一个名为 DatasetReader 的抽象,它负责从原始格式(无论是原始文本还是一些奇特的基于 XML 的格式)中读取数据集并将其返回为一组实例。我们将使用 StanfordSentimentTreeBankDatasetReader(),它是一种特定处理 SST 数据集的 DatasetReader,如下所示:
reader = StanfordSentimentTreeBankDatasetReader() train_path = 'https:/./s3.amazonaws.com/realworldnlpbook/data/stanfordSentimentTreebank/trees/train.txt' dev_path = 'https:/./s3.amazonaws.com/realworldnlpbook/data/stanfordSentimentTreebank/trees/dev.txt'
此片段将为 SST 数据集创建一个数据集读取器,并定义训练和开发文本文件的路径。
2.3 使用词嵌入
从这一部分开始,我们将开始构建情感分析器的神经网络架构。Architecture 只是神经网络结构的另一个词。构建神经网络很像建造房屋等结构。第一步是弄清楚如何将输入(例如,情感分析的句子)馈送到网络中。
正如我们之前所见,自然语言处理中的所有内容都是离散的,这意味着形式和含义之间没有可预测的关系(记得“rat”和“sat”)。另一方面,神经网络最擅长处理数字和连续的东西,这意味着神经网络中的所有内容都需要是浮点数。我们如何在这两个世界之间“搭桥”——离散和连续?关键在于词嵌入的使用,我们将在本节中详细讨论。
2.3.1 什么是词嵌入?
Word embeddings 是现代自然语言处理中最重要的概念之一。从技术上讲,嵌入是通常离散的东西的连续向量表示。词嵌入是一个词的连续向量表示。如果你对向量的概念不熟悉,vector 是数学上对数字的单维数组的名称。简单来说,词嵌入是用一个 300 元素数组(或任何其他大小的数组)填充的非零浮点数来表示每个单词的一种方式。概念上非常简单。那么,为什么它在现代自然语言处理中如此重要和普遍呢?
正如我在第一章中提到的,自然语言处理的历史实际上是对语言“离散性”的持续战斗的历史。在计算机眼中,“猫”和“狗”的距离与它们与“披萨”的距离是相同的。编程上处理离散词的一种方法是为各个词分配索引,如下所示(这里我们简单地假设这些索引按字母顺序分配):
- index(“cat”) = 1
- index(“dog”) = 2
- index(“pizza”) = 3
- …
这些分配通常由查找表管理。一个 NLP 应用或任务处理的整个有限单词集被称为词汇。但是这种方法并不比处理原始单词更好。仅仅因为单词现在用数字表示,就不意味着你可以对它们进行算术运算,并得出“猫”与“狗”(1 和 2 之间的差异)同样相似,就像“狗”与“披萨”(2 和 3 之间的差异)一样。这些指数仍然是离散和任意的。
“如果我们可以在数值尺度上表示它们呢?”几十年前,一些自然语言处理研究人员想知道。我们能否想出一种数值尺度,其中单词是
以点表示,以使语义上更接近的词(例如,“狗”和“猫”,它们都是动物)在几何上也更接近?从概念上讲,数值尺度将类似于图 2.4 中所示的尺度。
图 2.4 一维空间中的词嵌入
这是一个进步。现在我们可以表示“猫”和“狗”彼此之间比“披萨”更相似的事实。但是,“披萨”仍然比“猫”更接近“狗”。如果你想把它放在一个距离“猫”和“狗”都一样远的地方怎么办?也许只有一个维度太限制了。在这个基础上再添加一个维度如何,如图 2.5 所示?
图 2.5 二维空间中的词嵌入
许多改进!因为计算机在处理多维空间方面非常擅长(因为你可以简单地用数组表示点),你可以一直这样做,直到你有足够数量的维度。让我们有三个维度。在这个三维空间中,你可以将这三个词表示如下:
- vec(“cat”) = [0.7, 0.5, 0.1]
- vec(“dog”) = [0.8, 0.3, 0.1]
- vec(“pizza”) = [0.1, 0.2, 0.8]
图 2.6 说明了这个三维空间。
图 2.6 三维空间中的词嵌入
这里的 x 轴(第一个元素)表示“动物性”的某种概念,而 z 轴(第三维)对应于“食物性”。(我编造了这些数字,但你明白我的意思。)这就是单词嵌入的本质。您只是将这些单词嵌入到了一个三维空间中。通过使用这些向量,您已经“知道”了语言的基本构建块是如何工作的。例如,如果您想要识别动物名称,那么您只需查看每个单词向量的第一个元素,并查看值是否足够高。与原始单词索引相比,这是一个很好的起点!
你可能会想知道这些数字实际上来自哪里。这些数字实际上是使用一些机器学习算法和大型文本数据集“学习”的。我们将在第三章中进一步讨论这一点。
顺便说一下,我们有一种更简单的方法将单词“嵌入”到多维空间中。想象一个具有与单词数量相同的维度的多维空间。然后,给每个单词一个向量,其中填充了零但只有一个 1,如下所示:
- vec(“cat”) = [1, 0, 0]
- vec(“dog”) = [0, 1, 0]
- vec(“pizza”) = [0, 0, 1]
请注意,每个向量在对应单词的索引位置只有一个 1。这些特殊向量称为one-hot 向量。这些向量本身并不非常有用,不能很好地表示这些单词之间的语义关系——这三个单词彼此之间的距离都是相等的——但它们仍然(是一种非常愚蠢的)嵌入。当嵌入不可用时,它们通常被用作机器学习算法的输入。
2.3.2 使用单词嵌入进行情感分析
首先,我们创建数据集加载器,负责加载数据并将其传递给训练流水线,如下所示(稍后在本章中对此数据进行更多讨论):
sampler = BucketBatchSampler(batch_size=32, sorting_keys=["tokens"]) train_data_loader = MultiProcessDataLoader(reader, train_path, batch_sampler=sampler) dev_data_loader = MultiProcessDataLoader(reader, dev_path, batch_sampler=sampler)
AllenNLP 提供了一个有用的 Vocabulary 类,管理着一些语言单位(如字符、单词和标签)到它们的 ID 的映射。您可以告诉该类从一组实例中创建一个 Vocabulary 实例,如下所示:
vocab = Vocabulary.from_instances(chain(train_data_loader.iter_instances(), dev_data_loader.iter_instances()), min_count={'tokens': 3})
然后,您需要初始化一个 Embedding 实例,它负责将 ID 转换为嵌入,如下代码片段所示。嵌入的大小(维度)由 EMBEDDING_DIM 决定:
token_embedding = Embedding(num_embeddings=vocab.get_vocab_size('tokens'), embedding_dim=EMBEDDING_DIM)
最后,您需要指定哪些索引名称对应于哪些嵌入,并将其传递给 BasicTextFieldEmbedder,如下所示:
word_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})
现在,您可以使用 word_embeddings 将单词(或更准确地说是标记,我将在第三章中更详细地讨论)转换为它们的嵌入。
2.4 神经网络
越来越多的现代自然语言处理应用程序是使用神经网络构建的。你可能已经看到了许多现代神经网络模型在计算机视觉和游戏领域所取得的惊人成就(例如自动驾驶汽车和打败人类冠军的围棋算法),而自然语言处理也不例外。在本书中,我们将使用神经网络来构建大多数自然语言处理示例和应用程序。在本节中,我们讨论了神经网络是什么以及它们为什么如此强大。
2.4.1 什么是神经网络?
神经网络是现代自然语言处理(以及许多其他相关人工智能领域,如计算机视觉)的核心。它是如此重要,如此广泛的研究主题,以至于需要一本书(或者可能是几本书)来全面解释它是什么以及所有相关的模型、算法等。在本节中,我将简要解释其要点,并根据需要在后面的章节中详细介绍。
简而言之,神经网络(也称为人工神经网络)是一个通用的数学模型,它将一个向量转换为另一个向量。就是这样。与你在大众媒体中读到和听到的内容相反,它的本质是简单的。如果你熟悉编程术语,可以将其看作是一个接受一个向量,内部进行一些计算,并将另一个向量作为返回值的函数。那么它为什么如此重要呢?它与编程中的普通函数有何不同呢?
第一个区别在于神经网络是可训练的。不要把它仅仅看作是一个固定的函数,而更像是一组相关函数的“模板”。如果你使用编程语言编写了一个包含一些常数的数学方程组的函数,当你输入相同的输入时,你总是会得到相同的结果。相反,神经网络可以接收“反馈”(输出与期望输出的接近程度)并调整其内部常数。那些“神奇”的常数被称为权重或更普遍地称为参数。下次运行时,你期望它的答案更接近你想要的结果。
第二个区别在于它的数学能力。如果可能的话,如果你要使用你最喜欢的编程语言编写一个执行情感分析等功能的函数,那将会非常复杂。(还记得第一章中那个可怜的软件工程师吗?)理论上,只要有足够的模型能力和训练数据,神经网络就能够近似于任何连续函数。这意味着,无论你的问题是什么,只要输入和输出之间存在关系,并且你为模型提供足够的计算能力和训练数据,神经网络就能够解决它。
神经网络通过学习非线性函数来实现这一点。什么是线性函数呢?线性函数是指,如果你将输入改变了 x,输出将始终以 c * x 的常数倍变化,其中 c 是一个常数。例如,2.0 * x 是线性的,因为如果你将 x 改变 1.0,返回值总是增加 2.0。如果你将这个函数画在图上,输入和输出之间的关系形成一条直线,这就是为什么它被称为线性的原因。另一方面,2.0 * x * x 不是线性的,因为返回值的变化量不仅取决于你改变 x 的量,还取决于 x 的值。
这意味着线性函数无法捕捉输入和输出之间以及输入变量之间的更复杂的关系。相反,诸如语言之类的自然现象是高度非线性的。如果你改变了输入 x(例如,句子中的一个词),输出的变化量不仅取决于你改变了多少 x,还取决于许多其他因素,如 x 本身的值(例如,你将 x 改变为什么词)以及其他变量(例如,x 的上下文)是什么。神经网络,这种非线性数学模型,有可能捕捉到这样复杂的相互作用。
2.4.2 循环神经网络(RNNs)和线性层
两种特殊类型的神经网络组件对情感分析非常重要——循环神经网络(RNNs)和线性层。我将在后面的章节中详细解释它们,但我会简要描述它们是什么以及它们在情感分析(或一般而言,句子分类)中的作用。
循环神经网络(RNN)是一种带有循环的神经网络,如图 2.7 所示。它具有一个内部结构,该结构被一次又一次地应用于输入。用编程的类比来说,这就像编写一个包含 for word in sentence:循环遍历输入句子中的每个单词的函数。它可以输出循环内部变量的中间值,或者循环完成后变量的最终值,或者两者兼而有之。如果你只取最终值,你可以将 RNN 用作将句子转换为具有固定长度的向量的函数。在许多自然语言处理任务中,你可以使用 RNN 将句子转换为句子的嵌入。还记得词嵌入吗?它们是单词的固定长度表示。类似地,RNN 可以产生句子的固定长度表示。
图 2.7 循环神经网络(RNN)
我们在这里将使用的另一种类型的神经网络组件是线性层。线性层,也称为全连接层,以线性方式将一个向量转换为另一个向量。正如前面提到的,层只是神经网络的一个子结构的花哨术语,因为你可以将它们堆叠在一起形成一个更大的结构。
请记住,神经网络可以学习输入和输出之间的非线性关系。为什么我们想要有更受限制(线性)的东西呢?线性层用于通过减少(或增加)维度来压缩(或扩展)向量。例如,假设你从 RNN 接收到一个 64 维的向量(64 个浮点数的数组)作为句子的嵌入,但你只关心对预测有重要作用的少量数值。在情感分析中,你可能只关心与五种不同情感标签对应的五个数值,即极强正面、正面、中性、负面和极强负面。但是你无法从嵌入的 64 个数值中提取出这五个数值。这正是线性层派上用场的地方 - 你可以添加一个层,将一个 64 维的向量转换为一个 5 维的向量,而神经网络会想办法做得很好,如图 2.8 所示。
图 2.8 线性层
2.4.3 情感分析的架构
现在,你已经准备好将各个组件组合起来构建情感分析器的神经网络了。首先,你需要按照以下步骤创建 RNN:
encoder = PytorchSeq2VecWrapper( torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))
不要太担心 PytorchSeq2VecWrapper 和 batch_first=True。在这里,你正在创建一个 RNN(或更具体地说,一种叫做 LSTM 的 RNN)。输入向量的大小是 EMBEDDING_DIM,我们之前看到的,而输出向量的大小是 HIDDEN_DIM。
接下来,你需要创建一个线性层,如下所示:
self.linear = torch.nn.Linear(in_features=encoder.get_output_dim(), out_features=vocab.get_vocab_size('labels'))
输入向量的大小由 in_features 定义,而输出向量的大小则由 out_features 定义。因为我们要将句子嵌入转换为一个向量,其元素对应于五个情感标签,所以我们需要指定编码器输出的大小,并从词汇表中获取标签的总数。
最后,我们可以连接这些组件并构建一个模型,如下所示的代码。
列表 2.1 构建情感分析模型
class LstmClassifier(Model): def __init__(self, word_embeddings: TextFieldEmbedder, encoder: Seq2VecEncoder, vocab: Vocabulary, positive_label: str = '4') -> None: super().__init__(vocab) self.word_embeddings = word_embeddings self.encoder = encoder self.linear = torch.nn.Linear(in_features=encoder.get_output_dim(), out_features=vocab.get_vocab_size('labels')) self.loss_function = torch.nn.CrossEntropyLoss() ❶ def forward(self, ❷ tokens: Dict[str, torch.Tensor], label: torch.Tensor = None) -> torch.Tensor: mask = get_text_field_mask(tokens) embeddings = self.word_embeddings(tokens) encoder_out = self.encoder(embeddings, mask) logits = self.linear(encoder_out) output = {"logits": logits} if label is not None: self.accuracy(logits, label) self.f1_measure(logits, label) output["loss"] = self.loss_function(logits, label) ❸ return output
❶ 定义损失函数(交叉熵)
❷ forward() 函数是模型中大部分计算发生的地方。
❸ 计算损失并将其分配给返回字典中的“loss”键
我希望你专注于最重要的函数 forward(),每个神经网络模型都有它。它的作用是接收输入,经过神经网络的子组件处理后,产生输出。虽然这个函数有一些我们尚未涉及的陌生逻辑(例如掩码和损失),但重要的是你可以像将输入(标记)转换的函数一样将模型的子组件(词嵌入,RNN 和线性层)链接在一起,并在管道的末尾得到一些称为logits的东西。在统计学中,logit 是一个具有特定含义的术语,但在这里,你可以将其视为类别的分数。对于特定标签的分数越高,表示该标签是正确的信心就越大。
2.5 损失函数和优化
神经网络使用有监督学习进行训练。如前所述,有监督学习是一种基于大量标记数据学习将输入映射到输出的机器学习类型。到目前为止,我只介绍了神经网络如何接收输入并生成输出。我们如何才能使神经网络生成我们实际想要的输出呢?
神经网络不仅仅是像常规的编程语言中的函数那样。它们是可训练的,意味着它们可以接收一些反馈并调整其内部参数,以便下一次为相同的输入产生更准确的输出。请注意,这包含两个部分-接收反馈和调整参数,分别通过损失函数和优化来实现,下面我将解释它们。
损失函数是衡量机器学习模型输出与期望输出之间距离的函数。实际输出与期望输出之间的差异称为损失。在某些情况下,损失也称为成本。无论哪种情况,损失越大,则模型越差,你希望它尽可能接近零。例如,以情感分析为例。如果模型认为一句话是 100%的负面,但训练数据显示它是非常积极的,那么损失会很大。另一方面,如果模型认为一句话可能是 80%的负面,而训练标签确实是负面的,那么损失会很小。如果两者完全匹配,损失将为零。
PyTorch 提供了广泛的函数来计算损失。我们在这里需要的是交叉熵损失,它通常用于分类问题,如下所示:
self.loss_function = torch.nn.CrossEntropyLoss()
后续可以通过以下方式将预测和来自训练集的标签传递给它来使用:
output["loss"] = self.loss_function(logits, label)
然后,这就是魔术发生的地方。 由于其数学属性,神经网络知道如何改变其内部参数以使损失变小。 在接收到一些大损失后,神经网络会说:“哎呀,抱歉,那是我的错,但我下一轮会做得更好!” 并更改其参数。 记得我说过编写一个具有一些魔术常量的编程语言的函数吗? 神经网络就像那样的函数,但它们确切地知道如何改变魔术常量以减少损失。 它们对训练数据中的每个实例都这样做,以便尽可能为尽可能多的实例产生更多的正确答案。 当然,它们在调整参数仅一次后就不能达到完美的答案。 需要对训练数据进行多次通过,称为epochs。 图 2.9 显示了神经网络的整体训练过程。
图 2.9 神经网络的整体训练过程
神经网络从输入中使用当前参数集计算输出的过程称为前向传递。 这就是为什么列表 2.1 中的主要函数被称为 forward()。 将损失反馈给神经网络的方式称为反向传播。 通常使用一种称为随机梯度下降(SGD)的算法来最小化损失。 将损失最小化的过程称为优化,用于实现此目的的算法(例如 SGD)称为优化器。 您可以使用 PyTorch 初始化优化器如下:
optimizer = optim.Adam(model.parameters())
在这里,我们使用一种称为 Adam 的优化器。 在神经网络社区中提出了许多类型的优化器,但共识是没有一种优化算法适用于任何问题,您应该准备为您自己的问题尝试多种优化算法。
好了,那是很多技术术语。 暂时你不需要了解这些算法的细节,但如果你学习一下这些术语及其大致含义会很有帮助。 如果你用 Python 伪代码来写整个训练过程,它将显示为第 2.2 列表所示。 请注意,有两个嵌套循环,一个是在 epochs 上,另一个是在 instances 上。
第 2.2 列表神经网络训练循环的伪代码
MAX_EPOCHS = 100 model = Model() for epoch in range(MAX_EPOCHS): for instance, label in train_set: prediction = model.forward(instance) loss = loss_function(prediction, label) new_model = optimizer(model, loss) model = new_model
2.6 训练您自己的分类器
在本节中,我们将使用 AllenNLP 的训练框架来训练我们自己的分类器。 我还将简要介绍批处理的概念,这是在训练神经网络模型中使用的重要实用概念。
2.6.1 批处理
到目前为止,我忽略了一个细节——批处理。 我们假设每个实例都会进行一次优化步骤,就像您在之前的伪代码中看到的那样。 但实际上,我们通常会将若干个实例分组并将它们馈送到神经网络中,每个组更新模型参数,而不是每个实例。 我们将这组实例称为一个批次。
批处理是一个好主意,原因有几个。第一个是稳定性。任何数据都存在噪声。您的数据集可能包含采样和标记错误。如果您为每个实例更新模型参数,并且某些实例包含错误,则更新受到噪声的影响太大。但是,如果您将实例分组为批次,并为整个批次计算损失,而不是为单个实例计算,您可以“平均”小错误,并且反馈到您的模型会稳定下来。
第二个原因是速度。训练神经网络涉及大量的算术操作,如矩阵加法和乘法,并且通常在 GPU(图形处理单元)上进行。因为 GPU 被设计成可以并行处理大量的算术操作,所以如果您一次传递大量数据并一次处理它,而不是逐个传递实例,通常会更有效率。把 GPU 想象成一个海外的工厂,根据您的规格制造产品。因为工厂通常被优化为大量制造少量种类的产品,并且在通信和运输产品方面存在开销,所以如果您为大量产品制造少量订单,而不是为少量产品制造大量订单,即使您希望以任何方式获得相同数量的产品,也更有效率。
使用 AllenNLP 轻松将实例分组成批次。该框架使用 PyTorch 的 DataLoader 抽象,负责接收实例并返回批次。我们将使用一个 BucketBatchSampler,它将实例分组成长度相似的桶,如下代码片段所示。我将在后面的章节中讨论它的重要性:
sampler = BucketBatchSampler(batch_size=32, sorting_keys=["tokens"]) train_data_loader = MultiProcessDataLoader(reader, train_path, batch_sampler=sampler) dev_data_loader = MultiProcessDataLoader(reader, dev_path, batch_sampler=sampler)
参数 batch_size 指定了批量的大小(批量中的实例数)。调整此参数通常有一个“最佳点”。它应该足够大,以产生我之前提到的批处理的任何效果,但也应该足够小,以便批次适合 GPU 内存,因为工厂有一次可以制造的产品的最大容量。
2.6.2 将一切放在一起
现在您已经准备好训练情感分析器了。我们假设您已经定义并初始化了您的模型如下:
model = LstmClassifier(word_embeddings, encoder, vocab)
查看完整的代码清单(www.realworldnlpbook.com/ch2.html#sst-nb
),了解模型的外观和如何使用它。
AllenNLP 提供了 Trainer 类,它作为将所有组件放在一起并管理训练流水线的框架,如下所示:
trainer = GradientDescentTrainer( model=model, optimizer=optimizer, data_loader=train_data_loader, validation_data_loader=dev_data_loader, patience=10, num_epochs=20, cuda_device=-1) trainer.train()
你向训练器提供模型、优化器、迭代器、训练集、开发集和你想要的时期数,并调用 train 方法。最后一个参数,cuda_device,告诉训练器使用哪个设备(CPU 或 GPU)进行训练。在这里,我们明确地使用 CPU。这将运行列在列表 2.2 中的神经网络训练循环,并显示进展情况,包括评估指标。
2.7 评估你的分类器
当训练自然语言处理/机器学习模型时,你应该始终监控损失随时间的变化。如果训练正常进行,你应该看到损失随时间而减少。它不一定每个时期都会减少,但作为一般趋势,它应该会减少,因为这正是你告诉优化器要做的事情。如果它在增加或显示出奇怪的值(如 NaN),通常意味着你的模型过于局限或代码中存在错误的迹象。
除了损失之外,监控你在任务中关心的其他评估指标也很重要。损失是一个纯数学概念,衡量了模型与答案之间的接近程度,但较小的损失并不总是能保证在自然语言处理任务中获得更好的性能。
你可以使用许多评估指标,取决于你的自然语言处理任务的性质,但无论你在做什么任务,你都需要了解的一些指标包括准确率、精确率、召回率和 F-度量。粗略地说,这些指标衡量了你的模型预测与数据集定义的预期答案匹配的程度。暂时来说,知道它们用于衡量分类器的好坏就足够了(更多细节将在第四章介绍)。
要在训练期间使用 AllenNLP 监控和报告评估指标,你需要在你的模型类中实现 get_metrics() 方法,该方法返回从指标名称到它们的值的字典,如下所示。
列表 2.3 定义评估指标
def get_metrics(self, reset: bool = False) -> Dict[str, float]: return {'accuracy': self.accuracy.get_metric(reset), **self.f1_measure.get_metric(reset)}
self.accuracy 和 self.f1_measure 在 init() 中定义如下:
self.accuracy = CategoricalAccuracy() self.f1_measure = F1Measure(positive_index)
当你使用定义好的指标运行 trainer.train() 时,你会在每个时期后看到类似下面的进度条:
accuracy: 0.7268, precision: 0.8206, recall: 0.8703, f1: 0.8448, batch_loss: 0.7609, loss: 0.7194 ||: 100%|##########| 267/267 [00:13<00:00, 19.28it/s] accuracy: 0.3460, precision: 0.3476, recall: 0.3939, f1: 0.3693, batch_loss: 1.5834, loss: 1.9942 ||: 100%|##########| 35/35 [00:00<00:00, 119.53it/s]
你可以看到训练框架报告了这些指标,分别针对训练集和验证集。这不仅有助于评估模型,还有助于监控训练的进展。如果看到任何异常值,比如极低或极高的数字,你会知道出了问题,甚至在训练完成之前就能发现。
你可能已经注意到训练集和验证集的指标之间存在很大的差距。具体来说,训练集的指标比验证集的指标高得多。这是过拟合的常见症状,我之前提到过,即模型在训练集上拟合得非常好,以至于失去了在外部的泛化能力。这就是为什么监控指标使用验证集也很重要,因为仅仅通过观察训练集的指标,你无法知道它是表现良好还是过拟合!
2.8 部署你的应用程序
制作自己的 NLP 应用程序的最后一步是部署它。训练模型只是故事的一半。你需要设置它,以便它可以为它从未见过的新实例进行预测。确保模型提供预测在实际的 NLP 应用程序中是至关重要的,而且在这个阶段可能会投入大量的开发工作。在本节中,我将展示如何使用 AllenNLP 部署我们刚刚训练的模型。这个主题在第十一章中会更详细地讨论。
2.8.1 进行预测
要对你的模型从未见过的新实例进行预测(称为测试实例),你需要通过与训练相同的神经网络管道来传递它们。它必须完全相同——否则,你将冒着结果扭曲的风险。这被称为训练-服务偏差,我将在第十一章中解释。
AllenNLP 提供了一个方便的抽象称为预测器,它的工作是接收原始形式的输入(例如,原始字符串),将其通过预处理和神经网络管道传递,并返回结果。我为 SST 编写了一个特定的预测器称为 SentenceClassifierPredictor(realworldnlpbook.com/ch2 .html#predictor
),你可以按照以下方式调用它:
predictor = SentenceClassifierPredictor(model, dataset_reader=reader) logits = predictor.predict('This is the best movie ever!')['logits']
注意,预测器返回模型的原始输出,在这种情况下是 logits。记住,logits 是与目标标签对应的一些分数,所以如果你想要预测的标签本身,你需要将其转换为标签。你现在不需要理解所有的细节,但可以通过首先取 logits 的 argmax 来完成这个操作,argmax 返回具有最大值的 logit 的索引,然后通过查找 ID 来获取标签,如下所示:
label_id = np.argmax(logits) print(model.vocab.get_token_from_index(label_id, 'labels'))
如果这个打印出“4”,那么恭喜你!标签“4”对应着“非常积极”,所以你的情感分析器刚刚预测到句子“这是有史以来最好的电影!”是非常积极的,这的确是正确的。
2.8.2 提供预测
最后,你可以使用 AllenNLP 轻松部署训练好的模型。如果你使用 JSON 配置文件(我将在第四章中解释),你可以将训练好的模型保存到磁盘上,然后快速启动一个基于 Web 的界面,你可以向你的模型发送请求。要做到这一点,你需要安装 allennlp-server,这是一个为 AllenNLP 提供预测的 Web 接口的插件,如下所示:
git clone https:/./github.com/allenai/allennlp-server pip install —editable allennlp-server
假设你的模型保存在 examples/sentiment/model 下,你可以使用以下 AllenNLP 命令运行一个基于 Python 的 Web 应用程序:
$ allennlp serve \ --archive-path examples/sentiment/model/model.tar.gz \ --include-package examples.sentiment.sst_classifier \ --predictor sentence_classifier_predictor \ --field-name sentence
如果你使用浏览器打开 http:/./localhost:8000/,你将看到图 2.10 中显示的界面。
图 2.10 在 Web 浏览器上运行情感分析器
尝试在句子文本框中输入一些句子,然后点击预测。您应该在屏幕右侧看到逻辑值。它们只是一组原始的逻辑值,很难阅读,但您可以看到第四个值(对应标签“非常积极”)是最大的,模型正在按预期工作。
您还可以直接从命令行向后端进行 POST 请求,如下所示:
curl -d '{"sentence": "This is the best movie ever!"}' -H "Content-Type: application/json" \ -X POST http:/./localhost:8000/predict
这应该返回与上面看到的相同的 JSON:
{"logits":[-0.2549717128276825,-0.35388273000717163, -0.0826418399810791,0.7183976173400879,0.23161858320236206]}
好了,就到这里吧。在这一章中我们讨论了很多内容,但不要担心——我只是想告诉你,构建一个实际可用的自然语言处理应用程序是很容易的。也许你曾经发现一些关于神经网络和深度学习的书籍或在线教程让人望而生畏,甚至在创建任何实际可用的东西之前就放弃了学习。请注意,我甚至没有提到任何诸如神经元、激活、梯度和偏导数等概念,这些概念通常是其他学习资料在最初阶段教授的。这些概念确实很重要,而且有助于了解,但多亏了强大的框架如 AllenNLP,你也能够构建实用的自然语言处理应用程序,而不必完全了解其细节。在后面的章节中,我将更详细地讨论这些概念。
摘要
- 情感分析是一种文本分析技术,用于自动识别文本中的主观信息,如其极性(积极或消极)。
- 训练集、开发集和测试集用于训练、选择和评估机器学习模型。
- 词嵌入使用实数向量表示单词的含义。
- 循环神经网络(RNN)和线性层用于将一个向量转换为另一个不同大小的向量。
- 使用优化器训练神经网络,以使损失(实际输出与期望输出之间的差异)最小化。
- 在训练过程中监视训练集和开发集的指标非常重要,以避免过拟合。
第三章:单词和文档嵌入
本章包括
- 单词嵌入是什么以及它们为什么重要
- Skip-gram 模型如何学习单词嵌入以及如何实现它
- GloVe 嵌入是什么以及如何使用预训练的向量
- 如何使用 Doc2Vec 和 fastText 训练更高级的嵌入
- 如何可视化单词嵌入
在第二章中,我指出神经网络只能处理数字,而自然语言中几乎所有内容都是离散的(即,分离的概念)。要在自然语言处理应用中使用神经网络,你需要将语言单位转换为数字,例如向量。例如,如果你希望构建一个情感分析器,你需要将输入句子(单词序列)转换为向量序列。在本章中,我们将讨论单词嵌入,这是实现这种桥接的关键。我们还将简要介绍几个在理解嵌入和神经网络的一般性质中重要的基本语言组件。
3.1 引入嵌入
正如我们在第二章中讨论的,嵌入是通常离散的事物的实值向量表示。在本节中,我们将重新讨论嵌入是什么,并详细讨论它们在自然语言处理应用中的作用。
3.1.1 什么是嵌入?
单词嵌入是一个单词的实值向量表示。如果你觉得向量的概念令人生畏,可以把它们想象成一维的浮点数数组,就像下面这样:
- vec(“cat”) = [0.7, 0.5, 0.1]
- vec(“dog”) = [0.8, 0.3, 0.1]
- vec(“pizza”) = [0.1, 0.2, 0.8]
因为每个数组都包含三个元素,你可以将它们绘制为三维空间中的点,如图 3.1 所示。请注意,语义相关的单词(“猫”和“狗”)被放置在彼此附近。
图 3.1 单词嵌入在三维空间中
注意 实际上,你可以嵌入(即,用一系列数字表示)不仅仅是单词,还有几乎任何东西 —— 字符、字符序列、句子或类别。你可以使用相同的方法嵌入任何分类变量,尽管在本章中,我们将专注于自然语言处理中两个最重要的概念 —— 单词和句子。
3.1.2 嵌入为什么重要?
嵌入为什么重要?嗯,单词嵌入不仅重要,而且 至关重要 用于使用神经网络解决自然语言处理任务。神经网络是纯数学计算模型,只能处理数字。它们无法进行符号操作,例如连接两个字符串或使动词变为过去时,除非这些项目都用数字和算术操作表示。另一方面,自然语言处理中的几乎所有内容,如单词和标签,都是符号和离散的。这就是为什么你需要连接这两个世界,使用嵌入就是一种方法。请参阅图 3.2,了解如何在自然语言处理应用中使用单词嵌入的概述。
图 3.2 使用词嵌入与 NLP 模型
词嵌入,就像任何其他神经网络模型一样,可以进行训练,因为它们只是一组参数(或“魔法常数”,我们在上一章中谈到过)。词嵌入在以下三种情况下与您的 NLP 模型一起使用:
- 情况 1:同时使用任务的训练集训练词嵌入和您的模型。
- 情况 2:首先,独立训练词嵌入使用更大的文本数据集。或者,从其他地方获取预训练的词嵌入。然后使用预训练的词嵌入初始化您的模型,并同时使用任务的训练集对它们和您的模型进行训练。
- 情况 3:与情况 2 相同,除了您在训练模型时固定词嵌入。
在第一种情况下,词嵌入是随机初始化的,并且与您的 NLP 模型一起使用相同的数据集进行训练。这基本上就是我们在第二章中构建情感分析器的方式。用一个类比来说,这就像一个舞蹈老师同时教一个婴儿走路和跳舞。这并不是完全不可能的事情(事实上,有些婴儿可能通过跳过走路部分而成为更好、更有创意的舞者,但不要在家里尝试这样做),但很少是一个好主意。如果先教会婴儿正确站立和行走,然后再教会他们如何跳舞,他们可能会有更好的机会。
类似地,同时训练 NLP 模型和其子组件词嵌入并不罕见。但是,许多大规模、高性能的 NLP 模型通常依赖于使用更大数据集预训练的外部词嵌入(情况 2 和 3)。词嵌入可以从未标记的大型文本数据集中学习,即大量的纯文本数据(例如维基百科转储),这通常比用于任务的训练数据集(例如斯坦福情感树库)更容易获得。通过利用这样的大量文本数据,您可以在模型看到任务数据集中的任何实例之前就向其教授关于自然语言的许多知识。在一个任务上训练机器学习模型,然后为另一个任务重新利用它被称为迁移学习,这在许多机器学习领域中,包括 NLP 在内,变得越来越受欢迎。我们将在第九章进一步讨论迁移学习。
再次使用跳舞婴儿的类比,大多数健康的婴儿都会自己学会站立和行走。他们可能会得到一些成人的帮助,通常来自他们的亲近照顾者,比如父母。然而,这种“帮助”通常比从聘请的舞蹈老师那里得到的“训练信号”丰富得多,也更便宜,这就是为什么如果他们先学会走路,然后再学会跳舞,效果会更好的原因。许多用于行走的技能会转移到跳舞上。
方案 2 和方案 3 之间的区别在于,在训练 NLP 模型时是否调整了词嵌入,或者精调了词嵌入。这是否有效可能取决于你的任务和数据集。教你的幼儿芭蕾可能会对他们的步履有好处(例如通过改善他们的姿势),从而可能对他们的舞蹈有积极的影响,但是方案 3 不允许发生这种情况。
你可能会问的最后一个问题是:嵌入是怎么来的呢?之前我提到过,它们可以从大量的纯文本中进行训练。本章将解释这是如何实现的以及使用了哪些模型。
3.2 语言的基本单元:字符、词和短语
在解释词嵌入模型之前,我会简单介绍一些语言的基本概念,如字符、词和短语。当你设计你的 NLP 应用程序的结构时,了解这些概念将会有所帮助。图 3.3 展示了一些例子。
3.2.1 字符
字符(在语言学中也称为字形)是书写系统中的最小单位。在英语中,“a”、“b"和"z"都是字符。字符本身并不一定具有意义,也不一定在口语中代表任何固定的声音,尽管在某些语言中(例如中文)大多数字符都有这样的特点。许多语言中的典型字符可以用单个 Unicode 代码点(通过 Python 中的字符串文字,如”\uXXXX")表示,但并非总是如此。许多语言使用多个 Unicode 代码点的组合(例如重音符号)来表示一个字符。标点符号,如".“(句号)、”,“(逗号)和”?"(问号),也是字符。
图 3.3 NLP 中使用的语言基本单元
3.2.2 词、标记、词素和短语
词是语言中可以独立发音并通常具有一定意义的最小单位。在英语中,“apple”、"banana"和"zebra"都是词。在大多数使用字母脚本的书面语言中,词通常由空格或标点符号分隔。然而,在一些语言(如中文、日文和泰文)中,词并没有明确由空格分隔,并且需要通过预处理步骤(称为分词)来识别句子中的词。
NLP 中与单词相关的一个概念是标记。标记是在书面语言中扮演特定角色的连续字符字符串。大多数单词(“苹果”,“香蕉”,“斑马”)在书写时也是标记。标点符号(如感叹号“!”)是标记,但不是单词,因为不能单独发音。在 NLP 中,“单词”和“标记”通常可以互换使用。实际上,在 NLP 文本(包括本书)中,当你看到“单词”时,通常指的是“标记”,因为大多数 NLP 任务只处理以自动方式处理的书面文本。标记是一个称为标记化的过程的输出,我将在下面进行详细解释。
另一个相关概念是形态素。形态素是语言中的最小意义单位。一个典型的单词由一个或多个形态素组成。例如,“苹果”既是一个单词,也是一个形态素。“苹果”是由两个形态素“苹果”和“-s”组成的单词,用来表示名词的复数形式。英语中还包含许多其他形态素,包括“-ing”,“-ly”,“-ness”和“un-”。在单词或句子中识别形态素的过程称为形态分析,它在 NLP/语言学应用中有广泛的应用,但超出了本书的范围。
短语是一组在语法角色上扮演特定角色的单词。例如,“the quick brown fox”是一个名词短语(像一个名词那样表现的一组词),而“jumps over the lazy dog”是一个动词短语。在 NLP 中,短语的概念可能被宽泛地用来表示任何一组单词。例如,在许多 NLP 文献和任务中,像“洛杉矶”这样的词被视为短语,虽然在语言学上,它们更接近一个词。
3.2.3 N-grams
最后,在 NLP 中,你可能会遇到n-gram的概念。n-gram 是一个或多个语言单位(如字符和单词)的连续序列。例如,一个单词 n-gram 是一个连续的单词序列,如“the”(一个单词),“quick brown”(两个单词),“brown fox jumps”(三个单词)。同样,字符 n-gram 由字符组成,例如“b”(一个字符),“br”(两个字符),“row”(三个字符)等,它们都是由“brown”组成的字符 n-gram。当 n = 1 时,大小为 1 的 n-gram 称为unigram。大小为 2 和 3 的 n-gram 分别被称为bigram和trigram。
在 NLP 中,单词 n-gram 通常被用作短语的代理,因为如果枚举一个句子的所有 n-gram,它们通常包含语言上有趣的单元,与短语(例如“洛杉矶”和“起飞”)对应。类似地,当我们想捕捉大致对应于形态素的子词单位时,我们使用字符 n-gram。在 NLP 中,当你看到“n-grams”(没有限定词)时,它们通常是单词n-gram。
注意:有趣的是,在搜索和信息检索中,n-grams 通常指用于索引文档的字符 n-grams。当你阅读论文时,要注意上下文暗示的是哪种类型的 n-grams。
3.3 分词、词干提取和词形还原
我们介绍了在自然语言处理中经常遇到的一些基本语言单位。在本节中,我将介绍一些典型自然语言处理流水线中处理语言单位的步骤。
3.3.1 分词
分词 是将输入文本分割成较小单元的过程。有两种类型的分词:单词分词和句子分词。单词分词 将一个句子分割成标记(大致相当于单词和标点符号),我之前提到过。句子分词 则将可能包含多个句子的文本分割成单个句子。如果说分词,通常指的是 NLP 中的单词分词。
许多自然语言处理库和框架支持分词,因为它是自然语言处理中最基本且最常用的预处理步骤之一。接下来,我将向你展示如何使用两个流行的自然语言处理库——NLTK (www.nltk.org/
) 和 spaCy (spacy.io/
) 进行分词。
注意:在运行本节示例代码之前,请确保两个库都已安装。在典型的 Python 环境中,可以通过运行 pip install nltk 和 pip install spacy 进行安装。安装完成后,你需要通过命令行运行 python -c “import nltk; nltk.download(‘punkt’)” 来下载 NLTK 所需的数据和模型,以及通过 python -m spacy download en 来下载 spaCy 所需的数据和模型。你还可以通过 Google Colab (realworldnlpbook.com/ch3.html#tokeni zation
) 在不安装任何 Python 环境或依赖项的情况下运行本节中的所有示例。
要使用 NLTK 的默认单词和句子分词器,你可以从 nltk.tokenize 包中导入它们,如下所示:
>>> import nltk >>> from nltk.tokenize import word_tokenize, sent_tokenize
你可以用一个字符串调用这些方法,它们会返回一个单词或句子的列表,如下所示:
>>> s = '''Good muffins cost $3.88\nin New York. Please buy me two of them.\n\nThanks.''' >>> word_tokenize(s) ['Good', 'muffins', 'cost', '$', '3.88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] >>> sent_tokenize(s) ['Good muffins cost $3.88\nin New York.', 'Please buy me two of them.', 'Thanks.']
NLTK 实现了除我们在此使用的默认方法之外的各种分词器。如果你有兴趣探索更多选项,可以参考其文档页面 (www.nltk.org/api/nltk.tokenize.html
)。
你可以使用 spaCy 如下进行单词和句子的分词:
>>> import spacy >>> nlp = spacy.load('en_core_web_sm') >>> doc = nlp(s) >>> [token.text for token in doc] ['Good', 'muffins', 'cost', '$', '3.88', '\n', 'in', 'New', 'York', '.', ' ', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', '\n\n', 'Thanks', '.'] >>> [sent.string.strip() for sent in doc.sents] ['Good muffins cost $3.88\nin New York.', 'Please buy me two of them.', 'Thanks.']
请注意,NLTK 和 spaCy 的结果略有不同。例如,spaCy 的单词分词器保留换行符(‘\n’)。标记器的行为因实现而异,并且没有每个自然语言处理从业者都同意的单一标准解决方案。尽管标准库(如 NLTK 和 spaCy)提供了一个良好的基线,但根据您的任务和数据,准备好进行实验。此外,如果您处理的是英语以外的语言,则您的选择可能会有所不同(并且可能会根据语言而有所限制)。如果您熟悉 Java 生态系统,Stanford CoreNLP(stanfordnlp.github.io/CoreNLP/
)是另一个值得一试的良好自然语言处理框架。
最后,用于基于神经网络的自然语言处理模型的一个日益流行和重要的标记化方法是字节对编码(BPE)。字节对编码是一种纯统计技术,将文本分割成任何语言的字符序列,不依赖于启发式规则(如空格和标点符号),而只依赖于数据集中的字符统计信息。我们将在第十章更深入地学习字节对编码。
真实世界的自然语言处理(一)(3)https://developer.aliyun.com/article/1519727