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

mr.*_*rre 9 python performance spacy

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

最初设定

对于每个解决方案,我都从初始设置开始

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?'
Run Code Online (Sandbox Code Playgroud)

解决方案:使用扩展属性(仅限单token匹配)

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()
Run Code Online (Sandbox Code Playgroud)

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

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

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()
Run Code Online (Sandbox Code Playgroud)

每个循环 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()
Run Code Online (Sandbox Code Playgroud)

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

解决方法:直接使用短语匹配器

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()
Run Code Online (Sandbox Code Playgroud)

每个循环 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()
Run Code Online (Sandbox Code Playgroud)

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

结论

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

我很惊讶令牌匹配器优于短语匹配器。我以为是相反的:

如果您需要匹配大型术语列表,您还可以使用 PhraseMatcher 并创建 Doc 对象而不是标记模式,整体效率更高

我是否在这里遗漏了一些重要的东西,或者我可以在更大范围内相信这个分析吗?

Ine*_*ani 9

我认为最终,这一切都归结为在速度、代码的可维护性以及这段逻辑适合您的应用程序的更大画面的方式之间找到最佳权衡。在文本中查找几个字符串不太可能是您尝试做的最终目标——否则,您可能不会使用 spaCy 并且会坚持使用正则表达式。您的应用程序需要如何“使用”匹配的结果以及匹配在更大的上下文中意味着什么应该激励您选择的方法。

正如您在结论中所提到的,如果您的匹配项根据定义是“命名实体”,那么将它们添加到其中doc.ents就很有意义,甚至可以为您提供一种将逻辑与统计预测相结合的简单方法。即使它稍微增加了一些开销,它仍然可能胜过您必须自己编写的任何脚手架。

对于每个解决方案,我都从初始设置开始

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

我很惊讶令牌匹配器优于短语匹配器。我以为会相反

一种可能的解释是,您正在以非常小的规模和单标记模式分析这些方法,其中短语匹配器引擎与常规标记匹配器相比并没有真正的优势。另一个因素可能是匹配不同的属性(例如,LOWER而不是TEXT/ ORTH)需要在匹配期间创建一个Doc反映匹配属性值的新属性。这应该很便宜,但它仍然是一个额外的对象。所以一个测试Doc "extract January"实际上会变成"extract january"(当匹配时LOWER)或者甚至"VERB PROPN"当匹配时POS。这就是使匹配其他属性起作用的技巧。

关于其PhraseMatcher工作原理及其机制通常更快的一些背景知识:当您向 中添加Doc对象时PhraseMatcher,它会在模式中包含的标记上设置标志,表明它们与给定的模式匹配。然后它调用常规Matcher并使用先前设置的标志添加基于令牌的模式。当你匹配时,spaCy 只需要检查标志而不需要检索任何标记属性——这应该使匹配本身在规模上显着更快。

这实际上带来了另一种您可以进行分析以进行比较的方法:使用Vocab.add_flag在相应的词位上设置布尔标志(词汇中的条目,而不是上下文相关的标记)。Vocab 条目被缓存,所以你应该只需要为像"january". 但是,这种方法仅对单个令牌才真正有意义,因此相对有限。

我是否在这里遗漏了一些重要的东西,或者我可以在更大范围内相信这个分析吗?

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

最后,直接使用 spaCy 的Cython API永远是最快的。因此,如果速度是您的首要考虑并且您想要优化所有内容,那么 Cython 将是您的最佳选择。