調和技研 技術ブログ

調和技研で取り組んでいる技術を公開していきます

GiNZAによるNLP事始め

はじめに

調和技研 高松です。最近は、自然言語処理の案件にかかわることが多いです。 ここでは、最近v2.0がリリースされたばかりのGiNZAを使ったNLPの1つを試してみます。

NLP

自然言語処理 (NLP: natural language processing) は人間が日常的に使っている言葉をコンピュータによって処理すること全般を指し、翻訳、要約、検索など様々な応用があります。 最近だと、チャットボットなどが人気でしょうか。

spaCy, GiNZA

spaCy*1NLPライブラリの1つで、速度や品質が「Industrial-Strength」なことを謳っているライブラリです。 構文解析やその可視化などが容易にできます。 spaCy自体でも日本語は対応していますが、GiNZA*2はspaCyとSudachiPyを利用したUD対応の構文解析器です。 利用者としては、単にspaCyを使うように使えます。

UD

UD (universal dependencies) *3構文解析の表現方法として、構造表現やタグセットを言語横断*4で定義しようというなかなか野心的な試みです。 spaCy, GiNZAはUDに対応したライブラリです。 形態素解析としてはMeCab, 係り受け解析としてはCaboChaが日本語処理では有名ですが、UDベースのライブラリでは、言語非依存の枠組みの中で固有表現抽出や文章分類といった機能を使うことができます。

インストール

2019/7/8にv2.0.0がリリースされました。 インストールはpipで出来ます。 以降では、Google Colaboratory 上で動くサンプルで説明していきます。

!pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"

上記を実行して、最後に以下のような出力が出れば、成功です。

Successfully installed SudachiDict-core-20190531 SudachiPy-0.3.3 ginza-2.0.0 ja-ginza-2.1.0

インストール後はランタイムを再起動する必要があると思います。

解析

下記のdocを作るところで解析が走ります。

import spacy
nlp = spacy.load('ja_ginza')
doc = nlp('私は彼にアドバイスした。')
print('# 表象型 原型 pos_ tag_ dep_ head.i')
for sent in doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)
    print('EOS')

成功すれば、以下のような出力になります。

※load('ja_ginza') で失敗する場合、ランタイムの再起動が必要

# 表象型 原型 pos_ tag_ dep_ head.i
0 私 私 PRON 代名詞 nmod 4
1 は は ADP 助詞-係助詞 case 0
2 彼 彼 PRON 代名詞 obl 4
3 に に ADP 助詞-格助詞 case 2
4 アドバイス アドバイス VERB 名詞-普通名詞-サ変可能 ROOT 4
5 し 為る AUX 動詞-非自立可能 aux 4
6 た た AUX 助動詞 aux 4
7 。 。 PUNCT 補助記号-句点 punct 4
EOS

tag_の部分の出力はMeCabなどを使ったことがあれば、見たことがあるかもしれません。 UniDic(をベースにした辞書) の品詞情報が(中分類以下はハイフンで連結されて)格納されています。

pos_ はUDにおけるPOS (part-of-speech) tagです。VERB は動詞を表しています。 UDのタグは用法主義といって、同じ単語でも使い方によってタグが変わります。*5

上記の「アドバイス」はUniDic的には名詞(サ変可能)ですが、UD的にはVERB(動詞)です。 GiNZAはこの辺を、形態素解析後のポスト処理で訂正しているようです。*6

可視化

spaCyの可視化ツールで解析構造を可視化できます。簡単...。

from spacy import displacy
options = {"fine_grained": False}
displacy.render(doc, style="dep", jupyter=True, options=options)
options = {"fine_grained": True}
displacy.render(doc, style="dep", jupyter=True, options=options)

f:id:chowagiken_takamatsu:20190712175422p:plain

optionsfine_grained: Trueを指定すると、tag_の方で表示します。

...。あれ⁉ 「アドバイス」が'NOUN'(名詞)になってますね。 spaCyのコードを追っかけたところ、spacy.displacyの中で、docのuser_dataを削除 *7しています。 どうも、GiNZAはuser_dataを使ってポスト処理による訂正を保持しているようで、これを消すと訂正前の値が表示されるようです。 これは、GiNZAとspaCyのどちらのissueに上げようかな...。

因みに、英語の場合も同じようなツリー構造になります。異なる言語でも同様のツリー構造になるところがUDのメリットの1つです。

en_nlp = spacy.load("en_core_web_sm")
en_doc = en_nlp('I advised him.')
for sent in en_doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)
    print('EOS')
displacy.render(en_doc, style="dep", jupyter=True)

f:id:chowagiken_takamatsu:20190711211455p:plain

応用

最後に、「言語処理100本ノック 2015」*8から、題材として使われている『吾輩は猫である』を拝借して、解析してみます。

import urllib.request

url = 'http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt'
req = urllib.request.Request(url=url)
with urllib.request.urlopen(req) as res:
    body = res.read().decode('utf-8')

解析は2分半ほどかかりました。

%%time
doc2 = nlp(body)

「100本ノック 46. 動詞の格フレーム情報の抽出」 のように、文章の構造を見てみます。 以下では、「我輩」が(何かを)「見て」いる文を探してみましょう。 UDでの関係は 見る → 我が輩*9 の構造になります。 また、関係ラベルをobj(目的語)以外に限定することで、(誰かが)吾輩を見た文章を除外します。

# 吾輩が何か見たパターンを抽出

def print_subtree(title, token):
  print('  ' + title + ': ', end='')
  for t in token.subtree:
    print(t.orth_+' ', end='')
  print('')

def print_pattern(st):
  """SVO的なものを出力"""
  if st.dep_== 'nsubj':
    print_subtree('主語', st)
  if st.dep_== 'iobj':
    print_subtree('間接目的語', st)

  vt = st.head
  print('  動詞: ' + vt.orth_)
  
  for ot in vt.children:
    if ot.dep_ == 'obj':
      print_subtree('目的語', ot)
  print('')
  
  
count_all = count_s = 0
for sent in doc2.sents:
  for token in sent:
    if token.lemma_=='我が輩' and token.head.lemma_=='見る':
      count_all += 1
      if token.dep_ != 'obj':
        count_s += 1
        print(sent)
        print_pattern(token)
print(f"not obj={count_s}, all={count_all}")

実行結果は下記です。(抜粋)

吾輩はここで始めて人間というものを見た。
  間接目的語: 吾輩 は 
  動詞: 見
  目的語: 人間 と いう もの を 

...

現に吾輩は今朝風呂場でこの鏡を見たのだ。
  主語: 吾輩 は 
  動詞: 見
  目的語: この 鏡 を 

と考え定めた吾輩はにゃあにゃあと甘えるごとく、訴うるがごとく、あるいはまた怨ずるがごとく泣いて見た。
  主語: 考え 定め た 吾輩 は にゃあにゃあ と 甘える ごとく 、 訴 うる が ごとく 、 
  動詞: 見

not obj=12, all=14

「吾輩」と「見る」が関係を持つ文章は14ありますが、目的語になっていないのは12個抽出できました。

所感

NLPもライブラリが整備され、最新の成果が非常に利用しやすくなってきたと思います。