MeCabで形態素解析してテキストを単語に分解するとき、分かち書きしたテキストと、品詞情報が得られます。その単語の出現頻度等を集計した後で、この単語はこの品詞、という情報を付与して絞り込み等をやりたくなったのでその方法をメモしておきます。
実は以前ワードクラウドを作った時に品詞別に色を塗るために似たようなコードを作っています。今回の記事はその改良版です。
参考: WordCloudの文字の色を明示的に指定する
この記事では次のようなコードを使いました。(参照した記事は先行するコードでMeCabのTaggerインスタンスを作ってる前提なのでその辺ちょっと補って書きます。)
import MeCab
tagger = MeCab.Tagger()
def get_pos(word):
parsed_lines = tagger.parse(word).split("\n")[:-2]
features = [l.split('\t')[1] for l in parsed_lines]
pos = [f.split(',')[0] for f in features]
pos1 = [f.split(',')[1] for f in features]
# 名詞の場合は、 品詞細分類1まで返す
if pos[0] == "名詞":
return f"{pos[0]}-{pos1[0]}"
# 名詞以外の場合は 品詞のみ返す
else:
return pos[0]
参照した記事で補足説明書いてますとおり、このコードは単語をもう一回MeCabにかけて品詞を取得しています。その時に万が一単語がさらに複数の形態素に分割されてしまった場合、1つ目の形態素の品詞を返すようになっています。
このコードを書いた時、単語がさらに分解されるってことは理論上はありうるけど、滅多にないだろう、と楽観的に考えていました。ところが、色々検証していると実はそんな例が山ほどあることがわかってきました。
例えば、「中国語」という単語がありますが、これ単体でMeCabに食わせると「中国」と「語」に分かれます。以下が実行例です。
# 形態素解析結果に「中国語」が出る例
$ echo "彼は中国語を話す" | mecab
彼 名詞,代名詞,一般,*,*,*,彼,カレ,カレ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
中国語 名詞,一般,*,*,*,*,中国語,チュウゴクゴ,チューゴクゴ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
話す 動詞,自立,*,*,五段・サ行,基本形,話す,ハナス,ハナス
EOS
# 「中国語」がさらに「中国」 と「語」に分かれる
$ echo "中国語" | mecab
中国 名詞,固有名詞,地域,国,*,*,中国,チュウゴク,チューゴク
語 名詞,接尾,一般,*,*,*,語,ゴ,ゴ
EOS
「中国語」が固有名詞、地域、国と判定されるとちょっと厄介ですね。
他にも、「サバサバ」は「サバ」「サバ」に割れます。
$ echo "ワタシってサバサバしてるから" | mecab
ワタシ 名詞,固有名詞,組織,*,*,*,*
って 助詞,格助詞,連語,*,*,*,って,ッテ,ッテ
サバサバ 名詞,サ変接続,*,*,*,*,サバサバ,サバサバ,サバサバ
し 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
てる 動詞,非自立,*,*,一段,基本形,てる,テル,テル
から 助詞,接続助詞,*,*,*,*,から,カラ,カラ
EOS
$ echo "サバサバ" | mecab
サバ 名詞,一般,*,*,*,*,サバ,サバ,サバ
サバ 名詞,一般,*,*,*,*,サバ,サバ,サバ
EOS
他にも「ありえる」が「あり」「える」とか、「無責任」が「無」「責任」とか「ビュッフェ」が「ビュッ」「フェ」など、かなりの種類の単語が再度分解されます。
ということで、冒頭にあげた get_pos メソッドは思っていたよりもずっと誤判定しやすいということがわかってきました。
前置きが長くなってきましたが、このことを踏まえて、単語を再度分割することのないようにその単語としての品詞情報を取得できないかを考えました。
結局、制約付き解析機能を使って実現できそうだということがわかりました。
参考: MeCabの制約付き解析機能を試す
要するに、MeCabに渡された単語はそれで1単語だ、という制約を課せば良いわけです。
そのためには、-pオプション付きでTaggerを生成し、「{単語}{タブ}*(アスタリスク)」という形式のテキストに変換してTaggerでparseすれば大丈夫です。
Pythonのコードで書くと次のようになりますね。
import MeCab
tagger = MeCab.Tagger("-p")
def get_pos(word):
# 制約付き解析の形態素断片形式にする
p_token = f"{word}\t*"
# 出力のEOS部分を捨てる
parsed_line = tagger.parse(p_token).splitlines()[0]
feature = parsed_line.split("\t")[1]
# ,(カンマ)で区切り、品詞,品詞細分類1,品詞細分類2,品詞細分類3 の4項目残す
pos_list = feature.split(",")[:4]
# もう一度 ,(カンマ) で結合して返す
return ",".join(pos_list)
# 利用例
print(get_pos("中国語"))
# 名詞,一般,*,*
上のコードは、品詞を再分類3まで取得するようにしましたが、最初の品詞だけ取得するとか、*(アスタリスク)の部分は省略するといった改修はお好みに合わせて容易にできると思います。
これで一旦今回の記事の目的は果たされました。
ただ、元の文中でその単語が登場したときの品詞が取得されているか、という観点で見るとこのコードも完璧ではありません。
表層系や原型が等しいが品詞が異なる単語が複数存在する場合、通常のMeCabの最小コスト法に則って品詞の一つが選ばれることになります。BOS/EOSへの連接コストとその品詞の単語の生起コストが考慮されて最小になるものが選ばれる感じですね。
分かち書き前のテキストで使われていたときの品詞が欲しいんだ、となると後からそれを付与するのは困難というより不可能なので、分かち書きした時点でちゃんと保存してどこかに取っておくようにしましょう。
あとおまけで、このコードを書いてる時に気づいたMeCabの制約付き解析機能の注意点を書いておきます。MeCabを制約付き解析モードで使っている時に、「表層\t素性パターン」”ではない”テキスト、つまり文断片と呼ばれている文字列を改行コード付けずに渡すとクラッシュするようです。
-p 付きで起動したときは、「表層\t素性パターン」形式の形態素断片か改行コードを必ず含むテキストで使うようにしましょう。
jupyter notebookでやると カーネルごとお亡くなりになりますので特に要注意です。
ちょっとコンソールでやってみますね。
$ python
>>> import MeCab
>>> tagger = MeCab.Tagger("-p")
>>> tagger.parse("中国語")
Segmentation fault: 11
# これでPythonが強制終了になる
$
改行コードつければ大丈夫であることは以下のようにして確認できます。
$ python
>>> import MeCab
>>> tagger = MeCab.Tagger("-p")
>>> tagger.parse("中国語\n")
'中国\t名詞,固有名詞,地域,国,*,*,中国,チュウゴク,チューゴク\n語\t名詞,接尾,一般,*,*,*,語,ゴ,ゴ\nEOS\n'
-p をつけてないときは別に改行コードなしのテキストも読み込んでくれるのでこれはちょっと意外でした。
制約付き解析(-p付き)でMeCabを使っている時に、「Segmentation fault: 11」が出たらこのことを思い出してください。