Skip to content

Small Dictionary

20年ほど前、検索エンジンを作っていた。日本語の検索には形態素解析が必要で、当時の定番だったChaSenを使っていた。正しい文章に対しては正確に動く。だがインターネットのテキストは正しくない。

新しいアイドルの名前、発売されたばかりの商品名、SNSで生まれた動詞。「ググる」「バズる」「タピる」。辞書に載っていない単語は、形態素解析器にとって存在しない単語だ。存在しない単語はインデックスに正しく載らない。ユーザーが検索しても見つからない。辞書を更新すればいい。だがインターネットは辞書のメンテナンスより速く新語を生む。

当時から思っていたことがある。未知語はある程度、文脈から推測できるのではないか。漢字の後にひらがなが続けば動詞の可能性がある。カタカナが連続すれば外来語か固有名詞だ。辞書がなくても、文字の並びと文法の知識があれば、かなりの精度で解析できるのではないか。

20年越しでそれを形にした。日本語の形態素解析器をC++で書き、WebAssemblyにコンパイルした。gzip後400KB未満。ブラウザで動く。サーバーは要らない。

現在の事実上の標準であるMeCabの辞書はIPAdicで約20MB。その辞書にすべての単語とコストが入っていて、テキストをスキャンし、辞書にマッチする候補を列挙し、コスト最小のパスを選ぶ。辞書の質が精度を決める。明快なアプローチだ。

辞書を削ると、代わりに文法を実装する必要がある。「食べられない」をMeCabは辞書から「食べ」「られ」「ない」を引いて解く。辞書なしで解くなら、「食」が漢字で「べられない」がひらがなだから動詞の可能性がある、一段活用の未然形に助動詞「られる」の未然形が接続し、さらに否定の「ない」が続いている、と文法規則から組み立てるしかない。五段活用、一段活用、カ変、サ変。音便。接続規則。辞書を削るほど、日本語への理解が求められた。

助詞や助動詞のように、これ以上増えない品詞はハードコードした。変わらないものはコードに埋める。変わるものだけを小さな辞書に入れる。ルールで生成可能な単語は辞書に入れない。辞書は例外のためにのみ存在する。そう決めた。

テストケースはJSONで定義した。C++のテストコードに期待値を埋め込むと、ケースが増えるたびに保守が苦しくなる。入力と期待される形態素列のペアをJSONに外出しし、MeCabの出力を参照データとして使っている。2,000件を超えた。

ただし、MeCabは間違えることがある。「飲まされた」をMeCabは「飲まさ・れ・た」と分割する。だが文法的には「飲ま・さ・れ・た」が正しい。動詞の未然形「飲ま」に使役の「さ」、受身の「れ」、過去の「た」。MeCabは使役の「さ」を動詞に吸収してしまう。しかもこの挙動は一貫しておらず、「読まされた」は正しく分割する。辞書ベースの解析器は、辞書の登録内容に引きずられる。間違えている場合は、文法的に正しい方を採用する。MeCabは参考にするが、盲従はできない。

ここで気づいたことがある。Cコンパイラにはgccというオラクルがある。どんな入力に対しても正解を返してくれる絶対的な基準だ。テストを書くとき、gccの出力と一致するかを見ればいい。AIにCコンパイラを作らせることができたのは、このオラクルがあったからだ。

形態素解析にはオラクルがない。MeCabは近いが完全ではない。そもそも日本語の分かち書きに唯一の正解があるのかどうかすら曖昧だ。「東京都」は「東京」+「都」か、「東京都」という一語か。文脈と用途で答えが変わる。正解を定義すること自体が曖昧な領域では、クリーンルームでAIに丸投げすることもできない。自分の頭で規則を書き、自分の判断で正解を決めるしかない。

精度はMeCabには及ばない。20MBの辞書が持つ力は大きい。日本語には例外が多すぎる。ルールで捉えきれない当て字、方言、表記揺れ。辞書を小さくするという判断は、精度とのトレードオフを常に抱えている。

それでも、辞書に載っていない単語に対して「存在しない」と言わない解析器を作りたかった。辞書を小さくする作業は、引き算に見えて足し算だった。削った分だけ、日本語を理解する必要がある。