[numpy] 2次元配列の高速化

例えば10000×10000の2次元配列全体に、何らかの処理をするとします。
2重forループが思い浮かぶところですが、pythonのforループが激遅いというのは有名です。
(正確には「配列インデックスアクセスが遅い」「基本的な演算に毎回型チェックが入るから遅い」などの複合的な要素があるのでやや語弊がある表現ではあります。)

pythonで単純な処理をループする場合の高速化の手段のひとつに、numpyがあります。
一方で、numpyで簡潔に書ける処理はある程度限定されており、全ての計算をnumpyで書くのは難しい場合もあります。そのときに使える考え方のひとつに、forループ1重分をnumpyにするというものがあります。具体例を見ていきましょう。

スポンサーリンク

二項係数の生成

0n,k10000に対して、二項係数nCkの値を生成しましょう。
値が非常に大きくなる(k=0nnCk=2nより、10000Ckの最大値は21000010001以上210000以下で、およそ3000桁の数となる)ので、ここでは下10桁を計算することにします。

二項係数の計算方法もいくつかの方法がありますが、今回は漸化式
nCk=n1Ck1+n1Ck を用いることとします。まずは2重ループによる実装です。

31秒かかりました。これをnumpyで高速化します。漸化式に注目すると、

comb[1:, 1:] = comb[:-1, 1:] + comb[:-1, :-1]

という式が思い浮かびますが、これでは上手くいきませんね。comb[n, k]を計算する段階では、「計算結果の」comb[n1, k]などの値を用いる必要があり、これらの計算を同時に行うことはできないからです。また、modもある程度の頻度で取ってあげないといけません。

そこで、それぞれの行ごとにはループ処理をしたまま、各行をnumpyで計算してあげます。

1秒で計算が終わっており、約30倍の高速化に成功しました。

前者のコードでは、「ループ」や、ループ変数による配列参照がO(108)回行われています。
一方で、後者のコードでは、それらはO(104)回しか行われていません。オーバーヘッドとなっていた部分の処理回数のオーダーを削減することで、総合的な処理時間を大幅に削減することに成功しています。

<まとめ>

2次元ループ を 1次元ループに変更することで、ループに起因するボトルネックのほとんどを解消できる。
したがって大きな軸1方向についてnumpyが使えれば、巨大な2次元配列は十分高速に処理できる。

問題例

最近私がAtCoderで経験した例になります。ネタバレを含むのでご注意ください。
いずれも 2019/06/19 現在で、Python, PyPyでの正答提出者の中で最も実行時間の短いコードを実現しています。

AtCoder ABC 130-E  Common Subsequence

問題文:
自分の提出: → (簡略化

次のような漸化式によって、dpおよびdp_cumを更新していく問題です。
dp[n,m]={dpcum[n1,m1](S[n]=T[m])0(otherwise),dpcum[n,m]=in,jmdp[n,m]. 全ての配列の要素をこのルールで一気に計算するのは難しそうですが、n を固定して m に関するベクトルだと思います。すると、dp[n]dp[n1] のシフト(スライス)です。ただし T が特定の値となる場所を 0 に書き直さなければいけませんが、これも簡単です(np.whereを使う、あるいは (T==S[n]) というbool値のarrayを掛ける。さらに dpcum[n] も、 dpcum[n1]dp[n] の累積和を加えたものとして、numpyで容易に計算できます。

n についてのループが残りますが、1次元分のループをnumpyで高速化してしまえば、そこはボトルネックには本質的にはひびきません。

実際の実装では、上述の dpcum に相当するものを dp として処理を進めています。

AtCoder ABC 018-C 菱型カウント

問題文:
自分の提出:

AtCoder AGC 033-A Darker and Darker

問題文:
自分の提出:

 この2問は、ほとんど同じ問題です。各マスが、最寄りの黒マスからどれだけの距離にあるかを調べる問題です。黒マスからの距離を、
・左にしか移動できないとして考えた数値
・左・右にしか移動できないとして考えた数値
・左・右・上に…

と更新していきます。各段階での更新では、
dp[n,m]=min(dp[n1,m]+1,dp[n,m])
という種類の漸化式を使うことになります。これをdp[n]というベクトルをdp[n1]というベクトルから計算していると見なせば、1次元分のループをnumpy化することは容易です。

タイトルとURLをコピーしました