spaCy v2.0:自定义流水线组件与扩展属性实战
随着spaCy v2.0的发布候选版本日益临近,我们兴奋地实现了一些最后的重要功能。其中最好的改进之一是一个用于添加流水线组件以及向Doc、Span和Token对象注册扩展属性的新系统。本文将向您介绍这一新功能,并通过一个示例扩展包spacymoji作为结尾。
spaCy是一个用于Python中高级自然语言处理的开源库。新版本可通过spacy-nightly在pip上获取。要尝试本文中的示例,您需要最新版本2.0.0a17。有关新功能的详细信息,请参阅此页面。有关新模型的概述,请参阅模型目录。
设置自定义属性
先前版本的spaCy相当难以扩展,尤其是核心的Doc、Token和Span对象。它们不能直接实例化,因此创建有用的子类需要涉及大量复杂的抽象。继承也并不能令人满意,因为它无法组合不同的定制化需求。我们希望允许人们为spaCy开发扩展,并确保这些扩展可以协同使用。如果每个扩展都要求spaCy返回一个不同的Doc子类,那么就无法做到这一点。
为了解决这个问题,我们引入了一个新的动态字段,允许在运行时添加新的属性、属性和方法:
importspacyfromspacy.tokensimportDoc Doc.set_extension("is_greeting",default=False)nlp=spacy.load("en_core_web_sm")doc=nlp(u"hello world")doc._.is_greeting=True我们认为._属性在可读性和明确性之间取得了良好的平衡。扩展应该易于使用,但同时也应清楚哪些是内置的,哪些不是——否则,您将无法追溯所读代码的文档或实现。._属性还确保了对spaCy的更新不会因命名空间冲突而破坏扩展代码。
另一个在扩展开发中一直缺失的是修改处理流水线的便捷方式。早期版本的spaCy硬编码了流水线,因为只支持英语。spaCy v1.0允许在运行时更改流水线,但这在很大程度上对用户是隐藏的:您在文本上调用nlp,然后一系列操作就发生了——但具体是什么呢?如果您需要添加一个应在词性标注和句法分析之间运行的处理过程,您必须深入研究spaCy的内部结构。在spaCy v2.0中,终于有了一个API可以做到这一点,而且非常简单:
向流水线添加自定义组件
nlp=spacy.load("en_core_web_sm")component=MyComponent()nlp.add_pipe(component,after="tagger")doc=nlp(u"This is a sentence")自定义流水线组件
从根本上说,流水线是一个按顺序在Doc上调用的函数列表。流水线可以由模型设置,并由用户修改。一个流水线组件可以是一个持有状态的复杂类,也可以是一个向Doc添加内容并返回它的非常简单的Python函数。
在底层,当您在文本字符串上调用nlp时,spaCy执行以下步骤:
doc=nlp.make_doc(u'This is a sentence')# 从原始文本创建一个Docforname,procinnlp.pipeline:# 按顺序遍历组件doc=proc(doc)# 在Doc上调用每个组件nlp对象是Language的一个实例,它包含了您正在使用的语言的数据和标注方案,以及一个预定义的组件流水线,如标注器、解析器和实体识别器。如果您加载了一个模型,Language实例还可以访问模型的二进制数据。所有这些都特定于每个模型,并在模型的meta.json中定义——例如,一个西班牙语NER模型需要的权重、语言数据和流水线组件与英语解析和标注模型不同。这也是为什么流水线状态总是由Language类持有。spacy.load()将所有这些组合在一起,并返回一个具有设置好的流水线和访问二进制数据权限的Language实例。
v2.0中的spaCy流水线简单地是一个(name, function)元组的列表,描述了组件名称和要在Doc对象上调用的函数:
>>>nlp.pipeline[('tagger',<spacy.pipeline.Tagger>),('parser',<spacy.pipeline.DependencyParser>),('ner',<spacy.pipeline.EntityRecognizer>)]为了方便修改流水线,有几个内置方法来获取、添加、替换、重命名或移除单个组件。spaCy的默认流水线组件,如标注器、解析器和实体识别器,现在都遵循相同的、一致的API,并且是Pipe的子类。如果您正在开发自己的组件,使用PipeAPI将使其完全可训练和可序列化。一个组件至少需要是一个接受Doc并返回它的可调用对象:
defmy_component(doc):print("文档长度为 {} 个字符,包含 {} 个词符。".format(len(doc.text),len(doc)))returndoc然后可以使用nlp.add_pipe()方法将该组件添加到流水线的任何位置。参数before、after、first和last允许您指定组件名称以在新组件之前或之后插入,或者告诉spaCy将其插入到流水线的最前面(即直接在分词之后)或最后面。
nlp=spacy.load("en_core_web_sm")nlp.add_pipe(my_component,name="print_length",last=True)doc=nlp(u"This is a sentence.")Doc、Token和Span上的扩展属性
当您实现自己的、用于修改Doc的流水线组件时,您通常希望扩展API,以便您添加的信息可以方便地访问。spaCy v2.0引入了一种新机制,允许您注册自己的属性、属性和方法,这些将在._命名空间中可用,例如,doc._.my_attr。可以通过set_extension()方法注册的扩展主要有三种类型:
- 属性扩展:为属性设置一个默认值,该值可以被覆盖。
- 属性扩展:定义一个getter和一个可选的setter函数。
- 方法扩展:分配一个函数,该函数作为对象方法可用。
Doc.set_extension("hello_attr",default=True)Doc.set_extension("hello_property",getter=get_value,setter=set_value)Doc.set_extension("hello_method",method=lambdadoc,name:"Hi {}!".format(name))doc._.hello_attr# Truedoc._.hello_property# get_value的返回值doc._.hello_method("Ines")# 'Hi Ines!'能够轻松地将自定义数据写入Doc、Token和Span,意味着使用spaCy的应用程序可以充分利用内置数据结构和Doc对象作为包含所有信息的单一事实来源的优势:
- 在分词和解析过程中不会丢失信息,因此您可以始终将标注与原始字符串关联起来。
- Token和Span是Doc的视图,因此它们始终保持最新且一致。
- 通过
doc.c可以高效地在C级别访问底层的TokenC*数组。 - API可以标准化为传递Doc对象,并在必要时从中读取和写入。更少的签名使函数更具可重用性和可组合性。
例如,假设您的数据包含地理信息,如国家名称,并且您使用spaCy提取这些名称并添加更多细节,如国家的首都或GPS坐标。或者,您的应用程序需要使用spaCy的命名实体识别器查找公众人物的名字,并检查维基百科上是否有关于他们的页面。
以前,您通常会对文本运行spaCy以获取您感兴趣的信息,将其保存到数据库,然后稍后向其中添加更多数据。这很有效,但这也意味着您丢失了与原始文档的所有引用。或者,您可以序列化您的文档,并将附加数据与其各自的词符索引引用一起存储。同样,这很有效,但总体而言这是一个相当不尽人意的解决方案。在spaCy v2.0中,您可以简单地将所有这些数据写入文档、词符或跨度上的自定义属性,使用您选择的名称。例如,token._.country_capital、span._.wikipedia_url或doc._.included_persons。
以下示例展示了一个简单的流水线组件,它使用REST Countries API获取所有国家,在文档中查找国家名称,合并匹配的跨度,分配实体标签GPE(地缘政治实体),并将国家的首都、纬度/经度坐标和一个布尔值is_country添加到词符属性中。您也可以在GitHub上找到一个更详细的版本。
importrequestsfromspacy.tokensimportToken,Spanfromspacy.matcherimportPhraseMatcherclassCountries(object):name='countries'# 流水线中显示的组件名称def__init__(self,nlp,label="GPE"):# 从API请求所有国家数据r=requests.get("https://restcountries.eu/rest/v2/all")# 为便于查找创建字典self.countries={c['name']:cforcinr.json()}# 初始化匹配器并为所有国家名称添加模式self.matcher=PhraseMatcher(nlp.vocab)self.matcher.add("COUNTRIES",None,*[nlp(c)forcinself.countries.keys()])# 从词汇表获取标签IDself.label=nlp.vocab.strings[label]# 在Token上注册扩展Token.set_extension("is_country",default=False)Token.set_extension("country_capital")Token.set_extension("country_latlng")def__call__(self,doc):matches=self.matcher(doc)spans=[]# 稍后保留跨度以便合并for_,start,endinmatches:# 为匹配的国家创建Span并分配标签entity=Span(doc,start,end,label=self.label)spans.append(entity)fortokeninentity:# 设置词符属性的值token._.set("is_country",True)token._.set("country_capital",self.countries[entity.text]["capital"])token._.set("country_latlng",self.countries[entity.text]["latlng"])# 覆盖doc.ents并添加实体——不要替换!doc.ents=list(doc.ents)+spansforspaninspans:span.merge()# 最后合并所有跨度以避免索引不匹配returndoc# 不要忘记返回Doc!该示例还使用了spaCy的PhraseMatcher,这是v2.0中引入的另一个很酷的功能。与词符模式不同,短语匹配器可以接受Doc对象列表,让您快速高效地匹配大型术语列表。当您将该组件添加到流水线并处理文本时,所有国家都会自动标记为GPE实体,并且自定义属性在词符上可用:
nlp=spacy.load("en_core_web_sm")component=Countries(nlp)nlp.add_pipe(component,before="tagger")doc=nlp(u"Some text about Colombia and the Czech Republic")print([(ent.text,ent.label_)forentindoc.ents])# [('Colombia', 'GPE'), ('Czech Republic', 'GPE')]print([(token.text,token._.country_capital)fortokenindociftoken._.is_country])# [('Colombia', 'Bogotá'), ('Czech Republic', 'Prague')]在这种情况下,我们能够通过一次对REST API的请求获取所有数据。但是,您也可以通过单个对象上的getter函数实现API请求,或添加一个方法属性来传递额外的参数。或者,考虑一个Token方法,它接受另一个国家名称或GPS坐标,并计算到该词符所属国家的距离?这一切现在都成为可能了!
使用getter和setter,您还可以在Doc和Span上实现引用自定义Token属性的属性——例如,文档是否包含国家。由于getter仅在您访问属性时被调用,您可以在这里引用Token的is_country属性,该属性已在处理步骤中设置。完整的实现请参见完整示例。
has_country=lambdatokens:any([token._.is_countryfortokenintokens])Doc.set_extension("has_country",getter=has_country)Span.set_extension("has_country",getter=has_country)spaCy扩展
为自定义扩展提供一个直观的API和一个清晰定义的输入/输出(Doc/Doc),也有助于使更大的代码库更易于维护,并允许开发者与他人分享他们的扩展并可靠地测试它们。这对于使用spaCy的团队很重要,对于希望发布自己的包、扩展和插件的开发者也是如此。
我们希望这种新架构将有助于鼓励一个spaCy组件的社区生态系统,以覆盖任何潜在的使用案例——无论多么具体。组件可以范围广泛,从为方便起见添加相当琐碎属性的简单扩展,到利用外部库(如PyTorch、scikit-learn和TensorFlow)的复杂模型。用户可能希望拥有许多组件,我们很乐意能够提供更多与spaCy一起打包的内置流水线组件——例如,更好的句子边界检测、语义角色标注和情感分析。但同时,也存在明确的需求,需要使spaCy针对特定用例可扩展,使其与其他库更好地互操作,并将所有这些结合起来以更新和训练统计模型。
示例:使用spacymoji处理Emoji
长期以来,为spaCy添加更好的emoji支持一直是我“某天要做的酷事”清单上的项目。Emoji很有趣,包含许多相关的语义信息,而且据说现在在Twitter文本中比连字符更常见。在过去的两年里,它们也变得更加复杂。除了常规的emoji字符及其Unicode表示外,您现在还可以使用肤色修饰符,它们放在常规emoji之后,并产生一个可见字符。例如,👍 + 🏿 = 👍🏿。此外,一些字符可以形成“ZWJ序列”,例如,由零宽连接符连接的多个emoji合并成一个符号。例如,👨 + ZWJ + 🎤 = 👨🎤(官方名称为“男歌手”,我称之为“Bowie”)。
从v2.0开始,spaCy的分词器将所有emoji和其他符号拆分为独立的词符,使它们更容易与文本的其余部分分开。然而,emoji的Unicode范围相当随意且经常更新。spaCy的分词器使用的\p{Other_Symbol}或\p{So}类别是一个很好的近似,但它也包括其他图标和装饰符号。所以,如果您只想处理emoji,除了匹配一个精确的列表外别无他法。幸运的是,emoji包在这里为我们提供了支持。
spacymoji是一个spaCy扩展和流水线组件,它检测文本中的单个emoji和序列,将它们合并成一个词符,并为Doc、Span和Token分配自定义属性。例如,您可以检查一个文档或跨度是否包含emoji,检查一个词符是否为emoji,并检索其人类可读的描述。
importspacyfromspacymojiimportEmoji nlp=spacy.load('en')emoji=Emoji(nlp)nlp.add_pipe(emoji,first=True)doc=nlp(u"This is a test 😻 👍🏿")assertdoc._.has_emojiassertlen(doc._.emoji)==2assertdoc[2:5]._.has_emojiassertdoc[4]._.is_emojiassertdoc[5]._.emoji_desc==u'thumbs up dark skin tone'assertdoc._.emoji[1]==(u'👍🏿',5,u'thumbs up dark skin tone')通过将该组件添加为流水线中的第一个组件,跨度在分词后立即合并,并在文档被解析之前完成。如果您的文本包含大量emoji,这甚至可能使解析器的准确性得到不错的提升,因为解析器每个emoji只看到一个词符。
spacymoji组件使用PhraseMatcher在emoji查找表中查找精确的emoji序列的出现位置,并生成相应的emoji跨度。如果emoji由多个字符组成——例如,带有肤色修饰符的emoji或组合的ZWJ序列——它还会将它们合并成一个词符。emoji快捷方式,如:thumbs_up:,被转换为人类可读的描述,可作为token._.emoji_desc使用。您也可以传入您自己的查找表,将emoji映射到自定义描述。
后续步骤
如果您受到启发并想构建自己的扩展,请参阅本指南以获取一些提示、技巧和最佳实践。随着深度学习工具和技术的发展,现在有许多用于预测各种类型NLP标注的模型。用于共指消解、信息提取和摘要等任务的模型现在可以轻松地用于驱动spaCy扩展——您所要做的就是添加扩展属性,并将模型挂接到流水线中。我们期待着看到您构建的成果!
资源
- spaCy v2.0: v2.0 (alpha) 中的新特性
- 自定义流水线:使用指南 (alpha)
- 代码示例:组件和属性
- spacymoji:GitHub上的扩展FINISHED
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
