精通-Spacy-全-
精通 Spacy(全)
原文:
zh.annas-archive.org/md5/d848687a58d83b16a77351263479306d译者:飞龙
前言
spaCy 是一个工业级、高效的 NLP Python 库。它提供了各种预训练模型和即用功能。精通 spaCy 为你提供了 spaCy 功能和真实世界应用的端到端覆盖。
你将首先安装 spaCy 并下载模型,然后逐步学习 spaCy 的功能和原型化真实世界的 NLP 应用。接下来,你将熟悉使用 spaCy 流行的可视化工具 displaCy 进行可视化。本书还为你提供了模式匹配的实用示例,帮助你进入语义世界的词向量领域。统计信息提取方法也进行了详细解释。随后,你将涵盖一个交互式商业案例研究,展示如何结合 spaCy 功能创建真实世界的 NLP 流程。你将实现诸如情感分析、意图识别和上下文解析等机器学习模型。本书还进一步关注使用 TensorFlow 的 Keras API 与 spaCy 一起进行分类,包括意图分类和情感分析,以及它们在流行数据集上的应用和分类结果的解释。
在本书结束时,你将能够自信地使用 spaCy,包括其语言功能、词向量和分类器,来创建自己的 NLP 应用。
本书面向对象
本书面向希望精通 NLP 的数据科学家和机器学习者,以及希望掌握 spaCy 并用它构建应用的 NLP 开发者。希望亲自动手使用 Python 和 spaCy 的语言和语音专业人士,以及希望快速使用 spaCy 原型化应用的软件开发者,也会发现本书很有帮助。为了充分利用本书,需要具备 Python 编程语言的入门级知识。对语言学术语的入门级理解,例如解析、词性标注和语义相似性,也将很有用。
本书涵盖内容
第一章,spaCy 入门,开始你的 spaCy 之旅。本章为你提供了 Python 中 NLP 的概述。在本章中,你将安装 spaCy 库和 spaCy 语言模型,并探索 displaCy,spaCy 的可视化工具。总体而言,本章将帮助你开始安装和理解 spaCy 库。
第二章,使用 spaCy 的核心操作,教你 spaCy 的核心操作,例如创建语言流程、分词文本以及将文本分解成句子以及 Container 类。本章详细介绍了 Container 类 token、Doc 和 Span。
第三章, 语言特征,深入挖掘 spaCy 的全部功能。本章探讨了语言特征,包括 spaCy 最常用的功能,如词性标注器、依存句法分析器、命名实体识别器和合并/拆分。
第四章, 基于规则的匹配,教您如何通过匹配模式和短语从文本中提取信息。您将使用形态学特征、词性标注、正则表达式和其他 spaCy 功能来形成模式对象,以供 spaCy 匹配器对象使用。
第五章, 与词向量及语义相似性工作,教您关于词向量及其相关语义相似性方法。本章包括词向量计算,如距离计算、类比计算和可视化。
第六章, 将一切整合:使用 spaCy 进行语义解析,是一个完全实战的章节。本章教您如何使用 spaCy 设计一个针对航空公司旅行信息系统(ATIS)的票务预订系统 NLU,这是一个著名的飞机票务预订系统数据集。
第七章, 自定义 spaCy 模型,教您如何训练、存储和使用自定义的统计管道组件。您将学习如何使用自己的数据更新现有的统计管道组件,以及如何从头开始使用自己的数据和标签创建统计管道组件。
第八章, 使用 spaCy 进行文本分类,教您如何进行 NLP 中一个非常基本且流行的任务:文本分类。本章探讨了使用 spaCy 的Textcategorizer组件以及使用 TensorFlow 和 Keras 进行文本分类。
第九章, spaCy 与 Transformers,探讨了 NLP 中最热门的最新话题——transformers,以及如何与 TensorFlow 和 spaCy 一起使用。您将学习如何使用 BERT 和 TensorFlow,以及 spaCy v3.0 基于 transformer 的预训练管道。
第十章, 将一切整合:使用 spaCy 设计您的聊天机器人,带您进入对话式人工智能的世界。您将在一个真实的餐厅预订数据集上执行实体提取、意图识别和上下文处理。
为了充分利用这本书
首先,您需要在您的系统上安装并运行 Python 3。代码示例使用 spaCy v3.0 进行测试,然而,由于向后兼容性,大多数代码与 spaCy v2.3 兼容。对于 scikit-learn、pandas、NumPy 和 matplotlib 等辅助库,可以使用 pip 上可用的最新版本。我们从第七章**,自定义 spaCy 模型开始使用 TensorFlow、transformers 和辅助库,因此您可以在到达第七章时安装这些库。

我们不时使用 Jupyter 笔记本。您可以在书的 GitHub 页面上查看笔记本。如果您想使用 Jupyter 笔记本,那很好;您可以通过 pip 安装 Jupyter。如果您不想,您仍然可以将代码复制粘贴到 Python shell 中并使代码工作。
如果您使用的是本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将有助于您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Mastering-spaCy。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781800563353_ColorImages.pdf.
使用的约定
本书使用了多种文本约定。
文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“最后,validation_split参数用于评估实验。”
代码块设置如下:
import spacy
nlp = spacy.load("en_subwords_wiki_lg")
任何命令行输入或输出都按以下方式编写:
wget https://github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter08/data/Reviews.zip
粗体: 表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“以下图表说明了狗和猫之间的距离,以及狗、犬种猎犬和猫之间的距离:”
小贴士或重要提示
显示如下。
联系我们
我们欢迎读者的反馈。
customercare@packtpub.com.
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一情况。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
copyright@packt.com 并附有链接到相关材料。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解更多关于 Packt 的信息,请访问 packt.com。
第一部分:spaCy 入门
本节将从使用 Python 和 spaCy 的自然语言处理(NLP)概述开始。您将了解本书的组织结构和如何最大限度地利用本书。然后,您将开始安装 spaCy 及其统计模型,并快速进入 spaCy 的世界。基本操作、通用约定和可视化是本节的核心吸引力。
本节包含以下章节:
-
第一章, spaCy 入门
-
第二章, 使用 spaCy 的核心操作
第一章:spaCy 入门
在本章中,我们将全面介绍使用 Python 和 spaCy 开发的自然语言处理(NLP)应用程序开发。首先,我们将看到 NLP 开发如何与 Python 密不可分,以及 spaCy 作为 Python 库提供的内容概述。
经过热身之后,您将快速开始使用 spaCy,通过下载库和加载模型。然后,您将通过可视化 spaCy 的几个功能来探索 spaCy 的流行可视化器 displaCy。
到本章结束时,您将了解使用 spaCy 可以实现什么,以及如何使用 spaCy 代码规划您的旅程。您也将对您的开发环境感到满意,因为您已经在接下来的章节中安装了所有必要的 NLP 任务包。
在本章中,我们将涵盖以下主要主题:
-
spaCy 概述
-
安装 spaCy
-
安装 spaCy 的统计模型
-
使用 displaCy 进行可视化
技术要求
本章代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter01
spaCy 概述
在开始学习 spaCy 代码之前,我们将首先概述 NLP 在现实生活中的应用、使用 Python 的 NLP 以及使用 spaCy 的 NLP。在本节中,我们将了解为什么使用 Python 和 spaCy 开发 NLP 应用程序的原因。我们将首先看到 Python 如何与文本处理紧密结合,然后我们将了解 spaCy 在 Python NLP 库中的位置。让我们从 Python 和 NLP 之间的紧密关系开始我们的探索之旅。
NLP 的兴起
在过去的几年里,人工智能的许多分支都引起了很大的关注,包括 NLP、计算机视觉和预测分析等。但 NLP 究竟是什么?机器或代码如何解决人类语言?
NLP 是人工智能的一个子领域,它分析文本、语音和其他形式的人类生成语言数据。人类语言很复杂——即使是简短的段落也包含对先前单词的引用、指向现实世界对象的指针、文化引用以及作者或说话者的个人经历。图 1.1 展示了这样一个示例句子,它包括对相对日期(最近)的引用,只有了解说话者的人才能解决的短语(关于说话者父母居住的城市),以及拥有关于世界的普遍知识的人(城市是人类共同生活的地方):

图 1.1 – 包含许多认知和文化方面的人类语言示例
那么,我们如何处理这样一个复杂结构呢?我们也有我们的武器;我们用统计模型来模拟自然语言,并处理语言特征,将文本转换为良好的结构化表示。这本书为你提供了所有必要的背景和工具,让你能够从文本中提取意义。到这本书的结尾,你将拥有使用一个伟大的工具——spaCy 库来处理文本的统计和语言知识。
尽管自然语言处理最近才流行起来,但通过许多现实世界的应用,包括搜索引擎、翻译服务和推荐引擎,处理人类语言已经存在于我们的生活中。
搜索引擎如 Google 搜索、Yahoo 搜索和 Microsoft Bing 是我们日常生活的重要组成部分。我们寻找家庭作业帮助、烹饪食谱、名人信息、我们最喜欢的电视剧的最新一集;我们日常生活中使用的各种信息。甚至在英语中(也在许多其他语言中)有一个动词 to google,意思是 在 Google 搜索引擎上查找一些信息。
搜索引擎使用高级 NLP 技术,包括将查询映射到语义空间,其中相似的查询由相似的向量表示。一个快速技巧叫做 自动完成,当我们输入查询的前几个字母时,查询建议就会出现在搜索栏上。自动完成看起来很复杂,但的确算法是搜索树遍历和字符级距离计算的组合。一个过去的查询由其字符序列表示,其中每个字符对应于搜索树中的一个节点。字符之间的 弧 根据过去查询的流行度分配权重。
然后,当一个新的查询到来时,我们会通过遍历树来比较当前查询字符串和过去的查询。一个基本的计算机科学(CS)数据结构,树,被用来表示查询列表,谁能想到呢?图 1.2 展示了在字符树上的遍历:

图 1.2 – 自动完成示例
这是一个简化的解释;真正的算法通常融合了多种技术。如果你想了解更多关于这个主题的信息,你可以阅读关于数据结构的优秀文章:blog.notdot.net/2010/07/Damn-Cool-Algorithms-Levenshtein-Automata 和 blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK-Trees。
继续讨论搜索引擎,搜索引擎也知道如何将非结构化数据转换为结构化和关联数据。当我们输入 Diana Spencer 到搜索栏中,就会出现以下内容:

图 1.3 – 搜索查询 "Diana Spencer" 的结果
搜索引擎是如何将“黛安娜·斯宾塞”这个名字与她的著名名字戴安娜公主联系起来的?这被称为实体链接。我们链接提及相同真实世界实体的实体。实体链接算法关注表示语义关系和知识。这个 NLP 领域被称为语义网。你可以在www.cambridgesemantics.com/blog/semantic-university/intro-semantic-web/了解更多信息。我在职业生涯初期在一家搜索引擎公司担任知识工程师,并真正享受这份工作。这是 NLP 中的一个非常吸引人的主题。
你可以开发的内容实际上没有限制:搜索引擎算法、聊天机器人、语音识别应用和用户情感识别应用。NLP 问题具有挑战性但非常吸引人。本书的使命是为你提供一个包含所有必要工具的工具箱。NLP 开发的第一步是明智地选择我们将使用的编程语言。在下一节中,我们将解释为什么 Python 是首选武器。让我们继续到下一节,看看 NLP 和 Python 之间的字符串联系。
使用 Python 进行 NLP
正如我们之前提到的,自然语言处理(NLP)是人工智能的一个子领域,它分析文本、语音和其他形式的人类生成语言数据。作为一名行业专业人士,我处理文本数据的第一选择是 Python。一般来说,使用 Python 有很多好处:
-
它易于阅读,看起来非常类似于伪代码。
-
它易于编写和测试代码。
-
它具有高度的抽象级别。
由于以下原因,Python 是开发 NLP 系统的绝佳选择:
-
简单性:Python 易于学习。你可以专注于 NLP 而不是编程语言的细节。
-
效率:它允许更容易地开发快速 NLP 应用原型。
-
流行度:Python 是最受欢迎的语言之一。它拥有庞大的社区支持,使用 pip 安装新库非常容易。
-
AI 生态系统存在:Python 中有大量的开源 NLP 库。许多机器学习(ML)库,如 PyTorch、TensorFlow 和 Apache Spark,也提供了 Python API。
-
sentenc.split(),在其他语言中可能相当痛苦,例如 C++,你必须处理流对象来完成这个任务。
当我们将所有前面的点结合起来时,以下图像就会出现——Python 与字符串处理、AI 生态系统和 ML 库相交,为我们提供最佳的 NLP 开发体验:

图 1.4 – 使用 Python 的 NLP 概述
我们将在这本书中使用 Python 3.5+。尚未安装 Python 的用户可以按照realpython.com/installing-python/上的说明进行操作。我们建议下载并使用 Python 3 的最新版本。
在 Python 3.x 中,默认编码是Unicode,这意味着我们可以使用 Unicode 文本而无需过多担心编码。我们不会在这里详细介绍编码的细节,但你可以将 Unicode 视为 ASCII 的扩展集,包括更多字符,如德语字母的重音符号和法语字母的带音符号。这样我们就可以处理德语、法语以及许多其他非英语语言。
复习一些有用的字符串操作
在 Python 中,文本由str类表示。字符串是不可变的字符序列。创建字符串对象很简单——我们用引号包围文本:
word = 'Hello World'
现在的word变量包含字符串Hello World。正如我们提到的,字符串是字符序列,因此我们可以请求序列的第一个元素:
print (word [0])
H
总是记得在print中使用括号,因为我们正在使用 Python 3.x 进行编码。我们可以以类似的方式访问其他索引,只要索引没有超出范围:
word [4]
'o'
字符串长度如何?我们可以使用len方法,就像使用list和其他序列类型一样:
len(word)
11
我们还可以使用序列方法遍历字符串的字符:
for ch in word:
print(ch)
H
e
l
l
o
W
o
r
l
d
小贴士
请注意本书中的缩进。在 Python 中,缩进是我们确定控制块和函数定义的一般方式,我们将在本书中应用此约定。
现在我们来回顾一些更复杂的字符串方法,比如计数字符、查找子字符串和更改字母大小写。
count方法计算字符串中字符的出现次数,所以这里的输出是3:
word.count('l')
3
通常,您需要找到字符的索引以进行一系列的子字符串操作,如切割和切片字符串:
word.index(e)
1
类似地,我们可以使用find方法在字符串中搜索子字符串:
word.find('World')
6
如果子字符串不在字符串中,find返回-1:
word.find('Bonjour')
-1
查找子字符串的最后出现也很简单:
word.rfind('l')
9
我们可以通过upper和lower方法更改字母的大小写:
word.upper()
'HELLO WORLD'
upper方法将所有字符转换为大写。同样,lower方法将所有字符转换为小写:
word.lower()
'hello world'
capitalize方法将字符串的第一个字符大写:
'hello madam'.capitalize()
'Hello madam'
title方法将字符串转换为标题大小写。标题大小写字面意思是制作标题,因此字符串中的每个单词都被大写:
'hello madam'.title()
'Hello Madam'
从其他字符串形成新字符串可以通过几种方式完成。我们可以通过相加来连接两个字符串:
'Hello Madam!' + 'Have a nice day.'
'Hello Madam!Have a nice day.'
我们还可以将字符串与一个整数相乘。输出将是字符串根据整数指定的次数进行拼接:
'sweet ' * 5
'sweet sweet sweet sweet '
join是一个常用的方法;它接受一个字符串列表并将它们连接成一个字符串:
' '.join (['hello', 'madam'])
'hello madam'
有许多子字符串方法。替换子字符串意味着将所有出现都替换为另一个字符串:
'hello madam'.replace('hello', 'good morning')
'good morning madam'
通过索引获取子字符串被称为切片。您可以通过指定起始索引和结束索引来切片字符串。如果我们只想获取第二个单词,我们可以这样做:
word = 'Hello Madam Flower'
word [6:11]
'Madam'
获取第一个单词类似。留空第一个索引意味着索引从零开始:
word [:5]
'Hello'
留空第二个索引也有特殊含义——它表示字符串的其余部分:
word [12:]
'Flower'
我们现在了解了一些 Pythonic NLP 操作。现在我们可以更深入地了解 spaCy。
获取 spaCy 库的高级概述
spaCy 是一个开源的 Python NLP 库,其创造者将其描述为 工业级 NLP,作为贡献者,我可以保证这是真的。spaCy 随带预训练的语言模型和 60 多种语言的词向量。
spaCy 专注于生产和发布代码,与其更学术的前辈不同。最著名且最常用的 Python 前辈是 NLTK。NLTK 的主要重点是向学生和研究人员提供一个语言处理的概念。它从未对效率、模型精度或成为工业级库提出任何要求。spaCy 从第一天起就专注于提供生产就绪的代码。您可以期望模型在真实世界数据上表现良好,代码效率高,能够在合理的时间内处理大量文本数据。以下是从 spaCy 文档(https://spacy.io/usage/facts-figures#speed-comparison)中的效率比较:

图 1.5 – spaCy 与其他流行 NLP 框架的速度比较
spaCy 的代码也是以专业的方式进行维护的,问题按标签分类,新版本尽可能覆盖所有修复。您始终可以在 spaCy GitHub 仓库 https://github.com/explosion/spaCy 上提出问题,报告错误,或向社区寻求帮助。
另一个前辈是 CoreNLP(也称为 StanfordNLP)。CoreNLP 是用 Java 实现的。尽管 CoreNLP 在效率方面有所竞争,但 Python 通过简单的原型设计和 spaCy 作为软件包的更专业性而获胜。代码得到了良好的维护,问题在 GitHub 上跟踪,每个问题都标记了一些标签(如错误、功能、新项目)。此外,库代码和模型的安装也很简单。与提供向后兼容性一起,这使得 spaCy 成为一个专业的软件项目。以下是 spaCy 文档中的详细比较 https://spacy.io/usage/facts-figures#comparison:

图 1.6 – spaCy、NLTK 和 CoreNLP 的功能比较
在本书中,我们将使用 spaCy 的最新版本 v2.3 和 v3.0(本书编写时使用的版本)来处理所有计算语言学和机器学习目的。以下是最新版本中的功能:
-
保留原始数据的分词。
-
统计句分割。
-
命名实体识别。
-
词性标注(POS)。
-
依存句法分析。
-
预训练词向量。
-
与流行的深度学习库轻松集成。spaCy 的 ML 库
Thinc提供了 PyTorch、TensorFlow 和 MXNet 的薄包装器。spaCy 还通过spacy-transformers库提供了HuggingFaceTransformers 的包装器。我们将在 第九章**,spaCy 和 Transformers 中看到更多关于Transformers的内容。 -
工业级速度。
-
内置的可视化工具,displaCy。
-
支持 60 多种语言。
-
16 种语言的 46 个最先进的统计模型。
-
空间高效的字符串数据结构。
-
高效的序列化。
-
简单的模型打包和使用。
-
大型社区支持。
我们快速浏览了 spaCy 作为 NLP 库和软件包。我们将在本书中详细探讨 spaCy 提供的内容。
读者小贴士
这本书是一本实用指南。为了最大限度地利用这本书,我建议读者在自己的 Python shell 中复现代码。如果不遵循并执行代码,就无法正确理解 NLP 概念和 spaCy 方法,这就是为什么我们将接下来的章节安排如下:
-
语言/ML 概念解释
-
使用 spaCy 的应用程序代码
-
结果评估
-
方法论的挑战
-
克服挑战的专业技巧和窍门
安装 spaCy
让我们从安装和设置 spaCy 开始。spaCy 与 64 位 Python 2.7 和 3.5+ 兼容,可以在 Unix/Linux、macOS/OS X 和 Windows 上运行。pip (https://pypi.org/) 和 conda (https://conda.io/en/latest/)。pip 和 conda 是最受欢迎的发行包之一。
pip 是最省心的选择,因为它会安装所有依赖项,所以让我们从这里开始。
使用 pip 安装 spaCy
你可以使用以下命令安装 spaCy:
$ pip install spacy
如果你已经在你的系统上安装了多个 Python 版本(例如 Python 2.8、Python 3.5、Python 3.8 等),那么请选择你想要使用的 Python 版本的 pip。例如,如果你想使用 Python 3.5 的 spaCy,你可以这样做:
$ pip3.5 install spacy
如果你已经在你的系统上安装了 spaCy,你可能想升级到 spaCy 的最新版本。本书中使用的是 spaCy 2.3;你可以使用以下命令检查你拥有哪个版本:
$ python –m spacy info
这就是版本信息输出的样子。这是在我的 Ubuntu 机器的帮助下生成的:

图 1.8 – Visual Studio 和 Python 发行版兼容性表
如果您到目前为止没有遇到任何问题,那么这意味着 spaCy 已安装并正在您的系统上运行。您应该能够将 spaCy 导入到您的 Python 命令行中:
import spacy
现在,您已成功安装 spaCy – 祝贺您并欢迎加入 spaCy 的大千世界!如果您遇到安装问题,请继续阅读下一节,否则您可以继续进行语言模型安装。
安装 spaCy 时的问题排除
在安装过程中可能会出现一些问题。好消息是,我们使用的是一个非常流行的库,所以很可能其他开发者已经遇到了相同的问题。大多数问题已在 Stack Overflow (stackoverflow.com/questions/tagged/spacy) 和 spaCy GitHub 问题部分 (github.com/explosion/spaCy/issues) 中列出。然而,在本节中,我们将讨论最常见的问题及其解决方案。
最常见的一些问题如下:
-
Python 发行版不兼容:在这种情况下,请相应地升级您的 Python 版本,然后进行全新安装。
-
升级破坏了 spaCy:很可能是您的安装目录中遗留了一些包。最好的解决方案是首先通过以下步骤完全删除 spaCy 包:
pip uninstall spacy然后按照提到的安装说明进行全新安装。
-
您无法在 Mac 上安装 spaCy:在 Mac 上,请确保您没有跳过以下步骤,以确保您正确安装了 Mac 命令行工具并启用了 pip:
$ xcode-select –install
通常,如果您有正确的 Python 依赖项,安装过程将顺利进行。
我们已经设置好并准备好使用 spaCy,让我们继续使用 spaCy 的语言模型。
安装 spaCy 的统计模型
spaCy 的安装不包括 spaCy 管道任务所需的统计语言模型。spaCy 语言模型包含从一组资源收集的特定语言知识。语言模型使我们能够执行各种 NLP 任务,包括词性标注和命名实体识别(NER)。
不同的语言有不同的模型,并且是针对特定语言的。同一语言也有不同的模型可供选择。我们将在本节末尾的小贴士中详细说明这些模型之间的差异,但基本上训练数据是不同的。底层统计算法是相同的。目前支持的一些语言如下:

图 1.9 – spaCy 模型概览
支持的语言数量正在迅速增长。你可以在spaCy 模型和语言页面(spacy.io/usage/models#languages)上查看支持的语言列表。
为不同的语言提供了几个预训练模型。对于英语,以下模型可供下载:en_core_web_sm、en_core_web_md和en_core_web_lg。这些模型使用以下命名约定:
-
en代表英语,de代表德语,等等。 -
core表示通用模型,用于词汇、语法、实体和向量。 -
web(维基百科),news(新闻,媒体)Twitter,等等。 -
lg代表大型,md代表中等,sm代表小型。
下面是一个典型的语言模型的样子:

图 1.10 – 小型 spaCy 英语网络模型
大型模型可能需要大量的磁盘空间,例如en_core_web_lg占用 746 MB,而en_core_web_md需要 48MB,en_core_web_sm仅占用 11MB。中等大小的模型适用于许多开发目的,因此本书中我们将使用英语md模型。
小贴士
它是一个好习惯,将模型类型与你的文本类型相匹配。我们建议尽可能选择与你的文本最接近的类型。例如,社交媒体类型的词汇将与维基百科类型的词汇非常不同。如果你有社交媒体帖子、报纸文章、财经新闻等,即更多来自日常生活的语言,你可以选择网络类型。维基百科类型适用于相当正式的文章、长文档和技术文档。如果你不确定哪种类型最适合,你可以下载几个模型,并测试一些来自你自己的语料库的示例句子,看看每个模型的表现如何。
既然我们已经了解了如何选择模型,让我们下载我们的第一个模型。
安装语言模型
自从 v1.7.0 版本以来,spaCy 提供了一项重大优势:将模型作为 Python 包安装。你可以像安装任何其他 Python 模块一样安装 spaCy 模型,并将它们作为你 Python 应用程序的一部分。它们有适当的版本控制,因此可以作为依赖项添加到你的requirements.txt文件中。你可以手动从下载 URL 或本地目录安装模型,或者通过pip安装。你可以在本地文件系统的任何位置放置模型数据。
您可以通过 spaCy 的 download 命令下载模型。download 会寻找与您的 spaCy 版本最兼容的模型,然后下载并安装它。这样您就不必担心模型与您的 spaCy 版本之间可能存在的任何不匹配。这是安装模型的最简单方法:
$ python -m spacy download en_core_web_md
上述命令选择并下载与您本地 spaCy 版本最兼容的特定模型版本。另一种选择是执行以下操作:
$ python -m spacy download en
$ python –m spacy download de
$ python –m spacy download fr
这些命令为每种语言安装最兼容的 默认 模型并创建快捷链接。要下载确切的模型版本,需要执行以下操作(尽管您通常不需要这样做):
$ python -m spacy download en_core_web_lg-2.0.0 --direct
download 命令在幕后部署 pip。当您进行下载时,pip 会安装该包并将其放置在您的 site-packages 目录中,就像其他任何已安装的 Python 包一样。
下载完成后,我们可以通过 spaCy 的 load() 方法加载这些包。
这就是我们迄今为止所做的一切:
$ pip install spacy
$ python –m spacy download en
import spacy
nlp = spacy.load('en_core_web_md')
doc = nlp('I have a ginger cat.')
或者,我们可以提供完整的模型名称:
$ pip install spacy
$ python -m spacy download en_core_web_md
import spacy
nlp = spacy.load('en_core_web_md')
doc = nlp('I have a ginger cat.')
我们也可以通过 pip 下载模型:
-
首先,我们需要下载我们想要下载的模型的链接。
-
我们导航到模型发布页面(https://github.com/explosion/spacy-models/releases),找到所需的模型,并复制存档文件的链接。
-
然后,我们使用模型链接进行
pip install。
以下是一个使用自定义 URL 下载的示例命令:
$ pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-2.0.0/en_core_web_lg-2.0.0.tar.gz
您可以按照以下方式安装本地文件:
$ pip install /Users/yourself/en_core_web_lg-2.0.0.tar.gz
这会将模型安装到您的 site-packages 目录中。然后我们运行 spacy.load() 通过其包名加载模型,创建一个快捷链接以给它一个自定义名称(通常是更短的名字),或者将其作为模块导入。
将语言模型作为模块导入也是可能的:
import en_core_web_md
nlp = en_core_web_md.load()
doc = nlp('I have a ginger cat.')
小贴士
在专业的软件开发中,我们通常将模型下载作为自动化流程的一部分。在这种情况下,使用 spaCy 的 download 命令是不可行的;相反,我们使用带有模型 URL 的 pip。您也可以将模型添加到 requirements.txt 文件中作为包。
您如何加载模型由您自己的喜好决定,也取决于您正在工作的项目需求。
到目前为止,我们已经准备好探索 spaCy 世界。现在,让我们了解 spaCy 强大的可视化工具 displaCy。
使用 displaCy 进行可视化
可视化 是每个数据科学家工具箱中的重要工具。可视化是向您的同事、老板以及任何技术或非技术受众解释某些概念的最简单方法。语言数据可视化特别有用,并允许您一眼就识别出数据中的模式。
有许多 Python 库和插件,例如 Matplotlib、seaborn、TensorBoard 等。作为一个工业级库,spaCy 自带其可视化工具 – Doc 对象。我们将从最简单的方法开始探索 – 使用 displaCy 的交互式演示。
开始使用 displaCy
前往explosion.ai/demos/displacy使用交互式演示。在要解析的文本框中输入你的文本,然后点击右侧的搜索图标以生成可视化。结果可能看起来如下:

图 1.11 – displaCy 的在线演示
可视化工具对提交的文本执行两个句法解析、词性标注和依存句法解析,以可视化句子的句法结构。不用担心词性标注和依存句法解析是如何工作的,因为我们在接下来的章节中会探讨它们。现在,只需将结果视为句子结构。
你会注意到两个复选框,“合并标点”和“合并短语”。合并标点将标点符号标记合并到前一个标记中,从而提供更紧凑的视觉呈现(在长文档上效果极佳)。
第二种选择,“他们都是美丽健康的孩子们,有着强烈的食欲。”它包含两个名词短语,“美丽健康的孩子们”和“强烈的食欲”。如果我们合并它们,结果如下:

图 1.12 – 合并名词短语后的一个示例解析
未合并时,每个形容词和名词都单独显示:

图 1.13 – 同句子的一个未合并的解析
第二次解析有点过于繁琐且难以阅读。如果你处理的是长句子的文本,如法律文章或维基百科条目,我们强烈建议合并。
你可以从右侧的模型框中选择一个统计模型用于当前支持的语言。此选项允许你在不下载和安装到本地机器的情况下尝试不同的语言模型。
实体可视化
displaCy 的实体可视化工具突出显示文本中的命名实体。在线演示可在explosion.ai/demos/displacy-ent/找到。我们尚未介绍命名实体,但你可以将它们视为重要实体的专有名词,例如人名、公司名、日期、城市和国家名称等。提取实体将在第三章“语言特征”和第四章“基于规则的匹配”中详细讲解。
在线演示与句法解析演示类似。将文本输入到文本框中,然后点击搜索按钮。以下是一个示例:

图 1.14 – 一个实体可视化的示例
右侧包含实体类型的复选框。您可以选择与您的文本类型匹配的复选框,例如,例如,对于金融文本,选择 MONEY 和 QUANTITY。同样,就像在句法解析演示中一样,您可以从可用的模型中选择。
在 Python 中可视化
随着 spaCy 最新版本的推出,displaCy 可视化器已集成到核心库中。这意味着您可以在机器上安装 spaCy 后立即开始使用 displaCy!让我们通过一些示例来了解。
以下代码段是在本地机器上启动 displaCy 的最简单方法:
import spacy
from spacy import displacy
nlp = spacy.load('en_core_web_md')
doc= nlp('I own a ginger cat.')
displacy.serve(doc, style='dep')
如前文片段所示,以下是我们所做的工作:
-
我们导入
spaCy。 -
随后,我们从核心库中导入
displaCy。 -
我们加载了在 安装 spaCy 的统计模型 部分下载的英语模型。
-
一旦加载完成,我们创建一个
Doc对象传递给displaCy。 -
然后,我们通过调用
serve()来启动displaCy网络服务器。 -
我们还将
dep传递给style参数,以查看依赖关系解析结果。
启动此代码后,您应该看到 displaCy 的以下响应:

图 1.15 – 在本地启动 displaCy
响应中添加了一个链接,http://0.0.0.0:5000,这是 displaCy 渲染您图形的本地地址。请点击链接并导航到网页。您应该看到以下内容:

图 1.16 – 在您的浏览器中查看结果可视化
这意味着 displaCy 生成了一个依赖关系解析结果的可视化,并在您的本地主机上渲染了它。在您完成显示可视化并想要关闭服务器后,您可以按 Ctrl +C 来关闭 displaCy 服务器并返回 Python 壳:

图 1.17 – 关闭 displaCy 服务器
关闭后,您将无法可视化更多示例,但您将继续看到您已经生成的结果。
如果您希望使用另一个端口或由于端口 5000 已被占用而出现错误,您可以使用 displaCy 的 port 参数使用另一个端口号。将前一个代码块的最后一条行替换为以下行就足够了:
displacy.serve(doc, style='dep', port= '5001')
在这里,我们明确提供了端口号 5001。在这种情况下,displaCy 将在 http://0.0.0.0:5001 上渲染图形。
生成实体识别器的方式类似。我们将 ent 传递给 style 参数而不是 dep:
import spacy
from spacy import displacy
nlp = spacy.load('en_core_web_md')
doc= nlp('Bill Gates is the CEO of Microsoft.')
displacy.serve(doc, style='ent')
结果应该看起来像以下这样:

图 1.18 – 实体可视化显示在您的浏览器上
让我们继续探讨其他我们可以用于显示结果的平台。
在 Jupyter 笔记本中使用 displaCy
Jupyter notebook 是日常数据科学工作的重要组成部分。幸运的是,displaCy 可以检测您是否正在 Jupyter notebook 环境中编码,并返回可以直接在单元格中显示的标记。
如果您系统上没有安装 Jupyter notebook 但希望使用它,可以按照test-jupyter.readthedocs.io/en/latest/install.html中的说明进行操作。
这次我们将调用 render() 而不是 serve()。其余的代码保持不变。您可以将以下代码输入或粘贴到您的 Jupyter 笔记本中:
import spacy
from spacy import displacy
nlp = spacy.load('en_core_web_md')
doc= nlp('Bill Gates is the CEO of Microsoft.')
displacy.render(doc, style='dep')
结果应该看起来像下面这样:


图 1.19 – displaCy 在 Jupyter 笔记本中的渲染结果
将 displaCy 图形导出为图像文件
通常,我们需要将使用 displaCy 生成的图形导出为图像文件,以便将其放入演示文稿、文章或论文中。在这种情况下,我们也可以调用 displaCy:
import spacy
from spacy import displacy
from pathlib import Path
nlp = spacy.load('en_core_web_md')
doc = nlp('I'm a butterfly.')
svg = displacy.render(doc, style='dep', jupyter=False)
filename = 'butterfly.svg'
output_path = Path ('/images/' + file_name)
output_path.open('w', encoding='utf-8').write(svg)
我们导入 spaCy 和 displaCy。然后加载英语语言模型,然后像往常一样创建一个 Doc 对象。然后我们调用 displacy.render() 并将输出捕获到 svg 变量中。其余的就是将 svg 变量写入名为 butterfly.svg 的文件中。
我们已经到达了可视化章节的结尾。我们创建了美观的视觉效果,并学习了使用 displaCy 创建视觉效果的所有细节。如果您想了解如何使用不同的背景图像、背景颜色和字体,您可以访问 displaCy 文档spacy.io/usage/visualizers。
通常,我们需要创建具有不同颜色和样式的视觉效果,而 displaCy 文档中包含了关于样式的详细信息。文档还包括如何将 displaCy 集成到您的 Web 应用程序中。spaCy 作为项目有很好的文档记录,文档中包含了我们所需的所有内容!
摘要
本章为您介绍了使用 Python 和 spaCy 的 NLP。您现在对为什么使用 Python 进行语言处理以及为什么选择 spaCy 创建您的 NLP 应用程序的原因有了简要的了解。我们还通过安装 spaCy 和下载语言模型开始了我们的 spaCy 之旅。本章还介绍了可视化工具 – displaCy。
在下一章中,我们将继续我们的激动人心的 spaCy 之旅,探讨 spaCy 核心操作,如分词和词形还原。这将是我们第一次详细接触 spaCy 的功能。让我们继续探索,一起了解更多吧!
第二章:使用 spaCy 的核心操作
在本章中,你将学习使用 spaCy 的核心操作,例如创建语言管道、分词文本以及将文本分解成句子。
首先,你将学习什么是语言处理管道以及管道组件。我们将继续介绍 spaCy 的通用约定——重要的类和类组织——以帮助你更好地理解 spaCy 库的组织结构,并对你对库本身有一个坚实的理解。
你将接着学习第一个管道组件——分词器。你还将了解一个重要的语言学概念——词形还原——以及它在自然语言理解(NLU)中的应用。随后,我们将详细介绍容器类和spaCy 数据结构。我们将以有用的 spaCy 特性结束本章,这些特性你将在日常 NLP 开发中使用。
在本章中,我们将涵盖以下主要主题:
-
spaCy 约定的概述
-
介绍分词
-
理解词形还原
-
spaCy 容器对象
-
更多 spaCy 特性
技术要求
本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter02
spaCy 约定的概述
每个 NLP 应用程序都包含对文本进行处理的几个步骤。正如你在第一章中看到的,我们总是创建了名为nlp和doc的实例。但我们到底做了什么呢?
当我们在文本上调用nlp时,spaCy 会应用一些处理步骤。第一步是分词,以生成一个Doc对象。然后,Doc对象会进一步通过Doc处理,然后传递给下一个组件:

图 2.1 – 处理管道的高级视图
当我们加载语言模型时,会创建一个 spaCy 管道对象。在以下代码段中,我们加载了一个英语模型并初始化了一个管道:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("I went there")
在前面的代码中实际上发生了以下情况:
-
我们首先导入了
spaCy。 -
在第二行,
spacy.load()返回了一个Language类实例,nlp。Language类是文本处理管道。 -
然后,我们将
nlp应用于示例句子我去那里,并得到了一个Doc类实例,doc。
Language类在幕后将所有先前的管道步骤应用于你的输入句子。在将nlp应用于句子后,Doc对象包含标记、词形还原,如果标记是实体,则标记为实体(我们将在稍后详细介绍这些是什么以及如何实现)。每个管道组件都有一个明确定义的任务:

图 2.2 – 管道组件和任务
spaCy 语言处理管道始终依赖于统计模型及其能力。这就是为什么我们总是将语言模型作为代码中的第一步通过spacy.load()加载。
每个组件都对应一个spaCy类。spaCy类有自解释的名称,如Language和Doc类 – 让我们看看所有处理管道类及其职责:

图 2.3 – spaCy 处理管道类
不要被类的数量吓倒;每个类都有独特的功能,可以帮助你更好地处理文本。
有更多数据结构可以表示文本数据和语言数据。例如,Doc 容器类包含关于句子、单词和文本的信息。除了 Doc 之外,还有其他容器类:

图 2.4 – spaCy 容器类
最后,spaCy 为向量、语言词汇和注释提供了辅助类。在这本书中,我们将经常看到Vocab类。Vocab代表一种语言的词汇。Vocab 包含我们加载的语言模型中的所有单词:

图 2.5 – spaCy 辅助类
spaCy 库的核心数据结构是Doc和Vocab。Doc对象通过拥有标记序列及其所有属性来抽象文本。Vocab对象为所有其他类提供集中式的字符串和词汇属性。这样 spaCy 就避免了存储多个语言数据的副本:

图 2.6 – spaCy 架构
你可以将组成前面 spaCy 架构的对象分为两类:容器和处理管道组件。在本章中,我们将首先了解两个基本组件,分词器和词形还原器,然后我们将进一步探索容器对象。
spaCy 在幕后为我们执行所有这些操作,使我们能够专注于我们自己的应用程序开发。在这个抽象级别上,使用 spaCy 进行 NLP 应用程序开发并非巧合。让我们从Tokenizer类开始,看看它为我们提供了什么;然后我们将在本章中逐个探索所有容器类。
介绍分词
我们在图 2.1中看到,文本处理管道的第一步是分词。分词总是第一个操作,因为所有其他操作都需要标记单元。
分词简单来说就是将句子拆分成其标记单元。标记单元是语义的一个单位。你可以将标记单元想象为文本中最小的有意义的部分。标记单元可以是单词、数字、标点符号、货币符号以及任何其他构成句子的有意义的符号。以下是一些标记单元的例子:
-
USA -
N.Y. -
city -
33 -
3rd -
! -
… -
? -
`'s'
输入到 spaCy 标记化器的是 Unicode 文本,结果是 Doc 对象。以下代码显示了标记化过程:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("I own a ginger cat.")
print ([token.text for token in doc])
['I', 'own', 'a', 'ginger', 'cat', '.']
以下是我们刚刚所做的事情:
-
我们首先导入
spaCy。 -
然后,我们通过
en快捷方式加载了英语语言模型以创建nlpLanguage类的实例。 -
然后,我们将
nlp对象应用于输入句子以创建Doc对象,doc。Doc对象是一个Token对象序列的容器。当我们创建Doc对象时,spaCy 隐式地生成Token对象。 -
最后,我们打印出前一句子的标记列表。
就这样,我们只用了三行代码就完成了标记化。你可以通过以下方式可视化标记化:
![图 2.7 – “我有一只姜黄色猫。”的标记化]

图 2.7 – “我有一只姜黄色猫。”的标记化
如示例所示,标记化确实可能很棘手。有许多方面我们应该注意:标点符号、空白、数字等等。使用 text.split(" ") 从空白处分割可能很有吸引力,看起来它对于示例句子 我有一只姜黄色猫 是有效的。
那么,"It's been a crazy week!!!" 这个句子呢?如果我们使用 split(" "),得到的标记将是 It's、been、a、crazy、week!!!,这并不是你想要的。首先,It's 不是一个标记,它是两个标记:it 和 's'。week!!! 不是一个有效的标记,因为标点符号没有被正确分割。此外,!!! 应该按符号标记化,并生成三个 !。 (这可能看起来不是一个重要的细节,但请相信我,这对 情感分析 非常重要。我们将在 第八章**,使用 spaCy 进行文本分类 中介绍情感分析。) 让我们看看 spaCy 标记化器生成了什么:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("It's been a crazy week!!!")
print ([token.text for token in doc])
['It', "'s", 'been', 'a', 'crazy', 'week', '!', '!', '!']
这次句子是这样分割的:
![图 2.8 – 引号和标点符号的标记化]

图 2.8 – 引号和标点符号的标记化
spaCy 是如何知道在哪里分割句子的?与其他管道部分不同,标记化器不需要统计模型。标记化基于语言特定的规则。你可以在这里看到指定语言的数据示例:github.com/explosion/spaCy/tree/master/spacy/lang。
标记化器异常定义了异常的规则,例如 it's、don't、won't、缩写等等。如果你查看英语的规则:github.com/explosion/spaCy/blob/master/spacy/lang/en/tokenizer_exceptions.py,你会看到规则看起来像 {ORTH: "n't", LEMMA: "not"},这描述了 n't 对标记化器的分割规则。
前缀、后缀和内嵌词主要描述了如何处理标点符号 – 例如,如果句尾有一个句号,我们就将其分割,否则,它很可能是缩写的一部分,如 N.Y.,我们不应该对其进行操作。在这里,ORTH 表示文本,而 LEMMA 表示不带任何屈折变化的词的基本形式。以下示例展示了 spaCy 分词算法的执行过程:

图 2.9 – spaCy 使用异常规则进行分词(图片来自 spaCy 分词指南(https://spacy.io/usage/linguistic-features#tokenization))
分词规则取决于个别语言的语法规则。像分割句号、逗号或感叹号这样的标点符号规则在许多语言中或多或少是相似的;然而,一些规则是特定于个别语言的,例如缩写词和撇号的使用。spaCy 通过允许手动编码的数据和规则来支持每个语言都有其特定的规则,因为每个语言都有自己的子类。
小贴士
spaCy 提供了非破坏性分词,这意味着我们总是可以从标记中恢复原始文本。在分词过程中,空白和标点信息被保留,因此输入文本保持原样。
每个 Language 对象都包含一个 Tokenizer 对象。Tokenizer 类是执行分词的类。当你创建 Doc 类实例时,通常不直接调用这个类,而 Tokenizer 类在幕后工作。当我们想要自定义分词时,我们需要与这个类进行交互。让我们看看它是如何完成的。
自定义分词器
当我们处理特定领域,如医学、保险或金融时,我们经常会遇到需要特别注意的单词、缩写和实体。你将处理的多数领域都有其特有的单词和短语,需要自定义分词规则。以下是如何向现有的 Tokenizer 类实例添加特殊案例规则的方法:
import spacy
from spacy.symbols import ORTH
nlp = spacy.load("en_core_web_md")
doc = nlp("lemme that")
print([w.text for w in doc])
['lemme', 'that']
special_case = [{ORTH: "lem"}, {ORTH: "me"}]
nlp.tokenizer.add_special_case("lemme", special_case)
print([w.text for w in nlp("lemme that")])
['lem', 'me', 'that']
这里是我们所做的工作:
-
我们再次从导入
spacy开始。 -
然后,我们导入了
ORTH符号,这意味着正字法;即文本。 -
我们继续创建一个
Language类对象,nlp,并创建了一个Doc对象,doc。 -
我们定义了一个特殊案例,其中单词
lemme应该分词为两个标记,lem和me。 -
我们将规则添加到了
nlp对象的分词器中。 -
最后一行展示了新规则是如何工作的。
当我们定义自定义规则时,标点分割规则仍然适用。我们的特殊案例将被识别为结果,即使它被标点符号包围。分词器将逐步分割标点符号,并将相同的处理过程应用于剩余的子串:
print([w.text for w in nlp("lemme!")])
['lem', 'me', '!']
如果你定义了一个带有标点的特殊规则,则特殊规则将优先于标点分割:
nlp.tokenizer.add_special_case("...lemme...?", [{"ORTH": "...lemme...?"}])
print([w.text for w in nlp("...lemme...?")])
'...lemme...?'
小贴士
只有在真正需要的时候才通过添加新规则来修改分词器。相信我,使用自定义规则可能会得到相当意外的结果。真正需要这种情况之一是处理 Twitter 文本,它通常充满了标签和特殊符号。如果你有社交媒体文本,首先将一些句子输入到 spaCy NLP 管道中,看看分词是如何进行的。
调试分词器
spaCy 库有一个用于调试的工具:nlp.tokenizer.explain(sentence)。它返回(tokenizer rule/pattern, token)元组,帮助我们了解分词过程中确切发生了什么。让我们看一个例子:
import spacy
nlp = spacy.load("en_core_web_md")
text = "Let's go!"
doc = nlp(text)
tok_exp = nlp.tokenizer.explain(text)
for t in tok_exp:
print(t[1], "\t", t[0])
Let SPECIAL-1
's SPECIAL-2
go TOKEN
! SUFFIX
在前面的代码中,我们导入了spacy,并像往常一样创建了一个Language类实例,nlp。然后我们使用句子Let's go!创建了一个Doc类实例。之后,我们向nlp的Tokenizer类实例tokenizer请求对这句话分词的解释。nlp.tokenizer.explain()逐个解释了分词器使用的规则。
在将句子分割成词素之后,现在是时候将文本分割成句子了。
句子分割
我们看到将句子分割成词素并不是一个简单直接的任务。那么将文本分割成句子呢?由于标点、缩写等原因,标记句子开始和结束的位置确实要复杂一些。
一个 Doc 对象的句子可以通过doc.sents属性访问:
import spacy
nlp = spacy.load("en_core_web_md")
text = "I flied to N.Y yesterday. It was around 5 pm."
doc = nlp(text)
for sent in doc.sents:
print(sent.text)
I flied to N.Y yesterday.
It was around 5 pm.
确定句子边界比分词更复杂。因此,spaCy 使用依存句法分析器来执行句子分割。这是 spaCy 的独特功能——没有其他库将如此复杂的思想付诸实践。一般来说,结果非常准确,除非你处理的是非常特定类型的文本,例如来自对话领域或社交媒体文本。
现在我们知道了如何将文本分割成句子并将句子分词。我们准备好逐个处理词素了。让我们从词元化开始,这是语义分析中常用的一种操作。
理解词元化
词元是词素的基本形式。你可以将词元想象成词典中词素出现的形态。例如,eating的词元是eat;eats的词元也是eat;ate同样映射到eat。词元化是将词形还原到其词元的过程。以下是一个使用 spaCy 进行词元化的快速示例:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("I went there for working and worked for 3 years.")
for token in doc:
print(token.text, token.lemma_)
I -PRON-
went go
there
for for
working work
and and
worked work
for for
3 3
years year
. .
到现在为止,你应该熟悉代码的前三行做了什么。回想一下,我们导入了spacy库,使用spacy.load加载了一个英语模型,创建了一个管道,并将管道应用于前面的句子以获取一个 Doc 对象。在这里,我们遍历了词素以获取它们的文本和词元。
在第一行中,你看到 –PRON-,这看起来不像一个真正的标记。这是一个 代词词元,一个用于个人代词词元的特殊标记。这是出于语义目的的一个例外:个人代词 you、I、me、him、his 等看起来不同,但在意义上,它们属于同一组。spaCy 为代词词元提供了这个技巧。
如果这一切听起来过于抽象,请不要担心——让我们通过一个现实世界的例子来看看词元化的实际应用。
NLU 中的词元化
词元化是 NLU 的重要步骤。我们将在本小节中通过一个示例来讲解。假设你为票务预订系统设计了一个 NLP 管道。你的应用程序处理客户的句子,从中提取必要的信息,然后将它传递给预订 API。
NLP 管道旨在提取旅行的形式(航班、巴士或火车)、目的地城市和日期。应用程序需要验证的第一件事是旅行方式:
fly – flight – airway – airplane - plane
bus
railway – train
我们有这个关键词列表,并希望通过在关键词列表中搜索标记来识别旅行方式。进行此搜索的最紧凑方式是查找标记的词元。考虑以下客户句子:
List me all flights to Atlanta.
I need a flight to NY.
I flew to Atlanta yesterday evening and forgot my baggage.
在这里,我们不需要在关键词列表中包含动词 fly 的所有词形(fly、flying、flies、flew 和 flown),对于单词 flight 也是如此;我们将所有可能的变体都缩减到了基本形式 – fly 和 flight。不要只考虑英语;像西班牙语、德语和芬兰语这样的语言也有许多来自单个词元的词形。
当我们想要识别目的地城市时,词元化也很有用。全球城市有许多昵称,而预订 API 只能处理官方名称。默认的分词器和词元化器不会区分官方名称和昵称。在这种情况下,你可以添加特殊规则,就像我们在 介绍分词 部分中看到的那样。以下代码玩了一个小把戏:
import spacy
from spacy.symbols import ORTH, LEMMA
nlp = spacy.load('en')
special_case = [{ORTH: 'Angeltown', LEMMA: 'Los Angeles'}]
nlp.tokenizer.add_special_case(u'Angeltown', special_case)
doc = nlp(u'I am flying to Angeltown')
for token in doc:
print(token.text, token.lemma_)
I -PRON-
am be
flying fly
to to
Angeltown Los Angeles
我们通过将 Angeltown 的词元替换为官方名称 Los Angeles 来为该词定义了一个特殊情况。然后我们将这个特殊情况添加到 Tokenizer 实例中。当我们打印标记词元时,我们看到 Angeltown 正如我们希望的那样映射到 Los Angeles。
理解词元化与词干提取的区别
词元是单词的基本形式,并且总是语言词汇的一部分。词干不一定必须是有效的单词。例如,improvement 的词元是 improvement,但词干是 improv。你可以把词干看作是承载意义的单词的最小部分。比较以下示例:
Word Lemma
university university
universe universe
universal universal
universities university
universes universe
improvement improvement
improvements improvements
improves improve
上述词元-词元示例展示了如何通过遵循语言的语法规则来计算词元。在这里,复数形式的词元是其单数形式,第三人称动词的词元是动词的基本形式。让我们将它们与以下词根-词对示例进行比较:
Word Stem
university univers
universe univer
universal univers
universities universi
universes univers
improvement improv
improvements improv
improves improv
在前面的例子中,需要注意的第一个和最重要的点是词元不必是语言中的有效单词。第二个点是许多单词可以映射到同一个词干。此外,来自不同语法类别的单词也可以映射到同一个词干;例如,名词 improvement 和动词 improves 都映射到 improv。
尽管词干不是有效单词,但它们仍然承载着意义。这就是为什么词干提取在 NLU 应用中通常被使用的原因。
词干提取算法对语言的语法一无所知。这类算法主要通过从词的起始或结束部分修剪一些常见的后缀和前缀来工作。
词干提取算法比较粗糙,它们从词的首部和尾部切割单词。对于英语,有几种可用的词干提取算法,包括 Porter 和 Lancaster。你可以在 NLTK 的演示页面上尝试不同的词干提取算法:text-processing.com/demo/stem/。
相反,词形还原考虑了单词的形态分析。为此,重要的是要获得算法查找的词典,以便将形式与其词元联系起来。
spaCy 通过字典查找提供词形还原,每种语言都有自己的字典。
小贴士
词干提取和词形还原都有它们自己的优势。如果你只对文本应用统计算法,而不进行进一步的语义处理(如模式查找、实体提取、指代消解等),词干提取会给出非常好的结果。此外,词干提取可以将大型语料库缩减到更适中的大小,并提供紧凑的表示。如果你在管道中使用语言特征或进行关键词搜索,请包括词形还原。词形还原算法准确,但计算成本较高。
spaCy 容器对象
在本章的开头,我们看到了一个包含 Doc、Token、Span 和 Lexeme 的容器对象列表。我们已经在代码中使用了 Token 和 Doc。在本小节中,我们将详细查看容器对象的属性。
使用容器对象,我们可以访问 spaCy 分配给文本的语言属性。容器对象是文本单元(如文档、标记或文档的片段)的逻辑表示。
spaCy 中的容器对象遵循文本的自然结构:文档由句子组成,句子由标记组成。
我们在开发中最广泛地使用 Doc、Token 和 Span 对象,分别代表文档、单个标记和短语。容器可以包含其他容器,例如文档包含标记和片段。
让我们逐一探索每个类及其有用的属性。
Doc
我们在代码中创建了 Doc 对象来表示文本,所以你可能已经意识到 Doc 代表文本。
我们已经知道如何创建 Doc 对象:
doc = nlp("I like cats.")
doc.text 返回文档文本的 Unicode 表示:
doc.text
I like cats.
Doc 对象的构建块是 Token,因此当您迭代 Doc 时,您会得到 Token 对象作为项目:
for token in doc:
print(token.text)
I
like
cats
.
同样的逻辑也适用于索引:
doc[1]
like
Doc 的长度是它包含的标记数量:
len(doc)
4
我们已经看到了如何获取文本的句子。doc.sents 返回句子列表的迭代器。每个句子都是一个 Span 对象:
doc = nlp("This is a sentence. This is the second sentence")
doc.sents
<generator object at 0x7f21dc565948>
sentences = list(doc.sents)
sentences
["This is a sentence.", "This is the second sentence."]
doc.ents 提供文本中的命名实体。结果是 Span 对象的列表。我们将在稍后详细讨论命名实体——现在,请将它们视为专有名词:
doc = nlp("I flied to New York with Ashley.")
doc.ents
(New York, Ashley)
另一个句法属性是 doc.noun_chunks。它产生文本中找到的名词短语:
doc = nlp("Sweet brown fox jumped over the fence.")
list(doc.noun_chunks)
[Sweet brown fox, the fence]
doc.lang_ 返回 doc 创建的语言:
doc.lang_
'en'
一个有用的序列化方法是 doc.to_json。这是将 Doc 对象转换为 JSON 的方法:
doc = nlp("Hi")
json_doc = doc.to_json()
{
"text": "Hi",
"ents": [],
"sents": [{"start": 0, "end": 3}],
"tokens": [{"id": 0, "start": 0, "end": 3, "pos": "INTJ", "tag": "UH", "dep": "ROOT", "head": 0}]
}
小贴士
您可能已经注意到我们调用 doc.lang_,而不是 doc.lang。doc.lang 返回语言 ID,而 doc.lang_ 返回语言的 Unicode 字符串,即语言名称。您可以在以下 Token 特性中看到相同的约定,例如,token.lemma_、token.tag_ 和 token.pos_。
Doc 对象具有非常实用的属性,您可以使用这些属性来理解句子的句法属性,并在您自己的应用程序中使用它们。让我们继续了解 Token 对象及其提供的功能。
Token
Token 对象代表一个单词。Token 对象是 Doc 和 Span 对象的构建块。在本节中,我们将介绍 Token 类的以下属性:
-
token.text -
token.text_with_ws -
token.i -
token.idx -
token.doc -
token.sent -
token.is_sent_start -
token.ent_type
我们通常不会直接构建 Token 对象,而是先构建 Doc 对象,然后访问其标记:
doc = nlp("Hello Madam!")
doc[0]
Hello
token.text 与 doc.text 类似,并提供底层 Unicode 字符串:
doc[0].text
Hello
token.text_with_ws 是一个类似的属性。如果 doc 中存在,它将提供带有尾随空白的文本:
doc[0].text_with_ws
'Hello '
doc[2].text_with_ws
'!"
找到标记的长度与找到 Python 字符串的长度类似:
len(doc[0])
5
token.i 给出标记在 doc 中的索引:
token = doc[2]
token.i
2
token.idx 提供了标记在 doc 中的字符偏移量(字符位置):
doc[0].idx
0
doc[1].idx
6
我们还可以按照以下方式访问创建 token 的 doc:
token = doc[0]
token.doc
Hello Madam!
获取 token 所属的句子与访问创建 token 的 doc 的方式类似:
token = doc[1]
token.sent
Hello Madam!
token.is_sent_start 是另一个有用的属性;它返回一个布尔值,指示标记是否开始句子:
doc = nlp("He entered the room. Then he nodded.")
doc[0].is_sent_start
True
doc[5].is_sent_start
True
doc[6].is_sent_start
False
这些是您每天都会使用的 Token 对象的基本属性。还有另一组属性,它们与句法和语义更相关。我们已经在上一节中看到了如何计算标记的词元:
doc = nlp("I went there.")
doc[1].lemma_
'go'
您已经了解到 doc.ents 提供了文档中的命名实体。如果您想了解标记是哪种类型的实体,请使用 token.ent_type_:
doc = nlp("President Trump visited Mexico City.")
doc.ents
(Trump, Mexico City)
doc[1].ent_type_
'PERSON'
doc[3].ent_type_
'GPE' # country, city, state
doc[4].ent_type_
'GPE' # country, city, state
doc[0].ent_type_
'' # not an entity
与词性标注相关的两个语法功能是token.pos_和token.tag。我们将在下一章学习它们是什么以及如何使用它们。
另一套语法功能来自依存句法分析器。这些功能包括dep_、head_、conj_、lefts_、rights_、left_edge_和right_edge_。我们将在下一章中也介绍它们。
小贴士
如果你之后不记得所有功能,这是完全正常的。如果你不记得一个功能的名称,你总是可以执行dir(token)或dir(doc)。调用dir()将打印出对象上所有可用的功能和方法的名称。
Token 对象具有丰富的功能集,使我们能够从头到尾处理文本。让我们继续到 Span 对象,看看它为我们提供了什么。
Span
Span 对象代表文本的短语或片段。技术上,Span 必须是一系列连续的标记。我们通常不会初始化 Span 对象,而是通过切片 Doc 对象来创建:
doc = nlp("I know that you have been to USA.")
doc[2:4]
"that you"
尝试切片一个无效的索引将引发一个IndexError。Python 字符串的大多数索引和切片规则也适用于文档切片:
doc = nlp("President Trump visited Mexico City.")
doc[4:] # end index empty means rest of the string
City.
doc[3:-1] # minus indexes are supported
doc[6:]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "span.pyx", line 166, in spacy.tokens.span.Span.__repr__
File "span.pyx", line 503, in spacy.tokens.span.Span.text.__get__
File "span.pyx", line 190, in spacy.tokens.span.Span.__getitem__
IndexError: [E201] Span index out of range.
doc[1:1] # empty spans are not allowed
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "span.pyx", line 166, in spacy.tokens.span.Span.__repr__
File "span.pyx", line 503, in spacy.tokens.span.Span.text.__get__
File "span.pyx", line 190, in spacy.tokens.span.Span.__getitem__
IndexError: [E201] Span index out of range.
创建 Span 还有另一种方法——我们可以使用char_span对 Doc 对象进行字符级别的切片:
doc = nlp("You love Atlanta since you're 20.")
doc.char_span(4, 16)
love Atlanta
Span 对象的基本构建块是 Token 对象。如果你迭代一个 Span 对象,你会得到 Token 对象:
doc = nlp("You went there after you saw me")
span = doc[2:4]
for token in span:
print(token)
there
after
你可以将 Span 对象视为一个初级文档对象,实际上它是由它创建的文档对象的一个视图。因此,文档的大多数功能也适用于 Span。例如,len是相同的:
doc = nlp("Hello Madam!")
span = doc[1:2]
len(span)
1
Span 对象也支持索引。切片 Span 对象的结果是另一个 Span 对象:
doc = nlp("You went there after you saw me")
span = doc[2:6]
span
there after you saw
subspan = span[1:3]
after you
char_spans也适用于 Span 对象。记住,Span 类是一个初级文档类,因此我们也可以在 Span 对象上创建字符索引的跨度:
doc = nlp("You went there after you saw me")
span = doc[2:6]
span.char_span(15,24)
after you
就像 Token 知道它创建的文档对象一样;Span 也知道它创建的文档对象:
doc = nlp("You went there after you saw me")
span = doc[2:6]
span.doc
You went there after you saw me
span.sent
You went there after you saw me
我们也可以在原始的Doc中定位Span:
doc = nlp("You went there after you saw me")
span = doc[2:6]
span.start
2
span.end
6
span.start_char
9
span.end_char
28
span.start是 Span 的第一个标记的索引,而span.start_char是 Span 在字符级别上的起始偏移量。
如果你想要一个新的 Doc 对象,你可以调用span.as_doc()。它将数据复制到一个新的 Doc 对象中:
doc = nlp("You went there after you saw me")
span = doc[2:6]
type(span)
<class 'spacy.tokens.span.Span'>
small_doc = span.as_doc()
type(small_doc)
<class 'spacy.tokens.doc.Doc'>
span.ents、span.sent、span.text和span.text_wth_ws与它们对应的文档和标记方法类似。
亲爱的读者,我们已经到达了详尽章节的结尾。接下来,我们将在本节中探讨更多关于详细文本分析的功能和方法。
更多 spaCy 功能
大多数 NLP 开发都是基于标记和跨度进行的;也就是说,它处理标签、依存关系、标记本身和短语。大多数时候我们会消除没有太多意义的短词;我们以不同的方式处理 URL,等等。我们有时做的事情取决于 标记形状(标记是一个短词或标记看起来像 URL 字符串)或更语义的特征(例如,标记是一个冠词,或标记是一个连词)。在本节中,我们将通过示例查看这些标记特征。我们将从与标记形状相关的特征开始:
doc = nlp("Hello, hi!")
doc[0].lower_
'hello'
token.lower_ 返回标记的小写形式。返回值是一个 Unicode 字符串,这个特征等同于 token.text.lower()。
is_lower 和 is_upper 与它们的 Python 字符串方法 islower() 和 isupper() 类似。is_lower 返回 True 如果所有字符都是小写,而 is_upper 则对大写字母做同样处理:
doc = nlp("HELLO, Hello, hello, hEllO")
doc[0].is_upper
True
doc[0].is_lower
False
doc[1].is_upper
False
doc[1].is_lower
False
is_alpha 返回 True 如果标记的所有字符都是字母。非字母字符的例子包括数字、标点和空白字符:
doc = nlp("Cat and Cat123")
doc[0].is_alpha
True
doc[2].is_alpha
False
is_ascii 返回 True 如果标记的所有字符都是 ASCII 字符。
doc = nlp("Hamburg and Göttingen")
doc[0].is_ascii
True
doc[2].is_ascii
False
is_digit 返回 True 如果标记的所有字符都是数字:
doc = nlp("Cat Cat123 123")
doc[0].is_digit
False
doc[1].is_digit
False
doc[2].is_digit
True
is_punct 返回 True 如果标记是标点符号:
doc = nlp("You, him and Sally")
doc[1]
,
doc[1].is_punct
True
is_left_punct 和 is_right_punct 分别返回 True 如果标记是左标点符号或右标点符号。右标点符号可以是任何关闭左标点符号的标记,例如右括号、> 或 ». 左标点符号类似,左括号 < 和 « 是一些例子:
doc = nlp("( [ He said yes. ] )")
doc[0]
(
doc[0].is_left_punct
True
doc[1]
[
doc[1].is_left_punct
True
doc[-1]
)
doc[-1].is_right_punct
True
doc[-2]
]
doc[-2].is_right_punct
True
is_space 返回 True 如果标记仅包含空白字符:
doc = nlp(" ")
doc[0]
len(doc[0])
1
doc[0].is_space
True
doc = nlp(" ")
doc[0]
len(doc[0])
2
doc[0].is_space
True
is_bracket 返回 True 对于括号字符:
doc = nlp("( You said [1] and {2} is not applicable.)")
doc[0].is_bracket, doc[-1].is_bracket
(True, True)
doc[3].is_bracket, doc[5].is_bracket
(True, True)
doc[7].is_bracket, doc[9].is_bracket
(True, True)
is_quote 返回 True 对于引号:
doc = nlp("( You said '1\" is not applicable.)")
doc[3]
'
doc[3].is_quote
True
doc[5]
"
doc[5].is_quote
True
is_currency 返回 True 对于货币符号,如 $ 和 €(此方法由我实现):
doc = nlp("I paid 12$ for the tshirt.")
doc[3]
$
doc[3].is_currency
True
like_url、like_num 和 like_email 是关于标记形状的方法,分别返回 True 如果标记看起来像 URL、数字或电子邮件。当我们要处理社交媒体文本和抓取的网页时,这些方法非常方便:
doc = nlp("I emailed you at least 100 times")
doc[-2]
100
doc[-2].like_num
True
doc = nlp("I emailed you at least hundred times")
doc[-2]
hundred
doc[-2].like_num
True doc = nlp("My email is duygu@packt.com and you can visit me under https://duygua.github.io any time you want.")
doc[3]
duygu@packt.com
doc[3].like_email
True
doc[10]
https://duygua.github.io/
doc[10].like_url
True
token.shape_ 是一个不寻常的特征——在其他 NLP 库中没有类似的东西。它返回一个字符串,显示标记的 orthographic 特征。数字被替换为 d,大写字母被替换为 X,小写字母被替换为 x。您可以将结果字符串用作机器学习算法中的特征,并且标记形状可以与文本情感相关联:
doc = nlp("Girl called Kathy has a nickname Cat123.")
for token in doc:
print(token.text, token.shape_)
Girl Xxxx
called xxxx
Kathy Xxxxx
has xxx
a x
nickname xxxx
Cat123 Xxxddd
. .
is_oov 和 is_stop 是语义特征,与前面的形状特征相对。is_oov 返回 True 如果标记是 Out Of Vocabulary (OOV),即不在 Doc 对象的词汇表中。OOV 单词对于语言模型以及处理管道组件都是未知的:
doc = nlp("I visited Jenny at Mynks Resort")
for token in doc:
print(token, token.is_oov)
I False
visited False
Jenny False
at False
Mynks True
Resort False
is_stop 是机器学习算法中经常使用的一个特征。通常,我们会过滤掉那些意义不大的词,例如 the,a,an,and,just,with 等等。这样的词被称为停用词。每种语言都有自己的停用词列表,你可以在这里访问英语停用词:github.com/explosion/spaCy/blob/master/spacy/lang/en/stop_words.py
doc = nlpI just want to inform you that I was with the principle.")
for token in doc:
print(token, token.is_stop)
I True
just True
want False
to True
inform False
you True
that True
I True
was True
with True
the True
principle False
. False
我们已经详尽地列出了 spaCy 的句法、语义和正字法特性。不出所料,许多方法都集中在 Token 对象上,因为分词是文本的句法单位。
摘要
我们现在已经到达了 spaCy 核心操作和基本特性的详尽章节的结尾。这一章节为你提供了 spaCy 库类和方法的全面概述。我们深入探讨了语言处理管道,并了解了管道组件。我们还覆盖了一个基本但重要的句法任务:分词。我们继续探讨了词形还原这一语言概念,并学习了 spaCy 功能的一个实际应用。我们详细探讨了 spaCy 容器类,并以精确且实用的 spaCy 特性结束了这一章节。到目前为止,你对 spaCy 语言管道有了很好的掌握,并且对完成更复杂的任务充满信心。
在下一章中,我们将深入挖掘 spaCy 的全部语言能力。你将发现包括 spaCy 最常用的功能:词性标注器、依存句法分析器、命名实体和实体链接在内的语言特性。
第三章:第二节:spaCy 功能
在本节中,我们将通过查看最强大和最常用的功能来深入揭示并检查 spaCy。我们将从句法到语义揭示语言特征,提供带有模式匹配的实用配方,并利用词向量进入语义世界。我们还将详细讨论统计信息提取方法。我们将涵盖一个详细的案例研究,展示如何结合所有 spaCy 功能来创建一个真实的 NLP 管道。
本节包含以下章节:
-
第三章, 语言特征
-
第四章, 基于规则的匹配
-
第五章, 与词向量及语义相似性协同工作
-
第六章, 整合一切 – 使用 spaCy 进行语义解析
第三章:语言特征
本章深入探讨了 spaCy 的全部功能。你将发现语言特征,包括 spaCy 最常用的功能,如词性(POS)标签器、依存句法分析器、命名实体识别器以及合并/拆分功能。
首先,你将学习 POS 标签的概念,了解 spaCy POS 标签器的功能,以及如何将 POS 标签放入你的自然语言理解(NLU)应用中。接下来,你将学习通过依存句法分析器以结构化的方式表示句子句法。你将了解 spaCy 的依存标签以及如何通过揭示性示例来解释 spaCy 依存标签器的结果。然后,你将学习一个非常重要的 NLU 概念,它是许多自然语言处理(NLP)应用的核心——命名实体识别(NER)。我们将通过 NER 从文本中提取信息的示例。最后,你将学习如何合并/拆分你提取的实体。
在本章中,我们将涵盖以下主要主题:
-
什么是 POS 标签?
-
依存句法分析简介
-
介绍 NER
-
合并和拆分标记
技术要求
本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter03
什么是 POS 标签?
在上一章讨论 spaCy Token类功能时,我们简要地看到了术语POS 标签和POS 标签化。从名称上看,它们指的是将 POS 标签标记到标记上的过程。这里仍有一个问题:什么是 POS 标签?在本节中,我们将详细探讨 POS 的概念以及如何在我们的 NLP 应用中充分利用它。
POS 标签的缩写是part-of-speech tagging。词性是一个句法类别,每个词都根据其在句子中的功能落入一个类别。例如,英语有九个主要类别:动词、名词、代词、限定词、形容词、副词、介词、连词和感叹词。我们可以描述每个类别的功能如下:
-
动词:表示动作或存在状态
-
名词:识别人、地点或事物,或命名这些中的一个(专有名词)
-
代词:可以替换名词或名词短语
-
限定词:放在名词前面以表达数量或阐明名词所指的内容——简而言之,名词引入者
-
形容词:修饰名词或代词
-
副词:修饰动词、形容词或另一个副词
-
介词:将名词/代词与其他句子部分连接起来
-
连词:将单词、从句和句子粘合在一起
-
感叹词:以突然和感叹的方式表达情感
这个核心类别集,没有任何语言特定的形态学或句法特征,被称为pos_特征,并以下列示例描述它们:
![图 3.1 – 使用示例解释 spaCy 通用标签]
![图 B16570_03_01.jpg]
![图 3.1 – 使用示例解释 spaCy 通用标签]
在整本书中,我们提供了使用英语的示例,因此在本节中,我们将专注于英语。不同的语言提供不同的标签集,spaCy 通过每个语言子模块下的tag_map.py支持不同的标签集。例如,当前的英语标签集位于lang/en/tag_map.py下,德语标签集位于lang/de/tag_map.py下。此外,同一种语言可以支持不同的标签集;因此,spaCy 和其他 NLP 库总是指定它们使用的标签集。spaCy 的英语 POS 标签器使用Ontonotes 5标签集,德语的 POS 标签器使用TIGER Treebank标签集。
spaCy 支持的每种语言都有自己的精细粒度标签集和标记方案,这是一种特定的标记方案,通常涵盖形态学特征、动词的时态和语态、名词的数量(单数/复数)、代词的人称和数量信息(第一、第二、第三人称单数/复数)、代词类型(人称、指示、疑问)、形容词类型(比较级或最高级)等等。
spaCy 支持精细粒度的 POS 标签以满足语言特定的需求,tag_特征对应于精细粒度标签。以下截图展示了这些精细粒度 POS 标签及其对英语更通用 POS 标签的映射:
![图 3.2 – 精细粒度的英语标签和通用标签映射]
![图 B16570_03_02.jpg]
图 3.2 – 精细粒度的英语标签和通用标签映射
如果您之前没有使用过 POS 标签,不要担心,因为通过我们的示例练习,您将变得熟悉。我们总是会包括对我们使用的标签的解释。您也可以在标签上调用spacy.explain()。我们通常以两种方式调用spacy.explain(),要么直接在标签名称字符串上,要么使用token.tag_,如下面的代码片段所示:
spacy.explain("NNS)
'noun, plural'
doc = nlp("I saw flowers.")
token = doc[2]
token.text, token.tag_, spacy.explain(token.tag_)
('flowers', 'NNS', 'noun, plural')
如果你想了解更多关于 POS 的信息,你可以在两个优秀的资源中了解更多:http://partofspeech.org/上的词性,以及www.butte.edu/departments/cas/tipsheets/grammar/parts_of_speech.html上的八种词性。
如您所见,POS 标签提供了对句子非常基本的句法理解。POS 标签在 NLU 中被广泛使用;我们经常想要找到句子中的动词和名词,并更好地区分一些词的意义(关于这个话题很快会有更多讨论)。
每个单词都根据其 上下文(其他周围的单词及其 POS 标签)被标记为一个 POS 标签。POS 标签器是顺序统计模型,这意味着 一个单词的标签取决于其词邻标记、它们的标签以及单词本身。POS 标记一直以不同的形式进行。序列到序列学习(Seq2seq)始于早期的隐马尔可夫模型(HMMs),并演变为神经网络模型——通常是长短期记忆(LSTM)变体(spaCy 也使用 LSTM 变体)。您可以在 ACL 网站上见证最先进 POS 标记的发展(aclweb.org/aclwiki/POS_Tagging_(State_of_the_art)。
现在是时候看看一些代码了。同样,spaCy 通过 token.pos (int) 和 token.pos_ (unicode) 特性提供通用 POS 标签。细粒度 POS 标签可通过 token.tag (int) 和 token.tag_ (unicode) 特性获得。让我们通过一些例子来了解更多您将遇到最多的标签。以下例子包括名词、专有名词、代词和动词标签的例子:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("Alicia and me went to the school by bus.")
for token in doc:
token.text, token.pos_, token.tag_, \
spacy.explain(token.pos_), spacy.explain(token.tag_)
...
('Alicia', 'PROPN', 'NNP', 'proper noun', 'noun, proper singular')
('and', 'CCONJ', 'CC', 'coordinating conjunction', 'conjunction, coordinating')
('me', 'PRON', 'PRP', 'pronoun', 'pronoun, personal')
('went', 'VERB', 'VBD', 'verb', 'verb, past tense')
('to', 'ADP', 'IN', 'adposition', 'conjunction, subordinating or preposition')
('school', 'NOUN', 'NN', 'noun', 'noun, singular or mass')
('with', 'ADP', 'IN', 'adposition', 'conjunction, subordinating or preposition')
('bus', 'NOUN', 'NN', 'noun', 'noun, singular or mass')
('.', 'PUNCT', '.', 'punctuation', 'punctuation mark, sentence closer')
我们遍历了标记并打印了标记的文本、通用标签和细粒度标签,以及在此概述的解释:
-
Alicia是一个专有名词,正如预期的那样,NNP是专有名词的标签。 -
me是一个代词,bus是一个名词。NN是单数名词的标签,PRP是个人代词标签。 -
动词标签以
V开头。在这里,有三个动词,如下所示:
现在,考虑以下句子:
doc = nlp("My friend will fly to New York fast and she is staying there for 3 days.")
for token in doc:
token.text, token.pos_, token.tag_, \
spacy.explain(token.pos_), spacy.explain(token.tag_)
…
('My', 'DET', 'PRP$', 'determiner', 'pronoun, possessive')
('friend', 'NOUN', 'NN', 'noun', 'noun, singular or mass')
('will', 'VERB', 'MD', 'verb', 'verb, modal auxiliary')
('fly', 'VERB', 'VB', 'verb', 'verb, base form')
('to', 'ADP', 'IN', 'adposition', 'conjunction, subordinating or preposition')
('New', 'PROPN', 'NNP', 'proper noun', 'noun, proper singular')
('York', 'PROPN', 'NNP', 'proper noun', 'noun, proper singular')
('fast', 'ADV', 'RB', 'adverb', 'adverb')
('and', 'CCONJ', 'CC', 'coordinating conjunction', 'conjunction, coordinating')
('she', 'PRON', 'PRP', 'pronoun', 'pronoun, personal')
('is', 'AUX', 'VBZ', 'auxiliary', 'verb, 3rd person singular present')
('staying', 'VERB', 'VBG', 'verb', 'verb, gerund or present participle')
('there', 'ADV', 'RB', 'adverb', 'adverb')
('for', 'ADP', 'IN', 'adposition', 'conjunction, subordinating or preposition')
('3', 'NUM', 'CD', 'numeral', 'cardinal number')
('days', 'NOUN', 'NNS', 'noun', 'noun, plural')
('.', 'PUNCT', '.', 'punctuation', 'punctuation mark, sentence closer')
让我们从动词开始。正如我们在第一个例子中指出的那样,动词标签以 V 开头。在这里有三个动词,如下所示:
-
fly:基本形式 -
staying:一个 -ing 形式 -
is:一个助动词
对应的标签是 VB、VBG 和 VBZ。
另一个细节是 New 和 York 都被标记为专有名词。如果一个专有名词由多个标记组成,那么所有标记都接受 NNP 标签。My 是一个物主代词,被标记为 PRP$,与前面的个人代词 me 及其标签 PRP 相比。
让我们继续一个可以根据上下文是动词或名词的单词:ship。在以下句子中,ship 被用作动词:
doc = nlp("I will ship the package tomorrow.")
for token in doc:
token.text, token.tag_, spacy.explain(token.tag_)
...
('I', 'PRP', 'pronoun, personal')
('will', 'MD', 'verb, modal auxiliary')
('ship', 'VB', 'verb, base form')
('the', 'DT', 'determiner')
('package', 'NN', 'noun, singular or mass')
('tomorrow', 'NN', 'noun, singular or mass')
('.', '.', 'punctuation mark, sentence closer')
在这里,ship 被标记为动词,正如我们所预期的那样。我们的下一句话也包含了单词 ship,但作为名词。现在,spaCy 标签器能否正确地标记它?请看下面的代码片段以了解详情:
doc = nlp("I saw a red ship.")
for token in doc:
... token.text, token.tag_, spacy.explain(token.tag_)
...
('I', 'PRP', 'pronoun, personal')
('saw', 'VBD', 'verb, past tense')
('a', 'DT', 'determiner')
('red', 'JJ', 'adjective')
('ship', 'NN', 'noun, singular or mass')
('.', '.', 'punctuation mark, sentence closer')
Et voilà!这次,单词 ship 现在被标记为名词,正如我们想要看到的那样。标签器检查了周围的单词;在这里,ship 与一个限定词和一个形容词一起使用,spaCy 推断它应该是一个名词。
这个句子怎么样:
doc = nlp("My cat will fish for a fish tomorrow in a fishy way.")
for token in doc:
token.text, token.pos_, token.tag_, \
spacy.explain(token.pos_), spacy.explain(token.tag_)
…
('My', 'DET', 'PRP$', 'determiner', 'pronoun, possessive')
('cat', 'NOUN', 'NN', 'noun', 'noun, singular or mass')
('will', 'VERB', 'MD', 'verb', 'verb, modal auxiliary')
('fish', 'VERB', 'VB', 'verb', 'verb, base form')
('for', 'ADP', 'IN', 'adposition', 'conjunction, subordinating or preposition')
('a', 'DET', 'DT', 'determiner', 'determiner')
('fish', 'NOUN', 'NN', 'noun', 'noun, singular or mass')
('tomorrow', 'NOUN', 'NN', 'noun', 'noun, singular or mass')
('in', 'ADP', 'IN', 'adposition', 'conjunction, subordinating or preposition')
('a', 'DET', 'DT', 'determiner', 'determiner')
('fishy', 'ADJ', 'JJ', 'adjective', 'adjective')
('way', 'NOUN', 'NN', 'noun', 'noun, singular or mass')
('.', 'PUNCT', '.', 'punctuation', 'punctuation mark, sentence closer')
我们想用单词 fish 的不同用法来愚弄标签器,但标签器足够智能,能够区分动词 fish、名词 fish 和形容词 fishy。这是它如何做到的:
-
首先,
fish紧随情态动词will之后,因此标注器将其识别为动词。 -
其次,
fish作为句子的宾语,并被限定词修饰;标签最有可能是名词。 -
最后,
fishy以y结尾,并在句子中位于名词之前,因此它显然是一个形容词。
spaCy 标注器在这里对预测一个棘手的句子做了非常顺利的工作。在非常准确的标注示例之后,我们心中只剩下一个问题:为什么我们需要词性标注?
词性标注在自然语言理解中的重要性是什么,我们为什么需要区分词的类别呢?答案很简单:许多应用程序需要知道词的类型以获得更好的准确性。以机器翻译系统为例:fish (V)和fish (N)在西班牙语中对应不同的单词,如下面的代码片段所示:
I will fish/VB tomorrow. -> Pescaré/V mañana.
I eat fish/NN. -> Como pescado/N.
句法信息可以在许多自然语言理解任务中使用,而玩一些词性标注的小技巧可以帮助你的自然语言理解代码。让我们继续一个传统问题:词义消歧(WSD),以及如何借助 spaCy 标注器来解决这个问题。
词义消歧
词义消歧(WSD)是在句子中决定一个特定单词使用的哪个词义的经典自然语言理解问题。一个词可以有多个词义——例如,考虑单词bass。以下是我们能想到的一些词义:
-
低音——海鲈鱼,鱼(
N) -
低音——最低的男性声音(
N) -
低音——音域最低的男性歌手(
N)
确定单词的词义在搜索引擎、机器翻译和问答系统中可能至关重要。对于先前的例子,不幸的是,词性标注器对bass的帮助不大,因为标注器将所有词义都标记为名词。我们需要比词性标注器更多的东西。那么,单词beat怎么样?让我们来看看这个例子:
-
击败——猛烈打击(
V) -
击败——在游戏或比赛中击败某人(
V) -
击败——音乐或诗歌中的节奏(
N) -
击败——鸟的翅膀动作(
N) -
击败——完全耗尽(
ADJ)
在这里,词性标注确实能帮上大忙。ADJ标签明确地决定了词义;如果单词beat被标注为ADJ,它识别的词义是完全耗尽。这里的V和N标签并非如此;如果单词beat被标记为V标签,其词义可以是猛烈打击或击败某人。词义消歧是一个开放性问题,已经提出了许多复杂的统计模型。然而,如果你需要一个快速的原型,你可以在某些情况下(例如在先前的例子中)借助 spaCy 标注器来解决这个问题。
自然语言理解应用中的动词时态和语气
在上一章中,我们通过使用词形还原的例子,从旅行社应用程序中获得了动词的基础形式(这些形式摆脱了时态和语气)。在本小节中,我们将关注如何使用在词形还原过程中丢失的动词时态和语气信息。
动词时态和体可能是动词提供给我们最有趣的信息,告诉我们动作何时发生以及动词的动作是已完成还是正在进行的。时态和体一起表明动词对当前时间的参照。英语有三种基本时态:过去、现在和未来。时态伴随着简单、进行/连续或完成体。例如,在句子“我在吃饭”中,动作“吃”发生在现在并且是持续的,因此我们描述这个动词为“现在进行/连续”。
到目前为止,一切顺利。那么,我们如何在我们的旅行社 NLU 中使用这些信息呢?考虑以下可以指向我们的 NLU 应用的客户句子:
I flew to Rome.
I have flown to Rome.
I'm flying to Rome.
I need to fly to Rome.
I will fly to Rome.
在所有句子中,动作是“飞”:然而,只有一些句子表示有订票意图。让我们想象以下带有周围上下文的句子:
I flew to Rome 3 days ago. I still didn't get the bill, please send it ASAP.
I have flown to Rome this morning and forgot my laptop on the airplane. Can you please connect me to lost and found?
I'm flying to Rome next week. Can you check flight availability?
I need to fly to Rome. Can you check flights on next Tuesday?
I will fly to Rome next week. Can you check the flights?
快速浏览一下,动词“fly”的过去式和完成式根本不表示预订意图。相反,它们指向客户投诉或客户服务问题。另一方面,不定式和现在进行时形式则指向预订意图。让我们用以下代码段标记和词干化动词:
sent1 = "I flew to Rome".
sent2 = "I'm flying to Rome."
sent3 = "I will fly to Rome."
doc1 = nlp(sent1)
doc2 = nlp(sent2)
doc3 = nlp(sent3)
for doc in [doc1, doc2, doc3]
print([(w.text, w.lemma_) for w in doc if w.tag_== 'VBG' or w.tag_== 'VB'])
...
[]
[('flying', 'fly')]
[('fly', 'fly')]
我们逐个迭代了三个doc对象,并对每个句子检查标记的细粒度标签是否为VBG(现在进行时的动词)或VB(基本/不定式形式的动词)。基本上,我们过滤出了现在进行时和基本形式的动词。你可以将这个过程视为动词的语义表示,形式为(词形,词干,标签),如下面的代码片段所示:
flying: (fly, VBG)
我们已经覆盖了一个语义任务和一个形态学任务——词义消歧和动词的时态/体。我们将继续探讨一个棘手的问题:如何充分利用一些特殊的标签——即数字、符号和标点符号标签。
理解数字、符号和标点符号标签
如果你查看英语的 POS(词性标注),你会注意到NUM、SYM和PUNCT标签。这些分别是数字、符号和标点的标签。这些类别被细分为细粒度类别:$、SYM、''、-LRB-和-RRB-。这些在下面的屏幕截图中有显示:

图 3.3 – spaCy 标点符号标签,一般和细粒度
让我们标记一些包含数字和符号的示例句子,如下所示:
doc = nlp("He earned $5.5 million in 2020 and paid %35 tax.")
for token in doc:
token.text, token.tag_, spacy.explain(token.tag_)
...
('He', 'PRP', 'pronoun, personal')
('earned', 'VBD', 'verb, past tense')
('$', '$', 'symbol, currency')
('5.5', 'CD', 'cardinal number')
('million', 'CD', 'cardinal number')
('in', 'IN', 'conjunction, subordinating or preposition')
('2020', 'CD', 'cardinal number')
('and', 'CC', 'conjunction, coordinating')
('paid', 'VBD', 'verb, past tense')
('35', 'CD', 'cardinal number')
('percent', 'NN', 'noun, singular or mass')
('tax', 'NN', 'noun, singular or mass')
('.', '.', 'punctuation mark, sentence closer')
我们再次遍历了标记并打印了细粒度的标签。标记器能够区分符号、标点符号和数字。甚至单词“百万”也被识别为数字!
现在,关于符号标签怎么办?货币符号和数字提供了一种系统地提取货币描述的方法,在金融文本(如财务报告)中非常方便。我们将在第四章的基于规则的匹配中看到如何提取货币实体。
就这样——你已经到达了本节详尽内容的结尾!有很多东西需要分解和消化,但我们向你保证,你对工业 NLP 工作的投资是值得的。现在,我们将继续探讨另一个句法概念——依赖句法解析。
依赖句法解析简介
如果你已经熟悉 spaCy,你肯定遇到过 spaCy 依赖句法解析器。尽管许多开发者看到 spaCy 文档中的依赖句法解析器,但他们可能对此有所顾虑,或者不知道如何充分利用这一功能。在本部分,你将探索一种系统性地表示句子句法的方法。让我们从依赖句法实际上是什么开始。
什么是依赖句法解析?
在上一节中,我们专注于词性标注——单词的句法类别。尽管词性标注提供了关于相邻单词标签的信息,但它们并没有给出给定句子中非相邻单词之间的任何关系。
在本节中,我们将关注依赖句法解析——一种更结构化的探索句子句法的方法。正如其名所示,依赖句法解析与通过标记之间的依赖关系分析句子结构相关。依赖句法解析器标记句子标记之间的句法关系,并连接句法相关的标记对。依赖或依赖关系是两个标记之间的有向链接。
依赖句法解析的结果始终是一个树,如下截图所示:
![图 3.4 – 依赖树的示例(摘自维基百科)]

图 3.4 – 依赖树的示例(摘自维基百科)
如果你不太熟悉树形数据结构,你可以在这个优秀的计算机科学资源中了解更多信息:
https://www.cs.cmu.edu/~clo/www/CMU/DataStructures/Lessons/lesson4_1.htm
依赖关系
那么依赖关系有什么用呢?在 NLP 中,许多统计方法都围绕单词的向量表示展开,并将句子视为单词的序列。正如你在图 3.4中可以看到的,一个句子不仅仅是标记的序列——它有一个结构;句子中的每个单词都有一个明确的角色,例如动词、主语、宾语等等;因此,句子肯定是有结构的。这种结构在聊天机器人、问答系统和机器翻译中得到广泛的应用。
最有用的应用之一是确定句子中的宾语和主语。再次,让我们回到我们的旅行社应用程序。想象一下,一位客户正在抱怨服务。比较以下两个句子,“我把邮件转发给你”和“你把邮件转发给我”;如果我们消除了停用词“我”、“你”、“我”和“的”,剩下的就是:
I forwarded you the email. -> forwarded email
You forwarded me the email. -> forwarded email
尽管句子的剩余部分相同,但句子具有非常不同的含义,需要不同的答案。在第一个句子中,句子主语是I(因此,答案很可能会以你开头)和第二个句子的主语是you(这将结束于一个I答案)。
显然,依存分析器帮助我们更深入地了解句子的句法和语义。让我们从依存关系开始探索。
句法关系
spaCy 为每个标记分配一个依存标签,就像其他语言特征(如词元或词性标签)一样。spaCy 使用有向弧显示依存关系。以下截图显示了名词与其修饰名词的形容词之间的依存关系示例:

图 3.5 – 名词与其形容词之间的依存关系
依存标签描述了两个标记之间的句法关系类型如下:其中一个标记是flower,它是“head”,而blue是其依赖/子节点。
依存标签分配给子节点。标记对象有dep (int)和dep_ (unicode)属性,它们包含依存标签,如下面的代码片段所示:
doc = nlp("blue flower")
for token in doc:
token.text, token.dep_
…
('blue', 'amod')
('flower', 'ROOT')
在这个例子中,我们遍历了标记并打印了它们的文本和依存标签。让我们一步一步地回顾一下发生了什么,如下所示:
-
blue承认了amod标签。amod是形容词-名词关系的依存标签。有关amod关系的更多示例,请参阅图 3.7。 -
flower是ROOT。ROOT是依存树中的一个特殊标签;它分配给句子的主要动词。如果我们处理的是一个短语(而不是完整的句子),则ROOT标签分配给短语的根,即短语的中心名词。在blue flower短语中,中心名词flower是短语的根。 -
每个句子/短语都有一个根节点,它是分析树的根(记住,依存分析的结果是一个树)。
-
树节点可以有多个子节点,但每个节点只能有一个父节点(由于树的限制,以及不包含循环的树)。换句话说,每个标记恰好有一个头节点,但父节点可以有多个子节点。这就是为什么依存标签分配给依赖节点的原因。
这里是 spaCy 英语依存标签的完整列表:

图 3.6 – spaCy 英语依存标签列表
这是一个很长的列表!不用担心——你不需要记住每个列表项。让我们首先看看最常见的和最有用的标签列表,然后我们将看到它们是如何将标记连接到一起的。首先是这个列表:
-
amod:形容词修饰语 -
aux:助动词 -
compound:复合 -
dative:宾格宾语 -
det:限定词 -
dobj:直接宾语 -
nsubj:名词主语 -
nsubjpass:名词主语,被动 -
nummod:数字修饰语 -
poss:所有格修饰语 -
root:根
让我们看看上述标签的使用示例以及它们表达的关系。amod 是形容词修饰语。从名称上可以理解,这种关系修饰名词(或代词)。在下面的屏幕截图中,我们看到白色修饰羊:




图 3.10 – det 关系
接下来,我们来看两种宾语关系,dative 和 dobj。dobj 关系存在于动词和它的直接宾语之间。一个句子可以有一个以上的宾语(如下面的例子所示);直接宾语是动词所作用的对象,其他的是间接宾语。
直接宾语通常用 dative 关系标记,指向 dative 宾语,它从动词那里接受间接动作。在下面的屏幕截图中,间接宾语是 me,直接宾语是 book:


图 3.12 – nsubj 关系
在下面的屏幕截图中,you 是句子的被动名词主语:

图 3.13 – nsubjpass 关系
我们现在已经涵盖了句子主语和宾语关系。现在,我们将发现两个修饰关系;一个是 nummod poss nummod 很容易找到;它在 3 和 books 之间:

图 3.14 – nummod 关系
一个所有格修饰语发生在所有格代词和名词之间,或者所有格 's' 和名词之间。在下面的屏幕截图所示的句子中,my 是名词 book 的所有格标记:

图 3.15 – "my" 和 "book" 之间的 poss 关系
最后,但同样重要的是,是 root label,它不是一个真实的关系,而是句子动词的标记。根词在句法树中没有真正的父节点;根是句子的主要动词。在前面的句子中,took 和 given 是相应的根。两个句子的主要动词都是助动词 is 和 are。请注意,根节点没有进入弧线——也就是说,没有父节点。
这些是我们 NLU 目的最有用的标签。你绝对不需要记住所有的标签,因为你在下一页的练习中会变得熟悉。此外,你可以在需要时通过 spacy.explain() 向 spaCy 询问任何标签。执行此操作的代码如下所示:
spacy.explain("nsubj")
'nominal subject'
doc = nlp("I own a ginger cat.")
token = doc[4]
token.text, token.dep_, spacy.explain(token.dep_)
('cat', 'dobj', 'direct object')
深吸一口气,因为有很多东西要消化!让我们练习如何利用依存标签。
再次,token.dep_ 包含了从属标记的依存标签。token.head 属性指向头/父标记。只有根标记没有父标记;spaCy 在这种情况下指向标记本身。让我们将 图 3.7 中的示例句子二分,如下所示:
doc = nlp("I counted white sheep.")
for token in doc:
token.text, token.pos_, token.dep_
...
('I', 'PRP', 'nsubj')
('counted', 'VBD', 'ROOT')
('white', 'JJ', 'amod')
('sheep', 'NNS', 'dobj')
('.', '.', 'punct')
我们遍历了标记并打印了细粒度的词性标注和依存标签。counted 是句子的主要动词,并被标记为 ROOT。现在,I 是句子的主语,而 sheep 是直接宾语。white 是一个形容词,修饰名词 sheep,因此其标签是 amod。我们再深入一层,这次打印标记的头,如下所示:
doc = nlp(“I counted white sheep.”)
for token in doc:
token.text, token.tag_, token.dep_, token.head
...
('I', 'PRP', 'nsubj', counted)
('counted', 'VBD', 'ROOT', counted)
('white', 'JJ', 'amod', sheep)
('sheep', 'NNS', 'dobj', counted)
('.', '.', 'punct', counted)
可视化如下:

图 3.16 – 简单句的一个示例解析
当 token.head 属性也参与其中时,同时遵循代码和视觉是一个好主意。让我们一步一步地来理解视觉和代码是如何匹配的:
-
我们从根开始阅读解析树。它是主要动词:
counted。 -
下一步,我们跟随其左侧的弧线指向代词
I,它是句子的名词主语,并被标记为nsubj。 -
现在,回到根节点,
counted。这次,我们向右导航。跟随dobj弧线到达名词sheep。sheep由形容词white修饰,具有amod关系,因此这个句子的直接宾语是white sheep。
即使这样简单、平铺直叙的句子也有一个复杂的依存句法分析树,读起来很复杂,对吧?不要急——通过练习你会习惯的。让我们检查一个更长、更复杂的句子的依存句法树,如下所示:
doc = nlp("We are trying to understand the difference.")
for token in doc:
token.text, token.tag_, token.dep_, token.head
...
('We', 'PRP', 'nsubj', trying)
('are', 'VBP', 'aux', trying)
('trying', 'VBG', 'ROOT', trying)
('to', 'TO', 'aux', understand)
('understand', 'VB', 'xcomp', trying)
('the', 'DT', 'det', difference)
('difference', 'NN', 'dobj', understand)
('.', '.', 'punct', trying)
现在,这次情况看起来有些不同,正如我们在图 3.17中将会看到的。我们定位到主要动词和根节点trying(它没有进入弧线)。trying这个词的左侧看起来可以管理,但右侧有一系列弧线。让我们从左侧开始。代词we被标记为nsubj,因此这是句子的名词主语。另一个左侧弧线,标记为aux,指向trying的从属动词are,它是主要动词trying的助动词。
到目前为止,一切顺利。现在,右侧发生了什么?trying通过xcomp关系连接到第二个动词understand。动词的xcomp(或开放补语)关系是一个没有自己主语的从句。在这里,to understand the difference从句没有主语,因此它是一个开放补语。我们跟随从第二个动词understand开始的dobj弧线,到达名词difference,它是to understand the difference从句的直接宾语,结果是:
![图 3.17 – 一个复杂的解析示例
![img/B16570_03_17.jpg]
图 3.17 – 一个复杂的解析示例
这是对这个示例句子的深入分析,它确实看起来并不那么复杂。接下来,我们处理一个包含拥有自己名词主语的从句的句子,如下所示:
doc = nlp("Queen Katherine, who was the mother of Mary Tudor, died at 1536.")
for token in doc:
token.text, token.tag_, token.dep_, token.head
...
('Queen', 'NNP', 'compound', Katherine)
('Katherine', 'NNP', 'nsubj', died)
(',', ',', 'punct', Katherine)
('who', 'WP', 'nsubj', was)
('was', 'VBD', 'relcl', Katherine)
('the', 'DT', 'det', mother)
('mother', 'NN', 'attr', was)
('of', 'IN', 'prep', mother)
('Mary', 'NNP', 'compound', Tudor)
('Tudor', 'NNP', 'pobj', of)
(',', ',', 'punct', Katherine)
('died', 'VBD', 'ROOT', died)
('at', 'IN', 'prep', died)
('1536', 'CD', 'pobj', at)
为了使视觉效果足够大,我已经将可视化分成了两部分。首先,让我们找到根节点。根节点位于右侧。died是动词的主句和根节点(再次强调,它没有进入弧线)。右侧的其他部分没有复杂的内容。
另一方面,左侧有一些有趣的内容——实际上,一个关系从句。让我们将关系从句结构一分为二:
-
我们从专有名词
Katherine开始,它与died通过nsubj关系连接,因此是句子的主语。 -
我们看到一个复合弧从
Katherine指向专有名词Queen。在这里,Queen是一个头衔,所以它与Katherine的关系是复合的。右侧Mary和Tudor之间也存在相同的关系,姓氏和名字也通过复合关系联系在一起。
现在是时候将关系从句who was the mother of Mary Tudor一分为二了,如下所示:
-
首先,关系从句中提到的是
Katherine,所以我们看到从Katherine到关系从句中的was的relcl(关系从句)弧线。 -
who是从句的名词主语,通过nsubj关系与was相连。正如你在下面的截图中所见,依存句法树与之前例子句子中的不同,那个例子句子中的从句没有名词主语:

图 3.18 – 带有相对从句的依存句法树,左侧部分

图 3.19 – 相同句子的右侧部分
如果你觉得自己无法记住所有关系,这是很正常的。不用担心——总是找到句子的根/主动词,然后跟随从根出发的弧线深入,就像我们之前做的那样。你总是可以查看 spaCy 文档(spacy.io/api/annotation#dependency-parsing)来了解关系类型意味着什么。直到你对这个概念和细节熟悉起来,请慢慢来。
这已经非常全面了!亲爱的读者——正如我们之前所说,请花些时间消化和实践例子句子。displaCy 在线演示是一个很好的工具,所以不要害羞,尝试你自己的例子句子并查看解析结果。你发现这一部分内容较多是很正常的。然而,这一部分是普通语言学以及第四章中信息提取和模式匹配练习的坚实基础,基于规则的匹配。在第六章中,将一切整合:使用 spaCy 进行语义解析的案例研究之后,你会更加得心应手。给自己一些时间,通过书中各个例子来消化依存句法分析。
依存句法分析之后是什么?毫无疑问,你一定经常在 NLU 世界中听到 NER(命名实体识别)这个词。让我们来探讨这个非常重要的 NLU 概念。
介绍 NER
我们以一个分词器开始了这一章,接下来我们将看到另一个非常实用的分词器——spaCy 的 NER 分词器。正如 NER 的名字所暗示的,我们感兴趣的是寻找命名实体。
什么是命名实体?命名实体是我们可以用一个专有名称或感兴趣的量来指代的现实世界中的对象。它可以是一个人、一个地方(城市、国家、地标、著名建筑)、一个组织、一家公司、一个产品、日期、时间、百分比、货币金额、一种药物或疾病名称。一些例子包括 Alicia Keys、巴黎、法国、勃兰登堡门、世界卫生组织、谷歌、保时捷卡宴等等。
命名实体总是指向一个特定的对象,而这个对象可以通过相应的命名实体来区分。例如,如果我们标记句子 巴黎是法国的首都,我们将 巴黎 和 法国 解析为命名实体,但不会标记单词 首都。原因是 首都 并不指向一个特定的对象;它是许多对象的通用名称。
NER 分类与 POS 分类略有不同。在这里,分类的数量可以高达我们想要的。最常见的分类是人物、地点和组织,几乎所有可用的 NER 标签器都支持这些分类。在下面的屏幕截图中,我们可以看到相应的标签:

图 3.20 – 最常见的实体类型
spaCy 支持广泛的实体类型。您使用哪些类型取决于您的语料库。如果您处理财务文本,您最可能比 WORK_OF_ART 更频繁地使用 MONEY 和 PERCENTAGE。
下面是 spaCy 支持的实体类型列表:

图 3.21 – spaCy 支持的实体类型完整列表
就像 POS 标签统计模型一样,NER 模型也是顺序模型。第一个现代 NER 标签模型是一个 条件随机场(CRF)。CRFs 是用于结构化预测问题(如标记和解析)的序列分类器。如果您想了解更多关于 CRF 实现细节的信息,可以阅读此资源:https://homepages.inf.ed.ac.uk/csutton/publications/crftutv2.pdf。当前最先进的 NER 标记是通过神经网络模型实现的,通常是 LSTM 或 LSTM+CRF 架构。
文档中的命名实体可以通过 doc.ents 属性访问。doc.ents 是一个 Span 对象的列表,如下面的代码片段所示:
doc = nlp("The president Donald Trump visited France.")
doc.ents
(Donald Trump, France)
type(doc.ents[1])
<class 'spacy.tokens.span.Span'>
spaCy 还会为每个标记分配实体类型。命名实体的类型可以通过 token.ent_type (int) 和 token.ent_type_ (unicode) 获取。如果标记不是命名实体,则 token.ent_type_ 只是一个空字符串。
就像 POS 标签和依存标签一样,我们可以对标签字符串或 token.ent_type_ 调用 spacy.explain(),如下所示:
spacy.explain("ORG")
'Companies, agencies, institutions, etc.
doc = nlp("He worked for NASA.")
token = doc[3]
token.ent_type_, spacy.explain(token.ent_type_)
('ORG', 'Companies, agencies, institutions, etc.')
让我们来看一些示例,看看 spaCy NER 标签器的实际应用,如下所示:
doc = nlp("Albert Einstein was born in Ulm on 1879\. He studied electronical engineering at ETH Zurich.")
doc.ents
(Albert Einstein, Ulm, 1879, ETH Zurich)
for token in doc:
token.text, token.ent_type_, \
spacy.explain(token.ent_type_)
...
('Albert', 'PERSON', 'People, including fictional')
('Einstein', 'PERSON', 'People, including fictional')
('was', '', None)
('born', '', None)
('in', '', None)
('Ulm', 'GPE', 'Countries, cities, states')
('on', '', None)
('1879', 'DATE', 'Absolute or relative dates or periods')
('.', '', None)
('He', '', None)
('studied', '', None)
('electronical', '', None)
('engineering', '', None)
('at', '', None)
('ETH', 'ORG', 'Companies, agencies, institutions, etc.')
('Zurich', 'ORG', 'Companies, agencies, institutions, etc.')
('.', '', None)
我们逐个遍历标记并打印标记及其实体类型。如果标记未被标记为实体,则 token.ent_type_ 只是一个空字符串,因此没有 spacy.explain() 的解释。对于属于 NE 的标记,返回适当的标记。在先前的句子中,Albert Einstein、Ulm、1879 和 ETH Zurich 分别被正确标记为 PERSON、GPE、DATE 和 ORG。
让我们看看一个更长且更复杂的句子,其中包含非英语实体,并查看 spaCy 如何标记它,如下所示:
doc = nlp("Jean-Michel Basquiat was an American artist of Haitian and Puerto Rican descent who gained fame with his graffiti and street art work")
doc.ents
(Jean-Michel Basquiat, American, Haitian, Puerto Rican)
for ent in doc.ents:
ent, ent.label_, spacy.explain(ent.label_)
...
(Jean-Michel Basquiat, 'PERSON', 'People, including fictional')
(American, 'NORP', 'Nationalities or religious or political groups')
(Haitian, 'NORP', 'Nationalities or religious or political groups')
(Puerto Rican, 'NORP', 'Nationalities or religious or political groups')
看起来不错!spaCy 标签器平滑地识别了一个带有 - 的人实体。总的来说,标签器在处理不同实体类型时表现相当好,正如我们在示例中看到的那样。
在标记具有不同句法特征的标记之后,我们有时希望将实体合并/拆分到更少/更多的标记中。在下一节中,我们将看到合并和拆分是如何进行的。在此之前,我们将看到 NER 标签的实际应用案例。
实际案例
NER 是 spaCy 中流行且经常使用的管道组件。NER 是理解文本主题的关键组件之一,因为命名实体通常属于一个语义类别。例如,特朗普总统在我们的脑海中唤起了政治主题,而莱昂纳多·迪卡普里奥则更多关于电影。如果你想深入了解解决文本意义和理解谁做了什么,你也需要命名实体。
这个现实世界的例子包括处理一篇《纽约时报》文章。让我们先运行以下代码来下载文章:
from bs4 import BeautifulSoup
import requests
import spacy
def url_text(url_string):
res = requests.get(url)
html = res.text
soup = BeautifulSoup(html, 'html5lib')
for script in soup(["script", "style", 'aside']):
script.extract()
text = soup.get_text()
return " ".join(text.split())
ny_art = url_text("https://www.nytimes.com/2021/01/12/opinion/trump-america-allies.html")
nlp = spacy.load("en_core_web_md")
doc = nlp(ny_art)
我们下载了文章,BeautifulSoup是一个流行的 Python 包,用于从 HTML 中提取文本和nlp对象,将文章主体传递给nlp对象,并创建了一个Doc对象。
让我们从文章的实体类型计数开始分析,如下所示:
len(doc.ents)
136
对于包含许多实体的新闻文章来说,这是一个完全正常的数字。让我们进一步对实体类型进行分组,如下所示:
from collections import Counter
labels = [ent.label_ for ent in doc.ents]
Counter(labels)
Counter({'GPE': 37, 'PERSON': 30, 'NORP': 24, 'ORG': 22, 'DATE': 13, 'CARDINAL': 3, 'FAC': 2, 'LOC': 2, 'EVENT': 1, 'TIME': 1, 'WORK_OF_ART': 1})
最频繁的实体类型是GPE,表示国家、城市或州。其次是PERSON,而第三频繁的实体标签是NORP,表示国籍/宗教政治团体。接下来是组织、日期和基数类型实体。
我们能否通过查看实体或理解文本主题来总结文本?为了回答这个问题,让我们首先统计实体中最频繁出现的标记,如下所示:
items = [ent.text for ent in doc.ents]
Counter(items).most_common(10)
[('America', 12), ('American', 8), ('Biden', 8), ('China', 6), ('Trump', 5), ('Capitol', 4), ('the United States', 3), ('Washington', 3), ('Europeans', 3), ('Americans', 3)]
看起来像是一个语义组!显然,这篇文章是关于美国政治的,可能还涉及美国在政治上如何与世界其他国家互动。如果我们打印出文章中的所有实体,我们可以看到这里的猜测是正确的:
print(doc.ents)
(The New York Times SectionsSEARCHSkip, indexLog inToday, storyOpinionSupported byContinue, LaughingstockLast week's, U.S., U.S., Ivan KrastevMr, Krastev, Jan., 2021 A, Rome, Donald Tramp, Thursday, Andrew Medichini, Associated PressDonald Trump, America, America, Russian, Chinese, Iranian, Jan. 6, Capitol, Ukraine, Georgia, American, American, the United States, Trump, American, Congress, Civil War, 19th-century, German, Otto von Bismarck, the United States of America, America, Capitol, Trump, last hours, American, American, Washington, Washington, Capitol, America, America, Russia, at least 10, Four years, Trump, Joe Biden, two, American, China, Biden, America, Trump, Recep Tayyip Erdogan, Turkey, Jair Bolsonaro, Brazil, Washington, Russia, China, Biden, Gianpaolo Baiocchi, H. Jacob Carlson, Social Housing Development Authority, Ezra Klein, Biden, Mark Bittman, Biden, Gail Collins, Joe Biden, Jake Sullivan, Biden, trans-Atlantic, China, Just a week ago, European, Sullivan, Europe, America, China, Biden, Europeans, China, German, Chinese, the European Union's, America, Christophe Ena, the European Council on Foreign Relations, the weeks, American, the day, Biden, Europeans, America, the next 10 years, China, the United States, Germans, Trump, Americans, Congress, America, Bill Clinton, Americans, Biden, the White House, the United States, Americans, Europeans, the past century, America, the days, Capitol, democratic, Europe, American, America, Ivan Krastev, the Center for Liberal Strategies, the Institute for Human Sciences, Vienna, Is It Tomorrow Yet?:, The New York Times Opinion, Facebook, Twitter (@NYTopinion, Instagram, AdvertisementContinue, IndexSite Information Navigation© 2021, The New York Times, GTM, tfAzqo1rYDLgYhmTnSjPqw>m_preview)
我们通过将文本粘贴到displaCy 命名实体可视化器(explosion.ai/demos/displacy-ent/)来制作整篇文章的可视化。以下截图是从捕获了部分可视化的演示页面中获取的:

图 3.22 – 使用 displaCy 可视化的《纽约时报》文章的实体
spaCy 的 NER 为我们理解文本以及向我们自己、同事和利益相关者展示美观的视觉提供了强大的功能。
合并和拆分标记
我们在前一节中提取了命名实体,但如果我们想合并或拆分多词命名实体怎么办?还有,如果分词器在某些异国情调的标记上表现不佳,你想手动拆分它们怎么办?在本小节中,我们将介绍一种针对我们的多词表达式、多词命名实体和错别字的非常实用的补救措施。
doc.retokenize是合并和拆分跨度段的正确工具。让我们通过合并一个多词命名实体来查看重分词的示例,如下所示:
doc = nlp("She lived in New Hampshire.")
doc.ents
(New Hampshire,)
[(token.text, token.i) for token in doc]
[('She', 0), ('lived', 1), ('in', 2), ('New', 3), ('Hampshire', 4), ('.', 5)]
len(doc)
6
with doc.retokenize() as retokenizer:
retokenizer.merge(doc[3:5], \
attrs={"LEMMA": "new hampshire"})
...
[(token.text, token.i) for token in doc]
[('She', 0), ('lived', 1), ('in', 2), ('New Hampshire', 3), ('.', 4)]
len(doc)
5
doc.ents
(New Hampshire,)
[(token.lemma_) for token in doc]
['-PRON-', 'live', 'in', 'new hampshire', '.']
这就是我们前面代码中所做的:
-
首先,我们从样本句子创建了一个
doc对象。 -
然后,我们使用
doc.ents打印了其实体,结果是预期的New Hampshire。 -
在下一行,对于每个标记,我们打印了
token.text,包括句子中的标记索引(token.i)。 -
此外,我们通过在
doc对象上调用len来检查doc对象的长度,结果是6(.也是一个标记)。
现在,我们想要合并位置3到5(3包含在内;5不包含),所以我们做了以下操作:
-
首先,我们调用了
retokenizer方法的merge(indices, attrs)。attrs是我们想要分配给新标记的标记属性字典,例如lemma、pos、tag、ent_type等。 -
在前面的例子中,我们设置了新标记的词元;否则,词元只会是
New(我们想要合并的跨度的起始标记的词元)。 -
然后,我们打印了标记以查看操作是否按我们的意愿进行。当我们打印新标记时,我们看到新的
doc[3]是New Hampshire标记。 -
此外,
doc对象现在长度为5,所以我们减少了 doc 一个标记。doc.ents保持不变,新标记的词元是new hampshire,因为我们用attrs设置了它。
看起来不错,那么如何将多词标记分割成几个标记呢?在这种情况下,要么是你想要修复的文本中存在拼写错误,要么是自定义标记化对于你的特定句子来说不满意。
分割一个跨度比合并跨度要复杂一些,原因如下:
-
我们正在改变依赖树。
-
我们需要为新标记分配新的词性标签、依赖标签和必要的标记属性。
-
基本上,我们需要考虑如何为新创建的标记分配语言特征。
让我们通过以下修复拼写错误的例子来看看如何处理新标记:
doc = nlp("She lived in NewHampshire")
len(doc)
5
[(token.text, token.lemma_, token.i) for token in doc]
[('She', '-PRON-', 0), ('lived', 'live', 1), ('in', 'in', 2), ('NewHampshire', 'NewHampshire', 3), ('.', '.', 4)]
for token in doc:
token.text, token.pos_, token.tag_, token.dep_
...
('She', 'PRON', 'PRP', 'nsubj')
('lived', 'VERB', 'VBD', 'ROOT')
('in', 'ADP', 'IN', 'prep')
('NewHampshire', 'PROPN', 'NNP', 'pobj')
('.', 'PUNCT', '.', 'punct')
在分割操作之前,依赖树看起来是这样的:

图 3.23 – 样本句子在重新标记前的依赖树
现在,我们将doc[3]、NewHampshire分割成两个标记:New和Hampshire。我们将通过attrs字典为新标记分配细粒度的词性标签和依赖标签。我们还将通过heads参数传递新标记的父标记来重新排列依赖树。在排列头部时,有两个事情需要考虑,如下所述:
-
首先,如果你在以下代码段中给出一个相对位置(例如
(doc[3], 1)),这意味着doc[3]的头部将是+1 位置的标记,即新设置中的doc[4](请参见以下可视化)。 -
其次,如果你给出一个绝对位置,这意味着在原始
Doc对象中的位置。在以下代码片段中,heads列表中的第二项意味着Hampshire标记的头部是原始 Doc 中的第二个标记,即in标记(请参阅图 3.23)。
分割后,我们打印了新标记的列表和语言属性。我们还检查了doc对象的新长度,现在为6。你可以在这里看到结果:
with doc.retokenize() as retokenizer:
heads = [(doc[3], 1), doc[2]]
attrs = {"TAG":["NNP", "NNP"],
"DEP": ["compound", "pobj"]}
retokenizer.split(doc[3], ["New", "Hampshire"],
heads=heads, attrs=attrs)
...
[(token.text, token.lemma_, token.i) for token in doc]
[('She', '-PRON-', 0), ('lived', 'live', 1), ('in', 'in', 2), ('New', 'New', 3), ('Hampshire', 'Hampshire', 4), ('.', '.', 5)]
for token in doc:
token.text, token.pos_, token.tag_, token.dep_
...
('She', 'PRON', 'PRP', 'nsubj')
('lived', 'VERB', 'VBD', 'ROOT')
('in', 'ADP', 'IN', 'prep')
('New', 'PROPN', 'NNP', 'pobj')
('Hampshire', 'PROPN', 'NNP', 'compound')
('.', 'PUNCT', '.', 'punct')
len(doc)
6
这是分割操作后依赖树的模样(请与图 3.22进行比较):

图 3.24 – 分割操作后的依赖树
你可以应用合并和分割到任何跨度,而不仅仅是命名实体跨度。这里最重要的部分是正确排列新的依赖树和语言属性。
摘要
就这样——你到达了本章的结尾!这确实是一次详尽且漫长的旅程,但我们已经完全揭示了 spaCy 的真实语言能力。本章为您提供了 spaCy 的语言特征及其使用方法的详细信息。
你学习了关于词性标注和应用的知识,还有许多示例。你还了解了一个重要但不太为人所知且使用得很好的 spaCy 特性——依赖标签。然后,我们发现了一个著名的 NLU 工具和概念,NER。我们看到了如何通过示例进行命名实体提取。我们用一个非常实用的工具结束了本章,这个工具可以合并和分割我们在前几节计算出的跨度。
那么,接下来是什么?在下一章中,我们又将发现一个你将在日常 NLP 应用程序代码中每天都会使用的 spaCy 特性——spaCy 的Matcher类。我们不想在这个美好的主题上给出剧透,所以让我们一起继续我们的旅程吧!
第四章:基于规则的匹配
基于规则的实体提取对于任何 NLP 管道都是必不可少的。某些类型的实体,如时间、日期和电话号码,具有独特的格式,可以通过一组规则识别,而无需训练统计模型。
在本章中,你将学习如何通过匹配模式和短语快速从文本中提取信息。你将使用 Matcher 对象。你将继续使用基于规则的匹配来细化统计模型,以提高准确性。
到本章结束时,你将了解信息提取的关键部分。你将能够提取特定格式的实体,以及特定领域的实体。
在本章中,我们将介绍以下主要主题:
-
基于标记的匹配
-
短语匹配器
-
实体规则器
-
结合 spaCy 模型和匹配器
基于标记的匹配
到目前为止,我们已经探讨了需要统计模型和其用法的复杂语言概念,以及使用 spaCy 的应用。某些 NLU 任务可以在没有统计模型帮助的情况下以巧妙的方式解决。其中一种方式是 正则表达式,我们用它来将预定义的模式与我们的文本进行匹配。
正则表达式(正则表达式)是一系列字符,它指定了一个搜索模式。正则表达式描述了一组遵循指定模式的字符串。正则表达式可以包括字母、数字以及具有特殊意义的字符,例如 ?, ., 和 ***。Python 的内置库提供了强大的支持来定义和匹配正则表达式。还有一个名为 regex 的 Python 3 库,其目标是未来取代 re。
正在积极使用 Python 开发 NLP 应用的读者肯定遇到过正则表达式代码,甚至更好地,他们自己编写过正则表达式。
那么,正则表达式看起来是什么样子的呢?以下正则表达式匹配以下字符串:
-
巴拉克·奥巴马
-
巴拉克·奥巴马
-
巴拉克·侯赛因·奥巴马
reg = r"Barack\s(Hussein\s)?Obama"
这个模式可以读作:字符串 Barack 可以可选地后跟字符串 Hussein(正则表达式中的 ? 字符表示可选的,即 0 或 1 次出现)并且应该后跟字符串 Obama。单词间的空格可以是一个空格字符、一个制表符或任何其他空白字符(\s 匹配所有类型的空白字符,包括换行符)。
即使对于如此简短且简单的模式,它也不太易读,对吧?这就是正则表达式的缺点,它是以下:
-
难以阅读
-
难以调试
-
容易出错,尤其是在空格、标点符号和数字字符方面
由于这些原因,许多软件工程师不喜欢在生产代码中使用正则表达式。spaCy 提供了一个非常干净、可读、生产级和可维护的替代方案:Matcher类。Matcher类可以将我们预定义的规则与Doc和Span对象中的标记序列进行匹配;此外,规则可以引用标记或其语言属性(关于这个主题,在本节稍后部分会详细介绍)。
让我们从如何调用Matcher类的基本例子开始:
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en")
doc = nlp("Good morning, I want to reserve a ticket.")
matcher = Matcher(nlp.vocab)
pattern = [{"LOWER": "good"}, {"LOWER": "morning"},
{"IS_PUNCT": True}]
matcher.add("morningGreeting", None, pattern)
matches = matcher(doc)
for match_id, start, end in matches:
m_span = doc[start:end]
print(start, end, m_span.text)
...
0 3 Good morning,
它看起来很复杂,但不要感到害怕,我们会逐行讲解:
-
在第一行,我们导入了
spacy;这应该是熟悉的。 -
在第二行,我们导入了
Matcher类,以便在代码的其余部分中使用它。 -
在接下来的几行中,我们像往常一样创建了
nlp对象,并使用我们的示例句子创建了doc对象。 -
现在,请注意:一个
matcher对象需要用Vocabulary对象进行初始化,因此在第 5 行,我们用语言模型词汇表初始化了我们的matcher对象(这是通常的做法)。 -
接下来要做的是定义我们想要匹配的模式。在这里,我们将
pattern定义为列表,其中每个括号内的列表项代表一个标记对象。
你可以按照以下方式阅读前面代码片段中的模式列表:
-
一个文本降低后的内容为
good的标记 -
一个文本降低后的内容为
morning的标记 -
一个标点符号标记(即,
IS_PUNCT特征为True)
然后,我们需要将这个模式引入到matcher中;这正是matcher.add()行所做的事情。在第 7 行,我们将我们的模式引入到matcher对象中,并将其命名为morningGreeting。最后,我们可以在第 8 行通过调用matcher在doc上执行匹配操作。之后,我们检查得到的结果。一个匹配结果是形式为(match id, start position, end position)的三元组列表。在最后一行,我们遍历结果列表并打印匹配结果的起始位置、结束位置和文本。
正如你可能已经注意到的,Good和morning之间的空白根本无关紧要。实际上,我们可以在它们之间放两个空格,写下Good morning,结果将是相同的。为什么?因为Matcher匹配标记和标记属性。
一个模式始终指的是一个连续的标记对象序列,并且每个花括号中的项对应一个标记对象。让我们回到前面代码片段中的模式:
pattern = [{"LOWER": "good"}, {"LOWER": "morning"},
{"IS_PUNCT": True}]
我们可以看到,结果始终是三个标记的匹配。
我们能否添加多个模式?答案是肯定的。让我们通过一个例子来看一下,同时也会看到一个match_id的例子如下:
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en")
doc = nlp("Good morning, I want to reserve a ticket. I will then say good evening!")
matcher = Matcher(nlp.vocab)
pattern1 = [{"LOWER": "good"}, {"LOWER": "morning"},
{"IS_PUNCT": True}]
matcher.add("morningGreeting", [pattern1])
pattern2 = [{"LOWER": "good"}, {"LOWER": "evening"},
{"IS_PUNCT": True}]
matcher.add("eveningGreeting", [pattern2])
matches = matcher(doc)
for match_id, start, end in matches:
pattern_name = nlp.vocab_strings[match_id]
m_span = doc[start:end]
print(pattern_name, start, end, m_span.text)
...
morningGreeting 0 3 Good morning,
eveningGreeting 15 18 good evening!
这次我们做了一些不同的处理:
-
在第 8 行,我们定义了第二个模式,同样匹配三个标记,但这次是
evening而不是morning。 -
在下一行,我们将它添加到了
matcher中。此时,matcher包含了 2 个模式:morningGreeting和eveningGreeting。 -
再次,我们在我们的句子上调用
matcher并检查结果。这次结果列表有两个项目,Good morning和good evening!,对应于两个不同的模式,morningGreeting和eveningGreeting。
在前面的代码示例中,pattern1 和 pattern2 只有一个标记不同:evening/morning。我们是否可以说 evening 或 morning?我们也可以这样做。以下是 Matcher 识别的属性:

图 4.1 – Matcher 的标记属性
让我们逐个通过一些示例来回顾这些属性。在前面的例子中,我们使用了 LOWER;这意味着标记文本的小写形式。ORTH 和 TEXT 与 LOWER 类似:它们意味着标记文本的精确匹配,包括大小写。以下是一个示例:
pattern = [{"TEXT": "Bill"}]
前面的代码将匹配 BIll,但不会匹配 bill。LENGTH 用于指定标记长度。以下代码查找所有长度为 1 的标记:
doc = nlp("I bought a pineapple.")
matcher = Matcher(nlp.vocabulary)
pattern = [{"LENGTH": 1}]
matcher.add("onlyShort", [pattern])
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
...
0 1 I
2 3 a
下一个标记属性块是 IS_ALPHA, IS_ASCII, 和 IS_DIGIT。这些特性对于查找数字标记和 普通 单词(不包括任何有趣的字符)很有用。以下模式匹配两个标记的序列,一个数字后面跟一个普通单词:
doc1 = nlp("I met him at 2 o'clock.")
doc2 = nlp("He brought me 2 apples.")
pattern = [{"IS_DIGIT": True},{"IS_ALPHA": True}]
matcher.add("numberAndPlainWord", [pattern])
matcher(doc1)
[]
matches = matcher(doc2)
len(matches)
1
mid, start, end = matches[0]
print(start, end, doc2[start:end])
3, 5, 2 apples
在前面的代码段中,2 o'clock 没有匹配到模式,因为 o'clock 包含一个撇号,这不是一个字母字符(字母字符是数字、字母和下划线字符)。2 apples 匹配,因为标记 apples 由字母组成。
IS_LOWER, IS_UPPER, 和 IS_TITLE 是用于识别标记大小写的有用属性。如果标记全部为大写字母,则 IS_UPPER 为 True;如果标记以大写字母开头,则 IS_TITLE 为 True。如果标记全部为小写字母,则 IS_LOWER 为 True。想象一下,我们想要在文本中找到强调的单词;一种方法就是寻找全部为大写字母的标记。大写标记在情感分析模型中通常具有显著的重要性。
doc = nlp("Take me out of your SPAM list. We never asked you to contact me. If you write again we'll SUE!!!!")
pattern = [{"IS_UPPER": True}]
matcher.add("capitals", [pattern])
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
...
5, 6, SPAM
22, 23, SUE
IS_PUNCT, IS_SPACE, 和 IS_STOP 通常用于包含一些辅助标记的模式中,分别对应于标点符号、空格,而 IS_SENT_START 是另一个有用的属性;它匹配句子开头的标记。以下是一个以 can 开头且第二个单词首字母大写的句子模式:
doc1 = nlp("Can you swim?")
doc2 = nlp("Can Sally swim?")
pattern = [{"IS_SENT_START": True, "LOWER": "can"},
{"IS_TITLE": True}]
matcher.add("canThenCapitalized", [pattern])
matcher(doc)
[]
matches = matcher(doc2)
len(matches)
1
mid, start, end = matches[0]
print(start, end, doc2[start:end])
0, 2, Can Sally
在这里,我们做了不同的事情:我们将两个属性放入一个花括号中。在这个例子中,pattern 中的第一个项目表示一个标记是句子的第一个标记,并且其小写文本为 can。我们可以添加任意多的属性。例如,{"IS_SENT_START": False, "IS_TITLE": True, "LOWER": "bill"} 是一个完全有效的属性字典,它描述了一个首字母大写、不是句子第一个标记且文本为 bill 的标记。因此,它是那些不作为句子第一个单词出现的 Bill 实例的集合。
LIKE_NUM、LIKE_URL 和 LIKE_EMAIL 是与标记形状相关的属性;记住,我们在 第三章**,语言特征 中看到了它们。这些属性匹配看起来像数字、URL 和电子邮件的标记。
虽然前面的代码看起来简短且简单,但在 NLU 应用中,形状属性可以成为救命稻草。大多数时候,你需要的只是形状和语言属性的巧妙组合。
在看到形状属性之后,让我们看看 POS、TAG、DEP、LEMMA 和 SHAPE 语言属性。你在上一章中看到了这些标记属性;现在我们将使用它们进行标记匹配。以下代码片段查找以助动词开头的句子:
doc = nlp("Will you go there?')
pattern = [{"IS_SENT_START": True, "TAG": "MD"}]
matcher.add([pattern])
matches = matcher(doc)
len(matches)
1
mid, start, end = matches[0]
print(start, end, doc[start:end])
0, 1, Will
doc2 = nlp("I might go there.")
matcher(doc2)
[]
你可能还记得从 第三章**,语言特征 中,MD 是情态动词和助动词的标签。前面的代码片段是查找是/否疑问句的标准方法。在这种情况下,我们通常寻找以情态动词或助动词开头的句子。
小贴士
不要害怕与 TEXT/LEMMA 和 POS/TAG 一起工作。例如,当 match 是动词时,它与 to go together 是一致的,或者当它是名词时,它可以是一个 fire starter tool。在这种情况下,我们按照以下方式区分:
{"LEMMA": "match", "POS": "VERB"} 以及
{"LEMMA": "match", "POS": "NOUN".
类似地,你可以将其他语言特征与标记形状属性结合起来,以确保你只提取你想要的模式。
在接下来的章节中,我们将看到更多将语言特征与 Matcher 类结合的示例。现在,我们将探索更多匹配器功能。
扩展语法支持
匹配器允许在花括号内使用一些运算符,从而使模式更加丰富。这些运算符用于扩展比较,类似于 Python 的 in、not in 和比较运算符。以下是运算符列表:


图 4.2 – 丰富的比较运算符列表
在我们的第一个例子中,我们使用两个不同的模式匹配了 good evening 和 good morning。现在,我们可以通过使用 IN 来匹配 good morning/evening,使用一个模式如下:
doc = nlp("Good morning, I'm here. I'll say good evening!!")
pattern = [{"LOWER": "good"},
{"LOWER": {"IN": ["morning", "evening"]}},
{"IS_PUNCT": True}]
matcher.add("greetings", [pattern])
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
...
0, 3, Good morning,
10, 13, good evening!
比较运算符通常与 LENGTH 属性一起使用。以下是一个查找长标记的示例:
doc = nlp("I suffered from Trichotillomania when I was in college. The doctor prescribed me Psychosomatic medicine.")
pattern = [{"LENGTH": {">=" : 10}}]
matcher.add("longWords", [pattern])
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
...
3, 4, Trichotillomania
14, 15, Psychosomatic
这些词处理起来很有趣!现在,我们将继续探讨匹配器模式的另一个非常实用的功能,即类似正则表达式的运算符。
类似正则表达式的运算符
在本章的开头,我们指出 spaCy 的 Matcher 类提供了对正则表达式操作的更干净、更易读的等效方法,确实更加干净和易读。最常见的正则表达式操作是可选匹配(?)、至少匹配一次(+)和匹配 0 或多次(*)。spaCy 的 Matcher 也通过以下语法提供这些运算符:


图 4.3 – OP 键描述
本章的第一个正则表达式示例是匹配巴拉克·奥巴马的名字,中间名是可选的。正则表达式如下:
R"Barack\s(Hussein\s)?Obama
? 操作符在 Hussein 之后意味着括号中的模式是可选的,因此这个正则表达式匹配了 Barack Obama 和 Barack Hussein Obama。我们在匹配器模式中使用 ? 操作符如下:
doc1 = nlp("Barack Obama visited France.")
doc2 = nlp("Barack Hussein Obama visited France.")
pattern = [{"LOWER": "barack"},
{"LOWER": "hussein", "OP": "?"},
{"LOWER": "obama"}]
matcher.add("obamaNames", [pattern])
matcher(doc1)
[(1881848298847208418, 0, 2)]
matcher(doc2)
[(1881848298847208418, 0, 3)]
在这里,通过在第二个列表项中使用 "OP": "?",我们使这个标记可选。matcher 选择了第一个文档对象中的 Barack Obama 和第二个文档对象中的 Barack Hussein Obama。
我们之前指出,+ 和 * 操作符与它们的正则表达式对应物具有相同的意思。+ 表示标记应该至少出现一次,而 * 表示标记可以出现 0 次或多次。让我们看一些例子:
doc1 = nlp("Hello hello hello, how are you?")
doc2 = nlp("Hello, how are you?")
doc3 = nlp("How are you?")
pattern = [{"LOWER": {"IN": ["hello", "hi", "hallo"]},
“OP”:”*”, {"IS_PUNCT": True}]
matcher.add("greetings", [pattern])
for mid, start, end in matcher(doc1):
print(start, end, doc1[start:end])
...
2, 4, hello,
1, 4, hello hello,
0, 4, Hello hello hello,
for mid, start, end in matcher(doc2):
print(start, end, doc2[start:end])
...
0 2 Hello,
matcher(doc3)
...
[]
这是发生了什么:
-
在模式中,第一个标记表示 hello、hi、hallo 应该出现 1 次或多次,第二个标记是标点符号。
-
第三个
doc对象完全不匹配;没有问候词。 -
第二个
doc对象匹配hello,。
当我们来看第一个 doc 对象匹配的结果时,我们看到不仅有 1 个,而是有 3 个不同的匹配。这是完全正常的,因为确实有三个序列与模式匹配。如果你仔细查看匹配结果,所有这些都与我们创建的模式匹配,因为 hello、hello hello 和 hello hello hello 都与 (hello)+ 模式匹配。
让我们用 * 做相同的模式,看看这次会发生什么:
doc1 = nlp("Hello hello hello, how are you?")
doc2 = nlp("Hello, how are you?")doc3 = nlp("How are you?")
pattern = [{"LOWER": {"IN": ["hello", "hi", "hallo"]},
"OP": "+"}, {"IS_PUNCT": True}]
matcher.add("greetings", [pattern])
for mid, start, end in matcher(doc1):
print(start, end, doc1[start:end])
...
(0, 4, Hello hello hello,)
(1, 4, hello hello,)
(2, 4, hello,)
(3, 4, ,)
(7, 8, ?)
for mid, start, end in matcher(doc2):
start, end, doc2[start:end]
...
(0, 2, hello,)
(1, 2, ,)
(5, 6, ?)
for mid, start, end in matcher(doc3):
start, end, doc3[start:end]
...
(3, 4, ?)
在第一个 doc 对象的匹配中,有两个额外的项:"" 和 ?。"*" 操作符匹配 0 或更多,所以我们的 (hello)*punct_character 模式抓取了 "" 和 ?。同样适用于第二个和第三个文档:单独的标点符号而没有任何问候词被选中。这可能在你的 NLP 应用程序中不是你想要的。
上述例子是一个很好的例子,我们在创建我们的模式时应该小心;有时,我们得到不想要的匹配。因此,我们通常考虑使用 IS_SENT_START 并注意 "*" 操作符。
spaCy 匹配器类还接受一个非常特殊的模式,即通配符标记模式。通配符标记将匹配任何标记。我们通常用它来选择独立于其文本或属性或我们忽略的单词。让我们看一个例子:
doc = nlp("My name is Alice and his name was Elliot.")
pattern = [{"LOWER": "name"},{"LEMMA": "be"},{}]
matcher.add("pickName", [pattern])
for mid, start, end in matcher(doc):
print(start, end, doc[start:end])
...
1 4 name is Alice
6 9 name was Elliot
在这里,我们想要捕获句子中的名字。我们通过解析形式为 name is/was/be firstname 的标记序列来实现。第一个标记模式,LOWER: "name",匹配文本小写为 name 的标记。第二个标记模式,LEMMA: "be",匹配 is、was 和 be 标记。第三个标记是通配符标记 {},它表示 任何 标记。我们使用这个模式拾取任何在 name is/was/be 之后出现的标记。
当我们想要忽略一个标记时,我们也会使用通配符标记。让我们一起做一个例子:
doc1 = nlp("I forwarded his email to you.")
doc2 = nlp("I forwarded an email to you.")
doc3 = nlp("I forwarded the email to you.")
pattern = [{"LEMMA": "forward"}, {}, {"LOWER": "email"}]
matcher.add("forwardMail", [pattern])
for mid, start, end in matcher(doc1):
print(start, end, doc1[start:end])
...
1 4 forwarded his email
for mid, start, end in matcher(doc2):
print(start, end, doc2[start:end])
...
1 4 forwarded an email
for mid, start, end in matcher(doc3):
. print(start, end, doc3[start:end])
...
1 4 forwarded the email
这与前面的例子正好相反。在这里,我们想要提取 forward email 序列,并且我们允许一个标记在 forward 和 email 之间。在这里,语义上重要的部分是转发电子邮件的动作;它是谁的电子邮件并不那么重要。
到目前为止,我们已经在本章中多次提到了正则表达式,因此现在是时候看看 spaCy 的 Matcher 类如何使用正则表达式语法了。
正则表达式支持
当我们匹配单个标记时,通常我们想要允许一些变化,例如常见的拼写错误、UK/US 英语字符差异等等。正则表达式非常适合这项任务,而 spaCy Matcher 提供了对标记级正则表达式匹配的全面支持。让我们探索我们如何使用正则表达式为我们自己的应用服务:
doc1 = nlp("I travelled by bus.")
doc2 = nlp("She traveled by bike.")
pattern = [{"POS": "PRON"},
{"TEXT": {"REGEX": "[Tt]ravell?ed"}}]
for mid, start, end in matcher(doc1):
print(start, end, doc1[start:end])
...
0 2 I traveled
for mid, start, end in matcher(doc2):
print(start, end, doc2[start:end])
...
0 2 I travelled
在这里,我们的第二个标记模式是 [Tt]ravell?ed,这意味着标记可以是首字母大写也可以不是。此外,在第一个 l 后面有一个可选的 l。允许双元音和 ise/ize 变化是处理英国和美式英语变体的标准方式。
使用正则表达式的一种另一种方法是不仅与文本一起使用,还与 POS 标签一起使用。以下代码段做了什么?
doc = nlp("I went to Italy; he has been there too. His mother also has told me she wants to visit Rome.")
pattern = [{"TAG": {"REGEX": "^V"}}]
matcher.add("verbs", [pattern])
for mid, start, end in matcher(doc):
print(start, end, doc1[start:end])
...
1 2 went
6 7 has
7 8 been
14 15 has
15 16 told
18 19 wants
20 21 visit
我们已经提取了所有的不定式动词(你可以将不定式动词视为非情态动词)。我们是如何做到这一点的?我们的标记模式包括正则表达式 ^V,这意味着所有以 V 开头的细粒度 POS 标签:VB、VGD、VBG、VBN、VBP 和 VBZ。然后我们提取了具有动词 POS 标签的标记。
看起来很复杂!在 NLU 应用中,我们偶尔会使用一些技巧;在阅读本书的示例时,你也会学会它们。我们鼓励你回顾我们的示例,然后尝试一些你自己的示例句子。
匹配器在线演示
在整个匹配过程中,我们偶尔会看到匹配结果的可视化。正则表达式提供了 regex101 (regex101.com/),这是一个在线工具,用于检查你的正则表达式模式是否正确工作(正则表达式总是会有惊喜)。以下图显示了示例模式和对其进行的文本检查:

图 4.4 – 一个正则表达式匹配示例和模式解释
右侧的解释相当详细且具有启发性。这是一个不仅被 NLP 学习者/初学者,而且被专业人士使用的工具(正则表达式有时可能很难阅读)。
spaCy 匹配器在其在线演示页面(explosion.ai/demos/matcher)上提供了一个类似的工具。我们可以创建模式并交互式地测试它们与想要测试的文本。
在以下屏幕截图中,我们可以看到一个匹配示例。在右侧,我们可以选择属性、值和运算符(如 +、*、! 和 ?)。在做出此选择后,演示在复选框下方右侧输出相应的模式字符串。在左侧,我们首先选择我们想要的 spaCy 语言模型(在这个例子中,是英语核心小型),然后查看结果:

图 4.5 – spaCy 匹配器在线演示
就像 regex101 一样,spaCy 的 Matcher 演示可以帮助你看到为什么你的模式匹配或未匹配。
PhraseMatcher
在处理金融、医疗或法律文本时,我们经常有长长的列表和字典,并希望将文本与我们的列表进行扫描。正如我们在前节中看到的,Matcher 模式相当是手工制作的;我们单独为每个标记编写代码。如果你有一长串短语列表,Matcher 就不太方便了。不可能一个接一个地编写所有术语。
spaCy 提供了一种将文本与长字典进行比较的解决方案 – PhraseMatcher 类。PhraseMatcher 类帮助我们匹配长字典。让我们从一个例子开始:
import spacy
from spacy.matcher import PhraseMatcher
nlp = spacy.load("en_core_web_md")
matcher = PhraseMatcher(nlp.vocab)
terms = ["Angela Merkel", "Donald Trump", "Alexis Tsipras"]
patterns = [nlp.make_doc(term) for term in terms]
matcher.add("politiciansList", None, *patterns)
doc = nlp("3 EU leaders met in Berlin. German chancellor Angela Merkel first welcomed the US president Donald Trump. The following day Alexis Tsipras joined them in Brandenburg.")
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
…
9 11 Angela Merkel
16 18 Donald Trump
22 24 Alexis Tsipras
我们所做的是:
-
首先,我们导入了
spacy,然后导入了PhraseMatcher类。 -
在导入之后,我们创建了一个
Language对象,nlp,并初始化了一个PhraseMatcher对象,matcher,并为其提供了词汇表。 -
接下来的两行是我们创建模式列表的地方。
-
在第 6 行,我们对每个术语调用了
nlp.make_doc()来创建模式。 -
make_doc()从每个术语创建一个 Doc,在处理方面非常高效,因为它只调用Tokenizer而不是整个管道。 -
其余的代码与我们在 Matcher 中的操作类似:我们遍历了生成的跨度。
这样,我们通过它们的精确文本值来匹配模式。如果我们想通过其他属性来匹配它们呢?这里是一个通过 LOWER 属性匹配的例子:
matcher = PhraseMatcher(nlp.vocab, attr="LOWER")
terms = ["Asset", "Investment", "Derivatives",
"Demand", "Market"]
patterns = [nlp.make_doc(term) for term in terms]
matcher.add("financeTerms", None, *patterns)
doc = nlp("During the last decade, derivatives market became an asset class of their own and influenced the financial landscape strongly.")
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
…
5 6 derivatives
6 7 market
在第 1 行,在创建 PhraseMatcher 实例时,我们传递了一个额外的参数,attr=LOWER。这样,PhraseMatcher 在匹配时使用了 token.lower 属性。注意,术语是大写的,而匹配结果是小写的。
PhraseMatcher 的另一个可能用途是匹配 SHAPE 属性。这种匹配策略可以用于系统日志,其中 IP 地址、日期和其他数值经常出现。这里的好事是,你不需要担心数值是如何分词的,你只需将其留给 PhraseMatcher。让我们看一个例子:
matcher = PhraseMatcher(nlp.vocab, attr="SHAPE")
ip_nums = ["127.0.0.0", "127.256.0.0"]
patterns = [nlp.make_doc(ip) for ip in ip_nums]
matcher.add("IPNums", None, *pattern)
doc = nlp("This log contains the following IP addresses: 192.1.1.1 and 192.12.1.1 and 192.160.1.1 .")
for mid, start, end in matcher(doc):
print(start, end, doc[start:end])
8 9 192.1.1.1
12 13 192.160.1.1
就这样!我们成功匹配了标记和短语;剩下的是命名实体。命名实体提取是任何 NLP 系统的一个基本组成部分,你将设计的多数管道都将包括一个 命名实体识别(NER)组件。下一节将专门介绍基于规则的命名实体提取。
EntityRuler
在介绍 Matcher 时,我们看到了可以通过使用 ENT_TYPE 属性使用 Matcher 提取命名实体。我们回忆起前一章中提到的 ENT_TYPE 是一个语言属性,它指的是标记的实体类型,例如人、地点或组织。让我们看一个例子:
pattern = [{"ENT_TYPE": "PERSON"}]
matcher.add("personEnt", [pattern])
doc = nlp("Bill Gates visited Berlin.")
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
...
0 1 Bill
1 2 Gates
再次,我们创建了一个名为 matcher 的 Matcher 对象,并在 Doc 对象 doc 上调用它。结果是两个标记,Bill 和 Gates;匹配器总是在标记级别进行匹配。我们得到了 Bill 和 Gates,而不是完整的实体 Bill Gates。如果你想要得到完整的实体而不是单个标记,你可以这样做:
pattern = [{"ENT_TYPE": "PERSON", "OP": "+"}]
matcher.add("personEnt", [pattern])
doc = nlp("Bill Gates visited Berlin.")
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
...
0 1 Bill
1 2 Gates
0 2 Bill Gates
通常,我们会将两个或多个实体组合在一起,或者与其他语言属性一起提取信息。以下是一个例子,说明我们如何理解句子中的动作以及句子中哪个人物执行了这个动作:
pattern = [{"ENT_TYPE": "PERSON", "OP": "+"}, {
"POS" : "VERB"}]
matcher.add("personEntAction", [pattern])
doc = nlp("Today German chancellor Angela Merkel met with the US president.")
matches = matcher(doc)
for mid, start, end in matches:
print(start, end, doc[start:end])
...
4 6 Merkel met
3 6 Angela Merkel met
我们注意到匹配器在这里返回了两个匹配项;通常,我们会遍历结果并选择最长的匹配项。
在前面的例子中,我们匹配了 spaCy 统计模型已经提取的实体。如果我们有特定领域的实体想要匹配怎么办?例如,我们的数据集由关于古希腊哲学家的维基页面组成。哲学家的名字自然是希腊语,并不遵循英语统计模式;预计在用英语文本训练的标记器中,偶尔会失败地提取实体名称。在这些情况下,我们希望 spaCy 识别我们的实体并将它们与统计规则结合起来。
spaCy 的 EntityRuler 是一个组件,它允许我们在统计模型之上添加规则,从而创建一个更强大的 NER 模型。
EntityRuler 不是一个匹配器,它是一个可以通过 nlp.add_pipe 添加到我们管道中的管道组件。当它找到匹配项时,匹配项会被追加到 doc.ents 中,而 ent_type 将会是我们在模式中传递的标签。让我们看看它是如何工作的:
doc = nlp("I have an acccount with chime since 2017")
doc.ents
(2017,)
patterns = [{"label": "ORG",
"pattern": [{"LOWER": "chime"}]}]
ruler = nlp.add_pipe("entity_ruler")
ruler.add_patterns(patterns)
doc.ents
(chime, 2017)
doc[5].ent_type_
'ORG'
就这样,真的非常简单,但也很强大!我们只用了几行代码就添加了自己的实体。
Matcher 类和 EntityRuler 是 spaCy 库中令人兴奋且强大的功能,正如我们从示例中看到的。现在,我们将进入一个专门的部分,介绍一些快速且非常实用的技巧。
结合 spaCy 模型和匹配器
在本节中,我们将介绍一些技巧,这些技巧将指导你了解你在 NLP 职业生涯中会遇到的各种实体提取类型。所有示例都是现成的、真实世界的技巧。让我们从数字格式化的实体开始。
提取 IBAN 和账户号码
IBAN 和账户号码是金融和银行业中经常出现的两种重要实体类型。我们将学习如何解析它们。
IBAN 是一种国际银行账户号码格式。它由两位数字的国家代码后跟数字组成。以下是一些来自不同国家的 IBAN:

图 4.6 – 来自不同国家的 IBAN 格式(来源:维基百科)
我们如何为 IBAN 创建一个模式?显然,在所有情况下,我们从一个大写字母开始,后面跟着两个数字。然后可以跟任意数量的数字。我们可以将国家代码和接下来的两个数字表示如下:
{"SHAPE": "XXdd"}
在这里,XX对应于两个大写字母,dd是两个数字。然后XXdd模式完美地匹配 IBAN 的第一个块。那么其他数字块呢?对于其他块,我们需要匹配一个 1 到 4 位的数字块。正则表达式\d{1,4}表示由 1 到 4 位数字组成的标记。这个模式将匹配一个数字块:
{"TEXT": {"REGEX": "\d{1,4}"}}
我们有几个这样的块,所以匹配 IBAN 数字块的模式如下:
{"TEXT": {"REGEX": "\d{1,4}"}, "OP": "+"}
然后,我们将第一个块与其他块结合起来。让我们看看代码和匹配结果:
doc = nlp("My IBAN number is BE71 0961 2345 6769, please send the money there.")
doc1 = nlp("My IBAN number is FR76 3000 6000 0112 3456 7890 189, please send the money there.")
pattern = [{"SHAPE": "XXdd"},
{"TEXT": {"REGEX": "\d{1,4}"}, "OP":"+"}]
matcher = Matcher(nlp.vocab)
matcher.add("ibanNum", [pattern])
for mid, start, end in matcher(doc):
print(start, end, doc[start:end])
...
4 6 BE71 0961
4 7 BE71 0961 2345
4 8 BE71 0961 2345 6769
for mid, start, end in matcher(doc1):
print(start, end, doc1[start:end])
...
4 6 FR76 3000
4 7 FR76 3000 6000
4 8 FR76 3000 6000 0112
4 9 FR76 3000 6000 0112 3456
4 10 FR76 3000 6000 0112 3456 7890
4 11 FR76 3000 6000 0112 3456 7890 189
在解析数字实体时,你可以始终遵循类似的策略:首先,将实体分成一些有意义的部分/块,然后尝试确定各个块的形式或长度。
我们成功解析了 IBAN,现在我们可以解析账户号码。解析账户号码有点棘手;账户号码只是普通的数字,没有特殊的形式帮助我们区分它们和普通数字。那么我们该怎么办呢?在这种情况下,我们可以进行上下文查找;我们可以查看数字标记周围,看看我们是否可以在数字标记周围找到account number或account num。这个模式应该可以解决问题:
{"LOWER": "account"}, {"LOWER": {"IN": ["num", "number"]}},{}, {"IS_DIGIT": True}
我们在这里使用了一个通配符:{}表示任何标记。我们允许一个标记在number和account number之间;这可以是is,was等等。让我们看看代码:
doc = nlp("My account number is 8921273.")
pattern = [{"LOWER": "account"},
{"LOWER": {"IN": ["num", "number"]}},{},
{"IS_DIGIT": True}]
matcher = Matcher(nlp.vocab)
matcher.add("accountNum", [pattern])
for mid, start, end in matcher(doc):
print(start, end, doc[start:end])
...
1 5 account number is 8921273
如果你想,你可以在匹配中包含一个所有格代词,如my,your或his,具体取决于应用程序的需求。
银行号码就到这里。现在我们将提取另一种常见的数字实体,电话号码。
提取电话号码
电话号码的格式可能因国家而异,匹配电话号码通常是一项棘手的工作。这里最好的策略是明确你想要解析的国家电话号码格式。如果有几个国家,你可以在匹配器中添加相应的单个模式。如果你有太多的国家,那么你可以放宽一些条件,采用更通用的模式(我们将看到如何做到这一点)。
让我们从美国的电话号码格式开始。美国国内电话号码写作(541) 754-3010,国际电话号码写作+1 (541) 754-3010。我们可以用可选的+1来形成我们的模式,然后是一个三位数的区号,然后是两个用可选的-分隔的数字块。 以下是模式:
{"TEXT": "+1", "OP": "?"}, {"TEXT": "("}, {"SHAPE": "ddd"}, {"TEXT": ")"}, {"SHAPE": "ddd"}, {"TEXT": "-", "OP": "?"}, {"SHAPE": "dddd"}
让我们看看一个例子:
doc1 = nlp("You can call my office on +1 (221) 102-2423 or email me directly.")
doc2 = nlp("You can call me on (221) 102 2423 or text me.")
pattern = [{"TEXT": "+1", "OP": "?"}, {"TEXT": "("},
{"SHAPE": "ddd"}, {"TEXT": ")"},
{"SHAPE": "ddd"}, {"TEXT": "-", "OP": "?"},
{"SHAPE": "dddd"}]
matcher = Matcher(nlp.vocab)
matcher.add("usPhonNum", [pattern])
for mid, start, end in matcher(doc1):
print(start, end, doc1[start:end])
...
6 13 +1 (221) 102-2423
for mid, start, end in matcher(doc2):
print(start, end, doc2[start:end])
...
5 11 (221) 102-2423
我们是否可以将模式做得更通用,以便也适用于其他国家呢?在这种情况下,我们可以从一个 1 到 3 位的国家代码开始,后面跟着一些数字块。这将匹配更广泛的数字集合,因此最好小心不要匹配你文本中的其他数字实体。
我们将转到文本实体,从数字实体开始。现在我们将处理社交媒体文本,并提取社交媒体文本中可能出现的不同类型的实体。
提取提及
想象一下分析一个关于公司和产品的社交媒体帖子数据集,你的任务是找出哪些公司在以何种方式被提及。数据集将包含这种类型的句子:
CafeA is very generous with the portions.
CafeB is horrible, we waited for mins for a table.
RestaurantA is terribly expensive, stay away!
RestaurantB is pretty amazing, we recommend.
我们要找的可能是最常见的模式,即 BusinessName is/was/be adverb 形式的模式。以下模式将有效:
[{"ENT_TYPE": "ORG"}, {"LEMMA": "be"}, {"POS": "ADV", "OP": "*"}, {"POS": "ADJ"}]
在这里,我们寻找一个组织类型实体,然后是 is/was/be,然后是可选的副词,最后是一个形容词。
如果你想提取一个特定的企业,比如说公司 ACME,你只需要将第一个标记替换为特定的公司名称:
[{"LOWER": "acme"}, {"LEMMA": "be"}, {"POS": "ADV", "OP": "*"}, {"POS": "ADJ"}]
就这样,简单易行!在提取社交媒体提及之后,接下来要做的事情是提取哈希标签和表情符号。
哈希标签和表情符号提取
处理社交媒体文本是一个热门话题,并且有一些挑战。社交媒体文本包括两种不寻常的标记类型:哈希标签和表情符号。这两种标记类型都对文本意义有巨大影响。哈希标签通常指代句子的主题/宾语,而表情符号可以自己赋予句子的情感。
哈希标签由一个位于开头的 # 字符组成,然后是 ASCII 字符的单词,没有单词间的空格。一些例子包括 #MySpace、#MondayMotivation 等等。spaCy 分词器将这些单词分词为两个标记:
doc = nlp("#MySpace")
[token.text for token in doc]
['#', 'MySpace']
因此,我们的模式需要匹配两个标记,即 # 字符和其余部分。以下模式可以轻松匹配哈希标签:
{"TEXT": "#"}, {"IS_ASCII": True}
以下代码提取了一个哈希标签:
doc = nlp("Start working out now #WeekendShred")
pattern = [{"TEXT": "#"}, {"IS_ASCII": True}]
matcher = Matcher(nlp.vocab)
matcher.add("hashTag", [pattern])
matches = matcher(doc)
for mid, start, end in matches:
print(start, end doc[start:end])
...
4 6 #WeekendShred
那表情符号呢?表情符号通常根据它们的情感值编码为列表,例如积极、消极、快乐、悲伤等等。在这里,我们将表情符号分为两类,积极和消极。以下代码在文本中找到了选定的表情符号:
pos_emoji = ["", "", "", "", "", ""]
neg_emoji = ["", "", "", "", "", ""]
pos_patterns = [[{"ORTH": emoji}] for emoji in pos_emoji]
neg_patterns = [[{"ORTH": emoji}] for emoji in neg_emoji]
matcher = matcher(nlp.vocab)
matcher.add("posEmoji", pos_patterns)
matcher.add("negEmoji", neg_patterns)
doc = nlp(" I love Zara ")
for mid, start, end in matcher(doc):
print(start, end, doc[start:end])
...
3 4
哈哈,表情符号
欢快地被提取了!我们将在情感分析章节中也使用表情符号。
现在,让我们提取一些实体。我们将从扩展命名实体的常见程序开始。
扩展命名实体
经常,我们希望将命名实体的范围向左或向右扩展。想象一下,你想要提取带有头衔的 PERSON 类型命名实体,这样你可以轻松地推断性别或职业。spaCy 的 NER 类已经提取了人名,那么头衔呢?
doc = nlp("Ms. Smith left her house 2 hours ago.")
doc.ents
(Smith, 2 hours ago)
正如你所见,单词 Ms. 没有包含在命名实体中,因为它不是人名的组成部分。一个快速的解决方案是创建一个新的实体类型,称为 TITLE:
patterns = [{"label": "TITLE", "pattern": [{"LOWER": {"IN": ["ms.", "mr.", "mrs.", "prof.", "dr."]}}]}]
ruler = nlp.add_pipe("entity_ruler")
ruler.add_patterns(patterns)
nlp.add_pipe(ruler)
doc = nlp("Ms. Smith left her house")
print([(ent.text, ent.label_) for ent in doc.ents])
[('Ms.', 'TITLE'), ('SMITH', 'PERSON')]
这是一个快速且非常实用的方法。如果你处理维基文本或财经文本,你会经常遇到解析标题的情况。
在我们的下一个也是最后一个例子中,我们将结合 POS 属性、依存标签和命名实体。
结合语言特征和命名实体
在为句子赋予意义时,我们通过考虑它们出现的上下文来评估词义。单独匹配单词通常不能帮助我们理解完整的意义。在大多数自然语言理解任务中,我们必须结合语言特征。
想象一下你正在解析专业传记,并为主题制作一份工作历史。你希望提取人名、他们曾经居住的城市以及他们目前工作的城市。
显然,我们会寻找单词 live;然而,这里的关键在于 POS 标签:它是现在时还是过去时。为了确定哪个城市/地点,我们将使用由依存标签提供的句法信息。
让我们检查以下示例:
doc = nlp("Einstein lived in Zurich.")
[(ent.text, ent.label_) for ent in doc.ents]
[('Einstein', 'PERSON'), ('Zurich', 'GPE')]
这里是前述示例的视觉表示:


图 4.7 – 示例解析,句子中的实体 "Einstein" 是主题
在这里,lived 是句子的主要动词,因此是句子的根。Einstein 是句子的主语,同时也是居住的人实体。正如我们所见,Einstein 标记的头部是 lived。句子中还有一个地点实体,Zurich。如果我们跟随从 lived 出发的弧线,我们通过介词附加到达 Zurich。最后,为了确定动词的时态,我们可以检查 POS 标签。让我们在下面的代码中看看:
person_ents = [ent for ent in doc.ents if ent.label_ == "PERSON"]
for person_ent in person_entities:
#We use head of the entity's last token
head = person_ent[-1].head
If head.lemma_ == "live":
#Check if the children of live contains prepositional
attachment
preps = [token for token in head.children if token.dep_ == "prep"]
for prep in preps:
places = [token for token in prep.children if token.ent_type_ == "GPE"]
# Verb is in past or present tense
print({'person': person_ent, 'city': places,
'past': head.tag_ == "VBD"})
在这里,我们结合了 POS 标签信息、依存标签(因此句子的句法信息)和命名实体。一开始你可能觉得这不容易理解,但通过练习你会掌握的。
摘要
本章向你介绍了 spaCy 的一个非常实用且强大的功能——spaCy 的匹配器类。你学习了如何使用语言和标记级特征进行基于规则的匹配。你了解了 Matcher 类,spaCy 的基于规则的匹配器。我们通过使用不同的标记特征(如形状、词元、文本和实体类型)来探索 Matcher 类。
然后,你学习了关于 EntityRuler 的内容,这是另一个非常有用的类,你可以用它做很多事情。你学习了如何使用 EntityRuler 类提取命名实体。
最后,我们将本章所学内容与你的先前知识相结合,通过几个示例将语言特征与基于规则的匹配相结合。你学习了如何提取模式、特定格式的实体以及特定领域的实体。
通过本章,你完成了语言特征的介绍。在下一章,我们将通过一个非常重要的概念——词向量——深入统计语义的世界。你将发现统计在表示单词、短语和句子方面的力量。让我们共同探索语义的世界吧!
第五章:处理词向量和语义相似度
词向量是方便的工具,并且几乎有十年时间是自然语言处理的热门话题。词向量基本上是一个词的密集表示。这些向量令人惊讶的地方在于,语义相似的词具有相似的词向量。词向量非常适合语义相似度应用,例如计算词、短语、句子和文档之间的相似度。在词级别上,词向量提供了关于同义性、语义类比等方面的信息。我们可以通过使用词向量来构建语义相似度应用。
词向量是由利用相似词出现在相似上下文中的事实的算法产生的。为了捕捉一个词的意义,词向量算法收集了目标词出现的周围词的信息。通过周围词捕捉词的语义的这种范例被称为分布语义。
在本章中,我们将介绍分布语义范式及其相关的语义相似度方法。我们将首先从概念上了解文本向量化,以便你知道词向量解决哪些 NLP 问题。
接下来,我们将熟悉词向量计算,如距离计算、类比计算和可视化。然后,我们将学习如何从 spaCy 的预训练词向量中受益,以及如何导入和使用第三方向量。最后,我们将通过 spaCy 深入了解高级语义相似度方法。
在本章中,我们将涵盖以下主要主题:
-
理解词向量
-
使用 spaCy 的预训练向量
-
使用第三方词向量
-
高级语义相似度方法
技术要求
在本章中,我们除了 spaCy 之外还使用了某些外部 Python 库来进行代码可视化。如果您想在本章中生成词向量可视化,您将需要以下库:
-
NumPy
-
scikit-learn
-
Matplotlib
您可以在此书的 GitHub 仓库中找到本章的代码:github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter05.
理解词向量
词向量(或word2vec)的发明是自然语言处理领域最令人激动的进步之一。那些在实践自然语言处理的你们肯定在某一点上听说过词向量。本章将帮助你们理解导致词 2vec 发明的底层理念,了解词向量是什么样的,以及如何在自然语言处理应用中使用它们。
统计世界使用数字,包括统计自然语言处理算法在内的所有统计方法都使用向量。因此,在处理统计方法时,我们需要将每个现实世界的数量表示为向量,包括文本。在本节中,我们将了解我们可以用不同的方式将文本表示为向量,并发现词向量如何为单词提供语义表示。
我们将首先通过涵盖最简单的实现方式——独热编码——来发现文本向量化。
独热编码
独热编码是将向量分配给单词的一种简单直接的方法:给词汇表中的每个单词分配一个索引值,然后将此值编码到一个稀疏向量中。让我们看一个例子。在这里,我们将考虑一个披萨订购应用的词汇表;我们可以按照它们在词汇表中出现的顺序给每个单词分配一个索引:
1 a
2 e-mail
3 I
4 cheese
5 order
6 phone
7 pizza
8 salami
9 topping
10 want
现在,词汇表中单词的向量将为 0,除了单词对应索引值的位:
a 1 0 0 0 0 0 0 0 0 0
e-mail 0 1 0 0 0 0 0 0 0 0
I 0 0 1 0 0 0 0 0 0 0
cheese 0 0 0 1 0 0 0 0 0 0
order 0 0 0 0 1 0 0 0 0 0
phone 0 0 0 0 0 1 0 0 0 0
pizza 0 0 0 0 0 0 1 0 0 0
salami 0 0 0 0 0 0 0 1 0 0
topping 0 0 0 0 0 0 0 0 1 0
want 0 0 0 0 0 0 0 0 0 1
现在,我们可以将一个句子表示为一个矩阵,其中每一行对应一个单词。例如,句子I want a pizza可以表示为以下矩阵:
I 0 0 1 0 0 0 0 0 0 0
want 0 0 0 0 0 0 0 0 0 1
a 1 0 0 0 0 0 0 0 0 0
pizza 0 0 0 0 0 0 1 0 0 0
如我们从先前的词汇和索引中可以看到,向量的长度等于词汇表中的单词数量。每个维度都专门对应一个单词。当我们对文本应用独热编码向量化时,每个单词都被其向量所替代,句子被转换成一个 (N, V) 矩阵,其中 N 是句子中的单词数量,V 是词汇表的大小。
这种表示文本的方式计算简单,调试和理解也容易。到目前为止看起来不错,但这里存在一些潜在问题,例如以下内容:
-
这些向量是稀疏的。每个向量包含许多 0,但只有一个
1。显然,如果我们知道具有相似意义的单词可以分组并共享一些维度,这将是一种空间浪费。此外,通常数值算法并不喜欢高维和稀疏向量。 -
其次,如果词汇表的大小超过一百万个单词怎么办?显然,我们需要使用一百万维的向量,这在内存和计算方面实际上是不可行的。
-
另一个问题是没有向量具有任何意义。相似单词并没有以某种方式分配到相似的向量中。在先前的词汇中,单词
cheese、topping、salami和pizza实际上携带相关的意义,但它们的向量在没有任何方式上相关。这些向量确实是随机分配的,取决于词汇表中相应单词的索引。独热编码的向量根本不捕捉任何语义关系。
词向量是为了回答上述问题而发明的。
词向量
词向量是解决前面问题的解决方案。词向量是一个固定大小、密集、实值的向量。从更广泛的角度来看,词向量是文本的学习表示,其中语义相似的单词具有相似的向量。以下是一个词向量看起来像什么。这已被从Glove 英语向量中提取出来(我们将在如何生成词向量部分详细探讨 Glove):
the 0.418 0.24968 -0.41242 0.1217 0.34527 -0.044457 -0.49688 -0.17862 -0.00066023 -0.6566 0.27843 -0.14767 -0.55677 0.14658 -0.0095095 0.011658 0.10204 -0.12792 -0.8443 -0.12181 -0.016801 -0.33279 -0.1552 -0.23131 -0.19181 -1.8823 -0.76746 0.099051 -0.42125 -0.19526 4.0071 -0.18594 -0.52287 -0.31681 0.00059213 0.0074449 0.17778 -0.15897 0.012041 -0.054223 -0.29871 -0.15749 -0.34758 -0.045637 -0.44251 0.18785 0.0027849 -0.18411 -0.11514 -0.78581
这是单词the的 50 维向量。正如你所见,维度是浮点数。但这些维度代表什么呢?这些单个维度通常没有固有的含义。相反,它们代表向量空间中的位置,这些向量之间的距离表示了对应单词意义的相似性。因此,一个单词的意义分布在维度上。这种表示单词意义的方式被称为分布语义。
我们已经提到,语义相似的单词具有相似的表达。让我们看看不同单词的向量以及它们如何提供语义表示。为此,我们可以使用 TensorFlow 的词向量可视化工具,网址为projector.tensorflow.org/。在这个网站上,Google 提供了 10,000 个单词的词向量。每个向量是 200 维的,并投影到三个维度进行可视化。让我们看看我们谦逊的披萨订购词汇中单词cheese的表示:

图 5.1 – “cheese”单词及其语义相似单词的向量表示
如我们所见,单词cheese在语义上与其他关于食物的单词分组。这些是经常与单词cheese一起使用的单词:酱汁、可乐、食物等等。在下面的截图中,我们可以看到按余弦距离排序的最近单词(将余弦距离视为计算向量之间距离的一种方式):

图 5.2 – 三维空间中“cheese”的最近点
那么,关于专有名词呢?词向量是在大型语料库上训练的,例如维基百科,这就是为什么一些专有名词的表示也是学习得到的。例如,专有名词elizabeth由以下向量表示:

图 5.3 – elizabeth 的向量表示
备注
注意,前一个截图中的所有单词都是小写的。大多数词向量算法会将所有词汇输入单词转换为小写,以避免存在两个相同单词的表示。
在这里,我们可以看到 elizabeth 确实指向英格兰女王伊丽莎白。周围的词包括 monarch、empress、princess、royal、lord、lady、crown、England、Tudor、Buckingham、她母亲的名字 anne、她父亲的名字 henry,甚至她母亲竞争对手的女王名字 catherine!crown 这样的普通词和 henry 这样的专有名词都与 elizabeth 一起分组。我们还可以看到,所有邻近词的句法类别都是名词;动词不会与名词一起使用。
词向量可以捕捉同义词、反义词以及诸如动物、地点、植物、人名和抽象概念等语义类别。接下来,我们将深入探讨语义,并探索词向量提供的令人惊讶的功能——词类比。
类比和向量运算
我们已经看到,学习到的表示可以捕捉语义。更重要的是,词向量以有意义的方式支持向量运算,如向量加法和减法。实际上,添加和减去词向量是支持类比的一种方式。
词类比是一对词之间的语义关系。存在许多类型的关系,例如同义性、反义性和整体部分关系。一些例子包括(King – man, Queen – woman)、(airplane – air, ship - sea)、(fish – sea, bird - air)、(branch – tree, arm – human)、(forward – backward, absent – present)等等。
例如,我们可以将女王和国王之间的性别映射表示为 Queen – Woman + Man = King。在这里,如果我们从 Queen 中减去 woman 并加上 man,我们得到 King。然后,这个类比可以读作,queen is to king as woman is to man。嵌入可以生成诸如性别、时态和首都等显著的类比。以下图表显示了这些类比:

图 5.4 – 由词向量创建的类比(来源:https://developers.google.com/machine-learning/crash-course/embeddings/translating-to-a-lower-dimensional-space)
显然,词向量为 NLP 开发者提供了强大的语义能力,但它们是如何产生的呢?我们将在下一节中了解更多关于词向量生成算法的内容。
词向量是如何产生的
产生词向量的方法不止一种。让我们看看最流行的预训练向量以及它们的训练方式:
-
word2vec是谷歌创建的用于生成词向量的统计算法。词向量是通过神经网络架构训练的,该架构处理单词窗口并预测每个单词的向量,这取决于周围的单词。这些预训练的词向量可以从
developer.syn.co.in/tutorial/bot/oscova/pretrained-vectors.html#word2vec-and-glove-models下载。这里我们不会深入细节,但你可以阅读关于算法和数据准备步骤的出色博客jalammar.github.io/illustrated-word2vec/以获取更多信息。 -
Glove向量以另一种方式训练,是由斯坦福 NLP 小组发明的。这种方法依赖于奇异值分解,它用于单词共现矩阵。关于 Glove 算法的全面指南可在
www.youtube.com/watch?v=Fn_U2OG1uqI找到。预训练的向量可在nlp.stanford.edu/projects/glove/找到。 -
fastText是由 Facebook Research 创建的,与 word2vec 类似,但提供了更多功能。word2vec 根据周围的上下文预测单词,而 fastText 预测子词;也就是说,字符 n-gram。例如,单词椅子生成了以下子词:
ch, ha, ai, ir, cha, hai, air
fastText 为每个子词生成一个向量,包括拼写错误、数字、部分单词和单个字符。在处理拼写错误和罕见单词时,fastText 非常稳健。它可以计算非标准词典单词的向量。
Facebook Research 发布了 157 种语言的预训练 fastText 向量。你可以在fasttext.cc/docs/en/crawl-vectors.html找到这些模型。
所有的先前算法都遵循相同的思想:相似单词出现在相似语境中。语境——围绕一个单词的周围单词——在任何情况下都是生成特定单词的词向量的关键。所有使用先前三种算法生成的预训练词向量都是在像维基百科、新闻或推特这样的大型语料库上训练的。
小贴士
当我们说到相似单词时,首先想到的概念是同义性。同义词出现在相似的语境中;例如,自由和自由都意味着相同的事情:
我们希望拥有免费的健康医疗、教育和自由。
我们希望拥有免费的健康医疗、教育和自由。
那么,反义词呢?反义词可以在相同的语境中使用。以爱和恨为例:
我讨厌猫。
我喜欢猫。
如您所见,反义词也出现在类似的环境中;因此,通常它们的向量也是相似的。如果您的下游 NLP 任务在这方面很敏感,使用词向量时要小心。在这种情况下,始终要么训练自己的向量,要么通过在下游任务中训练来改进您的词向量。您可以使用 Gensim 包([radimrehurek.com/gensim/](https://radimrehurek.com/gensim/))来训练自己的词向量。Keras 库允许在下游任务上训练词向量。我们将在 第八章**,使用 spaCy 进行文本分类 中重新讨论这个问题。
现在我们对词向量有了更多的了解,让我们看看如何使用 spaCy 的预训练词向量。
使用 spaCy 的预训练向量
我们在 第一章**,spaCy 入门 中安装了一个中等大小的英语 spaCy 语言模型,这样我们就可以直接使用词向量。词向量是许多 spaCy 语言模型的一部分。例如,en_core_web_md 模型包含 20,000 个单词的 300 维向量,而 en_core_web_lg 模型包含 685,000 个单词词汇的 300 维向量。
通常,小型模型(那些以 sm 结尾的)不包含任何词向量,但包含上下文敏感的张量。您仍然可以进行以下语义相似度计算,但结果不会像词向量计算那样准确。
您可以通过 token.vector 方法访问一个单词的向量。让我们通过一个例子来看看这个方法。以下代码查询了 banana 的单词向量:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("I ate a banana.")
doc[3].vector
以下截图是在 Python 壳中拍摄的:

图 5.5 – 单词 "banana" 的词向量
token.vector 返回一个 NumPy ndarray。您可以在结果上调用 numpy 方法:
type(doc[3].vector)
<class 'numpy.ndarray'>
doc[3].vector.shape
(300,)
在此代码段中,首先,我们查询了单词向量的 Python 类型。然后,我们在向量上调用了 NumPy 数组的 shape() 方法。
Doc 和 Span 对象也有向量。句子或 span 的向量是其单词向量的平均值。运行以下代码并查看结果:
doc = nlp("I like a banana,")
doc.vector
doc[1:3].vector
只有模型词汇表中的单词才有向量;不在词汇表中的单词称为 token.is_oov 和 token.has_vector,这两个方法可以用来查询一个标记是否在模型的词汇表中并且有单词向量:
doc = nlp("You went there afskfsd.")
for token in doc:
token.is_oov, token.has_vector
(False, True)
(False, True)
(False, True)
(True, False)
(False, True)
这基本上就是我们使用 spaCy 的预训练词向量的方法。接下来,我们将探讨如何调用 spaCy 的语义相似度方法在 Doc、Span 和 Token 对象上。
相似度方法
在 spaCy 中,每个容器类型对象都有一个相似度方法,允许我们通过比较它们的词向量来计算其他容器对象的语义相似度。
我们可以计算两个容器对象之间的语义相似度,即使它们是不同类型的容器。例如,我们可以比较一个Token对象和一个Doc对象,以及一个Doc对象和一个Span对象。以下示例计算了两个Span对象之间的相似度:
doc1 = nlp("I visited England.")
doc2 = nlp("I went to London.")
doc1[1:3].similarity(doc2[1:4])
0.6539691
我们还可以比较两个Token对象,伦敦和英格兰:
doc1[2].similarity(doc2[3])
0.73891276
句子的相似度是通过在Doc对象上调用similarity()来计算的:
doc1.similarity(doc2)
0.7995623615797786
上述代码段计算了两个句子我访问了英格兰和我去了伦敦之间的语义相似度。相似度得分足够高,以至于它认为这两个句子是相似的(相似度的范围从0到1,其中0表示无关,1表示相同)。
毫不奇怪,当你比较一个对象与自身时,similarity()方法返回1:
doc1.similarity(doc1)
1.0
有时用数字判断距离是困难的,但查看纸上的向量也可以帮助我们理解我们的词汇词是如何分组的。以下代码片段可视化了一个简单的词汇,包含两个语义类别。第一个类别是动物,而第二个类别是食物。我们预计这两个类别的词将在图形上形成两个组:
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
import numpy as np
import spacy
nlp = spacy.load("en_core_web_md")
vocab = nlp("cat dog tiger elephant bird monkey lion cheetah burger pizza food cheese wine salad noodles macaroni fruit vegetable")
words = [word.text for word in vocab]
>>> vecs = np.vstack([word.vector for word in vocab if word.has_vector])
pca = PCA(n_components=2)
vecs_transformed = pca.fit_transform(vecs)
plt.figure(figsize=(20,15))
plt.scatter(vecs_transformed[:,0], vecs_transformed[:,1])
for word, coord in zip(words, vecs_transformed):
x,y = coord
plt.text(x,y,word, size=15)
plt.show()
这段代码片段实现了很多功能。让我们来看看:
-
首先,我们导入了 matplotlib 库来创建我们的图形。
-
下面的两个导入是为了计算向量。
-
我们像往常一样导入了
spacy并创建了一个nlp对象。 -
然后,我们从我们的词汇表中创建了一个
Doc对象。 -
接下来,我们通过调用
np.vstack将词向量垂直堆叠。 -
由于向量是 300 维的,我们需要将它们投影到二维空间以进行可视化。我们通过提取两个主成分来执行这种投影,这被称为主成分分析(PCA)。
-
代码的其余部分处理 matplotlib 函数调用以创建散点图。
结果的视觉如下所示:
![图 5.6 – 两个语义类别分组]
![img/B16570_5_6.jpg]
图 5.6 – 两个语义类别分组
哇!我们的 spaCy 词向量真的起作用了!在这里,我们可以看到在可视化中分组的两类语义。注意,动物之间的距离更短,分布更均匀,而食物类别在组内形成了群体。
之前,我们提到过我们可以创建自己的词向量或者在我们自己的语料库上对它们进行优化。一旦我们完成了这些,我们能否在 spaCy 中使用它们呢?答案是肯定的!在下一节中,我们将学习如何将自定义词向量加载到 spaCy 中。
使用第三方词向量
我们也可以在 spaCy 中使用第三方词向量。在本节中,我们将学习如何将第三方词向量包导入 spaCy。我们将使用来自 Facebook AI 的 fastText 的基于子词的预训练向量。您可以在 fasttext.cc/docs/en/english-vectors.html 查看所有可用的英语预训练向量的列表。
包的名字标识了向量的维度、词汇量大小以及向量将要训练的语料库类型。例如,wiki-news-300d-1M-subword.vec.zip 表示它包含在维基百科语料库上训练的 100 万 300 维度的词向量。
让我们开始下载向量:
-
在您的终端中,输入以下命令。或者,您可以将 URL 复制并粘贴到浏览器中,下载应该会开始:
$ wget https://dl.fbaipublicfiles.com/fasttext/vectors-english/wiki-news-300d-1M-subword.vec.zip前一行将下载 300 维度的词向量到您的机器上。
-
接下来,我们将解压以下
.zip文件。您可以通过右键单击或使用以下代码进行解压:wiki-news-300d-1M-subword.vec file. -
现在,我们已经准备好了使用 spaCy 的
init-model命令:wiki-news-300d-1M-subword.vec vectors into spaCy's vector format.b) Creates a language model directory named `en_subwords_wiki_lg` that contains the newly created vectors. -
如果一切顺利,您应该会看到以下消息:
Reading vectors from wiki-news-300d-1M-subword.vec Open loc 999994it [02:05, 7968.84it/s] Creating model... 0it [00:00, ?it/s] Successfully compiled vocab 999731 entries, 999994 vectors -
通过这样,我们已经创建了语言模型。现在,我们可以加载它:
import spacy nlp = spacy.load("en_subwords_wiki_lg") -
现在,我们可以使用这个
nlp对象创建一个doc对象,就像我们使用 spaCy 的默认语言模型一样:doc = nlp("I went there.")
我们刚刚创建的模型是一个使用词向量初始化的空模型,因此它不包含任何其他管道组件。例如,调用 doc.ents 将会失败并显示错误。所以,当与第三方向量一起工作时,请小心,并在可能的情况下优先使用内置的 spaCy 向量。
高级语义相似性方法
在本节中,我们将探讨用于单词、短语和句子相似性的高级语义相似性方法。我们已经学习了如何使用 spaCy 的 similarity 方法计算语义相似性并获得了一些分数。但这些分数意味着什么?它们是如何计算的?在我们查看更高级的方法之前,首先,我们将学习语义相似性是如何计算的。
理解语义相似性
当我们收集文本数据(任何类型的数据)时,我们希望看到一些例子是如何相似、不同或相关的。我们希望通过计算它们的相似度分数来衡量两段文本的相似度。在这里,术语 语义相似性 出现了;语义相似性是一个定义在文本上的 度量,其中两个文本之间的距离基于它们的语义。
数学中的度量基本上是一个距离函数。每个度量在向量空间上诱导一个拓扑。词向量是向量,因此我们想要计算它们之间的距离,并将其用作相似度分数。
现在,我们将了解两种常用的距离函数:欧几里得距离和余弦距离。让我们从欧几里得距离开始。
欧几里得距离
在 k 维空间中,两点之间的欧几里得距离是它们之间路径的长度。两点之间的距离是通过勾股定理计算的。我们通过求和每个坐标差的平方,然后取这个和的平方根来计算这个距离。以下图显示了两个向量(狗和猫)之间的欧几里得距离:

图 5.7 – 狗和猫向量之间的欧几里得距离
欧几里得距离对词向量意味着什么?首先,欧几里得距离没有向量方向的概念;重要的是向量的大小。如果我们拿一支笔从原点画一个向量到 dog 点(让我们称它为 dog vector)并同样对 cat 点(让我们称它为 cat vector)做同样的操作,然后从其中一个向量减去另一个向量,那么距离基本上就是这个差向量的幅度。
如果我们在 dog 中添加两个语义相似的词(canine,terrier)并使其成为三个词的文本会发生什么?显然,狗向量现在将增长幅度,可能是在同一方向上。这次,由于几何形状(如下所示),距离将更大,尽管第一段文本(现在是 dog canine terrier)的语义保持不变。
这就是使用欧几里得距离进行语义相似度计算的主要缺点 – 空间中两个向量的方向没有被考虑。以下图说明了 dog 和 cat 之间的距离以及 dog canine terrier 和 cat 之间的距离:

图 5.8 – “dog”和“cat”之间的距离,以及“dog canine terrier”和“cat”之间的距离
我们如何解决这个问题?有一种计算相似度的另一种方法可以解决这个问题,称为余弦相似度。让我们看看。
余弦距离和余弦相似度
与欧几里得距离相反,余弦距离更关注空间中两个向量的方向。两个向量的余弦相似度基本上是这两个向量所形成的角度的余弦。以下图显示了 dog 和 cat 向量之间的角度:

图 5.9 – 狗和猫向量之间的夹角。在这里,语义相似度是通过 cos(θ) 计算的
余弦相似度允许的最大相似度分数是 1。这是在两个向量之间的角度为 0 度时获得的(因此,向量重合)。当两个向量之间的角度为 90 度时,两个向量的相似度为 0。
余弦相似性在向量增长时提供了可扩展性。我们在这里再次引用图 5.8。如果我们增长其中一个输入向量,它们之间的角度保持不变,因此余弦相似度得分相同。
注意,在这里,我们计算的是语义相似度得分,而不是距离。当向量重合时,最高可能值为 1,而当两个向量垂直时,最低得分为 0。余弦距离是 1 – cos(θ),这是一个距离函数。
spaCy 使用余弦相似性来计算语义相似度。因此,调用similarity方法帮助我们进行余弦相似度计算。
到目前为止,我们已经学习了如何计算相似度得分,但我们还没有发现我们应该寻找意义的单词。显然,句子中的所有单词对句子的语义影响并不相同。相似度方法会为我们计算语义相似度得分,但为了使该计算的结果有用,我们需要选择正确的关键词进行比较。为了理解这一点,考虑以下文本片段:
Blue whales are the biggest mammals in the world. They're observed in California coast during spring.
如果我们感兴趣的是寻找地球上最大的哺乳动物,短语“最大的哺乳动物”和“在世界上”将是关键词。将这些短语与搜索短语“最大的哺乳动物”和“在地球上”进行比较应该会给出一个高相似度得分。但如果我们感兴趣的是了解世界上的某些地方,那么“加利福尼亚”将是关键词。“加利福尼亚”在语义上与单词“地理”相似,而且更好,实体类型是地理名词。
我们已经学习了如何计算相似度得分。在下一节中,我们将学习如何查找意义所在。我们将从句子中提取关键短语和命名实体,然后将其用于相似度得分计算。我们将首先通过一个文本分类案例研究来介绍,然后通过关键短语提取来提高任务结果。
使用语义相似性对文本进行分类
确定两个句子的语义相似度可以帮助你将文本分类到预定义的类别中,或者仅突出相关的文本。在本案例研究中,我们将过滤掉所有与单词“香水”相关的电子商务网站的用户评论。假设你需要评估以下用户评论:
I purchased a science fiction book last week.
I loved everything related to this fragrance: light, floral and feminine …
I purchased a bottle of wine.
在这里,我们可以看到只有第二句话是相关的。这是因为它包含了单词“香气”,以及描述香味的形容词。为了理解哪些句子是相关的,我们可以尝试几种比较策略。
首先,我们可以将“香水”与每个句子进行比较。回想一下,spaCy 通过平均其标记的词向量来为句子生成一个词向量。以下代码片段将前面的句子与“香水”搜索关键字进行比较:
sentences = nlp("I purchased a science fiction book last week. I loved everything related to this fragrance: light, floral and feminine... I purchased a bottle of wine. ")
key = nlp("perfume")
for sent in sentences.sents:
print(sent.similarity(key))
...
0.2481654331382154
0.5075297559861377
0.42154297167069865
在这里,我们执行了以下步骤:
-
首先,我们创建了包含前三个句子的
Doc对象。 -
然后,对于每个句子,我们计算了它与
perfume的相似度得分。 -
然后,我们通过在句子上调用
similarity()方法来打印得分。
perfume和第一句话的相似度很小,这表明这句话与我们搜索的关键词不太相关。第二句话看起来相关,这意味着我们正确地识别了语义相似度。
第三句话怎么样?脚本确定第三句话在某种程度上是相关的,最可能的原因是它包含了单词bottle,而香水通常装在瓶子中销售。单词bottle与单词perfume出现在相似的环境中。因此,这个句子和搜索关键词的相似度得分并不足够低;此外,第二句和第三句的得分也没有足够接近,以至于第二句变得重要。
与将关键词与整个句子进行比较相比,还有一个潜在问题。在实践中,我们偶尔会处理相当长的文本,例如网页文档。对非常长的文本进行平均会降低关键词的重要性。
为了提高性能,我们可以提取重要的词语。让我们看看我们如何在句子中找到关键短语。
提取关键短语
进行语义分类的更好方法是提取重要的词语/短语,并将它们与搜索关键词进行比较。我们不需要将关键词与不同的词性进行比较,而是可以将关键词仅与名词短语进行比较。名词短语是句子的主语、直接宾语和间接宾语,承担着句子大部分语义的比重。
例如,在句子Blue whales live in California.中,你可能想关注blue whales、whales、California或whales in California。
类似地,在关于香水的上一句话中,我们专注于挑选出名词,fragrance。在不同的语义任务中,你可能需要其他上下文词,如动词,来判断句子的主题,但对于语义相似度来说,名词短语占的比重最大。
那么,名词短语是什么?一个名词短语(NP)是由一个名词及其修饰语组成的一组词语。修饰语通常是代词、形容词和限定词。以下短语是名词短语:
A dog
My dog
My beautiful dog
A beautiful dog
A beautiful and happy dog
My happy and cute dog
spaCy 通过解析依存句法分析器的输出来提取名词短语。我们可以通过使用doc.noun_chunks方法来查看句子的名词短语:
doc = nlp("My beautiful and cute dog jumped over the fence")
doc.noun_chunks
<generator object at 0x7fa3c529be58>
list(doc.noun_chunks)
[My beautiful and cute dog, the fence]
让我们稍微修改一下前面的代码片段。这次,我们不是将搜索关键词perfume与整个句子进行比较,而是只将其与句子的名词短语进行比较:
for sent in sentences.sents:
nchunks = [nchunk.text for nchunk in sent.noun_chunks]
nchunk_doc = nlp(" ".join(nchunks))
print(nchunk_doc.similarity(key))
0.21390893517254456
0.6047741393523175
0.44506391511570403
在前面的代码中,我们做了以下操作:
-
首先,我们遍历了句子。
-
然后,对于每个句子,我们提取了名词短语并将它们存储在一个 Python 列表中。
-
接下来,我们将列表中的名词短语连接成一个 Python 字符串,并将其转换为
Doc对象。 -
最后,我们将名词短语的
Doc对象与搜索关键词perfume进行比较,以确定它们的语义相似度分数。
如果我们将这些分数与之前的分数进行比较,我们会看到第一句话仍然不相关,所以它的分数略有下降。第二句话的分数显著增加。现在,第二句话和第三句话的分数看起来离我们很远,以至于我们可以自信地说第二句话是最相关的句子。
提取和比较命名实体
在某些情况下,我们不会提取每个名词,而只会关注专有名词;因此,我们想要提取命名实体。假设我们想要比较以下段落:
"Google Search, often referred as Google, is the most popular search engine nowadays. It answers a huge volume of queries every day."
"Microsoft Bing is another popular search engine. Microsoft is known by its star product Microsoft Windows, a popular operating system sold over the world."
"The Dead Sea is the lowest lake in the world, located in the Jordan Valley of Israel. It is also the saltiest lake in the world."
我们的代码应该能够识别出前两段是关于大型科技公司及其产品的,而第三段是关于地理位置的。
比较这些句子中的所有名词短语可能并不很有帮助,因为其中许多,如volume,与分类不相关。这些段落的主题由其内的短语决定;也就是说,Google Search、Google、Microsoft Bing、Microsoft、Windows、Dead Sea、Jordan Valley和Israel。spaCy 可以识别这些实体:
doc1 = nlp("Google Search, often referred as Google, is the most popular search engine nowadays. It answers a huge volume of queries every day.")
doc2 = nlp("Microsoft Bing is another popular search engine. Microsoft is known by its star product Microsoft Windows, a popular operating system sold over the world.")
doc3 = nlp("The Dead Sea is the lowest lake in the world, located in the Jordan Valley of Israel. It is also the saltiest lake in the world.")
doc1.ents
(Google,)
doc2.ents
(Microsoft Bing, Microsoft, Microsoft, Windows)
doc3.ents
(The Dead Sea, the Jordan Valley, Israel)
现在我们已经提取了我们想要比较的单词,让我们计算相似度分数:
ents1 = [ent.text for ent in doc1.ents]
ents2 = [ent.text for ent in doc2.ents]
ents3 = [ent.text for ent in doc3.ents]
ents1 = nlp(" ".join(ents1))
ents2 = nlp(" ".join(ents2))
ents3 = nlp(" ".join(ents3))
ents1.similarity(ents2)
0.6078712596225045
ents1.similarity(ents3)
0.374100398233877
ents2.similarity(ents3)
0.36244710903224026
观察这些图表,我们可以看到第一段和第二段之间的相似度最高,这两段都是关于大型科技公司的。第三段与其他段落并不真正相似。我们是如何仅使用词向量就得到这个计算的?可能是因为单词Google和Microsoft经常出现在新闻和其他社交媒体文本语料库中,从而创建了相似的词向量。
恭喜!你已经到达了高级语义相似度方法部分的结尾!你探索了将词向量与诸如关键词和命名实体等语言特征结合的不同方法。通过完成这一部分,我们现在可以总结本章内容。
摘要
在本章中,你使用了词向量,这些是表示词义的字节浮点向量。首先,你学习了不同的文本向量化方法,以及如何使用词向量和分布式语义。然后,你探索了词向量允许的向量操作以及这些操作带来的语义。
你还学习了如何使用 spaCy 的内置词向量,以及如何将第三方向量导入 spaCy。最后,你学习了基于向量的语义相似度以及如何将语言概念与词向量结合以获得最佳语义效果。
下一章充满了惊喜——我们将研究一个基于实际案例的研究,这将使你能够将前五章学到的知识结合起来。让我们看看 spaCy 在处理现实世界问题时能做什么!
第六章:整合一切:使用 spaCy 进行语义解析
这是一个纯实践的部分。在本章中,我们将应用我们迄今为止所学的内容,应用到航空旅行信息系统(ATIS),一个著名的机票预订系统数据集。首先,我们将了解我们的数据集并做出基本统计。作为第一个自然语言理解(NLU)任务,我们将使用两种不同的方法提取命名实体,使用 spaCy 匹配器和通过遍历依存树。
下一个任务是确定用户话语的意图。我们也将以不同的方式探索意图识别:通过提取动词及其直接宾语,使用词表,以及通过遍历依存树来识别多个意图。然后你将匹配你的关键词到同义词列表中,以检测语义相似性。
此外,你还将使用基于词向量语义相似性的方法进行关键词匹配。最后,我们将结合所有这些信息为数据集话语生成语义表示。
到本章结束时,你将学会如何完全语义处理一个真实世界的数据集。你将学会如何提取实体、识别意图以及执行语义相似性计算。本章的工具正是你将为真实世界的自然语言处理(NLP)管道所构建的工具,包括一个 NLU 聊天机器人和一个 NLU 客户支持应用。
在本章中,我们将涵盖以下主要主题:
-
提取命名实体
-
使用依存关系进行意图识别
-
语义解析的语义相似性方法
-
整合一切
技术要求
在本章中,我们将处理一个数据集。数据集和本章的代码可以在github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter06找到。
我们除了使用 spaCy 之外,还使用了 Python 的 pandas 库来操作我们的数据集。我们还使用了 awk 命令行工具。pandas 可以通过 pip 安装,而 awk 在许多 Linux 发行版中是预安装的。
提取命名实体
在许多 NLP 应用中,包括语义解析,我们通过检查实体类型并将实体提取组件放入我们的 NLP 管道中来开始寻找文本的意义。命名实体在理解用户文本的意义中起着关键作用。
我们还将通过从我们的语料库中提取命名实体来启动一个语义解析管道。为了了解我们想要提取哪种类型的实体,首先,我们将了解 ATIS 数据集。
了解 ATIS 数据集
在本章中,我们将使用 ATIS 语料库。ATIS 是一个知名的数据集;它是意图分类的标准基准数据集之一。该数据集包括想要预订航班、获取航班信息(包括航班费用、目的地和时刻表)的客户话语。
无论 NLP 任务是什么,您都应该用肉眼检查您的语料库。我们想要了解我们的语料库,以便我们将对语料库的观察整合到我们的代码中。在查看我们的文本数据时,我们通常会关注以下方面:
-
有哪些类型的话语?是短文本语料库,还是语料库由长文档或中等长度的段落组成?
-
语料库包含哪些类型的实体?人名、城市名、国家名、组织名等等。我们想要提取哪些?
-
标点符号是如何使用的?文本是否正确标点,或者根本不使用标点?
-
语法规则是如何遵循的?大写是否正确?用户是否遵循了语法规则?是否有拼写错误?
在开始任何处理之前,我们将检查我们的语料库。让我们先下载数据集:
$ wget
https://github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter06/data/atis_intents.csv
数据集是一个两列的 CSV 文件。首先,我们将使用 pandas 对数据集统计进行一些洞察。pandas 是一个流行的数据处理库,常被数据科学家使用。您可以在 pandas.pydata.org/pandas-docs/version/0.15/tutorials.html 上了解更多信息:
-
让我们从将 CSV 文件读入 Python 开始。我们将使用 pandas 的
read_csv方法:import pandas as pd dataset = pd.read_csv("data/atis_intents.csv", header=None)数据集变量为我们保存了 CSV 对象。
-
接下来,我们将对数据集对象调用
head()。head()输出数据集的前 10 列:dataset.head()结果如下所示:
![图 6.1 – 数据集概览
![图片]()
图 6.1 – 数据集概览
如您所见,数据集对象包含行和列。它确实是一个 CSV 对象。第一列包含意图,第二列包含用户话语。
-
现在我们可以打印一些示例话语:
for text in dataset[1].head(): print(text) i want to fly from boston at 838 am and arrive in denver at 1110 in the morning what flights are available from pittsburgh to baltimore on thursday morning what is the arrival time in san francisco for the 755 am flight leaving washington cheapest airfare from tacoma to orlando round trip fares from pittsburgh to philadelphia under 1000 dollars如我们所见,第一个用户想要预订航班;他们包括了目的地、出发城市和航班时间。第三个用户询问特定航班的到达时间,第五个用户提出了一个价格限制的查询。这些话语没有大写或标点符号。这是因为这些话语是语音识别引擎的输出。
-
最后,我们可以看到按意图划分的话语数量分布:
grouped = dataset.groupby(0).size() print(grouped) atis_abbreviation 147 atis_aircraft 81 atis_aircraft#atis_flight#atis_flight_no 1 atis_airfare 423 atis_airfare#atis_flight_time 1 atis_airline 157 atis_airline#atis_flight_no 2 atis_airport 20 atis_capacity 16 atis_cheapest 1 atis_city 19 atis_distance 20 atis_flight 3666您可以在本书的 GitHub 仓库中找到数据集探索代码,链接为
github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter06/ATIS_dataset_exploration.ipynb。 -
在这一点之后,我们只处理话语文本。因此,我们可以删除第一列。为此,我们将使用 Unix 工具 awk 玩一个小技巧:
awk -F ',' '{print $2}' atis_intents.csv > atis_utterances.txt
在这里,我们打印了输入 CSV 文件的第二列(其中字段分隔符是 ,),并将输出重定向到名为 atis_utterances.txt 的文本文件。现在,我们的话语已经准备好处理,我们可以继续提取实体。
使用 Matcher 提取命名实体
如我们所见,这是一个航班数据集。因此,我们期望看到城市/国家名称、机场名称和航空公司名称:
-
这里有一些示例:
does american airlines fly from boston to san francisco what flights go from dallas to tampa show me the flights from montreal to chicago what flights do you have from ontario The users also provide the dates, times, days of the weeks they wish to fly on. These entities include numbers, month names, day of the week names as well as time adverbs such as next week, today, tomorrow, next month. Let's see some example entities: list flights from atlanta to boston leaving between 6 pm and 10 pm on august eighth i need a flight after 6 pm on wednesday from oakland to salt lake city show me flights from minneapolis to seattle on july second what flights leave after 7 pm from philadelphia to boston -
此外,
atis_abbreviation意图包含关于某些缩写的询问。航班缩写可以是票价代码(例如,M = 经济舱),航空公司名称代码(例如,联合航空公司 = UA),以及机场代码(例如,柏林机场 = BER),等等。以下是一些示例:what does the abbreviation ua mean what does restriction ap 57 mean explain restriction ap please what's fare code yn -
让我们可视化一些数据集中的话语。以下截图显示了带有其类型的实体:
![图 6.2 – 带有实体和实体类型高亮的示例语料库句子;由 displaCy 在线演示生成]()
图 6.2 – 带有实体和实体类型高亮的示例语料库句子;由 displaCy 在线演示生成
-
我们可以更系统地看到所有实体类型及其频率。以下代码段执行以下操作:
a) 它读取了我们之前在数据集探索子节中创建的话语文本文件。
b) 它遍历每个话语并创建一个 Doc 对象。
c) 它从当前的文档对象中提取实体。
d) 它使用实体的标签更新全局实体标签列表。
e) 它最终使用计数器对象计算每个标签的频率。
这里是代码:
from collections import Counter import spacy nlp = spacy.load("en_core_web_md") corpus = open("atis_utterances.txt", "r").read().split("\n") all_ent_labels = [] for sentence in corpus: doc = nlp(sentence.strip()) ents = doc.ents all_ent_labels += [ent.label_ for ent in ents] c = Counter(all_ent_labels) print(c) Counter({'GPE (location names), DATE, TIME, and ORGANIZATION. Obviously, the location entities refer to destination and source cities/countries, hence they play a very important role in the overall semantic success of our application. -
我们首先通过 spaCy Matcher 提取位置实体,通过搜索
preposition location_name形式的模式。以下代码提取了由介词引导的位置实体:import spacy from spacy.matcher import Matcher nlp = spacy.load("en_core_web_md") matcher = Matcher(nlp.vocab) pattern = [{"POS": "ADP"}, {"ENT_TYPE": "GPE"}] matcher.add("prepositionLocation", [pattern]) doc = nlp("show me flights from denver to boston on tuesday") matches = matcher(doc) for mid, start, end in matches: print(doc[start:end]) ... from denver to boston我们已经看到了如何初始化 Matcher 对象并向其中添加模式。尽管如此,我们还是回顾一下如何使用 Matcher 对象提取匹配项。以下是这段代码所做的事情:
a) 我们从导入
spacy和spacy.matcher类在第 1-2 行开始。b) 我们在第 3 行创建了一个语言管道对象,
nlp。c) 在第 4 行,我们使用语言词汇初始化了 Matcher 对象。
d) 在第 5 行,我们创建了一个匹配两个标记的模式,一个介词(
POS标签ADP表示 adposition = preposition + postposition)和一个位置实体(标签GPE表示 location entity)。e) 我们将此模式添加到 Matcher 对象中。
f) 最后,我们在一个示例语料库句子中请求匹配项并打印了匹配项。
-
虽然在这个数据集中
from和to介词占主导地位,但关于离开和到达的动词可以与各种介词一起使用。以下是数据集中的一些更多示例句子:doc = nlp("i'm looking for a flight that goes from ontario to westchester and stops in chicago") matches = matcher(doc) for mid, start, end in matches: print(doc[start:end]) ... from ontario to westchester in chicago第二个示例句子是一个疑问句:
doc = nlp("what flights arrive in chicago on sunday on continental") matches = matcher(doc) for mid, start, end in matches: print(doc[start:end]) ... in chicago数据集中另一个示例句子包含一个目的地实体中的缩写:
doc = nlp("yes i'd like a flight from long beach to st. louis by way of dallas") matches = matcher(doc) for mid, start, end in matches: print(doc[start:end]) ... from long to st of dallas我们最后的示例句子又是一个疑问句:
doc = nlp("what are the evening flights flying out of dallas") matches = matcher(doc) for mid, start, end in matches: print(doc[start:end]) ... of dallas在这里,我们可以看到一些短语动词,例如
arrive in,以及介词和动词的组合,例如stop in和fly out of。By the way of Dallas完全没有动词。用户表示他们想在达拉斯停留。to、from、in、out和of是在旅行语境中常用的介词。 -
在提取了位置信息之后,我们现在可以提取航空公司信息。
ORG实体标签表示一个组织,在我们的数据集中对应于航空公司名称。以下代码段提取了组织名称,可能是多词名称:pattern = {"ENT_TYPE": "ORG", "ORG. We wanted to capture one or more occurrences to capture the multi-word entities as well, which is why we used the OP: "+" operator. -
提取日期和时间并不非常不同;你可以用
ENT_TYPE: DATE和ENT_TYPE: TIME代码复制前面的操作。我们鼓励你自己尝试。以下截图展示了日期和时间实体在细节上的样子:![图 6.3 – 带有日期和时间实体的高亮示例数据集句子。该图像由 displaCy 在线演示生成图 6.3 – 带有日期和时间实体的高亮示例数据集句子。该图像由 displaCy 在线演示生成
-
接下来,我们将提取缩写类型实体。提取缩写实体稍微有些复杂。首先,我们将看看缩写是如何出现的:
what does restriction ap 57 mean? what does the abbreviation co mean? what does fare code qo mean what is the abbreviation d10 what does code y mean what does the fare code f and fn mean what is booking class c只有这些句子中的一个包含实体。第一个示例句子包含一个
AMOUNT实体,其值为57。除此之外,缩写根本没有任何实体类型标记。在这种情况下,我们必须向匹配器提供一些自定义规则。让我们先做一些观察,然后形成一个匹配器模式:a) 缩写可以分成两部分 – 字母和数字。
b) 字母部分可以是 1-2 个字符长。
c) 数字部分也是 1-2 个字符长。
d) 数字的存在表明了一个缩写实体。
e) 以下单词的存在表明了一个缩写实体:class、code、abbreviation。
f) 缩写的
POS标签是名词。如果候选词是 1 个或 2 个字母的词,那么我们可以查看POS标签,看它是否是名词。这种方法消除了假阳性,例如 us(代词)、me(代词)、a(限定词)和 an(限定词)。 -
现在我们将这些观察结果放入匹配器模式中:
pattern1 = [{"TEXT": {"REGEX": "\w{1,2}\d{1,2}"}}] pattern2 = [{"SHAPE": { "IN": ["x", "xx"]}}, {"SHAPE": { "IN": ["d", "dd"]}}] pattern3 = [{"TEXT": {"IN": ["class", "code", "abbrev", "abbreviation"]}}, {"SHAPE": { "IN": ["x", "xx"]}}] pattern4 = [{"POS": "NOUN", "SHAPE": { "IN": ["x", "xx"]}}]然后我们使用我们定义的模式创建一个匹配器对象:
matcher = Matcher(nlp.vocab) matcher.add("abbrevEntities", [pattern1, pattern2, pattern3, pattern4])我们现在已经准备好将我们的句子输入到匹配器中:
sentences = [ 'what does restriction ap 57 mean', 'what does the abbreviation co mean', 'what does fare code qo mean', 'what is the abbreviation d10', 'what does code y mean', 'what does the fare code f and fn mean', 'what is booking class c' ] 18\. We're ready to feed our sentences to the matcher:for sent in sentences: doc = nlp(sent) matches = matcher(doc) for mid, start, end in matches: print(doc[start:end]) ... ap 57 57 abbreviation co co code qo d10 code y code f class c c在前面的代码中,我们定义了四个模式:
a) 第一个模式匹配单个标记,该标记由 1-2 个字母和 1-2 个数字组成。例如,
d1、d10、ad1和ad21将匹配此模式。b) 第二种模式匹配两个标记的缩写,第一个标记是 1-2 个字母,第二个标记是 1-2 个数字。缩写
ap 5、ap 57、a 5和a 57将匹配此模式。c) 第三种模式也匹配两个标记。第一个标记是一个上下文线索词,例如
class或code,第二个标记应该是一个 1-2 个字母的标记。一些匹配示例是code f、code y和class c。d) 第四种模式提取了
POS标签为NOUN的 1-2 个字母的短词。前文句子中的某些匹配示例是c和co。
spaCy 匹配器通过允许我们利用标记形状、上下文线索和标记 POS 标签来简化我们的工作。在本小节中,我们通过提取地点、航空公司名称、日期、时间和缩写词,成功地进行了实体提取。在下一小节中,我们将更深入地研究句子语法,并从上下文提供很少线索的句子中提取实体。
使用依存树提取实体
在前一小节中,我们提取了上下文提供明显线索的实体。从以下句子中提取目的地城市很容易。我们可以寻找 to + GPE 模式:
I want to fly to Munich tomorrow.
但假设用户提供了以下句子之一:
I'm going to a conference in Munich. I need an air ticket.
My sister's wedding will be held in Munich. I'd like to book a flight.
在这里,介词 to 在第一句话中指的是 conference,而不是 Munich。在这句话中,我们需要一个类似于 to + .... + GPE 的模式。然后,我们必须小心哪些词可以放在 "to" 和城市名称之间,以及哪些词不应该出现。例如,这个句子有完全不同的含义,不应该匹配:
I want to book a flight to my conference without stopping at Berlin.
在第二句话中,根本就没有 to。从这些例子中我们可以看到,我们需要检查词语之间的句法关系。在 第三章**,语言特征 中,我们已经看到了如何解释依存树来理解词语之间的关系。在本小节中,我们将遍历依存树。
遍历依存树意味着以自定义的顺序访问标记,而不一定是从左到右。通常,一旦找到我们想要的东西,我们就停止遍历依存树。再次强调,依存树显示了其词语之间的句法关系。回想一下 第三章**,语言特征 中,这些关系是用有向箭头表示的,连接关系的头部和子节点。句子中的每个词都必须至少涉及一个关系。这个事实保证了我们在遍历句子时会访问每个词。
回想
在继续编写代码之前,首先让我们回顾一下关于依赖树的一些概念。ROOT 是一个特殊的依赖标签,总是分配给句子的主谓。spaCy 使用弧来显示句法关系。其中一个标记是句法父节点(称为 HEAD),另一个是依赖节点(称为 CHILD)。以一个例子来说明,在 图 6.3 中,going 有 3 个句法子节点 – I,m 和 to。同样,to 的句法头是 going(这对 I 和 m 也适用)。
回到我们的例子,我们将迭代话语依赖树,以找出介词 to 是否与地点实体 Munich 有句法关系。首先,让我们看看我们的例子句子 I'm going to a conference in Munich 的依赖分析,并记住依赖树的样子:

图 6.4 – 示例句子的依赖分析
动词 going 没有进入的弧,所以 going 是依赖树的根(当我们查看代码时,我们会看到依赖标签是 ROOT)。这是应该发生的,因为 going 是句子的主谓。如果我们跟随右边的弧,我们会遇到 to;跳过右边的弧,我们会到达 Munich。这表明 to 和 Munich 之间存在句法关系。
现在我们用代码遍历依赖树。有两种可能的方式将 to 和 Munich 连接起来:
-
从左到右。我们从 to 开始,通过访问 "to" 的句法子节点来尝试到达 Munich。这种方法可能不是一个很好的主意,因为如果 "to" 有多个子节点,那么我们需要检查每个子节点并跟踪所有可能的路径。
-
从右到左。我们从 Munich 开始,跳到其头,然后跟随头的头,依此类推。由于每个词只有一个头,所以可以保证只有一条路径。然后我们确定 to 是否在这条路径上。
以下代码段实现了第二种方法,从 Munich 开始依赖树遍历,并查找 to:
import spacy
nlp = spacy.load("en_core_web_md")
def reach_parent(source_token, dest_token):
source_token = source_token.head
while source_token != dest_token:
if source_token.head == source_token:
return None
source_token = source_token.head
return source_token
doc = nlp("I'm going to a conference in Munich.")
doc[-2]
Munich
doc[3]
to
doc[-1]
.
reach_parent(doc[-2], doc[3])
to
reach_parent(doc[-1], doc[3])
None
在 reach_parent 函数中,以下适用:
-
我们从一个源标记开始,尝试到达目标标记。
-
在
while循环中,我们从源标记开始迭代每个标记的头。 -
当我们到达源标记或句子的根时,循环停止。
-
我们通过
source_token == source_token.head这一行测试是否到达了根。因为根标记总是指代自身,其头也是自身(记住在依赖树中,根没有进入的弧)。 -
最后,我们在两个不同的测试用例上测试了我们的函数。在第一个测试用例中,源和目的地相关联,而在第二个测试用例中,没有关系,因此函数返回
None。
这种方法与前面小节的相当直接的方法非常不同。自然语言很复杂,难以处理,当你需要使用它们时,了解可用的方法和拥有必要的工具箱是很重要的。
接下来,我们将深入一个热门主题——使用句法方法进行意图识别。让我们看看设计一个好的意图识别组件的一些关键点,包括识别多个意图。
使用依存关系进行意图识别
在提取实体之后,我们想要找出用户携带的意图类型——预订航班、在他们已预订的航班上购买餐点、取消航班等等。如果你再次查看意图列表,你会看到每个意图都包括一个动词(预订)以及动词所作用的宾语(航班、酒店、餐点)。
在本节中,我们将从话语中提取及物动词及其直接宾语。我们将通过提取及物动词及其直接宾语来开始我们的意图识别部分。然后,我们将探讨如何通过识别动词和名词的同义词来理解用户的意图。最后,我们将看到如何使用语义相似度方法来确定用户的意图。在我们继续提取及物动词及其直接宾语之前,让我们首先快速回顾一下及物动词和直接/间接宾语的概念。
语言学入门
在本节中,我们将探讨与句子结构相关的某些语言学概念,包括动词和动词-宾语关系。动词是句子的一个非常重要的组成部分,因为它表明了句子中的动作。句子的宾语是受到动词动作影响的物体/人。因此,句子动词和宾语之间存在自然联系。及物性概念捕捉了动词-宾语关系。及物动词是需要一个宾语来对其施加动作的动词。让我们看看一些例子:
I bought flowers.
He loved his cat.
He borrowed my book.
在这些示例句子中,bought、loved和borrowed是及物动词。在第一个句子中,bought是及物动词,而flowers是其宾语,即句子主语“我”所购买的物体。而loved – “他的猫”和borrowed – “我的书”是及物动词-宾语例子。我们将再次关注第一个句子——如果我们擦除flowers宾语会发生什么?
I bought
买了什么?没有宾语,这个句子根本没有任何意义。在前面的句子中,每个宾语都完成了动词的意义。这是理解动词是否及物的一种方式——擦除宾语并检查句子是否在语义上保持完整。
有些动词是及物的,有些动词是不及物的。不及物动词是与及物动词相反的;它不需要宾语来对其施加动作。让我们看看一些例子:
Yesterday I slept for 8 hours.
The cat ran towards me.
When I went out, the sun was shining.
Her cat died 3 days ago.
在所有前面的句子中,动词在没有宾语的情况下是有意义的。如果我们删除除了主语和宾语之外的所有单词,这些句子仍然是有效的:
I slept.
The cat ran.
The sun was shining.
Her cat died.
将不及物动词与宾语搭配是没有意义的。你不能让某人/某物跑,你不能让某人/某物发光,你当然也不能让某人/某物死亡。
句子宾语
正如我们之前提到的,宾语是受到动词动作影响的物体/人。动词所陈述的动作是由句子主语执行的,而句子宾语受到影响。
一个句子可以是直接的或间接的。直接宾语回答问题谁? / 什么? 你可以通过问主语{动词}什么/谁?来找到直接宾语。以下是一些例子:
I bought flowers. I bought what? - flowers
He loved his cat. He loved who? - his cat
He borrowed my book. He borrowed what? - my book
间接宾语回答问题为了什么?/为了谁?/给谁?。让我们看看一些例子:
He gave me his book. He gave his book to whom? - me
He gave his book to me. He gave his book to whom? -me
间接宾语通常由介词 to、for、from 等引导。正如你从这些例子中看到的,间接宾语也是一个宾语,并且受到动词动作的影响,但它在句子中的角色略有不同。间接宾语有时被视为直接宾语的接受者。
这就是你需要了解的关于及物/不及物动词和直接/间接宾语的知识,以便消化本章的内容。如果你想了解更多关于句子语法的知识,你可以阅读 Emily Bender 的杰出著作《自然语言处理的语言学基础》Emily Bender:(dl.acm.org/doi/book/10.5555/2534456)。我们已经涵盖了句子语法的基础知识,但这仍然是一个深入了解语法的极好资源。
提取及物动词及其直接宾语
在识别意图的同时,我们通常将这些步骤应用于用户的说话:
-
将句子拆分为标记。
-
依存句法分析由 spaCy 执行。我们遍历依存树以提取我们感兴趣的标记和关系,这些标记和关系是动词和直接宾语,如下面的图所示:

图 6.5 – 来自语料库的示例句子的依存句法分析
在这个例子句子中,及物动词是找到,直接宾语是航班。关系dobj将及物动词与其直接宾语连接起来。如果我们沿着弧线追踪,从语义上看,我们可以看到用户想要执行的动作是找到,他们想要找到的物体是一个航班。我们可以将找到和航班合并成一个单词,findAflight或findFlight,这可以成为这个意图的名称。其他意图可以是bookFlight(预订航班)、cancelFlight(取消航班)、bookMeal(预订餐点)等等。
让我们以更系统的方式提取动词和直接宾语。我们首先通过在句子中寻找 dobj 标签来定位直接宾语。为了定位及物动词,我们查看直接宾语的句法主语。一个句子可以包含多个动词,因此在处理动词时要小心。以下是代码:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("find a flight from washington to sf")
for token in doc:
if token.dep_ == "dobj":
print(token.head.text + token.text.capitalize())
findFlight
在这段代码中,以下内容适用:
-
我们将管道应用于我们的样本句子。
-
接下来,我们通过寻找依赖标签为
dobj的标记来定位直接宾语。 -
当我们定位到直接宾语时,通过获取直接宾语的句法主语来定位相应的及物动词。
-
最后,我们打印出动词和宾语以生成这个意图的名称。
太好了!意图识别成功!在这里,我们识别出一个单一意图。有些语句可能包含多个意图。在下一节中,我们将学习如何根据本节的技巧识别多个意图。
提取具有联合关系的多个意图
有些语句包含多个意图。例如,考虑以下来自语料库的语句:
show all flights and fares from denver to san francisco
在这里,用户想要列出所有航班,同时还想查看票价信息。一种处理方式是将这些意图视为一个单一且复杂的意图。在这种情况下,我们可以将这个复杂意图表示为 action: show, objects: flights, fares。另一种更常见的方式是给这种语句标注多个意图。在数据集中,这个示例语句被标记为两个意图 atis_flight#atis_airfare:

图 6.6 – 示例数据集语句的依存树。conj 关系在 "flight" 和 "fares" 之间
在前面的图中,我们看到 dobj 弧将 show 和 flights 连接起来。conj 弧将 flights 和 fares 连接起来,表示它们之间的联合关系。联合关系由诸如 and 或 or 这样的连词构建,表示一个名词通过这个连词与另一个名词相连。在这种情况下,我们提取直接宾语及其并列成分。现在让我们看看我们如何将这个过程转化为代码:
import spacy
nlp = spacy.load("en_core_web_md")
doc = nlp("show all flights and fares from denver to san francisco")
for token in doc:
if token.dep_ == "dobj":
dobj = token.text
conj = [t.text for t in token.conjuncts]
verb = donj.head
print(verb, dobj, conj)
show flights ['fares']
在这里,我们遍历所有标记以定位句子的直接宾语。当我们找到直接宾语时,我们获取其并列成分。之后,找到及物动词的方式与之前的代码段相同,我们提取直接宾语的主语。在提取动词和两个宾语之后,如果我们愿意,可以将它们组合起来创建两个意图名称 – showFlights 和 showFares。
使用词表识别意图
在某些情况下,除了及物动词和直接宾语之外的其他标记可能包含用户意图的语义。在这种情况下,你需要进一步深入句法关系并更深入地探索句子结构。
以我们的数据集中的一个以下语句为例:
i want to make a reservation for a flight
在这个句子中,最能描述用户意图的动词-宾语对是想要-航班。然而,如果我们查看图 6.7中的分析树,我们会看到想要和航班在分析树中并没有直接关联。想要与及物动词制作相关,而航班与直接宾语预订分别相关:
![Figure 6.7 – 数据集中示例句子的分析树
![img/B16570_6_7.jpg]
Figure 6.7 – 数据集中示例句子的分析树
那么,我们将做什么呢?我们可以玩一个技巧,保留一个包含辅助动词的列表,例如想要、需要、制作和需要。以下是代码:
doc = nlp("i want to make a reservation for a flight")
dObj =None
tVerb = None
# Extract the direct object and its transitive verb
for token in doc:
If token.dep_ == "dobj":
dObj = token
tVerb = token.head
# Extract the helper verb
intentVerb = None
verbList = ["want", "like", "need", "order"]
if tVerb.text in verbList:
intentVerb = tVerb
else:
if tVerb.head.dep_ == "ROOT":
helperVerb = tVerb.head
# Extract the object of the intent
intentObj = None
objList = ["flight", "meal", "booking"]
if dObj.text in objList:
intentObj = dObj
else:
for child in dObj.children:
if child.dep_ == "prep":
intentObj = list(child.children)[0]
break
elif child.dep_ == "compound":
intentObj = child
break
print(intentVerb.text + intentObj.text.capitalize())
wantFlight
这是我们的逐步操作:
-
我们首先定位直接宾语及其及物动词。
-
一旦找到它们,我们就将它们与我们的预定义词汇列表进行比较。对于这个例子,我们使用了两个简化的列表,
verbList包含辅助动词列表,而objList包含我们想要提取的可能宾语词汇列表。 -
我们检查了及物动词。如果它不在辅助动词列表中,那么我们就检查句子的主要动词(由
ROOT标记),它是及物动词的头部。如果及物动词是句子的主要动词,那么这个动词的句法头部就是它自己(tVerb.head是tVerb)。因此,if tVerb.head.dep_ == "ROOT"这一行评估为True,这种实现是有效的。 -
接下来,我们检查直接宾语。如果它不在可能宾语列表中,那么我们就检查它的句法子节点。对于每个子节点,我们检查该子节点是否是直接宾语的介词。如果是,我们就选择该子节点的子节点(它只能有一个子节点)。
-
最后,我们打印出表示意图名称的字符串,它是
wantFlight。
在这一点上,深呼吸。消化和处理信息需要时间,尤其是当它涉及到句子句法时。你可以尝试从语料库中提取不同的句子,并通过检查点/在代码中添加打印语句来查看脚本的行为。
在下一节中,我们将探讨一个非常实用的工具,即使用同义词列表。让我们继续前进到下一节,学习如何充分利用语义相似性。
语义解析的语义相似性方法
自然语言使我们能够用不同的方式和不同的词汇表达相同的概念。每种语言都有同义词和语义相关的词汇。
作为一名 NLP 开发者,在为聊天机器人应用、文本分类或任何其他语义应用开发语义解析器时,你应该记住,用户为每个意图使用相当广泛的短语和表达方式。实际上,如果你正在使用 RASA([rasa.com/](https://rasa.com/))或 Dialogflow([https://dialogflow.cloud.google.com/](https://dialogflow.cloud.google.com/))等平台构建聊天机器人,你会被要求为每个意图提供尽可能多的句子示例。然后,这些句子被用来在幕后训练意图分类器。
通常有两种方法来识别语义相似性,要么使用同义词词典,要么使用基于词向量语义相似度方法。在本节中,我们将讨论这两种方法。让我们先从如何使用同义词词典来检测语义相似性开始。
使用同义词列表进行语义相似度
我们已经遍历了我们的数据集,并看到不同的动词被用来表达相同的行为。例如,“降落”、“到达”和“飞往”动词具有相同的意义,而“离开”、“出发”和“从飞往”动词形成另一个语义组。
我们已经看到,在大多数情况下,及物动词和直接宾语表达意图。判断两个句子是否代表相同意图的一个简单方法就是检查动词和直接宾语是否是同义词。
让我们举一个例子,并比较数据集中的两个示例句子。首先,我们准备一个小型的同义词词典。我们只包括动词和名词的基本形式。在进行比较时,我们也使用单词的基本形式:
verbSynsets = [
("show", "list"),
("book", "make a reservation", "buy", "reserve")
]
objSynsets = [
("meal", "food"),
("aircraft", "airplane", "plane")
]
每个同义词集(synset)包括我们领域的同义词集合。我们通常包括语言通用的同义词(飞机-飞机)和领域特定的同义词(书-购买)。
synsets 已经准备好使用,我们准备开始使用 spaCy 代码。让我们一步一步来:
-
首先,我们构建两个与我们要比较的两个句子对应的 doc 对象,
doc和doc2:doc = nlp("show me all aircrafts that cp uses") doc2 = nlp("list all meals on my flight") -
然后,我们提取第一句话的及物动词和直接宾语:
for token in doc: if token.dep_ == "dobj": obj = token.lemma_ verb = token.head.lemma_ break -
然后,我们对第二个句子做同样的处理:
for token in doc2: if token.dep_ == "dobj": obj2 = token.lemma_ verb2 = token.head.lemma_ break verb, obj ('show' , 'aircraft') verb2, obj2 ('list', 'meal') -
我们获得了第一个动词的 synset。然后,我们检查第二个动词列表是否在这个 synset 中,这返回
True:vsyn = [syn for syn in verbSynsets if verb in item] vsyn[0] ("show", "list") v2 in vsyn[0] True -
同样地,我们获得了第一个直接宾语“飞机”的 synset。然后我们检查第二个直接宾语“餐”是否在这个 synset 中,这显然是不正确的:
osyn = [syn for syn in objSynsets if obj in item] osyn[0] ("aircraft", "airplane", "plane") obj2 in vsyn[0] False -
我们推断出前两个句子不指代相同的意图。
同义词列表对于语义相似度计算非常有用,许多现实世界的 NLP 应用都受益于这样的预编译列表。但使用同义词并不总是适用。对于大型同义词集,对句子中的每个词进行字典查找可能变得低效。在下一节中,我们将介绍一种更高效的用词向量计算语义相似度的方法。
使用词向量识别语义相似度
在第五章 使用词向量和语义相似度 中,我们已经看到词向量携带语义信息,包括同义信息。在非常具体的领域工作且同义词数量较少时,同义词列表很有用。在某些时候,处理大型的同义词集可能变得低效,因为我们必须每次都进行字典查找以获取动词和直接宾语。然而,词向量为我们提供了一种非常方便且基于向量的计算语义相似度的方法。
让我们再次回顾前一小节中的代码。这次,我们将使用 spaCy 词向量来计算单词之间的语义距离。让我们一步一步来:
-
首先,我们构建两个想要比较的
doc对象:doc = nlp("show me all aircrafts that cp uses") doc2 = nlp("list all meals on my flight") -
然后我们提取第一句话的动词和宾语:
for token in doc: if token.dep_ == "dobj": obj = token verb = token.head break -
我们对第二句话重复相同的步骤:
for token in doc2: if token.dep_ == "dobj": obj2 = token verb2 = token.head break verb, obj ('show' , 'aircraft') verb2, obj2 ('list', 'meal') -
现在,我们使用基于词向量的 spaCy 相似度方法计算两个直接宾语之间的语义相似度:
obj.similarity(obj2) 0.15025872 # A very low score, we can deduce these 2 utterances are not related at this point. -
最后,我们计算动词之间的相似度:
verb.similarity(verb2) 0.33161193之前的代码与之前的代码不同。这次,我们直接使用了标记对象;不需要进行词形还原。然后我们调用了 spaCy 的
token.similarity(token2)方法来计算直接宾语之间的语义距离。得到的分数非常低。在这个时候,我们推断这两个语句并不代表相同的意图。
这是一个计算语义相似度的简单且高效的方法。我们在第一章中提到,spaCy 为 NLP 开发者提供了易于使用且高效的工具,现在我们可以看到原因了。
将所有这些放在一起
我们已经以几种方式提取了实体并识别了意图。我们现在准备好将所有这些放在一起来计算用户语句的语义表示!
-
我们将处理示例数据集的语句:
show me flights from denver to philadelphia on tuesday我们将使用一个字典对象来存储结果。结果将包括实体和意图。
-
让我们提取实体:
import spacy from spacy.matcher import Matcher nlp = spacy.load("en_core_web_md") matcher = Matcher(nlp.vocab) pattern = [{"POS": "ADP"}, {"ENT_TYPE": "GPE"}] matcher.add("prepositionLocation", [pattern]) # Location entities doc = nlp("show me flights from denver to philadelphia on tuesday") matches = matcher(doc) for mid, start, end in matches: print(doc[start:end]) ... from denver to philadelphia # All entities: ents = doc.ents (denver, philedelphia, tuesday) -
基于这些信息,我们可以生成以下语义表示:
{ 'utterance': 'show me flights from denver to philadelphia on tuesday', 'entities': { 'date': 'tuesday', 'locations': { 'from': 'denver', 'to': 'philadelphia' } } } -
接下来,我们将执行意图识别以生成完整的语义解析:
import spacy nlp = spacy.load("en_core_web_md") doc = nlp("show me flights from denver to philadelphia on tuesday") for token in doc: if token.dep_ == "dobj": print(token.head.lemma_ + token.lemma_.capitalize()) showFlight -
在确定意图后,这个语句的语义解析现在看起来是这样的:
{ 'utterance': 'show me flights from denver to philadelphia on tuesday', 'intent ': ' showFlight', 'entities': { 'date': 'tuesday', 'locations': { 'from': 'denver', 'to': 'philadelphia' } } }
最终结果是,提取了这一话语、意图和实体的完整语义表示。这是一个可机器读取和使用的输出。我们将此结果传递给系统组件,该组件调用了 NLP 应用程序以生成响应动作。
摘要
恭喜!你已经到达了这一非常紧张章节的结尾!
在本章中,你学习了如何生成一个完整的语义解析。首先,你对你自己的数据集进行了发现,以获得关于数据集分析的认识。然后,你学习了使用两种不同的技术提取实体——使用 spaCy Matcher 和通过遍历依存树。接下来,你通过分析句子结构学习了执行意图识别的不同方法。最后,你将所有信息汇总起来以生成语义解析。
在接下来的章节中,我们将转向更多机器学习方法。下一节将关注如何在你自己的数据上训练 spaCy NLP 管道组件。让我们继续前进,并为我们自己定制 spaCy!
第三部分:使用 spaCy 进行机器学习
本节讨论了如何使用 spaCy 构建高级 NLP 模型需要知识、分析和实践。你将在定制自己的统计模型、尝试全新的变压器以及设计自己的 NLP 管道的同时,对不同的 NLP 机器学习任务进行实验。你将从一个 NLP 大师那里学到一些技巧和窍门。
本节包括以下章节:
-
第七章,定制 spaCy 模型
-
第八章,使用 spaCy 进行文本分类
-
第九章,spaCy 和变压器
-
第十章,整合一切 – 使用 spaCy 设计你的聊天机器人
第七章:自定义 spaCy 模型
在本章中,你将学习如何训练、存储和使用自定义统计管道组件。首先,我们将讨论何时应该进行自定义模型训练。然后,你将学习模型训练的一个基本步骤——如何收集和标注自己的数据。
在本章中,你还将学习如何充分利用Prodigy,这个注释工具。接下来,你将学习如何使用自己的数据更新现有的统计管道组件。我们将使用我们自己的标记数据更新 spaCy 管道的命名实体识别器(NER)组件。
最后,你将学习如何使用自己的数据和标签从头开始创建统计管道组件。为此,我们将再次训练一个 NER 模型。本章将带你完成一个完整的机器学习实践,包括收集数据、标注数据和为信息提取训练模型。
到本章结束时,你将准备好在自己的数据上训练 spaCy 模型。你将具备收集数据、将数据预处理成 spaCy 可以识别的格式,以及最终使用这些数据训练 spaCy 模型的全套技能。在本章中,我们将涵盖以下主要主题:
-
开始数据准备
-
标注和准备数据
-
更新现有的管道组件
-
从头开始训练管道组件
技术要求
本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter07。
开始数据准备
在前面的章节中,我们看到了如何在我们的应用程序中充分利用 spaCy 的预训练统计模型(包括POS 标记器、NER 和依存句法分析器)。在本章中,我们将看到如何为我们的自定义领域和数据自定义统计模型。
spaCy 模型在通用 NLP 任务中非常成功,例如理解句子的语法、将段落分割成句子以及提取一些实体。然而,有时我们处理的是 spaCy 模型在训练期间没有见过的非常具体的领域。
例如,Twitter 文本包含许多非正规词汇,如标签、表情符号和提及。此外,Twitter 句子通常只是短语,而不是完整的句子。在这里,spaCy 的 POS 标记器由于是在完整的、语法正确的英语句子上训练的,因此表现不佳,这是完全合理的。
另一个例子是医学领域。医学领域包含许多实体,如药物、疾病和化学化合物名称。这些实体不应该被 spaCy 的 NER 模型识别,因为它没有疾病或药物实体标签。NER 对医学领域一无所知。
训练定制模型需要时间和精力。在开始训练过程之前,你应该决定是否真的需要进行训练。为了确定你是否真的需要定制训练,你需要问自己以下问题:
-
spaCy 模型在你的数据上表现足够好吗?
-
你的领域是否包含许多在 spaCy 模型中缺失的标签?
-
在 GitHub 或其他地方已经有了预训练的模型/应用程序吗?(我们不希望重新发明轮子。)
让我们在接下来的章节中详细讨论这些问题。
spaCy 模型在你的数据上表现足够好吗?
如果模型表现足够好(准确率高于 0.75),那么你可以通过另一个 spaCy 组件来定制模型输出。例如,假设我们在导航领域工作,并且我们有以下这样的语句:
navigate to my home
navigate to Oxford Street
让我们看看 spaCy 的 NER 模型为这些句子输出了哪些实体:
import spacy
nlp = spacy.load("en_core_web_md")
doc1 = nlp("navigate to my home")
doc1.ents
()
doc2 = nlp("navigate to Oxford Street")
doc2.ents
(Oxford Street,)
doc2.ents[0].label_
'FAC'
spacy.explain("FAC")
'Buildings, airports, highways, bridges, etc.'
在这里,home根本不被识别为实体,但我们希望它被识别为地点实体。此外,spaCy 的 NER 模型将Oxford Street标记为FAC,这意味着建筑/公路/机场/桥梁类型的实体,这并不是我们想要的。
我们希望这个实体被识别为GPE,即一个地点。在这里,我们可以进一步训练 NER 来识别街道名称为GPE,以及识别一些地点词,如work、home和my mama's house为GPE。
另一个例子是报纸领域。在这个领域,提取了人、地点、日期、时间和组织实体,但你还需要一个额外的实体类型——vehicle(汽车、公共汽车、飞机等等)。因此,而不是从头开始训练,你可以使用 spaCy 的EntityRuler(在第四章,基于规则的匹配*)添加一个新的实体类型。始终首先检查你的数据,并计算 spaCy 模型的成功率。如果成功率令人满意,那么使用其他 spaCy 组件进行定制。
你的领域是否包含许多在 spaCy 模型中缺失的标签?
例如,在先前的报纸示例中,只有vehicle这个实体标签在 spaCy 的 NER 模型标签中缺失。其他实体类型都被识别了。在这种情况下,你不需要定制训练。
再次考虑医疗领域。实体包括疾病、症状、药物、剂量、化学化合物名称等等。这是一份专业且长的实体列表。显然,对于医疗领域,你需要定制模型训练。
如果我们需要定制模型训练,我们通常遵循以下步骤:
-
收集你的数据。
-
标注你的数据。
-
决定更新现有模型或从头开始训练模型。
在数据收集步骤中,我们决定收集多少数据:1,000 个句子、5,000 个句子,或者更多。数据量取决于你的任务和领域的复杂性。通常,我们从可接受的数据量开始,进行第一次模型训练,看看它的表现;然后我们可以添加更多数据并重新训练模型。
在收集您的数据集后,您需要以某种方式标注您的数据,以便 spaCy 的训练代码能够识别它。在下一节中,我们将了解训练数据格式以及如何使用 spaCy 的 Prodigy 工具标注数据。
第三个要点是决定从头开始训练一个空白模型,还是对现有模型进行更新。在这里,经验法则是这样的:如果您的实体/标签存在于现有模型中,但您没有看到非常好的性能,那么使用您自己的数据更新模型,例如在先前的导航示例中。如果您的实体根本不在当前的 spaCy 模型中,那么您可能需要进行定制训练。
小贴士
不要急于训练您自己的模型。首先,检查您是否真的需要定制模型。始终记住,从头开始训练一个模型需要数据准备、模型训练和保存,这意味着您将花费时间、金钱和精力。好的工程实践是明智地使用您的资源。
我们将从构建模型的第一步开始:准备我们的训练数据。让我们继续到下一节,看看如何准备和标注我们的训练数据。
标注和准备数据
训练模型的第一步始终是准备训练数据。您通常从客户日志中收集数据,然后通过将数据作为 CSV 文件或 JSON 文件导出,将它们转换为数据集。spaCy 模型训练代码与 JSON 文件一起工作,因此在本章中我们将使用 JSON 文件。
在收集我们的数据后,我们标注我们的数据。标注意味着标记意图、实体、词性标签等。
这是一个标注数据的示例:
{
"sentence": "I visited JFK Airport."
"entities": {
"label": "LOC"
"value": "JFK Airport"
}
正如您所看到的,我们将统计算法指向我们希望模型学习的内容。在这个例子中,我们希望模型学习关于实体的知识,因此我们提供带有标注实体的示例。
手动编写 JSON 文件可能会出错且耗时。因此,在本节中,我们还将了解 spaCy 的标注工具 Prodigy,以及一个开源数据标注工具Brat。Prodigy 不是开源的或免费的,但我们将介绍它是如何工作的,以便您更好地了解标注工具的一般工作原理。Brat 是开源的,并且可以立即供您使用。
使用 Prodigy 标注数据
Prodigy 是数据标注的现代工具。我们将使用 Prodigy 网络演示(prodi.gy/demo)来展示标注工具的工作原理。
让我们开始吧:
-
我们导航到 Prodigy 网络演示页面,查看一个由 Prodigy 提供的示例文本,以便进行标注,如下截图所示:![图 7.1 – Prodigy 界面;照片来自他们的网络演示页面
![img/B16570_7_1.jpg]
图 7.1 – Prodigy 界面;照片来自他们的网络演示页面
上述截图显示了我们要标注的示例文本。截图底部的按钮展示了接受此训练示例、拒绝此示例或忽略此示例的方法。如果示例与我们的领域/任务无关(但以某种方式涉及数据集),则忽略此示例。如果文本相关且标注良好,则接受此示例,并将其加入我们的数据集。
-
接下来,我们将对实体进行标注。标注实体很简单。首先,我们从上面的工具栏中选择一个实体类型(在这里,这个语料库包括两种类型的实体,
PERSON和ORG。您想标注哪些实体取决于您;这些是您提供给工具的标签。)然后,我们只需用光标选择我们想要标注为实体的单词,如下面的截图所示:

图 7.2 – 在网络演示中注释 PERSON 实体
在我们完成文本注释后,我们点击接受按钮。一旦会话结束,您可以将标注数据导出为 JSON 文件。当您完成注释工作后,您可以点击保存按钮来正确结束会话。点击保存会自动将标注数据导出为 JSON 文件。就是这样。Prodigy 提供了一种非常高效的数据标注方法。
使用 Brat 进行数据标注
另一个注释工具是Brat,这是一个免费且基于网络的文本注释工具(brat.nlplab.org/introduction.html)。在 Brat 中,可以注释关系以及实体。您还可以将 Brat 下载到您的本地机器上,用于注释任务。基本上,您将数据集上传到 Brat,并在界面上注释文本。以下截图显示了 CoNLL 数据集示例中的一个已注释句子:

图 7.3 – 一个示例已注释句子
您可以在 Brat 演示网站上的示例数据集上进行操作(brat.nlplab.org/examples.html),或者通过上传您自己的数据的小子集开始。在注释会话完成后,Brat 会自动导出注释数据的 JSON 文件。
spaCy 训练数据格式
如我们之前所述,spaCy 训练代码与 JSON 文件格式一起工作。让我们看看训练数据格式的详细情况。
对于命名实体识别(NER),您需要提供句子及其注释的列表。每个注释应包括实体类型、实体在字符中的起始位置以及实体在字符中的结束位置。让我们看看数据集的一个示例:
training_data = [
("I will visit you in Munich.", {"entities": [(20, 26, "GPE")]}),
("I'm going to Victoria's house.", {
"entities": [
(13, 23, "PERSON"),
(24, 29, "GPE")
]})
("I go there.", {"entities": []})
]
此数据集包含三个示例对。每个示例对包括一个句子作为第一个元素。对中的第二个元素是一个标注实体的列表。在第一个示例句子中,只有一个实体,即Munich。该实体的标签是GPE,在句子中的起始位置是第 20 个字符,结束位置是第 25 个字符。同样,第二个句子包含两个实体;一个是PERSON,Victoria's,另一个实体是GPE,house。第三个句子不包含任何实体,因此列表为空。
我们不能直接将原始文本和标注输入到 spaCy 中。相反,我们需要为每个训练示例创建一个Example对象。让我们看看代码:
import spacy
from spacy.training import Example
nlp = spacy.load("en_core_web_md")
doc = nlp("I will visit you in Munich.")
annotations = {"entities": [(20, 26, "GPE")]}
example_sent = Example.from_dict(doc, annotations)
在此代码段中,首先,我们从示例句子创建了一个 doc 对象。然后,我们将 doc 对象及其以字典形式提供的标注输入到创建Example对象中。我们将在下一节的训练代码中使用Example对象。
为训练依存句法分析器创建示例句子略有不同,我们将在从头开始训练管道组件部分进行介绍。
现在,我们已经准备好训练我们自己的 spaCy 模型了。我们将首先了解如何更新 NLP 管道的统计模型。为此,我们将借助自己的示例进一步训练 NER 组件。
更新现有的管道组件
在本节中,我们将使用自己的示例进一步训练 spaCy 的 NER 组件,以识别导航领域。我们之前已经看到了一些导航领域语句的示例以及 spaCy 的 NER 模型如何标注某些示例语句中的实体:
navigate/0 to/0 my/0 home/0
navigate/0 to/0 Oxford/FAC Street/FAC
显然,我们希望 NER 表现更好,能够识别诸如街道名称、区域名称以及其他诸如家、工作和办公室等地点名称的实体。现在,我们将我们的示例输入到 NER 组件中,并进行更多训练。我们将分三步进行 NER 的训练:
-
首先,我们将禁用所有其他统计管道组件,包括词性标注器和依存句法分析器。
-
我们将我们的领域示例输入到训练过程中。
-
我们将评估新的 NER 模型。
此外,我们还将学习如何进行以下操作:
-
将更新的 NER 模型保存到磁盘。
-
当我们想要使用更新的 NER 模型时,请读取它。
让我们开始,深入了解 NER 模型的训练过程。正如我们在前面的列表中所指出的,我们将分几个步骤训练 NER 模型。我们将从第一步开始,即禁用 spaCy NLP 管道中的其他统计模型。
禁用其他统计模型
在开始训练过程之前,我们禁用了其他管道组件,因此我们只训练目标组件。以下代码段禁用了除了 NER 之外的所有管道组件。我们在开始训练过程之前调用此代码块:
other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'ner']
nlp.disable_pipes(*other_pipes)
另一种编写此代码的方式如下:
other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'ner']
with nlp.disable_pipes(*other_pipes):
# training code goes here
在前面的代码块中,我们利用了nlp.disable_pipes返回一个上下文管理器的事实。使用with语句确保我们的代码释放分配的资源(例如文件句柄、数据库锁或多个线程)。如果您不熟悉这些语句,您可以在 Python 教程中了解更多:book.pythontips.com/en/latest/context_managers.html。
我们已经完成了训练代码的第一步。现在,我们准备进行模型训练过程。
模型训练过程
如我们在第三章中提到的,在介绍命名实体识别部分中的语言特征,spaCy 的 NER 模型是一个神经网络模型。要训练一个神经网络,我们需要配置一些参数并提供训练示例。神经网络每次预测都是其权重值的总和;因此,训练过程通过我们的示例调整神经网络的权重。如果您想了解更多关于神经网络如何工作,您可以阅读优秀的指南neuralnetworksanddeeplearning.com/。
在训练过程中,我们将对训练集进行多次遍历,并对每个示例进行多次展示(一次迭代称为一个epoch),因为只展示一次示例是不够的。在每次迭代中,我们会对训练数据进行打乱,以确保训练数据的顺序不重要。这种训练数据的打乱有助于彻底训练神经网络。
在每个 epoch 中,训练代码使用一个小数值更新神经网络的权重。优化器是更新神经网络权重以符合损失函数的函数。在 epoch 结束时,通过比较实际标签与神经网络当前输出计算一个损失值。然后,优化器函数可以基于这个损失值更新神经网络的权重。
在下面的代码中,我们使用了随机梯度下降(SGD)算法作为优化器。SGD 本身也是一个迭代算法。它的目的是最小化一个函数(对于神经网络,我们希望最小化损失函数)。SGD 从损失函数上的一个随机点开始,以步长沿着斜坡向下移动,直到达到该函数的最低点。如果您想了解更多关于 SGD 的信息,您可以访问斯坦福大学优秀的神经网络课程deeplearning.stanford.edu/tutorial/supervised/OptimizationStochasticGradientDescent/。
将所有这些放在一起,以下是用于训练 spaCy 的 NER 模型导航域的代码。让我们一步一步来:
-
在前三行中,我们进行必要的导入。
random是 Python 库,包括用于几个分布(包括均匀分布、伽马分布和贝塔分布)的伪随机生成器的方法。在我们的代码中,我们将使用random.shuffle来打乱我们的数据集。shuffle将序列就地打乱:import random import spacy from spacy.training import Example -
接下来,我们将创建一个语言管道对象,
nlp:nlp = spacy.load("en_core_web_md") -
然后,我们将定义我们的导航领域训练集句子。每个示例都包含一个句子及其注释:
trainset = [ ("navigate home", {"entities": [(9,13, "GPE")]}), ("navigate to office", {"entities": [(12,18, "GPE")]}), ("navigate", {"entities": []}), ("navigate to Oxford Street", {"entities": [(12, 25, "GPE")]}) ] -
我们希望迭代我们的数据 20 次,因此 epochs 的数量是
20:epochs = 20 -
在接下来的两行中,我们禁用了其他管道组件,只留下 NER 进行训练。我们使用
with statement来调用nlp.disable_pipe作为上下文管理器:other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'ner'] with nlp.disable_pipes(*other_pipes): -
我们创建了一个
optimizer对象,正如我们之前讨论的那样。我们将这个optimizer对象作为参数传递给训练方法:optimizer = nlp.create_optimizer() -
然后,对于每个 epoch,我们将使用
random.shuffle来打乱我们的数据集:for i in range(epochs): random.shuffle(trainset) -
对于数据集中的每个示例句子,我们将从句子及其注释创建一个
Example对象:example = Example.from_dict(doc, annotation) -
我们将
Example对象和optimizer对象传递给nlp.update。实际的训练方法是nlp.update。这是 NER 模型接受训练的地方:nlp.update([example], sgd=optimizer) -
一旦完成 epochs,我们将新训练的 NER 组件保存到名为
navi_ner的目录下:ner = nlp.get_pipe("ner") ner.to_disk("navi_ner")'
nlp.update每次调用都会输出一个损失值。调用此代码后,你应该会看到类似于以下截图的输出(损失值可能不同):

图 7.4 – NER 训练的输出
就这样!我们已经为导航领域训练了 NER 组件!让我们尝试一些示例句子,看看它是否真的起作用了。
评估更新的 NER
现在我们可以测试我们全新的更新后的 NER 组件。我们可以尝试一些带有同义词和释义的示例来测试神经网络是否真的学会了导航领域,而不是仅仅记住我们的示例。让我们看看结果如何:
-
这些是训练句子:
navigate home navigate to office navigate navigate to Oxford Street -
让我们用同义词
house来替换home,并在to my中添加两个更多的词:doc= nlp("navigate to my house") doc.ents (house,) doc.ents[0].label_ 'GPE' -
它起作用了!
House被识别为GPE类型的实体。我们是否可以用一个类似的动词drive me来替换navigate,并创建第一个示例句子的释义:doc= nlp("drive me to home") doc.ents (home,) doc.ents[0].label_ 'GPE' -
现在,我们尝试一个稍微不同的句子。在下一个句子中,我们不会使用同义词或释义。我们将用地区名称“苏豪”来替换“牛津街”。让我们看看这次会发生什么:
doc= nlp("navigate to Soho") doc.ents (Soho,) doc.ents[0].label_ 'GPE' -
正如我们之前提到的,我们更新了统计模型,因此,NER 模型并没有忘记它已经知道的实体。让我们用另一个实体类型进行测试,看看 NER 模型是否真的没有忘记其他实体类型:
doc = nlp("I watched a documentary about Lady Diana.") doc.ents (Lady Diana,) doc.ents[0].label_ 'PERSON'
太棒了!spaCy 的神经网络不仅能识别同义词,还能识别同一类型的实体。这就是我们为什么使用 spaCy 进行 NLP 的原因之一。统计模型非常强大。
在下一节中,我们将学习如何保存我们训练的模型并将模型加载到我们的 Python 脚本中。
保存和加载自定义模型
在前面的代码段中,我们已经看到了如何如下序列化更新的 NER 组件:
ner = nlp.get_pipe("ner")
ner.to_disk("navi_ner")
我们将模型序列化,这样我们就可以在我们想要的时候将它们上传到其他 Python 脚本中。当我们想要上传一个定制的 spaCy 组件时,我们执行以下步骤:
import spacy
nlp = spacy.load('en', disable=['ner'])
ner = nlp.create_pipe("ner")
ner.from_disk("navi_ner")
nlp.add_pipe(ner, "navi_ner")
print(nlp.meta['pipeline'])
['tagger', 'parser', 'navi_ner']
这里是我们遵循的步骤:
-
我们首先加载没有命名实体识别(NER)的管道组件,因为我们想添加我们自己的 NER。这样,我们确保默认的 NER 不会覆盖我们的自定义 NER 组件。
-
接下来,我们创建一个 NER 管道组件对象。然后我们从我们序列化的目录中加载我们的自定义 NER 组件到这个新创建的组件对象中。
-
然后我们将我们的自定义 NER 组件添加到管道中。
-
我们打印管道的元数据,以确保加载我们的自定义组件成功。
现在,我们也学会了如何序列化和加载自定义组件。因此,我们可以继续前进到一个更大的任务:从头开始训练 spaCy 统计模型。我们将再次训练 NER 组件,但这次我们将从头开始。
从头开始训练管道组件
在上一节中,我们看到了如何根据我们的数据更新现有的 NER 组件。在本节中,我们将为医学领域创建一个全新的 NER 组件。
让我们从一个小数据集开始,以了解训练过程。然后我们将实验一个真实的医疗 NLP 数据集。以下句子属于医学领域,包括药物和疾病名称等医疗实体:
Methylphenidate/DRUG is effectively used in treating children with epilepsy/DISEASE and ADHD/DISEASE.
Patients were followed up for 6 months.
Antichlamydial/DRUG antibiotics/DRUG may be useful for curing coronary-artery/DISEASE disease/DISEASE.
以下代码块展示了如何从头开始训练 NER 组件。正如我们之前提到的,最好创建我们自己的 NER,而不是更新 spaCy 的默认 NER 模型,因为医疗实体根本不被 spaCy 的 NER 组件识别。让我们看看代码,并将其与上一节的代码进行比较。我们将一步一步来:
-
在前三行中,我们进行了必要的导入。我们导入了
spacy和spacy.training.Example。我们还导入了random来打乱我们的数据集:import random import spacy from spacy.training import Example -
我们定义了我们的三个示例的训练集。对于每个示例,我们包括一个句子及其注释的实体:
train_set = [ ("Methylphenidate is effectively used in treating children with epilepsy and ADHD.", {"entities": [(0, 15, "DRUG"), (62, 70, "DISEASE"), (75, 79, "DISEASE")]}), ("Patients were followed up for 6 months.", {"entities": []}), ("Antichlamydial antibiotics may be useful for curing coronary-artery disease.", {"entities": [(0, 26, "DRUG"), (52, 75, "DIS")]}) ] -
我们还列出了我们想要识别的实体集合——
DIS代表疾病名称,DRUG代表药物名称:entities = ["DIS", "DRUG"] -
我们创建了一个空白模型。这与我们在上一节中所做的不同。在上一节中,我们使用了 spaCy 的预训练英语语言管道:
nlp = spacy.blank("en") -
我们还创建了一个空的 NER 组件。这与上一节的代码不同。在上一节中,我们使用了预训练的 NER 组件:
ner = nlp.add_pipe("ner") ner <spacy.pipeline.ner.EntityRecognizer object at 0x7f54b50044c0> -
接下来,我们通过使用
ner.add_label将每个医疗标签添加到空 NER 组件中:for ent in entities: ner.add_label(ent) -
我们将 epoch 的数量定义为
25:epochs = 25 -
接下来的两行禁用了除了 NER 之外的其他组件:
other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'ner'] with nlp.disable_pipes(*other_pipes): -
我们通过调用
nlp.begin_training创建了一个优化器对象。这与上一节不同。在上一节中,我们通过调用nlp.create_optimizer创建了一个优化器对象,这样 NER 就不会忘记它已经知道的标签。在这里,nlp.begin_training使用0初始化 NER 模型的权重,因此 NER 模型忘记了之前学到的所有内容。这正是我们想要的;我们想要一个从零开始训练的空白 NER 模型:optimizer = nlp.begin_training() -
对于每个时代,我们都会对我们的小型训练集进行洗牌,并使用我们的示例训练 NER 组件:
for i in range(25): random.shuffle(train_set) for text, annotation in train_set: doc = nlp.make_doc(text) example = Example.from_dict(doc, annotation) nlp.update([example], sgd=optimizer)
这是此代码段输出的内容(损失值可能不同):

图 7.5 – 训练过程中的损失值
它真的起作用了吗?让我们测试一下新训练的 NER 组件:
doc = nlp("I had a coronary disease.")
doc.ents
(coronary disease,)
doc.ents[0].label_
'DIS'
太好了 – 它起作用了!让我们也测试一些负面示例,即 spaCy 预训练的 NER 模型可以识别但我们的模型不能识别的实体:
doc = nlp("I met you at Trump Tower.")
doc.ents
()
doc = nlp("I meet you at SF.")
doc.ents
()
这看起来也不错。我们全新的 NER 只识别医疗实体。让我们可视化我们的第一个示例句子,看看 displaCy 如何展示新的实体:
from spacy import displacy
doc = nlp("I had a coronary disease.")
displacy.serve(doc, style="ent")
此代码块生成了以下可视化:

图 7.6 – 示例句子的可视化
我们在小型数据集上成功训练了 NER 模型。现在是时候使用真实世界的数据集了。在下一节中,我们将深入研究处理一个关于热门话题的非常有趣的语料库;挖掘冠状病毒医疗文本。
使用真实世界的数据集
在本节中,我们将在一个真实世界的语料库上进行训练。我们将在由 艾伦人工智能研究所 提供的 CORD-19 语料库上训练一个 NER 模型(allenai.org/)。这是一个开放挑战,让文本挖掘者从该数据集中提取信息,以帮助全球的医疗专业人员对抗冠状病毒病。CORD-19 是一个开源数据集,收集了超过 50 万篇关于冠状病毒疾病的学术论文。训练集包括 20 个标注的医疗文本样本:
-
让我们从查看一个示例训练文本开始:
The antiviral drugs amantadine and rimantadine inhibit a viral ion channel (M2 protein), thus inhibiting replication of the influenza A virus.[86] These drugs are sometimes effective against influenza A if given early in the infection but are ineffective against influenza B viruses, which lack the M2 drug target.[160] Measured resistance to amantadine and rimantadine in American isolates of H3N2 has increased to 91% in 2005.[161] This high level of resistance may be due to the easy availability of amantadines as part of over-the-counter cold remedies in countries such as China and Russia,[162] and their use to prevent outbreaks of influenza in farmed poultry.[163][164] The CDC recommended against using M2 inhibitors during the 2005–06 influenza season due to high levels of drug resistance.[165]从这个例子中我们可以看到,真实世界的医疗文本可以相当长,它可以包含许多医疗术语和实体。名词、动词和实体都与医学领域相关。实体可以是数字(
91%)、数字和单位(100 ng/ml,25 microg/ml)、数字字母组合(H3N2)、缩写(CDC),以及复合词(qRT-PCR,PE-labeled)。医疗实体以多种形状(数字、数字和字母组合、复合词)出现,以及非常特定于领域。因此,医疗文本与日常口语/书面语言非常不同,并且肯定需要定制训练。
-
实体标签也可以是复合词。以下是该语料库包含的实体类型列表:
Pathogen MedicalCondition Medicine我们将数据集转换成可以用于 spaCy 训练的形式。数据集可在本书的 GitHub 仓库中找到:
github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter07/data。 -
让我们继续下载数据集。在您的终端中输入以下命令:
$wget https://github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter07/data/corona.json这将把数据集下载到您的机器上。如果您愿意,您也可以从 GitHub 手动下载数据集。
-
现在,我们将对数据集进行一些预处理,以恢复在将数据集作为
json格式导出时发生的格式变化:import json with open("data/corona.json") as f: data = json.loads(f.read()) TRAIN_DATA = [] for (text, annotation) in data: new_anno = [] for anno in annotation["entities"]: st, end, label = anno new_anno.append((st, end, label)) TRAIN_DATA.append((text, {"entities": new_anno}))此代码段将读取数据集的
JSON文件,并按照 spaCy 训练数据规范对其进行格式化。 -
接下来,我们将进行统计模型训练:
a) 首先,我们将进行相关导入:
import random import spacy from spacy.training import Exampleb) 其次,我们将初始化一个空的 spaCy 英语模型,并向此空白模型添加一个 NER 组件:
nlp = spacy.blank("en") ner = nlp.add_pipe("ner") print(ner) print(nlp.meta)c) 接下来,我们定义 NER 组件要识别的标签,并将这些标签介绍给它:
labels = ['Pathogen', 'MedicalCondition', 'Medicine'] for ent in labels: ner.add_label(ent) print(ner.labels)d) 最后,我们准备好定义训练循环:
epochs = 100 other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'ner'] with nlp.disable_pipes(*other_pipes): optimizer = nlp.begin_training() for i in range(100): random.shuffle(TRAIN_DATA) for text, annotation in TRAIN_DATA: doc = nlp.make_doc(text) example = Example.from_dict(doc, annotation) nlp.update([example], sgd=optimizer)此代码块与上一节的训练代码相同,只是
epochs变量的值不同。这次,我们迭代了100个周期,因为实体类型、实体值和训练样本文本在语义上更为复杂。如果您有时间,我们建议您至少进行 500 次迭代。对于这个数据集,100 次迭代的数据就足以获得良好的结果,但 500 次迭代将进一步提升性能。 -
让我们可视化一些示例文本,看看我们新训练的医疗 NER 模型如何处理医疗实体。我们将使用
displaCy代码可视化我们的医疗实体:from spacy import displacy doc = nlp("One of the bacterial diseases with the highest disease burden is tuberculosis, caused by Mycobacterium tuberculosis bacteria, which kills about 2 million people a year.") displacy.serve(doc, style="ent")以下截图突出了两个实体 -
结核病和导致该病的细菌作为病原体实体:![图 7.7 – 突出显示示例医疗文本的实体]()
图 7.7 – 突出显示示例医疗文本的实体
-
这次,让我们看看关于致病细菌的文本中的实体。这个示例文本包含许多实体,包括几个疾病和病原体名称。所有疾病名称,如
肺炎、破伤风和麻风,都被我们的医疗 NER 模型正确提取。以下displaCy代码突出了这些实体:doc2 = nlp("Pathogenic bacteria contribute to other globally important diseases, such as pneumonia, which can be caused by bacteria such as Streptococcus and Pseudomonas, and foodborne illnesses, which can be caused by bacteria such as Shigella, Campylobacter, and Salmonella. Pathogenic bacteria also cause infections such as tetanus, typhoid fever, diphtheria, syphilis, and leprosy. Pathogenic bacteria are also the cause of high infant mortality rates in developing countries.") displacy.serve(doc2, style="ent")这是前面代码块生成的可视化:

图 7.8 – 突出显示疾病和病原体实体的示例文本
看起来不错!我们已经成功训练了 spaCy 的医学领域 NER 模型,现在 NER 可以从医疗文本中提取信息。这标志着本节的结束。我们学习了如何训练统计管道组件以及准备训练数据和测试结果。这些是在掌握 spaCy 和机器学习算法设计方面的重要步骤。
摘要
在本章中,我们探讨了如何根据我们自己的领域和数据定制 spaCy 统计模型。首先,我们学习了决定我们是否真的需要定制模型训练的关键点。然后,我们经历了一个统计算法设计的重要部分——数据收集和标注。
在这里,我们还了解了两种标注工具——Prodigy 和 Brat。接下来,我们通过更新 spaCy 的命名实体识别(NER)组件,使用我们的导航领域数据样本开始了模型训练。我们学习了必要的模型训练步骤,包括禁用其他管道组件,创建示例对象以保存我们的示例,并将我们的示例输入到训练代码中。
最后,我们学习了如何在小型玩具数据集和真实医疗领域数据集上从头开始训练 NER 模型。
通过本章,我们迈入了统计自然语言处理(NLP)的游乐场。在下一章,我们将进一步探索统计建模,并学习使用 spaCy 进行文本分类。让我们继续前进,看看 spaCy 会带给我们什么!
第八章:使用 spaCy 进行文本分类
本章致力于一个非常基础且流行的自然语言处理任务:文本分类。您将首先学习如何训练 spaCy 的文本分类组件 TextCategorizer。为此,您将学习如何准备数据并将数据输入到分类器中;然后我们将继续训练分类器。您还将在一个流行的情感分析数据集上练习您的 TextCategorizer 技能。
接下来,您还将使用流行的框架 TensorFlow 的 Keras API 与 spaCy 一起进行文本分类。您将学习神经网络的基础知识、使用 LSTM 对序列数据进行建模,以及如何使用 Keras 的文本预处理模块准备文本以进行机器学习任务。您还将学习如何使用 tf.keras 设计神经网络。
在此之后,我们将进行一个端到端的文本分类实验,从数据准备到使用 Keras Tokenizer 预处理文本,再到设计神经网络、模型训练以及解释分类结果。这是一整套机器学习的内容!
在本章中,我们将涵盖以下主要主题:
-
理解文本分类的基础知识
-
训练 spaCy 文本分类器
-
使用 spaCy 进行情感分析
-
使用 spaCy 和 Keras 进行文本分类
技术要求
在 训练 spaCy 文本分类器 和 使用 spaCy 进行情感分析 这两部分的代码与 spaCy v3.0 兼容。
在 使用 spaCy 和 Keras 进行文本分类 这一部分中,需要以下 Python 库:
-
TensorFlow >=2.2.0
-
NumPy
-
pandas
-
Matplotlib
您可以使用以下命令使用 pip 安装这些库的最新版本:
pip install tensorflow
pip install numpy
pip install pandas
pip install matplotlib
我们还在最后两节中使用了 Jupyter 笔记本。您可以根据 Jupyter 网站的说明(jupyter.org/install)将 Jupyter 笔记本安装到您的系统上。如果您不想使用笔记本,也可以将代码复制粘贴为 Python 代码。
您可以在本书的 GitHub 仓库中找到本章的代码和数据文件,网址为 github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter08。
让我们先从 spaCy 的文本分类组件开始,然后我们将过渡到设计我们自己的神经网络。
理解文本分类的基础知识
文本分类是将一组预定义的标签分配给文本的任务。给定一组预定义的类别和一些文本,您想要了解该文本属于哪个预定义类别。在开始分类任务之前,我们必须根据数据的性质自行确定类别。例如,客户评论可以是积极的、消极的或中性的。
文本分类器用于检测邮箱中的垃圾邮件、确定客户评论的情感、理解客户的意图、对客户投诉工单进行分类等等。
文本分类是 NLP 的基本任务。它在商业世界中越来越重要,因为它使企业能够自动化其流程。一个直接的例子是垃圾邮件过滤器。每天,用户都会收到许多垃圾邮件,但大多数时候用户从未看到这些邮件,也没有收到任何通知,因为垃圾邮件过滤器帮助用户免受无关邮件的打扰,并节省了删除这些邮件的时间。
文本分类器可以有不同的类型。一些分类器关注文本的整体情感,一些分类器关注检测文本的语言,还有一些分类器只关注文本中的某些单词,例如动词。以下是一些最常见的文本分类类型及其用例:
-
主题检测:主题检测是理解给定文本主题的任务。例如,客户电子邮件中的文本可能是询问退款、请求过去的账单,或者仅仅是抱怨客户服务。
-
情感分析:情感分析是理解文本是否包含关于给定主题的积极或消极情绪的任务。情感分析常用于分析产品和服务客户评价。
-
语言检测:语言检测是许多 NLP 系统(如机器翻译)的第一步。
下图展示了一个客户服务自动化系统的文本分类器:

图 8.1 – 主题检测用于使用预定义标签标记客户投诉
在技术细节方面,文本分类是一个监督学习任务。这意味着分类器可以根据示例输入文本-类别标签对来预测文本的类别标签。因此,为了训练文本分类器,我们需要一个标记数据集。标记数据集基本上是一系列文本-标签对。以下是一个包含五个训练句子及其标签的示例数据集:
This shampoo is great for hair.
POSITIVE
I loved this shampoo, best product ever!
POSITIVE
My hair has never been better, great product. POSITIVE
This product make my scalp itchy.
NEGATIVE
Not the best quality for this price.
NEGATIVE
然后,我们通过向分类器展示文本和相应的类别标签来训练分类器。当分类器看到训练文本中没有的新文本时,它就会根据训练阶段看到的例子预测这个未见文本的类别标签。文本分类器的输出总是一个类别标签。
根据使用的类别数量,文本分类也可以分为三个类别:
-
二元文本分类意味着我们希望将文本分类为两个类别。
-
多类文本分类意味着存在超过两个类别。每个类别是互斥的——一个文本只能属于一个类别。等价地,一个训练实例只能被标记为一个类别标签。例如,对客户评价进行评级。评价可以有 1,2,3,4 或 5 颗星(每个星级类别是一个类别)。
-
多标签文本分类是多类分类的推广,其中可以为每个示例文本分配多个标签。例如,使用多个标签对有毒社交媒体消息进行分类。这样,我们的模型可以区分不同级别的毒性。类标签通常是毒性、严重毒性、侮辱、威胁、淫秽。一条消息可以包含侮辱和威胁,或者被归类为侮辱、毒性和淫秽等。因此,对于这个问题,使用多个类别更合适。
标签是我们希望作为输出看到的类别的名称。一个类标签可以是分类的(字符串)或数字的(一个数字)。以下是一些常用的类标签:
-
对于情感分析,我们通常使用正负类标签。它们的缩写,pos 和 neg,也常被使用。二元类标签也很受欢迎 - 0 表示负面情感,1 表示正面情感。
-
这同样适用于二元分类问题。我们通常用 0-1 作为类标签。
-
对于多类和多标签问题,我们通常用有意义的名称命名类别。对于一个电影类型分类器,我们可以使用家庭、国际、周日晚上、迪士尼、动作等标签。数字也用作标签。对于一个五类分类问题,我们可以使用标签 1、2、3、4 和 5。
现在我们已经涵盖了文本分类的基本概念,让我们来做一些编码!在下一节中,我们将探讨如何训练 spaCy 的文本分类器组件。
训练 spaCy 文本分类器
在本节中,我们将了解 spaCy 文本分类器组件 TextCategorizer 的详细信息。在 第二章,spaCy 的核心操作 中,我们了解到 spaCy NLP 管道由组件组成。在 第三章,语言特征 中,我们学习了 spaCy NLP 管道的核心组件,包括句子分词器、词性标注器、依存句法分析器和 命名实体识别(NER)。
TextCategorizer 是一个可选的可训练管道组件。为了训练它,我们需要提供示例及其类别标签。我们首先将 TextCategorizer 添加到 NLP 管道中,然后进行训练过程。图 8.2 展示了 TextCategorizer 组件在 NLP 管道中的确切位置;该组件位于基本组件之后。在以下图中,TextCategorizer 组件。
![Figure 8.2 – TextCategorizer in the nlp pipeline
![img/B16570_8_2.jpg]
Figure 8.2 – TextCategorizer in the nlp pipeline
spaCy 的TextCategorizer背后是一个神经网络架构。TextCategorizer为我们提供了用户友好的端到端方法来训练分类器,因此我们不必直接处理神经网络架构。在接下来的使用 spaCy 和 Keras 进行文本分类部分,我们将设计自己的神经网络架构。在查看架构之后,我们将深入到TextCategorizer代码中。首先让我们了解TextCategorizer类。
了解 TextCategorizer 类
现在让我们详细了解TextCategorizer类。首先,我们从管道组件中导入TextCategorizer:
from spacy.pipeline.textcat import DEFAULT_SINGLE_TEXTCAT_MODEL
TextCategorizer有两种形式,单标签分类器和多标签分类器。正如我们在上一节中提到的,多标签分类器可以预测多个类别。单标签分类器对每个示例只预测一个类别,且类别是互斥的。前面的import行导入单标签分类器,接下来的代码导入多标签分类器:
from spacy.pipeline.textcat_multilabel import DEFAULT_MULTI_TEXTCAT_MODEL
接下来,我们需要为TextCategorizer组件提供一个配置。我们在这里提供两个参数,一个阈值值和一个模型名称(根据分类任务,可以是Single或Multi)。TextCategorizer内部为每个类别生成一个概率,如果一个类别的概率高于阈值值,则将该类别分配给文本。
文本分类的传统阈值是0.5,然而,如果您想做出更有信心的预测,可以将阈值提高,例如 0.6、0.7 或 0.8。
将所有内容整合起来,我们可以将单标签TextCategorizer组件添加到nlp管道中,如下所示:
from spacy.pipeline.textcat import DEFAULT_SINGLE_TEXTCAT_MODEL
config = {
"threshold": 0.5,
"model": DEFAULT_SINGLE_TEXTCAT_MODEL
}
textcat = nlp.add_pipe("textcat", config=config)
textcat
<spacy.pipeline.textcat.TextCategorizer object at 0x7f0adf004e08>
将多标签组件添加到nlp管道中类似:
from spacy.pipeline.textcat_multilabel import
DEFAULT_MULTI_TEXTCAT_MODEL
config = {
"threshold": 0.5,
"model": DEFAULT_MULTI_TEXTCAT_MODEL
}
textcat = nlp.add_pipe("textcat_multilabel", config=config)
textcat
<spacy.pipeline.textcat.TextCategorizer object at 0x7f0adf004e08>
在前面每个代码块的最后一行,我们向nlp管道对象添加了一个TextCategorizer管道组件。新创建的TextCategorizer组件被textcat变量捕获。我们现在可以开始训练TextCategorizer组件了。训练代码看起来与第七章**,自定义 spaCy 模型中的 NER 组件训练代码非常相似,除了一些细节上的不同。
格式化 TextCategorizer 的训练数据
让我们从准备一个小型训练集开始我们的代码。我们将准备一个客户情感数据集用于二元文本分类。标签将称为sentiment,可以获取两个可能的值,0 和 1 分别对应负面和正面情感。以下训练集包含 6 个示例,其中 3 个是正面的,3 个是负面的:
train_data = [
("I loved this product, very easy to use.", {"cats": {"sentiment": 1}}),
("I'll definitely purchase again. I recommend this product.", {"cats": {"sentiment": 1}}),
("This is the best product ever. I loved the scent and the feel. Will buy again.", {"cats": {"sentiment": 1}}),
("Disappointed. This product didn't work for me at all", {"cats": {"sentiment": 0}}),
("I hated the scent. Won't buy again", {"cats": {"sentiment": 0}}),
("Truly horrible product. Very few amount of product for a high price. Don't recommend.", {"cats": {"sentiment": 0}})
]
每个训练示例都是一个文本和嵌套字典的元组。字典包含 spaCy 能识别的类标签格式。cts字段表示类别。然后我们包括类标签的情感及其值。该值始终应该是浮点数。
在代码中,我们将我们选择的类别标签引入到TextCategorizer组件中。让我们看看完整的代码。首先,我们进行必要的导入:
import random
import spacy
from spacy.training import Example
from spacy.pipeline.textcat import DEFAULT_SINGLE_TEXTCAT_MODEL
我们导入了内置库random来打乱我们的数据集。我们像往常一样导入了spacy,并导入了Example来准备 spaCy 格式的训练示例。在代码块的最后一行,我们导入了文本分类器模型。
接下来,我们将进行管道和TextCategorizer组件的初始化:
nlp = spacy.load("en_core_web_md")
config = {
"threshold": 0.5,
"model": DEFAULT_SINGLE_TEXTCAT_MODEL
}
textcat = nlp.add_pipe("textcat", config=config)
现在,我们将对新建的TextCategorizer组件textcat做一些工作。我们将通过调用add_label将我们的标签sentiment引入到TextCategorizer组件中。然后,我们需要用我们的示例初始化这个组件。这一步与我们在第七章**自定义 spaCy 模型中 NER 训练代码所做的不一样。
原因是命名实体识别(NER)是一个基本组件,因此管道总是初始化它。TextCategorizer是一个可选组件,它作为一个空白统计模型提供。以下代码将我们的标签添加到TextCategorizer组件中,然后使用训练示例初始化TextCategorizer模型的权重:
textcat.add_label("sentiment")
train_examples = [Example.from_dict(nlp.make_doc(text), label) for text,label in train_data]
textcat.initialize(lambda: train_examples, nlp=nlp)
注意,我们将示例作为Example对象传递给textcat.initialize。回想一下第七章,自定义 spaCy 模型,spaCy 的训练方法始终与Example对象一起工作。
定义训练循环
我们准备定义训练循环。首先,我们将禁用其他管道组件,以便只训练textcat。其次,我们将通过调用resume_training创建一个优化器对象,保留现有统计模型的权重。对于每个 epoch,我们将逐个遍历训练示例并更新textcat的权重。我们遍历数据 20 个 epochs。以下代码定义了训练循环:
epochs=20
with nlp.select_pipes(enable="textcat"):
optimizer = nlp.resume_training()
for i in range(epochs):
random.shuffle(train_data)
for text, label in train_data:
doc = nlp.make_doc(text)
example = Example.from_dict(doc, label)
nlp.update([example], sgd=optimizer)
那就是全部了!用这段相对简短的代码片段,我们训练了一个文本分类器!以下是我机器上的输出(你的损失值可能不同):

图 8.3 – 每个 epoch 的损失值
测试新组件
让我们测试新的文本分类器组件。doc.cats属性持有类别标签:
doc2 = nlp("This product sucks")
doc2.cats
{'sentiment': 0.09907063841819763}
doc3 = nlp("This product is great")
doc3.cats
{'sentiment': 0.9740120000120339}
太棒了!我们的小数据集成功训练了 spaCy 文本分类器,用于二元文本分类问题,确实是一个情感分析任务。现在,我们将看看如何使用 spaCy 的TextCategorizer进行多标签分类。
为多标签分类训练 TextCategorizer
从第一部分回忆,多标签分类意味着分类器可以为示例文本预测多个标签。自然地,这些类别根本不是互斥的。为了训练一个多标签分类器,我们需要提供一个包含具有多个标签的示例的数据库。
要为多标签分类训练 spaCy 的 TextCategorizer,我们再次从构建一个小型训练集开始。这次,我们将形成一组电影评论,标签为 FAMILY、THRILLER 和 SUNDAY_EVENING。以下是我们的小型数据集:
train_data = [
("It's the perfect movie for a Sunday evening.", {"cats": {"SUNDAY_EVENING": True}}),
("Very good thriller", {"cats": {"THRILLER": True}}),
("A great movie for the kids and all the family" , {"cats": {"FAMILY": True}}),
("An ideal movie for Sunday night with all the family. My kids loved the movie.", {"cats": {"FAMILY": True, "SUNDAY_EVENING":True}}),
("A perfect thriller for all the family. No violence, no drugs, pure action.", {"cats": {"FAMILY": True, "THRILLER": True}})
]
我们提供了一些只有一个标签的示例,例如第一个示例(train_data 的第一句话,上一代码块的第二行),我们还提供了具有多个标签的示例,例如 train_data 的第四个示例。
我们将在形成训练集之后进行导入:
import random
import spacy
from spacy.training import Example
from spacy.pipeline.textcat_multilabel import
DEFAULT_MULTI_TEXTCAT_MODEL
这里,最后一行与上一节中的代码不同。我们导入了多标签模型而不是单标签模型。
接下来,我们将多标签分类器组件添加到 nlp 管道中。再次注意管道组件的名称——这次是 textcat_multilabel,与上一节的 textcat 相比:
config = {
"threshold": 0.5,
"model": DEFAULT_MULTI_TEXTCAT_MODEL
}
textcat = nlp.add_pipe("textcat_multilabel", config=config)
将标签添加到 TextCategorizer 组件并初始化模型与 训练 spaCy 文本分类器 部分类似。这次,我们将添加三个标签而不是一个:
labels = ["FAMILY", "THRILLER", "SUNDAY_EVENING"]
for label in labels:
textcat.add_label(label)
train_examples = [Example.from_dict(nlp.make_doc(text), label) for text,label in train_data]
textcat.initialize(lambda: train_examples, nlp=nlp)
我们已经准备好定义训练循环。代码函数与上一节的代码类似。唯一的区别是第一行中的组件名称。现在它是 textcat_multilabel:
epochs=20
with nlp.select_pipes(enable="textcat_multilabel"):
optimizer = nlp.resume_training()
for i in range(epochs):
random.shuffle(train_data)
for text, label in train_data:
doc = nlp.make_doc(text)
example = Example.from_dict(doc, label)
nlp.update([example], sgd=optimizer)
输出应该类似于上一节的输出,每个 epoch 的损失值。现在,让我们测试我们全新的多标签分类器:
doc2 = nlp("Definitely in my Sunday movie night list")
doc2.cats
{'FAMILY': 0.9044250249862671, 'THRILLER': 0.34271398186683655, 'SUNDAY_EVENING': 0.9801468253135681}
注意到每个标签在输出中都存在一个正概率。而且,这些概率的总和并不等于 1,因为它们不是互斥的。在这个例子中,SUNDAY_EVENING 和 THRILLER 标签的概率预测是正确的,但 FAMILY 标签的概率看起来并不理想。这主要是因为我们没有提供足够的例子。通常,对于多标签分类问题,分类器需要比二分类更多的例子,因为分类器需要学习更多的标签。
我们已经学习了如何训练 spaCy 的 TextCategorizer 组件进行二进制文本分类和多标签文本分类。现在,我们将在一个真实世界的数据集上训练 TextCategorizer 以进行情感分析问题。
使用 spaCy 进行情感分析
在本节中,我们将处理一个真实世界的数据集,并在该数据集上训练 spaCy 的 TextCategorizer。在本章中,我们将使用 Kaggle 上的 Amazon Fine Food Reviews 数据集(www.kaggle.com/snap/amazon-fine-food-reviews)。原始数据集非常大,有 10 万行。我们采样了 4,000 行。这个数据集包含了关于在亚马逊上销售的精致食品的客户评论。评论包括用户和产品信息、用户评分和文本。
您可以从本书的 GitHub 仓库下载数据集。在您的终端中输入以下命令:
wget https://github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter08/data/Reviews.zip
或者,您可以点击前面的命令中的 URL,下载将开始。您可以使用以下方法解压 zip 文件:
unzip Reviews.zip
或者,您可以在 ZIP 文件上右键单击并选择Extract here来解压 ZIP 文件。
探索数据集
现在,我们已经准备好探索数据集了。在本节中,我们将使用 Jupyter 笔记本。如果您已经安装了 Jupyter,您可以直接执行笔记本单元格。如果您系统上没有 Jupyter Notebook,您可以按照 Jupyter 网站上的说明进行操作(jupyter.org/install)。
让我们一步一步地进行数据集探索:
-
首先,我们将进行读取和可视化数据集的导入:
import pandas as pd import matplotlib.pyplot as plt %matplotlib inline -
我们将读取 CSV 文件到 pandas DataFrame 中,并输出 DataFrame 的形状:
reviews_df=pd.read_csv('data/Reviews.csv') reviews_df.shape (3999, 10) -
接下来,我们通过打印前 10 行来检查数据集的行和列:
reviews_df.head()生成的视图告诉我们有 10 行,包括评论文本和评论评分:
![图 8.4 – 评论数据框的前 10 行]()
图 8.4 – 评论数据框的前 10 行
-
我们将使用
Text和Score列;因此,我们将删除其他不会使用的列。我们还将调用dropna()方法来删除包含缺失值的行::reviews_df = reviews_df[['Text','Score']].dropna() -
我们可以快速查看评论评分的分布:
ax=reviews_df.Score.value_counts().plot(kind='bar', colormap='Paired') plt.show() -
这段代码调用了
dataframe reviews_df的plot方法,并展示了一个条形图:![图 8.5 – 评论评分的分布]()
图 8.5 – 评论评分的分布
5 星评价的数量相当高;看起来顾客对购买的食品很满意。然而,如果一个类别比其他类别有显著更多的权重,这可能会在训练数据中造成不平衡。
类别不平衡通常会给分类算法带来麻烦。例如,当一个类别比其他类别有显著更多的训练示例时,这被认为是不平衡(通常示例之间的比例为 1:5)。处理不平衡的方法有很多,其中一种方法是上采样/下采样。在下采样中,我们从多数类别随机删除训练示例。在上采样中,我们随机复制少数类别的训练示例。这两种方法的目标都是平衡多数和少数类别的训练示例数量。
在这里,我们将应用另一种方法。我们将合并 1 星、2 星、3 星评价和 4 星、5 星评价,以获得一个更平衡的数据集。
-
为了防止这种情况,我们将 1 星、2 星和 3 星评价视为负面评价,而将超过 4 星的评价视为正面评价。以下代码段将所有评分少于 4 星的评论分配为负面标签,将所有评分高于 4 星的评论分配为正面标签:
reviews_df.Score[reviews_df.Score<=3]=0 reviews_df.Score[reviews_df.Score>=4]=1 -
让我们再次绘制评分的分布:
ax=reviews_df.Score.value_counts().plot(kind='bar', colormap='Paired') plt.show()结果的评分分布看起来比图 8.5好得多。尽管如此,正面评论的数量仍然更多,但负面评论的数量也很显著,如下面的图表所示:

图 8.6 – 正面和负面评分的分布
在处理完数据集后,我们将其缩减为包含负面和正面评分的两列数据集。我们再次调用reviews_df.head(),以下是我们得到的结果:

图 8.7 – DataFrame 的前四行
我们在这里结束数据集的探索。我们看到了评论分数和类别标签的分布。现在数据集已经准备好进行处理了。我们删除了未使用的列,并将评论分数转换为二进制类别标签。让我们继续开始训练过程!
训练 TextClassifier 组件
现在,我们已经准备好开始训练过程了。这次我们将使用多标签分类器训练一个二进制文本分类器。再次,让我们一步一步来:
-
我们首先按照以下方式导入 spaCy 类:
import spacy import random from spacy.training import Example from spacy.pipeline.textcat_multilabel import DEFAULT_MULTI_TEXTCAT_MODEL -
接下来,我们将创建一个 pipeline 对象
nlp,定义分类器配置,并将TextCategorizer组件添加到nlp中,配置如下:nlp = spacy.load("en_core_web_md") config = { "threshold": 0.5, "model": DEFAULT_MULTI_TEXTCAT_MODEL } textcat = nlp.add_pipe("textcat_multilabel", config=config) -
在创建文本分类器组件后,我们将训练句子和评分转换为 spaCy 可用的格式。我们将使用
iterrows()遍历 DataFrame 的每一行,对于每一行,我们将提取Text和Score字段。然后,我们将从评论文本创建一个 spaCyDoc对象,并创建一个包含类别标签的字典。最后,我们将创建一个Example对象并将其追加到训练示例列表中:train_examples = [] for index, row in reviews_df.iterrows(): text = row["Text"] rating = row["Score"] label = {"POS": True, "NEG": False} if rating == 1 else {"NEG": True, "POS": False} train_examples.append(Example.from_dict(nlp.make_doc(text), {"cats": label})) -
我们将使用
POS和NEG标签分别表示正面和负面情感。我们将这些标签引入新组件,并用示例初始化该组件:textcat.add_label("POS") textcat.add_label("NEG") textcat.initialize(lambda: train_examples, nlp=nlp) -
我们已经准备好定义训练循环了!我们遍历了训练集两个 epoch,但如果你愿意,可以遍历更多。以下代码片段将训练新的文本分类器组件:
epochs = 2 with nlp.select_pipes(enable="textcat_multilabel"): optimizer = nlp.resume_training() for i in range(epochs): random.shuffle(train_examples) for example in train_examples: nlp.update([example], sgd=optimizer) -
最后,我们将测试文本分类器组件对两个示例句子的处理效果:
doc2 = nlp("This is the best food I ever ate") doc2.cats {'POS': 0.9553419947624207, 'NEG': 0.061326123774051666} doc3 = nlp("This food is so bad") doc3.cats {'POS': 0.21204468607902527, 'NEG': 0.8010350465774536}
由于我们使用了多标签分类器,NEG和POS标签都出现在预测结果中。结果看起来不错。第一句话输出了一个非常高的正面概率,第二句话则被预测为负面,概率也很高。
我们已经完成了 spaCy 文本分类器组件的训练。在下一节中,我们将深入探讨一个非常流行的深度学习库 Keras 的世界。我们将探索如何使用另一个流行的机器学习库——TensorFlow 的 Keras API 来编写 Keras 代码进行文本分类。让我们继续探索 Keras 和 TensorFlow!
使用 spaCy 和 Keras 进行文本分类
在本节中,我们将学习如何使用另一个非常流行的 Python 深度学习库TensorFlow及其高级 APIKeras来将 spaCy 与神经网络结合使用。
深度学习是一系列基于神经网络的机器学习算法的统称。神经网络是受人类大脑启发的算法,包含相互连接的层,这些层由神经元组成。每个神经元都是一个数学运算,它接收输入,将其与其权重相乘,然后将总和通过激活函数传递给其他神经元。以下图表展示了一个具有三层结构的神经网络架构——输入层、隐藏层和输出层:
![图 8.8 – 具有三层的神经网络架构]
![img/B16570_8_8.jpg]
图 8.8 – 具有三层的神经网络架构
TensorFlow是一个端到端的开源机器学习平台。TensorFlow 可能是研究工程师和科学家中最受欢迎的深度学习库。它拥有庞大的社区支持和优秀的文档,可在www.tensorflow.org/找到。
Keras是一个高级深度学习 API,可以在 TensorFlow、Theano 和 CNTK 等流行的机器学习库之上运行。Keras 在研发领域非常受欢迎,因为它支持快速原型设计和提供了一个用户友好的 API 来构建神经网络架构。
TensorFlow 2通过紧密集成 Keras 并提供高级 APItf.keras,在机器学习方法上引入了重大变化。TensorFlow 1 在符号图计算和其他低级计算方面有些丑陋。随着 TensorFlow 2 的推出,开发者可以利用 Keras 的用户友好性以及 TensorFlow 的低级方法。
神经网络通常用于计算机视觉和 NLP 任务,包括目标检测、图像分类、场景理解以及文本分类、词性标注、文本摘要和自然语言生成。
在接下来的章节中,我们将详细介绍使用tf.keras实现的文本分类神经网络架构的细节。在整个本节中,我们将使用我们在技术要求部分中提到的 TensorFlow 2。让我们从一些神经网络基础知识开始,然后开始构建我们的 Keras 代码。
什么是层?
神经网络是通过连接层形成的。层基本上是神经网络的构建块。一个层由几个神经元组成,如图 8.8所示。
在图 8.8中,这个神经网络的第一层有两个子层,第二层有六个神经元。每个子层中的每个神经元都与下一层的所有神经元相连。每个子层可能具有不同的功能;有些子层可以降低其输入的维度,有些子层可以将输入展平(展平意味着将多维向量折叠成一维),等等。在每一层,我们转换输入向量并将它们传递给下一层以获得最终向量。
Keras 提供了不同类型的层,例如输入层、密集层、dropout 层、嵌入层、激活层、循环层等等。让我们逐一了解一些有用的层:
-
输入层:输入层负责将我们的输入数据发送到网络的其余部分。在初始化输入层时,我们提供输入数据形状。
-
密集层:密集层将给定形状的输入转换为所需的输出形状。图 8.8中的层 2 代表一个密集层,它将 5 维输入折叠成 1 维输出。
-
循环层:Keras 为 RNN、GRU 和 LSTM 单元提供了强大的支持。如果您对 RNN 变体完全不熟悉,请参阅技术要求部分中的资源。在我们的代码中,我们将使用 LSTM 层。LSTM 层小节包含输入和输出形状信息。在下一小节使用 LSTMs 进行序列建模中,我们将深入了解使用 LSTMs 进行建模的细节。
-
Dropout 层:Dropout 是一种防止过拟合的技术。当神经网络记住数据而不是学习数据时,就会发生过拟合。Dropout 层随机选择一定数量的神经元,并在正向和反向传递中(即在一次迭代中)将它们的权重设置为零。我们通常在密集层之后放置 dropout 层。
这些是在 NLP 模型中使用的基本层。下一小节将致力于使用 LSTMs 建模序列数据,这是 NLP 统计建模的核心。
使用 LSTMs 进行序列建模
LSTM是一种 RNN 变体。RNNs是特殊的神经网络,可以按步骤处理序列数据。在通常的神经网络中,我们假设所有输入和输出都是相互独立的。当然,对于文本数据来说,这并不正确。每个单词的存在都依赖于相邻的单词。
例如,在机器翻译任务中,我们通过考虑之前预测的所有单词来预测一个单词。RNN 通过保持一个i来捕获关于过去序列元素的信息,我们输入单词xi,RNN 为这个时间步输出一个值,hi:


图 8.9 – RNN 示意图,摘自 Colah 关于 LSTMs 的著名博客
LSTM 是为了解决 RNN 的一些计算问题而发明的。RNN 存在着在序列中忘记一些数据的问题,以及由于链式乘法导致的数值稳定性问题,称为 梯度消失和梯度爆炸。如果您感兴趣,可以参考 Colah 的博客,您将在 参考文献 部分找到链接。
LSTM 单元比 RNN 单元稍微复杂一些,但计算逻辑是相同的:我们在每个时间步输入一个输入单词,LSTM 在每个时间步输出一个输出值。以下图显示了 LSTM 单元内部的情况。请注意,输入步骤和输出步骤与 RNN 的对应步骤相同:

图 8.10 – 来自 Colah 的 LSTM 博文文章的 LSTM 插图
Keras 对 RNN 变体 GRU 和 LSTM 提供了广泛的支持,同时还有一个简单的 API 用于训练 RNN。RNN 变体对于 NLP 任务至关重要,因为语言数据的本质是序列性的:文本是一系列单词,语音是一系列声音,等等。
现在我们已经学会了在我们的设计中使用哪种类型的统计模型,我们可以转向一个更实际的主题:如何表示单词序列。在下一节中,我们将学习如何使用 Keras 的预处理模块将单词序列转换为单词 ID 序列,并同时构建词汇表。
Keras Tokenizer
正如我们在上一节中提到的,文本是序列数据(单词或字符的序列)。我们将句子作为单词序列输入。神经网络只能处理向量,因此我们需要一种将单词向量化的方法。在第五章,“使用单词向量和语义相似性”,我们看到了如何使用单词向量来向量化单词。单词向量是单词的连续表示。为了向量化一个单词,我们遵循以下步骤:
-
我们将每个句子进行分词,并将句子转换成单词序列。
-
我们从 步骤 1 中出现的单词集合创建一个词汇表。这些是我们神经网络设计中应该被识别的单词。
-
创建词汇表应该为每个单词分配一个 ID。
-
然后将单词 ID 映射到单词向量。
让我们看看一个简短的例子。我们可以处理一个小型语料库中的三个句子:
data = [
"Tomorrow I will visit the hospital.",
"Yesterday I took a flight to Athens.",
"Sally visited Harry and his dog."
]
让我们先对单词进行分词成句子:
import spacy
nlp = spacy.load("en_core_web_md")
sentences = [[token.text for token in nlp(sentence)] for sentence in data]
for sentence in sentences:
sentence
...
['Tomorrow', 'I', 'will', 'visit', 'the', 'hospital', '.']
['Yesterday', 'I', 'took', 'a', 'flight', 'to', 'Athens', '.']
['Sally', 'visited', 'Harry', 'and', 'his', 'dog', '.']
在前面的代码中,我们遍历了由调用 nlp(sentence) 生成的 Doc 对象的所有标记。请注意,我们没有过滤掉标点符号。过滤标点符号取决于任务。例如,在情感分析中,标点符号如 "!" 与结果相关。在这个例子中,我们将保留标点符号。
Keras 的文本预处理模块使用 Tokenizer 类创建词汇表,并将 trn 单词序列转换为单词-ID 序列。以下代码段展示了如何使用 Tokenizer 对象:
from tensorflow.keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(lower=True)
tokenizer.fit_on_texts(data)
tokenizer
<keras_preprocessing.text.Tokenizer object at 0x7f89e9d2d9e8>
tokenizer.word_index
{'i': 1, 'tomorrow': 2, 'will': 3, 'visit': 4, 'the': 5, 'hospital': 6, 'yesterday': 7, 'took': 8, 'a': 9, 'flight': 10, 'to': 11, 'athens': 12, 'sally': 13, 'visited': 14, 'harry': 15, 'and': 16, 'his': 17, 'dog': 18}
我们做了以下事情:
-
我们从 Keras 文本预处理模块导入了
Tokenizer。 -
我们使用参数
lower=True创建了一个tokenizer对象,这意味着tokenizer在构建词汇表时应该将所有单词转换为小写。 -
我们在
data上调用tokenizer.fit_on_texts来构建词汇表。fit_on_text在单词序列上工作;输入始终应为单词的列表。 -
我们通过打印
tokenizer.word_index来检查词汇表。Word_index基本上是一个字典,其中键是词汇表中的单词,值是单词-ID。
为了获取一个单词的单词-ID,我们调用 tokenizer.texts_to_sequences。请注意,此方法的输入始终应为列表,即使我们只想输入一个单词。在下面的代码段中,我们将一个单词输入作为一个列表(注意列表括号):
tokenizer.texts_to_sequences(["hospital"])
[[6]]
tokenizer.texts_to_sequences(["hospital", "took"])
[[6], [8]]
texts_to_sequences 的逆操作是 sequences_to_texts。sequences_to_texts 方法将输入一个列表的列表,并返回相应的单词序列:
tokenizer.sequences_to_texts([[3,2,1]])
['will tomorrow i']
tokenizer.sequences_to_texts([[3,2,1], [5,6,10]])
['will tomorrow i', 'the hospital flight']
我们还注意到,单词-ID 从 1 开始,而不是 0。0 是一个保留值,具有特殊含义,表示填充值。Keras 无法处理不同长度的句子,因此我们需要将较短的句子填充到最长句子的长度。我们将数据集中的每个句子填充到最大长度,通过在句子的开始或末尾添加填充单词来实现。Keras 插入 0 作为填充,这意味着它不是一个真正的单词,而是一个填充值。让我们用一个简单的例子来理解填充:
from tensorflow.keras.preprocessing.sequence import pad_sequences
sequences = [[7], [8,1], [9,11,12,14]]
MAX_LEN=4
pad_sequences(sequences, MAX_LEN, padding="post")
array([[ 7, 0, 0, 0],
[ 8, 1, 0, 0],
[ 9, 11, 12, 14]], dtype=int32)
pad_sequences(sequences, MAX_LEN, padding="pre")
array([[ 0, 0, 0, 7],
[ 0, 0, 8, 1],
[ 9, 11, 12, 14]], dtype=int32)
我们的序列长度为 1、2 和 4。我们对这个序列列表调用了 pad_sequences,每个序列都被填充为零,使其长度达到 MAX_LEN=4,即最长序列的长度。我们可以使用 post 和 pre 选项从右或左填充序列。在先前的代码中,我们使用 post 选项填充我们的句子,因此句子是从右向左填充的。
如果我们将所有这些放在一起,完整的文本预处理步骤如下:
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
tokenizer = Tokenizer(lower=True)
tokenizer.fit_on_texts(data)
seqs = tokenizer.texts_to_sequences(data)
MAX_LEN=7
padded_seqs = pad_sequences(seqs, MAX_LEN, padding="post")
padded_seqs
array([[ 2, 1, 3, 4, 5, 6, 0],
[ 7, 1, 8, 9, 10, 11, 12],
[13, 14, 15, 16, 17, 18, 0]], dtype=int32)
现在,我们已经将句子转换成了单词-ID 的序列。我们离将单词向量化又近了一步。在下一个小节中,我们将最终将单词转换成向量。然后我们的句子就可以输入到神经网络中了。
嵌入单词
我们准备好将单词转换成单词向量了。将单词嵌入到向量中是通过嵌入表来实现的。嵌入表基本上是一个查找表。每一行都包含一个单词的单词向量。我们通过单词-ID 来索引行,因此获取一个单词的单词向量的流程如下:
-
Tokenizer。Tokenizer包含所有词汇,并将每个词汇映射到一个 ID,这是一个整数。 -
单词-ID->单词向量:单词-ID 是一个整数,因此可以用作嵌入表行的索引。每个单词-ID 对应一行,当我们想要获取一个单词的单词向量时,我们首先获取其单词-ID,然后在嵌入表行中使用此单词-ID 进行查找。
以下图表显示了将单词嵌入到词向量中的工作方式:

图 8.11 – 使用 Keras 将单词转换为单词向量的步骤
记住,在前一个部分中,我们从一个句子列表开始。然后我们做了以下几步:
-
我们将每个句子分解成单词,并使用 Keras 的
Tokenizer构建一个词汇表。 -
Tokenizer对象包含一个单词索引,这是一个单词->单词-ID映射。 -
在获得单词-ID 之后,我们可以通过这个单词-ID 查找嵌入表中的行,并获取一个单词向量。
-
最后,我们将这个单词向量馈送到神经网络中。
训练神经网络并不容易。我们必须采取几个步骤将句子转换为向量。在这些初步步骤之后,我们就准备好设计神经网络架构并进行模型训练了。
文本分类的神经网络架构
在本节中,我们将为我们的文本分类器设计神经网络架构。我们将遵循以下步骤来训练分类器:
-
首先,我们将对评论句子进行预处理、标记化和填充。在此步骤之后,我们将获得一个序列列表。
-
我们将通过输入层将这个序列列表馈送到神经网络中。
-
接下来,我们将通过查找嵌入层中的单词 ID 来向量化每个单词。在这个时候,一个句子现在是一个单词向量的序列,每个单词向量对应一个单词。
-
之后,我们将把单词向量序列馈送到 LSTM 中。
-
最后,我们将使用 sigmoid 层压缩 LSTM 输出以获得类概率。
让我们从再次记住数据集开始。
数据集
我们将使用与使用 spaCy 进行情感分析部分相同的亚马逊美食评论数据集。在那个部分中,我们已经使用 pandas 处理了数据集,并将其减少到两列和二进制标签。以下是reviews_df数据集的显示方式:

图 8.12 – reviews_df.head()的结果
我们将对数据集进行一些转换。我们将从每个数据集行中提取评论文本和评论标签,并将它们附加到 Python 列表中:
train_examples = []
labels = []
for index, row in reviews_df.iterrows():
text = row["Text"]
rating = row["Score"]
labels.append(rating)
tokens = [token.text for token in nlp(text)]
train_examples.append(tokens)
注意,我们将一个单词列表附加到train_examples中,因此这个列表的每个元素都是一个单词列表。接下来,我们将调用 Keras 的Tokenizer对这个单词列表进行操作以构建我们的词汇表。
数据和词汇准备
我们已经处理了我们的数据集,因此我们准备好对数据集句子进行标记化并创建词汇表。让我们一步一步来:
-
首先,我们将进行必要的导入:
from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences import numpy as np -
我们准备好将
Tokenizer对象拟合到我们的单词列表。首先,我们将拟合Tokenizer,然后我们将通过调用texts_to_sequences将单词转换为它们的 ID:tokenizer = Tokenizer(lower=True) tokenizer.fit_on_texts(train_examples) sequences = tokenizer.texts_to_sequences(train_examples) -
然后,我们将短序列填充到最大长度为
50(我们选择了这个数字)。同时,这将截断长评论到50个单词的长度:MAX_LEN = 50 X = pad_sequences(sequences, MAX_LEN, padding="post") -
现在
X是一个包含50个单词的序列列表。最后,我们将这个评论列表和标签转换为numpy数组:X = np.array(X) y = np.array(labels)
到目前为止,我们已经准备好将数据输入到我们的神经网络中。我们将数据输入到输入层。对于所有必要的导入,请参考我们 GitHub 仓库中本节的工作簿:github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter08/Keras_train.ipynb。
注意,我们没有进行任何词形还原/词干提取或停用词去除。这完全没问题,实际上这是与神经网络算法一起使用的标准方式,因为具有相同词根的单词(如 liked、liking、like)将获得相似的词向量(回想一下第五章**,处理词向量和语义相似性,相似单词获得相似的词向量)。此外,停用词在不同的上下文中频繁出现,因此神经网络可以推断出这些单词只是语言中的常见单词,并不携带太多重要性。
输入层
以下代码块定义了我们的输入层:
sentence_input = Input(shape=(None,))
不要被None作为输入形状所困惑。在这里,None表示这个维度可以是任何标量数,因此,当我们想要 Keras 推断输入形状时,我们使用这个表达式。
嵌入层
我们如下定义嵌入层:
embedding = Embedding(\
input_dim = len(tokenizer.word_index)+1,\
output_dim = 100)(sentence_input)
在定义嵌入层时,输入维度应该是词汇表中的单词数量(在这里,由于索引从1开始而不是0,所以有一个加号+1。索引0是为填充值保留的)。
在这里,我们选择了输出形状为100,因此词汇表单词的词向量将是100维度的。根据任务的复杂度,常见的词向量维度是50、100和200。
LSTM 层
我们将词向量输入到我们的 LSTM 中:
LSTM_layer = LSTM(units=256)(embedding)
在这里,units参数表示隐藏状态的维度。由于 LSTM 架构,LSTM 输出形状和隐藏状态形状是相同的。在这里,我们的 LSTM 层将输出一个256维度的向量。
输出层
我们从 LSTM 层获得了一个256维度的向量,并希望将其压缩成一个1维度的向量(这个向量的可能值是0和1,它们是类别标签):
output_dense = Dense(1, activation='sigmoid')(LSTM_layer)
我们使用了 sigmoid 函数来压缩值。sigmoid 函数是一个 S 形函数,将输入映射到[0-1]的范围内。你可以在deepai.org/machine-learning-glossary-and-terms/sigmoid-function了解更多关于这个函数的信息。
编译模型
在定义模型后,我们需要使用优化器、损失函数和评估指标来编译它:
model = \
Model(inputs=[sentence_input],outputs=[output_dense])
model.compile(optimizer="adam",loss="binary_crossentropy",\
metrics=["accuracy"])
自适应矩估计(ADAM)是深度学习中的一种流行优化器。它基本上调整神经网络应该学习多快的速度。你可以在这篇博客文章中了解不同的优化器:ruder.io/optimizing-gradient-descent/。二元交叉熵是用于二元分类任务的损失。Keras 根据任务支持不同的损失函数。你可以在 Keras 网站上找到列表,网址为keras.io/api/losses/。
度量是我们用来评估模型性能的函数。准确度度量基本上是比较预测标签和真实标签匹配的次数。支持的度量列表可以在 Keras 的文档中找到(keras.io/api/metrics/)。
模型拟合和实验评估
最后,我们将模型拟合到我们的数据上:
model.fit(x=X,
y=y,
batch_size=64,
epochs=5,
validation_split=0.2)
在这里,x是训练示例的列表,y是标签的列表。我们想要对数据进行 5 次遍历,因此将epochs参数设置为5。
我们以64的批量大小对数据进行5次遍历。通常,我们不会一次性将整个数据集放入内存中(由于内存限制),而是将数据集分批提供给分类器,每个批次被称为batch_size=64,这意味着我们希望一次性提供 64 个训练句子。
最后,参数validation_split用于评估实验。该参数简单地将 20%的数据作为验证集,并在该验证集上验证模型。我们的实验结果准确度为 0.795,对于这样一个基本的神经网络设计来说相当不错。
我们鼓励你进行更多实验。你可以通过在不同位置(如嵌入层之后或 LSTM 层之后)放置 dropout 层来更多地实验代码。另一种实验方法是尝试不同的嵌入维度值,例如 50、150 和 200,并观察准确度的变化。同样适用于 LSTM 层的隐藏维度——你可以用不同的值而不是 256 来实验。
在本节中,我们使用tf.keras完成了训练,并结束了本章。Keras 是一个伟大、高效且用户友好的深度学习 API;spaCy 和 Keras 的组合特别强大。文本分类是 NLP 的一个基本任务,我们发现了如何使用 spaCy 来完成这个任务。
摘要
我们完成了关于一个非常热门的 NLP 主题——文本分类——的章节。在本章中,你首先了解了文本分类的概念,如二元分类、多标签分类和多类分类。接下来,你学习了如何训练TextCategorizer,spaCy 的文本分类组件。你学习了如何将你的数据转换为 spaCy 训练格式,然后使用这些数据训练TextCategorizer组件。
在使用 spaCy 的TextCategorizer学习文本分类后,在最后一节中,你学习了如何结合 spaCy 代码和 Keras 代码。首先,你学习了神经网络的基础知识,包括一些实用的层,如密集层、dropout 层、嵌入层和循环层。然后,你学习了如何使用 Keras 的Tokenizer进行数据分词和预处理。
你快速回顾了使用 LSTMs 进行序列建模,以及从第五章,“处理词向量与语义相似性”,回顾词向量,以更好地理解嵌入层。最后,你通过tf.keras代码进行了神经网络设计。你学习了如何使用 LSTM 设计和评估一个统计实验。
看起来有很多!确实,这是一堆材料;如果需要时间消化,请不要担心。练习文本分类可能会很紧张,但最终,你将获得关键的 NLP 技能。
下一章将再次介绍一项全新的技术:transformers。在下一章中,我们将探讨如何仅用几行代码设计高精度的 NLP 管道。让我们进入下一章,看看 transformers 能为你的 NLP 技能带来什么!
参考文献
如果你熟悉神经网络,特别是 RNN 变体,这将是有益的但不是强制性的。以下是一些关于神经网络的优秀材料:
-
免费在线书籍:神经网络与深度学习 (
neuralnetworksanddeeplearning.com/)
RNN 变体,特别是 LSTMs,也有很好的教程:
-
WildML 博客上的 RNN 教程:
www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/ -
多伦多大学 RNN 教程:
www.cs.toronto.edu/~tingwuwang/rnn_tutorial.pdf -
Colah 的博客:
colah.github.io/posts/2015-08-Understanding-LSTMs/ -
Michael Phi 的博客文章:
towardsdatascience.com/illustrated-guide-to-lstms-and-gru-s-a-step-by-step-explanation-44e9eb85bf21
尽管我们在本章介绍了神经网络,但你仍然可以阅读这些参考文献,以了解更多关于神经网络如何工作的信息。在第十章,“将一切整合:使用 spaCy 设计你的聊天机器人”,将会有更多关于神经网络和 LSTM 概念的说明。
第九章:spaCy 和 Transformers
在本章中,你将了解自然语言处理(NLP)的最新热门话题——transformers,以及如何使用 TensorFlow 和 spaCy 来使用它们。
首先,你将了解 transformers 和迁移学习。其次,你将学习常用 Transformer 架构——双向 Transformer 编码器表示(BERT)的架构细节。你还将了解BERT 分词器和WordPiece算法是如何工作的。然后,你将学习如何快速开始使用 HuggingFace 库中预训练的 transformers 模型。接下来,你将练习如何使用 TensorFlow 和 Keras 微调 HuggingFace Transformers。最后,你将了解spaCy v3.0如何将 transformers 模型作为预训练管道集成。
到本章结束时,你将完成本书的统计自然语言处理(NLP)主题。你将把你在第八章“使用 spaCy 进行文本分类”中获得的 Keras 和 TensorFlow 知识与你对 transformers 的知识相结合。你将能够仅用几行代码,借助 Transformer 模型和迁移学习的能力,构建最先进的 NLP 模型。
在本章中,我们将涵盖以下主要主题:
-
Transformers 和迁移学习
-
理解 BERT
-
Transformers 和 TensorFlow
-
Transformers 和 spaCy
技术要求
在本章中,我们将使用transformers和tensorflow Python 库以及spaCy。你可以通过pip安装这些库:
pip install transformers
pip install "tensorflow>=2.0.0"
本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter09。
Transformers 和迁移学习
2017 年,随着 Vaswani 等人发表的研究论文《Attention Is All You Need》的发布,自然语言处理(NLP)领域发生了一个里程碑事件(arxiv.org/abs/1706.03762),该论文介绍了一种全新的机器学习思想和架构——transformers。在 NLP 中的 Transformers 是一个新颖的想法,旨在解决序列建模任务,并针对长短期记忆(LSTM)架构提出的一些问题(回想一下第八章“使用 spaCy 进行文本分类”中的 LSTM 架构)。以下是论文如何解释 transformers 的工作原理:
“Transformer 是第一个完全依赖自注意力来计算其输入和输出表示的转导模型,而不使用序列对齐的 RNN 或卷积。”
在这个上下文中,转导意味着通过将输入单词和句子转换为向量来转换输入单词和句子。通常,一个变压器在像 Wiki 或新闻这样的大型语料库上训练。然后,在我们的下游任务中,我们使用这些向量,因为它们携带有关词义、句子结构和句子语义的信息(我们将在 Transformers 和 TensorFlow 部分中看到如何精确地使用这些向量)。
我们已经在 第五章,使用词向量与语义相似性 中探讨了预训练词向量的想法。Glove 和 FastText 等词向量已经在维基百科语料库上进行了训练,我们直接使用它们进行我们的语义相似性计算。通过这种方式,我们将来自 Wiki 语料库的词义信息导入到我们的语义相似性计算中。从预训练词向量或预训练统计模型中导入知识被称为 迁移学习。
Transformers 提供了数千个预训练模型来执行 NLP 任务,例如文本分类、文本摘要、问答、机器翻译以及超过 100 种语言的自然语言生成。Transformers 的目标是使最先进的 NLP 对每个人可访问。
以下截图显示了 HuggingFace 提供的 Transformer 模型列表(我们将在 HuggingFace Transformers 部分中了解 HuggingFace Transformers)。每个模型都由架构名称(Bert、DistilBert 等)、可能的语言代码(en、de、multilingual 等,如以下截图左侧所示)以及有关模型是否区分大小写的信息(模型区分大小写字符)的组合命名。
此外,在 图 9.1 的左侧,我们看到任务名称。每个模型都标有任务名称。我们选择适合我们任务的模型,例如文本分类或机器翻译:

图 9.1 – HuggingFace Transformers 列表,摘自 HuggingFace 网站
要了解变压器的好处,我们首先将回顾来自 第八章,使用 spaCy 进行文本分类 的 LSTM 架构。在前一章中,我们已经使用 Keras 和 LSTM 架构进入了统计建模的世界。LSTM 在建模文本方面非常出色;然而,它们也有一些缺点:
-
LSTM 架构在处理长文本时有时会遇到学习困难。由于随着时间步的推移,LSTM 可能会忘记之前处理的一些单词,因此在长文本中的统计依赖关系可能难以通过 LSTM 来表示。
-
LSTM 的本质是序列性的。我们每个时间步处理一个单词。显然,并行化学习过程是不可能的;我们必须按顺序处理。不允许并行化造成性能瓶颈。
Transformers 通过完全不使用循环层来解决这些问题。如果我们看一下以下内容,其架构与 LSTM 架构完全不同。Transformer 架构由两部分组成 – 左侧的输入编码器块(称为编码器)和右侧的输出解码器块(称为解码器)。以下图表取自这篇论文,展示了 Transformer 架构:

图 9.4 – “bank”这个词的词向量
在这里,尽管这两个句子中的“bank”这个词有两个完全不同的含义,但词向量是相同的,因为 Glove 和 FastText 是静态的。每个词只有一个向量,向量在训练后保存到文件中。然后,我们下载这些预训练的向量并将它们加载到我们的应用程序中。
相反,BERT 的词向量是动态的。BERT 可以根据输入句子为同一个词生成不同的词向量。以下图表显示了 BERT 生成的词向量,与图 9.4中的词向量形成对比:

图 9.5 – BERT 在两个不同语境下为同一单词“bank”生成的两个不同的词向量
BERT 是如何生成这些词向量的?在下一节中,我们将探讨 BERT 架构的细节。
BERT 架构
正如上一节中已经提到的,BERT 是一个 transformer 编码器堆叠,这意味着几个编码器层堆叠在一起。第一层随机初始化词向量,然后每个编码器层转换前一个编码器层的输出。论文介绍了 BERT 的两个模型大小:BERT Base 和 BERT Large。以下图表展示了 BERT 架构:

图 9.6 – BERT Base 和 Large 架构,分别有 12 和 24 个编码器层
两个 BERT 模型都有大量的编码器层。BERT Base 有 12 个编码器层,BERT Large 有 24 个编码器层。生成的词向量维度也不同;BERT Base 生成 768 大小的词向量,而 BERT Large 生成 1024 大小的词向量。
正如我们在上一节中提到的,BERT 为每个输入单词输出词向量。以下图表展示了 BERT 输入和输出的高级概述(现在暂时忽略 CLS 标记;你将在BERT 输入格式部分了解它):

图 9.7 – BERT 模型输入词和输出词向量
在前面的图表中,我们可以看到 BERT 输入和输出的高级概述。实际上,BERT 输入必须以特殊格式存在,并包括一些特殊标记,如图 9.7中的 CLS。在下一节中,你将了解 BERT 输入格式的细节。
BERT 输入格式
我们已经介绍了 BERT 架构,现在让我们了解如何使用 BERT 生成输出向量。为此,我们将了解 BERT 输入数据格式。BERT 输入格式可以表示一个句子,也可以表示一对句子(对于如问答和语义相似度等任务,我们输入两个句子到模型)作为一个标记序列。
BERT 与[CLS]、[SEP]和[PAD]类一起工作:
-
BERT 的第一个特殊标记是
[CLS]。每个输入序列的第一个标记必须是[CLS]。我们在分类任务中使用此标记作为输入句子的聚合。在非分类任务中,我们忽略此标记。 -
[SEP]表示[CLS]句子[SEP],对于两个句子,输入看起来像[CLS]句子 1[SEP]句子 2[SEP]。 -
[PAD]是一个特殊标记,表示填充。回顾前一章,我们使用填充值来使我们的数据集中的句子长度相等。BERT 接收固定长度的句子;因此,我们在将句子输入 BERT 之前对其进行填充。我们可以输入 BERT 的标记最大长度是512。
关于分词单词呢?回顾前一小节,我们一次将一个单词输入到我们的 Keras 模型中。我们使用 spaCy 分词器将输入句子分词成单词。BERT 的工作方式略有不同,BERT 使用 WordPiece 分词。一个“词片”字面上是一个单词的一部分。WordPiece 算法将单词分解成几个子词。想法是将复杂/长的标记分解成更简单的标记。例如,单词playing被分词为play和##ing。一个##字符放在每个词片之前,表示这个标记不是语言词汇中的单词,而是一个词片。
让我们看看更多的例子:
playing play, ##ing
played play, ##ed
going go, ##ing
vocabulary = [play,go, ##ing, ##ed]
这样,我们通过 WordPiece 分组常见的子词更紧凑地表示语言词汇。WordPiece 分词在罕见/未见过的单词上创造了奇迹,因为这些单词被分解成它们的子词。
在对输入句子进行分词并添加特殊标记后,每个标记被转换为它的 ID。之后,作为最后一步,我们将标记 ID 序列输入 BERT。
总结一下,这是我们如何将句子转换为 BERT 输入格式的:

图 9.8 – 将输入句子转换为 BERT 输入格式
BERT 分词器有不同方法来执行之前描述的所有任务,但它还有一个编码方法,将这些步骤合并为单个步骤。我们将在Transformers 和 TensorFlow部分详细说明如何使用 BERT 分词器。在那之前,我们将学习用于训练 BERT 的算法。
BERT 是如何训练的?
BERT 是在一个大型未标记的维基百科语料库和庞大的书籍语料库上训练的。BERT 的创造者在谷歌研究 GitHub 仓库中这样描述了 BERT,github.com/google-research/bert,如下所示:
“然后我们在一个大型语料库(维基百科 + BookCorpus)上训练了一个大型模型(12 层到 24 层的 Transformer),长时间(1M 更新步骤),这就是 BERT。”
BERT 使用两种训练方法进行训练,掩码语言模型(MLM)和下一句预测(NSP)。让我们首先了解掩码语言模型的细节。
语言模型是预测给定前一个标记序列的下一个标记的任务。例如,给定单词序列“Yesterday I visited”,语言模型可以预测下一个标记为“church”、“hospital”、“school”等中的一个。掩码语言模型略有不同。在这个方法中,我们随机将一定比例的标记用[MASK]替换,并期望 MLM(Masked Language Modeling)能够预测被掩码的单词。
BERT 中的掩码语言模型实现如下:首先,随机选择输入标记中的 15 个。然后,发生以下情况:
-
在选择的标记中,有 80%被替换成了
[MASK]。 -
在选择的标记中,有 20%被替换成了词汇表中的另一个标记。
-
剩余的 10%保持不变。
LMM(Language Modeling Masked)的训练示例句子如下:
[CLS] Yesterday I [MASK] my friend at [MASK] house [SEP]
接下来,我们将探讨其他算法 NSP 的细节。
正如其名所示,NSP(Next Sentence Prediction)是指根据输入句子预测下一个句子的任务。在这个方法中,我们将两个句子输入到 BERT 中,并期望 BERT 能够预测句子的顺序,更具体地说,是第二个句子是否是紧跟在第一个句子之后的句子。
让我们做一个 NSP 的示例输入。我们将使用[SEP]标记分隔的两个句子作为输入:
[CLS] A man robbed a [MASK] yesterday [MASK] 8 o'clock [SEP] He [MASK] the bank with 6 million dollars [SEP]
Label = IsNext
在这个例子中,第二个句子可以跟在第一个句子后面;因此,预测的标签是IsNext。那么这个例子呢:
[CLS] Rabbits like to [MASK] carrots and [MASK] leaves [SEP] [MASK] Schwarzenegger is elected as the governor of [MASK] [SEP]
Label= NotNext
这对句子生成了NotNext标签,因为很明显它们在上下文或语义上并不相关。
就这样!我们已经了解了 BERT 的架构;我们还学习了 BERT 输入数据格式的细节以及 BERT 是如何被训练的。现在,我们准备好深入 TensorFlow 代码了。在下一节中,我们将看到如何将我们迄今为止所学的内容应用到 TensorFlow 代码中。
Transformers 和 TensorFlow
在本节中,我们将使用 TensorFlow 深入研究 transformers 代码。许多组织,包括 Google (github.com/google-research/bert)、Facebook (github.com/pytorch/fairseq/blob/master/examples/language_model/README.md)和 HuggingFace (github.com/huggingface/transformers),都将预训练的 transformer 模型以开源的形式提供给开发者社区。所有列出的组织都提供了预训练模型和良好的接口,以便将 transformers 集成到我们的 Python 代码中。这些接口与 PyTorch 或 TensorFlow 或两者都兼容。
在本章中,我们将使用 HuggingFace 的预训练 transformers 及其 TensorFlow 接口来访问 transformer 模型。HuggingFace 是一家专注于 NLP 的 AI 公司,并且非常致力于开源。在下一节中,我们将更详细地了解 HuggingFace Transformers 中可用的内容。
HuggingFace Transformers
在第一部分,我们将了解 HuggingFace 的预训练模型,使用这些模型的 TensorFlow 接口,以及一般性的 HuggingFace 模型约定。我们在 图 9.1 中看到,HuggingFace 提供了不同种类的模型。每个模型都针对一个任务,如文本分类、问答和序列到序列建模。
以下图表来自 HuggingFace 文档,展示了 distilbert-base-uncased-distilled-squad 模型的详细信息。在文档中,首先在图表的左上角标记了任务(问答标签),然后是支持此模型的深度学习库(PyTorch、TensorFlow、TFLite、TFSavedModel),训练所用的数据集(在这个例子中是 squad),模型语言(en 表示英语),以及许可证和基础模型名称(在这种情况下是 DistilBERT)。
一些模型使用类似的算法进行训练,因此属于同一个模型家族。以 DistilBERT 家族为例,包括许多模型,如 distilbert-base-uncased 和 distilbert-multilingual-cased。每个模型名称也包含一些信息,例如大小写(模型识别大写/小写差异)或模型语言,如 en、de 或 multilingual:

图 9.9 – distilbert-base-uncased-distilled-squad 模型的文档
我们在上一节中已经详细探讨了 BERT。HuggingFace 文档提供了每个模型家族和单个模型的 API 的详细信息。图 9.10 展示了可用的模型列表和 BERT 模型架构变体的列表:

图 9.10 – 左侧列出可用的模型,右侧列出 BERT 模型变体
BERT 模型有多种变体,适用于各种任务,如文本分类、问答和下一句预测。这些模型中的每一个都是通过在 BERT 输出之上添加一些额外的层获得的。回想一下,BERT 输出是输入句子中每个单词的词向量序列。例如,BERTForSequenceClassification 模型是通过在 BERT 词向量之上放置一个密集层(我们在上一章中介绍了密集层)获得的。
在本章的剩余部分,我们将探讨如何使用这些架构中的某些部分来完成我们的任务,以及如何使用 Keras 与 BERT 词向量结合使用。在所有这些任务之前,我们将从基本的分词任务开始,为我们的输入句子做准备。让我们在下一节中查看分词器的代码。
使用 BERT 分词器
在理解 BERT部分,我们已经看到 BERT 使用 WordPiece 算法进行分词。每个输入单词都被分解成子词。让我们看看如何使用 HuggingFace 库准备我们的输入数据。
以下行展示了分词器的基本用法:
from transformers import BertTokenizer
btokenizer =\
BertTokenizer.from_pretrained('bert-base-uncased')
tokens = btokenizer.tokenize(sentence)
tokens
['he', 'lived', 'characteristic', '##ally', 'idle', 'and', 'romantic', '.']
ids = btokenizer.convert_tokens_to_ids(tokens)
ids
[2002, 2973, 8281, 3973, 18373, 1998, 6298, 1012]
在前面的代码块中,我们遵循的步骤如下:
-
首先,我们导入了
BertTokenizer。不同的模型有不同的分词器;例如,XLNet 模型的分词器被称为XLNetTokenizer。 -
其次,我们在分词器对象上调用
from_pretrained方法,并提供了模型名称。请注意,我们不需要手动下载预训练的bert-base-uncased模型;此方法会自动下载模型。 -
然后,我们调用了
tokenize方法。tokenize基本上通过将所有单词分解成子词来分词句子。 -
我们打印标记以检查子词。单词“he”、“lived”、“idle”等存在于分词器的词汇表中,因此保持不变。“Characteristically”是一个罕见单词,因此不在分词器的词汇表中。然后,分词器将这个单词分解成子词“characteristic”和“##ally”。请注意,“##ally”以“##”字符开头,以强调这是一个单词的一部分。
-
接下来,我们通过调用
convert_tokens_to_ids将标记转换为它们的标记 ID。
那么[CLS]和[SEP]标记呢?在前面的章节中,我们已经看到我们必须在输入句子的开头和结尾添加这两个特殊标记。对于前面的代码,我们需要进行一个额外的步骤,并手动添加我们的特殊标记。我们能否将这些预处理步骤合并为一步呢?答案是肯定的;BERT 提供了一个名为encode的方法,它执行以下操作:
-
在输入句子中添加
CLS和SEP标记 -
通过将标记分解成子词来分词句子
-
将标记转换为它们的标记 ID
我们直接在输入句子上调用encode方法,如下所示:
from transformers import BertTokenizer
btokenizer =\
BertTokenizer.from_pretrained('bert-base-uncased')
sentence = "He lived characteristically idle and romantic."
ids = btokenizer.encode(sentence)
ids
[101, 2002, 2973, 8281, 3973, 18373, 1998, 6298, 1012, 102]
此代码段在单步中输出标记 ID,而不是依次调用tokenize和convert_tokens_to_ids。结果是 Python 列表。
关于填充句子呢?在前面章节中,我们已经看到数据集中的所有输入句子都应该具有相同的长度,因为 BERT 无法处理变长句子。因此,我们需要将短句子填充到数据集中最长句子的长度。此外,如果我们想使用 TensorFlow 张量而不是普通列表,我们需要编写一些转换代码。HuggingFace 库提供了encode_plus来简化我们的工作,并将所有这些步骤合并为一个方法,如下所示:
from transformers import BertTokenizer
btokenizer =\
BertTokenizer.from_pretrained('bert-base-uncased')
sentence = "He lived characteristically idle and romantic."
encoded = btokenizer.encode_plus(
text=sentence,
add_special_tokens=True,
max_length=12,
pad_to_max_length=True,
return_tensors="tf"
)
token_ids = encoded["input_ids"]
print(token_ids)
tf.Tensor([[ 101 2002 2973 8281 3973 18373 1998 6298 1012 102 0 0]], shape=(1, 12), dtype=int32)
在这里,我们直接在我们的输入句子上调用encode_plus。现在,我们的句子被填充到长度为 12(序列末尾的两个0 ID 是填充标记),同时特殊标记[CLS]和[SEP]也被添加到句子中。输出直接是一个 TensorFlow 张量,包括标记 ID。
encode_plus方法接受以下参数:
-
text:输入句子。 -
add_special_tokens:添加CLS和SEP标记。 -
max_length:您希望句子达到的最大长度。如果句子短于max_length标记,我们希望填充句子。 -
pad_to_max_length:如果我们想填充句子,则提供True,否则提供False。 -
return_tensors:如果我们希望输出是一个张量,则传递此参数,否则输出是一个 Python 列表。可用的选项是tf和pt,分别对应 TensorFlow 和 PyTorch。
如我们所见,BERT 分词器为输入句子提供了几种方法。准备数据并不那么直接,但通过练习你会习惯的。我们总是鼓励你尝试使用自己的文本运行代码示例。
现在,我们已经准备好处理转换后的输入句子。让我们继续将我们的输入句子提供给 BERT 模型以获得 BERT 词向量。
提示
总是检查您应该与您的 transformer 一起使用的分词器类的名称。有关模型及其对应分词器的列表,可在huggingface.co/transformers/找到。
获取 BERT 词向量
在本节中,我们将检查 BERT 模型的输出。正如我们在理解 BERT部分所述,BERT 模型的输出是一系列词向量,每个输入词一个向量。BERT 有一个特殊的输出格式,在本节中,我们将详细检查 BERT 输出。
让我们先看看代码:
from transformers import BertTokenizer, TFBertModel
btokenizer =\
BertTokenizer.from_pretrained('bert-base-uncased')
bmodel = TFBertModel.from_pretrained("bert-base-uncased")
sentence = "He was idle."
encoded = btokenizer.encode_plus(
text=sentence,
add_special_tokens=True,
max_length=10,
pad_to_max_length=True,
return_attention_mask=True,
return_tensors="tf"
)
inputs = encoded["input_ids"]
outputs = bmodel(inputs)
这段代码与上一节的代码非常相似。在这里,我们也导入了TFBertModel。之后,我们使用预训练模型bert-base-uncased初始化我们的 BERT 模型。然后,我们使用encode_plus将我们的输入句子转换为 BERT 输入格式,并将结果捕获在输入变量tf.tensor中。我们将我们的句子输入到 BERT 模型中,并用outputs变量捕获这个输出。那么outputs变量中有什么呢?
BERT 模型的输出是两个元素的元组。让我们打印输出对的形状:
outputs[0].shape
(1, 10, 768)
outputs[1].shape
(1, 768)
输出的第一个元素是形状 (batch size, sequence length, hidden size)。我们只输入了一个句子,因此这里的批大小是 1(批大小是我们一次输入给模型的句子数量)。这里的序列长度是 10,因为我们向分词器输入了 max_length=10,并将我们的句子填充到长度为 10。hidden_size 是 BERT 的一个参数。在 BERT 架构 部分,我们已经提到 BERT 的隐藏层大小是 768,因此产生具有 768 维度的词向量。所以,第一个输出元素包含每个词的 768 维向量,因此它包含 10 个词 x 768 维向量。
第二个输出是一个 768 维的向量。这个向量基本上是 [CLS] 标记的词嵌入。回想一下 BERT 输入格式 部分,[CLS] 标记是整个句子的聚合。你可以将 [CLS] 标记的嵌入视为句子中所有词嵌入的汇总版本。输出元组的第二个元素的形状始终是 (batch size, hidden_size)。基本上,我们为每个输入句子收集 [CLS] 标记的嵌入。
太棒了!我们已经提取了 BERT 嵌入。接下来,我们将使用这些嵌入来用 TensorFlow 和 tf.keras 训练我们的文本分类模型。
使用 BERT 进行文本分类
在本节中,我们将使用 BERT 和 tf.keras 训练一个二进制文本分类器。我们将重用前一章中的一些代码,但这次代码会更短,因为我们将以 BERT 替换嵌入和 LSTM 层。完整的代码可在 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter09/BERT_spam.ipynb.ipynb。在本节中,我们将跳过数据准备。我们使用了 Kaggle 的 SMS Spam Collection 数据集。你还可以在 GitHub 仓库的 data/ 目录下找到该数据集。
让我们从导入 BERT 模型和分词器开始:
from transformers import BertTokenizer, TFBertModel
bert_tokenizer =\
BertTokenizer.from_pretrained("bert-base-uncased")
bmodel = TFBertModel.from_pretrained("bert-base-uncased")
我们已经导入了 BertTokenizer 分词器和 BERT 模型,TFBertModel。我们使用预训练的 bert-base-uncased 模型初始化了分词器和 BERT 模型。请注意,模型的名称以 TF 开头——所有 HuggingFace 预训练模型的 TensorFlow 版本名称都以 TF 开头。当你未来想要玩转其他 transformer 模型时,请注意这个细节。
我们还将导入 Keras 层和函数,以及 numpy:
import numpy as np
import tensorflow
from tensorflow.keras.layers import Dense, Input
from tensorflow.keras.models import Model
现在,我们准备好使用 BertTokenizer 处理输入数据:
input_ids=[]
for sent in sentences:
bert_inp=bert_tokenizer.encode_plus(sent,add_special_tokens = True,max_length =64,pad_to_max_length = True,return_attention_mask = True)
input_ids.append(bert_inp['input_ids'])
input_ids=np.asarray(input_ids)
labels=np.array(labels)
正如我们在 使用 BERT 分词器 部分中看到的,这段代码将为数据集中的每个输入句子生成标记 ID,并将它们追加到一个列表中。标签是类标签的列表,由 0 和 1 组成。然后我们将 Python 列表 input_ids 和标签转换为 numpy 数组,以便将它们输入到我们的 Keras 模型中。
最后,我们通过以下几行代码定义了我们的 Keras 模型:
inputs = Input(shape=(64,), dtype="int32")
bert = bmodel(inputs)
bert = bert[1]
outputs = Dense(units=1, activation="sigmoid")(bert)
model = Model(inputs, outputs)
就这样!我们只用了五行代码就定义了基于 BERT 的文本分类器!让我们分析一下代码:
-
首先,我们定义了输入层,它将句子输入到我们的模型中。形状是
(64,),因为每个输入句子长度为 64 个标记。当我们调用encode_plus方法时,我们将每个句子填充到 64 个标记的长度。 -
接下来,我们将输入句子输入到 BERT 模型中。
-
在第三行,我们提取了 BERT 输出的第二个输出。回想一下,BERT 模型的输出是一个元组。输出元组的第一个元素是一系列词向量,第二个元素是一个代表整个句子的单个向量,称为
bert[1]提取了池化后的输出向量;这是一个形状为(1, 768)的向量。 -
接下来,我们通过 sigmoid 函数将池化后的输出向量压缩为形状为 1 的向量,这就是类别标签。
-
我们使用输入和输出定义了我们的 Keras 模型。
在这里,BERT 模型只需一行代码就能将维基语料库的巨大知识迁移到你的模型中。在训练结束时,这个模型达到了0.96的准确率。我们通常只进行一个 epoch 的训练,因为 BERT 即使在中等规模的语料库上也容易过拟合。
其余的代码处理了 Keras 模型的编译和拟合。请注意,BERT 有巨大的内存需求。您可以从谷歌研究 GitHub 链接中查看所需的 RAM 量:github.com/google-research/bert#out-of-memory-issues。
如果你在机器上运行本节代码时遇到困难,可以使用Google Colab,它通过浏览器提供 Jupyter 笔记本环境。您可以通过colab.research.google.com/notebooks/intro.ipynb立即开始使用 Google Colab。我们的训练代码在 Google Colab 上大约运行 1.5 小时,尽管只是进行一个 epoch,但更大的数据集可能需要更多时间。
在本节中,我们学习了如何从头开始训练一个 Keras 模型使用 BERT。现在,我们将转向一个更简单的任务。我们将探讨如何使用预训练的 transformer 流水线。让我们进入下一节以获取详细信息。
使用 Transformer 流水线
HuggingFace Transformers 库提供了流水线,帮助开发者立即从 transformer 代码中受益,而无需任何自定义训练。流水线是一个分词器和一个预训练模型的组合。
HuggingFace 为各种 NLP 任务提供了各种模型。以下是一些 HuggingFace 流水线提供的任务:
-
情感分析
-
问答
-
命名实体识别
-
文本摘要
-
翻译
您可以在 Huggingface 文档中查看所有任务的完整列表:huggingface.co/transformers/task_summary.html。在本节中,我们将探讨情感分析和问答(与其他任务的管道使用类似)的管道。
让我们通过一些例子来了解一下。我们将从情感分析开始:
from transformers import pipeline
nlp = pipeline("sentiment-analysis")
sent1 = "I hate you so much right now."
sent2 = "I love fresh air and exercising."
result1 = nlp(sent1)
result2 = nlp(sent2)
在前面的代码片段中,我们采取了以下步骤:
-
首先,我们从
transformers库中导入了管道函数。此函数通过将任务名称作为参数创建管道对象。因此,我们在第二行通过调用此函数创建了我们的情感分析管道对象nlp。 -
接下来,我们定义了两个具有负面和正面情感的示例句子。
-
然后,我们将这些句子输入到管道对象
nlp中。
这里是输出结果:
result1
[{'label': 'NEGATIVE', 'score': 0.9984998}]
result2
[{'label': 'POSITIVE', 'score': 0.99987185}]
这效果非常好!接下来,我们将尝试问答。让我们看看代码:
from transformers import pipeline
nlp = pipeline("question-answering")
res = nlp({
'question': 'What is the name of this book?',
'context': "I'll publish my new book Mastering spaCy soon."
})
print(res)
{'score': 0.0007240351873990664, 'start': 25, 'end': 40, 'answer': 'Mastering spaCy'}
再次,我们导入了管道函数并使用它创建了一个管道对象,nlp。在问答任务中,我们需要向模型提供上下文(模型工作的相同背景信息)以及我们的问题。在提供我们即将出版的新作品即将问世的信息后,我们询问了模型关于这本书的名称。答案是 Mastering spaCy;在这个对儿上,transformer 真是创造了奇迹!我们鼓励您尝试自己的例子。
我们已经完成了对 HuggingFace Transformers 的探索。现在,我们将进入本章的最后一部分,看看 spaCy 在 Transformer 方面能为我们提供什么。
Transformers 和 spaCy
spaCy v3.0 伴随着许多新功能和组件的发布。无疑,最令人兴奋的新功能是基于 Transformer 的管道。新的基于 Transformer 的管道将 spaCy 的准确性提升到了业界领先水平。将 Transformer 集成到 spaCy NLP 管道中引入了一个名为Transformer的额外管道组件。此组件允许我们使用所有 HuggingFace 模型与 spaCy 管道一起使用。如果我们回想一下第二章,spaCy 的核心操作,这是没有 Transformer 的 spaCy NLP 管道的样子:



随着 v3.0 的发布,v2 风格的 spaCy 模型仍然得到支持,并引入了基于 Transformer 的模型。一个基于 Transformer 的管道组件看起来如下:



对于每个支持的语言,基于 Transformer 的模型和 v2 风格模型都在文档的“模型”页面下列出(以英语为例:spacy.io/models/en)。基于 Transformer 的模型可以有不同的尺寸和管道组件,就像 v2 风格模型一样。此外,每个模型也有语料库和体裁信息,就像 v2 风格模型一样。以下是“模型”页面上的一个英语基于 Transformer 的语言模型示例:


图 9.13 – spaCy 英语基于 Transformer 的语言模型
如我们从前面的截图中所见,第一个管道组件是一个 Transformer,其余的管道组件是我们已经在第三章、“语言特性”中介绍过的。Transformer 组件生成词表示,并处理 WordPiece 算法将单词分解为子词。词向量被输入到管道的其余部分。
下载、加载和使用基于 Transformer 的模型与 v2 风格模型相同。目前,英语有两种预训练的基于 Transformer 的模型,en_core_web_trf和en_core_web_lg。让我们从下载en_core_web_trf模型开始:
python3 -m spacy download en_core_web_trf
这应该产生类似于以下输出的结果:
Collecting en-core-web-trf==3.0.0
Downloading https://github.com/explosion/spacy-models/releases/download/en_core_web_trf-3.0.0/en_core_web_trf-3.0.0-py3-none-any.whl (459.7 MB)
|████████████████████████████████| 459.7 MB 40 kB/s
Requirement already satisfied: spacy<3.1.0,>=3.0.0 in /usr/local/lib/python3.6/dist-packages (from en-core-web-trf==3.0.0) (3.0.5)
一旦模型下载完成,应该生成以下输出:
Successfully installed en-core-web-trf-3.0.0 spacy-alignments-0.8.3 spacy-transformers-1.0.2 tokenizers-0.10.2 transformers-4.5.1
Download and installation successful
You can now load the package via spacy.load('en_core_web_trf')
加载基于 Transformer 的模型与 v2 风格模型的做法相同:
import spacy
nlp = spacy.load("en_core_web_trf")
在加载我们的模型并初始化管道后,我们可以像使用 v2 风格模型一样使用这个模型:
doc = nlp("I visited my friend Betty at her house.")
doc.ents
(Betty,)
for word in doc:
print(word.pos_, word.lemma_)
...
PRON I
VERB visit
PRON my
NOUN friend
PROPN Betty
ADP at
PRON her
NOUN house
PUNCT .
到目前为止一切顺利,但接下来有什么新内容呢?让我们来检查一些来自 Transformer 组件的新特性。我们可以通过doc._.trf_data.trf_data访问与 Transformer 组件相关的特性,它包含由 Transformer 生成的词片段、输入ids和向量。让我们逐一检查这些特性:
doc = nlp("It went there unwillingly.")
doc._.trf_data.wordpieces
WordpieceBatch(strings=[['<s>', 'It', 'Gwent', 'Gthere', 'Gunw', 'ill', 'ingly', '.', '</s>']], input_ids=array([[ 0, 243, 439, 89, 10963, 1873, 7790, 4, 2]]), attention_mask=array([[1, 1, 1, 1, 1, 1, 1, 1, 1]]), lengths=[9], token_type_ids=None)
在前面的输出中,我们看到五个元素:词片段、输入 ID、注意力掩码、长度和标记类型 ID。词片段是由 WordPiece 算法生成的子词。这个句子的词片段如下:
<s>
It
Gwent
Gthere
Gunw
Ill
ingly
.
</s>
在这里,<s>和</s>是特殊标记,用于句子的开头和结尾。单词unwillingly被分解为三个子词 – unw、ill和ingly。字符G用于标记词边界。没有G的标记是子词,例如前一个词片段列表中的ill和ingly(除了句子中的第一个单词,句子的第一个单词由<s>标记)。
接下来,我们必须查看input_ids。输入 ID 与我们之前在“使用 BERT 分词器”部分介绍的输入 ID 具有相同的意义。这些基本上是由 Transformer 的分词器分配的子词 ID。
注意力掩码是一个由 0 和 1 组成的列表,用于指示 Transformer 应关注哪些标记。0 对应于 PAD 标记,而所有其他标记都应该有一个相应的 1。
lengths 是将句子分解为子词后的句子长度。在这里,它显然是 9,但请注意,len(doc) 输出 5,而 spaCy 总是操作语言词汇。
token_type_ids 由 Transformer 标记化器用于标记两个句子输入任务(如问答)的句子边界。在这里,我们只提供了一个文本,因此此功能不适用。
我们可以看到,由 Transformer 生成的标记向量 doc._.trf_data.tensors 包含了 Transformer 的输出,每个单词的单词向量序列,以及池化输出向量(我们在 获取 BERT 单词向量 部分介绍了这些概念。如果你需要刷新记忆,请参考本节):
doc._.trf_data.tensors[0].shape
(1, 9, 768)
doc._.trf_data.tensors[1].shape
(1, 768)
元组的第一个元素是标记的向量。每个向量是 768 维的;因此 9 个单词产生 9 x 768 维的向量。元组的第二个元素是池化输出向量,它是输入句子的聚合表示,因此其形状为 1x768。
这标志着我们对基于 spaCy 的 Transformer 管道的探索结束。再次强调,spaCy 提供了用户友好的 API 和打包,即使是像 transformers 这样的复杂模型也不例外。Transformer 集成是使用 spaCy 进行 NLP 的另一个很好的理由。
摘要
你完成了一章关于 NLP 中一个非常热门主题的详尽章节。恭喜!在本章中,你首先学习了 Transformer 模型是什么以及迁移学习是什么。然后,你了解了常用的 Transformer 架构,BERT。你学习了架构细节和特定的输入格式,以及 BERT Tokenizer 和 WordPiece 算法。
接下来,你通过使用流行的 HuggingFace Transformers 库熟悉了 BERT 代码。你练习了在自定义数据集上使用 TensorFlow 和 Keras 对 BERT 进行微调,以完成情感分析任务。你还练习了使用预训练的 HuggingFace 管道来完成各种 NLP 任务,例如文本分类和问答。最后,你探索了新版本 spaCy(spaCy v3.0)与 Transformers 的集成。
到本章结束时,你已经完成了这本书中关于统计 NLP 的章节。现在你准备好将所学的一切结合起来构建一个现代 NLP 管道。让我们继续到下一章,看看我们如何使用我们新的统计技能!
第十章:整合一切:使用 spaCy 设计您的聊天机器人
在本章中,您将利用迄今为止所学的一切来设计一个聊天机器人。您将执行实体提取、意图识别和上下文处理。您将使用不同的句法和语义解析方式、实体提取和文本分类方法。
首先,您将探索我们将用于收集其中话语的语言信息的数据集。然后,您将通过结合 spaCy 的Matcher类进行实体提取。之后,您将使用两种不同的技术进行意图识别:基于模式的方法和 TensorFlow 和 Keras 的统计文本分类。您将训练一个字符级 LSTM 来分类话语意图。
最后一节是关于句子和对话级语义的章节。您将深入研究诸如代词消解、语法疑问句类型和区分主语和宾语等语义主题。
到本章结束时,您将准备好设计一个真正的聊天机器人自然语言理解(NLU)管道。您将通过结合之前章节中学到的内容——语言和统计——结合几个 spaCy 管道组件,如命名实体识别(NER)、依存句法分析器和词性标注器。
在本章中,我们将涵盖以下主要主题:
-
对话式人工智能简介
-
实体提取
-
意图识别
技术要求
在本章中,我们将使用 NumPy、TensorFlow 和 scikit-learn 以及 spaCy。您可以通过以下命令使用pip安装这些库:
pip install numpy
pip install tensorflow
pip install scikit-learn
您可以在本书的 GitHub 仓库中找到本章的代码和数据:github.com/PacktPublishing/Mastering-spaCy/tree/main/Chapter10。
对话式人工智能简介
我们欢迎您来到我们最后一章,也是非常激动人心的一章,您将使用 spaCy 和 TensorFlow 设计一个聊天机器人 NLU 管道。在本章中,您将学习从多轮聊天机器人-用户交互中提取意义的方法。通过学习和应用这些技术,您将迈入对话式人工智能开发的步伐。
在深入技术细节之前,有一个基本问题:什么是聊天机器人?我们可以在哪里找到它?对话式人工智能究竟是什么意思?
对话式人工智能(conversational AI)是机器学习的一个领域,旨在创建能够使用户与机器进行基于文本或语音交互的技术。聊天机器人、虚拟助手和语音助手是典型的对话式人工智能产品。
聊天机器人是一种设计用来在聊天应用中与人类进行对话的软件应用。聊天机器人在包括人力资源、市场营销和销售、银行和医疗保健在内的广泛商业领域以及个人、非商业领域(如闲聊)中都很受欢迎。许多商业公司,如 Sephora(Sephora 拥有两个聊天机器人——一个在 Facebook 消息平台上的虚拟化妆师聊天机器人和一个在 Facebook 消息平台上的客户服务聊天机器人)、IKEA(IKEA 有一个名为 Anna 的客户服务聊天机器人)、AccuWeather 等,都拥有客户服务和常见问题解答聊天机器人。
即时通讯服务如 Facebook Messenger 和 Telegram 为开发者提供了连接其机器人的接口。这些平台还为开发者提供了详细的指南,例如 Facebook Messenger API 文档:(developers.facebook.com/docs/messenger-platform/getting-started/quick-start/) 或 Telegram 机器人 API 文档:(core.telegram.org/bots)。
虚拟助手也是一种软件代理,它会在用户请求或提问时执行一些任务。一个著名的例子是亚马逊 Alexa。Alexa 是一个基于语音的虚拟助手,可以执行许多任务,包括播放音乐、设置闹钟、阅读有声读物、播放播客,以及提供关于天气、交通、体育等方面的实时信息。Alexa Home 可以控制连接的智能家居设备,并执行各种任务,例如开关灯光、控制车库门等。
其他知名的例子包括 Google Assistant 和 Siri。Siri 集成在苹果公司的多个产品中,包括 iPhone、iPad、iPod 和 macOS。在 iPhone 上,Siri 可以进行电话拨打、接听电话,以及发送和接收短信以及 WhatsApp 消息。Google Assistant 也可以执行各种任务,例如提供实时航班、天气和交通信息;发送和接收短信;设置闹钟;提供设备电池信息;检查您的电子邮件收件箱;与智能家居设备集成等。Google Assistant 可在 Google Maps、Google Search 以及独立的 Android 和 iOS 应用程序中使用。
以下是一些最受欢迎和知名的虚拟助手列表,以给您更多关于市场上有哪些产品的想法:
-
Amazon Alexa
-
阿里巴巴集团的 AllGenie
-
三星公司的 Bixby
-
华为公司的 Celia
-
百度公司的 Duer
-
Google Assistant
-
微软公司的 Cortana
-
苹果公司的 Siri
-
腾讯公司的 Xiaowei
所有这些虚拟助手都是基于语音的,通常通过一个唤醒词来激活。唤醒词是一个特殊的词或短语,用于激活语音助手。例如,“嘿,Alexa”、“嘿,Google”和“嘿,Siri”,分别是亚马逊 Alexa、谷歌助手和 Siri 的唤醒词。如果您想了解更多关于这些产品的开发细节,请参阅本章的参考文献部分。
现在,我们来探讨一下技术细节。这些产品的 NLP 组件是什么?让我们详细看看这些 NLP 组件。
对话式人工智能产品的 NLP 组件
一个典型的基于语音的对话式人工智能产品包括以下组件:
-
语音转文本组件:将用户语音转换为文本。该组件的输入是一个 WAV/mp3 文件,输出是一个包含用户话语的文本文件。
-
对话式 NLU 组件:这个组件对用户话语文本执行意图识别和实体提取。输出是用户意图和实体列表。在当前话语中解决对先前话语的引用是在这个组件中完成的(请参阅指代消解部分)。
-
对话管理器:保持对话记忆,以进行有意义的连贯对话。您可以把这个组件看作是对话记忆,因为这个组件通常保存一个对话状态。对话状态是对话的状态:到目前为止出现的实体、到目前为止出现的意图等等。该组件的输入是先前的对话状态和当前用户解析的带有意图和实体的内容。该组件的输出是新的对话状态。
-
回答生成器:根据前几个阶段的全部输入,生成系统对用户话语的响应。
-
文本转语音:这个组件将系统的答案生成语音文件(WAV 或 mp3)。
每个组件都是单独训练和评估的。例如,语音转文本组件是在标注的语音语料库上训练的(训练是在语音文件和相应的转录上进行的)。NLU 组件是在意图和实体标注的语料库上训练的(类似于我们在第 6、7、8 和 9 章中使用的数据集)。在本章中,我们将重点关注 NLU 组件的任务。对于基于文本的产品,第一个和最后一个组件是不必要的,通常被电子邮件或聊天客户端集成所取代。
另一种被称为端到端语音理解(SLU)的范式。在 SLU 架构中,系统是端到端训练的,这意味着输入到系统的是语音文件,输出是系统的响应。每种方法都有其优缺点;您可以参考参考文献部分获取更多资料。
作为本书的作者,我很高兴能结合我的领域经验向您介绍这一章节。我在对话式人工智能领域工作了一段时间,并且每天都在为我们的产品解决语言和语音处理方面的挑战。我和我的同事们正在构建世界上第一个驾驶员数字助手 Chris(技巧与窍门:如何与 Chris 交流 – 基本语音命令,www.youtube.com/watch?v=Qwnjszu3exY)。Chris 可以打电话、接听来电、阅读和发送 WhatsApp 和短信、播放音乐、导航和闲聊。以下是 Chris:

图 10.1 – 车载语音助手 Chris(这是作者正在开发的产品)
从前面的例子中我们可以看到,对话式人工智能最近已经成为一个热门话题。作为一名自然语言处理专业人士,你很可能会在一个对话产品或相关领域工作,比如语音识别、文本转语音或问答。本章中介绍的技术,如意图识别、实体提取和代词消解,也适用于广泛的 NLU 问题。让我们深入技术部分。我们将从探索本章中我们将使用的整个数据集开始。
了解数据集
在第六章、第七章、第八章和第九章中,我们针对文本分类和实体提取目的使用了著名的真实世界数据集。在这些章节中,我们总是将数据集探索作为首要任务。数据探索的主要目的是为了了解数据集文本的性质,以便在我们的算法中制定应对该数据集的策略。如果我们回顾一下*第六章**,使用 spaCy 进行语义解析:整合一切,以下是我们探索过程中应该关注的主要点:
-
有哪些类型的语句?是简短的文本、完整的句子、长段落还是文档?语句的平均长度是多少?
-
语料库包含哪些实体?人名、组织名、地理位置、街道名?我们想要提取哪些?
-
标点符号是如何使用的?文本是否正确使用了标点,或者完全没有使用标点?
-
语法规则是如何遵循的?大写是否正确,用户是否遵循了语法规则?是否有拼写错误?
我们之前使用的语料库由(text, class_label)对组成,用于文本分类任务,或者由(text, list_of_entities)对组成,用于实体提取任务。在本章中,我们将处理一个更复杂的任务,即聊天机器人设计。因此,数据集将更加结构化和复杂。
聊天机器人设计数据集通常以 JSON 格式存储,以保持数据集结构。在这里,结构意味着以下内容:
-
保持用户和系统话语的顺序
-
标记用户话语的槽位
-
标注用户话语的意图
在本章中,我们将使用谷歌研究团队的《Schema-Guided Dialogue》数据集(SGD)(github.com/google-research-datasets/dstc8-schema-guided-dialogue)。该数据集包含标注的用户与虚拟助手交互。原始数据集包含超过 20,000 个对话片段,涉及多个领域,包括餐厅预订、电影预订、天气查询和旅行票务预订。对话包括用户和虚拟助手的轮流话语。在本章中,我们不会使用这个庞大的数据集的全部;相反,我们将使用关于餐厅预订的子集。
让我们开始下载数据集。你可以从本书的 GitHub 仓库下载数据集:github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter10/data/restaurants.json。或者,你可以编写以下代码:
$ wget https://github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter10/data/restaurants.json
如果你用文本编辑器打开文件并查看前几行,你会看到以下内容:
{
"dialogue_id": "1_00000",
"turns": [
{
"speaker": "USER",
"utterance": "I am feeling hungry so I would like to find a place to eat.",
"slots": [],
"intent": "FindRestaurants"
},
{
"speaker": "SYSTEM",
"utterance": "Do you have a specific which you want the eating place to be located at?",
"slots": []
}
首先,数据集由对话片段组成,每个对话片段都有一个dialogue_id实例。每个对话片段是一个有序的轮流列表,每个轮流属于用户或系统。turns字段是一个用户/系统轮流的列表。turns列表的每个元素都是一个轮流。一个轮流包括说话者(用户或系统)、说话者的话语、槽位列表和用户话语的意图。
下面是从数据集中的一些示例用户话语:
Hi. I'd like to find a place to eat.
I want some ramen, I'm really craving it. Can you find me an afforadable place in Morgan Hill?
I would like for it to be in San Jose.
Yes, please make a reservation for me.
No, Thanks
Hi i need a help, i am very hungry, I am looking for a restaurant
Yes, on the 7th for four people.
No. Can you change it to 1 pm on the 9th?
Yes. What is the phone number? Can I buy alcohol there?
从这些示例话语中我们可以看到,用户话语中使用了大写字母和标点符号。用户可能会犯拼写错误,例如第二句话中的单词afforadable。还有一些语法错误,例如第五句话中Thanks一词首字母大写的错误。另一个大写错误发生在第六句话中,其中代词I被错误地写成了i两次。
此外,一个话语可以包含多个句子。第一个话语以问候句开始,最后两个句子各自以肯定或否定回答句开始。第四句话也以Yes开始,但不是作为一个独立的句子;相反,它与第二个句子用逗号隔开。
对于多句话语的意图识别,这是我们通常需要注意的一个点——这类话语可以包含多个意图。此外,对于多句话语的答案生成也有些棘手;有时我们只需要生成一个答案(例如,对于前面代码中的第二句话)或有时我们需要为每个用户句子生成一个答案(例如,对于前面代码中的最后一句话)。
这是一个餐厅预订数据集,因此它自然包括用户话语中的某些槽位,如位置、菜系、时间、日期、人数等。我们的数据集包括以下槽位:
city
cuisine
date
phone_number
restaurant_name
street_address
time
以下是包含先前槽位类型及其值的示例句子:
Find me Ethiopian/cuisine cuisine in Berkeley/city.
The phone number is 707-421-0835/phone_number. Your reservation is confirmed.
No, change the time to 7 pm/time and for one person only.
No, change it on next friday/date.
现在,我们来讨论意图识别的类别标签及其分布。以下是类别标签的分布:
552 FindRestaurants
625 ReserveRestaurant
56 NONE
NONE是表示对话结束或只是说谢谢的话语的特殊类别标签。这类话语通常与餐厅预订无关。意图为列出餐厅并获取信息的话语被标记为FindRestaurants类别标签,而包含预订意图的话语被标记为ReserveRestaurants。让我们看看每个类别的示例话语:
No, Thanks NONE
No, thank you very much. NONE
Nothing much. I'm good. NONE
I am feeling hungry so I would like to find a place to eat. FindRestaurants
Hi i need a help, i am very hungry, I am looking for a restaurant FindRestaurants
Ok, What is the address? How pricey are they? FindRestaurants
Please can you make the reservation ReserveRestaurant
That's good. Do they serve liquor and what is there number? ReserveRestaurant
Thank you so much for setting that up. ReserveRestaurant
我们注意到后续句子,例如第 6、8 和 9 句话,被标记为FindRestaurants和ReserveRestaurant意图。这些句子不直接包含寻找/预订的意图,但它们继续了关于寻找/预订餐厅的对话,并且仍然对餐厅/预订进行查询。因此,尽管这些句子中没有明确指出寻找/预订的动作,但意图仍然是寻找/预订餐厅。
就这样——我们使用本节初步工作收集了关于我们数据集的足够见解。有了这些见解,我们准备构建我们的 NLU 管道。我们将从提取用户话语实体开始。
实体提取
在本节中,我们将实现我们聊天机器人 NLU 管道的第一步,并从数据集话语中提取实体。以下是我们数据集中标记的实体:
city
date
time
phone_number
cuisine
restaurant_name
street_address
为了提取实体,我们将使用 spaCy NER 模型和 spaCy 的Matcher类。让我们先从提取city实体开始。
提取城市实体
我们首先提取city实体。我们将从回忆一些关于 spaCy NER 模型和实体标签的信息开始,这些信息来自第三章**,语言特征和*第六章**,使用 spaCy 进行语义解析:
-
首先,我们回忆一下 spaCy 中城市和国家的命名实体标签是
GPE。让我们再次询问 spaCy,GPE标签对应的是什么:import spacy nlp = spacy.load("en_core_web_md") spacy.explain("GPE") 'Countries, cities, states' -
其次,我们还回忆起我们可以通过
ents属性访问Doc对象的实体。我们可以找到以下标记为 spaCy NER 模型的实体:import spacy nlp = spacy.load("en_core_web_md") doc = nlp("Can you please confirm that you want to book a table for 2 at 11:30 am at the Bird restaurant in Palo Alto for today") doc.ents (2, 11:30 am, Bird, Palo Alto, today) for ent in doc.ents: print(ent.text, ent.label_) 2 CARDINAL 11:30 am TIME Bird PRODUCT Palo Alto GPE today DATE
在这个代码段中,我们通过调用doc.ents列出了这个话语中所有的命名实体。然后,我们通过调用ent.label_检查了实体标签。检查输出,我们看到这个话语包含五个实体 - 一个序数实体(2),一个TIME实体(11:30 am),一个PRODUCT实体(Bird,这不是餐厅的理想标签),一个CITY实体(Palo Alto),和一个DATE实体(today)。GPE类型的实体是我们想要的;Palo Alto是美国的一个城市,因此被 spaCy NER 模型标记为GPE。
书中github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter10/extract_city_ents.py的脚本在 GitHub 上输出了包含城市实体的所有话语。从该脚本的输出中,我们可以看到 spaCy NER 模型在这个语料库上的GPE实体表现非常好。我们不需要用我们的自定义数据训练 spaCy NER 模型。
我们提取了城市实体,我们的聊天机器人知道在哪个城市寻找餐厅。现在,我们将提取日期和时间,以便我们的聊天机器人能够进行真正的预订。
提取日期和时间实体
提取DATE和TIME实体与提取CITY实体类似,我们在上一节中看到了。我们再次回顾语料库话语,看看 spaCy NER 模型在从我们的语料库中提取DATE和TIME实体方面的成功程度。
让我们看看语料库中的几个示例话语:
import spacy
nlp = spacy.load("en_core_web_md")
sentences = [
"I will be eating there at 11:30 am so make it for then.",
"I'll reach there at 1:30 pm.",
"No, change it on next friday",
"Sure. Please confirm that the date is now next Friday and for 1 person.",
"I need to make it on Monday next week at half past 12 in the afternoon.",
"A quarter past 5 in the evening, please."
]
在下面的代码中,我们将提取这些示例话语的实体:
for sent in sentences:
doc = nlp(sent)
ents = doc.ents
print([(ent.text, ent.label_) for ent in ents])
[('11:30 am', 'TIME')]
[('1:30 pm', 'TIME')]
[('next friday', 'DATE')]
[('next Friday', 'DATE'), ('1', 'CARDINAL')]
[('Monday next week', 'DATE'), ('half past 12', 'DATE')]
[('A quarter past 5', 'DATE')]
[('the evening', 'TIME'), ('4:45', 'TIME')]
看起来不错!输出相当成功:
-
第一句和第二句中的时间实体
11:30 am和1:30 pm被成功提取。 -
第三句和第四句中的
DATE实体next friday和next Friday也被提取出来。请注意,第一个实体包含一个拼写错误:friday应该写成Friday - 尽管如此,spaCy NER 模型仍然成功提取了这个实体。 -
第五句包含了
DATE实体和TIME实体。我们可以将DATE实体Monday next week分为两部分:Monday- 一个星期几和next week- 一个相对日期(确切的日期取决于话语的日期)。这个实体由两个名词短语组成:Monday(名词)和next week(形容词名词)。spaCy 可以处理这样的多词实体。这个话语的时间实体half past 12也是一个多词实体。这个实体由一个名词(half),一个介词(past)和一个数字(12)组成。 -
对于第六个话语的多词
TIME实体A quarter past 5也是如此。以下是这个实体的依存句法树:

图 10.2 – 时间实体“5 点过一刻”的依存树
前面的例子确实看起来相当不错,但以下这些表述如何:
sentences = [
"Have a great day.",
"Have a nice day.",
"Have a good day",
"Have a wonderful day.",
"Have a sunny and nice day"
]
for sent in sentences:
doc = nlp(sent)
ents = doc.ents
print([(ent.text, ent.label_) for ent in ents])
[('a great day', 'DATE')]
[('a nice day', 'DATE')]
[]
[]
[]
哎呀呀 – 看起来我们有一些day,因为日期实体错误地被识别了。我们在这里能做什么?
幸运的是,这些错误匹配并没有形成像第三和第四句中的a good day和a wonderful day这样的模式。只有单词序列a great day和a nice day被标记为实体。然后,我们可以用以下两种模式过滤 spaCy NER 的结果:
sentence = 'Have a nice day.'
doc = nlp(sentence)
wrong_matches = ["a great day", "a nice day"]
date_ents = [ent for ent in doc.ents if ent.label_ == "DATE"]
date_ents = list(filter(lambda e: e.text not in wrong_matches, date_ents))
date_ents
[]
前面的代码块执行以下步骤:
-
首先,我们定义了一个我们不想被识别为
DATE实体的短语列表。 -
我们通过遍历
doc中的所有实体,并选择标签为DATE的实体,在第 3 行提取了 Doc 对象的DATE实体。 -
在下一行,我们过滤了不在
wrong_matches列表中的实体。 -
我们打印了结果。正如预期的那样,
date实体的最终结果是空列表。
太好了,我们已经提取了DATE、TIME和CITY实体。对于所有三种实体类型,我们直接使用了 spaCy NER 模型,因为 spaCy NER 可以识别日期、时间和地点实体。那么phone_number实体呢?spaCy NER 根本不包含这样的标签。所以,我们将使用一些Matcher类的技巧来处理这种实体类型。让我们提取电话号码。
提取电话号码
我们在第四章**,基于规则的匹配中进行了Matcher类在包含数字的实体上的实践。我们还可以从第四章**,基于规则的匹配中回忆起,匹配数字类型实体确实可能相当棘手;特别是提取电话号码需要特别注意。电话号码可以以不同的格式出现,包括带连字符的(212-44-44)、区号((312) 790 12 31)、国家及区号(+49 30 456 222),以及不同国家的数字位数。因此,我们通常检查以下几点:
-
语料库中的电话号码实体是以多少种国家格式编写的?
-
数字块是如何分隔的 – 是用连字符、空格,还是两者都用?
-
一些电话号码中是否有区号块?
-
一些电话号码中是否有国家代码块?
-
国家代码块是否以+或 00 开头,或者两种格式都使用?
让我们检查一些我们的电话号码实体,然后:
You can call them at 415-775-1800\. And they do not serve alcohol.
Their phone number is 408-374-3400 and they don't have live music.
Unfortunately no, they do not have live music, however here is the number: 510-558-8367.
所有电话类型的实体都出现在系统对话中。聊天机器人检索餐厅的电话号码并将其提供给用户。聊天机器人通过在数字块之间放置破折号来形成电话号码实体。此外,所有电话号码都采用美国电话号码格式。因此,电话号码格式是统一的,形式为 ddd-ddd-dddd。这对于定义匹配模式非常有用。我们只需定义一个模式,就可以匹配所有电话号码实体。
让我们先看看一个示例电话号码是如何进行分词的:
doc= nlp("The phone number is 707-766-7600.")
[token for token in doc]
[The, phone, number, is, 707, -, 766, -, 7600, .]
每个数字块都被分词为一个标记,每个破折号字符也被分词为一个标记。因此,在我们的匹配器模式中,我们将寻找一个由五个标记组成的序列:一个三位数,一个破折号,再次是一个三位数,再次是一个破折号,最后是一个四位数。然后,我们的匹配器模式应该看起来像这样:
{"SHAPE": "ddd"}, {"TEXT": "-"}, {"SHAPE": "ddd"}, {"TEXT": "-"}, {"SHAPE": "dddd"}
如果你还记得第四章中的“基于规则的匹配”,SHAPE 属性指的是标记形状。标记形状表示字符的形状:d 表示数字,X 表示大写字母,而 x 表示小写字母。因此,{"SHAPE": "ddd"} 表示由三个数字组成的标记。这个模式将匹配形式为 ddd-ddd-dddd 的五个标记。让我们用我们的全新模式对一个语料库对话进行尝试:
from spacy.matcher import Matcher
matcher = Matcher(nlp.vocab)
pattern = [{"SHAPE": "ddd"}, {"TEXT": "-"}, {"SHAPE": "ddd"}, {"TEXT": "-"}, {"SHAPE": "dddd"}]
matcher.add("usPhoneNum", [pattern])
doc= nlp("The phone number is 707-766-7600.")
matches = matcher(doc)
for mid, start, end in matches:
print(doc[start:end])
707-766-7600
哇!我们的新模式如预期地匹配了一个电话号码类型实体!现在,我们将处理菜系类型,以便我们的聊天机器人可以预订。让我们看看如何提取菜系类型。
提取菜系类型
提取菜系类型比提取人数或电话类型要容易得多;实际上,它类似于提取城市实体。我们可以直接使用 spaCy NER 标签来提取菜系类型 – NORP。NORP 实体标签指的是民族或政治团体:
spacy.explain("NORP")
'Nationalities or religious or political groups'
幸运的是,我们语料库中的菜名与国籍相吻合。因此,菜名被 spaCy 的 NER 标记为 NORP。
首先,让我们看看一些示例对话:
Is there a specific cuisine type you enjoy, such as Mexican, Italian or something else?
I usually like eating the American type of food.
Find me Ethiopian cuisine in Berkeley.
I'm looking for a Filipino place to eat.
I would like some Italian food.
Malaysian sounds good right now.
让我们提取这些对话的实体,并检查 spaCy 的 NER 标签如何将菜系类型标记如下:
for sent in sentences:
doc = nlp(sent
[(ent.text, ent.label_) for ent in doc.ents]
[('Mexican', 'NORP'), ('Italian', 'NORP')]
[('American', 'NORP')]
[('Ethiopian', 'NORP'), ('Berkeley', 'GPE')]
[('Filipino', 'NORP')]
[('Italian', 'NORP')]
[('Malaysian', 'NORP')]
现在,我们能够从用户对话中提取城市、日期和时间、人数和菜系实体。我们构建的命名实体提取模块的结果包含了聊天机器人需要提供给预订系统的所有信息。以下是一个带有提取实体的示例对话:
I'd like to reserve an Italian place for 4 people by tomorrow 19:00 in Berkeley.
{
entities: {
"cuisine": "Italian",
"date": "tomorrow",
"time": "19:00",
"number_people": 4,
"city": "Berkeley"
}
在这里,我们完成了语义解析的第一部分,即提取实体。完整的语义解析还需要一个意图。现在,我们将进入下一部分,使用 TensorFlow 和 Keras 进行意图识别。
意图识别
意图识别(也称为意图分类)是将预定义标签(意图)分类到用户表述的任务。意图分类基本上是文本分类。意图分类是一个已知且常见的 NLP 任务。GitHub 和 Kaggle 托管了许多意图分类数据集(请参阅参考文献部分以获取一些示例数据集的名称)。
在现实世界的聊天机器人应用中,我们首先确定聊天机器人必须运行的领域,例如金融和银行、医疗保健、市场营销等。然后我们执行以下循环动作:
-
我们确定了一组我们想要支持的意图,并准备了一个带有
(表述,标签)对的标记数据集。我们在该数据集上训练我们的意图分类器。 -
接下来,我们将我们的聊天机器人部署给用户,并收集真实用户数据。
-
然后我们检查我们的聊天机器人在真实用户数据上的表现。在这个阶段,通常我们会发现一些新的意图和一些聊天机器人未能识别的表述。我们将新的意图扩展到我们的意图集合中,将未识别的表述添加到我们的训练集中,并重新训练我们的意图分类器。
-
我们进入步骤 2并执行步骤 2-3,直到聊天机器人 NLU 的质量达到良好的准确度水平(> 0.95)。
我们的语料库是一个真实世界的语料库;它包含拼写错误和语法错误。在设计我们的意图分类器时——尤其是在进行基于模式的分类时——我们需要对这样的错误有足够的鲁棒性。
我们将分两步进行意图识别:基于模式的文本分类和统计文本分类。我们在第八章**,使用 spaCy 进行文本分类中看到了如何使用 TensorFlow 和 Keras 进行统计文本分类。在本节中,我们将再次使用 TensorFlow 和 Keras。在此之前,我们将了解如何设计基于模式的文本分类器。
基于模式的文本分类
基于模式的分类意味着通过将预定义的模式列表与文本匹配来对文本进行分类。我们将预编译的模式列表与表述进行比较,并检查是否存在匹配。
一个直接的例子是垃圾邮件分类。如果一个电子邮件包含以下模式之一,例如你中了彩票和我是尼日利亚王子,那么这封电子邮件应该被分类为垃圾邮件。基于模式的分类器与统计分类器结合使用,以提高整个系统的准确度。
与统计分类器不同,基于模式的分类器很容易构建。我们根本不需要在训练 TensorFlow 模型上花费任何努力。我们将从我们的语料库中编译一个模式列表,并将其提供给 Matcher。然后,Matcher 可以在表述中查找模式匹配。
要构建一个基于模式的分类器,我们首先需要收集一些模式。在本节中,我们将对带有NONE标签的表述进行分类。让我们先看看一些表述示例:
No, Thanks
No, thank you very much.
That is all thank you so much.
No, that is all.
Nope, that'll be all. Thanks!
No, that's okay.
No thanks. That's all I needed help with.
No. This should be enough for now.
No, thanks.
No, thanks a lot.
No, thats all thanks.
通过观察这些话语,我们看到带有 NONE 标签的话语遵循一些模式:
-
大多数话语都以
No,或No.开头。 -
说
thank you的模式也很常见。模式Thanks、thank you和thanks a lot在前面代码的大多数话语中都出现过。 -
一些辅助短语,例如
that is all、that'll be all、that's OK和this should be enough也常被使用。
基于这些信息,我们可以创建以下三个 Matcher 模式:
[{"LOWER": {"IN": ["no", "nope"]}}, {"TEXT": {"IN": [",", "."]}}]
[{"TEXT": {"REGEX": "[Tt]hanks?"}}, {"LOWER": {"IN": ["you", "a lot"]}, "OP": "*"}]
[{"LOWER": {"IN": ["that", "that's", "thats", "that'll",
"thatll"]}}, {"LOWER": {"IN": ["is", "will"]}, "OP": "*"}, {"LOWER": "all"}]
让我们逐个分析这些模式:
-
第一个模式匹配标记序列
no,、no.、nope,、nope.、No,、No.、Nope,和Nope.。第一个项目匹配两个标记no和nope,无论是大写还是小写。第二个项目匹配标点符号,和.。 -
第二个模式匹配
thank、thank you、thanks和thanks a lot,无论是大写还是小写。第一个项目匹配thank和thankss?。在正则表达式语法中,s字符是可选的。第二个项目对应于单词you和a lot,它们可能跟在thanks?之后。第二个项目是可选的;因此,模式也匹配thanks和thank。我们使用了操作符OP: *来使第二个项目可选;回想一下 第四章**,基于规则的匹配,Matcher 支持不同操作符的语法,例如*、+和?。 -
第三个模式匹配标记序列
that is all、that's all、thats all等等。请注意,第一个项目包含一些拼写错误,例如thats和thatll。我们故意包含这些拼写错误,以便匹配对用户输入错误更加健壮。
前三个模式的组合将匹配 NONE 类的话语。你可以通过将它们添加到 Matcher 对象中来尝试这些模式,看看它们是如何匹配的。
高级技巧
在设计基于规则的系统时,始终牢记用户数据并不完美。用户数据包含拼写错误、语法错误和错误的字母大小写。始终将健壮性作为高优先级,并在用户数据上测试您的模式。
我们通过使用一些常见模式构建了一个无统计模型的分类器,并成功分类了一个意图。那么其他两个意图——FindRestaurants 和 ReserveRestaurant 呢?这些意图的话语在语义上要复杂得多,所以我们无法处理模式列表。我们需要统计模型来识别这两个意图。让我们继续使用 TensorFlow 和 Keras 训练我们的统计文本分类器。
使用字符级 LSTM 对文本进行分类
在本节中,我们将训练一个用于识别意图的字符级 LSTM 架构。我们已经在第八章**,使用 spaCy 进行文本分类中练习了文本分类。回顾本章内容,LSTM 是按顺序处理的模型,一次处理一个输入时间步。我们按照以下方式在每个时间步输入一个单词:
![Figure 10.3 – 在每个时间步向 LSTM 输入一个单词]

![Figure 10.3 – 在每个时间步向 LSTM 输入一个单词]
正如我们在第八章**,使用 spaCy 进行文本分类中提到的,LSTM 有一个内部状态(你可以将其视为内存),因此 LSTM 可以通过在其内部状态中保持过去信息来模拟输入序列中的顺序依赖关系。
在本节中,我们将训练一个字符级 LSTM。正如其名所示,我们将逐字符输入话语,而不是逐词输入。每个话语将被表示为字符序列。在每个时间步,我们将输入一个字符。这就是从Figure 10.3中输入话语的样子:
![Figure 10.4 – 将句子 "我想吃意大利菜" 的前两个单词输入]

![Figure 10.4 – 将句子 "我想吃意大利菜" 的前两个单词输入]
我们注意到空格字符也被作为输入,因为空格字符也是话语的一部分;对于字符级任务,数字、空格和字母之间没有区别。
让我们开始构建 Keras 模型。在这里我们将跳过数据准备阶段。你可以在意图分类笔记本github.com/PacktPublishing/Mastering-spaCy/blob/main/Chapter10/Intent-classifier-char-LSTM.ipynb中找到完整的代码。
我们将直接使用 Keras 的 Tokenizer 来创建一个词汇表。回顾第八章**,使用 spaCy 进行文本分类,我们知道我们使用 Tokenizer 来完成以下操作:
-
从数据集句子中创建一个词汇表。
-
为数据集中的每个标记分配一个标记 ID。
-
将输入句子转换为标记 ID。
让我们看看如何执行每个步骤:
-
在第八章**,使用 spaCy 进行文本分类中,我们将句子分词为单词,并为单词分配标记 ID。这次,我们将输入句子分解为其字符,然后为字符分配标记 ID。Tokenizer 提供了一个名为
char_level的参数。以下是字符级分词的 Tokenizer 代码:from tensorflow.keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(char_level=True, lower=True) tokenizer.fit_on_texts(utterances) -
上述代码段将创建一个从输入字符中提取的词汇表。我们使用了
lower=True参数,因此 Tokenizer 将输入句子的所有字符转换为小写。在初始化我们的词汇表上的Tokenizer对象后,我们现在可以检查其词汇表。以下是 Tokenizer 词汇表的前 10 项:tokenizer.word_index {' ': 1, 'e': 2, 'a': 3, 't': 4, 'o': 5, 'n': 6, 'i': 7, 'r': 8, 's': 9, 'h': 10}正如与词级词汇表一样,索引
0被保留用于一个特殊标记,即填充字符。回想一下第八章**,使用 spaCy 进行文本分类,Keras 无法处理变长序列;数据集中的每个句子都应该具有相同的长度。因此,我们通过在句子末尾或句子开头添加填充字符来将所有句子填充到最大长度。 -
接下来,我们将每个数据集句子转换为标记 ID。这是通过调用 Tokenizer 的
texts_to_sequences方法实现的:utterances = tokenizer.texts_to_sequences(utterances) utterances[0] [17, 2, 9, 25, 1, 7, 1, 22, 3, 6, 4, 1, 7, 4, 1, 5, 6, 1, 4, 10, 2, 1, 28, 28, 4, 10] -
接下来,我们将所有输入句子填充到长度为
150:MAX_LEN = 150 utterances =\ pad_sequences(utterances, MAX_LEN, padding="post")我们准备好将转换后的数据集输入到我们的 LSTM 模型中。我们的模型简单而非常高效:我们在双向 LSTM 层之上放置了一个密集层。以下是模型架构:
utt_in = Input(shape=(MAX_LEN,)) embedding_layer = Embedding(input_dim = len(tokenizer.word_index)+1, output_dim = 100, input_length=MAX_LEN) lstm =\ Bidirectional(LSTM(units=100, return_sequences=False)) utt_embedding = embedding_layer(utt_in) utt_encoded = lstm(utt_embedding) output = Dense(1, activation='sigmoid')(utt_encoded) model = Model(utt_in, output)双向 LSTM 层意味着两个 LSTM 堆叠在一起。第一个 LSTM 从左到右(正向)遍历输入序列,第二个 LSTM 从右到左(反向)遍历输入序列。对于每个时间步,正向 LSTM 和反向 LSTM 的输出被连接起来生成一个单独的输出向量。以下图展示了我们的具有双向 LSTM 的架构:
![Figure 10.5 – 双向 LSTM 架构
![img/Figure_10_5.jpg]
图 10.5 – 双向 LSTM 架构
-
接下来,我们通过调用
model.fit来编译我们的模型并在我们的数据集上训练它:model.compile(loss = 'binary_crossentropy', optimizer = "adam", metrics=["accuracy"]) model.fit(utterances, labels, validation_split=0.1, epochs = 10, batch_size = 64)在这里,我们使用以下内容编译我们的模型:
a) 二元交叉熵损失,因为这是一个二元分类任务(我们有两个类别标签)。
b)
Adam优化器,它将通过调整训练步骤的大小来帮助训练过程更快地运行。请参阅参考文献部分和第八章**,使用 spaCy 进行文本分类,以获取有关Adam优化器的更多信息。c) 准确率作为我们的成功指标。准确率是通过比较预测标签与实际标签相等的频率来计算的。
在调整我们的模型后,我们的模型在验证集上给出了0.8226的准确率,这相当不错。
现在,只剩下一个问题:为什么我们这次选择训练一个字符级模型?字符级模型确实有一些优点:
-
字符级模型对拼写错误高度容忍。考虑一下拼写错误的单词charactr – 是否缺少字母e并不太影响整个句子的语义。对于我们的数据集,我们将从这种鲁棒性中受益,因为我们已经在数据集探索中看到了用户的拼写错误。
-
词汇量小于词级模型。字母表中的字符数(对于任何给定语言)是固定的且较低(最多 50 个字符,包括大写和小写字母、数字和一些标点符号);但一个语言中的单词数量要大得多。因此,模型大小可能会有所不同。主要区别在于嵌入层;嵌入表的大小为
(vocabulary_size, output_dim)(参见模型代码)。鉴于输出维度相同,与数千行相比,50 行确实很小。
在本节中,我们能够从话语中提取用户意图。意图识别是理解句子语义的主要步骤,但还有更多吗?在下一节中,我们将深入研究句子级和对话级语义。更多语义分析
这是一个专门关于聊天机器人 NLU 的章节。在本节中,我们将探索句子级语义和句法信息,以生成对输入话语的更深入理解,并提供答案生成的线索。
在本节的其余部分,请将答案生成组件视为黑盒。我们提供句子的语义分析,并根据这个语义分析生成答案。让我们先剖析句子语法,并检查话语的主语和对象。
区分主语和对象
回想一下第三章**,语言特征,一个句子有两个重要的语法成分:一个主语和一个对象。主语是执行句子动词所给动作的人或物:
Mary picked up her brother.
He was a great performer.
It was rainy on Sunday.
Who is responsible for this mess?
The cat is very cute.
Seeing you makes me happy.
主语可以是名词、代词或名词短语。
一个对象是动词所给动作执行的对象或人。一个对象可以是名词、代词或名词短语。以下是一些例子:
Lauren lost her book.
I gave her/direct object a book/indirect object.
到目前为止,一切顺利,但这个信息如何帮助我们进行聊天机器人 NLU?
提取主语和对象有助于我们理解句子结构,从而为句子的语义分析增加一个层次。句子主语和对象信息直接与答案生成相关。让我们看看我们数据集中的一些话语示例:
Where is this restaurant?
下图显示了此话语的依存句法分析。主语是名词短语这家餐厅:
![Figure 10.6 – 示例话语的依存句法分析
![Figure 10_6.jpg]
图 10.6 – 示例话语的依存句法分析
我们如何生成这个句子的答案?显然,答案应该以this restaurant(或它所指的餐厅)为主语。以下是一些可能的答案:
The restaurant is located at the corner of 5th Avenue and 7th Avenue.
The Bird is located at the corner of 5Th Avenue and 7Th Avenue.
如果用户将this restaurant放入宾语角色,答案会改变吗?让我们从数据集中取一些示例语句:
Do you know where this restaurant is?
Can you tell me where this restaurant is?
显然,用户再次询问餐厅的地址。系统需要提供餐厅地址信息。然而,这次,这些句子的主语是你:

图 10.7 – 第一句话的依存句法分析
这个问题的主语是你,所以答案可以以一个I开头。这是一个疑问句,因此答案可以以yes/no开头,或者答案可以直接提供餐厅的地址。以下句子都是可能的答案:
I can give the address. Here it is: 5th Avenue, no:2
Yes, of course. Here's the address: 5th Avenue, no:2
Here's the address: 5Th Avenue, no:2
同样的短语the restaurant作为主语或宾语并不影响用户的意图,但它会影响答案的句子结构。让我们更系统地看看信息。前例句子的语义分析如下:
{
utt: "Where is this restaurant?",
intent: "FindRestaurants",
entities: [],
structure: {
subjects: ["this restaurant"]
}
}
{
utt: "Do you know where is this restaurant is?",
intent: "FindRestaurants",
entities: [],
structure: {
subjects: ["you"]
}
}
当我们将这些语义分析输入到答案生成模块时,该模块可以通过考虑当前语句、对话历史、语句意图和语句的句子结构(目前仅考虑句子主语信息)来生成答案。
在这里,我们通过查看语句的依存树来提取语句的句子结构信息。依存句法分析能否为我们提供更多关于语句的信息?答案是肯定的。我们将在下一节中看到如何提取句子类型。
句子类型解析
在本节中,我们将提取用户语句的句子类型。语法有四种主要的句子类型,根据其目的进行分类:
Declarative: John saw Mary.
Interrogative: Can you go there?
Imperative: Go there immediately.
Exclamation: I'm excited too!
在聊天机器人 NLU 中,句子类型略有不同;我们根据主语和宾语的词性以及目的来分类句子。以下是聊天机器人 NLU 中使用的某些句子类型:
Question sentence
Imperative sentence
Wish sentence
让我们检查每种句子类型及其结构特性。我们首先从疑问句开始。
疑问句
当用户想要提问时,会使用疑问句。疑问句可以通过两种方式形成,要么使用疑问代词,要么将情态动词或助动词置于句首:
How did you go there?
Is this the book that you recommended?
因此,我们将疑问句分为两类,wh-疑问句和是非疑问句。正如其名所示,wh-疑问句以一个wh 词(wh 词指的是疑问代词,如哪里、什么、谁和如何)开头,而是非疑问句是通过使用情态动词或助动词来构成的。
这种分类如何帮助我们?从句法上讲,是/否问题应该用是或否来回答。因此,如果我们的聊天机器人 NLU 将一个是/否问题传递给答案生成模块,答案生成器应该评估这条信息并生成一个以是/否开头的答案。Wh-问题旨在获取关于主语或宾语的信息,因此答案生成模块应该提供关于句子主语或宾语的信息。考虑以下话语:
Where is this restaurant?
这个话语生成了以下依存句法分析:

图 10.8 – 示例 Wh-问题的依存句法分析
在这里,话语的主语是this restaurant;因此,答案生成器应该通过关联Where和this restaurant来生成答案。关于以下话语呢:
Which city is this restaurant located?
这个话语的依存句法分析如下:

图 10.9 -- 示例 Wh-问题的依存句法分析
在这里,句子结构略有不同。Which city是句子的主语,而this restaurant是子句的主语。在这里,答案生成模块应该通过关联which city和this restaurant来生成答案。
我们现在将转向祈使句类型。
祈使句
祈使句在聊天机器人用户的说话中相当常见。祈使句是通过将主要动词置于句首来构成的。以下是我们数据集中的一些话语示例:
Find me Ethiopian cuisine in Berkeley.
Find me a sushi place in Alameda.
Find a place in Vallejo with live music.
Please reserve me at 6:15 in the evening.
Reserve for six in the evening.
Reserve it for half past 1 in the afternoon.
如我们所见,祈使话语在用户话语中相当常见,因为它们简洁直接。我们可以通过查看单词的词性标注来识别这些类型的句子:要么第一个词是动词,要么句子以请开头,第二个词是动词。以下 Matcher 模式匹配祈使话语:
[{"POS": "VERB, "IS_SENT_START": True}]
[{"LOWER": "please", IS_SENT_START: True}, {"POS": "VERB"}]
答案生成器将如何处理这些类型的句子?祈使句通常包含生成答案的句法和语义元素;主要动词提供动作,通常后面跟着一系列宾语。以下是对话语Find me Ethiopian cuisine in Berkeley的解析示例:

图 10.10 – 示例话语的依存句法分析
从图中,我们可以看到这个句子的句法成分,如Find(动作),Ethiopian cuisine(一个宾语)和Berkeley(一个宾语)。这些成分为答案生成器提供了一个清晰的模板来生成对这个话语的答案:答案生成器应该向餐厅数据库查询Ethiopian cuisine和Berkeley的匹配项,并列出匹配的餐厅。
现在我们转向下一类句子,愿望句。让我们详细看看这些句子。
愿望句
愿望句在语义上与祈使句相似。区别在于句法:愿望句以诸如 I'd like to、Can I、Can you 和 May I 等短语开头,指向一个愿望。以下是我们数据集中的几个例子:
I'd like to make a reservation.
I would like to find somewhere to eat, preferably Asian food.
I'd love some Izakaya type food.
Can you find me somewhere to eat in Dublin?
Can we make it three people at 5:15 pm?
Can I make a reservation for 6 pm?
提取动词和宾语与我们对祈使句所做的工作类似,因此语义分析相当相似。
在提取句子类型后,我们可以将其包含到我们的语义分析结果中,如下所示:
{
utt: "Where is this restaurant?",
intent: "FindRestaurants",
entities: [],
structure: {
sentence_type: "wh-question",
subjects: ["this restaurant"]
}
}
现在,我们有了丰富的语义和句法表示的输入话语。在下一节中,我们将超越句子级语义,进入对话级语义。让我们继续到下一节,看看我们如何处理对话级语义。
代词消解
在本节中,我们将探讨语言学概念代词和连贯性。在语言学中,连贯性意味着将文本在语义上粘合在一起的语法联系。这个文本可以是一个单独的句子、一个段落或一个对话片段。考虑以下两个句子:
I didn't like this dress. Can I see another one
在这里,单词 one 指的是第一句话中的连衣裙。人类可以轻松地解决这个问题,但对于软件程序来说,这并不那么直接。
还要考虑以下对话片段:
Where are you going?
To my grandma's.
第二句话完全可以理解,尽管句子中的一些部分缺失:
I'm going to my grandma's house.
在书面和口头语言中,我们每天都在使用这样的捷径。然而,在编程时解决这些捷径需要引起注意,尤其是在聊天机器人自然语言理解(NLU)中。考虑以下来自我们数据集的话语和对话片段:
示例 1:
- Do you want to make a reservation?
- Yes, I want to make one.
示例 2:
- I've found 2 Malaysian restaurants in Cupertino. Merlion Restaurant & Bar is one.
- What is the other one?
示例 3:
- There's another restaurant in San Francisco that's called Bourbon Steak Restaurant.
- Yes, I'm interested in that one.
示例 4:
- Found 3 results, Asian pearl Seafood Restaurant is the best one in Fremont city, hope you like it.
- Yes, I like the same.
示例 5:
- Do you have a specific which you want the eating place to be located at?
- I would like for it to be in San Jose.
示例 6:
- Would you like a reservation?
- Yes make it for March 10th.
前面句子和对话中所有加粗的部分都是名为 one、more、same、it 等的语言事件的例子。代词消解意味着解决代词所指的确切短语。
我们如何将此信息应用到我们的聊天机器人 NLU 中呢?
首先,我们需要确定一个话语是否涉及代词消解,以及我们是否需要进行代词消解。再次考虑以下对话片段:
Do you want to make a reservation?
Yes, I want to make one.
第二个话语的依存句法分析如下:

图 10.11 – 示例话语的依存句法分析
首先,one 出现在句子的直接宾语位置,并且没有其他直接宾语。这意味着 one 应该是一个代词。为了解决 one 指代什么,我们将回顾对话的第一个话语。以下依存句法分析属于第一个话语,Do you want to make a reservation?:

图 10.12 – 示例话语的依存句法分析
如果我们查看图 10.11,我们会看到句子有一个直接宾语,“一个预订”,因此“一个”应该指代“一个预订”。然后,我们可以将生成的语义解析安排如下:
{
utt: "Where is this restaurant?",
intent: "ReserveRestaurant",
entities: [],
structure: {
sentence_type: "declarative",
subjects: ["one"]
anaphoras: {
"one": "a reservation"
}
}
}
将“一个”替换为“一个预订”可以使句子的意图更清晰。在我们的聊天机器人 NLU 中,我们只有两个意图,但如果有更多意图,比如预订取消、退款等,怎么办?那么“我想订一个”也可以意味着进行取消或获取退款。
因此,我们将指代词消解放在意图识别之前,并输入完整的句子,其中指代词被它们所指的短语所替换。这样,意图分类器接收到的句子中,直接宾语是一个名词短语,而不是“一个”、“相同的”、“它”或“更多”等单词,这些单词本身没有任何意义。
现在,在用 Keras 进行统计意义提取(通过提取意图)之后,在本节中,你学习了使用特殊 NLU 技术处理句子语法和语义的方法。你已经准备好结合你所知道的所有技术,为你的未来职业设计自己的聊天机器人 NLU。这本书从语言概念开始,继续到统计应用,在本章中,我们将它们全部结合起来。你已经准备好继续前进。在所有你将设计的 NLU 管道中,始终尝试从不同的角度看待问题,并记住你在本书中学到的内容。
摘要
就这样!你已经到达了这一详尽章节的结尾,也到达了这本书的结尾!
在本章中,我们设计了一个端到端聊天机器人 NLU 管道。作为第一个任务,我们探索了我们的数据集。通过这样做,我们收集了关于话语的语言学信息,并理解了槽位类型及其对应的值。然后,我们执行了聊天机器人 NLU 的一个重要任务——实体提取。我们使用 spaCy NER 模型以及 Matcher 提取了多种类型的实体,如城市、日期/时间和菜系。然后,我们执行了另一个传统的聊天机器人 NLU 管道任务——意图识别。我们使用 TensorFlow 和 Keras 训练了一个字符级 LSTM 模型。
在最后一节中,我们深入探讨了句子级和对话级语义。我们通过区分主语和宾语来处理句子语法,然后学习了句子类型,最后学习了指代词消解的语言学概念。我们通过结合几个 spaCy 管道组件(如 NER、依存句法分析器和词性标注器)所学的知识,在语言和统计上应用了之前章节的内容。
参考文献
这里为本章提供了一些参考文献:
关于语音助手产品:
-
Alexa 开发者博客:
developer.amazon.com/blogs/home/tag/Alexa -
Alexa 科学博客:
www.amazon.science/tag/alexa -
微软关于聊天机器人的出版物:
academic.microsoft.com/search?q=chatbot
Keras 层和优化器:
-
Keras 层:
keras.io/api/layers/ -
Keras 优化器:
keras.io/api/optimizers/ -
Adam 优化器:
arxiv.org/abs/1412.6980
对话式人工智能的数据集:
-
来自谷歌研究团队的 Taskmaster:
github.com/google-research-datasets/Taskmaster/tree/master/TM-1-2019 -
来自谷歌研究团队的模拟对话数据集:
github.com/google-research-datasets/simulated-dialogue -
来自微软的对话挑战数据集:
github.com/xiul-msr/e2e_dialog_challenge -
对话状态跟踪挑战数据集:
github.com/matthen/dstc







浙公网安备 33010602011771号