N-gram かな漢字・漢字かな変換(C++版)

作った。

リポジトリはこちら。

https://github.com/hiroshi-manabe/ngram-converter-cpp



以前、N-gram 漢字-かな変換という記事で、N-gram を使ったかな漢字・漢字かな変換を公開した。

内部で使用しているアルゴリズムについては、可変次数 N-gram デコードのアルゴリズムの記事や、N-gram かな漢字変換 (スライド)で紹介した通り。

精度は、http://d.hatena.ne.jp/nokuno/20111103/1320317225で検証していただいた通り、それなりに出ていたと思うが、いかんせん速度が遅いのが問題だった。ちょっと長い文章を変換すると数秒間も時間がかかってしまう。これでは実用にならない。

それで、仕事を辞めて時間があるので、それを C++ で書き直してみた。N-gram の保存には、Faster and Smaller N-Gram Language Modelsで紹介されている手法を使った(公開されている実装は参考にしていない)。論文中で "Compressed" として解説されているもの。遅いのだが、容量との兼ね合いでこれにした。

また、N-gram 候補をすべて検索すると時間がかかるので、無駄なクエリを減らすためにブルームフィルタを使った。

使った N-gram データは、前回と同じく、@さんが公開されている言語モデル配布ページからダウンロードしたもの。


使用方法は、ルートディレクトリで

> make

すると、"converter_main" という実行ファイルができる。"-i" オプションで付属の辞書を指定して実行すると、入力待ちになり変換が行える。

かな漢字変換なら、"dict/bccwj4_rev" を指定。

> ./converter_main -i dict/bccwj4_rev
きょうはいいてんきですね
今日はいい天気ですね

漢字かな変換なら、"dict/bccwj4" を指定。

> ./converter_main -i dict/bccwj4
今日はいい天気ですね
きょうはいいてんきですね

変換結果は、backoff のデータの持ち方を変えたりしたこともあり、Python 版と必ずしも同じではない。

速度は、かな漢字はそこそこ、漢字かなは結構速い(適当)。

使っている N-gram は 4-gram までで、数は 1000万ほど。容量は、かな漢字・漢字かなそれぞれ 40MBほど。Python版では 60MB だったので、少しは小さくなった。


かな漢字・漢字かな変換プログラムとしてはこれでもいいのだが、これを IME にするためには、さらにいろいろな作業が必要になる。

最低限、次の二つ。

1. 文節区切りができるようにする。
2. 未知語(用言含む)を入れられるようにする。

欲を言えば、もう一つ。

3. 意味による書き分けと、好みによる書き分けをそれぞれ区別して扱えるようにする。


文節区切りができないと使えないというのは、(SKK などを除く)一般の日本語 IME を使う人なら実感としてわかると思う。日本語は同音異義語が多く、しかも文脈からはどうやっても確定できない場合もある。

(例:「それじゃいってみるよ」とだけ入力された場合、それが「それじゃ行ってみるよ」か「それじゃ言ってみるよ」かは確定できない。前者の方が数が多く、そちらを先に出すにしても、「行ってみるよ」にフォーカスを当てて「言ってみるよ」に変えられるようにするというインターフェースはどうしても必要になる。)

これは@さんのデータではどうにもならない。元データである「現代日本語書き言葉均衡コーパス(BCCWJ)」のデータを購入した(21万円)ので、どれが自立語かの情報を保ったまま N-gram を構築できないかやってみようと思う。これをうまくやろうとすると、いろいろと面倒なことがありそうだ。


未知語を入れるというのは、BCCWJ がいくら大きなコーパスだといっても、入っていない単語は数多くあるからだ。コーパスに出てくる単語の数と、現代的な IME の辞書に必要な単語の数の間には大きな開きがある。今回使った N-gram データでの語彙数は 16万ほどだが、各分野のメジャーな用語を入れて、ユーザーにストレスのないようなものにするには、少なくとも 50万ほど(用言は基本形で数える)は必要だ。

もっとも、BCCWJ は形態素単位のコーパスなので、複数形態素の単語は分割して N-gram のようにして入れるのか等、考えなければならないところも多い。

また、新たに辞書から用言を入れるとなると、その活用なども考えなければならない。


意味による書き分けと好みによる書き分けの区別というのは、文脈に依存させるべきかどうかが変わるので重要だ。

例えば、「くすりがきかなかった」「はなしをきかなかった」の「きかなかった」の違いは意味によるものだ。だから、文脈による影響を受けるべきで、「薬が効かなかった」「話を聞かなかった」とするのがよい。

好みによる書き分けというのは、「寂しい/淋しい」のようなもの。この二つには意味の違いがあるわけではない。しかし、N-gram などの言語モデルで見ると、例えばコーパスにたまたま「淋しそうにつぶやいた」という文が出てきたというだけのことで、普段は「さびしい」が「寂しい」に変換されるのに、「さびしそうにつぶやいた」と変換した時だけ「淋しそうにつぶやいた」になってしまうということが起こりうる(っていうか、今の変換プログラムではそうなる)。これでは、使用感からいうと、うっとうしいだけだ。多少なりとも文章にこだわりのある人であれば、「寂しい」と「淋しい」のどちらを使うかは統一したいものだ。さらにこだわりのある人で、感覚によって使い分けたいという人もいるかもしれないが、その場合でも依存するのは文脈ではないので、文脈によってまちまちなものが出てくるというのは使いにくい。

(もっとも、この「意味」と「好み」というのは、きれいに分かれるわけではない。例えば、「テレビを見る」と「テレビを観る」の間に意味の違いはまったくない。それでは、これは完全な好みかというとそうでもない。書き手は「テレビ」という単語に反応して「観る」を選んでいる。

英語のように "watch" と "look at" のような区別のある言語であれば、それぞれの単語に意味の違いがある。たとえば、「相手の顔をみる」という時に、どちらの動詞を使うかというので意味が変わる。しかし、日本語はそうではない。「みる」は一つしかなく、「見る」と書いても「観る」と書いても意味は同じだ。

意味が同じである以上、「看板をみる」「テレビをみる」ではどちらも「見る」を提示するのが理にはかなっている。しかし、テレビ・映画といった単語に反応して「観る」と書くような人にとっては、「看板をみる」では「見る」を、「テレビをみる」では「観る」を提示するのがいいということになるのかもしれない。使用者が、どれだけ気取った書き手であるかによって正解が変わってくる)

BCCWJ には、「語彙素」という欄があり、このあたりをある程度まとめてくれている。たとえば、コーパスに「テレビを見る」とあっても「テレビを観る」とあっても、「みる」の部分の語彙素は「見る」となっている。これは、どちらも日本語の動詞としては一つのものなので妥当なところだ。

難しいのは、「語彙素」として同じもの(日本語の音として一つの動詞であるもの)でも、慣習的に書き分けが要求されるものが多いということだ。「早い」と「速い」などはその一例だ。日本人が「はやい」という時、英語で言う "early" と "fast" にまたがる大きな概念が一つあり、それを状況によって書き分けている。これは「おそい」でも同じことだ。「おそい」の場合は "late" と "slow" にまたがる概念だが、たまたまこれらの書き分けは発達せず、どちらも「遅い」と書くことになっている。だから、BCCWJ で「早い」と「速い」を「早い」という一つの語彙素としてまとめるのは正しいのだが、実際上は、これらを文脈によって書き分けなければならない。そのために、これは「意味による書き分け」として分類しなければならず、そうすると BCCWJ の分類とはずれが生じてくる。


これらの他にも、コーパス自体の誤り、サイズが大きすぎるということなどのさまざまな問題があるが、とりあえず上記の問題がある程度クリアできたら、IME として動かすことを考えたい。

(2012/06/11 追記: 腐れ Makefile を @ 様に添削していただきました。)