Weitere ähnliche Inhalte Ähnlich wie High performance python computing for data science (20) High performance python computing for data science1. High Performance Python Computing for Data Science
~データ分析でPythonを高速化したいときに見る何か~
株式会社ブレインパッド
佐藤 貴海
@tkm2261
PyData.Tokyo Meetup #4
2015年4月3日PyData.Tokyo Meetup #4 1
16. Python高速化の選択肢
2015年4月3日PyData.Tokyo Meetup #4 16
ハード
ウェア
レベル
ソフト
ウェア
レベル
コンピュータレベル
言語レベル
コーディングレベル
独断と偏見による、データサイエンティストが利用可能なレベルの高速化選択肢
Hadoop Streaming
Spark
H2o (機械学習限定)
マルチプロセス計算 (multiprocessing)
GPGPU (Numba)
C拡張(Cython)
JITコンパイル(PyPy, Numba)
BLAS最適化
クラスタレベル
リスト内包表記
辞書型の活用
Numpy等の利用
疎行列の利用
(番外編)プロファイラの利用
17. クラスタレベルの高速化
2015年4月3日PyData.Tokyo Meetup #4 17
ハード
ウェア
レベル
ソフト
ウェア
レベル
言語レベル
コーディングレベル
C拡張(Cython)
JITコンパイル(PyPy, Numba)
BLAS最適化
リスト内包表記
辞書型の活用
Numpy等の利用
疎行列の利用
(番外編)プロファイラの利用
コンピュータレベル
マルチプロセス計算 (multiprocessing)
GPGPU (Numba)
Hadoop Streaming
Spark
H2o (機械学習限定)
クラスタレベル
19. クラスタレベルの高速化
2015年4月3日PyData.Tokyo Meetup #4 19
Hadoop Streaming
標準入出力経由で任意言語でMapReduceを使えるライブラリ
数時間以上かかるプログラムで、お金があるときの選択肢
メリット
デメリット
Amazon EMRで簡単構築
MapReduceなので多段処理に弱い
最近話題のオープンソース分散機械学習ライブラリ
メリット 機械学習アルゴリズムの網羅率が高い(RBF, GBT, GLM, DNN)
デメリット ETL(データ処理)はまた別途必要
24. コンピュータレベルの高速化
2015年4月3日PyData.Tokyo Meetup #4 24
ハード
ウェア
レベル
ソフト
ウェア
レベル
言語レベル
コーディングレベル
Hadoop Streaming
Spark
H2o (機械学習限定)
C拡張(Cython)
JITコンパイル(PyPy, Numba)
BLAS最適化
クラスタレベル
リスト内包表記
辞書型の活用
Numpy等の利用
疎行列の利用
(番外編)プロファイラの利用
コンピュータレベル
マルチプロセス計算 (multiprocessing)
GPGPU (Numba)
31. Pool.map()
もっとも手軽に並列計算できるのがPool.map()
2015年4月3日PyData.Tokyo Meetup #4 31
from multiprocessing import Pool
import math
pool = Pool(processes=4) # 引数なしなら、最大コア数
print pool.map(math.sqrt, range(10)) # 0~10要素の平方根が表示
pool.close()
pool.join()
メリット
• 超手軽
• 普通のmap関数と入れ替えてデバッグが容易
デメリット
• 並列化前にはmap関数での実装は稀なので、要リファクタ
• 並列化対象関数の引数が1個限定なので、要リファクタ
• 関数と引数がシリアライズ(pickle)出来ないと使えない ※後述
後処理が重い場合はPool.imap()でイテレータ取得の方が良い
ただしchunksizeの設計が面倒
32. Pool.apply_async()
2015年4月3日PyData.Tokyo Meetup #4 32
for文を簡単に並列化したいときにオススメ
from multiprocessing import Pool
import math
pool = Pool(processes=4) # 引数なしなら、最大コア数
list_processes = []
for i in range(10):
# math.pow(x, y)はxのy乗を返す関数
list_processes.append(pool.apply_async(math.pow, (i, 0.5))) # 引数を複数設定可能
list_results = [process.get() for process in list_processes]
print list_results
pool.close()
pool.join()
メリット • 並列化前のfor文をそのまま残せる
デメリット
• map関数よりは記述量が多い
• 関数と引数がシリアライズ(pickle)出来ないと使えない ※後述
33. Process()
2015年4月3日PyData.Tokyo Meetup #4 33
関数と引数がシリアライズ出来ない時にはコレ、結果は共有メモリで取得
from multiprocessing import Process, Manager
def sqrt(x, map_results):
map_results[x] = x**0.5
if __name__ == '__main__':
list_processes = []
manager = Manager()
map_results = manager.dict()
for i in range(10):
process = Process(target=sqrt,
args=(i, map_results,))
process.start()
list_processes.append(process)
[process.join() for process in list_processes]
print map_results.values()
メリット • シリアライズ出来なくても使える
デメリット
• 前の2つよりは面倒
• 例外処理が面倒
35. GPGPU(General-purpose computing on graphics processing units)
基本的にGPGPUはCUDAでプログラミング
2015年4月3日PyData.Tokyo Meetup #4 35
GPUの演算資源を画像処理以外の目的に応用する技術
by wikipedia
GPGPU
CUDA NVIDIAが提供するGPU向けのC言語の統合開発環境
by wikipedia
注意:データサイエンティストには障壁が高いかも・・・
37. Numbaでのカーネル関数記述
2015年4月3日PyData.Tokyo Meetup #4 37
PythonにJITコンパイラを導入して高速化するモジュール ※後述
GPUも扱える。Numba
from numba import cuda
import numpy
@cuda.jit('void(f8[:, :], f8[:, :])')
def pairwise_distance(X, D):
'''距離行列計算関数
'''
M = X.shape[0]
N = X.shape[1]
#スレッドとブロックのインデックスを使って
#このスレッドで計算する地点ペアを特定
i, j = cuda.grid(2)
if i < M and j < M:
d = 0.0
for k in range(N):
tmp = X[i, k] - X[j, k]
d += tmp * tmp
D[i, j] = math.sqrt(d)
if __name__ == '__main__':
X = numpy.random.random((1000, 3)) # 3次元座標
D = numpy.empty((1000, 1000)) # 出力される距離行列
griddim = 100, 100 # 100*100にブロックを配置
blockdim = 16, 16 # ブロック内で16*16にスレッドを配置
pairwise_distance[griddim, blockdim](X, D)
STEP2
1600×1600のスレッドの場所で
どの2地点間の計算するか決定
※この設計だと1600以上の地点間は計算出来ない
STEP1
1600×1600のスレッドを用意
C言語での記述とほぼ同様のフォーマット
38. Numba(GPGPU) Tips
2015年4月3日PyData.Tokyo Meetup #4 38
• 基本的にはCPUよりGPUの方が速い
– ただし、GPUへのメモリ転送のオーバーヘッドと
GPUメモリが少ないことから、CPUが優ることも多い
• メモリチューンは高速化の道だが茨の道
– 同一ブロック内でアクセスできるシェアードメモリを使うと、さらなる高速化
– コレの実装はすごい面倒なので、データサイエンティストはやらなくても・・・
• ライブラリを使いましょう
– カーネル関数の自前実装は疲れるのでcuBLAS, cuFFT, cuRANDなどの
既存ライブラリを使いましょう
– Numbaの有料版のNumbaProでは使えます
• 詳細はブログに書きました
– 『Python高速化 Numba入門』
http://yutori-datascience.hatenablog.com/entry/2014/12/09/235628
39. 言語レベルの高速化
2015年4月3日PyData.Tokyo Meetup #4 39
ハード
ウェア
レベル
ソフト
ウェア
レベル
コーディングレベル
Hadoop Streaming
Spark
H2o (機械学習限定)
クラスタレベル
リスト内包表記
辞書型の活用
Numpy等の利用
疎行列の利用
(番外編)プロファイラの利用
コンピュータレベル
マルチプロセス計算 (multiprocessing)
GPGPU (Numba)
言語レベル
C拡張(Cython)
JITコンパイル(PyPy, Numba)
BLAS最適化
43. Numba(CPU)
基本的にはデコレータ一発
サポート外のpythonオブジェクトが無ければnopython=Trueでさらに高速化
2015年4月3日PyData.Tokyo Meetup #4 43
@jit('f8[:, :](f8[:, :], f8[:, :])', nopython=True)
def pairwise_distance(X, D):
M = X.shape[0]
N = X.shape[1]
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] = numpy.sqrt(d)
return D
普通のPython 4.69秒
Numba 0.015秒
scipyのpdist 0.007秒
3次元座標の1,000地点間の距離計算時間はこちら
上の実装は、対称行列分の半分はサボれるので納得の結果
44. Cython
3倍速いけど、あんまり速くない。不味いところあったら教えて下さい
2015年4月3日PyData.Tokyo Meetup #4 44
import numpy
cimport numpy
def pairwise_distance(numpy.ndarray[double, ndim=2] X,
numpy.ndarray[double, ndim=2] D):
cdef int i, j, k, M, N
cdef double d, tmp
M = X.shape[0]
N = X.shape[1]
for i in xrange(M):
for j in xrange(M):
d = 0.0
for k in xrange(N):
tmp = X[i, k] - X[j, k]
d += tmp * tmp
D[i, j] = numpy.sqrt(d)
return D
C言語っぽく型を、ひたすら固定 Numbaよりは若干面倒
*.pyxで保存して、import pyximport; pyximport.install()で使う
普通のPython 4.69秒
Cython 1.22秒
3次元座標の1,000地点間の距離計算時間はこちら
45. BLAS最適化
数値計算ならコレで結構速くなる
2015年4月3日PyData.Tokyo Meetup #4 45
数値計算をやると裏で必ずお世話になるのがBLASとLAPACK
Basic Linear Algebra Subprograms (BLAS)
-線型代数計算を実行するライブラリの標準仕様
Linear Algebra PACKage (LAPACK)
-BLAS上に構築された固有値計算などの高位な線形代数計算ライブラリ
現在様々なBLAS実装が公開されている
Intel MKL … MATLABはコレ 有償 すごく速い・高い・安心!
ATLAS … 自動チューンのBLAS BSD 速い
GotoBLAS2 … 後藤和茂氏作成のBLAS BSD かなり速い 開発停止
OpenBLAS … xianyi氏によるGotoBLAS2の後継BLAS BSD すごく速い
(MATLAB, R, Octave, numpy …)
・
・
・
46. BLASの比較
2015年4月3日PyData.Tokyo Meetup #4 46
引用:R BLAS: GotoBLAS2 vs OpenBLAS vs MKL (http://blog.felixriedel.com/2012/11/r-blas-gotoblas2-vs-openblas-vs-mkl/)
実行コード
A = matrix(rnorm(n*n),n,n)
A %*% A
solve(A)
svd(A)
RのデフォルトBLASから何倍早くなったか検証してるサイトがあったので紹介
最大で11倍ほど高速化
MKLが基本的に一番高速
OpenBLASも所によってはMKLを上回ることも
マルチスレッド環境では導入は必須かも
47. コーディングレベルの高速化
2015年4月3日PyData.Tokyo Meetup #4 47
ハード
ウェア
レベル
ソフト
ウェア
レベル
Hadoop Streaming
Spark
H2o (機械学習限定)
クラスタレベル
コンピュータレベル
マルチプロセス計算 (multiprocessing)
GPGPU (Numba)
言語レベル
C拡張(Cython)
JITコンパイル(PyPy, Numba)
BLAS最適化
コーディングレベル
リスト内包表記
辞書型の活用
Numpy等の利用
疎行列の利用
(番外編)プロファイラの利用
49. リスト内包表記
多くの方がご存知の通り、リスト内包表記は早いです
2015年4月3日PyData.Tokyo Meetup #4 49
list_results = []
for i in xrange(1000000):
list_results.append(str(i))
list_results = []
for i in xrange(1000000):
list_results += [str(i)]
list_results = [str(i) for i in xrange(100000)]
リストの足し算するよりも、
appendしたほうが速く
さらに、リスト内包表記の方が速いです
リスト内包表記は『内部的にはappendをせずに、直接リストにぶち込めるから』速いのだそうです。
『リスト内包表記はなぜ速い?』 http://dsas.blog.klab.org/archives/51742727.html
0.42秒
0.35秒
0.26秒
list_results = []
list_results_append = list_results.append
for i in xrange(1000000):
list_results_append(str(i))
さらに、appendのdotを外したほうが速く
0.32秒
list_results = map(str, xrange(100000))
実は、今回の場合はmapが一番速いです。(mapはlambda式とか使うと遅い)
0.13秒
55. Numpy等の利用
行列積とか今更比べても仕様がないので、私の失敗例
2015年4月3日PyData.Tokyo Meetup #4 55
pandasのデータフレームに対して多対一の距離計算を考えていて
user_all_vecs = numpy.random.random((100000, 100)) # ユーザ毎のベクトル(多)
some_user_vec = numpy.random.random(100) # あるユーザのベクトル(一)
pd_user_all_vecs = pandas.DataFrame(user_all_vecs) ) # pandas準備
インデックスを保持したいので行ごとにapplyで計算すると超遅く
scipyのcdistと再インデックス付与の方が断然速い
pd_user_all_vecs.apply(lambda vec: euclidean(vec, some_user_vec),
axis=1)
pandas.Series(cdist(pd_user_all_vecs,[some_user_vec])[:, 0],
index=pd_user_all_vecs.index)
3.9秒
0.06秒
BLASパワーはマジで偉大
60. scipy.sparseの利用
2015年4月3日PyData.Tokyo Meetup #4 60
色々提案されているので、主要な疎行列表現のみ紹介
COO表現
(COOdinate format)
行番号、列番号、要素の3配列で行列を表現
>>> row = numpy.array([0, 3, 1, 0])
>>> col = numpy.array([0, 3, 1, 2])
>>> data = numpy.array([4, 5, 7, 9])
>>> coo_matrix((data, (row, col)), shape=(4, 4)).toarray()
array([[4, 0, 9, 0],
[0, 7, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 5]])
メリット • 超簡単
デメリット
• 要素のアクセスが線形探索のみ(ソート済みなら二分木探索)
• 要素の追加が難しい
• この表現のまま行列演算は基本難しい
使用例
http://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html#scipy.sparse.coo_matrix
61. scipy.sparseの利用
2015年4月3日PyData.Tokyo Meetup #4 61
CSR表現
(Compressed Sparse Row format)
列番号、行の境目、要素の3配列で行列を表現
>>> indptr = numpy.array([0, 2, 3, 6])
>>> indices = numpy.array([0, 2, 2, 0, 1, 2])
>>> data = numpy.array([1, 2, 3, 4, 5, 6])
>>> csr_matrix((data, indices, indptr), shape=(3, 3)).toarray()
array([[1, 0, 2],
[0, 0, 3],
[4, 5, 6]])
使用例
メリット
• 行列演算がしやすい
• 行要素が定数個と仮定するならO(1)で要素アクセス可能
デメリット
• 作るのが面倒くさい
• 要素アクセスはO(1)だとしても面倒
http://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html#scipy.sparse.csr_matrix
62. scipy.sparseの利用
2015年4月3日PyData.Tokyo Meetup #4 62
CSC表現
(Compressed Sparse Column format)
行番号、列の境目、要素の3配列で行列を表現
使用例
メリット
• 行列演算がしやすい
• 行要素が定数個ならO(1)で要素アクセス可能
デメリット
• 作るのが面倒くさい
• 要素アクセスもO(1)だとしても面倒
http://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html#scipy.sparse.csc_matrix
>>> indptr = np.array([0, 2, 3, 6])
>>> indices = np.array([0, 2, 2, 0, 1, 2])
>>> data = np.array([1, 2, 3, 4, 5, 6])
>>> csc_matrix((data, indices, indptr), shape=(3, 3)).toarray()
array([[1, 0, 4],
[0, 0, 5],
[2, 3, 6]])
行と列が入れ替わっただけなので、
CSR表現と同じ
63. scipy.sparseの利用
2015年4月3日PyData.Tokyo Meetup #4 63
このへんはscipy.sparse特有の表現
LIL表現
(linked list sparse format)
各行毎に、列番号リストと要素リストで行列を表現
リストが標準のPythonならでは。性能はどれも平均的
dok表現
(Dictionary Of Keys based
sparse format)
行番号と列番号がキーになった辞書で行列を表現
O(1)で要素にアクセス可能だが、行列演算は厳しい
要素の上書きが必要な場合に威力を発揮
※要素アクセスは辞書型に比べ遅いので、行列にこだわらなければ辞書型で
68. Loggingをちゃんと使う
概算でよいなら、loggingでログを吐くのがオススメ
メソッド前と後ろでログ吐けば大体の時間はわかる
2015年4月3日PyData.Tokyo Meetup #4 68
2015-03-23 00:32:53,385 ga_aa_optimize.optimize 50 [INFO]start optimize with image_num=20, max_iter=50
2015-03-23 00:32:53,387 ga_aa_optimize.optimize 51 [INFO]start to make initial solutions.
2015-03-23 00:32:55,530 ga_aa_optimize.optimize 81 [INFO]row_shift is 0.
2015-03-23 00:32:55,530 ga_aa_optimize.optimize 82 [INFO]end making initial solutions in 2.14299988747 seconds.
2015-03-23 00:32:57,713 ga_aa_optimize.optimize 135 [INFO]1: 58517.0 | 2.17900013924
2015-03-23 00:33:00,621 ga_aa_optimize.optimize 135 [INFO]2: 57258.0 | 2.90799999237
2015-03-23 00:33:03,428 ga_aa_optimize.optimize 135 [INFO]3: 56428.0 | 2.80799984932
2015-03-23 00:33:06,232 ga_aa_optimize.optimize 135 [INFO]4: 56242.0 | 2.80300021172
2015-03-23 00:33:08,980 ga_aa_optimize.optimize 135 [INFO]5: 56173.0 | 2.74699997902
2015-03-23 00:33:11,894 ga_aa_optimize.optimize 135 [INFO]6: 56004.0 | 2.91499996185
2015-03-23 00:33:14,677 ga_aa_optimize.optimize 135 [INFO]7: 55948.0 | 2.78200006485
2015-03-23 00:33:17,608 ga_aa_optimize.optimize 135 [INFO]8: 55919.0 | 2.93099999428
2015-03-23 00:33:20,421 ga_aa_optimize.optimize 135 [INFO]9: 55919.0 | 2.81200003624
2015-03-23 00:33:23,270 ga_aa_optimize.optimize 135 [INFO]10: 55899.0 | 2.84799981117
http://qiita.com/amedama/items/b856b2f30c2f38665701
『ログってなに?printでいいだろ』って人はここを読むと良いかも
70. プロファイラの利用
2015年4月3日PyData.Tokyo Meetup #4 70
11711673 function calls (11703283 primitive calls) in 38.377 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
310291 18.719 0.000 18.719 0.000 ga_aa_optimize.py:246(<dictcomp>)
3900 4.525 0.001 26.568 0.007 ga_aa_optimize.py:221(_row_local_search)
100 3.393 0.034 5.959 0.060 ga_aa_optimize.py:100(generate_image)
200 3.186 0.016 3.330 0.017 ga_aa_optimize.py:162(calc_objective)
310565 2.762 0.000 3.118 0.000 {sorted}
100 1.323 0.013 27.891 0.279 ga_aa_optimize.py:194(local_search)
7777 1.055 0.000 1.055 0.000 {numpy.core.multiarray.concatenate}
310314 0.502 0.000 0.502 0.000 {numpy.core.multiarray.empty}
6559368 0.355 0.000 0.355 0.000 ga_aa_optimize.py:250(<lambda>)
310309 0.257 0.000 0.308 0.000 random.py:273(choice)
python -m cProfile -s time *.py 引数使用方法
結果
意味
ncalls :呼び出し回数
tottime :この関数が消費した時間の合計
percall : tottime を ncalls で割った値
cumtime :下位の関数を含むこの関数 消費時間の合計。
filename:lineno(function) その関数のファイル名、行番号、関数名
基本tottimeが大きい関数を高速化すればOK