MeCabのN-Best解のコストがどこで間違っているのか確認した

前回の記事で、MeCabのN-Best解について紹介し、そのコスト計算にバグがあることについて触れました。
参考: MeCabのN-Best解を出力する
2番目以降の解の(生起と連接)コストの総和が1番目の解と同じになってしまうのでしたね。

このバグについて、具体的にどこでどう計算がずれてしまっているのか確認したのでそれを記事にまとめておきます。これを調べる前までは、生起コストと連接コストは正しく取得できて、合計結果だけ上書きされてるんじゃないかなぁと予想していたのですが、実際は連接コストの値が書き換えられていました。ここが正しくて合計が合わないだけなら自分で足し合わせるだけでよかったのでちょっと残念な結果でした。

それでは検証した内容を順番に書いていきます。
まず、いつもの”すもももももももものうち”で、N-Best解を出力し検証用のサンプルを取得します。特に深い理由はないのですが、 1番目の解(=正しくコストが計算される)と、4番目の解(=コスト計算が誤っている)を見ていきます。

まずは単純に形態素解析していきます。また、コスト計算の結果を検証するために、コストに関する各種情報を出力する設定にして、4番目までのN-Best解を出力します。

import MeCab
import pandas as pd


text = "すもももももももものうち"

# 表層系(%m), 形態素種類(%s), 左文脈id(%phl), 右文脈id(%phr), 単語生起コスト(%c),
# 連接コスト(%pC), 生起コスト+連接コスト(%pn), 生起コスト+連接コストの累積(%pc), 素性(%H)
# を順番に出力する設定でTaggerを生成
tagger = MeCab.Tagger(
    " -F %m\\t%s\\t%phl\\t%phr\\t%c\\t%pC\\t%pn\\t%pc\\t%H\\n" +
    " -E EOS\\t%s\\t%phl\\t%phr\\t%c\\t%pC\\t%pn\\t%pc\\tEOS\\n"
)
parse_result = tagger.parseNBest(4, text)
print(parse_result)
"""
すもも	0	1285	1285	7546	-283	7263	7263	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	0	262	262	4669	-4158	511	7774	助詞,係助詞,*,*,*,*,も,モ,モ
もも	0	1285	1285	7219	17	7236	15010	名詞,一般,*,*,*,*,もも,モモ,モモ
も	0	262	262	4669	-4158	511	15521	助詞,係助詞,*,*,*,*,も,モ,モ
もも	0	1285	1285	7219	17	7236	22757	名詞,一般,*,*,*,*,もも,モモ,モモ
の	0	368	368	4816	-4442	374	23131	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	0	1313	1313	5796	-5198	598	23729	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS	3	0	0	0	-2484	-2484	21245	EOS
すもも	0	1285	1285	7546	-283	7263	7263	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	0	262	262	4669	-4158	511	7774	助詞,係助詞,*,*,*,*,も,モ,モ
もも	0	1285	1285	7219	17	7236	15010	名詞,一般,*,*,*,*,もも,モモ,モモ
もも	0	1285	1285	7219	62	7281	22291	名詞,一般,*,*,*,*,もも,モモ,モモ
も	0	262	262	4669	-4158	511	22802	助詞,係助詞,*,*,*,*,も,モ,モ
の	0	368	368	4816	-4487	329	23131	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	0	1313	1313	5796	-5198	598	23729	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS	3	0	0	0	-2484	-2484	21245	EOS
すもも	0	1285	1285	7546	-283	7263	7263	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
もも	0	1285	1285	7219	62	7281	14544	名詞,一般,*,*,*,*,もも,モモ,モモ
も	0	262	262	4669	-4158	511	15055	助詞,係助詞,*,*,*,*,も,モ,モ
もも	0	1285	1285	7219	17	7236	22291	名詞,一般,*,*,*,*,もも,モモ,モモ
も	0	262	262	4669	-4158	511	22802	助詞,係助詞,*,*,*,*,も,モ,モ
の	0	368	368	4816	-4487	329	23131	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	0	1313	1313	5796	-5198	598	23729	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS	3	0	0	0	-2484	-2484	21245	EOS
すもも	0	1285	1285	7546	-283	7263	7263	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
もも	0	1285	1285	7219	62	7281	14544	名詞,一般,*,*,*,*,もも,モモ,モモ
も	0	262	262	4669	-4158	511	15055	助詞,係助詞,*,*,*,*,も,モ,モ
も	0	262	262	4669	-4203	466	15521	助詞,係助詞,*,*,*,*,も,モ,モ
もも	0	1285	1285	7219	17	7236	22757	名詞,一般,*,*,*,*,もも,モモ,モモ
の	0	368	368	4816	-4442	374	23131	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	0	1313	1313	5796	-5198	598	23729	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS	3	0	0	0	-2484	-2484	21245	EOS
"""

コストの総和が4パターンとも全部 21245 になってしまっていますね。
この計算がどこで間違っているのか見ていきましょう。
このままだと非常に扱いにくいので、PandasのDataFrameにします。

# 改行、Tabで順に区切ってDataFrameにする
df = pd.DataFrame(
    data=[r.split() for r in parse_result.splitlines()],
    columns=[
        "表層系", "形態素種類", "左文脈id", "右文脈id", "単語生起コスト",
        "連接コスト", "生起コスト+連接コスト", "生起コスト+連接コストの累積", "素性",
    ]
)
# 数値で取得できた値はint型にしておく
for c in ["形態素種類", "左文脈id", "右文脈id", "単語生起コスト",
          "連接コスト", "生起コスト+連接コスト", "生起コスト+連接コストの累積", ]:
    df[c] = df[c].astype(int)

# 1番目の解と4番目の解をそれぞれ個別のデータフレームに取り出す
nb1_df = df.iloc[0: 8].copy()
nb4_df = df.iloc[24: 32].copy()
nb4_df.reset_index(inplace=True, drop=True)

これで、nb4_df に今回注目する4番目のN-Best解の情報が入りました。nb1_dfは検証用です。

最初に予想していたのは、”生起コスト+連接コストの累積”だけ書き換えられていて、”生起コスト+連接コスト”の総和は本当は正しい値になっているのではないか?ということです。ただ、この予想は早速外れました。

以下の通り、生起コスト+連接コスト列を足すと21245になってしまいます。
そのためこの累積計算には誤りはなく、正しく足し算されているようです。

print(nb4_df["生起コスト+連接コスト"].sum())
# 21245

次に疑ったのが、”生起コスト+連接コスト” が “生起コスト”と”連接コスト”の和になってないのではないか?ということです。しかしこの点についても問題ありませんでした。
次のコードの通り値を計算してみても、何も矛盾なく全部0になります。

print(nb4_df["生起コスト+連接コスト"]-nb4_df["単語生起コスト"]-nb4_df["連接コスト"])
"""
0    0
1    0
2    0
3    0
4    0
5    0
6    0
7    0
dtype: int64
"""

となると、単語生起コストがそもそも違う?という懸念が出てくるのですが、これも1つ目の解と4つ目の解を見比べてみると一致しており問題ないことがわかります。

print(nb1_df[["表層系", "単語生起コスト", "素性"]])
"""
   表層系  単語生起コスト                          素性
0  すもも     7546   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
1    も     4669        助詞,係助詞,*,*,*,*,も,モ,モ
2   もも     7219      名詞,一般,*,*,*,*,もも,モモ,モモ
3    も     4669        助詞,係助詞,*,*,*,*,も,モ,モ
4   もも     7219      名詞,一般,*,*,*,*,もも,モモ,モモ
5    の     4816        助詞,連体化,*,*,*,*,の,ノ,ノ
6   うち     5796  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
7  EOS        0                         EOS
"""

print(nb4_df[["表層系", "単語生起コスト", "素性"]])
"""
   表層系  単語生起コスト                          素性
0  すもも     7546   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
1   もも     7219      名詞,一般,*,*,*,*,もも,モモ,モモ
2    も     4669        助詞,係助詞,*,*,*,*,も,モ,モ
3    も     4669        助詞,係助詞,*,*,*,*,も,モ,モ
4   もも     7219      名詞,一般,*,*,*,*,もも,モモ,モモ
5    の     4816        助詞,連体化,*,*,*,*,の,ノ,ノ
6   うち     5796  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
7  EOS        0                         EOS
"""

順番は違いますが、名詞一般のすももは7546で、助詞系助詞のもは4669みたいにみていくと何も問題ないことがわかりますね。

ということで最後に残ったのが連接コストです。正直、これの正否を調べるのは面倒だったので後回しにしていたのですがここしか疑うところがなくなってしまいました。
IPA辞書データに含まれている matrix.def から正しいデータを拾ってきて比較検証するしかありません。

比較を効率にするために、次のようなコードを書いてみました。

dic_data_dir = "{コンパイル前の辞書データが含まれているパスを指定}"
# スペース区切りで読み込み
matrix_df = pd.read_csv(f"{dic_data_dir}/matrix.def", delimiter=" ")
# 1列目がindex扱いされてしまっているので再設定
matrix_df.reset_index(inplace=True,)
# 列名修正
matrix_df.columns = ["前の単語の右文脈id", "左文脈id", "辞書から取得した連接コスト"]

# 前の単語の右文脈idの列を作成
nb4_df["前の単語の右文脈id"] = nb4_df["右文脈id"].shift(1)
# 1つ目の単語の前はBOSで、その文脈idは0なのでそれで埋める
nb4_df.loc[0, "前の単語の右文脈id"] = 0

# 結合する
print(pd.merge(
    nb4_df,
    matrix_df,
    left_on=["前の単語の右文脈id", "左文脈id"],
    right_on=["前の単語の右文脈id", "左文脈id"],
)[["表層系", "左文脈id", "右文脈id", "連接コスト", "辞書から取得した連接コスト"]])
"""
   表層系  左文脈id  右文脈id  連接コスト  辞書から取得した連接コスト
0  すもも   1285   1285   -283           -283
1   もも   1285   1285     62             62
2    も    262    262  -4158          -4158
3    も    262    262  -4203            478
4   もも   1285   1285     17             17
5    の    368    368  -4442          -4442
6   うち   1313   1313  -5198          -5198
7  EOS      0      0  -2484          -2484
"""

これでN-Best解で出力された連接コストと、辞書から取得した連接コストが出力されした。

上から順番にみていくと、4行目(index3の行)で、出力された連接コストが-4203なのに、辞書から取得した連接コストが478になっているところがあります。

これのせいで結果(総和)が本来のコストよりずっと小さくなり、1番目の解と同じになってしまっていたのですね。

さて、今回のサンプルでは連接コストが1カ所誤った値になり総和がズレるということがわかりました。実はより長文で実験したりすると、このように値がズレるところが2カ所以上出てくることもわかっています。ズレる部分が何カ所発生するのか、また何単語目がズレるのかといったことに関する法則性はまだわかっていません。

もしかしたら生起コストの方がずれることもあるのかな?とは思ったのですが今のところそういう例は見つかっていないので一旦は大丈夫そうです。

とはいえ、合計する元データになる連接コストの値自体がずれてしまっているとなると自分で足し合わせて正しい値を得るということは少し面倒ですね。

上のコードみたいに、自分で辞書から連接コストを持ってきてそれを使って足し合わせる必要があります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です