日本語/英語の組み合わせ範疇文法パーサーを作った

toyccg
https://github.com/vertexoperator/toyccg


英語については、CCG(Combinatory Categorial Grammar)のparserは公開されているものが、いくつか存在する(疑問文とか命令文は、あんまり対応してなかったりする。toyccgも疑問文や命令文は、あんまり対応できてない。やればできるとは思う)けど、日本語については、知る限りではない(公開せずに実装している人はいると思うけど)ので。原理的に新しいことは何もないけど、実用的に使うのに、必要そうな機能は、一通り実装してあると思う


自然言語用のパーサーとしては

pythonで1500〜2000行程度((自然)言語非依存のコア部分は、1000行くらい。外部ライブラリは使ってないし他(プログラミング)言語への移植は楽と思う)で比較的小さい
・日本語と英語同時対応
機械学習フリー

などの特徴がある。機械学習を使用してないのは、そもそも、日本語の学習用データが存在しない(多分)(英語用では、CCGBankがあって、普通これを使うと思うけど、どうやって入手するのか謎なので、やっぱり使えない)のもあるし、機械学習頼りだと、手で修正するということがやりづらくなるという面もある。手でルール作るとか、時代の流れに逆行している気もするけど、実用上は悪いことばかりでもない(CCGBankはTreeBankからの自動生成っぽいけど、学習用データを自分で作る場合、やっぱり手間がかかるわけだし)

#CFGを確率化した確率文脈自由文法(PCFG)と同様、CCGを確率化した、確率的組み合わせ範疇文法(PCCG)のような方法も存在する


nltkのCCGパーサーとの違いは

・デフォルトで辞書を用意している
・normal form parsingをサポートしている
・nltkのパーサーで日本語を解析する場合、単語単位への分かち書きは別に行う必要があるけど、toyccgは、分かち書き構文解析を同時にやる

というあたり。分かち書き対応したおかげで、英語でも、New YorkとかWhite Houseみたいなのを一単語のようにして扱うことができるようになった(やろうと思えば、"This is a pen"のような文単位で統語範疇を辞書に登録することもできる)。あとまぁ、長単位・短単位みたいなのを気にする必要も、あまりない


#"数学的構造"みたいなのを辞書登録すると、数学+的+構造で分解するのと、"数学的構造"で単語扱いする結果が、ダブって出現することになる。こういう単語が多い文では、計算時間の増大にも繋がるし、本質的に同じ解析結果が沢山出現することにもなる。toyccgで、このパターンで計算量が増えるケースは結構ある


#CCGでは、長単位・短単位以外に、品詞についても、考える必要がない(これをメリットと捉えるかは人によるかもしれないけど)。名詞・動詞は分かりやすいけど、実際に作業していて、日本語文法の知識が殆どない私には、助詞・助動詞の区別は迷うことがあるし、例えば、もう死語のような気がするけど

「なう」の品詞ってなんですか?
http://togetter.com/li/258854

という話でも、CCGだと、"S\N"みたいな統語範疇を設定してやれば"渋谷なう"みたいな文章を解析できる。これに類似した統語範疇(つまり、"S[*]\N[*]")を持つ単語は、今のところ他にはない。動詞や形容詞は統語範疇として"S\NP"を持つので、近いと言えるかもしれない。"ご飯食べてるなう"だと、"S[now]\S"とするとか。統語素性[now]を付けているのは、"ご飯食べてるなうなう"みたいな文章を是としないため。



統語素性の設計について。日本語の統語素性は、ベースになるようなものがないので、勝手に作ったものが沢山ある(一応、「日本語文法の形式理論」という本が一冊出ていて、それは参考にした)。英語の場合は、CCGBankがデファクトスタンダードになっているので、なるべくそれに従っているけれど、改変してある部分もある(例えば、toyccgは前置詞句PPを使わない)。これらは、辞書の問題なので、辞書を差しかえれば、実装の方を変えることなく、他の統語範疇・統語素性設計に対応できる(はず)

#CCGBankは、結構謎のunary rulesがあって、本来のCCGからは、はみ出しているので、これが気に入らない。toyccgも、このへんの問題はクリアできてなくて、適切に統語範疇・統語素性を設計すれば解決するのか、それともどうしようもないのかは、不明。英語ではCCGBankが標準のようになっているけど、本当に、これでいいのかはよく検討するべきことと個人的には思う


unary rulesとして、どういうものがあるかは、
https://github.com/mikelewis0/easyccg/blob/e42d58e08eb2a86593d52f730c5afe222e939781/training/unaryRules
などを見ると分かる



機械学習ベースじゃないことのデメリットとして、構文解析できないケースが、増えてしまう(実用上は、何でもいいから、とりあえず結果を出してくれた方が嬉しい時もあれば、ダメな時には、あからさまに失敗してくれた方がいい時もあると思うので、一概にデメリットとも言い切れない。toyccgの場合、日本語として、文法が崩壊している文章は、解析されない可能性が高い)。一応、それなりには頑張っていて

>>> import toyccg.japanese as jpn
>>> jpn.run(u"すももももももももの内。")
test run : sentence=すももももももももの内。
すもも	N
も	(NP[sbj]/NP[nom-enum])\N
もも	N
も	NP[nom-enum]\N
もも	N
の	((S[nom]\NP[sbj])/(S[nom]\NP[sbj]))\N
内	S[nom]\NP[sbj]
。	ROOT\S[nom]

>>> jpn.run(u"象は鼻が長い。")
test run : sentence=象は鼻が長い。
象	N
は	NP[sbj]\N
鼻	N
が	NP[ga-acc]\N
長い	(S\NP[sbj])\NP[ga-acc]
。	ROOT\S

くらいの解析はできる。sentences.ja.txtには、解析できる文章として、どれくらいのものがあるか、100以上の例がある。一応、ニュースや論文に出てくるような、比較的固めの文章なら、そこそこ解析できるように頑張っている(解析できるとは言っていない)。twitter話し言葉で出てくるような文章となると、なかなか難しい。これらの文章では、格助詞の省略がよく起きて、難しい原因の一つとなる。その他、未知語が含まれている場合(これは、ニュースや論文でも、よくある。一応、文字種に基づく未知語推定を行えるしているが)や、倒置法とかは全く対応されてない


助詞が省略されていても、

>>> jpn.run(u"お腹すいた。")
test run : sentence=お腹すいた。

>>> jpn.parser.lexicon.setdefault(u"お腹",[]).append( "NP[sbj]" )
>>> jpn.run(u"お腹すいた。")
test run : sentence=お腹すいた。
お腹	NP[sbj]
すい	IV[euph]
た	(S\NP[sbj])\IV[euph]
。	ROOT\S

みたいに、適切な統語範疇を辞書登録してやれば、対応できないわけではないけど。安易にこういうことをして、間違った構文解析結果が増えてしまうと、それはそれで困る。


#既存部分への影響を、頭を使わずに、確実に抑えたいなら、NP[x]とか適当な新しい素性を導入して、動詞や形容詞のような単語にも、対応する統語範疇("S\NP[x]"など)を追加していくなどの手もある。スマートな解決策とは言えないけど、実用的な解析を行うには、そういうことも必要かもしれない。日本語は、話し言葉レベルでは、「私、昨日、学校行った」みたいな格助詞が全部落ちたような文章も可能であるから、そこまで行くと、現時点では、どれが主語か正しく判定するのは難しい



jpn.runは、形態素解析的な結果しか出さないけど、内部的には、構文木が作られている

>>> import toyccg.japanese as jpn
>>> r = jpn.parser.parse(u"あらゆる現実を、全て自分の方へ捻じ曲げたのだ。")
>>> t = r.next()
>>> print(t.show())
(LApp (LApp (SkipCommaJP (LApp (RApp [あらゆる:N/N] [現実:N]) [を:NP[obj]\N]) [、:COMMA]) (RBx [全て:S[null]/S[null]] (RBx (LApp (RApp (LApp [自分:N] [の:(N/N[base])\N]) [方:N[base]]) [へ:(S[null]/S[null])\N]) (LB (LApp [捻じ曲げ:TV[euph]] [た:(S[null]\NP[obj])\TV[euph]]) [のだ:S[null]\S[null]])))) [。:ROOT\S[null]])

あんまり見やすい出力ではないけど。多くの場合、構文解析結果は一意でないので、結果はジェネレータで、一つずつ返ってくる。

#デフォルトでは、ソースコード中で、shortcutと書いてある"近似"が有効になっていて、例えば、"かわいい私の妹"は、二種類の係り受け構造("かわいい"が"私"に係る場合と、"妹"に係る場合)が存在するが、どちらの場合も統語範疇は"N"となる(これは、本質的に構造が違うので、normal form parsingでは対処できない)ので、それ以降の解析には影響しない。こういうのが沢山あると、長文では、解析結果が沢山出てきて、計算時間が膨大になる。こういう状況を回避するため、先に出てきた結果だけを取って、それ以降のは使わないという処理をしている。最終的な構文木が一つしか必要ない場合は、shortcutをTrueにしておく方が、計算が速く済む(大体、ニュースとか論文とかの文章だと、これがないと一文の計算が全然終わらないことが多い)けど、本当に、全ての結果が必要な場合は、shortcutをFalseにする必要がある


分かち書きしたいだけなら、

>>> print [c.token for c in t.leaves()]
[u'\u3042\u3089\u3086\u308b', u'\u73fe\u5b9f', u'\u3092', u'\u3001', u'\u5168\u3066', u'\u81ea\u5206', u'\u306e', u'\u65b9', u'\u3078', u'\u637b\u3058\u66f2\u3052', u'\u305f', u'\u306e', u'\u3060', u'\u3002']

とかできる



感想。色々と実験するプロトタイプ的なつもりだったのだけど、思ったよりも、よく解析できるな、という印象(自画自賛)。


課題
・複合名詞対応
・格助詞落ち対応


今後。もう少し改善しようかと思ってたけど、飽きてしまったので、当分いじることはなさげ(完)