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 条评论
登录 后参与评论

相关文章

来自专栏彭湖湾的编程世界

【mock】后端不来过夜半,闲敲mock落灯花 (mockjs+Vuex+Vue实战)

mock的由来【假】 赵师秀:南宋时期的一位前端工程师 诗词背景:在一个梅雨纷纷的夜晚,正处于项目编码阶段,书童却带来消息:写后端的李秀才在几个时辰前就赶往临安...

23611
来自专栏程序你好

Apache Spark大数据处理 - 性能分析(实例)

今天的任务是将伦敦自行车租赁数据分为两组,周末和工作日。将数据分组到更小的子集进行进一步处理是一种常见的业务需求,我们将看到Spark如何帮助我们完成这项任务。

1253
来自专栏PHP在线

MongoDB数据结构设计中6条重要的经验法则

很多初学者认为在MongoDB中针对一对多建模唯一的方案就是在父文档中内嵌一个数组子文档,但是这是不准确的。因为你可以在MongoDB内嵌一个文档不代表你就必须...

3097
来自专栏生信技能树

rMATS这款差异可变剪切分析软件的使用体验

rMATS最近刚现在出了rMATS 4.0.1版,相比之间的rMATS 3.2.5版,其用C,Python,Cython重写了该软件,运算速度提升了100倍,并...

4863
来自专栏SDNLAB

SDN实战团分享(七):YANG模型与OpenDaylight南北向接口

YANG模型是什么? YANG模型是一种数据建模语言,用来建模由NETCONF协议、NETCONF远端过程调用(RPCs)、和NETCONF通知(notific...

4598
来自专栏飞雪无情的博客

Go语言实战笔记(二十二)| Go 基准测试

基准测试,是一种测试代码性能的方法,比如你有多种不同的方案,都可以解决问题,那么到底是那种方案性能更好呢?这时候基准测试就派上用场了。

1043
来自专栏拂晓风起

cocos2d-js 越来越慢的定时器schedule 制作不变慢的定时器

1154
来自专栏木子昭的博客

<技巧>python模块性能测试以python列表的内置函数append和insert为例以python列表insert方法和append方法快速创建1至1000的列表为例:

算法是程序的灵魂,优秀的算法能给程序的效率带来极大的提升,而算法的优劣,往往要经过大量的测试. 在硬件环境基本不变的前提下,对算法实验的次数越多,测试算法运...

3166
来自专栏Crossin的编程教室

【每周一坑】螺旋矩阵

今天这题,看起来挺简单,实际写出来并不容易。在以前公司我曾把它做过招聘的笔试题,结果惨不忍睹,不得不拿掉。 输出如图的螺旋矩阵: 1 2 3 4...

3467
来自专栏吉浦迅科技

DAY31:阅读global memory

892

扫码关注云+社区