spaCy中的令牌扩展,匹配器,短语匹配器和实体标尺

时间:2019-04-25 14:49:20

标签: python performance spacy

我正在尝试找出(快速)提取实体的最佳方法,例如一个月。我使用spaCy提出了5种不同的方法。

初始设置

对于每种解决方案,我首先进行初始设置

import spacy.lang.en    
nlp = spacy.lang.en.English()
text = 'I am trying to extract January as efficient as possible. But what is the best solution?'

解决方案:使用extension attributes(仅限于单个令牌匹配)

import spacy.tokens
NORM_EXCEPTIONS = {
    'jan': 'MONTH', 'january': 'MONTH'
}
spacy.tokens.Token.set_extension('norm', getter=lambda t: NORM_EXCEPTIONS.get(t.text.lower(), t.norm_))
def time_this():
    doc = nlp(text)
    assert [t for t in doc if t._.norm == 'MONTH'] == [doc[5]]

%timeit time_this()
  

每个循环76.4 µs±169 ns(平均±标准偏差,共运行7次,每个循环10000次)

解决方案:通过entity ruler使用短语匹配器

import spacy.pipeline
ruler = spacy.pipeline.EntityRuler(nlp)
ruler.phrase_matcher = spacy.matcher.PhraseMatcher(nlp.vocab, attr="LOWER")
ruler.add_patterns([{'label': 'MONTH', 'pattern': 'jan'}, {'label': 'MONTH', 'pattern': 'january'}])
nlp.add_pipe(ruler)
def time_this():
    doc = nlp(text)
    assert [t for t in doc.ents] == [doc[5:6]]
%timeit time_this()
  

每个循环131 µs±579 ns(平均±标准偏差,共运行7次,每个循环10000次)

解决方案:通过实体标尺使用令牌匹配器

import spacy.pipeline
ruler = spacy.pipeline.EntityRuler(nlp)
ruler.add_patterns([{'label': 'MONTH', 'pattern': [{'lower': {'IN': ['jan', 'january']}}]}])
nlp.add_pipe(ruler)
def time_this():
    doc = nlp(text)
    assert [t for t in doc.ents] == [doc[5:6]]
%timeit time_this()
  

每个循环72.6 µs±76.7 ns(平均±标准偏差,共运行7次,每个循环10000次)

解决方案:直接使用phrase matcher

import spacy.matcher
phrase_matcher = spacy.matcher.PhraseMatcher(nlp.vocab, attr="LOWER")
phrase_matcher.add('MONTH', None, nlp('jan'), nlp('january'))
def time_this():
    doc = nlp(text)
    matches = [m for m in filter(lambda x: x[0] == doc.vocab.strings['MONTH'], phrase_matcher(doc))]
    assert [doc[m[1]:m[2]] for m in matches] == [doc[5:6]]
%timeit time_this()
  

每个循环115 µs±537 ns(平均±标准偏差,共运行7次,每个循环10000次)

解决方案:直接使用token matcher

import spacy.matcher
matcher = spacy.matcher.Matcher(nlp.vocab)
matcher.add('MONTH', None, [{'lower': {'IN': ['jan', 'january']}}])
def time_this():
    doc = nlp(text)
    matches = [m for m in filter(lambda x: x[0] == doc.vocab.strings['MONTH'], matcher(doc))]
    assert [doc[m[1]:m[2]] for m in matches] == [doc[5:6]]
%timeit time_this()
  

每循环55.5 µs±459 ns(平均±标准偏差,共运行7次,每个循环10000次)

结论

具有的自定义属性仅限于单个令牌匹配,并且令牌匹配器似乎更快,因此似乎更可取。 EntityRuler似乎是最慢的,这并不奇怪,因为它正在更改Doc.ents。但是,将匹配项放在Doc.ents中非常方便,因此您可能仍要考虑使用此方法。

令我惊讶的是,令牌匹配器胜过短语匹配器。我认为那是相反的:

  

如果需要匹配大型术语列表,则还可以使用PhraseMatcher并创建Doc对象而不是令牌模式,这样总体上效率更高

问题

我在这里错过了重要的事情吗?还是我可以更信任这个分析?

1 个答案:

答案 0 :(得分:3)

最终,我认为最终归结为在速度,代码的可维护性以及此逻辑适合您的应用程序整体的方式之间找到最佳折衷。在文本中找到一些字符串不太可能成为您尝试做的最终目标–否则,您可能不会使用spaCy并坚持使用正则表达式。您的应用程序如何“消费”匹配结果以及在更大的上下文中匹配的含义应激发您选择的方法。

就像您在结论中提到的那样,如果将匹配项定义为“命名实体”,则将它们添加到doc.ents很有道理,甚至可以为您提供一种将逻辑与统计预测结合起来的简便方法。即使增加了一些额外的开销,它也可能仍然胜过您自己不得不自己编写的任何脚手架。

  

对于每种解决方案,我首先进行初始设置

如果您在同一会话中运行实验,例如在笔记本中,您可能需要在初始设置中包括Doc对象的创建。否则,词汇表条目的缓存理论上可能意味着nlp(text)的第一个调用比随后的调用要慢。不过,这可能微不足道。

  

令我惊讶的是,令牌匹配器胜过短语匹配器。我以为会相反

一种可能的解释是,您正在以很小的规模和单令牌模式对方法进行概要分析,而短语匹配器引擎实际上并没有比常规令牌匹配器有优势。另一个因素可能是,在不同属性上进行匹配(例如,LOWER而不是TEXT / ORTH)需要creating a new Doc during matching来反映匹配属性的值。这应该很便宜,但是它仍然是创建的另一个对象。因此,测试Doc "extract January"实际上会变成"extract january"(在LOWER上匹配时)甚至在"VERB PROPN"上匹配时变成POS。这就是使其他属性匹配起作用的技巧。

了解PhraseMatcher的工作原理以及其机制通常更快的原因:将Doc对象添加到PhraseMatcher时,它会在模式中包含的标记上设置标志,表示它们正在匹配给定的模式。然后,它调用常规Matcher,并使用先前设置的标志添加基于令牌的模式。进行匹配时,spaCy只需检查标志,而无需检索任何令牌属性,这将使匹配本身在规模上显着更快。

这实际上提出了您可能要进行比较的另一种方法:使用Vocab.add_flag在相应的词素上设置布尔标志(在vocab中输入,而不是上下文相关标记)。 Vocab条目已缓存,因此您只需为"january"之类的词素计算一次标志。但是,这种方法仅对单个令牌才有意义,因此是相对有限的。

  

我在这里错过了重要的事情吗?还是我可以更信任这个分析?

如果您想获得有意义的见解,则应该至少在中等规模的基础上进行基准测试。您不想在同一个小示例上循环10000次,而是在每个测试仅处理一次一次的数据集上进行基准测试。例如,几百个与您实际使用的数据相似的文档。有缓存效果(既在spaCy中,也在CPU中),内存分配的差异等都可能产生影响。

最后,直接使用spaCy的Cython API总是最快的。因此,如果速度是您的第一要务,而您想要优化的一切,那么Cython就是您的最佳选择。