保存与加载
如果您一直在修改管道、词汇表、向量和实体,或更新组件模型,最终会希望保存进度——例如nlp对象中的所有内容。这意味着需要将其内容和结构转换为可保存的格式,如文件或字节字符串。这个过程称为序列化。spaCy提供内置序列化方法并支持Pickle协议。
所有容器类,即 Language (nlp),
Doc, Vocab 和 StringStore
都提供以下方法:
| 方法 | 返回值 | 示例 |
|---|---|---|
to_bytes | bytes | data = nlp.to_bytes() |
from_bytes | object | nlp.from_bytes(data) |
to_disk | - | nlp.to_disk("/path") |
from_disk | object | nlp.from_disk("/path") |
序列化处理流程
在序列化处理流程时,请注意这只会保存各组件二进制数据以便spaCy能还原它们——而非完整对象。这是好事,因为这使得序列化过程更安全。但同时也意味着你需要妥善保存包含流程配置和所有相关设置的配置文件。
序列化
反序列化
这也是spaCy在加载处理流程时的底层实现方式:它会加载包含语言和处理流程信息的config.cfg文件,初始化语言类,根据配置创建并添加处理流程组件,然后加载二进制数据。您可以在此了解更多关于此过程的信息。
高效序列化Doc对象
如果您需要处理大量数据,很可能需要在不同机器间传递分析结果,无论是为了使用Dask或Spark这类工具,还是仅仅为了将工作保存到磁盘。通常使用Doc.to_array功能配合numpy数组序列化就足够了,但有时您可能需要更通用的方式来保存和恢复Doc对象。
DocBin 类可以轻松地对一组 Doc 对象进行序列化和反序列化操作,这比在每个单独的 Doc 对象上调用 Doc.to_bytes 要高效得多。您还可以控制保存哪些数据,并且可以将多个数据块合并在一起,以便进行简单的 map/reduce 式处理。
如果将store_user_data设置为True,则Doc.user_data也会被序列化,其中包含
扩展属性
的值(如果它们可以通过msgpack序列化)。
使用Pickle
在序列化spaCy的对象如Doc或EntityRecognizer时,请注意它们都需要共享的Vocab(包含字符串到哈希的映射、标签方案和可选的向量)。这意味着它们的序列化表示可能会变得非常大,特别是当加载了词向量时,因为它不仅包含对象本身,还包含它所依赖的整个共享词汇表。
如果需要序列化多个对象,尽量将它们一起序列化,而不是分开处理。例如,与其序列化所有管道组件,不如一次性序列化整个管道。同样,与其分别序列化多个Doc对象,不如序列化一个Doc对象列表。由于它们都引用同一个Vocab对象,该对象只会被包含一次。
序列化共享数据的对象
实现序列化方法
当你调用nlp.to_disk、
nlp.from_disk或加载一个流程包时,spaCy
会遍历流程中的组件,检查它们是否暴露了
to_disk或from_disk方法,如果有的话,就会调用该方法并传入流程目录路径加上组件名称字符串。例如,如果你调用
nlp.to_disk("/path"),命名实体识别器的数据将被保存在
/path/ner中。
如果您使用的自定义管道组件依赖外部数据——例如模型权重或术语列表——您可以通过让自定义组件暴露其自身的to_disk和from_disk或to_bytes和from_bytes方法,来利用spaCy内置的组件序列化功能。当包含该组件的nlp对象被保存或加载时,该组件将能够自行序列化和反序列化。
以下示例展示了一个自定义组件,该组件可存储任意JSON可序列化数据,允许用户添加数据,并能将数据保存到JSON文件或从中加载。
将组件添加到流水线并加载一些数据后,我们可以将nlp对象序列化到目录,这会调用自定义组件的to_disk方法。
目录的内容将如下所示。
CustomComponent.to_disk 将数据转换为JSON字符串并保存到其子目录中的文件data.json:
目录结构
当你重新加载数据时,spaCy会调用自定义组件的from_disk方法并传入给定文件路径,该组件随后可以加载data.json的内容,将其转换为Python对象并恢复组件状态。当然,这同样适用于其他类型的数据——例如,你可以添加一个模型包装器用于封装通过TensorFlow或PyTorch等其他库训练的模型,使得spaCy在加载流程包时能自动载入其权重参数。
使用入口点
入口点(Entry points)允许将您编写的Python包中的部分功能暴露给其他Python包使用。通过在其setup.py中暴露入口点,一个应用程序可以轻松定制另一个应用程序的行为。想要快速了解Python入口点的有趣介绍,请查看
这篇优秀的博客文章。
spaCy可以从多个不同的入口点加载自定义函数,以添加管道组件工厂、语言类和其他设置。要让spaCy使用您的入口点,您的包需要暴露这些入口点,并且需要安装在相同的环境中——仅此而已。
| 入口点 | 描述 |
|---|---|
spacy_factories | Group of entry points for pipeline component factories, keyed by component name. Can be used to expose custom components defined by another package. |
spacy_languages | Group of entry points for custom Language subclasses, keyed by language shortcut. |
spacy_lookups | Group of entry points for custom Lookups, including lemmatizer data. Used by spaCy’s spacy-lookups-data package. |
spacy_displacy_colors | Group of entry points of custom label colors for the displaCy visualizer. The key name doesn’t matter, but it should point to a dict of labels and color values. Useful for custom models that predict different entity types. |
将概率表加载到现有模型中
你可以将概率表从
spacy-lookups-data 加载到
现有的spaCy模型中,例如 en_core_web_sm。
从头开始训练模型时,您还可以在config.cfg中指定概率表。
config.cfg (节选)
通过入口点自定义组件
当你加载一个处理管道时,spaCy通常会使用其config.cfg配置文件来设置语言类并构建处理管道。管道被指定为一个字符串列表,例如pipeline = ["tagger", "parser", "ner"]。对于每个字符串,spaCy会调用nlp.add_pipe方法,并在由装饰器@Language.component和@Language.factory定义的所有工厂中查找该名称。这意味着你必须在加载管道之前导入自定义组件。
通过使用入口点,管道包和扩展包可以定义自己的"spacy_factories",当Language类初始化时,这些工厂将在后台自动加载。因此如果用户安装了你的包,即使他们没有显式导入,也能使用你的组件!
为了延续 这篇入门博客文章的主题, 考虑以下自定义spaCy 管道组件, 它被调用时会打印一条蛇:
snek.py
由于这是一个非常复杂和精密的模块,您希望将其拆分到自己的包中,以便进行版本控制并上传到PyPi。您还希望自定义包能够在config.cfg中定义pipeline = ["snek"]。为此,您需要能够告诉spaCy在哪里找到组件"snek"。如果不这样做,当您尝试加载管道时,spaCy会报错,因为没有内置的"snek"组件。要将条目添加到工厂中,您现在可以通过setup.py中的entry_points字典来公开它:
setup.py
同一个包可以暴露多个入口点。要让它们在spaCy中可用,你只需要在环境中安装该包:
spaCy现在能够创建管道组件"snek"——即使你从未导入snek_component。当你将nlp.config保存到磁盘时,它会包含你的"snek"组件条目,任何使用此配置训练的管道都将包含该组件并知道如何加载它——前提是你的snek包已安装。
与其让你的snek组件成为一个简单的无状态组件,你也可以将其设计成一个接收配置的工厂。用户可以在将你的组件添加到流水线时传入可选的config参数来自定义其外观 - 例如snek_style。
setup.py
该工厂还可以实现其他管道组件方法,如用于序列化的to_disk和from_disk,甚至是使组件可训练的update。如果一个组件公开了from_disk方法并被包含在管道中,spaCy会在加载时调用它。这使您可以随管道包一起提供自定义数据。当您使用nlp.to_disk保存管道时,如果组件公开了to_disk方法,它将被调用并传入磁盘路径。
上面的示例会将当前蛇序列化到数据目录中的snek.txt文件中。当加载使用snek组件的流水线时,它将打开snek.txt文件并使其可供组件使用。
通过入口点自定义语言类
延续上一个示例和
这篇关于入口点的博文的主题,
假设你想为自己的自定义管道实现一个SnekLanguage类——但你不一定想通过修改spaCy的代码来添加一门语言。那么在你的包中,你可以实现以下
自定义语言子类:
snek.py
除了spacy_factories之外,还有一个针对spacy_languages的入口点选项,它将语言代码映射到特定语言的Language子类:
setup.py
在spaCy中,您可以加载自定义的snk语言,它将通过自定义入口点解析为SnekLanguage。这对于您训练的流程包特别重要,这些包可以在它们的config.cfg中指定lang = snk,而不会因为核心库中没有该语言而导致spaCy报错。
通过入口点自定义displaCy颜色
如果您正在为自定义领域训练命名实体识别模型,最终可能会训练出一些在displacy可视化工具中没有预定义颜色的不同标签。spacy_displacy_colors入口点允许您定义一个将实体标签映射到颜色值的字典。这些颜色会被添加到预定义颜色中,并且可以覆盖现有值。
snek.py
根据上述颜色,入口点可以按如下方式定义。入口点需要有一个名称,因此我们使用键colors。不过名称并不重要,在入口点组中定义的任何内容都将被使用。
setup.py
安装该包后,使用displacy可视化文本时将应用自定义颜色。每当分配SNEK标签时,它将以#3dff74颜色显示。
保存、加载和分发训练好的pipeline
训练完你的流程后,通常你会希望保存其状态,并在之后重新加载。你可以通过Language.to_disk方法实现:
如果目录不存在,将会自动创建,并且整个流水线的数据、元数据和配置都会被导出。为了使流水线更便于部署,我们建议将其打包为Python package。
当你在spaCy v3.0+版本中保存一个管道时,将会导出两个文件:基于nlp.config的config.cfg文件,以及基于nlp.meta的meta.json文件。
- config: 用于创建当前
nlp对象的配置,包括其管道组件、模型,以及训练设置和超参数。可能包含对注册函数的引用,如管道组件或模型架构。给定配置后,spaCy能够重建整个对象树和nlp对象。导出的配置也可用于使用相同设置训练管道。 - meta: 关于管道和Python包的元信息,例如作者信息、许可证、版本、数据源和标签方案。这主要用于文档目的和打包管道。它对
nlp对象的功能没有影响。
生成管道包
spaCy 提供了一个便捷的 CLI 命令,可以创建所有必需的文件,并引导您完成元数据的生成。您也可以手动创建 meta.json 并将其放在数据目录中,或者使用 --meta 参数指定其路径。有关更多信息,请参阅 package 文档。
该命令将创建一个流水线包目录,并在该目录中运行
python -m build 来生成可安装的二进制.whl文件或
.tar.gz压缩包,您可以使用pip install进行安装。
安装二进制wheel文件通常效率更高。
目录结构
你也可以在cli/package.py源文件中找到所有文件的模板。
如果是手动创建包,请注意目录需要按照lang_name和lang_name-version的命名规范来命名。
包含自定义函数和组件
如果您的流程包含
自定义组件、模型
架构或其他代码,这些功能需要
在加载流程之前完成注册。否则,spaCy将无法
识别如何创建配置中引用的对象。如果您在Python中加载自己的
流程,只需在调用
spacy.load之前导入定义这些组件的代码,
即可使自定义组件可用。这也是CLI命令中
--code参数的工作原理。
通过spacy package命令,您可以使用--code参数提供一个或多个包含自定义注册函数的Python文件路径。
Python文件将被复制到包的根目录中,包的__init__.py会将这些文件作为模块导入。这确保了当管道被导入时(例如调用spacy.load时)函数会被注册。只需简单导入即可使注册的函数可用。
请确保包含自定义代码中引用的所有Python文件,包括被其他模块导入的文件。如果您的自定义代码依赖外部包,请确保它们已列在meta.json文件的"requirements"列表中。对于大多数使用场景,注册函数应能提供您所需的所有自定义功能,从自定义组件到自定义模型架构和生命周期钩子。不过,如果您确实需要更详细地自定义设置,可以编辑包的__init__.py文件以及被spacy.load调用的包load函数。
加载自定义管道包
要从数据目录加载一个处理管道,你可以使用
spacy.load()并传入本地路径。这会在目录中查找
config.cfg文件,并使用其中的lang和pipeline设置
来初始化一个带有处理管道的Language类,并加载
模型数据。
如果你想仅加载二进制数据,就需要创建一个Language类并调用from_disk方法。