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

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

import spacy
from spacy.tokensimport 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, procin 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', <spacy.pipeline.Tagger>), ('parser', <spacy.pipeline.DependencyParser>),
 ('ner', <spacy.pipeline.EntityRecognizer>)]

为了更方便地修改管道,有几种内置方法可以获取,添加,替换,重命名或删除单独的组件。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.tokensimport Token, Span
from spacy.matcherimport 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']: cfor cin 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 cin 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, endin matches:
            # create Span for matched country and assign label
            entity= Span(doc, start, end, label=self.label)
            spans.append(entity)
            for tokenin 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 spanin 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 entin doc.ents])
# [('Colombia', 'GPE'), ('Czech Republic', 'GPE')]

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

使用getter和setter还可以实现对属性归类,在Doc和Span引用自定义Token属性,比如文档是否含有国家。因为getter只有在访问属性时才被调用,所以你可以引用Token的is_country属性,这个属性已在处理步骤中设置了。

s_country= lambda tokens:any([token._.is_countryfor tokenin 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订阅号(atyun_com)

原文发表时间:2017-10-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java架构师进阶

从内到外的实现JVM

在社会化分工、软件行业细分专业化的趋势下,会真的参与到底层系统实现的人肯定是越来越少(比例上说)。真的会参与到JVM实现的人肯定是少数。

763
来自专栏更流畅、简洁的软件开发方式

【自然框架】注册会员活动——第一份代码的修改建议(第一版)

  感谢“好坏”提供代码,这是我看过的比较不错的三层结构的代码了,业务层并不是直接调用DAL,而是有其自身的逻辑判断,并不是传声筒,很赞。 我对这份代码,按照自...

2136
来自专栏奇点大数据

你想要的Python面试都在这里了【315+道题】

再将以上二进制拼接起来计算十进制结果:00001010 00000011 00001001 00001100 = ?

682
来自专栏互扯程序

毕业季,跳槽季,不刷点面试题怎么能行?

现在是资源共享的时代,同样也是知识分享的时代,如果你觉得本文能学到知识,请把知识与别人分享。 前言 马上就是一年一度的毕业季 跳槽季,找工作三大要素,简...

3135
来自专栏熊二哥

GOF设计模式快速学习

这段时间,学习状态比较一般,空闲时基本都在打游戏,和研究如何打好游戏,终于通过戏命师烬制霸LOL,玩笑了。为了和"学习"之间的友谊小船不翻,决定对以往学习过的G...

1789
来自专栏郭霖

Android图片加载框架最全解析(三),深入探究Glide的缓存机制

在本系列的上一篇文章中,我带着大家一起阅读了一遍Glide的源码,初步了解了这个强大的图片加载框架的基本执行流程。 不过,上一篇文章只能说是比较粗略地阅读了Gl...

42710
来自专栏大前端开发

【趣解编程】变量

如果把编程比作做菜的话,变量就是那些碗盆瓢勺,或装着原材料,或在做菜的过程中临时的摆放半成品,或装着最后的成品菜。

784
来自专栏mukekeheart的iOS之旅

iOS学习——内存泄漏检查及原因分析

项目的代码很多,前两天老大突然跟我说项目中某一个ViewController的dealloc()方法没有被调用,存在内存泄漏问题,需要排查原因,解决内存泄漏问...

3567
来自专栏张善友的专栏

事件流处理框架NEsper for .NET

复合事件处理(Complex Event Processing)介绍提到了开源的Esper,NEsper 是一个事件流处理(Event Stream Proce...

1966
来自专栏Web 开发

Backbone源码研究 – Backbone.Model

都因为 IE8 不支持 Object.defineProperty,但是业务还不能脱离 IE7 和 IE8,故研究下 Backbone.Model 的实现机制,...

520

扫码关注云+社区