パスワードを忘れた? アカウント作成
15077241 journal
人工知能

yasuokaの日記: spaCy + UniDic-COMBOの単語間係り受けと文節間係り受け

日記 by yasuoka

UniDic-COMBObunsetu_spansbunsetu_spanを実装したのだが、使い方が分かりにくいとの御意見をいただいた。私(安岡孝一)個人としては、文節間係り受けより単語間係り受けの方を使ってほしいのだが、この際なので、spaCyのDoc・Span・Tokenに、日本語の文章・文節・単語が、どう実装されているかを紹介しておこう。

>>> import unidic_combo
>>> nlp=unidic_combo.load("gendai")
>>> doc=nlp("私の名前は中野です。")
>>> print(doc,type(doc),len(doc))
私の名前は中野です。 <class 'spacy.tokens.doc.Doc'> 7

UniDic-COMBOの係り受け解析結果は、spaCyのDoc(文章)として返される。単語(Token)の配列だと考えていい。上の「私の名前は中野です。」は7つの単語から構成されており、doc[0]doc[1]、…、doc[6]で参照できる。とりあえず、doc[2]に注目してみよう。

>>> token=doc[2]
>>> print(token.orth_,type(token),token.lemma_,token.tag_,token.pos_)
名前 <class 'spacy.tokens.token.Token'> 名前 名詞-普通名詞-一般 NOUN

spaCyのTokenは、色々な情報を持っているのだが、基本的なところとしては.orth_(表層形)、.lemma_(辞書見出し形)、.tag_(品詞)、.pos_(Part-Of-Speech)が挙げられる。doc[2]の「名前」であれば、表層形と辞書見出し形は両方とも「名前」、品詞は「名詞-普通名詞-一般」、Part-Of-Speechは「NOUN」であることがわかる。

>>> print(token.i,token.doc)
2 私の名前は中野です。

Tokenが、Docの何番目の要素にあたるかは、.iに入っている。また、TokenからDocへ戻りたい場合は.docを使う。

>>> span=unidic_combo.bunsetu_span(token)
>>> print(span,type(span),len(span))
名前は <class 'spacy.tokens.span.Span'> 2

単語(Token)を含む文節(Span)が欲しい場合は、bunsetu_span(Token)を使う。上の例では「名前」を含む文節「名前は」を得ている。SpanもTokenの配列とみなせる。上の「名前は」であれば、span[0]に「名前」、span[1]に「は」のTokenが入っている。

>>> print(span.start,span.end,span.root)
2 4 名前

文節(Span)の主辞(中心となるToken)は、.rootに入っている。また、Spanを単語の配列として見た時、元のDocの何番目から何番目の要素にあたるかは、.start.endに入っている。上の例では、文節「名前は」はdoc[2:4](すなわちdoc[2]doc[3])にあたり、主辞は「名前」である。

>>> spanlist=unidic_combo.bunsetu_spans(doc)
>>> print(spanlist,type(spanlist),len(spanlist))
[私の, 名前は, 中野です。] <class 'list'> 3

文章(Doc)に含まれる全ての文節が欲しい場合は、bunsetsu_spans(Doc)を使う。Spanのリストが返されるので、注意されたい。上の例では「私の」「名前は」「中野です。」の3つのSpanが得られている。

ここまでが理解できたなら、次に単語間係り受けに関して、順に見ていこう。

>>> token=doc[2]
>>> print(token,token.dep_,token.head)
名前 nsubj 中野

Token(単語)の.headには、別のTokenが入っており、単語間係り受けを表す。.dep_は、係り受けの種類を示すタグである。上の例では、「名前」←「中野」という係り受けにnsubj(主語)というタグがつけられており、「名前」が「中野」の主語であることを示している。

>>> t=token.head
>>> print(t,t.dep_,t.head,t==t.head)
中野 ROOT 中野 True

単語間係り受けを辿っていくと、.headに自分自身が入っているTokenに行き当たる。これが、単語間係り受けにおける「ROOT Token」であり、「私の名前は中野です。」では「中野」が「ROOT Token」にあたる。

>>> print(list(t.lefts),t,list(t.rights))
[名前] 中野 [です, 。]

単語間係り受けを逆向きに辿るイテレータとして、Tokenには.lefts(文頭方向)と.rights(文末方向)が準備されている。上の例では、「名前」←「中野」、「中野」→「です」、「中野」→「。」という単語間係り受けが、「中野」から出ていることがわかる。

>>> import deplacy
>>> deplacy.render(doc,Japanese=True)
私   PRON  ═╗<╗     nmod(体言による連体修飾語)
の   ADP   <╝ ║     case(格表示)
名前 NOUN  ═╗═╝<╗   nsubj(主語)
は   ADP   <╝   ║   case(格表示)
中野 PROPN ═╗═══╝═╗ ROOT(親)
です AUX   <╝     ║ cop(繫辞)
。   PUNCT <══════╝ punct(句読点)

なお、単語間係り受けを可視化するツールとしては、deplacyがオススメだ。文章(Doc)内の全ての単語間係り受けを、テキスト環境で一望できる。

以下に述べるUniDic-COMBOの文節間係り受けは、実は、単語間係り受けを元にしている。ただし、文節間係り受けの矢印は、単語間係り受けとは逆方向に書くので、注意されたい。

>>> spanlist=unidic_combo.bunsetu_spans(doc)
>>> span=spanlist[1]
>>> print(list(span.lefts),span,list(span.rights))
[私] 名前は []

文節(Span)にも、.lefts.rightsが準備されている。ただし、これらが返すのは、Token(単語)のイテレータであり、Spanでは無い。

>>> s={unidic_combo.bunsetu_span(t) for t in span.lefts}
>>> print(s,span)
{私の} 名前は

そこで、bunsetu_span(Token)を使って、単語(Token)を文節(Span)へと拡張する。上の例では「私の」→「名前は」という文節間係り受けが、抽出されていることになる。

>>> from deplacy.deprelja import deprelja
>>> s={(unidic_combo.bunsetu_span(t),deprelja[t.dep_]) for t in span.lefts}
>>> print(s,span)
{(私の, '体言による連体修飾語')} 名前は

なお、文節間係り受けの種類を知りたい場合にも、deplacyが便利である。端的には、Tokenの.dep_(タグ)をdepreljaで読み替えればいい。上の例では、「私の」→「名前は」という文節間係り受けが、「体言による連体修飾語」(nmod)であることがわかる。

しかしながら、文節間係り受けを単語間係り受けの上に実装している以上、少なくともUniDic-COMBOにおいては、単語間係り受けを中心に作業すべきだろう。その上で、単語間係り受けで分かりにくい部分を、文節へと適宜拡張する、というあたりがいいと思う。

この議論は、yasuoka (21275)によって ログインユーザだけとして作成されたが、今となっては 新たにコメントを付けることはできません。
typodupeerror

ソースを見ろ -- ある4桁UID

読み込み中...