Sunday, 11 August 2019

Token extension versus matcher versus phrase matcher vs entity ruler in spaCy

I am trying to figure out the best way (fast) to extract entities, e.g. a month. I have come up with 5 different approaches using spaCy.

Initial setup

For each solution I start with an initial setup

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?'

Solution: using extension attributes (limited to single token matching)

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 per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Solution: using phrase matcher via 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 per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Solution: using token matcher via entity ruler

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 per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Solution: using phrase matcher directly

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 per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Solution: using token matcher directly

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 per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Conclusion

The custom attributes with is limited to single token matching and the token matcher seems to be faster so that seems to be preferable. The EntityRuler seems to be the slowest which isn't surprising since it is changing the Doc.ents. It is however quite convenient that you have your matches in Doc.ents so you might want to consider this method still.

I was quite surprised that the token matcher outperforms the phrase matcher. I thought it would be opposite:

If you need to match large terminology lists, you can also use the PhraseMatcher and create Doc objects instead of token patterns, which is much more efficient overall

Question

Am I missing something important here or can I trust this analysis on a larger scale?



from Token extension versus matcher versus phrase matcher vs entity ruler in spaCy

No comments:

Post a Comment