質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Jupyter

Jupyter (旧IPython notebook)は、Notebook形式でドキュメント作成し、プログラムの記述・実行、その実行結果を記録するツールです。メモの作成や保存、共有、確認などもブラウザ上で行うことができます。

Anaconda

Anacondaは、Python本体とPythonで利用されるライブラリを一括でインストールできるパッケージです。環境構築が容易になるため、Python開発者間ではよく利用されており、商用目的としても利用できます。

NumPy

NumPyはPythonのプログラミング言語の科学的と数学的なコンピューティングに関する拡張モジュールです。

並列処理

複数の計算が同時に実行される手法

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Q&A

解決済

3回答

3363閲覧

Pythonの高速化

s_miyazaki

総合スコア5

Jupyter

Jupyter (旧IPython notebook)は、Notebook形式でドキュメント作成し、プログラムの記述・実行、その実行結果を記録するツールです。メモの作成や保存、共有、確認などもブラウザ上で行うことができます。

Anaconda

Anacondaは、Python本体とPythonで利用されるライブラリを一括でインストールできるパッケージです。環境構築が容易になるため、Python開発者間ではよく利用されており、商用目的としても利用できます。

NumPy

NumPyはPythonのプログラミング言語の科学的と数学的なコンピューティングに関する拡張モジュールです。

並列処理

複数の計算が同時に実行される手法

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

3グッド

3クリップ

投稿2020/04/04 22:08

編集2020/04/07 12:21

前提・実現したいこと

Window10のノートPCでAnacondaをインストールして、jupyter notebook上でPython3のコードを書いています。numpyを用いています。
0と1を要素にもつm×n行列Aの列ベクトルをv1,v2,...,vnとします。viに対して、k連続する1をkに置き換えたベクトルをuiとします。u1,u2,...unを列ベクトルとしてもつ行列をBとします。
具体例を示します。
v1=[0,0,1,1,1,0,0,0,1,0,0,1,1,1,1,1,1,1,1,0,1,0]
のとき、
u1=[0,0,3,3,3,0,0,0,1,0,0,8,8,8,8,8,8,8,8,0,1,0]
です。
Aが与えられたとき、Bを得られるようなコードを書きました。しかし例えばm=n=20000だと、計算に40秒ほどかかってしまいます。この計算を数千回繰り返したいと考えておりますので、1回あたりの計算時間をより短くしたいと考えております。自分でもいくつかの方法を試しましたが、計算時間を短縮する有効な方法が分かりません。計算時間を短縮する方法について何かアドバイスを頂けたら幸いです。よろしくお願いいたします。

なお、k連続する1をkに置き換える方法については、
https://maspypy.com/numpy連続同一値の数え上げ-atcoder-abc-129-d
を参考にさせていただきました。

該当のソースコード

コード1

Python3

1import numpy as np 2from tqdm import tqdm 3 4Matrix=np.zeros((2*10**4, 2*10**4), dtype=np.int32) 5 6def f(x): 7 temp=np.arange(len(x)) 8 temp[x>0]=0 9 np.maximum.accumulate(temp, out = temp) 10 left=temp 11 12 x_reversed=x[::-1] 13 temp=np.arange(len(x)) 14 temp[x_reversed>0]=0 15 np.maximum.accumulate(temp, out = temp) 16 right=len(x)-1-temp[::-1] 17 18 y=right 19 y -= left + 1 20 y[x==0]=0 21 return y 22 23Hight=Matrix.shape[0] 24Width=Matrix.shape[1] 25 26for i in tqdm(range(Width)): 27 Matrix[0,i]=0 28 Matrix[-1,i]=0 29 Matrix[:,i]=f(Matrix[:,i])

試したこと

①Matrixのデータ型の変更
np.int16にすると計算時間は30秒ほど
np.int8にすると計算時間は22秒ほど
になりましたがデータ型はnp.int32を用いたいと考えています。

②行列に対してまとめて計算
コード1では元の行列の一つ一つの列ベクトルに対して、k連続する1をkに置き換える、という処理をしていますが、行列Aに対して、縦にk連続する1をkに置き換える、という処理をしてみました。

コード2

Python3

1import numpy as np 2 3Matrix=np.zeros((2*10**4, 2*10**4), dtype=np.int32) 4 5def f(x): 6 x[:,0]=np.zeros(len(x), dtype=np.int32) 7 x[0]=np.zeros(len(x), dtype=np.int32) 8 x[-1]=np.zeros(len(x), dtype=np.int32) 9 10 temp=np.arange(len(x)).repeat(len(x)).reshape(len(x),len(x)) 11 temp[x>0]=0 12 np.maximum.accumulate(temp, out = temp) 13 left=temp 14 15 x_reversed=x[::-1] 16 temp=np.arange(len(x)).repeat(len(x)).reshape(len(x),len(x)) 17 temp[x_reversed>0]=0 18 np.maximum.accumulate(temp, out = temp) 19 right=len(x)-1-temp[::-1] 20 21 y=right 22 y -= left + 1 23 y[x==0]=0 24 return y 25 26%time f(Matrix)

計算時間は45秒ほどとなり、効果はないことが分かりました。

③numbaを用いる
from numba import jit
を加え、
def f(x)
の1行上に
@jit
を付け加えたのですが、エラーメッセージが出たうえで、計算時間はほぼ変わりませんでした。
@jitの代わりに
@jit(nopython=True)
を付け加えたみたところ、エラーが表示され、計算が実行されませんでした。

コード1に対してnumbaを用いる方法をご存じでしたら教えていただけないでしょうか。(→hayataka2049さんの回答により解決しました)

④Cythonを用いる
Cythonというものを用いればPythonの高速化ができる、ということを知りました。そこで次のページを参考にしました。
https://qiita.com/kenmatsu4/items/7c08a85e41741e95b9ba

まず

%load_ext Cython

を実行し、次に

%%cython -n test_cython_code def fib(int n): cdef int i cdef double a=0.0, b=1.0 for i in range(n): a, b = a+b, a return a

を実行すると、

DistutilsPlatformError: Unable to find vcvarsall.bat

というエラーが出てしまいます。
このエラーを解消する方法をご存じでしたら教えていただけないでしょうか。

⑤CuPyを用いる
CuPyというものを使って、GPUを搭載したコンピューターで計算を行うと高速化できる可能性がある、ということを知りました。(自分のPCにはGPUは搭載されていません)
Google Colaboratoryを使うと、CuPyの性能を確認することができるそうです。以下のようなページを読んで、CuPyについて調べているところです。
https://qiita.com/samacoba/items/d18e6cf09f544477aff4

⑥並列処理を行う
複数のCPUがあるコンピュータでは、複数のCPUを用いることによって計算時間は短くなると思います。そこで並列処理について調べてみて、まずは以下のページを参考にしました。

http://iatlex.com/python/parallel_first

このページ内の以下のコードを実行してみました。

Python3

1######## 並列計算を使えるように ######### 2from multiprocessing import Pool 3 4##### 並列計算させる関数(処理):引数1つ ### 5##### この場合は,引数の二乗を返す関数 ### 6def nijou(x): 7 print( x*x ) 8 9###### 並列計算させてみる ######### 10if __name__ == "__main__": 11 p = Pool(4) 12 p.map( nijou, range(10) )#nijouに0,1,..のそれぞれを与えて並列演算

しかしjupyter notebookのセルの数字が*になったままになり、結果が出力されません。

コード1に対して複数のCPUを用いた並列処理をする方法をご存じでしたら教えていただけないでしょうか。

補足情報

その他に高速化する方法をご存じでしたら教えていただきたいです。

fu_3823, teamikl, yodel👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

meg_

2020/04/05 01:29

・リンクは「リンクの挿入」で記入してください。
hayataka2049

2020/04/07 12:01

解決済みの質問ってそんなに見られないので、トラブルであれば別途立てたほうが良いかもしれません。cythonのvcvarsall.batのエラーは検索すれば色々出てくるのと、環境が絡む質問は回答する側としては再現するのも回答するのも難しいので、できるだけ自力で解決していただいた方が良いとは思いますが……。
s_miyazaki

2020/04/07 12:25

修正しました。 そうなのですね。色々とありがとうございます。
guest

回答3

0

ベストアンサー

numbaで書いてみました。手元でいくつかのテストケースでは確認しましたが、絶対に正しいとは言い切れないので、ちゃんと動くかはご自身で確認してください。

方針としては、とにかくpython側で処理してしまうと遅いので、配列ごとnumbaに投げます。

numba側のプログラムの書き方は見ての如くです。CとかFORTRANのノリで書いてください。numpyの関数を呼び出すよりそっちの方が速いのです(新たなnumpy配列を返す関数はメモリ上に新たな配列を作るのですべて本質的には遅いのです)。

python

1import numpy as np 2from numba import jit 3 4@jit("i4[:,:](i4[:,:])", nopython=True) 5def f(A): 6 B = A.copy() 7 for i in range(B.shape[1]): # 列のループ 8 before = 0 9 start_pos = 0 10 cnt = 0 11 for j in range(B.shape[0]): # 行のループ 12 if B[j, i] == 1: # 1のとき 13 if before == 0: # 直前の状態が0なら1にして数え始める 14 start_pos = j 15 before = 1 16 cnt += 1 # 数える 17 else: # 0のとき 18 if before != 1: # 直前の状態が0なら無視して続ける 19 continue 20 else: # 直前の状態が1のとき 21 for k in range(start_pos, j): # 1が出た範囲をcntで埋める 22 B[k, i] = cnt 23 # 状態をリセットする 24 before = 0 25 cnt = 0 26 # 行が終わって状態が1のとき 27 if before == 1: 28 for k in range(start_pos, B.shape[0]): 29 B[k, i] = cnt 30 31 return B 32

2万の正方行列で10秒くらいでした。


列方向に見ていくのはキャッシュ効率の観点からするとあまり好ましくありません。ということで、同じロジックで列ベクトルではなく行ベクトルを扱うバージョンの関数も作ってみました。

入力を転置して与えてください。結果も転置されたものが出てきます。
(転置がビューかコピーかで変わるかな? とも思ったのですが、これで速くなったのでたぶんいいのでしょう)

python

1@jit("i4[:,:](i4[:,:])", nopython=True) 2def f_trans(A): 3 B = A.copy() 4 for i in range(B.shape[0]): # 行のループ 5 before = 0 6 start_pos = 0 7 cnt = 0 8 for j in range(B.shape[1]): # 列のループ 9 if B[i, j] == 1: # 1のとき 10 if before == 0: # 直前の状態が0なら1にして数え始める 11 start_pos = j 12 before = 1 13 cnt += 1 # 数える 14 else: # 0のとき 15 if before != 1: # 直前の状態が0なら無視して続ける 16 continue 17 else: # 直前の状態が1のとき 18 for k in range(start_pos, j): # 1が出た範囲をcntで埋める 19 B[i, k] = cnt 20 # 状態をリセットする 21 before = 0 22 cnt = 0 23 # 行が終わって状態が1のとき 24 if before == 1: 25 for k in range(start_pos, B.shape[1]): 26 B[i, k] = cnt 27 28 return B 29

こちらは6秒でした。

どちらにしても、処理時間のかなりの割合は1.6GBもある配列の生成とコピーで食っています。ということで、これで実用上問題にはならないでしょう。Cythonで同じロジックをやるともう少し速い可能性はあるのですが、numbaのjitコンパイルだって優秀です。

(ベンチマーク取ってるページがありましたが、これを見るとnumbaでいいじゃんとなる・・・
Python を高速化する Numba, Cython 等を使って Julia Micro-Benchmarks してみた - Qiita

int16やint8にすれば多少速くなりますが、8bitだと128個1が続いただけでオーバーフローなので都合が悪いでしょう。


行方向走査で、配列のコピーをやめてインプレース処理にすると関数自体の速度は1秒を切ります(0.6くらい)。使いたいかどうかはわかりませんが、一応載せておきます。

python

1@jit("i4[:,:](i4[:,:])", nopython=True) 2def f2_i(B): 3 for i in range(B.shape[0]): # 行のループ 4 before = 0 5 start_pos = 0 6 cnt = 0 7 for j in range(B.shape[1]): # 列のループ 8 if B[i, j] == 1: # 1のとき 9 if before == 0: # 直前の状態が0なら1にして数え始める 10 start_pos = j 11 before = 1 12 cnt += 1 # 数える 13 else: # 0のとき 14 if before != 1: # 直前の状態が0なら無視して続ける 15 continue 16 else: # 直前の状態が1のとき 17 # 1が出た範囲をcntで埋める 18 B[i, start_pos:j] = cnt 19 20 # 状態をリセットする 21 before = 0 22 cnt = 0 23 # 行が終わって状態が1のとき 24 if before == 1: 25 B[i, start_pos:B.shape[1]] = cnt 26 return B 27

逆に言うとコピーに時間がかかると思うので、新しい配列を返すつもりであれば本体の処理の高速化で受けられる恩恵はそんなにないのかもしれません。

投稿2020/04/06 18:03

編集2020/04/06 21:38
hayataka2049

総合スコア30935

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

s_miyazaki

2020/04/06 23:21

回答ありがとうございます! 説明もコードも大変分かりやすく、助かりました! 入力する行列は、関数を通した後は使わないのでin place処理で大丈夫です。 自分のノートPCでin place処理のコードを実行してみたところ、0.7sほどとなっていて、大幅な高速化ができていました。 これから細部の確認をしていきたいと思います。
hayataka2049

2020/04/06 23:59

速いのは転置して行ベクトルにした場合のものです。列ベクトル版も同様に作れますが、そこそこ遅くなるかと思います。その点はご留意ください。
guest

0

for文を使わないようにしたらちょっとはやくなりました

python

1Matrix = np.random.randint(0, 2, (2*10**4, 2*10**4), dtype=int) 2Matrix[[0, -1], :] = 0 3 4# %% 5Matrix1d = Matrix.ravel('F') 6repeat = np.ediff1d(np.concatenate( 7 ([True], Matrix1d[1:] != Matrix1d[:-1], [True])).nonzero()[0]) 8base_array = repeat.copy() 9base_array[::2] = 0 10result_Matrix = base_array.repeat(repeat).reshape(Matrix.shape).T

投稿2020/04/06 01:00

編集2020/04/06 01:04
kirara0048

総合スコア1399

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

s_miyazaki

2020/04/06 03:58

回答ありがとうございます! ①元の行列を1つのベクトルVにする ②Vと、Vの要素を1つずらしたベクトルV1を利用してVにおける0と1の切り替わる場所(I)を検出する ③Iを利用して、0または1が連続する回数を求める(repeat) ④Vの1つ目の要素は0と決まっているのでrepeatを使えば、Vのk連続の1をkに置換したベクトル(U)が得られる ということですね! 実行時間は40秒ほどとなりましたが、k連続の1をkに置換する別の考え方を知ることができました。ありがとうございました。
guest

0

  • Cythonについては解説記事がたくさんありますので、それらを読めばCの知識がなくても大丈夫かと思います。(複雑な処理をする場合は必要になるかもしれませんが)

  • 並列処理については「引数の二乗を返す関数」とありますが、値を返す処理が書かれていません。セルの処理が終わらない原因だと思います。(※windowsのjupyter notebookにおいて)


【追記】
windowsにおいてjupyter notebook では並列処理中のprint()が表示されませんね。処理自体はmap()の戻り値を受け取れば可能です。print()したい場合はターミナルで実行するなど他の方法でスクリプトを実行してみてください。

投稿2020/04/05 01:32

編集2020/04/05 01:47
meg_

総合スコア10760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

s_miyazaki

2020/04/06 04:00

回答ありがとうございます! Cythonの取り入れ方について、調べてみようと思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問