エイエイレトリック

なぐりがき

scikit-learnにmecab日本語分かち書きを組み込む方法

pythonで (深層学習ではない) 機械学習をするとなるとまず使うのがscikit-learn、さらに扱うデータがテキストとなると大体の場合 CountVectorizerや TfIdfVectorizer といった特徴量抽出 feature_extraction.text で前処理を行うことが定番だと思います。

しかし、これらの Vectorizer は日本語のような文節がない言語向けにつくられているわけではありません。

デフォルトの設定 analyzer="word" だとスペース区切りの分割が実行されるため、日本語の場合はmecabなどで分割する前処理が必要になります。

やり方としては大きく3つ、それぞれ例示してみます。

だいたい mecab-python3 を使う方法で書いていますが、fugashi・Sudachi・PyKNPといったトークナイザー・形態素解析器に置き換えても実行できるはずです。

使ったパッケージ・依存関係は以下の通りです。

python = "^3.7"
mecab-python3 = "^1.0.3"
scikit-learn = "^0.24.1"

1. 分かち書きしてからVectorizerに渡す

英語と同じように単語ごとスペースを入れておけば問題ないので、前処理として実行する。一番単純な方法です。

具体的には、sklearnを実行する前に MeCab.Tagger.parse を実行します。

gist.github.com

※「も」「の」が feature_names にないのはCountVectorizerがデフォルトで1文字単語を除外しているからです。 token_pattern が default=r"(?u)\b\w\w+\b" なので (?u)\b\w+\b に修正すると1文字もvocabに採用されます。

もしくはmecabコマンドの実行結果を元ファイルとは別に保存しておくという選択肢もあるかもしれません。

今となってはコーパスは生文で配布されていることが大半ですが、形態素解析済みで配布されているコーパスも存在します。NLTK Japanese Corpora - NLTKで使える日本語コーパス で紹介されているコーパスがこれにあたります。

あらかじめ形態素解析の結果を別ファイルで保存しておけば、毎回分かち書きを実行しなくて済むためモデルだけに注力でき、再現性が担保されるメリットがあります。

ですが、どうやって分かち書きしたか(どのトークナイザーか、どの辞書か)の情報は一緒に保存しないことが多いのできちんと記録・共有しておくのを忘れないよう注意しなくてはいけません。

最悪の場合、現状分かち書きされているファイルでしかモデルが実行できないとか、そういった類の問題が起こってしまう危険性があります。

1行1文書のデータに対してshellスクリプト分かち書きを設定するとしたら以下のような感じになりそうです。

#!/bin/sh
while read row; do
  text=`echo ${row} | cut -d , -f 1`
  echo "$text" | mecab -Owakati  >> $2
done < $1

参考

2. Vectorizerのanalyzerに渡す

処理機構がバラバラにならないよう、なるべくsklearnと一緒に使いたいと思うかもしれません。

実はVectorizer系のsklearnクラスの引数 analyzer が使えます。ドキュメントには

If a callable is passed it is used to extract the sequence of features out of the raw, unprocessed input.

https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html

と書いてあり、callableな関数も渡すことができることがわかります。 _analyze の実装見る限り analyzer の代わりにtokenizer に渡しても問題ないと思われます。

github.com

サンプルコード

mecab-python3の分かち書き結果を返す関数を設定して analyzer に渡す。

gist.github.com

mecab_analyzer によってCountVectorizerの設定として組み込めました。analyzer="word" の設定を上書きしているので1文字単語も語彙に登録されます。

analyzerに渡す関数を変更すれば名詞だけにしたり単語を原形で渡したりできます。

ひとつ問題になるのがこのVectorizer vec と一緒にmecabの情報を保存できないという点。

mecab-python3はあくまで(C++で書かれた)mecabpython wrapperなので、トークナイザの情報は保存できないです。

そのため、pickleで保存したモデルを別ファイルで読み込むとエラーになってしまいます。

応急処置っぽくはなりますが、 mecab_analyzer 関数を読み込むファイルと同じファイルに設定しておけば読み込みは可能です。

import pickle

# mecab_analyzer がないとエラー
# AttributeError: Can't get attribute 'mecab_analyzer' on <module '__main__' from 'load_vectorizer.py'>
import MeCab

mecab = MeCab.Tagger("-Owakati")


def mecab_analyzer(text):
    return mecab.parse(text).split()


texts = [
    "すもももももももものうち",
    "スモモもモモもモモのうち",
    "李も桃も桃のうち",
]

with open('outputs/sample.pickle', 'rb') as f:
    vec_new = pickle.load(f)
result = vec_new.fit_transform(texts)
print(result.toarray())
print(vec_new.get_feature_names())

参考

3. 分かち書きをする自作クラスを作る

pipelineで分かち書きを含めたモデルを作りたい、pickleで保存したい、様々な設定の分かち書きで実験したい、そんな場合は自作クラスを作るのがよさそうです。

sklearnのBaseEstimator を継承して、他のsklearnクラスと同様 fittransformfit_transform を設定すれば自作クラスとして機能します。

前述の通り、pickle化できるようにはクラスに工夫が必要になります。

自分で設定するのが面倒な人はサンプルコードを作ったので以下のコードを改変して使ってください。

pickle化するには

mecabのpickle化について、解説している記事がいくつかあるのでこれらを参考にします。

要するに、自作クラスのcallable object から Tagger オブジェクトをpickle化する時に外して、非pickle化の時に渡しなおせばよいということです。

具体的には__reduce_ex__ の設定をいじります。 __reduce__ に書いてある通りのタプルを返せばよいのですが、2番目の

呼出し可能オブジェクトに対する引数のタプル。呼出し可能オブジェクトが引数を受け取らない場合、空のタプルが与えられなければなりません。

で CountVectorizerにおけるanalyzer の情報 以外 を渡すようにします。

ただし、情報をはずしても__init__Taggerを呼び出せるような引数を設定しておく必要があります。 def __init(self, **kwargs) のような可変長引数で受け取る設定にしているとうまくいかない場合があります。

また、pickleを読み込む時に自作クラスがimportされていないと analyzer の方法と同様やはりエラーになるので注意です。

pip installできるモジュールにしておけばimportは必要ないと思うのですが、詳細は未確認です。。

サンプルコード

gist.github.com

これで mecab-python3 によってトークナイズしていることに加え辞書のパス情報もpickleで保存することができています。

辞書のパスが保存できるということは、OSが異なったり辞書の保存先をカスタマイズしていたりするとpickleを読み込む時にエラーになります。 あくまで同じ端末で使うために使う目的の自作クラスです。

まとめ

  1. 別々で実行 simple_mecab_sklearn.py · GitHub
  2. analyzerに渡す mecabをCountVectorizerのanalyzerに渡す · GitHub
  3. 自作クラスをつくる owakati_tokenizer.py · GitHub

個人的には2か3にしたいのですが、 「CountVectorizer はanalyzer設定含んでるクラスなのに、別クラスで分かち書きを設定するのは気持ち悪いな〜」とか、「analyzerに関数渡すの場合思いがけない設定に影響出るからな〜」とか考え方は色々あると思うので、最後は好みの問題になると思ってます。

一長一短でベストプラクティスがなかなか見つからないので、他に方法があれば教えてください。絶賛募集中です。