PyUblasとBoost.Pythonを使ったDTWの実装
PyUblasを試すために, 今回は距離関数の一つであるDTW(Dynamic Time Warping)を実装する.
DTWの説明は余所で読んでもらうとして, まあ2つの長さが異なるベクトルx,y の距離をO(|x|×|y|)で計算するアルゴリズムだと思ってもらえばいい. 今回はパフォーマンスにも着目するため, 比較対称としてmlpyのDTW関数を用いた.
Pythonで単純に書くと次のようになる.
def dtw(x,y): n = len(x) m = len(y) cost = zeros((n,m)) cost[0,0] = abs(x[0] - y[0]) for i in range(1,n): cost[i,0] = cost[i-1,0] + abs(x[i]-y[0]) for i in range(1,m): cost[0,i] = cost[0,i-1] + abs(x[0]-y[i]) for i in range(1,n): for j in range(1,m): cost[i,j] = min(cost[i-1,j],cost[i,j-1],cost[i-1,j-1]) + abs(x[i] - y[j]) return cost[n-1,m-1]
ただ, これは非常に遅い. 長さ16のベクトル同士の比較を10000回するだけで6.3秒かかる.
そこで, Boost.Pythonを用いてC++で実装することで高速化する. 全部は掲載しないが, メインのループは次のようになっている.
for(int i = 1;i <= n;i++,ite_x++){ numpy_vector<double>::const_iterator ite_y = y.begin(); for(int j = 1;j <= m;j++,ite_y++){ costs[i][j] = min(min(costs[i-1][j],costs[i-1][j-1]),costs[i][j-1]) + euc(*ite_x,*ite_y); } }
numpy_vectorはPyUblasで提供されているvectorである. これはindexでのアクセスが遅いとあったので(以前の日記を参照), Iteratorを用いてアクセスしている.
これによる高速化がどの程度かを調べるため最初のPythonで実装したもの, mlpy, Boost.Pythonで実装したもの, 3つのパフォーマンスを計測した. 長さ16のベクトル同士で100万回距離を計測している. また, コンパイルは-O2オプションを用いて行なっている.
手法 | 実行時間(s) |
---|---|
PurePython | たくさん |
mlpy | 10.8s |
Boost.Python | 10.4s |
mlpyよりちょっと速い! mlpyはCythonを使って実装しているように見える.
もっと速く!
更にBoost.Pythonを高速化するためにoprofileを用いたパフォーマンス分析を行なった. 計測は次のように行なっている.
sudo opcontrol --reset
sudo opcontrol --start
計測対象プログラム
sudo opcontrol --shutdown
opreport -tl1
ただし, コンパイルオプションで-gをつけているためパフォーマンスは大幅に悪化している. この計測結果は次のようになった.
samples % app name symbol name
434030 10.0486 extra_distance.so boost::python::handle<_object>::get() const
384109 8.8929 extra_distance.so pyublas::numpy_array::begin() const
310042 7.1781 no-vmlinux /no-vmlinux
268411 6.2142 extra_distance.so pyublas::numpy_array::ndim() const
248274 5.7480 extra_distance.so pyublas::numpy_array::stride(long) const
203019 4.7003 extra_distance.so pyublas::numpy_array::dim(long) const
これを見る限り, numpy_vectorの値へのアクセスに大きなコストがかかっている. そこで, Pythonから受け取った際にnumpy_vectorをboost::numeric::ublas::vectorに変換し, それに対して処理を行うように変更した.
その結果のパフォーマンスは
手法 | 実行時間(s) |
---|---|
PurePython | たくさん |
mlpy | 10.8s |
Boost.Python | 10.4s |
変換する奴 | 2.7s |
と大幅に向上した.
まとめ
PyUblasのvectorの要素へのアクセスは例えiteratorであっても遅い. まずiteratorの取得が結構めんどいっぽいし. これはメモリコピーが不要という利点を上回るものであり, 要素へのアクセスが多いような操作では, 一度boost::numericのvectorに変換した方がよい.
実験結果で注意しなければいけないのは, mlpyにおいてCythonで実装されたDTWよりもBoost.Pythonを適切に用いたDTWの方が速いように見えるのは, Cythonのせいでは無い.
現在, Boost側のDTW関数は, A,BとC, Dを渡すと, (A,B)(A,D)(B,C)(B,D)の間の距離を返すような仕様になっている. 一方, mlpyでは一つずつしかベクトルを渡せないため, 上の例ではAを2回型変換する必要がある. このことが, 大きなオーバーヘッドを産んでいるため, ちゃんとフェアな実験をしなおさなくてはならない.
もう一つ注意したいのは, contiguousであることが保証されているnumeric_matrixを使うという手だ. これは, メモリコピーの必要無く素早い処理が書けるのでは無いだろうか. これも試したい.
追記: 残念ながらnumpy_matrixとnumpy_vectorのパフォーマンスはさほど変わらなかった.