精通 Spacy(三)
原文:
annas-archive.org/md5/cab19785f4c5c60f7cc1edeb81e591d8译者:飞龙
第十章:使用 spaCy 训练实体链接器模型
实体链接是将文本提及映射到外部知识库中唯一标识符的 NLP 任务。本章将探讨如何使用 spaCy 训练实体链接模型,以及如何创建用于 NLP 训练的高质量数据集的最佳实践。我们还将学习如何使用自定义语料库读取器来训练 spaCy 组件。有了这些知识,你可以自定义任何 spaCy 组件,以便在训练模型时使用。
本章将涵盖以下内容:
-
理解实体链接在 NLP 中的概念和重要性
-
创建用于 NLP 训练的高质量数据集的最佳实践
-
使用 spaCy 训练实体链接器组件
-
利用自定义语料库读取器来训练 spaCy 组件
到本章结束时,你将能够开发与外部知识库集成的 NLP 模型,从而提高在现实世界场景中的准确性和适用性。
技术要求
本章的所有数据和代码都可以在github.com/PacktPublishing/Mastering-spaCy-Second-Edition找到。
理解实体链接任务
实体链接是将提到的实体与其在各个知识库中的对应条目进行关联的任务。例如,华盛顿实体可以指代人物乔治·华盛顿或美国的一个州。通过实体链接或实体解析,我们的目标是把实体映射到正确的现实世界表示。正如 spaCy 的文档所说,spaCy 的实体链接器架构需要三个主要组件:
-
一个用于存储唯一标识符、同义词和先验概率的知识库(
KB) -
一个用于生成可能标识符的候选生成步骤
-
一个用于从候选列表中选择最可能 ID 的机器学习模型
在 KB 中,每个文本提及(别名)都表示为一个可能或可能未链接到实体的候选对象。每个候选(别名,实体)对都分配一个先验概率。
在 spaCy 的实体链接器架构中,首先,我们使用共享的语言词汇表和固定大小实体向量的长度初始化一个 KB。然后,我们需要设置模型。spacy.EntityLinker.v2类使用spacy.HashEmbedCNN.v2模型作为默认的模型架构,这是 spaCy 的标准tok2vec层。
spacy.HashEmbedCNN.v2架构由一个多哈希嵌入嵌入层和一个最大输出窗口编码器编码层定义。
MultiHashEmbed 嵌入层使用子词特征,并构建一个嵌入层,该层使用哈希嵌入分别嵌入词汇属性("NORM"、"PREFIX"、"SUFFIX" 等)。哈希嵌入旨在解决嵌入大小的问题。在论文《用于高效词表示的哈希嵌入》(arxiv.org/pdf/1709.03933 )中,Svenstrup、Hansen 和 Winther 指出,即使是中等大小的嵌入尺寸(300 维度),如果词汇量很大(如 Google Word2Vec 中的 300 万个单词和短语),总参数数接近 10 亿。使用哈希嵌入是内存高效的,因为它是一个紧凑的数据结构,所需的存储空间比词袋表示少。
MaxoutWindowEncoder 编码层使用卷积来编码上下文。该层接受的两个主要参数是 window_size 和 depth。window_size 参数设置围绕每个标记连接的单词数量,以构建卷积。depth 参数设置卷积层的数量。
spacy.HashEmbedCNN.v2 和所有其他 spaCy 层都是使用 Thinc API 定义的。让我们看看定义这些层的代码(仅用于学习目的,因为我们只需要在 config.cfg 文件中指向这个定义):
@registry.architectures("spacy.HashEmbedCNN.v2")
def build_hash_embed_cnn_tok2vec(
*,
width: int,
depth: int,
embed_size: int,
window_size: int,
maxout_pieces: int,
subword_features: bool,
pretrained_vectors: Optional[bool],
) -> Model[List[Doc], List[Floats2d]]:
if subword_features:
attrs = ["NORM", "PREFIX", "SUFFIX", "SHAPE"]
row_sizes = [embed_size, embed_size // 2,
embed_size // 2, embed_size // 2]
else:
attrs = ["NORM"]
row_sizes = [embed_size]
return build_Tok2Vec_model(
embed=MultiHashEmbed(
width=width,
rows=row_sizes,
attrs=attrs,
include_static_vectors=bool(pretrained_vectors),
),
encode=MaxoutWindowEncoder(
width=width,
depth=depth,
window_size=window_size,
maxout_pieces=maxout_pieces,
),
)
我们可以看到,该层正在使用 MultiHashEmbed 来嵌入文本,并使用 MaxoutWindowEncoder 层来编码嵌入,为模型提供最终的线性输出。接下来,让我们看看 EntityLinker.v2 架构本身的代码:
@registry.architectures("spacy.EntityLinker.v2")
def build_nel_encoder(
tok2vec: Model, nO: Optional[int] = None
) -> Model[List[Doc], Floats2d]:
with Model.define_operators({">>": chain, "&": tuplify}):
token_width = tok2vec.maybe_get_dim("nO")
output_layer = Linear(nO=nO, nI=token_width)
model = (
((tok2vec >> list2ragged()) & build_span_maker())
>> extract_spans()
>> reduce_mean()
>> residual(Maxout(nO=token_width, nI=token_width,
nP=2, dropout=0.0)) # type: ignore
>> output_layer
)
model.set_ref("output_layer", output_layer)
model.set_ref("tok2vec", tok2vec)
# flag to show this isn't legacy
model.attrs["include_span_maker"] = True
return model
build_nel_encoder() 方法的参数是 tok2vec 模型和由 KB 中每个实体的编码向量的长度确定的输出维度 nO。当我们不设置 nO 时,它在调用 initialize 时会自动设置。我们将在 config.cfg 文件中定义 tok2vec 模型,如下所示:
[components.entity_linker.model]
@architectures = "spacy.EntityLinker.v2"
nO = null
[components.entity_linker.model.tok2vec]
@architectures = "spacy.HashEmbedCNN.v1"
pretrained_vectors = null
width = 96
depth = 2
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
现在您已经了解了 spaCy 和 Thinc 在底层如何交互以创建 EntityLinker 模型架构。如果您需要更改参数或尝试不同的模型,您知道去哪里更改这些设置。
在商业环境中,我们通常从没有可用的数据集开始,迫使我们创建自己的数据集。拥有高质量的数据集是训练性能良好的模型的关键组成部分,因此学习创建良好数据集的基本知识非常重要。让我们在下一节中讨论这个问题。
创建良好 NLP 语料库的最佳实践
由于微调技术的存在,幸运的是,我们今天不需要大量的数据来训练模型。然而,好的数据集仍然非常重要,因为它们对于保证和评估我们的 NLP 系统的性能至关重要。例如,如果未经仔细策划,语言中深深嵌入的偏见和文化细微差别可能会无意中塑造 AI 的输出。在文章《如何在不经意间制造出有种族歧视倾向的 AI》中(blog.conceptnet.io/posts/2017/how-to-make-a-racist-ai-without-really-trying/),研究人员罗宾·斯皮尔提供了一个关于情感分析的精彩教程,并展示了我们如何通过简单地重复使用在偏见数据上训练的嵌入来产生有种族歧视倾向的 AI 解决方案。
通常,NLP 任务的标注过程主要包括以下步骤:
-
定义问题或任务。我们试图解决什么问题?这个问题将指导我们如何选择用于标注的样本,如何一致性地标注数据,等等。
-
确定并准备一组代表性文本,作为语料库的起始材料。
-
定义一个标注方案,并对语料库的一部分进行标注,以确定数据解决任务的可行性。
-
标注语料库的大部分内容。
总是定义标注说明是一个好习惯,以确保数据集的标注一致性。这很重要,因为我们需要这种一致性,以便机器学习算法能够有效地泛化。安德鲁·吴在课程《生产中的机器学习》中讲述了标注过程中一致性的重要性(www.coursera.org/learn/introduction-to-machine-learning-in-production),其中,一个计算机视觉模型没有达到预期的性能,经过一些错误分析后,他们发现是由于标注过程中的不一致性。任务是寻找钢板图像中的缺陷,一些标注将整个有缺陷的区域标注为缺陷,而另一些则单独标注缺陷。
为了构建高质量的 NLP 数据集,我们可以从数据质量维度借用一些原则。一个好的标注数据集的第一个特征是一致性。数据在格式、标注和分类方面应该具有一致的结构和统一性。
一个好的数据集也应该代表目标应用领域。如果我们正在构建一个从公司业务文档中提取信息的管道,那么使用法律文本来训练嵌入模型可能不是一个好主意。
一个好的数据集的第三个也是最后一个特征是它有良好的文档记录。我们应该明确和透明地说明我们如何收集和选择数据,标签说明是什么,以及谁标注了数据。这种文档记录非常重要,因为它允许透明度、可重复性和偏差管理。
在接下来的章节中,我们创建了一个小数据集,我们创建了一个包含来自新闻网站提及我们想要消歧的实体(Taylor Swift、Taylor Lautner 和 Taylor Fritz)的句子的数据集。标签任务是标记新闻句子中的Taylor提及到每个提及的人。不幸的是,在现实生活中,我们通常不会遇到这样的简单场景,这些是我们遵循一致性、代表性和良好文档原则来构建数据集变得更为重要的时刻。
现在我们知道了如何创建语料库,让我们回到 spaCy,学习如何训练我们的实体链接器组件。
使用 spaCy 训练实体链接器组件
训练模型的第一个步骤是创建知识库。我们想要创建一个管道,用于检测对Taylor的引用是指 Taylor Swift(歌手)、Taylor Lautner(演员)还是 Taylor Fritz(网球运动员)。他们每个人在 Wikidata 上都有自己的页面和标识符,因此我们将使用 Wikidata 作为我们的知识库来源。要创建知识库,我们需要创建一个InMemoryLookupKB类的实例,传递共享的Vocab对象和我们将要用来编码实体的嵌入向量的大小。让我们创建我们的知识库:
-
首先,我们将选择语言对象(
en_core_web_md)并添加一个SpanRuler组件来匹配所有的taylor提及(这将用于创建语料库):import spacy nlp = spacy.load("en_core_web_md") ruler = nlp.add_pipe("span_ruler", after="ner") patterns = [{"label": "PERSON", "pattern": [{"LOWER": "taylor"}]}] ruler.add_patterns(patterns) -
我们将把知识库和模型保存到磁盘上,所以让我们定义这些文件:
kb_loc = "chapter_10/nel_taylor/my_kb" nlp_dir = "chapter_10/nel_taylor/my_nlp" -
最后,我们可以开始创建知识库。首先,我们实例化 kb 对象,传递
Vocab和向量的大小。en_core_web_md模型有 300 维的向量,所以我们为此设置实体向量的大小:import os from spacy.kb import InMemoryLookupKB kb = InMemoryLookupKB(vocab=nlp.vocab, entity_vector_length=300)
现在我们有了知识库,我们可以使用add_entity()方法添加实体,使用add_alias()方法添加提及(在我们的情况下,提及将是Taylor)以及每个实体的先验概率。让我们创建完成所有这些的代码:
-
首先,我们创建两个字典,一个包含我们每个实体的 Wikidata ID,另一个包含它们的描述:
entities = {'Q26876': 'Taylor Swift', 'Q23359': 'Taylor Lautner', 'Q17660516': 'Taylor Fritz'} descriptions = {'Q26876': 'American singer-songwriter (born 1989)', 'Q23359': 'American actor', 'Q17660516': 'American tennis player'} -
现在,是时候将实体添加到知识库中。每个 Wikidata QID 将有一个实体描述的向量表示。要添加实体,我们使用
add_entity()方法,目前我们可以为freq参数设置一个任意值(我们将告诉 spaCy 在训练模型的config.cfg文件中忽略这个频率):for qid, desc in descriptions.items(): desc_doc = nlp(desc) desc_vector = desc_doc.vector kb.add_entity(entity=qid, entity_vector=desc_vector, freq=111) -
现在,我们可以将提及(
alias)添加到知识库中。如果文本中出现Taylor Swift实体,我们毫无疑问它指的是歌手泰勒·斯威夫特。同样,如果文本中出现Taylor Lautner实体,我们毫无疑问它指的是演员泰勒·洛特纳。我们将通过将这些实体的概率设置为1来添加此信息到知识库中:for qid, name in entities.items(): kb.add_alias(alias=name, entities=[qid], probabilities=[1]) -
当实体的名字和姓氏都存在时,我们毫无疑问知道是谁,但如果文本只提到
Taylor呢?我们将为所有三个实体设置初始概率相等(每个实体 30%,因为概率之和不能超过 100%):qids = entities.keys() kb.add_alias(alias="Taylor", entities=qids, probabilities=[0.3, 0.3, 0.3]) -
我们的 KB 还没有完全设置好。让我们打印实体和别名来检查是否一切正常:
print(f"Entities in the KB: {kb.get_entity_strings()}") >>> Entities in the KB: ['Q23359', 'Q17660516', 'Q26876'] print(f"Aliases in the KB: {kb.get_alias_strings()}") >>> Aliases in the KB: ['Taylor Lautner', 'Taylor', 'Taylor Swift', 'Taylor Fritz'] -
现在,是时候将知识库和
nlp模型保存到磁盘上了,这样我们以后就可以使用它们:kb.to_disk(kb_loc) if not os.path.exists(nlp_dir): os.mkdir(nlp_dir) nlp.to_disk(nlp_dir)
在InMemoryLookupKB配置完成后,我们现在可以为 spaCy 准备训练数据。我们将处理的 CSV 文件包含名为text(句子)、person和label(实体的名称)、ent_start和ent_end(标记在句子中的位置)以及 Wikidata 的QID这些列。数据包含 49 个句子,我们将使用 80%进行训练(我们将将其分为train和dev集)和 20%进行测试。我们将分两步准备数据:首先,创建Doc对象,然后将其添加到DocBin的train和dev对象中。
为了创建Doc对象,我们将每个句子包装在一个Doc对象中,并使用 CSV 文件中的数据创建Span对象。这个Span对象有一个kb_id参数,我们将用它来设置实体的 Wikidata QID。让我们继续这样做:
-
首先,我们加载 CSV 文件,并获取 80%的行用于训练,其余的用于测试:
import pandas as pd df_labeled = pd.read_csv("https://raw.githubusercontent.com/PacktPublishing/Mastering-spaCy-Second-Edition/main/chapter_10/taylor_labeled_dataset.csv") df_train = df_labeled.sample(frac=0.8, random_state=123) df_test = df_labeled.drop(df_train.index) -
现在,我们实例化我们之前用于创建知识库的管道,将句子包装在它里面以创建
Doc对象,并为实体创建Span对象。我们将使用两个列表,一个用于存储docs,另一个用于存储QIDs:import spacy from spacy.tokens import Span from collections import Counter nlp_dir = "chapter_10/nel_taylor/my_nlp" nlp = spacy.load(nlp_dir) docs = [] QIDs = [] for _,row in df_train.iterrows(): sentence = row["text"] QID = row["QID"] span_start = row["ent_start"] span_end = row["ent_end"] doc = nlp(sentence) QIDs.append(QID) label_ent = "PERSON" ent_span = Span(doc, span_start, span_end, label_ent, kb_id=QID) doc.ents = [ent_span] docs.append(doc)
我们将使用QIDs列表将句子分割成train和dev的DocBin对象。为此,我们将获取每个 QID 的索引,使用前八个句子进行train,其余的留给dev。让我们在代码中这样做:
-
首先,我们导入对象并创建我们的空
DocBin对象:from spacy.tokens import DocBin import math train_docs = DocBin() dev_docs = DocBin() -
现在,我们遍历每个实体 QID,获取它们句子的索引,并将它们添加到每个
DocBin对象中:entities = {'Q26876': 'Taylor Swift', 'Q23359': 'Taylor Lautner', 'Q17660516': 'Taylor Fritz'} for QID in entities.keys(): indexes_sentences_qid = [i for i, j in enumerate(QIDs) if j == QID] for index in indexes_sentences_qid[0:8]: train_docs.add(docs[index]) for index in indexes_sentences_qid[8:]: dev_docs.add(docs[index]) -
现在我们可以将
DocBin文件保存到磁盘上:train_corpus = "chapter_10/nel_taylor/train.spacy" dev_corpus = "chapter_10/nel_taylor/dev.spacy" train_docs.to_disk(train_corpus) dev_docs.to_disk(dev_corpus)
当准备好train和dev集后,我们可以继续训练模型。我们将使用 spaCy 的 Nel Emerson 教程中使用的相同配置文件(github.com/explosion/projects/tree/v3/tutorials/nel_emerson)。您可以在 GitHub 仓库中获取此文件:github.com/PacktPublishing/Mastering-spaCy-Second-Edition/tree/main/chapter_10。
如果您需要刷新对 spaCy 训练过程的了解,可以参考第六章。为了训练EntityLinker组件,我们需要做一些新的事情,即使用一个包含用于训练的额外代码的自定义文件。我们将这样做,因为我们需要以EntityLinker需要的方式创建Example对象。让我们在下一节中更多关于这一点进行讨论。
使用自定义语料库读取器进行训练
spaCy 的Corpus类管理用于训练期间数据加载的标注语料库。默认的语料库读取器(spacy.Corpus.v1)使用Language类的make_doc()方法创建Example对象。此方法仅对文本进行分词。为了训练EntityLinker组件,它需要在文档中具有可用的实体。这就是为什么我们将创建自己的语料库读取器,并将其保存为名为custom_functions.py的文件。读取器应接收DocBin文件的路径和nlp对象作为参数。在方法内部,我们将遍历每个Doc以创建示例。让我们继续创建这个方法:
-
首先,我们禁用管道中的
EntityLinker组件,然后从DocBin文件中获取所有文档:def read_files(file: Path, nlp: "Language") -> Iterable[Example]: with nlp.select_pipes(disable="entity_linker"): doc_bin = DocBin().from_disk(file) docs = doc_bin.get_docs(nlp.vocab) -
现在,我们将为每个文档创建
Example对象。Example的第一个参数是经过nlp对象处理的文本,第二个参数是带有标注实体的doc:# ... for doc in docs: yield Example(nlp(doc.text), doc) -
为了在
config.cfg文件中引用此读取器,我们需要使用@spacy.registry装饰器进行注册。让我们导入我们将需要的库并注册读取器:from functools import partial from pathlib import Path from typing import Iterable, Callable import spacy from spacy.training import Example from spacy.tokens import DocBin @spacy.registry.readers("MyCorpus.v1") def create_docbin_reader(file: Path) -> Callable[["Language"], Iterable[Example]]: return partial(read_files, file) -
我们使用
partial函数,这样当 spaCy 代码在内部使用读取器时,我们只需要传递nlp对象。现在,我们可以在config.cfg文件中像这样引用这个MyCorpus.v1读取器:#config.cfg snippet [corpora] [corpora.train] @readers = "MyCorpus.v1" file = ${paths.train} [corpora.dev] @readers = "MyCorpus.v1" file = ${paths.dev} -
这里是我们的
custom_functions.py文件的完整源代码:from functools import partial from pathlib import Path from typing import Iterable, Callable import spacy from spacy.training import Example from spacy.tokens import DocBin @spacy.registry.readers("MyCorpus.v1") def create_docbin_reader(file: Path) -> Callable[["Language"], Iterable[Example]]: return partial(read_files, file) def read_files(file: Path, nlp: "Language") -> Iterable[Example]: # we run the full pipeline and not just nlp.make_doc to # ensure we have entities and sentences # which are needed during training of the entity linker with nlp.select_pipes(disable="entity_linker"): doc_bin = DocBin().from_disk(file) docs = doc_bin.get_docs(nlp.vocab) for doc in docs: yield Example(nlp(doc.text), doc) -
为了在调用
trainCLI 命令时使读取器可用,我们需要提供包含我们创建的 Python 代码的源代码。我们使用code参数来完成此操作。首先,我们获取config.cfg文件:!curl https://raw.githubusercontent.com/PacktPublishing/Mastering-spaCy-Second-Edition/main/chapter_10/config.cfg -o config.cfg -
现在,我们可以运行完整的命令来训练我们的
EntityLinker管道:python -m spacy train ./config.cfg --output entity_linking_taylor --paths.train ./nel_taylor/train.spacy --paths.dev ./nel_taylor/dev.spacy --paths.kb nel_taylor/my_kb --paths.base_nlp ./nel_taylor/my_nlp --code custom_functions.py
现在我们有了训练好的模型,我们需要对其进行测试。让我们在下一节中这样做。
测试实体链接模型
为了测试模型,我们将使用 train 命令从我们保存它的路径加载它,并在我们想要消歧义实体的文档上调用 nlp 对象。实体链接器模型将 kb_id 添加到实体中,我们可以用它来查看模型预测了哪个 Taylors。让我们使用一些 df_test 句子来评估模型:
-
首先,我们加载模型:
nlp = spacy.load("entity_linking_taylor/model-best") -
现在,我们处理句子。
doc.ents实体具有包含实体哈希值的kb_id属性,以及包含纯文本 QID 的kb_id_属性。让我们处理文本:text = 'Taylor struggled with chilly temperatures in Edinburgh, pausing the show to warm up her hands and to assist a distressed fan.' doc = nlp(text) -
现在,我们可以使用
displacy显示实体:from spacy import displacy displacy.serve(doc, style="ent") -
图 10.1 显示了结果。
Q26876是泰勒·斯威夫特的 Wikidata ID,所以这次模型也是正确的。
图 10.1 – 模型正确地消歧义了泰勒·斯威夫特的实体
让我们用另一个句子测试模型:
text = 'Now, Taylor has revealed that he had to re-audition for the part because the producers wanted to go in a different direction.'
doc = nlp(text)
- 图 10.2 显示了结果。
Q23359是泰勒·洛特纳的 Wikidata ID,所以模型在这点上也是正确的。
图 10.2 – 模型正确地消歧义了泰勒·洛特纳的实体
我们对模型采取轻松的态度,只评估这些简单的句子,因为这里的目的是仅展示如何从模型中获取结果。训练这个组件是一次相当漫长的旅程;恭喜!
摘要
在本章中,我们学习了如何使用 spaCy 训练 EntityLinker 组件。我们看到了一些实现细节,以了解更多关于 HashEmbedCNN.v2 层和 EntityLinker.v2 架构的信息。
我们还讨论了用于 NLP 训练的高质量数据集的一些特征,强调了一致性、代表性和详尽文档的重要性。最后,我们看到了如何创建用于训练实体链接模型的定制语料库读取器。有了这些知识,你可以定制任何其他 spaCy 组件。
在下一章和最后一章中,你将学习如何将 spaCy 与其他酷炫的开源库结合使用,以创建出色的 NLP 应用程序。那里见!
第十一章:将 spaCy 与第三方库集成
在本章中,我们将探讨如何将 spaCy 与第三方库集成,重点关注构建用于 NLP 任务的 Web 应用和 API。我们将从 Streamlit 开始,这是一个简化 Web 应用创建的 Python 框架,无需广泛的前端知识。我们将演示如何使用 Streamlit 和 spacy-streamlit 创建一个简单的 命名实体识别(NER)应用。在此之后,我们将深入研究 FastAPI,这是一个用于构建 API 的现代框架,以其速度和 Python 类型提示的使用而闻名。我们将学习如何创建一个使用 spaCy 模型从文本中提取实体的 API,展示了构建 NLP 驱动的服务的简便性。
在本章中,我们将涵盖以下主要主题:
-
使用 Streamlit 构建 spaCy 驱动的应用
-
使用 FastAPI 为 NLP 模型构建 API
技术要求
本章的所有数据和代码都可以在 github.com/PacktPublishing/Mastering-spaCy-Second-Edition 找到。
使用 Streamlit 构建 spaCy 驱动的应用
作为数据科学家和 NLP 工程师,我们在日常工作中主要使用的编程语言是 Python。如果你没有前端开发背景,曾经尝试构建 Web 应用或处理过 CSS,你就知道要在浏览器中开发应用程序是多么困难和令人沮丧。Streamlit 是一个 Python 框架,旨在帮助我们以简单快捷的方式使用纯 Python 创建 Web 应用。它建立在 Tornado Python Web 服务器框架之上(www.tornadoweb.org/en/stable/index.html),并在前端使用 React。幸运的是,我们不需要了解这些技术的使用方法来创建我们的 Web 应用,因为有了 Streamlit,我们只需声明一些 Python 变量就可以在页面上创建小部件。我们还可以通过使用 社区云 功能(streamlit.io/cloud)来平滑地部署、管理和共享 Streamlit Web 应用。部署只需一键即可完成,并且是免费的。
在本节中,我们将首先学习 Streamlit 的基础知识,然后学习如何使用 spacy-streamlit 包将 spaCy 可视化作为构建 Web 应用的基石。
使用 spacy-streamlit 构建 NLP 应用
要开始使用 Streamlit,第一步是使用 pip install streamlit 安装库。我们使用 Python 脚本创建 Web 应用。streamlit run app.py 命令在机器上运行本地服务器。为了检查安装是否正常,让我们运行 streamlit hello 命令。图 11 .1 展示了浏览器中的这个演示页面。
图 11.1 – Streamlit 欢迎页面
正如我们在引言部分所讨论的,使用 Streamlit 创建小部件和视觉组件可以通过简单地声明一个变量来完成。我们还可以通过使用 Streamlit 输入小部件来为应用程序添加交互性。主要的小部件有st.text_input、st.button和st.slider。该库有一个长长的页面元素列表。以下是一些主要元素及其描述:
-
st.write:这将内容写入页面,并自动检测内容的类型以正确呈现。如果您写入st.write('hi friends'),它将显示文本;如果您提供数据框而不是字符串,它将以适当的格式呈现数据框。 -
st.title:这将显示以标题格式化的文本。 -
st.markdown:这将显示以 Markdown 格式化的文本。 -
st.dataframe:这将以交互式表格的形式显示数据框。
spacy-streamlit包提供了可视化 spaCy 模型和开发交互式应用程序的工具。它具有各种组件,可以集成到您自己的 Streamlit 应用程序中,包括句法依存关系、命名实体、文本分类、标记属性等的可视化器。让我们构建一个应用程序,以了解 Streamlit 和spacy-streamlit是如何协同工作的。首先,您需要使用pip install spacy-streamlit安装该包。安装了库之后,让我们继续构建一个应用程序来显示我们在第八章中训练的模型中的实体:
-
首先,我们导入两个包,
Streamlit和spacy_streamlit:import streamlit as st import spacy_streamlit -
让我们为
st.text_input组件定义一个默认文本:DEFAULT_TEXT = """Givenchy is looking at buying U.K. startup for $1 billion""" -
现在我们定义第八章最终模型的路径:
spacy_model = "../chapter_08/pipelines/fashion_ner_with_base_entities" -
让我们添加一个标题和文本输入。
text_input的内容将被保存在text变量中:st.title("NER Fashion Brands App") text = st.text_area("Text to analyze", DEFAULT_TEXT, height=200)
我们还有一些工作要做,但首先,让我们看看只有这段代码时应用程序的样子。将代码保存到app.py脚本中,然后运行streamlit run app.py以在本地机器上提供服务。图 11.2展示了我们目前所拥有的内容。
图 11.2 – 带有 text_area 组件的 Streamlit 应用程序
当使用 Streamlit 构建应用时,每次用户与输入小部件交互时,库都会从顶部到底部重新运行整个 Python 脚本。我们的应用需要在处理文本之前加载模型,这意味着每次用户在 st.text_area 小部件中输入另一个文本时,他们可能需要等待模型再次加载。为了处理这种情况,Streamlit 有缓存功能(docs.streamlit.io/get-started/fundamentals/advanced-concepts#caching)和 spacy-streamlit 实现了 process_text() 方法。它缓存加载的模型并创建 Doc 对象。让我们使用该方法来处理我们应用中的文本,继续我们开始的列表:
-
process_text ()方法期望模型路径和将要处理的文本:doc = spacy_streamlit.process_text(spacy_model, text) -
最后,为了显示实体,我们将使用
visualize_ner()函数:spacy_streamlit.visualize_ner( doc, labels=["FASHION_BRAND", "GPE"], show_table=False, title="Fashion brands and locations", )
图 11 .3 显示了最终的应用。
图 11.3 – 使用 Streamlit 构建的 NER 应用
我们想添加一个按钮,让用户可以输入自己的文本并查看实体。由于每次我们与小部件交互时,Streamlit 都会重新运行所有脚本代码,所以我们不能使用 st.button 小部件。我们将使用 st.form 小部件,它将表单内的所有小部件值批量发送到 Streamlit。我们将使用 with 上下文管理器来创建表单。让我们试试:
-
我们将把所有应该重新加载的元素放入表单上下文管理器中。因此,当用户按下提交按钮时,我们将处理文档并使用可视化器显示它:
with st.form("my_form"): text = st.text_area("Text to analyze", DEFAULT_TEXT, height=200) submitted = st.form_submit_button("Submit") if submitted: doc = spacy_streamlit.process_text(spacy_model, text) spacy_streamlit.visualize_ner( doc, labels=["FASHION_BRAND", "GPE"], show_table=False, title="Fashion brands and locations", )
图 11 .4 显示了应用,现在有了 提交 按钮。
图 11.4 – 带有表单和提交按钮的 Streamlit 应用
-
下一个列表包含了网络应用的完整代码:
import streamlit as st import spacy_streamlit DEFAULT_TEXT = """Givenchy is looking at buying U.K. startup for $1 billion""" spacy_model = "../chapter_08/pipelines/fashion_ner_with_base_entities" st.title("NER Fashion Brands App") with st.form("my_form"): text = st.text_area("Text to analyze", DEFAULT_TEXT, height=200) submitted = st.form_submit_button("Submit") if submitted: doc = spacy_streamlit.process_text(spacy_model, text) spacy_streamlit.visualize_ner( doc, labels=["FASHION_BRAND", "GPE"], show_table=False, title="Fashion brands and locations", )
通过使用 streamlit 和 spacy-streamlit 库,我们仅用几行代码就能构建一个漂亮的 NLP 应用。其他 spacy-streamlit 可视化器包括 visualize_parser()、visualize_spans()、visualize_textcat()、visualize_similarity() 和 visualize_tokens()。
有时,我们需要为我们的 NLP 项目构建 API 而不是交互式网络应用。对于这种情况,我们可以使用另一个酷炫的库:FastAPI。让我们在下一节中了解更多关于它的信息。
使用 FastAPI 构建 NLP 模型的 API
FastAPI 是一个用于构建 API 的 Python 网络框架。它建立在另外两个 Python 库 Starlette 和 Pydantic 之上,使其成为可用的最快 Python 框架之一。FastAPI 基于标准的 Python 类型提示。通过类型提示,我们可以指定变量、函数参数或返回值的预期类型。这个特性帮助我们更早地捕捉到开发过程中的错误,所以在我们学习如何使用类型提示之前,让我们先了解如何使用它。
Python 类型提示 101
Python 是一种动态类型语言,这意味着它在运行时执行变量的类型检查。例如,如果我们运行此代码,它不会抛出任何错误:
string_or_int = "Hi friends"
string_or_int = 10
这运行得顺利,因为 Python 解释器处理了string_or_int从String到int的转换。这可能导致由于静默错误而出现的 bug。类型提示提供了一种在代码中指定预期类型的方法,因此它减少了我们创建 bug 的机会。让我们先看看一个没有类型提示的简单函数的例子:
def greeting(name):
return "Hello" + name
假设这个函数在一个代码库中,你只是导入它来在你的代码中使用。没有办法看到函数的作者期望name参数的类型,也没有关于返回类型的信息。为了获取这些信息,我们需要阅读并检查函数的代码。如果我们使用类型提示,情况就会改变:
def greeting(name: str) -> str:
return f"Hello {name}"
现在这些类型提示可以帮助你的代码编辑器,并显示类型。图 11.5显示了例如在Visual Studio Code中会发生什么。
图 11.5 – Visual Studio Code 中的类型提示
FastAPI 使用类型提示提供类型检查,导致 bug 和开发者引起的错误更少。现在我们知道了类型提示是什么,让我们使用 FastAPI 为我们的 spaCy 模型创建一个 API。
使用 FastAPI 为 spaCy 模型创建 API
我们构建 API 时使用的两个主要 HTTP 方法是GET和POST。GET用于请求不改变任何状态时,POST通常用于动作可以改变状态(向数据集添加元素、编辑值等)时。换句话说,GET用于读取数据,POST用于创建数据。在使用GET请求时,我们在 URL 地址中传递参数。在使用POST请求时,我们可以在请求体中传递参数。FastAPI 允许我们通过使用带有@app.get()或@app.post() Python 装饰器的函数来选择我们想要的请求类型。
让我们使用GET方法创建一个返回json对象的 API。首先,你需要使用pip install fastapi[standard]命令安装 FastAPI。现在创建一个包含以下命令的main.py文件:
-
首先,导入库并创建
app对象:from fastapi import FastAPI app = FastAPI() -
现在让我们创建
GET方法。我们将添加@app.get("/")装饰器。"/"表示调用此方法的 URL 路径将是我们的网站的根路径:@app.get("/") def root(): return {"message": "Hello World"} -
要在本地运行服务器,请转到命令行并输入
fastapi dev main.py。图 11.6显示了如果你打开浏览器并访问http://127.0.0.1:8000/URL 时应该看到的内容。
图 11.6 – GET 端点的响应
- FastAPI 的一个优点是它自动为 API 创建文档。默认情况下,它使用
Swagger UI(github.com/swagger-api/swagger-ui)。前往http://127.0.0.1:8000/docs查看。图 11 .7显示了我们的 FastAPI 的文档文档。
图 11.7 – 由 FastAPI 自动创建的我们的 API 文档
-
我们将创建一个 API,其风格与我们创建 Streamlit 网络应用时使用的风格相同。该 API 应接收一个文本列表,并返回一个包含实体的 JSON 响应。以下是一个请求示例:
{ "values": [ { "record_id": "1", "text": "Givenchy is looking at buying U.K. startup for $1 billion" } ] }
对于此请求,API 的响应应该是以下内容:
{
"values": [
{
"record_id": "1",
"data": {
"entities": [
{
"name": "Givenchy",
"label": "FASHION_BRAND",
"matches": [
{
"char_start": 0,
"char_end": 8,
"text": "Givenchy"
}
]
},
// [...] {
"name": "$1 billion",
"label": "MONEY",
"matches": [
{
"char_start": 47,
"char_end": 57,
"text": "$1 billion"
}
]
}
]
}
}
]
}
FastAPI 使用Pydantic模型来验证数据并构建 API 文档。让我们创建一些 Pydantic 模型来指定我们的 API 的POST请求。我们通过创建继承自 Pydantic 的BaseModel类的 Python 类来实现这一点:
-
为了提取实体,API 需要知道记录标识符和文本,因此让我们为它创建一个类:
from pydantic import BaseModel from typing import List class TextToExtractEntities(BaseModel): record_id: str text: str -
API 可以处理文本批次,因此请求的正文参数可以包含多个项目。让我们创建这个类:
class TextsRequest(BaseModel): values: List[TextToExtractEntities]
现在是时候使用 spaCy 提取实体以生成响应了。我们将创建一个与 Microsoft 的此 Cookiecutter 中使用的类非常相似的类:
github.com/microsoft/cookiecutter-spacy-fastapi
让我们开始吧:
-
我们应该初始化
EntityExtractor类,提供一个nlp模型以及记录标识符和记录文本的关键值:import spacy from spacy.language import Language class EntityExtractor: def __init__(self, nlp: Language, record_id_col_key: str = "record_id", record_text_col_key: str = "text"): self.nlp = nlp self.record_id_col_key = record_id_col_key self.record_text_col_key = record_text_col_key -
现在让我们创建一个提取实体的方法。该方法将遍历由
nlp.pipe()创建的文档,并为文本中的实体创建一个包含实体名称、标签以及实体起始和结束字符的列表:def extract_entities(self, records: List[Dict[str, str]]): ids = (item[self.record_id_col_key] for item in records) texts = (item[self.record_text_col_key] for item in records) response = [] for doc_id, spacy_doc in zip(ids, self.nlp.pipe(texts)): entities = {} for ent in spacy_doc.ents: ent_name = ent.text if ent_name not in entities: entities[ent_name] = { "name": ent_name, "label": ent.label_, "matches": [], } entities[ent_name]["matches"].append( {"char_start": ent.start_char, "char_end": ent.end_char, "text": ent.text} ) response.append({"id": doc_id, "entities": list(entities.values())}) return response -
为了测试此代码是否正常工作,让我们将其保存到
extractor.py脚本中,并按如下方式导入类:import spacy from pprint import pprint from extractor import EntityExtractor nlp = spacy.load("../chapter_08/pipelines/fashion_ner_with_base_entities") sentence = "Givenchy is looking at buying U.K. startup for $1 billion" doc = nlp(sentence) extractor = EntityExtractor(nlp) entities = extractor.extract_entities([{"record_id":1,"text":sentence}]) pprint(entities) -
我们使用在第八章中训练的模型来提取实体。响应应该是包含实体名称、标签和匹配项的字典:
[{ 'entities': [ { 'label': 'FASHION_BRAND', 'matches': [{ 'char_end': 8, 'char_start': 0, 'text': 'Givenchy' }], 'name': 'Givenchy' }, { 'label': 'GPE', 'matches': [{ 'char_end': 34, 'char_start': 30, 'text': 'U.K.' }], 'name': 'U.K.' }, { 'label': 'MONEY', 'matches': [{ 'char_end': 57, 'char_start': 47, 'text': '$1 billion' }], 'name': '$1 billion' } ], 'id': 1 }]
现在我们已经准备好完成我们的 API。/entities路径将是发送文本进行提取的POST端点。让我们回到 FastAPI 代码中创建此方法。
-
POST请求使用@app.post()装饰器定义。首先,我们需要将接收到的数据解析为具有record_id和text键的字典列表:@app.post("/entities") def extract_entities(body: TextsRequest): """Extract Named Entities from a batch of Records.""" documents = [] for item in body.values: documents.append({"record_id": item.record_id, "text": item.text}) -
现在我们将调用
EntityExtractor类,发送文档,并返回实体:entities_result = extractor.extract_entities(documents) response = [ {"record_id": er["record_id"], "data": {"entities": er["entities"]}} for er in entities_result ] return {"values": response} -
如果您将此代码添加到
main.py文件中,然后在终端中运行fastapi run main.py,并访问/docsURL,您现在应该能看到POST /entities端点规范。 -
关于 Swagger UI 的另一个酷点是,我们可以在浏览器中直接测试端点。点击
POST /entities向下的箭头按钮,然后点击右侧的Try it out按钮。现在你可以粘贴我们的示例请求,然后点击蓝色的Execute按钮。图 11.8 显示了POST /entities的箭头按钮。
图 11.8 – 发送数据到实体端点的按钮
下一个列表包含了我们 API 的完整代码:
from fastapi import FastAPI
from typing import List
from pydantic import BaseModel
import spacy
from extractor import EntityExtractor
app = FastAPI()
nlp = spacy.load("../chapter_08/pipelines/fashion_ner_with_base_entities")
extractor = EntityExtractor(nlp)
class TextToExtractEntities(BaseModel):
record_id: str
text: str
class TextsRequest(BaseModel):
values: List[TextToExtractEntities]
@app.get("/")
def root():
return {"message": "Hello World"}
@app.post("/entities")
def extract_entities(body: TextsRequest):
"""Extract Named Entities from a batch of Records."""
documents = []
for item in body.values:
documents.append({"record_id": item.record_id,
"text": item.text})
entities_result = extractor.extract_entities(documents)
response = [
{"record_id": er["record_id"],
"data": {"entities": er["entities"]}}
for er in entities_result
]
return {"values": response}
只用几行代码就能获得所有这些功能,这真是太酷了,对吧?这仅仅是使用 FastAPI 的基础知识。你可以在库的文档中查看更多高级内容,请访问 fastapi.tiangolo.com/learn/。
摘要
本章介绍了将 spaCy 与第三方库集成的两个强大工具:用于构建交互式网络应用的 Streamlit 和用于创建快速、类型安全的 API 的 FastAPI。我们展示了如何使用 Streamlit 和 spacy-streamlit 包构建一个 NER 网络应用,利用 Streamlit 的简洁性和交互性。然后我们转向使用 FastAPI 构建 API,强调类型提示在减少错误和提高代码可靠性方面的重要性。通过将 spaCy 与这些框架结合,你了解到你可以以最小的努力创建有效的、用户友好的 NLP 应用和服务。
在这一章的最后,我们结束了这本书。这是一段相当精彩的旅程!我希望你已经对 spaCy 的主要功能有了基本的了解,但更重要的是,我希望你现在能够使用 spaCy 创建遵循一些主要软件工程原则编写良好代码的解决方案。我迫不及待地想看看你接下来会构建什么!