crispy.data

spaCy で Invalid span が発生したら

2023-10-19
日本語に対して文字単位でラベル付けした内容を可視化しようとしていたら原因が分かりづらいエラーが発生したので対処方法を記載しておく。

発生したエラー

文字列に対して文字単位でラベル付きスパンを定義し、可視化しようとしたら、、、
nlp = spacy.blank('ja') text = 'おのののかなのか!' doc = nlp(text) doc.spans["annotation"] = [ doc.char_span(0, 5, 'PERSON') ] options = { "spans_key": "annotation", "colors": { "PERSON": 'skyblue' } } displacy.render(doc, style="span", options=options)
以下のエラーが発生した。
ValueError: [E855] Invalid span: span is not from the same doc.
「span が 同一の doc から指定されていないよ」と怒られているが、doc は一つしか定義していないし、エラーメッセージからどう対処すれば良いか判断に困った。

対応内容

char_span 関数の実行時に alignment_mode = ‘contract’ または alignment_mode = ‘expand’ を指定する。

原因

char_span 関数の alignment_mode を指定しない場合、alignment_mode = ‘strict’ として実行される。 alignment_mode = ‘strict’ の場合、指定した文字スパンと doc 内の token のスパンの区切り位置がちょうど同じである必要がある。 前掲の例だと、指定した文字列スパンの終端が 4 番目の token の真ん中になっていて不整合が生じている。
for token in doc: print(token, token.idx, token.idx + len(token))
お 0 1 のの 1 3 の 3 4 かな 4 6 の 6 7 か 7 8 ! 8 9
この不整合を許容する option が ‘contract’‘expand’‘contract’ は、より狭くスパンの判定を行うようになる。 前掲の例であれば、半端にスパンが被っている ”かな” はスパンに含めない挙動になる。 実際に可視化すると以下の出力になり、”かな” までラベルが振られていないことが確認できる。
逆に、’expand’ はより広くスパンの判定を行う。 つまり、“かな” は少しでも指定した文字列スパンと重複しているため、スパンに含める挙動になる。 ‘contract’ とは違い、”かな” までラベルが振られていることが以下で確認できる。

“おのののか” にラベル振りたいんだけど

”おののの” や ”おのののかな” にラベルを振るのではなく、tokenizer(または vocab)の制約を考慮せずに ”おのののか” にラベルを振りたい人もいるはず。 この場合、力技だが 1 文字の単語から構成された Doc を用意することで、token のスパンの不整合を生じずにラベルを振れる。
from spacy import displacy from spacy.tokens import Doc from spacy.vocab import Vocab text = 'おのののかなのか!' words = list(text) spaces = [False] * len(words) doc = Doc(Vocab(), words=words, spaces=spaces) doc.spans["annotation"] = [ doc.char_span(0, 5, 'PERSON') ] options = { "spans_key": "annotation", "colors": { "PERSON": "skyblue", } } displacy.render(doc, style="span", options=options)