マルチコアで形態素解析を行う1(プロファイリング編)
Pythonにはプロファイラというものが標準で付いていることを知りました。プロファイラを使用すると関数ごとの呼び出し回数や処理時間が計測できるので、どの部分が処理時間の足を引っ張ってるのかが簡単に分かります。無駄なループなどをコードとにらめっこして見つけるのは必須の技能でしょうけど、プロファイラを使用することで見つけられなかったボトルネックなども見つけることができるようです。
というわけで前回のプログラムを使用して試してみました。このプログラムではキーボード入力が必要な部分があったので、この部分だけをちょっとだけ変更しました(1〜10番目の記事と類似なものをそれぞれ見つけるように変更)。
プロファイラの使用方法は実行コマンドを
python -m cProfile プログラム名
にするだけ。さらにどの部分がボトルネックになってるのか見つけやすいように時間でソートします
python -m cProfile -s time プログラム名
プロファイラの結果がこちら
769225 function calls (763651 primitive calls) in 5.456 seconds
Ordered by: internal time
ncalls | tottime | percall | cumtime | percall | filename:lineno(function) |
---|---|---|---|---|---|
4 | 1.902 | 0.476 | 2.091 | 0.523 | feed.py:12(analysis_entrie) |
4 | 0.844 | 0.211 | 0.844 | 0.211 | {_socket.getaddrinfo} |
1121 | 0.245 | 0.000 | 0.799 | 0.001 | feedparser.py:788(pop) |
1470 | 0.236 | 0.000 | 0.303 | 0.000 | feed.py:42(cos_similar) |
1 | 0.164 | 0.164 | 0.305 | 0.305 | feedparser.py:11( |
40 | 0.152 | 0.004 | 0.152 | 0.004 | {method 'recv' of '_socket.socket' objects} |
147 | 0.140 | 0.001 | 0.140 | 0.001 | {_MeCab.Tagger_parseToNode} |
4 | 0.119 | 0.030 | 0.119 | 0.030 | {method 'connect' of '_socket.socket' objects} |
1 | 0.100 | 0.100 | 5.458 | 5.458 | feed.py:6( |
14918 | 0.091 | 0.000 | 0.152 | 0.000 | feedparser.py:347(__getattr__) |
基本的に外部ライブラリは手出しができないので、自分で作った関数の高速化を考えることになります。
この中で一番時間がかかっているのはanalysis_entrieで、RSSのエントリーごとに形態素解析をして単語を切り出したりしている処理をしています。次に時間がかかってる自作関数はcos_similarですが、analysis_entrieに比べるとおよそ1/8程の時間しかかかっていません(今回10件分しか計算していないというのもありますが)。よってanalysis_entrieの高速化を考えたほうがプログラム全体の処理時間を短くできそうです。
しかし、この関数には冗長なループはないのでこれ以上の効率化は難しいです。最終手段としてはPythonではなくてC, C++で関数を書き直すことですが、形態素解析を行うMeCabモジュールはSWIGというもので作られていて、既にC++で書かれたものを呼び出しているようです。それにせっかくPythonを使ってるのにC, C++でプログラムを書きたくないです。
ここでアプローチの方法を変えましょう。一本の流れのアルゴリズムをこれ以上効率化できないのであれば、流れの本数を増やしてやることを考えます。すなわち並列化、マルチタスクです。並列化に最も適している処理はベクトル演算で、理由は一つ一つの演算が独立しているので前の演算が終わるまで待つ必要がなく、並列化しやすいからです。同じことが多くの場合の形態素解析にも当てはまると思います。
例として形態素解析を使用して文書Aと文書Bの中の単語が登場した回数をそれぞれの文書ごとに数えるという処理を考えます。このとき文書Aと文書Bにおける形態素解析も単語の回数を数える処理もお互いの結果に全く関係が無いです。つまり、文書Aの処理が終わるまで待ってから文書Bの処理を行う必要はなく、同時に行っても全く問題がないわけです。よってこの部分の処理は並列化が可能です。
さて、最近のマルチコアCPUを効率良く使用する方法としてよく耳にするのは複数のスレッドで並列に処理するマルチスレッドですが、PythonにはGILという仕組みがあってマルチスレッドでプログラムを書いてもCPUをフルに使いきることはできないそうです。そこでPython2.6からはマルチスレッドだけでなくマルチプロセスをサポートするライブラリが登場しました。これを使えばマルチコアCPUでもそのパワーをフルに使い切ることができます。