読者です 読者をやめる 読者になる 読者になる

Write and Run

it's a simple way, but the only way.

日本語の形態素解析以外にもMeCabを使う、またはMeCabの辞書の仕組み

みなさん、和布蕪は好きですか。私は食べたことがありません。

さて、MeCab は優秀な日本語の形態素解析機として有名ですが、みなさんは MeCab をそれ以外の用途で使ったことがありますか。食わず嫌いは感心できませんねぇ。

日本語の形態素解析機としての振る舞いは MeCab のほんの一面に過ぎません。MeCab はいつも読んでる IPAdic が何語の辞書かなんて知りませんし、日本語の文法がハードコートされているわけでもありません。MeCab は、振る舞いの全てを辞書に決められているといっても過言ではないほど、辞書によって様々な「言語」を解析できるようになります。ここでいう「言語」とは、記号の並びの規則、またはその規則に則って並べられた記号列のことだと思ってください。つまり、「辞書」は言語の語彙だけでなく、規則を記述する能力を持っているのです。

MeCab の「辞書」に含まれる情報に軽く触れておきましょう。まずはいわゆる辞書、語彙セットです。これには必須の情報として「表層形」「左文脈 ID」「右文脈 ID」、それから「コスト」の4つが含まれています。IPAdic を用いて日本語を形態素解析したときの品詞やらという情報はこの4つの情報に追加で MeCab が読まないメタデータとして載っています。MeCab からしてみたら品詞がどうとか知ったことではないわけです。

表層形* 左文脈 ID* 右文脈 ID* コスト* ここから先はメタデータ
1285 1285 594 名詞,一般,,,,,本,ホン,ホン

じゃあどうやって形容詞は名詞にかかるだの、連用形のあとには用言が来るだのといった文法を MeCab は理解しているのでしょうか。そこで登場するのが「左文脈ID」「右文脈ID」です。

「辞書」には語彙の他に、連接表*1というものがあります。これには「左文脈 ID」と「右文脈 ID」のすべての組み合わせに関して、その組み合わせのコストが書かれています。品詞の細分類ごとに文脈IDを振ればそれらの組み合わせについてコストを決定することでなんとなく文法を定義できそうですよね。だんだんわかってきましたね。実際、IPAdic では、すべての品詞細分類に ID が振られており、その数なんと1316。品詞分類をもっと細かくした UniDic では5981種類もの ID があります。

文脈IDの「右」やら「左」とは一体何者なのでしょうか。A B という単語(形態素)の並びのコストを求めてみましょう。

A B
左文脈 ID
100
右文脈 ID
300
左文脈 ID
200
右文脈 ID
400
A→B のコストはこの
組み合わせを見る

「左の単語の右の ID」と「右の単語の左の ID」の組で表を読み、そこに書いてあるコストを読みます。この場合ですと、300 200 ですから、その行を連接表から探します。

右文脈 ID 左文脈 ID 連接コスト
…前略…
300 200 -50
…後略…

無事、A B の連接コストは -50 であることがわかりました。

なぜ左と右に ID があるのかという疑問に答えてくれる資料は見つからないわけですが、Python による日本語自然言語処理によれば、

品詞は形態素を左から見た場合と右から見た場合では違うものとして扱うのが一般的である

らしいので、そういうことだと思いましょう。しかしというべきかやはりというべきか、残念ながら IPAdic も UniDic も左文脈 ID と右文脈 ID には同じ値が入っており、あまり有効活用されていない仕組みです。

本題

MeCab の辞書の仕組みがわかったところで、やっと本題です。

マレー語(マレーシア語)の接頭辞・接尾辞と語幹を分割するために MeCab を使ってみます。IPAdic を食わせた MeCab は日本語の「文」を解析するわけですが、ここではマレー語の「単語」を解析します。

マレー語の単語は接頭辞・語幹・接尾辞からなります。また、接頭辞・接尾辞は原理的には複数個つくこともありますが、つかないかあるいは1個だけのことがほとんどです。また、語幹の頭文字によって接続できる接頭辞が異なることがあります。

さて、これらのルールを踏まえた上で、マレー語の単語用の連接表を作っていきましょう。

まず素性の種類を列挙します。Excel とかで適当にやります。

素性1 素性2
…前略…
prefix ber
prefix be
prefix di
prefix diper
prefix dipe
prefix kau
…中略…
headword *
headword r
headword b/f/v
headword d/c/j/z/sy
…後略…

語幹(ここでは headword と呼んでます)を接頭辞が接続できる頭文字ごとに分類しておくのがコツです。

あとは、これらの素性の種類同士すべての組み合わせに関して連接表を書けばいいだけです。「だけ」とはいうものの、結構なパターン数です。実際のファイルで43種類ありました。つまり、432で1849通りの組み合わせについて連接表を書く必要があります。あまり手作業ではやりたくないですね。

そこで、以下のようなルール表を用意して自動化します。

左素性1 左素性2 右素性1 右素性2 連接コスト
…前略…
prefix be headword r 50
prefix be headword 1000
prefix dipe headword r 50
prefix dipe headword 1000
prefix mem headword b/f/v 50
prefix mem headword 1000
…後略…

上のほうが優先順位が高く、空欄は任意の値であることを表しています。このルール表をすべての組み合わせに対して適用します。書き捨てのプログラムをガッと書いて適当にやりましょう。

word_class_list_file = open(File.expand_path('./word_class_list.txt', __FILE__))
rule_file = open(File.expand_path('./rule.txt', __FILE__))

matrix_file = open(File.expand_path('./matrix.def', __FILE__), 'w')
left_id_file = open(File.expand_path('./left-id.def', __FILE__), 'w')
right_id_file = open(File.expand_path('./mecab_dict/right-id.def', __FILE__), 'w')

WORD_CLASSES = word_class_list_file.each.with_index.map do |line, index|
  word_class = line.rstrip.split("\t")
  [index, word_class]
end

RULE = rule_file.each.map do |line|
  *pattern, cost = line.rstrip.split("\t", -1)

  left, right = pattern.map{|col| col.length == 0 ? nil : col }.each_slice(2).to_a

  [left.reject(&:nil?), right.reject(&:nil?), cost]
end

SORTED_RULE = RULE.sort_by {|(left, right, _)| [left.length,  left, right.length, right] }.reverse

matrix_file.puts("#{WORD_CLASSES.length} #{WORD_CLASSES.length}")
WORD_CLASSES.product(WORD_CLASSES).map do |(left_id, left_class), (right_id, right_class)|
  matched = SORTED_RULE.lazy.
    select {|(left, _, _)| left_class[0...left.length] == left }.
    find {|(_, right, _)| right_class[0...right.length] == right }

  _, _, cost = matched

  matrix_file.puts("#{left_id} #{right_id} #{cost}")
end

WORD_CLASSES.each do |(index, word_class)|
  line = "#{index} #{word_class.join(',')}"
  left_id_file.puts(line)
  right_id_file.puts(line)
end

上で用意した2つの表をそれぞれ word_class_list.txt, rule.txt という名の tab 区切りのテキストファイルとして保存して実行すると、matrix.def, left-id.def, right-id.def が吐き出されます。2千行弱のテキストファイルを人間の手で書くなんてことをする必要はありません。

あとは語幹(単語)リストのファイルを用意すれば辞書は完成です。お疲れ様でした。

まとめ

  • MeCab は辞書の変更だけで任意の言語に適用できるような柔軟な設計である
  • さらっと書いたけど、素性分類のコツが全て

*1:matrix.def というファイルで定義されている
参照: MeCab の辞書構造と汎用テキスト変換ツールとしての利用