keisukeのブログ

***乱雑です!自分用のメモです!*** 統計や機械学習の勉強と、読み物を書く練習と、備忘録用のブログ

numpyでvstackするかbuilt-inのlistから変換するか

Pythonには,数値計算,特にベクトル行列演算を高速かつ便利におこなう
numpyと呼ばれる強烈なライブラリがあります.

しかし,numpyのarray (Cの配列のようなものと考えて問題ない)は,
ベクトル行列演算をpythonのforループを用いた演算よりも果てしなく高速に実行できるのですが,
要素アクセスやメモリアロケーションを頻繁におこなったりすると
pythonのlistを使ったforループに速度で負けることがよくあります.


今回の問題は次のような設定です:
int型からなるnumpyのarray:labelsと,
float型からなるnumpyのarray:valuesが与えられる.
これらのarrayの長さは等しいものとする.
valuesについて,labelsの値の等しいインデックスの要素の和をそれぞれ計算せよ.

わかりづらいので,具体例で示すと:

labels = np.array([0,  0,  1,  1,  2,  2,  0  ])
values = np.array([0.1,0.2,0.3,0.4,0.5,0.6,0.7])
# results = np.array([0.1+0.2+0.7, 0.3+0.4, 0.5+0.6])

と,同じラベルを持つvaluesの要素をそれぞれ足しあわせた配列を結果として要求します.

これを解くために,いくつか解法を考えました.
その1:numpyの比較演算子をループ中で用いて,sliceを使って配列を切り出して足し合わせる方法

def naive_comp(labels, values, nof_labels):
    new_values = np.empty(nof_labels)
    for l in xrange(nof_labels):
        new_values[l] = values[labels==l].sum()
    return new_values

その2:numpyの比較演算子をループ中で用いて,内積をとる方法

def naive_dot(labels, values, nof_labels):
    new_values = np.empty(nof_labels)
    for l in xrange(nof_labels):
        new_values[l] = np.dot(values, labels==l)
    return new_values

その3:new_valuesへの要素アクセスを避けるため,numpy.vstackを用いて行列を前もって構築して行列積をとる方法

def vstack(labels, values, nof_labels):
    stacked_bool = (labels == 0)
    for l in xrange(1, nof_labels):
        stacked_bool = np.vstack((stacked_bool, labels==l))
    return np.dot(values, stacked_bool.T)

その4:new_valuesへの要素アクセスを避けるため,list.appendを用いて行列を前もって構築して行列積をとる方法

def builtinlist(labels, values, nof_labels):
    listofbools = list()
    for l in xrange(nof_labels):
        listofbools.append(labels==l)
    stacked_bool = np.array(listofbools)
    return np.dot(values, stacked_bool.T)

また,labelsとvaluesは次のように構築します:

    length = 1000
    nof_labels = 20
    labels = np.random.randint(nof_labels, size=length)
    values = np.random.random(size=length)

長さ1000,ラベルの種類20程度の小ささならばほぼ実行時間に違いはありませんでしたが,
長さやラベルの種類の数を増やすと実行時間に差が出始めます.

length=1000, nof_labels=20 length=100000, nof_labels=200 length=200000, nof_labels=1000
naive_comp 0.00022 0.043965 0.390768
naive_dot 0.000147 0.048827 0.478222
vstack 0.000352 0.479748 18.735946
builtinlist 0.000365 0.312864 3.148327

vstackは,lengthが大きくなってメモリが限界に近づくと露骨にパフォーマンスが落ちます.
巨大な行列を構築するときは,行列をだんだん成長させていくやり方だと
毎回メモリの再配置が行われて非常に非効率になります.
むしろ組み込みのリストで行列(のような何か)を作ってからそれをnumpy形式に変換した
ほうがずっと速くなります.

まとめ.

個人的には,builtinlist関数が一番速いんじゃないかと思っていましたが,
実際は一番遅いと思っていたnaive_compが最高速を記録しました.

下2つが上2つに負けた理由は,メモリアロケーションのせいだということは
想像がつくのですが,
上2つの中でnaive_dotがnaive_compに負けた理由がイマイチわかりません.
naive_dotは直接内積を計算しにかかるので,(内部の実装がどうなっているのか
確認していませんが)普通はメモリを新たに要求しないはずです.
一方,naive_compはいちいち新たなarrayを構築してそのsumをとっていますので,
毎回メモリを要求しています.

基本的にはメモリを要求しないほうが速いのですが,
この場合はnumpyのfancy indexingが非常に優秀ということなのでしょう.
特に,今回はTrue/Falseのarrayでindexingをしていますので,特別高速なのかもしれません*1

「numpyの要素アクセスは遅いからできるだけ避けろ」
「numpyのfancy indexingは便利だが使い方によってはボトルネックになるから気をつけろ」
というようなことをよく聞きますが,今回に限ってはその原則に従わず,
内積よりもfancy indexingを使ったほうが速いという結果になりました.
「numpyで計算するときにどうすれば速いか,という問題に一般的な答えはなく,
問題の特性に大きく依存する」
という,これもまたよく聞く内容を確かめる結果となりました.


ちなみに,巷で噂のpypy*2で同じように実験してみた結果,次のようになりました:

length=1000, nof_labels=20 length=100000, nof_labels=200 length=200000, nof_labels=1000
naive_comp 0.006402 0.182519 1.772592
naive_dot 0.003382 0.515398 4.860914
vstack 0.004108 5.220479 232.755964
builtinlist 0.003341 2.476905 24.333794

うーん・・・
今回はほとんどの計算がnumpyの範疇なので,pypyの得意分野ではないようです*3

*1:length=200000, nof_labels=1000のときは1ラベルあたりに割り当てられるインデックスの数は平均で200000/1000=200となります.素直に内積を取ろうとした時(naive_dot)は200000要素の掛け算がおこなわれますが,naive_compでは200要素の足し算でしかありません.もっとも,naive_compは足し算の前に200000要素の比較演算と200要素のメモリの読み書きがおこなわれるわけですが・・・

*2:pythonpythonを実装したら100倍速くなったとかいう謎の環境

*3:pypyのnumpyもpythonのnumpyより速いってpypyの公式に書いてあったけど