ちゃお... Python Advent Calendar 2015 18日目の記事です...
Pythonといったらデータサイエンスに強いし、データサイエンスといったら形態素解析が必要になることがあるし、形態素解析といったらMeCabだし――ということで、今回はPythonでのMeCabの処理を少しでも速くする豆知識を共有したいと思います!
parseToNodeを捨てよ parseを使おう
MeCabの解析結果を得るにはparse
とparseToNode
の2つのメソッドがあります。
わたしはもっぱらparseToNode
使ってたのですが、なんか遅いなーって思って、本当に遅いのか確かめるために処理時間測ってみました。現実的な設定でやった方が実用的だと思ったので、今回は夢野久作のドグラマグラから名詞を抽出することにします。
コード
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標準のmultiprocessing
やthreading
を使いやすくしたものです。たとえば、各プロセスにどれくらいの粒度でデータを分割して渡すかを自動で調整したり、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倍の差がつきました。小規模のテキストを扱い場合なら誤差の内かもしれませんが、ちょっとした規模の量を処理するときなんかに恩恵を受けると思います^^