一月ほど前の話になりますが、spaCy v3.3.1 がリリースされました。いくつかの機能の追加とバグフィックス が行われているのですが、その1つとしてSpanRuler と呼ばれるコンポーネント が追加されています。このコンポーネント はルールベースで固有表現認識などを行うための機能を備えています。日本語での解説を見かけなかったので、本記事で簡単に紹介します。
SpanRulerとは?
spaCyでルールベースの固有表現認識をする場合、EntityRuler がよく使われてきました。EntityRuler
はパターンに基づいて固有表現認識をするためのコンポーネント です。統計的なモデルと組み合わせて使うこともできるので、認識性能の向上にも役立ちます。パターンの書き方は、次に示すように、文字列を指定する方法と辞書を指定する方法があります。ラベルに関しては"label"
キーで指定します。
# 文字列マッチ
{"label": "ORG", "pattern": "Apple"}
# 辞書によるパターンの指定
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}
後の説明のために、EntityRuler
の使い方を簡単に紹介しておきましょう。典型的には、nlp
オブジェクトのadd_pipe
メソッドを呼び出してパイプラインにコンポーネント を追加します。そうすることで、doc.ents
からマッチした固有表現を得ることができます。次のコードでは、先ほど紹介した2つの方法でパターンを指定しています。
from spacy.lang.ja import Japanese
nlp = Japanese()
ruler = nlp.add_pipe("entity_ruler" )
patterns = [
{"label" : "FOOD" , "pattern" : "ナポリタン" },
{"label" : "FOOD" , "pattern" : [
{"POS" : "NOUN" , "OP" : "+" },
{"TEXT" : "スパゲティ" }
]}
]
ruler.add_patterns(patterns)
doc = nlp("ナポリタンとたらこスパゲティと和風きのこスパゲティ" )
for ent in doc.ents:
print (ent.text, ent.label_)
結果は次のようになります。
ナポリタン FOOD
たらこスパゲティ FOOD
和風きのこスパゲティ FOOD
SpanRuler
もEntityRuler
と同様にルールベースで固有表現認識などをできるのですが、より一般化されたコンポーネント である点が異なります。EntityRulerでは認識したエンティティをdoc.ents
に追加しましたが、SpanRuler
ではdoc.spans
かdoc.ents
にスパンを追加できます。doc.ents
は重複を許さないのに対し、doc.spans
は次の画像に示すような重 複を許容できます。
重複したスパン(Spancat: a new approach for span labeling より)
また、doc.ents
は重複を除去するためにスパンのフィルタリングをしているのですが、SpanRuler
では独自のフィルタリングアルゴリズム を適用できる点が異なります。デフォルトではfilter_spans 関数を使って、スパンが重なっている場合は、短いスパンよりも1番長いスパンを優先するようにフィルタリングしていますが、このアルゴリズム を変えることができるのです。
SpanRulerの使い方
EntityRuler
と同じく、SpanRuler
も通常はnlp.add_pipe
で追加します。nlp
オブジェクトがテキストに対して呼び出されると、doc
内でマッチを見つけ、指定されたパターンラベルをエンティティラベルとして、doc.spans["ruler"]
にスパンとして追加します。デフォルトのキー名は"ruler"
ですが、設定により変更可能です。doc.ents
とは異なり、doc.spans
では重複が許容されるのでフィルタリングは必要ありませんが、オプションでスパンにフィルタリングとソートを適用できます。
import spacy
nlp = spacy.blank("en" )
ruler = nlp.add_pipe("span_ruler" )
patterns = [
{"label" : "ORG" , "pattern" : "Apple" },
{"label" : "GPE" , "pattern" : [
{"LOWER" : "san" },
{"LOWER" : "francisco" }
]}
]
ruler.add_patterns(patterns)
doc = nlp("Apple is opening its first big office in San Francisco." )
print (doc.ents)
print ([(span.text, span.label_) for span in doc.spans["ruler" ]])
出力は次のようになります。デフォルトではdoc.spans
に認識結果が格納されていることがわかります。
()
[('Apple' , 'ORG' ), ('San Francisco' , 'GPE' )]
上の例では、doc.ents
が空でしたが、コンポーネント の設定としてannotate_ents: True
を渡すことでdoc.ents
に結果を格納できます。
nlp = spacy.blank("en" )
config = {"annotate_ents" : True }
ruler = nlp.add_pipe("span_ruler" , config=config)
ruler.add_patterns(patterns)
doc = nlp("Apple is opening its first big office in San Francisco." )
print (doc.ents)
print (doc.spans)
(Apple, San Francisco)
{'ruler' : [Apple, San Francisco]}
デフォルトのフィルタリングは1番長いスパンを残しましたが、カスタムフィルターを書けば、残すスパンを変更できます。まずは、デフォルトの場合について見てみましょう。今回の場合、パターンとして「San Francisco」「Giants」「San Francisco Giants」の3つを登録しています。
nlp = spacy.blank("en" )
config = {"annotate_ents" : True }
ruler = nlp.add_pipe("span_ruler" , config=config)
patterns = [
{"label" : "GPE" , "pattern" : [
{"LOWER" : "san" },
{"LOWER" : "francisco" }
]},
{"label" : "ORG" , "pattern" : [{"LOWER" : "giants" }]},
{"label" : "ORG" , "pattern" : [
{"LOWER" : "san" },
{"LOWER" : "francisco" },
{"LOWER" : "giants" }
]},
]
ruler.add_patterns(patterns)
doc = nlp("San Francisco Giants" )
print (doc.ents)
結果は次のようになります。1番長いスパンを残していることがわかります。
(San Francisco Giants,)
では、カスタムフィルターを書いて、適用してみましょう。コンポーネント の設定にents_filter
を渡すことで、カスタムフィルターを指定できます。
import itertools
from typing import Iterable, List
def filter_spans (spans: Iterable["Span" ]) -> List["Span" ]:
sorted_spans = sorted (spans, key=lambda span: span.end)
result = sorted_spans[:1 ]
for span in sorted_spans[1 :]:
if result[-1 ].end <= span.start:
result.append(span)
return result
def filter_chain_spans (*spans: Iterable["Span" ]) -> List["Span" ]:
return filter_spans(itertools.chain(*spans))
@ spacy.registry.misc ("spacy.maximized_spans_filter.v1" )
def make_maximized_spans_filter ():
return filter_chain_spans
nlp = spacy.blank("en" )
config = {
"annotate_ents" : True ,
"ents_filter" : {'@misc' : 'spacy.maximized_spans_filter.v1' }
}
ruler = nlp.add_pipe("span_ruler" , config=config)
ruler.add_patterns(patterns)
doc = nlp("San Francisco Giants" )
print (doc.ents)
結果は次のようになりました。フィルターを渡すことで、結果が変わることを確認できました。
(San Francisco, Giants)
ここで紹介した以外にも、いくつかのオプションを設定として渡すことができます。詳細は、SpanRuler
を参照してください。
参考資料