自然言語処理の深遠

Deep Dive Into Natural Language Processing

形態素解析を並列化して高速化するTip

自然言語処理ではその第一歩として形態素解析が行われることが多いと思います。 しかし、形態素解析をする際に、解析対象が大量にあると実行時間が結構かかります。 本記事では、Pythonconcurrent.futures モジュールを使った高速化方法を紹介します。

ナイーブな実装

想定している状況は、あるフォルダの中に大量のテキストファイルが存在し、そのファイルを読み込んで形態素解析するというものです。 以下のプログラムは、globを使ってファイルのリストを取得し、それらを読み込んでMeCab形態素解析をしています:

import glob
import MeCab

t = MeCab.Tagger()


def tokenize_parse(text_file):
    with open(text_file) as f:
        text = f.read()
        chunks = t.parse(text).splitlines()
        for chunk in chunks:
            pass  # do something                                                                                                                                                                        


for text_file in glob.glob("data/text/*/*.txt"):
    tokenize_parse(text_file)
    print('{} was processed'.format(text_file))

プログラムの内容はよくあるデータ処理のパターンに従っています:

  1. 処理したいファイルのリストを列挙する
  2. データを処理するhelper関数を書く
  3. ループ内でhelper関数を呼ぶことでデータを1件ずつ処理していく

このプログラムを7000ほどのテキストファイルに対して実行し、実行時間を計測してみましょう:

$ time python tokenizer_1.py
data/text/dokujo-tsushin/dokujo-tsushin-4778030.txt was processed
[... about 7375 more lines of output ...]
real    0m22.641s
user    0m10.134s
sys 0m1.139s

実行結果からこのプログラムの実行に 22.6秒 かかっていることがわかります。 並列に処理することで実行時間をどれくらい削減できるでしょうか?

パラレルな実装

データを並列に処理するためのアプローチとしては以下の方法が考えられます:

  1. テキストファイルのリストを分割する
  2. 分割数分のPythonプロセスを実行する
  3. 各プロセスは1で分割したデータを処理する
  4. 各プロセスの処理結果を統合する

並列処理するためにPython組み込みのモジュールである concurrent.futures を使うことができます。まずはimportしましょう:

import concurrent.futures

モジュールをimportしたら複数のPythonを並列に走らせるための命令を書きます。そのために ProcessPool を使います:

with concurrent.futures.ProcessPoolExecutor() as executor:

あとは executor.map() を使って通常のループを置き換えてしまいます:

text_files = glob.glob("data/text/*/*.txt")
for text_file, _ in zip(text_files, executor.map(tokenize_parse, text_files)):
    print('{} was processed'.format(text_file))

executor.map()関数の引数にはhelper関数と処理したデータを渡します。 そうすることで、リストの分割や分割したリストを子プロセスへ渡す、子プロセスを実行する、実行結果を統合するといった面倒な処理をしてくれます。

以上を踏まえて、ナイーブな実装を並列化してみました:

import concurrent.futures
import glob
import MeCab

t = MeCab.Tagger()


def tokenize_parse(text_file):
    with open(text_file) as f:
        text = f.read()
        chunks = t.parse(text).splitlines()
        for chunk in chunks:
            pass  # do something                                                                                                                                                                        


with concurrent.futures.ProcessPoolExecutor() as executor:
    text_files = glob.glob("data/text/*/*.txt")
    for text_file, _ in zip(text_files, executor.map(tokenize_parse, text_files)):
        print('{} was processed'.format(text_file))

実行時間を計測してみましょう:

$ time python tokenizer_2.py
data/text/dokujo-tsushin/dokujo-tsushin-4778030.txt was processed
[... about 7375 more lines of output ...]
real    0m10.284s
user    0m19.136s
sys 0m2.209s

実行時間は 10.3秒 になりました。ナイーブな場合と比べて約2倍に高速化されています。

まとめ

Python組み込みの concurrent.futures モジュールを使うことで簡単に並列処理を実装することができました。 実行時間を削減したいという方は並列化してみてください。