NLP研究者的福音—spaCy2.0中引入自定义的管道和扩展

2017年10月22日 由 yuxiangyu 发表 365736 0
旧版本的spaCy很难进行扩展。尤其是核心的Doc,Token和Span对象。他们没有直接实例化,所以创建一个有用的子类将涉及很多该死的抽象(想想FactoryFactoryConfigurationFactory类)。继承无法令人满意,因为它没有提供自定义组合的方法。我们希望让人们开发spaCy的扩展,并确保这些扩展可以同时使用。如果每个扩展都需要spaCy返回一个不同Doc子集,那就没办法实现它了。为了解决这个问题,我们引入了一个新的动态字段(dynamic field),允许在运行时添加新的特性,属性和方法:
import spacy
from spacy.tokens import Doc

Doc.set_attribute('is_greeting', default=False)

nlp = spacy.load('en')
doc = nlp(u'hello world')
doc._.is_greeting = True

我们认为“._”特性在清晰性和可读性之间取得了很好的平衡。扩展需要很好的使用,但也应该是清晰的展示哪些是内置的哪些不是,否则无法追踪你正在阅读的代码的文档或实现。“._”属性还确保对spaCy的更新不会因为命名空间冲突而破坏扩展代码。

扩展开发中缺少的另一件事是一种可以方便的修改处理管道的方法。早期版本的spaCy是硬编码管道,因为只支持英文。spaCy v1.0允许管道在运行时更改,但此过程通常藏得很深:你会调用nlp一个文本,但你不知道会发生什么?如果你需要在标记和解析之间添加进程,就必须深入研究spaCy的内部构成。而在spaCy v2.0中,他们总算做了一个接口:
nlp = spacy.load('en')
component = MyComponent()
nlp.add_pipe(component, after='tagger')
doc = nlp(u"This is a sentence")

定制管道组件


从根本上说,管道是一个按顺序访问Doc的函数的列表。它可以由模型设置,并由用户修改。管道组件可以是一个复杂的包含状态的类,也可以是一个非常简单的Python函数,它将一些东西添加到一个Doc并返回它。在“hood”下,当你在一串文本中调用nlp时,spaCy将执行以下步骤:
doc = nlp.make_doc(u'This is a sentence')   # create a Doc from raw text
for name, proc in nlp.pipeline: # iterate over components in order
doc = proc(doc) # call each component on the Doc

nlp对象是一种语言的实例,它包含你正在使用的语言的数据和注释方案,也包括预先定义的组件管道,如标记器,解析器和实体识别器。如果你正在加载模型,这个语言实例也可以访问该模型的二进制数据。所有这些都是针对每个模型,并在模型“meta.json-”中定义 例如,一个西班牙的NER模型需要不同的权重、语言数据和管道组件,而不是像英语那样的解析和标记模型。所以Language类总是带有管道状态。spacy.load()将其全部放在一起,然后返回一个带有管道集的语言实例并访问二进制数据。

2.0版本的spaCy管道只是一个(name, function)元组列表,即它描述组件名称并调用Doc对象的函数:
>>> nlp.pipeline
[('tagger', ), ('parser', ),
('ner', )]

为了更方便地修改管道,有几种内置方法可以获取,添加,替换,重命名或删除单独的组件。spaCy的默认管道组件,如标记器,解析器和实体识别器现在都遵循相同的接口,并且都是子类Pipe。如果你正在开发自己的组件,则使用Pipe接口会让它完全的可训练化和可序列化。至少,组件需要随时调用和返回Doc:
def my_component(doc):
print("The doc is {} characters long and has {} tokens."
.format(len(doc.text), len(doc))
return doc

然后可以使用nlp.add_pipe()方法将组件添加到管道的任何位置。可以使用的参数有:before,after,first和last。
nlp = spacy.load('en')
nlp.add_pipe(my_component, name='print_length', last=True)
doc = nlp(u"This is a sentence.")

Doc、Token和Span的扩展属性


当你对自己的管道组件进行修改时Doc,你通常需要扩展接口,以便你可以方便地访问自己添加的信息。spaCy v2.0引入了一种可以让你注册自己的特性、属性和方法的新机制,它们可以在“._”命名空间中使用如doc._.my_attr。大多数这三种类型的扩展可以通过set_extension()方法注册:

1.Attribute扩展:设置特性的默认值,可以被覆盖。

2.Property扩展:定义getter和可选的setter函数。

3.Method扩展:分配一个作为对象方法可用的函数。
Doc.set_extension('hello_attr', default=True)
Doc.set_extension('hello_property', getter=get_value, setter=set_value)
Doc.set_extension('hello_method', method=lambda doc, name: 'Hi {}!'.format(name))

doc._.hello_attr # True
doc._.hello_property # return value of get_value
doc._.hello_method('Ines') # 'Hi Ines!'

方便的将自定义数据写入Doc,Token和Span意味着使用spaCy的应用程序可以充分利用内置的数据结构和Doc对象的好处作为包含所有信息的唯一可信来源:

  • 在标记化和解析期间不会丢失任何信息,因此你始终可以将注释与原始字符串相关联。

  • 在Token和Span总是向Doc看齐,所以他们始终一致。

  • 高效的C级访问(C-level access)可以通过“doc.c”获得隐藏的“TokenC*”。

  • 接口可以将传递的Doc对象标准化,在需要时从它们中读取或写入。更少的特征使函数更容易复用和可组合。


例如,我们假设你的数据包含地址信息,如国家名,你使用spaCy来提取这些名称,并添加更多详细信息,如国家的首都或者GPS坐标。又或者也许你的应用程序需要使用spaCy的命名实体识别器查找公众人物的姓名,并检查维基百科上是否存在有关它们的页面。

在此之前,你通常会在文本上运行spaCy以获取您感兴趣的信息,将其保存到数据库中并在稍后添加更多数据。这样做没有问题,但也意味着你丢失了原始文档的所有引用。或者,你可能会序列化你的文档并额外存储引用数据,为它们建立自己的索引。这些方法很好,它们但不是很令人满意的解决方案。在spaCy v2.0中,你可以很方便的在文档、token或span中写入所有这些数据自定义的属性,如:token._.country_capital,span._.wikipedia_url或doc._.included_persons。

下面示例展示了使用“REST Countries API”获取所有国家的管道组件,在文档中查找国家名称,合并匹配的span,分配实体标签GPE(geopolitical entity),并添加国家的首都,经纬度坐标和一个布尔类型的“is_country”到token的属性。
import requests
from spacy.tokens import Token, Span
from spacy.matcher import PhraseMatcher

class Countries(object):
name = 'countries' # component name shown in pipeline

def __init__(self, nlp, label='GPE'):
# request all country data from the API
r = requests.get('https://restcountries.eu/rest/v2/all')
self.countries = {c['name']: c for c in r.json()} # create dict for easy lookup
# initialise the matcher and add patterns for all country names
self.matcher = PhraseMatcher(nlp.vocab)
self.matcher.add('COUNTRIES', None, *[nlp(c) for c in self.countries.keys()])
self.label = nlp.vocab.strings[label] # get label ID from vocab
# register extensions on the 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 = [] # keep the spans for later so we can merge them afterwards
for _, start, end in matches:
# create Span for matched country and assign label
entity = Span(doc, start, end, label=self.label)
spans.append(entity)
for token in entity: # set values of token attributes
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 = list(doc.ents) + spans # overwrite doc.ents and add entities – don't replace!
for span in spans:
span.merge() # merge all spans at the end to avoid mismatched indices
return doc # don't forget to return the Doc!

代码详细的版本可以访问下面的链接:

https://github.com/explosion/spaCy/blob/develop/examples/pipeline/custom_component_countries_api.py

该示例还使用了spaCy的PhraseMatcher,这是v2.0中引入的另一个很酷的功能。与token模式不同,PhraseMatcher可以获取Doc对象列表,让你能够更快更高效地匹配大型术语列表。当你将组件添加到管道并处理文本时,所有国家都将自动标记为GPE实体对象,自定义属性在token上可用:
nlp = spacy.load('en')
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_) for ent in doc.ents])
# [('Colombia', 'GPE'), ('Czech Republic', 'GPE')]

print([(token.text, token._.country_capital) for token in doc if token._.is_country])
# [('Colombia', 'Bogotá'), ('Czech Republic', 'Prague')]

使用getter和setter还可以实现对属性归类,在Doc和Span引用自定义Token属性,比如文档是否含有国家。因为getter只有在访问属性时才被调用,所以你可以引用Token的is_country属性,这个属性已在处理步骤中设置了。
s_country = lambda tokens: any([token._.is_country for token in tokens])
Doc.set_extension('has_country', getter=has_country)
Span.set_extension('has_country', getter=has_country)

关于spaCy的扩展


拥有一个简单的自定义扩展API和一个明确定义的输入或输出,同样有助于让庞大的代码库更加易于维护,并允许开发人员与他人共享他们的扩展,并可靠地测试它们。这不仅与使用spaCy的团队有关,而且也适用于希望发布自己的包、扩展和插件的开发人员。

我们希望这个新架构可以帮助支持spaCy组件的社区生态系统,使它可以包含任何可能存在的情况无论这种情况有多特殊。组件可以从简单的扩展为琐碎的属性添加提供便利,到复杂模型的使用,如PyTorch、scikit-learning和TensorFlow等外部库。我们希望能够提供更多内置的管道组件给spaCy,更好的句子边界检测,语义角色标签和情绪分析。但也必须有一些对特定的情况进行处理的spaCy扩展,使其与其他库更好地互操作,并将它们一起用来更新和训练统计模型。
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消