Subsections of spacy高级教程
命名实体识别
Subsections of 命名实体识别
使用 EntityRuler 创建训练集
训练集简介
在这个笔记本中,我们将更深入地探讨训练集,了解它们是什么,为什么它们很重要,以及如何使用 spaCy 的 EntityRuler 自动化创建一个良好(而非优秀!)的训练数据集,该数据集将需要手动检查。在下一个视频中,我将向您展示如何使用这个训练集在 spaCy 中训练一个自定义的 NER 模型。
除了 spaCy 能够很好地扩展(意味着它可以在小数据和大数据上表现良好)之外,它还很容易定制,并且可以执行高级机器学习方法,而无需具备机器学习的知识。然而,了解机器学习的基本知识(如本系列笔记本 03 中讨论的那样)是有帮助的,因为它将使您能够了解如何培养良好的训练集,以及为什么某些方法可能会失败或遇到困难。实际上,您将通过实践来培养对机器学习 NER 中什么有效、什么无效的感觉。
在本系列的第三本笔记本中,我提到训练机器学习模型的数据存在三种形式:训练数据、验证数据和测试数据。所有这些数据都将具有相同的形式。它将是一个列表数据结构,其中每个索引将包含一个文本(一个句子、段落或整个文本)。文本的长度将取决于您希望通过 ML NER 实现的目标。文本的大小将影响训练过程。然而,现在让我们忽略这一点。训练数据还需要另一个组件,即文本中实体的列表,包括其实体开始位置、结束位置和标签。在训练过程中,这些注释将允许卷积神经网络(spaCy 机器学习训练过程背后的架构)从数据中学习,并能够正确识别您正在训练的实体。
spaCy 训练集是什么样的?
SpaCy 要求您的训练数据具有非常特定的格式:
TRAIN_DATA = [ (TEXT AS A STRING, {“entities”: [(START, END, LABEL)]}) ]请注意,TRAIN_DATA 是大写的。在 Python 中,通常不将对象大写,但有少数例外。TRAIN_DATA 就是其中之一。我不知道这个约定的历史,但在每一本书/教程中,你都会看到 TRAIN_DATA 都是这样写的。当然,这不是必需的,但始终在代码中尽可能遵循 Python 风格是一种好习惯,这样别人就能更容易地阅读你的代码。任何机器学习从业者都会期望看到 TRAIN_DATA 是这样写的。
获取训练数据到这种格式非常困难,手动操作。研究人员必须计算字符来分配实体的起始和结束位置。即使你考虑使用 Python 内置的字符串函数来获取起始和结束字符,你也会遇到另一个问题。spaCy 的训练过程读取起始和结束字符的方式与你可能使用字符串函数计数的方式不同。这意味着在训练过程中,spaCy 将丢弃与标记起始和结束位置不匹配的注释。这是因为与你的字符串函数对文本进行分词的方式相比,spaCy 的分词方式不同。幸运的是,spaCy 通过 EntityRuler 内置了解决方案,以帮助你在这一过程中。
如果你对手动标注感兴趣,我强烈建议你探索来自 Explosion AI 的付费软件 Prodigy(https://prodi.gy/)。我并不是为了推广该产品而获得报酬。它很贵,但如果你需要做大量的标注(针对图像、文本、视频甚至音频),那么 Prodigy 就是你的工具。它有一个很好的用户界面,并且因为它是由为我们提供 spaCy 的同一团队开发的,所以它可以无缝地融入 spaCy 的工作流程。你可以在这里探索 Prodigy 的演示:https://prodi.gy/demo。
创建训练集
在下面的代码中,我们将通过 EntityRuler 创建 spaCy 机器学习训练集。换句话说,我们将使用基于规则的方法自动生成一个基本训练集。这个训练集会有错误吗?可能会。这就是为什么查看训练集并手动验证是个好主意。然而,以这种方式做,你可以大大增加原型设计,以查看你想要训练的定制实体是否有潜在可行性。在机器学习中,很少有针对特定领域的具体解决方案。如果有,人们就不需要专家了。实验通常是机器学习的游戏名称,NER 机器学习也不例外。
我们将创建一个空白英语模型,因为我们只会临时使用这个模型。我们不需要其他组件。这个模型将只包含 EntityRuler,我们将临时使用它来生成训练集。回想一下我们上一个笔记本,spaCy 的小型模型无法正确识别 Treblinka 作为地点吗?在下面的代码中,我们将从这三句话中创建一个基本的训练集,这将使我们能够生成一个非常小的训练集。我想明确一点。这些训练数据远远不足以训练一个模型。然而,这个过程扩展得非常好。
这里是之前看到的相同代码,但文本略有不同。注意输出结果。它已经正确地将 Treblinka 识别为 GPE。
#Import the requisite library
import spacy
#Build upon the spaCy Small Model
nlp = spacy.blank("en")
#Sample text
text = "Treblinka is a small village in Poland. Wikipedia notes that Treblinka is not large."
#Create the EntityRuler
ruler = nlp.add_pipe("entity_ruler")
#List of Entities and Patterns
patterns = [
{"label": "GPE", "pattern": "Treblinka"}
]
ruler.add_patterns(patterns)
doc = nlp(text)
#extract entities
for ent in doc.ents:
print (ent.text, ent.label_)现在,我们将稍微修改一下这段代码,以便我们可以生成一个略有不同的输出,一个包含文本起始和结束位置的输出。
#Import the requisite library
import spacy
#Build upon the spaCy Small Model
nlp = spacy.blank("en")
#Sample text
text = "Treblinka is a small village in Poland. Wikipedia notes that Treblinka is not large."
#Create the EntityRuler
ruler = nlp.add_pipe("entity_ruler")
#List of Entities and Patterns
patterns = [
{"label": "GPE", "pattern": "Treblinka"}
]
ruler.add_patterns(patterns)
doc = nlp(text)
#extract entities
for ent in doc.ents:
print (ent.text, ent.start_char, ent.end_char, ent.label_)注意现在,我们的输出为每个实体的起始和结束位置分别是 0,9 和 61,71。有了这些数据,我们现在可以开始生成我们想要的输出。然而,让我们先尝试将输入文本分解成句子,然后拥有两组不同的训练数据。
#Import the requisite library
import spacy
#Build upon the spaCy Small Model
nlp = spacy.load("en_core_web_sm")
#Sample text
text = "Treblinka is a small village in Poland. Wikipedia notes that Treblinka is not large."
#Create a blank list for appending later.
corpus = []
doc = nlp(text)
#use the spacy tokenizer to get the sentences.
for sent in doc.sents:
corpus.append(sent.text)
print (corpus)
#Build upon the spaCy Small Model
nlp = spacy.blank("en")
#Create the EntityRuler
ruler = nlp.add_pipe("entity_ruler")
#List of Entities and Patterns
patterns = [
{"label": "GPE", "pattern": "Treblinka"}
]
ruler.add_patterns(patterns)
#iterate over the sentences
for sentence in corpus:
doc = nlp(sentence)
#extract entities
for ent in doc.ents:
print (ent.text, ent.start_char, ent.end_char, ent.label_)注意现在我们得到了一个不同起始和结束的输出。现在,我们可以再次修改我们的代码,使其符合我们想要的格式:
#Import the requisite library
import spacy
#Build upon the spaCy Small Model
nlp = spacy.load("en_core_web_sm")
#Sample text
text = "Treblinka is a small village in Poland. Wikipedia notes that Treblinka is not large."
corpus = []
doc = nlp(text)
for sent in doc.sents:
corpus.append(sent.text)
#Build upon the spaCy Small Model
nlp = spacy.blank("en")
#Create the EntityRuler
ruler = nlp.add_pipe("entity_ruler")
#List of Entities and Patterns
patterns = [
{"label": "GPE", "pattern": "Treblinka"}
]
ruler.add_patterns(patterns)
TRAIN_DATA = []
#iterate over the corpus again
for sentence in corpus:
doc = nlp(sentence)
#remember, entities needs to be a dictionary in index 1 of the list, so it needs to be an empty list
entities = []
#extract entities
for ent in doc.ents:
#appending to entities in the correct format
entities.append([ent.start_char, ent.end_char, ent.label_])
TRAIN_DATA.append([sentence, {"entities": entities}])
print (TRAIN_DATA)练习
为了本视频中的练习,我想让你尝试用你熟悉的语料库来复制这个过程。制定一系列规则来识别你想要识别的几个或许多实体。不用担心找到所有实体。这样做的目的是生成一个足够好的训练集,具有足够的多样性,以便在下一笔记本中教授机器学习模型。在下面的视频中,我会向你展示如何使用《哈利·波特》第一本书中的角色在大规模上完成这个操作。
如何训练命名实体识别模型
spaCy 中机器学习模型的训练简介
在上一个笔记本中,我们使用 spaCy 的 EntityRuler 创建了一个基本的训练集用于机器学习模型。我们通过做出某些关于非常可能或肯定属于特定标签的事物的假设来完成这项工作。这种培养训练集的方法,从本质上讲是存在问题的。它可能会错过一些实体,并错误地标记其他实体。如果您希望这是用于训练最终模型的必要训练集,我鼓励您进行手动检查。如果您想使用此模型作为可以通过 Prodigy 培养更好训练集的基线模型,那么这种方法也将有效。
在这个笔记本中,我们不会对训练集进行优化,而是使用它来训练一个定制的 spaCy 机器学习 NER 模型。因此,本笔记本的重点将放在这些方法上,而不是结果上。
在 01.04:机器学习 NER 中,我们首次接触了机器学习及其一些基本原理。如果您还没有观看那个笔记本和其中的视频,我鼓励您在完成这个笔记本之前先观看,因为我将假设您已经对机器学习有了一定的了解。
我之所以更喜欢 spaCy 而不是其他 NLP 框架,是因为 spaCy 能够很好地扩展(适用于小数据和大数据)并且它的训练过程易于使用。NER 从业者不需要通过 PyTorch/FastAI 或 TensorFlow/Keras 创建自定义神经网络,尽管这些框架是使用起来最容易的,但学习曲线却很陡峭。相反,spaCy 的用户可以利用 spaCy 训练过程背后的预设计 CNN 架构。在 spaCy 3.0 版本(在撰写本笔记本时,夜间版本可用),预计在 2021 年初,用户还可以自定义这个神经网络架构,从而扩展 spaCy 的实用性和可定制性。
为了利用 spaCy 的训练过程,用户只需要了解一些基本概念,例如数据应该如何进入训练过程(在上一个笔记本中介绍)以及一些超参数(我们在训练过程中调整以尝试找到最佳结果的东西)。
准备数据
如前一个笔记本所述,您的输入数据应采用以下格式:
TRAIN_DATA = [ (TEXT AS A STRING, {“entities”: [(START, END, LABEL)]}) ]首先,让我们将上一段视频中的代码引入以生成我们的训练数据:
import spacy
nlp = spacy.load("en_core_web_sm")
text = "Treblinka is a small village in Poland. Wikipedia notes that Treblinka is not large."
corpus = []
doc = nlp(text)
for sent in doc.sents:
corpus.append(sent.text)
nlp = spacy.blank("en")
ruler = nlp.add_pipe("entity_ruler")
patterns = [
{"label": "GPE", "pattern": "Treblinka"}
]
ruler.add_patterns(patterns)
TRAIN_DATA = []
for sentence in corpus:
doc = nlp(sentence)
entities = []
for ent in doc.ents:
entities.append([ent.start_char, ent.end_char, ent.label_])
TRAIN_DATA.append([sentence, {"entities": entities}])
print (TRAIN_DATA)如何将训练数据转换为 spaCy 二进制文件
在此教科书的早期版本中,我们使用了 spaCy 2。在 spaCy 3 中进行训练的方式完全不同。虽然可以在脚本中处理一些基础知识,但我认为在终端中完成是最好的。因为这本教科书是基于许多 Jupyter Notebooks 的 JupyterBook,我们可以通过在单元格开头使用 ! 来执行基于终端的命令。为了训练机器学习模型,我们首先需要创建一个包含训练数据的 spaCy 二进制对象。我发现如果将来需要重复使用一小段代码,使用函数总是很好的,这样就可以保证可重复性。以下函数可在 spaCy 的仓库中找到:https://github.com/explosion/projects/blob/v3/pipelines/ner_demo/scripts/convert。我对其进行了轻微修改以更好地适应本教科书。
让我们来分解这个函数的作用。它的整个目的是将我们的 spaCy 2 格式 TRAIN_DATA 转换为 spaCy 3 的二进制训练数据。在我的工作流程中,我喜欢将这两个步骤分开。原因是我喜欢在将训练数据转换为 spaCy 3 二进制格式之前检查和验证我的训练数据。这允许进行最后的快速手动验证。该函数有三个参数:
- lang => 这将是空白模型的语言。使用“en”表示英语,“de”表示德语等
- TRAIN_DATA => 这将是训练数据,作为上面看到的列表的列表。
- output_path => 这将是 spaCy 二进制文件所在的输出目录。
这个函数的好处是,如果您的训练数据不匹配,它将简单地被忽略。这可以防止训练过程中的错误。为了让这个函数工作,它需要创建一个 DocBin 对象来保存。db = DocBin()允许我们运行 db.add()并逐个添加我们的训练数据。然后,这些数据被转换为二进制对象(体积更小,加载更快),并通过 db.to_disk()保存到磁盘上。
import srsly
import typer
import warnings
from pathlib import Path
import spacy
from spacy.tokens import DocBin
def convert(lang: str, TRAIN_DATA, output_path: Path):
nlp = spacy.blank(lang)
db = DocBin()
for text, annot in TRAIN_DATA:
doc = nlp.make_doc(text)
ents = []
for start, end, label in annot["entities"]:
span = doc.char_span(start, end, label=label)
if span is None:
msg = f"Skipping entity [{start}, {end}, {label}] in the following text because the character span '{doc.text[start:end]}' does not align with token boundaries:\n\n{repr(text)}\n"
warnings.warn(msg)
else:
ents.append(span)
doc.ents = ents
db.add(doc)
db.to_disk(output_path)现在函数已经准备好了,我们可以调用它。在这个简单的示例中,我们将故意在机器学习领域犯一个重大错误。我们不仅将使用一个小型的训练数据样本(只有 2 个),我们还将使用它们进行训练和验证。这一点我无法强调得更多。绝对不要这样做。我们在这里这样做,只是为了建立一个工作流程,并展示这种小型训练数据方法可能带来的问题。
spaCy 的 config.cfg 文件是什么?如何创建它?
现在我们已经准备好了训练数据,是时候开始准备我们的模型了。在 spaCy 3 中,我们对模型的神经网络架构和超参数有很多控制权。所有这些都在新的 config.cfg 文件中完成。这个配置文件在训练过程中提供给 spaCy,以便它知道要训练什么以及如何训练。为了创建 config.cfg 文件,我们首先需要创建一个 base_config.cfg 文件。为此,我们可以使用 spaCy 的便捷 GUI,位于此处:https://spacy.io/usage/training(向下滚动一点)。你会找到类似这样的内容:
为了我们的目的,选择“英语”,我们正在训练的语言,“ner”仅限,我们正在训练的模型,“CPU”(GPU 稍微复杂一些),以及效率(训练更快且更小,因为没有词向量)。您将把输出复制粘贴到 GUI 中,作为“base_config.cfg”保存在您的目录下。我们只会对这个 base_config.cfg 文件进行两项小的修改。我们将指定 train 和 dev 的路径(在路径的第一类别下可以看到)。我们将这些设置为我们的 train.spacy 和 valid.spacy 文件的位置。
现在基础配置文件已正确设置,是时候将其转换为 config.cfg 文件了。为此,我们需要执行一个终端命令。幸运的是,我们可以在 Jupyter Notebook 中完成这个操作。我已经将我的 base_config 文件放在了子文件夹 data 中。通过运行下面的命令,spaCy 会将 base_config 重新格式化为一个正确格式的 config.cfg 文件。
!python -m spacy init fill-config data/base_config.cfg data/config.cfg使用 config.cfg 文件配置好后,我们可以训练我们的第一个模型。在我们的例子中,我将把我们的模型放在 models/output 子文件夹中。我们运行以下命令,就得到了一个训练好的模型。
如何从 config.cfg 文件训练 spaCy 3 模型
!python -m spacy train data/config.cfg --output ./models/output上述输出告诉我们模型的迭代次数、样本数量以及一些指标。我们的模型显示 100%,但这并不意味着我们有一个好的模型。它是过拟合的,意味着它本质上记住了一个样本,特雷布林卡。尽管如此,让我们加载模型并看看它的表现如何。
trained_nlp = spacy.load("models/output/model-best")
text = "The village of Treblinka is located in Poland."
doc = trained_nlp(text)
for ent in doc.ents:
print (ent.text, ent.label_)请注意,我们给机器学习模型 NER 提供了一个新句子,并且它正确地将 Treblinka 识别为“GPE”。但我们不应该过于兴奋。对这个文本的微小修改会导致实体识别错误。
text = "Mark, from New York, said that he wants to go to Treblinkaa to speak to the locals."
doc = trained_nlp(text)
for ent in doc.ents:
print (ent.text, ent.label_)
if len(doc.ents) == 0:
print ("No entities found.")为什么我们的模型现在失败了?因为我们训练的是一个机器学习模型,而不是 EntityRuler。它知道 Treblinka 是一个 GPE,但它只学会了如果拼写正确才能识别它。这是一个糟糕的模型。机器学习 NER 模型会随着我们提供给它们的训练数据的增加而改进。然而,最重要的是,它们会随着我们提供给它们的更多样化训练数据的增加而改进。一个很好的经验法则是从 200 个训练样本开始,然后继续调整。你可能需要收集更多样化的训练数据,或者你可能需要重新考虑你的标签。另一种可能性是,你需要微调 config.cfg 文件中的超参数。我们将在本教材的剩余部分介绍这些问题和解决方案。然而,到目前为止,你应该已经很好地了解了 spaCy 3 中的训练过程。这个笔记本中讨论的材料到目前为止是最具挑战性的。在这里花些时间,在继续前进之前,要很好地了解这个过程。