どこまで速くできる? 達人に学ぶPython超高速データ分析~PyData.Tokyo Meetup #4イベントレポート

 登壇者のレベルの高い講演に加え、ヤフー、日本IBM、NTTデータ、AWS、Gunosy、Preferred Networksなど、第一線で活躍されている非常に質の高い聴講者にも参加いただき、大変充実した会になりました。

 Pythonは書きやすい言語仕様と豊富なライブラリが特徴で、手軽に複雑なデータ分析を行えますが、他言語と同じ感覚でプログラムを組むと、実行時のパフォーマンスでは他言語に軍配が上がることもあり、「Pythonは遅い」という印象を持っている方も多いようです。

 あまり広く知られていませんが、実行速度の問題を解決するにあたりPythonには非常に沢山の選択肢があります。言語仕様レベルでは、「内包表記」と呼ばれる手法を使うことでパフォーマンスを上げることができますし、Just In Time(JIT)コンパイラーを使うことで、コードを変更することなく高速化することもできます。また、多くの科学計算やデータ解析処理は、NumPyというライブラリを使うことで非常に簡潔に記述することができ、また処理速度も非常に高速です。

 今回の勉強会では、Pythonを使って高速に実行されるプログラムを作るためにはどうしたら良いのか、登壇者と参加者による非常に網羅的な検証が行われました。最終的にはNumPyライブラリの中の関数がどのように実装されているのかを解き明かすという非常にマニアックな終着点となりましたが、参加者の満足度が非常に高く、多くの方から「こんな内容を待っていた」との声が聞かれました。また通例のLT大会でも、オープンデータ、ディープラーニング、参加者の書籍出版などの幅広い内容で、懇親会は遅くまで大盛況でした。

 PyData.Tokyo オーガナイザーの田中(@atelierhide)です。

 データ分析、機械学習の分野でPythonの人気が高まっています。特に、強力な数値計算ライブラリであるNumPyとSciPy、インタラクティブな環境を提供するIPythonは、大企業やスタートアップのデータサイエンティスト、大学の研究者の間で広く使われています。その一方、Pythonはコンパイラ言語と比較すると処理速度が遅いことが課題として挙げられ、大量のデータを分析する際には、高速化が不可欠です。

 今回は1つ目の話題として、株式会社ブレインパッドの佐藤貴海さん(@tkm2261)に「High Performance Python Computing for Data Science ~データ分析でPythonを高速化したいときに見る何か~」というタイトルで講演をしていただきました。プログラミングの高速化手法について、基礎から応用まで幅広くカバーしていただき、Pythonだけにとどまらず、資料は高速化手法の教科書的なものとなっています。

 佐藤さんはさまざまな勉強会で大変興味深い発表をされています。佐藤さんの発表はTwitterやはてなブックマークでも毎回注目されており、人気のデータ分析エクスパートです。佐藤さんの資料一覧はTakami Sato's Presentations on SlideShareにアップロードされています。

10倍高速化すれば、10倍失敗できる

 この講演では、「データサイエンティストという立場で高速化が重要な理由」として佐藤さんが言及していた以下の言葉が最も印象に残りました。

 「機械学習の世界は、成功の数∝失敗の数、と思われるので、10倍高速化すれば、10倍失敗できる」

 顧客の要望に対して、的確なソリューションを迅速に提案することが要求されるビジネス現場で、特に重要な考え方だと言えます。その一方、コードの移植性や可読性を損なうような過度な高速化は避けるべきというお話もありました。データサイエンティストが状況に合わせて適切な手法を選択することが重要です。

4つの切り口で見る実用的な高速化

 Pythonによる実用的な高速化手法の選択肢として、4つの分類で説明していただきました。

  1. クラスタレベル:Hadoop、Spark
  2. コンピュータレベル:マルチプロセス、GPGPU
  3. 言語レベル:NumbaCython
  4. コーディングレベル:リスト内包表記、NumPy Array

 クラスタレベルでの高速化については、「分散型機械学習 with PySpark」をテーマにした『大規模並列処理:PythonとSparkの甘酸っぱい関係~PyData.Tokyo Meetup #3 イベントレポート』でも紹介していますので、参考にしてください。

コンピュータレベルでの高速化:マルチプロセス

 Pythonには、インタプリンタ上で1度に1つのスレッドのみが動作することを保証するGIL(Global Interpreter Lock)が実装されています。そのため、コンピュータレベルの高速化の手法として、デフォルトではマルチスレッドには対応していませんが、multiprocessingモジュールを使ったマルチプロセス計算が可能です。なお、マルチプロセスとマルチスレッドの違いは、メモリを共有するかしないかで理解することができます。マルチプロセスではプロセス間のメモリ共有はありません。

 最も簡単に並列計算ができるPool.map()のコードの例です。0から100までの平方根を要素に持つリストを4つのプロセスを使って作成します。

from multiprocessing import Pool
import math

pool = Pool(processes=4)
pool.map(math.sqrt, xrange(10000))

pool.close()
pool.join()

 このように手軽に計算できることがmultiprocessingモジュールのメリットですが、プロセス作成のオーバーヘッドが大きいため、1つの処理が数十秒程度かかるような処理で使用する場合にメリットがあります。

言語レベルでの高速化:Numba vs. Cython

 言語レベルでの高速化の手法として、NumbaとCythonを紹介します。例として、3次元座標の1000地点間の距離を計算します。

 NumbaはPythonにJIT(Just In Time)コンパイラを導入して高速化するモジュールで、Anacondaで有名なContinuum Analyticsにより開発されています。Anacondaをインストールすれば、Numbaも一緒にインストールされます。

import numpy as np
from numba.decorators import autojit

def pairwise_python(X):
    M = X.shape[0]
    N = X.shape[1]
    D = np.empty((M, M), dtype=np.float)
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = np.sqrt(d)
    return D

pairwise_numba = autojit(pairwise_python)

 一方、Cythonは、C言語によるPythonの拡張モジュールを使うことにより高速化します。Cythonを実行する際は、IPython NotebookのCythonマジックが便利です。

%%cython

import numpy as np
cimport cython
from libc.math cimport sqrt

@cython.boundscheck(False)
@cython.wraparound(False)
def pairwise_cython(double[:, ::1] X):
    cdef int M = X.shape[0]
    cdef int N = X.shape[1]
    cdef double tmp, d
    cdef double[:, ::1] D = np.empty((M, M), dtype=np.float64)
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = sqrt(d)
    return np.asarray(D)

 IPythonのtimeitマジックでパフォーマンスを比較します。NumbaとCythonを使うことで約500倍高速になっていることが分かります。

X = np.random.random((1000, 3))
%timeit pairwise_python(X) # 1 loops, best of 3: 5.69 s per loop
%timeit pairwise_numba(X) # 100 loops, best of 3: 11 ms per loop
%timeit pairwise_cython(X) # 100 loops, best of 3: 9.76 ms per loop

 実行結果はこちらで確認できます。

コーディングレベルでの高速化:Python数値計算はfor文を書いたら負け

 最後に、リスト内包表記とNumPy Arrayを例に、コーディングレベルでの高速化を紹介します。今回紹介している高速化の手法の中でも、最も手軽で、使用頻度も高いため、ぜひ試してみてください。

import math
import numpy as np

def list_append(x):
    results = []
    for i in xrange(x):
        results.append(math.sqrt(i))
    return results

def list_append2(x):
    results = []
    for i in xrange(x):
        results.append(math.sqrt(i))
    return results

def list_comp(x):
    results = [math.sqrt(i) for i in xrange(x)]
    return results

def list_map(x):
    results = map(math.sqrt, xrange(x))
    return results

def list_numpy(x):
    results = list(np.sqrt(np.arange(x)))
    return results

 IPythonのtimeitマジックでパフォーマンスを比較します。NumPy Arrayを使った場合が一番高速であることが分かります。

x = 10000
%timeit list_append(x) # 100 loops, best of 3: 3.09 ms per loop
%timeit list_append2(x) # 100 loops, best of 3: 3.09 ms per loop
%timeit list_comp(x) # 1000 loops, best of 3: 1.86 ms per loop
%timeit list_map(x) # 1000 loops, best of 3: 1.17 ms per loop
%timeit list_numpy(x) # 1000 loops, best of 3: 736 μs per loop

 実行結果はこちらで確認できます。

 今回のスライドは、High performance python computing for data scienceにアップロードされています。 佐藤さんがご自身のブログに掲載された講演レポートPydata.Tokyoで「High Performance Python Computing for Data Science」を話してきたも併せてご覧下さい。