语言处理管道
文档说明
版本
✔️ ver_1.0.1
概述
当你在文本中调用nlp
时,spaCy
首先标记文本以生成一个Doc
对象。这个Doc
随后会在被称为处理流水线在几个不同的步骤进行处理。经过训练的管道通常包括标记器(tagger)、词干分析器(lemmatizer)、解析器和实体识别器。每个管道组件都返回处理过的Doc
,然后传递给下一个组件。
管道的处理能力一般取决于组件、组件的模型以及训练方式。例如,命名实体识别的管道需要包含经过训练的命名实体识别器组件,该组件具有统计模型和权重,使其能够对实体标签进行预测。这就是每个管道在config
中指定其组件及其设置的原因:
[nlp]
pipeline = ["tok2vec", "tagger", "parser", "ner"]
[nlp]
pipeline = ["tok2vec", "tagger", "parser", "ner"]
管道中的组件顺序重要吗?
统计组件(像标记器或解析器等)通常是独立的,不共享任何数据。例如,命名实体识别器不使用标记器和解析器设置的任何功能。这意味着你可以交换它们,或从管道中删除单个组件,而不会影响其他组件。然而,组件可能共享一个token-to-vector
组件,比如Tok2Vec
或者Transformer
。你可以在有关嵌入层的文档中阅读有关此内容的更多信息 。
自定义组件也可能依赖于其他组件设置的注释。例如,自定义词干分析器可能需要词性标注的结果,因此只有在标记器(tagger)之后添加它才能工作。解析器将遵守预定义的句子边界,因此如果管道中的前一个组件设置了它们,则其依赖性预测可能会有所不同。同样,如果可以在统计实体识别器(ner
)之前或之后添加EntityRuler
:如果之前添加,实体识别器在进行预测时会考虑现有实体。EntityLinker
将命名实体解析到基于知识的IDs
中,所以在这之前应该有识别实体的管道组件,例如EntityRecognizer
。
分词器(tokenizer)为什么特别?
分词器是一个“特殊”组件,它不是常规管道的一部分。它也没有出现在nlp.pipe_names
中。原因是实际上只能有一个分词器,而所有其他管道组件都接收一个Doc
并返回一个Doc
,而分词器接收一串文本并将其转换为Doc
。不过,你仍然可以自定义分词器。nlp.tokenizer
是可写的,因此你可以从头创建自己的Tokenizer
,甚至可以将其替换为 完全自定义的函数。
处理文本
当你在文本上调用nlp
时,spaCy
首先对文本进行分词(tokenize),然后按顺序为Doc
调用组件,最后,返回处理后的Doc
,以便你做后续处理。
doc = nlp("This is a text")
doc = nlp("This is a text")
在处理大量文本时,统计模型在批处理文本时通常有更高的效率。nlp.pipe
方法传入一个可迭代的文本集合,然后返回一个处理的Doc
生成器(yield)。其中批处理工作在内部完成。
texts = ["This is a text", "These are lots of texts", "..."]
- docs = [nlp(text) for text in texts]
+ docs = list(nlp.pipe(texts))
texts = ["This is a text", "These are lots of texts", "..."]
- docs = [nlp(text) for text in texts]
+ docs = list(nlp.pipe(texts))
💡高效处理的小窍门
使用nlp.pipe
将文本作为流处理并将它们分批缓冲,而不是一个一个的处理。通常这样效率会更高。
只使用你需要的管道组件。管道中包含大量的未使用的组件会使得预测工作低效。为了防止这种情况发生,无论是在加载管道时还是在使用nlp.pipe
时,请使用disable
关键字参数禁用你不需要的组件。有关更多详细信息和示例,请参阅有关禁用管道组件的部分 。
在这个例子中,我们使用nlp.pipe
将(可能非常大)可迭代的文本作为流处理。因为我们只访问doc.ents
(由ner组件设置)中的命名实体,所以我们将在处理过程中禁用所有其他组件。nlp.pipe
产生Doc
对象,因此我们可以迭代它们并访问命名实体预测:
✏️ 试一试 现在将"ner"组件设置为
disable
。你将看到doc.ents
为空,因为,实体识别器没有运行。
# spaCy v3.0 · Python 3
import spacy
texts = [
"Net income was $9.4 million compared to the prior year of $2.7 million.",
"Revenue exceeded twelve billion dollars, with a loss of $1b.",
]
nlp = spacy.load("en_core_web_sm")
for doc in nlp.pipe(texts, disable=["tok2vec", "tagger", "parser", "attribute_ruler", "lemmatizer"]):
# Do something with the doc here
print([(ent.text, ent.label_) for ent in doc.ents])
# spaCy v3.0 · Python 3
import spacy
texts = [
"Net income was $9.4 million compared to the prior year of $2.7 million.",
"Revenue exceeded twelve billion dollars, with a loss of $1b.",
]
nlp = spacy.load("en_core_web_sm")
for doc in nlp.pipe(texts, disable=["tok2vec", "tagger", "parser", "attribute_ruler", "lemmatizer"]):
# Do something with the doc here
print([(ent.text, ent.label_) for ent in doc.ents])
重要的提示
请记住,使用nlp.pipe
时,它返回一个Doc
生成器(generator)而不是一个列表。所以,如果你想像列表一样使用它,则必须先调用list()
:
- docs = nlp.pipe(texts)[0] # will raise an error
+ docs = list(nlp.pipe(texts))[0] # works as expected
- docs = nlp.pipe(texts)[0] # will raise an error
+ docs = list(nlp.pipe(texts))[0] # works as expected
使用nlp.pipe
时,你可以使用as_tuples
选项 为每个文档传递上下文信息。如果as_tuples
是True
,那么输入应该是一个(text, context)
元组列表,输出是一个(doc, context)
元组列表。例如,你可以在上下文中传递元数据并将其保存在自定义属性中:
# spaCy v3.0 · Python 3
import spacy
from spacy.tokens import Doc
if not Doc.has_extension("text_id"):
Doc.set_extension("text_id", default=None)
text_tuples = [
("This is the first text.", {"text_id": "text1"}),
("This is the second text.", {"text_id": "text2"})
]
nlp = spacy.load("en_core_web_sm")
doc_tuples = nlp.pipe(text_tuples, as_tuples=True)
docs = []
for doc, context in doc_tuples:
# 这里需要手动赋值,as_tuples不能将将context直接映射到属性中
doc._.text_id = context["text_id"]
docs.append(doc)
for doc in docs:
print(f"{doc._.text_id}: {doc.text}")
# spaCy v3.0 · Python 3
import spacy
from spacy.tokens import Doc
if not Doc.has_extension("text_id"):
Doc.set_extension("text_id", default=None)
text_tuples = [
("This is the first text.", {"text_id": "text1"}),
("This is the second text.", {"text_id": "text2"})
]
nlp = spacy.load("en_core_web_sm")
doc_tuples = nlp.pipe(text_tuples, as_tuples=True)
docs = []
for doc, context in doc_tuples:
# 这里需要手动赋值,as_tuples不能将将context直接映射到属性中
doc._.text_id = context["text_id"]
docs.append(doc)
for doc in docs:
print(f"{doc._.text_id}: {doc.text}")
输出:
text1: This is the first text.
text2: This is the second text.
text1: This is the first text.
text2: This is the second text.
并行化处理
spaCy
中内置了nlp.pipe
并行处理的支持,使用n_process
选项实现:
# Multiprocessing with 4 processes
docs = nlp.pipe(texts, n_process=4)
# With as many processes as CPUs (use with caution!)
docs = nlp.pipe(texts, n_process=-1)
# Multiprocessing with 4 processes
docs = nlp.pipe(texts, n_process=4)
# With as many processes as CPUs (use with caution!)
docs = nlp.pipe(texts, n_process=-1)
随着平台的不同,使用并行化启动多个进程可能会增加系统开销。特别是在macOS/OS X
(Python 3.8版本)和Windows
中使用的默认启动方法spawn
在模型较大时可能会很慢,因为每个新进程都会在内存中复制一份模型数据副本。更多详细信息,请参阅有关多处理的Python文档。
对于较短的任务,尤其是在spawn
中,使用较少数量的进程结合较大的批量值(batch size)会更快。最佳batch_size
的设置则取决于管道组件、文档长度、进程数和可用内存量。
# Default batch size is `nlp.batch_size` (typically 1000)
# 默认的batch size 是 `nlp.batch_size` (一般是 1000)
docs = nlp.pipe(texts, n_process=2, batch_size=2000)
# Default batch size is `nlp.batch_size` (typically 1000)
# 默认的batch size 是 `nlp.batch_size` (一般是 1000)
docs = nlp.pipe(texts, n_process=2, batch_size=2000)
GPU
上的并行处理
通常不建议在GPU
上进行并行处理,因为RAM
太有限了。如果你想尝试一下,请注意,只能使用spawn
并且限制CUDA
。
使用
transformer
模型进行并行处理
在Linux
中,由于PyTorch
中的一个问题,转换器模型可能会因多线程处理而挂起或死锁 。一种建议是使用spawn
代替fork
另一种方法是在使用torch.set_num_threads(1)
在加载任何模型之前来限制线程的数量。
管道和内建的组件
spaCy
通过组合可重用的组件使得构建一个新的管道变得非常容易,这些组件包括spaCy
默认的标记器(tagger
)、解析器(parser
)和实体识别器以及你自己创建的管道函数。管道组件可以添加到已经存在的nlp
对象中,在初始化Language
类时指定,或在管道包中定义。CONFIG.CFG
(摘录)
[nlp]
lang = "en"
pipeline = ["tok2vec", "parser"]
[components]
[components.tok2vec]
factory = "tok2vec"
# Settings for the tok2vec component
[components.parser]
factory = "parser"
# Settings for the parser component
[nlp]
lang = "en"
pipeline = ["tok2vec", "parser"]
[components]
[components.tok2vec]
factory = "tok2vec"
# Settings for the tok2vec component
[components.parser]
factory = "parser"
# Settings for the parser component
当管道加载时,spaCy
首先查询meta.json
和config.cfg
。这些配置告诉spaCy
要使用什么语言类,哪些组件在管道中,以及应该如何创建这些组件。然后 spaCy
将执行以下操作:
- 通过
get_lang_class
使用给定的ID
加载语言类和数据并完成初始化。Languge
类包含了共享的词汇,分词(tokenization
)规则和语言的特定设置; - 遍历管道名称并在
[components]
块中查找每个组件名称。调用add_pipe
时factory
告诉spaCy
需要使用哪个组件工厂。设置被传递到工厂。 - 通过调用
from_disk
传入数据目录的路径,使模型数据可供Language
类使用
所以,当你键入以下代码。。。
nlp = spacy.load("en_core_web_sm")
nlp = spacy.load("en_core_web_sm")
管道的config.cfg
告诉spaCy
使用语言"en"
和["tok2vec", "tagger", "parser", "ner", "attribute_ruler", "lemmatizer"]
管道。然后,spaCy
将初始化spacy.lang.en.English
,创建每个管道组件并将其添加到处理管道中。然后,它将从数据目录加载模型数据并返回修改后的Language
类供你作为nlp
对象使用。
v3.0
的更改spaCy v3.0
引入了一个config.cfg
,其中包括对管道、其组件和训练过程的更详细的设置。你可以通过调用nlp.config.to_disk
导出当前nlp
对象的配置。
从根本上说,spaCy管道包由三个部分组成:权重(the weights
),即从目录加载的二进制数据;管道,即按顺序调用的函数管道;以及语言数据(language data
),如标记化(tokenization
)规则和特定于语言的设置。例如,西班牙语NER
管道与英语解析和标记管道相比,需要不同的权重、语言数据和组件。这也是管道状态始终由Language
类持有的原因。spacy.load
将所有这些打包并返回一个Language
实例,这个实例包含管道集以及对二进制数据的访问:
# SPACY.LOAD UNDER THE HOOD
lang = "en"
pipeline = ["tok2vec", "tagger", "parser", "ner", "attribute_ruler", "lemmatizer"]
data_path = "path/to/en_core_web_sm/en_core_web_sm-3.0.0"
cls = spacy.util.get_lang_class(lang) # 1. Get Language class, e.g. English
nlp = cls() # 2. Initialize it
for name in pipeline:
nlp.add_pipe(name) # 3. Add the component to the pipeline
nlp.from_disk(data_path) # 4. Load in the binary data
# SPACY.LOAD UNDER THE HOOD
lang = "en"
pipeline = ["tok2vec", "tagger", "parser", "ner", "attribute_ruler", "lemmatizer"]
data_path = "path/to/en_core_web_sm/en_core_web_sm-3.0.0"
cls = spacy.util.get_lang_class(lang) # 1. Get Language class, e.g. English
nlp = cls() # 2. Initialize it
for name in pipeline:
nlp.add_pipe(name) # 3. Add the component to the pipeline
nlp.from_disk(data_path) # 4. Load in the binary data
当你在文本上调用nlp
时,spaCy将对其进行tokenize
,然后顺序调用每个组件来处理Doc
。由于模型数据已加载,组件可以访问它以将注释分配给Doc
对象,随后分配给Token
和Span
,它们只是Doc
的视图,Doc
本身不拥有任何数据。所有组件都返回修改后的文档,然后由管道中的下一个组件处理。
#THE PIPELINE UNDER THE HOOD
# 引擎下的管道
doc = nlp.make_doc("This is a sentence") # Create a Doc from raw text
for name, proc in nlp.pipeline: # Iterate over components in order
doc = proc(doc) # Apply each component
#THE PIPELINE UNDER THE HOOD
# 引擎下的管道
doc = nlp.make_doc("This is a sentence") # Create a Doc from raw text
for name, proc in nlp.pipeline: # Iterate over components in order
doc = proc(doc) # Apply each component
nlp.pipeline
就是当前的处理管道,它返回一个(name, component)
元组,或者使用nlp.pipe_names
获取处理管道,它只返回一个人类可读的组件名称列表。
print(nlp.pipeline)
# [('tok2vec', <spacy.pipeline.Tok2Vec>), ('tagger', <spacy.pipeline.Tagger>), ('parser', <spacy.pipeline.DependencyParser>), ('ner', <spacy.pipeline.EntityRecognizer>), ('attribute_ruler', <spacy.pipeline.AttributeRuler>), ('lemmatizer', <spacy.lang.en.lemmatizer.EnglishLemmatizer>)]
print(nlp.pipe_names)
# ['tok2vec', 'tagger', 'parser', 'ner', 'attribute_ruler', 'lemmatizer']
print(nlp.pipeline)
# [('tok2vec', <spacy.pipeline.Tok2Vec>), ('tagger', <spacy.pipeline.Tagger>), ('parser', <spacy.pipeline.DependencyParser>), ('ner', <spacy.pipeline.EntityRecognizer>), ('attribute_ruler', <spacy.pipeline.AttributeRuler>), ('lemmatizer', <spacy.lang.en.lemmatizer.EnglishLemmatizer>)]
print(nlp.pipe_names)
# ['tok2vec', 'tagger', 'parser', 'ner', 'attribute_ruler', 'lemmatizer']
内置管道组件
spaCy
自带了几个使用字符串名称注册的内置管道组件。这意味着你可以通过调用nlp.add_pipe
来初始化它们,只要有它们的名字,spaCy
就知道如何创建它们。有关可用管道组件和组件功能的完整列表,请参阅API 文档。
nlp = spacy.blank("en")
nlp.add_pipe("sentencizer")
# add_pipe returns the added component
ruler = nlp.add_pipe("entity_ruler")
nlp = spacy.blank("en")
nlp.add_pipe("sentencizer")
# add_pipe returns the added component
ruler = nlp.add_pipe("entity_ruler")
禁用、排除和修改组件
如果你不需要管道的特定组件,例如,标记器(tagger
)或解析器(parser
),你可以禁用或排除它。这有时会产生很大的不同并提高加载和推理速度。你可以使用两种不同的机制:
- **禁用:**组件及其数据将随管道一起加载,但默认情况下会被禁用,并且不会作为处理管道的一部分运行。要运行它,你可以通过显式调用
nlp.enable_pipe
来启用它。 当你保存nlp
对象时,依然会包含禁用的组件,但默认情况是禁用状态。 - **排除:**不会加载组件及其数据。加载管道后,将不会引用被排除的组件。
禁用和排除的组件名称可以作为列表提供给spacy.load
来实现。
💡 可选的管道组件
该disable
机制使分布式部署带有可选组件的管道包变得容易,你可以在运行时启用或禁用这些可选组件。例如,你的管道可能包含一个基于统计的句子分割和基于规则句子分割组件,你就可以根据你的用例选择运行哪一个。
例如,
spaCy
中的en_core_web_sm
管道,包含了parser
和senter
用于执行句子分割,但senter
默认情况下是禁用的。
# Load the pipeline without the entity recognizer
nlp = spacy.load("en_core_web_sm", exclude=["ner"])
# Load the tagger and parser but don't enable them
nlp = spacy.load("en_core_web_sm", disable=["tagger", "parser"])
# Explicitly enable the tagger later on
nlp.enable_pipe("tagger")
# Load the pipeline without the entity recognizer
nlp = spacy.load("en_core_web_sm", exclude=["ner"])
# Load the tagger and parser but don't enable them
nlp = spacy.load("en_core_web_sm", disable=["tagger", "parser"])
# Explicitly enable the tagger later on
nlp.enable_pipe("tagger")
v3.0版本的更改
从v3.0
开始,disable
关键字参数指定要加载但禁用的组件,而不是根本不加载的组件。现在可以使用新的exclude
关键字参数单独指定不需要加载的组件。
作为快捷方式,你可以使用nlp.select_pipes
上下文管理器临时禁用给定块的某些组件。在with
代码块结束时,禁用的管道组件将自动恢复。或者,保存select_pipes
返回的对象, 直到该对象调用restore()
函数,即可恢复禁用的组件。如果你想防止大块不必要的代码缩进,这会很有用。
# 在代码块中禁用组件 DISABLE FOR BLOCK
# 1. Use as a context manager
with nlp.select_pipes(disable=["tagger", "parser", "lemmatizer"]):
doc = nlp("I won't be tagged and parsed")
doc = nlp("I will be tagged and parsed")
# 2. Restore manually
disabled = nlp.select_pipes(disable="ner")
doc = nlp("I won't have named entities")
disabled.restore()
# 在代码块中禁用组件 DISABLE FOR BLOCK
# 1. Use as a context manager
with nlp.select_pipes(disable=["tagger", "parser", "lemmatizer"]):
doc = nlp("I won't be tagged and parsed")
doc = nlp("I will be tagged and parsed")
# 2. Restore manually
disabled = nlp.select_pipes(disable="ner")
doc = nlp("I won't have named entities")
disabled.restore()
如果要禁用除一个或几个管道之外的所有管道,可以使用enable
关键字。就像disable
关键字一样,它需要一个管道名称列表,或者一个只定义一个管道的字符串。
# Enable only the parser
with nlp.select_pipes(enable="parser"):
doc = nlp("I will only be parsed")
# Enable only the parser
with nlp.select_pipes(enable="parser"):
doc = nlp("I will only be parsed")
nlp.pipe
方法也支持disable
关键字,如果你只想在处理期间禁用某些组件:
for doc in nlp.pipe(texts, disable=["tagger", "parser", "lemmatizer"]):
# Do something with the doc here
for doc in nlp.pipe(texts, disable=["tagger", "parser", "lemmatizer"]):
# Do something with the doc here
最后,你还可以使用remove_pipe
方法从现有管道中删除管道组件, rename_pipe
方法重命名管道组件,或replace_pipe
方法用自定义组件替换现有组件(更多详细信息,请参见自定义组件部分)。
nlp.remove_pipe("parser")
nlp.rename_pipe("ner", "entityrecognizer")
nlp.replace_pipe("tagger", "my_custom_tagger")
nlp.remove_pipe("parser")
nlp.rename_pipe("ner", "entityrecognizer")
nlp.replace_pipe("tagger", "my_custom_tagger")
Language
对象公开了不同的属性 ,让你可以检查所有可用组件和当前管道中的组件。
nlp = spacy.blank("en")
nlp.add_pipe("ner")
nlp.add_pipe("textcat")
assert nlp.pipe_names == ["ner", "textcat"]
nlp.disable_pipe("ner")
assert nlp.pipe_names == ["textcat"]
assert nlp.component_names == ["ner", "textcat"]
assert nlp.disabled == ["ner"]
nlp = spacy.blank("en")
nlp.add_pipe("ner")
nlp.add_pipe("textcat")
assert nlp.pipe_names == ["ner", "textcat"]
nlp.disable_pipe("ner")
assert nlp.pipe_names == ["textcat"]
assert nlp.component_names == ["ner", "textcat"]
assert nlp.disabled == ["ner"]
(所以,这里的pipeline
和components
的区别就是是否包含禁用的组件,所以工程中一般使用pipeline
即可。)
名称 | 描述 |
---|---|
nlp.pipeline | 返回按顺序排列的处理管道的(name, component)元组列表 |
nlp.pipe_names | 按顺序排列的管道组件名称 |
nlp.components | (name, component) 元组列表,包含禁用(disabled)的组件. |
nlp.component_names | 所有的组件名列表,包含禁用(disabled)的组件 |
nlp.disabled | 当前禁用的组件的名称 |
从现有管道采购组件 V 3.0
独立的管道组件可以跨管道重用。除了添加新的空白组件之外,你还可以通过在nlp.add_pipe
方法中设定source
参数从已经过训练的的管道中复制组件。 然后,第一个参数将被解释为源管道中组件的名称。例如,"ner"
。这对于训练管道特别有用, 因为它允许你混合和匹配组件,并创建完全自定义的管道包,其中包含已有的组件和基于数据训练的新组件。
已训练组件的重要说明
在跨管道重用组件时,请记住词汇(vocabulary
)、 向量(vectors
)和模型的设置必须匹配。如果经过训练的管道包含 词向量并且组件将它们用作特征,则你将其复制到的管道需要具有相同的可用向量。否则,它将无法进行相同的预测。
在训练的配置中
除了提供一个
factory
之外,训练配置中的组件块也可以定义一个source
。 该字符串需要是可加载的spaCy
管道包或路径。
[components.ner]
source = "en_core_web_sm"
component = "ner"
[components.ner]
source = "en_core_web_sm"
component = "ner"
默认情况下,源组件将在训练期间使用你的数据进行更新。如果你想按原样保留组件,你可以“冻结”它,但必须保证管道未使用共享Tok2Vec
层:
[training]
frozen_components = ["ner"]
[training]
frozen_components = ["ner"]
# spaCy v3.0 · Python 3
import spacy
# The source pipeline with different components
source_nlp = spacy.load("en_core_web_sm")
print(source_nlp.pipe_names)
# Add only the entity recognizer to the new blank pipeline
# 将实体识别组件添加到空白管道中
nlp = spacy.blank("en")
nlp.add_pipe("ner", source=source_nlp)
# 这里的source实际上使用了管道
print(nlp.pipe_names)
# spaCy v3.0 · Python 3
import spacy
# The source pipeline with different components
source_nlp = spacy.load("en_core_web_sm")
print(source_nlp.pipe_names)
# Add only the entity recognizer to the new blank pipeline
# 将实体识别组件添加到空白管道中
nlp = spacy.blank("en")
nlp.add_pipe("ner", source=source_nlp)
# 这里的source实际上使用了管道
print(nlp.pipe_names)
输出:
['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']
['ner']
['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']
['ner']
分析管道组件 V 3.0
nlp.analyze_pipes
方法能够分析当前管道中的组件,并输出相关信息,例如它们在Doc
和Token
上设置的属性,不管他们是否重新标记了Doc
或者他们在训练期间产生的分数(scores)。如果前一个组件没有设置当前组件需要的值,就会显示警告。例如,使用了实体链接器但没有在这之前运行实体识别组件。设置pretty=True
将漂亮地打印一个表格,而不是只返回结构化数据。
✏️ 小尝试
在"entity_linker"
之前添加"ner"
和"sentencizer"
组件。这个分析就应该没有问题,因为已经满足了要求。
# spaCy v3.0 · Python 3
import spacy
nlp = spacy.blank("en")
nlp.add_pipe("tagger")
# This is a problem because it needs entities and sentence boundaries
# 这里将会抛出问题,因为这之前需要实体和句子切分组件处理
nlp.add_pipe("entity_linker")
analysis = nlp.analyze_pipes(pretty=True)
# spaCy v3.0 · Python 3
import spacy
nlp = spacy.blank("en")
nlp.add_pipe("tagger")
# This is a problem because it needs entities and sentence boundaries
# 这里将会抛出问题,因为这之前需要实体和句子切分组件处理
nlp.add_pipe("entity_linker")
analysis = nlp.analyze_pipes(pretty=True)
!重要提示
管道分析是静态的,实际上也并不运行组件。这意味着它依赖于组件本身提供的信息。如果自定义组件声明它分配了一个属性但它没有分配,则管道分析将不会捕捉到它。
创建自定义管道组件
管道组件是一个函数,它接收一个Doc
对象,修改后在并返回一个Doc
对象。例如,通过使用当前的权重来进行预测并在文档上设置一些注释。通过向管道添加组件,你可以在处理过程中的任何时候访问Doc
,而不是只能在结束后才能修改它。
from spacy.language import Language
@Language.component("my_component")
def my_component(doc):
# Do something to the doc here
return doc
from spacy.language import Language
@Language.component("my_component")
def my_component(doc):
# Do something to the doc here
return doc
参数 | 类型 | 描述 |
---|---|---|
doc | Doc | Doc 是由前一个组件处理得到的对象。 |
RETURNS | Doc | Doc 由当前管道组件处理并返回的对象。 |
@Language.component装饰器可以让你把一个简单的函数变成一个管道组件。它至少需要一个参数,即组件工厂的名称。然后,就可以使用此名称将组件的实例添加到管道中。它也可以你的管道配置中进行配置,因此你可以使用你的组件保存、加载和训练管道。
可以使用add_pipe
方法将自定义组件添加到管道中。或者,可以指定添加位置,在指定组件在之前或之后添加它;把组件添加在管道开始或者最后;或者重新定义名称。如果组件未设置名称也没有name
属性,则使用函数名称作为名称。
nlp.add_pipe("my_component")
nlp.add_pipe("my_component", first=True)
nlp.add_pipe("my_component", before="parser")
nlp.add_pipe("my_component")
nlp.add_pipe("my_component", first=True)
nlp.add_pipe("my_component", before="parser")
参数 | 描述 |
---|---|
last | 如果设置为True,则在管道中最后添加组件(默认)。类型:bool |
first | 如果设置为True,则在管道开始位置添加组件。类型:bool |
before | 在组件之前添加新组件,使用字符串名称或索引定位组件。类型:Union[str, int] |
after | 在组件之后添加新组件,使用字符串名称或索引定位组件。类型:Union[str, int] |
v3.0版本更改
从v3.0
开始,组件需要使用@Language.component
或者@Language.factory
装饰器注册,然后spaCy
知道这个函数是一个组件。nlp.add_pipe
现在采用组件工厂的 字符串名称而不是组件函数。这不仅为你节省了代码行,还允许spaCy
验证和跟踪你的自定义组件,并确保它们可以被保存和加载。
- ruler = nlp.create_pipe("entity_ruler")
- nlp.add_pipe(ruler)
+ ruler = nlp.add_pipe("entity_ruler")
- ruler = nlp.create_pipe("entity_ruler")
- nlp.add_pipe(ruler)
+ ruler = nlp.add_pipe("entity_ruler")
示例:简单的无状态管道组件
以下管道中的组件接收Doc
并打印它的相关信息:标记(tokens)的数量、词性标注的标签和基于文档长度的条件消息。@Language.component
装饰器允许你以"info_component"
名称注册组件。
✏️ 小尝试
- 通过设置
first=True
在管道开始位置添加组件。你会看到词性标记为空,因为该组件现在在tagger
组件之前运行,这时标记尚不可用。 - 更改组件
name
或删除name
参数。将会看到此更改反映在nlp.pipe_names
中。
# spaCy v3.0 · Python 3
import spacy
from spacy.language import Language
@Language.component("info_component")
def my_component(doc):
print(f"After tokenization, this doc has {len(doc)} tokens.")
print("The part-of-speech tags are:", [token.pos_ for token in doc])
if len(doc) < 10:
print("This is a pretty short document.")
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("info_component", name="print_info", last=True)
print(nlp.pipe_names) # ['tagger', 'parser', 'ner', 'print_info']
doc = nlp("This is a sentence.")
# spaCy v3.0 · Python 3
import spacy
from spacy.language import Language
@Language.component("info_component")
def my_component(doc):
print(f"After tokenization, this doc has {len(doc)} tokens.")
print("The part-of-speech tags are:", [token.pos_ for token in doc])
if len(doc) < 10:
print("This is a pretty short document.")
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("info_component", name="print_info", last=True)
print(nlp.pipe_names) # ['tagger', 'parser', 'ner', 'print_info']
doc = nlp("This is a sentence.")
输出:
['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner', 'print_info']
After tokenization, this doc has 5 tokens.
The part-of-speech tags are: ['DET', 'AUX', 'DET', 'NOUN', 'PUNCT']
This is a pretty short document.
['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner', 'print_info']
After tokenization, this doc has 5 tokens.
The part-of-speech tags are: ['DET', 'AUX', 'DET', 'NOUN', 'PUNCT']
This is a pretty short document.
这是管道组件的另一个示例,它实现了自定义逻辑以改进现有的句子边界组件。自定义逻辑应该在标记化(tokennization)之后,并在依赖解析之前。这样,解析器也可以利用句子边界。
✏️ 小尝试
- 选择使用和不使用自定义管道组件时,打印
[token.dep_ for token in doc]
将看到预测的句子边界发生变化。 - 移除
else
块。所有其他标记(tokens)的is_sent_start
将设置为None
(缺失值),解析器将在两者之间分配句子边界。
# spaCy v3.0 · Python 3
import spacy
from spacy.language import Language
@Language.component("custom_sentencizer")
def custom_sentencizer(doc):
for i, token in enumerate(doc[:-2]):
# Define sentence start if pipe + titlecase token
if token.text == "|" and doc[i + 1].is_title:
doc[i + 1].is_sent_start = True
else:
# Explicitly set sentence start to False otherwise, to tell
# the parser to leave those tokens alone
doc[i + 1].is_sent_start = False
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("custom_sentencizer", before="parser") # Insert before the parser
doc = nlp("This is. A sentence. | This is. Another sentence.")
for sent in doc.sents:
print(sent.text)
# spaCy v3.0 · Python 3
import spacy
from spacy.language import Language
@Language.component("custom_sentencizer")
def custom_sentencizer(doc):
for i, token in enumerate(doc[:-2]):
# Define sentence start if pipe + titlecase token
if token.text == "|" and doc[i + 1].is_title:
doc[i + 1].is_sent_start = True
else:
# Explicitly set sentence start to False otherwise, to tell
# the parser to leave those tokens alone
doc[i + 1].is_sent_start = False
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("custom_sentencizer", before="parser") # Insert before the parser
doc = nlp("This is. A sentence. | This is. Another sentence.")
for sent in doc.sents:
print(sent.text)
输出:
This is. A sentence. |
This is. Another sentence.
This is. A sentence. |
This is. Another sentence.
组件工厂和有状态组件
组件工厂是接受参数设置并返回管道组件函数的可调用对象。如果你的组件是有状态的、你需要自定义组件的创建或者你需要访问当前nlp
对象或共享词汇(vocab)时,这就将非常有用。组件工厂可以使用@Language.factory
装饰器注册,函数需要至少两个命名参数,当组件被添加到管道时,它们会自动填充:
from spacy.language import Language
# 组件是component,这里是factory
@Language.factory("my_component")
def my_component(nlp, name):
return MyComponent()
from spacy.language import Language
# 组件是component,这里是factory
@Language.factory("my_component")
def my_component(nlp, name):
return MyComponent()
参数 | 描述 |
---|---|
nlp | 当前nlp 对象。可用于访问共享词汇(vocab)。类型:Language |
name | 管道中组件的实例名称。这使你可以识别同一组件的不同实例。类型:str |
nlp.add_pipe
方法中,用户可以通过config
参数传入参数设置。@Language.factory
装饰器还允许你定义default_config
用作后备。
# 使用config
import spacy
from spacy.language import Language
@Language.factory("my_component", default_config={"some_setting": True})
def my_component(nlp, name, some_setting: bool):
return MyComponent(some_setting=some_setting)
nlp = spacy.blank("en")
nlp.add_pipe("my_component", config={"some_setting": False})
# 使用config
import spacy
from spacy.language import Language
@Language.factory("my_component", default_config={"some_setting": True})
def my_component(nlp, name, some_setting: bool):
return MyComponent(some_setting=some_setting)
nlp = spacy.blank("en")
nlp.add_pipe("my_component", config={"some_setting": False})
@Language.factory
与@Language.component
有何不同?@Language.component
装饰器本质上是不需要任何设置的无状态管道组件的快捷方式。这意味着如果没有要传递的状态,你不必总是编写一个返回你的函数的函数。spaCy
可以处理这个问题。以下两段代码实际上是等效的:
# 使用 @Language.factory 创建无状态组件
@Language.factory("my_component")
def create_my_component():
def my_component(doc):
# Do something to the doc
return doc
return my_component
# 使用 @Language.component 创建无状态组件
@Language.component("my_component")
def my_component(doc):
# Do something to the doc
return doc
# 使用 @Language.factory 创建无状态组件
@Language.factory("my_component")
def create_my_component():
def my_component(doc):
# Do something to the doc
return doc
return my_component
# 使用 @Language.component 创建无状态组件
@Language.component("my_component")
def my_component(doc):
# Do something to the doc
return doc
可以用
@Language.factory
装饰器装饰类吗?
可以,@Language.factory
装饰器可以装饰函数或类。如果它装饰一个类,它期望__init__
方法接受参数nlp
和name
,并填充配置中的所有参数。也就是说,这能够使你的工厂成为一个独立的功能通常更简洁、更直观。这也是spaCy
内部的做法。
特定于语言的工厂 V 3.0
在许多用例中,你可能希望管道组件是特定于语言的。有时这需要每种语言完全不同的实现,有时唯一的区别是设置或数据。spaCy
允许你在Language
基类及其子类(如English
或German
)上注册同名工厂。工厂解析是从特定的子类开始。如果子类没有定义该名称的组件,spaCy
将检查基类(这个过程符合类继承过程)。
这是一个管道组件的示例,该组件重写了token
的规范化形式,Token.norm_
使用特定于语言的查找表。它使用"token_normalizer"
注册了两次,一次使用@English.factory
一次使用@German.factory
:
# spaCy v3.0 · Python 3
from spacy.lang.en import English
from spacy.lang.de import German
class TokenNormalizer:
def __init__(self, norm_table):
self.norm_table = norm_table
def __call__(self, doc):
for token in doc:
# Overwrite the token.norm_ if there's an entry in the data
token.norm_ = self.norm_table.get(token.text, token.norm_)
return doc
@English.factory("token_normalizer")
def create_en_normalizer(nlp, name):
return TokenNormalizer({"realise": "realize", "colour": "color"})
@German.factory("token_normalizer")
def create_de_normalizer(nlp, name):
return TokenNormalizer({"daß": "dass", "wußte": "wusste"})
nlp_en = English()
nlp_en.add_pipe("token_normalizer") # uses the English factory
print([token.norm_ for token in nlp_en("realise colour daß wußte")])
nlp_de = German()
nlp_de.add_pipe("token_normalizer") # uses the German factory
print([token.norm_ for token in nlp_de("realise colour daß wußte")])
# spaCy v3.0 · Python 3
from spacy.lang.en import English
from spacy.lang.de import German
class TokenNormalizer:
def __init__(self, norm_table):
self.norm_table = norm_table
def __call__(self, doc):
for token in doc:
# Overwrite the token.norm_ if there's an entry in the data
token.norm_ = self.norm_table.get(token.text, token.norm_)
return doc
@English.factory("token_normalizer")
def create_en_normalizer(nlp, name):
return TokenNormalizer({"realise": "realize", "colour": "color"})
@German.factory("token_normalizer")
def create_de_normalizer(nlp, name):
return TokenNormalizer({"daß": "dass", "wußte": "wusste"})
nlp_en = English()
nlp_en.add_pipe("token_normalizer") # uses the English factory
print([token.norm_ for token in nlp_en("realise colour daß wußte")])
nlp_de = German()
nlp_de.add_pipe("token_normalizer") # uses the German factory
print([token.norm_ for token in nlp_de("realise colour daß wußte")])
输出:
['realize', 'color', 'daß', 'wußte']
['realise', 'colour', 'dass', 'wusste']
['realize', 'color', 'daß', 'wußte']
['realise', 'colour', 'dass', 'wusste']
底层执行过程
在底层,特定于语言的工厂被添加到factories registry
以语言代码为前缀,例如"en.token_normalizer"
。在处理nlp.add_pipe
中的工厂时,spaCy
首先使用nlp.lang
检查工厂的特定语言版本,如果没有可用版本,则返回查找常规工厂名称。
示例:带设置的有状态组件
此示例展示了一个用于处理首字母缩略词的有状态管道组件:基于字典,它将检测首字母缩略词及其在两个方向上的扩展形式,并将它们作为自定义doc._.acronyms
扩展属性添加到列表中。在底层实现中使用PhraseMatcher
查找短语的实例。
工厂函数接受三个参数:spaCy
自动传入的共享nlp
对象和组件实例name
,以及case_sensitive
参数配置,指出匹配和首字母缩略词检测需要区分大小写。
✏️ 小尝试
- 通过
config
更改nlp.add_pipe
的配置设置,并设置"case_sensitive"
为True
。你应该会看到不再检测到“LOL”
的扩展首字母缩写词。 - 向字典中添加更多术语并更新处理后的文本,以便检测到它们。
- 向
nlp.add_pipe
中添加name
参数来更改组件名称。打印nlp.pipe_names
以查看管道中更改。 - 使用
print(nlp.config.to_str())
打印当前nlp
对象的配置,并检查[components]
块。你应该会看到acronyms
组件的条目,引用了工厂acronyms
和配置设置。
# spaCy v3.0 · Python 3
from spacy.language import Language
from spacy.tokens import Doc
from spacy.matcher import PhraseMatcher
import spacy
DICTIONARY = {"lol": "laughing out loud", "brb": "be right back"}
DICTIONARY.update({value: key for key, value in DICTIONARY.items()})
@Language.factory("acronyms", default_config={"case_sensitive": False})
def create_acronym_component(nlp: Language, name: str, case_sensitive: bool):
return AcronymComponent(nlp, case_sensitive)
class AcronymComponent:
def __init__(self, nlp: Language, case_sensitive: bool):
# Create the matcher and match on Token.lower if case-insensitive
# 如果是大小写不敏感,就创建Matcher匹配Token.lower
matcher_attr = "TEXT" if case_sensitive else "LOWER"
self.matcher = PhraseMatcher(nlp.vocab, attr=matcher_attr)
self.matcher.add("ACRONYMS", [nlp.make_doc(term) for term in DICTIONARY])
self.case_sensitive = case_sensitive
# Register custom extension on the Doc
if not Doc.has_extension("acronyms"):
Doc.set_extension("acronyms", default=[])
def __call__(self, doc: Doc) -> Doc:
# Add the matched spans when doc is processed
# doc执行时,添加匹配span
for _, start, end in self.matcher(doc):
span = doc[start:end]
acronym = DICTIONARY.get(span.text if self.case_sensitive else span.text.lower())
doc._.acronyms.append((span, acronym))
return doc
# Add the component to the pipeline and configure it
nlp = spacy.blank("en")
nlp.add_pipe("acronyms", config={"case_sensitive": False})
# Process a doc and see the results
doc = nlp("LOL, be right back")
print(doc._.acronyms)
# spaCy v3.0 · Python 3
from spacy.language import Language
from spacy.tokens import Doc
from spacy.matcher import PhraseMatcher
import spacy
DICTIONARY = {"lol": "laughing out loud", "brb": "be right back"}
DICTIONARY.update({value: key for key, value in DICTIONARY.items()})
@Language.factory("acronyms", default_config={"case_sensitive": False})
def create_acronym_component(nlp: Language, name: str, case_sensitive: bool):
return AcronymComponent(nlp, case_sensitive)
class AcronymComponent:
def __init__(self, nlp: Language, case_sensitive: bool):
# Create the matcher and match on Token.lower if case-insensitive
# 如果是大小写不敏感,就创建Matcher匹配Token.lower
matcher_attr = "TEXT" if case_sensitive else "LOWER"
self.matcher = PhraseMatcher(nlp.vocab, attr=matcher_attr)
self.matcher.add("ACRONYMS", [nlp.make_doc(term) for term in DICTIONARY])
self.case_sensitive = case_sensitive
# Register custom extension on the Doc
if not Doc.has_extension("acronyms"):
Doc.set_extension("acronyms", default=[])
def __call__(self, doc: Doc) -> Doc:
# Add the matched spans when doc is processed
# doc执行时,添加匹配span
for _, start, end in self.matcher(doc):
span = doc[start:end]
acronym = DICTIONARY.get(span.text if self.case_sensitive else span.text.lower())
doc._.acronyms.append((span, acronym))
return doc
# Add the component to the pipeline and configure it
nlp = spacy.blank("en")
nlp.add_pipe("acronyms", config={"case_sensitive": False})
# Process a doc and see the results
doc = nlp("LOL, be right back")
print(doc._.acronyms)
输出:
[(LOL, 'laughing out loud'), (be right back, 'brb')]
[(LOL, 'laughing out loud'), (be right back, 'brb')]
初始化和序列化组件数据
许多有状态组件依赖于字典和查找表等数据资源,这些在理想情况下应该是可配置的。例如,可以将上例中的DICTIONARY
设为注册函数的参数,这样AcronymComponent
就可以与不同的数据一起使用。较好的解决方案是使其成为组件工厂的参数,然后使用不同的字典对其进行初始化。
CONFIG.CFG
配置文件
[components.acronyms.data]
# 🚨 Problem: you don't want the data in the config
# !问题,你应该不希望在配置文件中存储大量数据
lol = "laugh out loud"
brb = "be right back"
[components.acronyms.data]
# 🚨 Problem: you don't want the data in the config
# !问题,你应该不希望在配置文件中存储大量数据
lol = "laugh out loud"
brb = "be right back"
@Language.factory("acronyms", default_config={"data": {}, "case_sensitive": False})
def create_acronym_component(nlp: Language, name: str, data: Dict[str, str], case_sensitive: bool):
# 🚨 Problem: data ends up in the config file
return AcronymComponent(nlp, data, case_sensitive)
@Language.factory("acronyms", default_config={"data": {}, "case_sensitive": False})
def create_acronym_component(nlp: Language, name: str, data: Dict[str, str], case_sensitive: bool):
# 🚨 Problem: data ends up in the config file
return AcronymComponent(nlp, data, case_sensitive)
然而,直接传入字典也是有问题的,因为这意味着如果一个组件保存了它的配置和设置,config.cfg
将包含整个数据的转储,因为这是创建组件时使用的配置。并且如果数据不是JSON
可序列化的,它也会失败。
选项 1:使用注册函数
**优点:**可以在
Python
中加载任何东西,易于通过设置进行添加和配置
**缺点:**要求函数及其依赖项在运行时可用
如果你传入的内容不是JSON
可序列化的数据。例如,像模型这样的自定义对象,就不可能保存其组件配置,因为spaCy
无法知道该对象是如何创建的,以及再次创建时需要做什么。这使得使用自定义组件保存、加载和训练自定义管道变得更加困难。一个简单的解决方案是注册一个返回资源的函数。注册表(registry)可以完成字符串名称和资源函数的映射,然后只需给定一个名称和可选的参数就可以创建对象,因为spaCy
会知道如何重新创建该对象。要注册一个返回自定义字典的函数,你可以使用@spacy.registry.misc
装饰器,它只包含一个参数:名称
什么是
MISC
注册表?registry
为不同类型的函数提供不同的类别。例如,模型架构、分词器(tokenizers
)或批处理器。misc
用于不属于其他任何地方的杂项功能。
# REGISTERED FUNCTION FOR ASSETS
# 资产注册函数
@spacy.registry.misc("acronyms.slang_dict.v1")
def create_acronyms_slang_dict():
dictionary = {"lol": "laughing out loud", "brb": "be right back"}
dictionary.update({value: key for key, value in dictionary.items()})
return dictionary
# REGISTERED FUNCTION FOR ASSETS
# 资产注册函数
@spacy.registry.misc("acronyms.slang_dict.v1")
def create_acronyms_slang_dict():
dictionary = {"lol": "laughing out loud", "brb": "be right back"}
dictionary.update({value: key for key, value in dictionary.items()})
return dictionary
在你的default_config
(以及稍后在你的 训练配置)中,就可以使用@misc
作为key
引用名称为"acronyms.slang_dict.v1"
的注册函数。这告诉spaCy
如何创建值,当你的组件被创建时,注册函数的结果将会传入。CONFIG.CFG
文件
[components.acronyms]
factory = "acronyms"
[components.acronyms.data]
@misc = "acronyms.slang_dict.v1"
[components.acronyms]
factory = "acronyms"
[components.acronyms.data]
@misc = "acronyms.slang_dict.v1"
- default_config = {"dictionary:" DICTIONARY}
+ default_config = {"dictionary": {"@misc": "acronyms.slang_dict.v1"}}
- default_config = {"dictionary:" DICTIONARY}
+ default_config = {"dictionary": {"@misc": "acronyms.slang_dict.v1"}}
(这里类似接口的味道,"acronyms.slang_dict.v1"
就是接口,任何数据的变化并不会影响接口)
使用注册函数还意味着你可以轻松地将自定义组件包含在你训练的管道中。为了确保spaCy
可以找到你的自定义@misc
函数,你可以通过--code
参数传入一个Python
文件。如果其他人需要使用你的组件,那么当他们自定义数据时只需要注册一个自己的函数并将更换数据名称。顺便说一下,注册函数也可以使用参数,并且这些参数也可以在配置中定义。你可以在使用自定义代码进行训练。
选项 2:使用管道保存数据并在初始化时加载一次
**优点:**让组件保存和加载自己的数据并反映用户更改,在训练前加载数据资产,而无需在运行时依赖它们
**缺点:**需要更多的组件方法,更复杂的配置和数据流
就像在你调用nlp.to_disk
时,模型会保存其二进制权重一样,组件还可以序列化其他任何数据资产,比如一个首字母缩略词字典。如果一个管道组件实现了它自己的to_disk
和from_disk
方法,它们将被nlp.to_disk
自动调用并接收到要保存或加载的目录的路径。然后该组件可以执行任何自定义保存或加载。如果用户对组件数据进行了更改,则这些更改将在nlp
对象保存时反映出来。有关更多示例,请参阅有关序列化方法的使用指南。
关于数据路径
spaCy
传递给序列化方法的path
参数由用户提供的路径和组件名称的目录组成。这意味着当你调用nlp.to_disk("/path")
时,acronyms
组件将收到目录路径/path/acronyms
,然后可以在该目录中创建文件。
自定义序列化方法 CUSTOM SERIALIZATION METHODS
import srsly
class AcronymComponent:
# other methods here...
def to_disk(self, path, exclude=tuple()):
srsly.write_json(path / "data.json", self.data)
def from_disk(self, path, exclude=tuple()):
self.data = srsly.read_json(path / "data.json")
return self
import srsly
class AcronymComponent:
# other methods here...
def to_disk(self, path, exclude=tuple()):
srsly.write_json(path / "data.json", self.data)
def from_disk(self, path, exclude=tuple()):
self.data = srsly.read_json(path / "data.json")
return self
srsly库
python
的高性能序列化工具,这个包把一些优秀的python
序列化库打包成一个单独的包,应用高级的api
使得编写序列化代码变得容易。目前支持JSON
,JSONL
,MessagePack
,Pickle
以及YAML
格式的序列化。更多详情参见srsly · PyPI
现在组件可以保存到目录并从目录加载。剩下的唯一问题就是如何加载初始数据?在Python
中,你可以自己调用管道的from_disk
方法。但是,如果你将组件添加到你的训练配置中,spaCy
就需要知道如何设置它,从开始到结束,包括用于初始化它的数据。
当然你也可以使用已注册的函数或文件加载器,例如srsly.read_json.v1
作为组件工厂的参数。但这种方法是有问题的:每次创建组件时组件工厂都会运行。这意味着它在训练前创建nlp
对象时就会运行,每次加载你的管道时也会运行。因此,你的运行时管道要么依赖于文件系统上的本地路径;要么加载两次:一次是在创建组件时,另一次是调用from_disk
加载数据时。CONFIG.CFG
文件
[components.acronyms.data]
# 🚨 Problem: Runtime pipeline depends on local path
# 问题:运行时管道依赖于本地路径
@readers = "srsly.read_json.v1"
path = "/path/to/slang_dict.json"
[components.acronyms.data]
# 🚨 Problem: Runtime pipeline depends on local path
# 问题:运行时管道依赖于本地路径
@readers = "srsly.read_json.v1"
path = "/path/to/slang_dict.json"
CONFIG.CFG
文件
[components.acronyms.data]
# 🚨 Problem: this always runs
# 问题:这会加载多次
@misc = "acronyms.slang_dict.v1"
[components.acronyms.data]
# 🚨 Problem: this always runs
# 问题:这会加载多次
@misc = "acronyms.slang_dict.v1"
@Language.factory("acronyms", default_config={"data": {}, "case_sensitive": False})
def create_acronym_component(nlp: Language, name: str, data: Dict[str, str], case_sensitive: bool):
# 🚨 Problem: data will be loaded every time component is created
return AcronymComponent(nlp, data, case_sensitive)
@Language.factory("acronyms", default_config={"data": {}, "case_sensitive": False})
def create_acronym_component(nlp: Language, name: str, data: Dict[str, str], case_sensitive: bool):
# 🚨 Problem: data will be loaded every time component is created
return AcronymComponent(nlp, data, case_sensitive)
为了解决这个问题,你的组件可以实现一个单独的initialize
方法,方法如果可用就会被nlp.initialize
调用。这通常发生在训练之前,而不是在管道加载的时候运行。有关更多背景信息,请参阅配置生命周期和 自定义初始化的使用指南。
组件的initialize
方法需要至少两个命名参数:一个get_examples
回调函数,用于访问训练示例;以及当前nlp
对象。这主要由可训练组件使用,这样就可以从数据中初始化模型和标签方案,这里我们可以忽略这些参数。该方法的所有其他参数都可以通过配置来定义,在这种情况下通常是一个data
字典。
CONFIG.CFG
配置文件
[initialize.components.my_component]
[initialize.components.my_component.data]
# ✅ This only runs on initialization
# 这仅仅在初始化时运行
@readers = "srsly.read_json.v1"
path = "/path/to/slang_dict.json"
[initialize.components.my_component]
[initialize.components.my_component.data]
# ✅ This only runs on initialization
# 这仅仅在初始化时运行
@readers = "srsly.read_json.v1"
path = "/path/to/slang_dict.json"
自定义初始化方法
class AcronymComponent:
def __init__(self):
self.data = {}
def initialize(self, get_examples=None, nlp=None, data={}):
self.data = data
class AcronymComponent:
def __init__(self):
self.data = {}
def initialize(self, get_examples=None, nlp=None, data={}):
self.data = data
当nlp.initialize
在训练之前运行(或在你自己的代码中调用它时),就会加载[initialize]
配置块并用于构造nlp
对象。然后,自定义首字母缩略词组件将传递从JSON
文件加载的数据。训练完成后,将运行组件的to_disk
方法将nlp
对象保存到磁盘。当spaCy
稍后加载该管道时,调用from_disk
方法将重新加载数据。
Python 类型提示和验证 V 3.0
spaCy
的configs
基于机器学习库Thinc's
配置系统,它支持 类型提示,甚至使用pydantic
实现先进的类型注释 。 如果你的组件工厂提供类型提示,则传入的值将根据预期类型进行检查。如果该值无法转换为整数,spaCy
将引发错误。pydantic
还提供了严格的类型,如StrictFloat
,这将强制该值是一个整数,如果不是则引发错误,例如,如果你的配置定义了一个浮点数。
如果你不使用
strict types
,则仍会接受可以强制转换为给定类型的值。例如,1
可以转换为float
或bool
类型,但不能转换为List[str]
。但是,如果类型是StrictFloat
,则只接受浮点数。
以下示例显示了用于调试的自定义管道组件。它可以添加到管道中的任何位置,并记录nlp
对象和Doc
通过管道的有关信息。log_level
可以设置显示的log
类型。例如,"INFO"
将显示信息日志,和基本的日志语句。而"DEBUG"
会显示所有信息。同时log_level
被注释为StrictStr
,因此它只接受字符串值。
✏️ 小尝试
- 更改
nlp.add_pipe
的config
使用日志级别"INFO"
。你将看到只会显示logger.info
记录的语句。 - 检查
pydantic
的文档约束类型,为log_level
写一个类型提示只接受完全匹配的字符串值"DEBUG"
,"INFO"
和"CRITICAL"
。
# spaCy v3.0 · Python 3
import spacy
from spacy.language import Language
from spacy.tokens import Doc
from pydantic import StrictStr
import logging
@Language.factory("debug", default_config={"log_level": "DEBUG"})
class DebugComponent:
def __init__(self, nlp: Language, name: str, log_level: StrictStr):
self.logger = logging.getLogger(f"spacy.{name}")
self.logger.setLevel(log_level)
self.logger.info(f"Pipeline: {nlp.pipe_names}")
def __call__(self, doc: Doc) -> Doc:
is_tagged = doc.has_annotation("TAG")
self.logger.debug(f"Doc: {len(doc)} tokens, is tagged: {is_tagged}")
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("debug", config={"log_level": "DEBUG"})
doc = nlp("This is a text...")
# spaCy v3.0 · Python 3
import spacy
from spacy.language import Language
from spacy.tokens import Doc
from pydantic import StrictStr
import logging
@Language.factory("debug", default_config={"log_level": "DEBUG"})
class DebugComponent:
def __init__(self, nlp: Language, name: str, log_level: StrictStr):
self.logger = logging.getLogger(f"spacy.{name}")
self.logger.setLevel(log_level)
self.logger.info(f"Pipeline: {nlp.pipe_names}")
def __call__(self, doc: Doc) -> Doc:
is_tagged = doc.has_annotation("TAG")
self.logger.debug(f"Doc: {len(doc)} tokens, is tagged: {is_tagged}")
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("debug", config={"log_level": "DEBUG"})
doc = nlp("This is a text...")
可训练(Trainable)的组件 V 3.0
我们可以使用spaCy
的TrainablePipe
类实现自己的可训练组件,具有自己的模型实例,可以通过spacy train
对Doc
对象进行预测和更新。这样你就可以将自定义的机器学习组件插入到你的管道中。
你将需要以下内容:
- 模型(model): 一个
Thinc
模型实例。它可以是基于Thinc
实现的模型,也可以是基于PyTorch
、TensorFlow
、MXNet
或完全自定义的解决方案中实现的包装模型。该模型必须采用一个Doc
列表对象作为输入,可以有任何类型的输出。 - TrainablePipe子类(TrainablePipe subclass):
TrainablePipe
的子类,至少实现两个方法:TrainablePipe.predict
和TrainablePipe.set_annotations
。 - 组件工厂(Component factory):使用
@Language.factory
注册的组件工厂,其中可配置nlp
对象、组件name
以及可选设置,并返回可训练组件的实例。
Thinc
Thinc是一个轻量级的深度学习库,它提供了一个优雅的、经过类型检查的、函数式编程的API来合成模型,并支持在其他框架中定义的层,如PyTorch、TensorFlow和MXNet。
你可以将Thinc作为一个接口层、一个独立的工具包或一个灵活的方式来开发新的模型。Thinc 的先前版本已经通过 spaCy 和 Prodigy 在数千家公司的生产中运行。官方称,其编写新的版本是为了让用户组成、配置和部署用他们喜欢的框架构建的自定义模型。
from spacy.pipeline import TrainablePipe
from spacy.language import Language
class TrainableComponent(TrainablePipe):
def predict(self, docs):
...
def set_annotations(self, docs, scores):
...
@Language.factory("my_trainable_component")
def make_component(nlp, name, model):
return TrainableComponent(nlp.vocab, model, name=name)
from spacy.pipeline import TrainablePipe
from spacy.language import Language
class TrainableComponent(TrainablePipe):
def predict(self, docs):
...
def set_annotations(self, docs, scores):
...
@Language.factory("my_trainable_component")
def make_component(nlp, name, model):
return TrainableComponent(nlp.vocab, model, name=name)
名称 | 描述 |
---|---|
predict | 将组件的模型应用于一批Doc 对象(不修改它们)并返回分数(scores)。 |
set_annotations | 修改一批Doc 对象,使用由predict 生成的预计算分数。 |
默认情况下,TrainablePipe.__init__
获取管道中的共享词汇、Model
以及组件实例的名称,实例名称可以用作损失函数中的键(key
)。所有其他关键字参数将变为可用TrainablePipe.cfg
并且也将与组件一起序列化。
为什么组件应该传递一个
Model
实例,而不是创建一个spaCy
的配置系统
自底向上解析管道组件和模型的配置描述。这意味着它将首先从registered architecture
创建一个Model
,验证其参数,然后将对象转发给组件。这意味着配置(config)可以表达非常复杂的嵌套对象树, 但对象不必一直将模型设置传递给组件。它还使组件更加模块化,并允许你在配置中交换不同的架构,并重用模型定义。CONFIG.CFG
(摘录)
[components]
[components.textcat]
factory = "textcat"
labels = []
# This function is created and then passed to the "textcat" component as
# the argument "model"
# 这个函数创建并作为model参数传入textcat组件中
[components.textcat.model]
@architectures = "spacy.TextCatBOW.v2"
exclusive_classes = true
ngram_size = 1
no_output_layer = false
[components.other_textcat]
factory = "textcat"
# This references the [components.textcat.model] block above
# 它引用了上面的[components.textcat.model]块
model = ${components.textcat.model}
labels = []
[components]
[components.textcat]
factory = "textcat"
labels = []
# This function is created and then passed to the "textcat" component as
# the argument "model"
# 这个函数创建并作为model参数传入textcat组件中
[components.textcat.model]
@architectures = "spacy.TextCatBOW.v2"
exclusive_classes = true
ngram_size = 1
no_output_layer = false
[components.other_textcat]
factory = "textcat"
# This references the [components.textcat.model] block above
# 它引用了上面的[components.textcat.model]块
model = ${components.textcat.model}
labels = []
因此,可训练管道组件工厂应该始终采用model
参数而不是在组件内部实例化Model
。要注册自定义架构,可以使用@spacy.registry.architectures
装饰器。另请参阅训练指南
解详细信息。
对于某些用例,还可以重写其他方法来自定义模型的更新、初始化、损失计算以及在训练输出中添加评估分数。
名称 | 描述 |
---|---|
update | 从一批Example 对象中学习,并更新组件的模型。其中,Example 对象包含预测和标准注释, |
initialize | 初始化模型。通过调用Model.initialize 函数完成初始化。并可以通过[initialize] 配置块传递自定义参数,配置块仅在训练期间或调用nlp.initialize 时加载的 ,而不是在运行时。 |
get_loss | 返回损失(loss)的一个元组和Example 对象的一组梯度。 |
score | 为一批Example 对象评分并返回分数字典。@Language.factory 装饰器可以定义组件的default_score_weights ,它可以决定在训练期间哪些键显示分数以及它们如何计入最终分数。 |
📖自定义可训练组件和模型 有关如何实现你自己的可训练组件和模型架构,以及如何将在
PyTorch
或TensorFlow
中实现的现有模型插入你的spaCy
管道的更多详细信息,请参阅层和模型架构
的使用指南。
扩展属性
spaCy
允许你在Doc
、Span
和Token
中设置自定义属性,这些属性和方法可用Doc._
、Span._
和Token._
调用,例如:Token._.my_attr
。 这使你可以存储与应用程序相关的附加信息,向spaCy
中添加新特性和功能,或者使用其他机器学习库实现你自己的训练的模型。它还可以让你利用spaCy
的数据结构和Doc
对象作为“单一事实来源”。
为什么使用
._
而不直接使用顶级属性?
通过._
写入属性而不是直接写入可以隔离属性,并且更容易确保向后兼容性。例如,如果你实现了自己的.coref
属性,但是有一天spaCy
也声明了它,这就会会破坏你的代码。同样,只需查看代码,你就会立即知道哪些是内置的,哪些属性是自定义的。例如,doc.sentiment
是spaCy
的,而doc._.sent_score
就不是。
._
是如何实现的?
你传入set_extension
的扩展的定义(如默认值、方法、getter和setter)将存储在Underscore
类的类属性中。如果写入扩展属性,例如doc._.hello = True
,该数据将存储在Doc.user_data
字典中。为了将underscore
(也就是_
)数据与其他字典条目分开,在元组中字符串"._."
放在名称之前。
有三种主要的扩展类型,可以使用Doc.set_extension
,Span.set_extension
和Token.set_extension
方法进行定义。
描述
- 特性(Attribute)扩展为特性设置默认值,可以随时手动重写。特性扩展就像普通变量一样,是在
Doc
、Span
或Token
上存储任意信息的最快方式。
Doc.set_extension("hello", default=True)
assert doc._.hello
doc._.hello = False
Doc.set_extension("hello", default=True)
assert doc._.hello
doc._.hello = False
- 属性(Property)扩展。定义一个
getter
和setter
函数,setter
函数是可选的。如果未提供setter
,则扩展属性将成为不可变的(这一点与类的成员变量一致)。getter
和setter
函数仅在检索属性时调用,你可以访问先前添加的扩展属性的值。例如,一个Doc
的getter
可以对Token
属性进行平均。对于Span
扩展中,你几乎总是想要使用属性,否则,你必须写入所有可能的Span
到Doc
中以确保值正确。
Doc.set_extension("hello", getter=get_hello_value, setter=set_hello_value)
assert doc._.hello
doc._.hello = "Hi!"
Doc.set_extension("hello", getter=get_hello_value, setter=set_hello_value)
assert doc._.hello
doc._.hello = "Hi!"
- 方法扩展。分配一个可用作对象方法的函数。方法扩展总是不可变的。有关更多详细信息和实现思路,请参阅这些示例。
Doc.set_extension("hello", method=lambda doc, name: f"Hi {name}!")
assert doc._.hello("Bob") == "Hi Bob!"
Doc.set_extension("hello", method=lambda doc, name: f"Hi {name}!")
assert doc._.hello("Bob") == "Hi Bob!"
在访问自定义扩展之前,需要在对象上使用set_extension
方法注册它,例如Doc
。请记住,扩展始终是全局添加的,而不仅仅是在特定实例上添加。如果同名属性已经存在,或者如果你尝试访问尚未注册的属性,spaCy
将抛出AttributeError
异常.
from spacy.tokens import Doc, Span, Token
fruits = ["apple", "pear", "banana", "orange", "strawberry"]
is_fruit_getter = lambda token: token.text in fruits
has_fruit_getter = lambda obj: any([t.text in fruits for t in obj])
Token.set_extension("is_fruit", getter=is_fruit_getter)
Doc.set_extension("has_fruit", getter=has_fruit_getter)
Span.set_extension("has_fruit", getter=has_fruit_getter)
from spacy.tokens import Doc, Span, Token
fruits = ["apple", "pear", "banana", "orange", "strawberry"]
is_fruit_getter = lambda token: token.text in fruits
has_fruit_getter = lambda obj: any([t.text in fruits for t in obj])
Token.set_extension("is_fruit", getter=is_fruit_getter)
Doc.set_extension("has_fruit", getter=has_fruit_getter)
Span.set_extension("has_fruit", getter=has_fruit_getter)
doc = nlp("I have an apple and a melon")
assert doc[3]._.is_fruit # get Token attributes
assert not doc[0]._.is_fruit
assert doc._.has_fruit # get Doc attributes
assert doc[1:4]._.has_fruit # get Span attributes
doc = nlp("I have an apple and a melon")
assert doc[3]._.is_fruit # get Token attributes
assert not doc[0]._.is_fruit
assert doc._.has_fruit # get Doc attributes
assert doc[1:4]._.has_fruit # get Span attributes
注册自定义属性后,你还可以使用内建的set
,get
和has
方法来修改和检索属性。如果你想传入字符串而不是调用doc._.my_attr
,这会更加有用
示例:通过REST API
数据的管道组件(待校对)
这个例子展示了一个管道组件,通过国家数据REST接口获得国家元数据,为国家设置实体属性,并且在Doc
和Span
上设置自定义属性。例如,首都,纬度/经度坐标甚至国旗。
# spaCy v3.0 · Python 3
import requests
from spacy.lang.en import English
from spacy.language import Language
from spacy.matcher import PhraseMatcher
from spacy.tokens import Doc, Span, Token
@Language.factory("rest_countries")
class RESTCountriesComponent:
def __init__(self, nlp, name, label="GPE"):
r = requests.get("https://restcountries.eu/rest/v2/all")
r.raise_for_status() # make sure requests raises an error if it fails
countries = r.json()
# Convert API response to dict keyed by country name for easy lookup
self.countries = {c["name"]: c for c in countries}
self.label = label
# Set up the PhraseMatcher with Doc patterns for each country name
self.matcher = PhraseMatcher(nlp.vocab)
self.matcher.add("COUNTRIES", [nlp.make_doc(c) for c in self.countries.keys()])
# Register attributes on the Span. We'll be overwriting this based on
# the matches, so we're only setting a default value, not a getter.
Span.set_extension("is_country", default=None)
Span.set_extension("country_capital", default=None)
Span.set_extension("country_latlng", default=None)
Span.set_extension("country_flag", default=None)
# Register attribute on Doc via a getter that checks if the Doc
# contains a country entity
Doc.set_extension("has_country", getter=self.has_country)
def __call__(self, doc):
spans = [] # keep the spans for later so we can merge them afterwards
for _, start, end in self.matcher(doc):
# Generate Span representing the entity & set label
entity = Span(doc, start, end, label=self.label)
# Set custom attributes on entity. Can be extended with other data
# returned by the API, like currencies, country code, calling code etc.
entity._.set("is_country", True)
entity._.set("country_capital", self.countries[entity.text]["capital"])
entity._.set("country_latlng", self.countries[entity.text]["latlng"])
entity._.set("country_flag", self.countries[entity.text]["flag"])
spans.append(entity)
# Overwrite doc.ents and add entity – be careful not to replace!
doc.ents = list(doc.ents) + spans
return doc # don't forget to return the Doc!
def has_country(self, doc):
"""Getter for Doc attributes. Since the getter is only called
when we access the attribute, we can refer to the Span's 'is_country'
attribute here, which is already set in the processing step."""
return any([entity._.get("is_country") for entity in doc.ents])
nlp = English()
nlp.add_pipe("rest_countries", config={"label": "GPE"})
doc = nlp("Some text about Colombia and the Czech Republic")
print("Pipeline", nlp.pipe_names) # pipeline contains component name
print("Doc has countries", doc._.has_country) # Doc contains countries
for ent in doc.ents:
if ent._.is_country:
print(ent.text, ent.label_, ent._.country_capital, ent._.country_latlng, ent._.country_flag)
# spaCy v3.0 · Python 3
import requests
from spacy.lang.en import English
from spacy.language import Language
from spacy.matcher import PhraseMatcher
from spacy.tokens import Doc, Span, Token
@Language.factory("rest_countries")
class RESTCountriesComponent:
def __init__(self, nlp, name, label="GPE"):
r = requests.get("https://restcountries.eu/rest/v2/all")
r.raise_for_status() # make sure requests raises an error if it fails
countries = r.json()
# Convert API response to dict keyed by country name for easy lookup
self.countries = {c["name"]: c for c in countries}
self.label = label
# Set up the PhraseMatcher with Doc patterns for each country name
self.matcher = PhraseMatcher(nlp.vocab)
self.matcher.add("COUNTRIES", [nlp.make_doc(c) for c in self.countries.keys()])
# Register attributes on the Span. We'll be overwriting this based on
# the matches, so we're only setting a default value, not a getter.
Span.set_extension("is_country", default=None)
Span.set_extension("country_capital", default=None)
Span.set_extension("country_latlng", default=None)
Span.set_extension("country_flag", default=None)
# Register attribute on Doc via a getter that checks if the Doc
# contains a country entity
Doc.set_extension("has_country", getter=self.has_country)
def __call__(self, doc):
spans = [] # keep the spans for later so we can merge them afterwards
for _, start, end in self.matcher(doc):
# Generate Span representing the entity & set label
entity = Span(doc, start, end, label=self.label)
# Set custom attributes on entity. Can be extended with other data
# returned by the API, like currencies, country code, calling code etc.
entity._.set("is_country", True)
entity._.set("country_capital", self.countries[entity.text]["capital"])
entity._.set("country_latlng", self.countries[entity.text]["latlng"])
entity._.set("country_flag", self.countries[entity.text]["flag"])
spans.append(entity)
# Overwrite doc.ents and add entity – be careful not to replace!
doc.ents = list(doc.ents) + spans
return doc # don't forget to return the Doc!
def has_country(self, doc):
"""Getter for Doc attributes. Since the getter is only called
when we access the attribute, we can refer to the Span's 'is_country'
attribute here, which is already set in the processing step."""
return any([entity._.get("is_country") for entity in doc.ents])
nlp = English()
nlp.add_pipe("rest_countries", config={"label": "GPE"})
doc = nlp("Some text about Colombia and the Czech Republic")
print("Pipeline", nlp.pipe_names) # pipeline contains component name
print("Doc has countries", doc._.has_country) # Doc contains countries
for ent in doc.ents:
if ent._.is_country:
print(ent.text, ent.label_, ent._.country_capital, ent._.country_latlng, ent._.country_flag)
这里更像是泛化意义的nlp,这样就可以在管道里对文本对象做很多事情。本例子中对文本做了是否包含国家的判断,当然你还可以做其他你想做的。
在这个例子中,在初始化时一次请求获取了所有数据。但是,如果你处理的文本包含不完整的国家/地区名称、拼写错误或外语版本,你还可以实现一个like_country
风格的getter
函数,该函数向搜索API 端点发出请求并返回最佳匹配结果。
用户hooks
虽然通常建议使用Doc._
,Span._
和Token._
代理来添加你自己的自定义属性,但spaCy
提供了一些例外来允许自定义内置方法,例如Doc.similarity
或者Doc.vector
使用你自己的hooks
,这可以依赖于你自己训练的组件。例如,你可以提供自己的动态句子分割算法或文档相似度方法。
Hooks
允许你通过向管道添加组件来自定义Doc
,Span
或Token
对象的某些行为。例如,想要自定义Doc.similarity
方法,你可以添加一个组件,将自定义函数设置为doc.user_hooks["similarity"]
. 内置的Doc.similarity
方法将检查user_hooks
字典,如果你进行了上述则委托给你的函数。通过将函数设置到Doc.user_span_hooks
和Doc.user_token_hooks
可以获得类似的结果。
实现说明hooks
存在于Doc
对象上,因为Span
和Token
对象是惰性创建的,并且不拥有任何数据。他们只是代理他们的父Doc
。这会更加方便,安装hooks
时我们只需要关心一个地方。
名称 | 自定义 |
---|---|
user_hooks | Doc.similarity,Doc.vector,Doc.has_vector,Doc.vector_norm,Doc.sents |
user_token_hooks | Token.similarity,Token.vector,Token.has_vector,Token.vector_norm,Token.conjuncts |
user_span_hooks | Span.similarity,Span.vector,Span.has_vector,Span.vector_norm,Span.root |
添加自定义相似度hooks
(ADD CUSTOM SIMILARITY HOOKS)
from spacy.language import Language
class SimilarityModel:
def __init__(self, name: str, index: int):
self.name = name
self.index = index
def __call__(self, doc):
doc.user_hooks["similarity"] = self.similarity
doc.user_span_hooks["similarity"] = self.similarity
doc.user_token_hooks["similarity"] = self.similarity
return doc
def similarity(self, obj1, obj2):
return obj1.vector[self.index] + obj2.vector[self.index]
@Language.factory("similarity_component", default_config={"index": 0})
def create_similarity_component(nlp, name, index: int):
return SimilarityModel(name, index)
from spacy.language import Language
class SimilarityModel:
def __init__(self, name: str, index: int):
self.name = name
self.index = index
def __call__(self, doc):
doc.user_hooks["similarity"] = self.similarity
doc.user_span_hooks["similarity"] = self.similarity
doc.user_token_hooks["similarity"] = self.similarity
return doc
def similarity(self, obj1, obj2):
return obj1.vector[self.index] + obj2.vector[self.index]
@Language.factory("similarity_component", default_config={"index": 0})
def create_similarity_component(nlp, name, index: int):
return SimilarityModel(name, index)
开发插件和包装器
社区对spaCy扩展和插件的所有新的可能都让我们感到兴奋,我们迫不及待地想看看你用它构建了什么!为了帮助你入门,这里有一些提示、技巧和最佳实践。有关其他spaCy
扩展的示例,请参见此处。
使用思路
- 添加新功能并挂载到模型上。例如,情感分析模型,或你喜欢的词形还原器。
spaCy
内置标记器(tagger)、解析器(parser)和实体识别器,管道前一步中就设置在Doc
上了。 - 集成其他库和
API
。例如,你的管道组件可以将附加信息和数据直接写入Doc
或Token
作为自定义属性,同时确保在此过程中不会丢失任何信息。这可以是其他库和模型生成的输出,也可以是一个外部REST API
服务。 - 调试和记录。例如,存储和(或)导出有关已处理文档当前状态的相关信息的组件,并将其插入管道的任何点。
最佳实践
扩展可以声明自己的._
命名空间并作为独立包存在。如果你正在开发一个工具或库,并希望其他人可以轻松地将它与spaCy
一起使用并将其添加到他们的管道中,那么你需要做的就是暴露一个接受一个Doc
的函数,修改它并返回它。
- 确保为你的管道组件类选择一个描述性和特定的名称,并将其设置为它的
name
属性。避免使用太常见或可能与内置或其他自定义组件发生冲突的名称。虽然可以将包命名为"spacy_my_extension"
,但请避免组件名称包含"spacy"
,因为这很容易导致混淆。
+ name = "myapp_lemmatizer"
- name = "lemmatizer"
+ name = "myapp_lemmatizer"
- name = "lemmatizer"
- 写入
Doc
,Token
或Span
对象时,尽可能使用getter
函数,并避免显式设置值。标记和跨度本身不拥有任何数据,它们被实现为C
扩展类。因此你通常不能像使用大多数纯Python
对象那样向它们添加新属性。
+ is_fruit = lambda token: token.text in ("apple", "orange")
+ Token.set_extension("is_fruit", getter=is_fruit)
- token._.set_extension("is_fruit", default=False)
- if token.text in ('"apple", "orange"):
- token._.set("is_fruit", True)
+ is_fruit = lambda token: token.text in ("apple", "orange")
+ Token.set_extension("is_fruit", getter=is_fruit)
- token._.set_extension("is_fruit", default=False)
- if token.text in ('"apple", "orange"):
- token._.set("is_fruit", True)
- 添加你的自定义属性的全局
Doc
,Token
或Span
对象,而不是一个具体的实例。尽早添加属性,例如,在扩展的__init__
方法中或在你的模块的全局范围内。这意味着在命名空间冲突的情况下,用户将立即看到错误,而不用等到管道运行时。
+ from spacy.tokens import Doc
+ def __init__(attr="my_attr"):
+ Doc.set_extension(attr, getter=self.get_doc_attr)
- def __call__(doc):
- doc.set_extension("my_attr", getter=self.get_doc_attr)
+ from spacy.tokens import Doc
+ def __init__(attr="my_attr"):
+ Doc.set_extension(attr, getter=self.get_doc_attr)
- def __call__(doc):
- doc.set_extension("my_attr", getter=self.get_doc_attr)
- 如果你在
Doc
,Token
或Span
上设置扩展属性,请包含一个选项以让用户更改这些属性名称。这可以更容易避免命名空间冲突并适应具有不同命名首选项的用户。我们建议为的类的__init__
方法添加一个attrs
参数,以便你可以将名称写入类属性并在你的组件中重复使用它们。
+ Doc.set_extension(self.doc_attr, default="some value")
- Doc.set_extension("my_doc_attr", default="some value")
+ Doc.set_extension(self.doc_attr, default="some value")
- Doc.set_extension("my_doc_attr", default="some value")
理想情况下,扩展应该是独立于
spaCy
的包,并且是可选的,其他指定为依赖项的包。他们可以自由分配给自己的._
命名空间,但应该坚持这一点。如果你的扩展程序的唯一工作是提供更好的.similarity
实现,并且你的文档明确说明了这一点,那么写入user_hooks
并重写spaCy
的内置方法就可以了。但是,第三方扩展**不应该默默地重写built-ins
**或其他扩展属性的设置。如果你希望发布依赖于自定义管道组件的管道包,你可以在包的依赖项中要求它,或者,如果组件是特定且轻量级的,那么就将 它与你的管道包一起提供。只要确保注册自定义组件的
@Language.component
或者@Language.factory
装饰器在你的包__init__.py
中运行或公开在入口点 。一旦你准备好与他人共享你的扩展程序,请务必添加文档和安装说明(你始终可以链接到此页面以获取更多信息)。让其他人可以轻松安装和使用你的扩展程序,例如将其上传到
PyPi
。如果你在GitHub
上共享你的代码,请不要忘记将其标记为spacy
和spacy-extension
来帮助人们找到它。如果你将其发布在Twitter
上,请随时标记@spacy_io
,以便我们查看。
包装其他模型和库
假设你有一个自定义实体识别器,它接受一个字符串列表并返回它们的BILUO
标签。给定一个输入,如["A", "text", "about", "Facebook"]
,它将预测并返回["O", "O", "O", "U-ORG"]
。将它集成到你的spaCy
管道,使之这些实体添加到doc.ents
,可以将它包装到一个自定义的管道组件函数中,并向它传递管道接受的Doc
对象的token文本。
training.biluo_tags_to_spans
在这里非常有用,因为它接受一个Doc
对象和基于token
的BILUO
标签,并返回带有标签的Span
对象序列到Doc
中。因此,你的包装器所要做的就是计算实体跨度并覆盖doc.ents
。
doc.ents
是如何工作的 当你向doc.ents
中添加spans
,spaCy
会自动将它们解析回底层tokens
并设置Token.ent_type
和Token.ent_iob
属性。根据定义,每个token
只能属于是一个实体,因此不允许实体跨度重叠。
import your_custom_entity_recognizer
from spacy.training import biluo_tags_to_spans
from spacy.language import Language
@Language.component("custom_ner_wrapper")
def custom_ner_wrapper(doc):
words = [token.text for token in doc]
custom_entities = your_custom_entity_recognizer(words)
doc.ents = biluo_tags_to_spans(doc, custom_entities)
return doc
import your_custom_entity_recognizer
from spacy.training import biluo_tags_to_spans
from spacy.language import Language
@Language.component("custom_ner_wrapper")
def custom_ner_wrapper(doc):
words = [token.text for token in doc]
custom_entities = your_custom_entity_recognizer(words)
doc.ents = biluo_tags_to_spans(doc, custom_entities)
return doc
可以通过nlp.add_pipe
方法将custom_ner_wrapper
其添加到空白管道中。 你还可以使用nlp.replace_pipe
将已训练的管道替换现有实体识别器。
下面是自定义模型的另一个示例your_custom_model
,它接受一个tokens
列表并返回细粒度词性标签、粗粒度词性标签、依赖标签和头部标记索引的列表。在这里,我们可以使用Doc.from_array
方法传入上述值来创建一个新的Doc
对象。要创建一个numpy
数组,我们需要整数,因此我们可以在StringStore
寻找字符串标签。此时doc.vocab.strings.add
方法就派上用场了,因为它返回字符串的整数ID
并将其添加到词汇表中。如果自定义模型使用与spaCy
的默认模型不同的标签方案,这一点将尤其重要。
示例:
SPACY-STANZA
有关用于统计标记化(tokenization)、标记和解析的端到端包装器的示例,请查看spacy-stanza
. 它使用了与本节示例非常相似的方法,唯一的区别是它完全替换了nlp
对象而不是提供管道组件,因为它还需要处理标记化(tokenization)。
import your_custom_model
from spacy.language import Language
from spacy.symbols import POS, TAG, DEP, HEAD
from spacy.tokens import Doc
import numpy
@Language.component("custom_model_wrapper")
def custom_model_wrapper(doc):
words = [token.text for token in doc]
spaces = [token.whitespace for token in doc]
pos, tags, deps, heads = your_custom_model(words)
# Convert the strings to integers and add them to the string store
pos = [doc.vocab.strings.add(label) for label in pos]
tags = [doc.vocab.strings.add(label) for label in tags]
deps = [doc.vocab.strings.add(label) for label in deps]
# Create a new Doc from a numpy array
attrs = [POS, TAG, DEP, HEAD]
arr = numpy.array(list(zip(pos, tags, deps, heads)), dtype="uint64")
new_doc = Doc(doc.vocab, words=words, spaces=spaces).from_array(attrs, arr)
return new_doc
import your_custom_model
from spacy.language import Language
from spacy.symbols import POS, TAG, DEP, HEAD
from spacy.tokens import Doc
import numpy
@Language.component("custom_model_wrapper")
def custom_model_wrapper(doc):
words = [token.text for token in doc]
spaces = [token.whitespace for token in doc]
pos, tags, deps, heads = your_custom_model(words)
# Convert the strings to integers and add them to the string store
pos = [doc.vocab.strings.add(label) for label in pos]
tags = [doc.vocab.strings.add(label) for label in tags]
deps = [doc.vocab.strings.add(label) for label in deps]
# Create a new Doc from a numpy array
attrs = [POS, TAG, DEP, HEAD]
arr = numpy.array(list(zip(pos, tags, deps, heads)), dtype="uint64")
new_doc = Doc(doc.vocab, words=words, spaces=spaces).from_array(attrs, arr)
return new_doc
句子边界和头部(heads) 如果你创建一个具有依赖项和头部(heads)
Doc
对象,spaCy
能够自动解析句子边界。但是,请注意,HEAD
值用于构造一个Doc
是相对于当前token
的token
索引。例如-1
是前一个标记。CoNLL
格式通常将头部注释为1-indexed
绝对索引并0
指示根。如果你的注释是这种情况,你需要先转换它们:
heads = [2, 0, 4, 2, 2]
new_heads = [head - i - 1 if head != 0 else 0 for i, head in enumerate(heads)]
heads = [2, 0, 4, 2, 2]
new_heads = [head - i - 1 if head != 0 else 0 for i, head in enumerate(heads)]
📖高级用法、序列化和入口点 有关如何编写和打包自定义组件的更多详细信息,通过入口点将它们提供给
spaCy
并实现你自己的序列化方法,请查看有关保存和加载的使用指南 。