PythonでのMeCabを速くするtips

More than 3 years have passed since last update.

ちゃお... Python Advent Calendar 2015 18日目の記事です...

Pythonといったらデータサイエンスに強いし、データサイエンスといったら形態素解析が必要になることがあるし、形態素解析といったらMeCabだし――ということで、今回はPythonでのMeCabの処理を少しでも速くする豆知識を共有したいと思います!


parseToNodeを捨てよ parseを使おう

MeCabの解析結果を得るにはparseparseToNodeの2つのメソッドがあります。

わたしはもっぱらparseToNode使ってたのですが、なんか遅いなーって思って、本当に遅いのか確かめるために処理時間測ってみました。現実的な設定でやった方が実用的だと思ったので、今回は夢野久作のドグラマグラから名詞を抽出することにします。


コード

https://gist.github.com/ikegami-yukino/68a741ef854de68871cc#file-parse_vs_parsetonode-ipynb

import MeCab

tagger = MeCab.Tagger('-d /usr/local/lib/mecab/dic/ipadic')

def preprocessing(sentence):
return sentence.rstrip()

def extract_noun_by_parse(path):
with open(path) as fd:
nouns = []
for sentence in map(preprocessing, fd):
for chunk in tagger.parse(sentence).splitlines()[:-1]:
(surface, feature) = chunk.split('\t')
if feature.startswith('名詞'):
nouns.append(surface)
return nouns

def extract_noun_by_parsetonode(path):
with open(path) as fd:
nouns = []
for sentence in map(preprocessing, fd):
node = tagger.parseToNode(sentence)
while node:
if node.feature.startswith('名詞'):
nouns.append(node.surface)
node = node.next
return nouns


結果


  • Py2: Python 2.7.10

  • Py3: Python 3.5.0

  • ASIS: MeCabリポジトリそのまま

  • NEW SWIG: SWIG 3.0.7でMeCabリポジトリのラッパーをつくりなおした場合

  • mecab-python3: PyPIにある mecab-python3

parse
parseToNode

Py2, ASIS
531 ms
642 ms

Py2, NEW SWIG
604 ms
630 ms

Py2, mecab-python3
547 ms
652 ms

Py3, ASIS
673 ms
1610 ms

Py3, NEW SWIG
684 ms
1640 ms

Py3, mecab-python3
654 ms
1610 ms

Python 3のparseToNodeだけ明らかに遅い......!?


原因

詳細なプロファイルを取ってみると、

         1362455 function calls in 2.177 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)
1 1.162 1.162 2.174 2.174 <ipython-input-5-e12642d808e1>:1(extract_noun_by_parsetonode)
313688 0.372 0.000 0.372 0.000 MeCab.py:35(_swig_setattr_nondynamic)
3416 0.251 0.000 0.262 0.000 {built-in method _MeCab.Tagger_parseToNode}
313688 0.131 0.000 0.629 0.000 MeCab.py:126(<lambda>)
313688 0.126 0.000 0.498 0.000 MeCab.py:48(_swig_setattr)
以下省略...

parseToNodeは形態素ごとにNodeインスタンスの生成時にNodeの全ての要素をあらかじめ取得するので、そのときのオーバーヘッドがだいぶ大きいようです。


結論

Python 3でMeCab使うときはparseToNodeじゃなくてparseを使いましょう (バッドノウハウだけど......)


joblibで並列化

さらに速くしたいとなったら並列化が頭をよぎりますよね。でも並列化っていうととっつきにくいイメージがあります。途中で失敗したときにうまくプロセスが死んでくれなかったり、途中でやめたいって思ってもKeyBoardInterruptが効かなかったり、データの分割数どれくらいがわからなかったり、今どれくらい処理してるのか経過わかんなかったり。。。

そこで、scikit-learnも採用しているjoblibというものを使います。joblibはいろいろできるんですけど、ここでは並列処理のjoblib.Parralelに着目します。これはざっくりいうとPython標準のmultiprocessingthreadingを使いやすくしたものです。たとえば、各プロセスにどれくらいの粒度でデータを分割して渡すかを自動で調整したり、KeyBoardInterruptでちゃんと終わってくれたり、途中経過を標準出力とかに流したりできます。かゆいところに手が届く!これなら並列化こわくない!💪😤


比較

並列化するとどれくらい速くなるか比較するために、またドグラマグラから名詞抽出しました。ボリューム大きめのテキストでやった方がわかりやすいので今回は長さを10倍してます。

コード: https://gist.github.com/ikegami-yukino/68a741ef854de68871cc#file-asis_vs_joblib-ipynb


def extract_noun(sentence):
nouns = []
sentence = preprocessing(sentence)
for chunk in tagger.parse(sentence).splitlines()[:-1]:
(surface, feature) = chunk.split('\t')
if feature.startswith('名詞'):
nouns.append(surface)
return nouns

# 並列なし
%timeit nouns = [extract_noun(sentence) for sentence in doc.splitlines()]

# 並列あり
from joblib import Parallel, delayed

%timeit nouns = Parallel(n_jobs=-1, pre_dispatch='all')(delayed(extract_noun)(sentence) for sentence in doc.splitlines())


結果

並列なし

1 loops, best of 3: 6.78 s per loop

並列あり
1 loops, best of 3: 3.72 s per loop

1.8倍くらい速くなりました!

ついでに並列化なしのparseToNodeで同じデータを処理させると



1 loops, best of 3: 1min 18s per loop

並列ありのparseと比べて20倍遅いです😱


総括

Python3でMeCabを使うときはparseToNodeを使うとオーバーヘッドが大きいのでparseを使った方が速く処理できます。さらに並列化するともっと処理時間が短くなります。ワーストケース (Python 3で並列なしでparseToNodeで名詞抽出する場合) 78秒かかる処理が、今回紹介したやり方では3.72秒となり、およそ20倍の差がつきました。小規模のテキストを扱い場合なら誤差の内かもしれませんが、ちょっとした規模の量を処理するときなんかに恩恵を受けると思います^^