MeCab の形態素解析誤りを修正する生起コストの求め方

「かつおたたき」、「りんごジュース」、「ロース肉薄切り」を MeCab + IPA 辞書で形態素解析すると以下のようになります。

% mecab
かつおたたき
かつ    接続詞,*,*,*,*,*,かつ,カツ,カツ
お      接頭詞,名詞接続,*,*,*,*,お,オ,オ
たたき  名詞,一般,*,*,*,*,たたき,タタキ,タタキ
EOS
りんごジュース
りん    副詞,助詞類接続,*,*,*,*,りん,リン,リン
ご      接頭詞,名詞接続,*,*,*,*,ご,ゴ,ゴ
ジュース        名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
ロース肉薄切り
ロース  名詞,一般,*,*,*,*,ロース,ロース,ロース
肉薄    名詞,サ変接続,*,*,*,*,肉薄,ニクハク,ニクハク
切り    名詞,接尾,一般,*,*,*,切り,ギリ,ギリ
EOS

日本人の感覚とはだいぶずれた結果になってしまっていますね!人間であれば連続する単語 (単語 bigram) の出現頻度に関する感覚や、各単語の意味に関する感覚を持っているので正しく解析できますが、MeCab は単語の出現しやすさ(生起コスト)と品詞のつながりやすさ(連接コスト)のみから最適解を求めているので、このような結果になります。

MeCab の動作原理に関しては以前別のブログで書いたので、興味のある方はこちらを参照してください。
日本語形態素解析の裏側を覗く!MeCab はどのように形態素解析しているか - クックパッド開発者ブログ

この解析結果を所望のものにするには、主に 2 つの選択肢があります。

  1. MeCab をドメイン適応させる1
  2. 既存の単語よりも低い生起コストの単語をユーザ辞書に追加する

ドメイン適応させようと思うと、ある程度の規模のアノテーション付きデータが必要なので、ピンポイントで修正したい問題に対処するにしては大仰です。
そうすると、アドホックな対応としてはユーザ辞書に単語を追加することになるわけですが、副作用を少なくするためにも生起コストは不必要に低くしたくないものです。

というわけで、だいぶ前にこんなツイートをしたわけですが、

これらの生起コストの求め方について説明します。

コストの確認方法

mecab コマンドでは -F オプションを指定することで出力フォーマットを変更することができます。2
次の例では標準のフォーマットの末尾に単語生起コスト (%pw)、連接コスト (%pC)、文頭からの累積コスト (%pc) を追加しています。

% mecab -F '%m\t%H\t%pw,%pC,%pc\n' -E 'EOS\t%pw,%pC,%pc\n'
かつおたたき
かつ    接続詞,*,*,*,*,*,かつ,カツ,カツ 4281,-2789,1492
お      接頭詞,名詞接続,*,*,*,*,お,オ,オ        6374,-484,7382
たたき  名詞,一般,*,*,*,*,たたき,タタキ,タタキ  8441,-3097,12726
EOS     0,-573,12153
りんごジュース
りん    副詞,助詞類接続,*,*,*,*,りん,リン,リン  4705,-1137,3568
ご      接頭詞,名詞接続,*,*,*,*,ご,ゴ,ゴ        6655,-451,9772
ジュース        名詞,一般,*,*,*,*,ジュース,ジュース,ジュース    3637,-3097,10312
EOS     0,-573,9739
ロース肉薄切り
ロース  名詞,一般,*,*,*,*,ロース,ロース,ロース  3692,-283,3409
肉薄    名詞,サ変接続,*,*,*,*,肉薄,ニクハク,ニクハク    4456,-557,7308
切り    名詞,接尾,一般,*,*,*,切り,ギリ,ギリ     8907,-5447,10768
EOS     0,-156,10612

N-Best 解の累積コストはバグっているので、所望の解析結果のコストは制約付き解析によって求める必要があります。
「かつおたたき」の場合は次のように求めます。

% mecab -p -F '%m\t%H\t%pw,%pC,%pc\n' -E 'EOS\t%pw,%pC,%pc\n'
かつお  *
たたき  *
EOS
かつお  名詞,一般,*,*,*,*,かつお,カツオ,カツオ  7269,-283,6986
たたき  名詞,接尾,一般,*,*,*,たたき,タタキ,タタキ       11246,-5090,13142
EOS     0,-156,12986

「たたき」は名詞一般な気がしますが、ひとまず分割単位が正しくなれば良しとしましょう。
最適解の累積コストは 12153、所望の解析結果の累積コストは 12986 なので、12986 - 12153 + 1 = 834 だけ「かつお」の生起コストを下げれば良いことになります。

適切なコストを求める Ruby スクリプト

手作業で適切な生起コストを求めるのはちょっと大変ですよね。というわけで、適切な生起コストのユーザ辞書を作成するための CSV を吐き出す簡易スクリプトを書いてみました。
用途に合わせていじってもらえればと思います。

#!/usr/bin/env ruby

require 'mecab'

def parse_to_nodes(sentence)
  node = MeCab::Tagger.new.parseToNode(sentence)
  build_nodes(node)
end

def convert_to_nodes(expected_result)
  expected = "#{expected_result.split(' ').map { |w| "#{w}\t*" }.join("\n")}\nEOS"
  node = MeCab::Tagger.new('-p').parseToNode(expected)
  build_nodes(node)
end

def build_nodes(node)
  nodes = []
  while node
    nodes << node
    node = node.next
  end
  nodes
end

$stdin.each do |line|
  line.chomp!

  # e.g. "かつおたたき	かつお たたき	かつお"
  sentence, expected_result, target_surface = line.split("\t")

  best_cost = parse_to_nodes(sentence).last.cost

  expected_nodes = convert_to_nodes(expected_result)
  target_node = expected_nodes.find { |n| n.surface == target_surface }
  if target_node.stat == MeCab::MECAB_UNK_NODE
    raise RuntimeError, "'#{target_surface}' is unknown"
  end

  new_wcost = target_node.wcost - (expected_nodes.last.cost - best_cost) - 1
  $stderr.puts "#{target_surface}: #{target_node.wcost} => #{new_wcost}"
  puts [
    target_node.surface,
    target_node.lcAttr,
    target_node.rcAttr,
    new_wcost,
    target_node.feature,
  ].join(',')
end

次のような、左から入力文、所望の分かち書き文、生起コストを求めたい単語の TSV ファイルを入力として渡すと、ユーザ辞書の CSV ファイルを出力します。

かつおたたき	かつお たたき	かつお
りんごジュース	りんご ジュース	りんご
ロース肉薄切り	ロース 肉 薄切り	薄切り

実行してみます。

% ruby /path/to/script < /path/to/tsv > user_dic.csv
かつお: 7269 => 6435
りんご: 7277 => 6895
薄切り: 4377 => 2412

生成された CSV ファイルの内容は次のようになっています。

% cat user_dic.csv
かつお,1285,1285,6435,名詞,一般,*,*,*,*,かつお,カツオ,カツオ
りんご,1285,1285,6895,名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
薄切り,1283,1283,2412,名詞,サ変接続,*,*,*,*,薄切り,ウスギリ,ウスギリ

この CSV ファイルからユーザ辞書を生成します。

$(mecab-config --libexecdir)/mecab-dict-index -d$(mecab-config --dicdir)/ipadic -u user.dic -f utf8 -t utf8 user_dic.csv

生成したユーザ辞書を使って解析してみます。

% mecab -u user.dic
かつおたたき
かつお  名詞,一般,*,*,*,*,かつお,カツオ,カツオ
たたき  名詞,接尾,一般,*,*,*,たたき,タタキ,タタキ
EOS
りんごジュース
りんご  名詞,一般,*,*,*,*,りんご,リンゴ,リンゴ
ジュース        名詞,一般,*,*,*,*,ジュース,ジュース,ジュース
EOS
ロース肉薄切り
ロース  名詞,一般,*,*,*,*,ロース,ロース,ロース
肉      名詞,一般,*,*,*,*,肉,ニク,ニク
薄切り  名詞,サ変接続,*,*,*,*,薄切り,ウスギリ,ウスギリ
EOS

良い感じですね!
上記の知見が、ちょっとでもサービスの辞書のメンテナンスのお役に立てば幸いです。

広告
Redshift の UDF では非 ASCII 文字を含む unicode 型を返すとエラーになる InnoDB の行レベルロックについて解説してみる