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

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

新規登録して質問してみよう
ただいま回答率
85.48%
Python 3.x

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

3回答

2067閲覧

【Python】並列処理の高速化について

reishisu

総合スコア39

Python 3.x

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2018/07/24 13:57

編集2018/07/25 16:13

前提・実現したいこと

以前、こちらの記事で質問させていただいたのですが補足でご指摘いただいた通りに粒度を大きくしてみたのですが全然高速化できておりません、、、

ですので、何処がボトルネックになっているかご指摘と改善出来るならアドバイス頂けるとありがたいです。

個人的には、並列化した時の通信時間よりforの方が時間がかかっている感じがするのでこのような結果になったのではないかなぁと推測しております。

元のソースコード

Python3

1import concurrent.futures 2import matplotlib.pyplot as plt 3import numpy as np 4import cv2, sys, os 5 6img = cv2.cvtColor(cv2.imread(sys.argv[1]), cv2.COLOR_BGR2RGB) 7useCPU = 1 8 9def main(): 10 """ 11 メイン関数 12 """ 13 try: 14 useCPU = int( input("使用するCPUのコアを入力してください[ 1 ~ {0} ] : ".format(os.cpu_count())) ) 15 except: 16 useCPU = os.cpu_count() 17 if useCPU > os.cpu_count(): 18 useCPU = os.cpu_count() 19 mulchProcess(useCPU=useCPU) 20 plt.imshow(img) 21 plt.show() 22 23def changeToGray( number: int, width: np.ndarray ): 24 """ 25 並列化する処理 ( グレースケールに変換する ) 26 @param number (int) : このプロセスの番号 27 @param width (np.ndarray) : 横1行の配列[ [R, G, B], ・・・・ ,[R, G, B] ] 28 @return number (int) : このプロセスの番号 29 @return width (np.ndarray) : 引数で受け取った配列をグレースケールに変換した配列 30 """ 31 for pixel in width: 32 gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11) 33 pixel[0] = gray 34 pixel[1] = gray 35 pixel[2] = gray 36 return number, width 37 38def mulchProcess(useCPU: int): 39 """ 40 マルチコアでプロセスを生成して実行させる処理 41 @param useCPU (int) : 使用するCPUのコア数 42 """ 43 with concurrent.futures.ProcessPoolExecutor(max_workers=useCPU) as executer: 44 fs = [ executer.submit(changeToGray, i, width) for width, i in zip( img, range(len(img)) ) ] 45 for future in concurrent.futures.as_completed(fs): 46 line_number = future.result()[0] 47 gray_width = future.result()[1] 48 img[line_number] = gray_width 49 50if __name__ == '__main__': 51 main()

直したソースコード

Python3

1import concurrent.futures 2import matplotlib.pyplot as plt 3import numpy as np 4import cv2, sys, os 5 6img = cv2.cvtColor(cv2.imread(sys.argv[1]), cv2.COLOR_BGR2RGB) 7useCPU = 1 8step = 1 9 10def main(): 11 """ 12 メイン関数 13 """ 14 try: 15 useCPU = int( input("使用するCPUのコアを入力してください[ 1 ~ {0} ] : ".format(os.cpu_count())) ) 16 except: 17 useCPU = os.cpu_count() 18 if useCPU > os.cpu_count(): 19 useCPU = os.cpu_count() 20 step = int( len(img) / useCPU ) 21 mulchProcess(useCPU=useCPU, step= step) 22 plt.imshow(img) 23 plt.show() 24 25def changeToGray( number: int, length: int ): 26 """ 27 並列化する処理 28 @param number (int) : 画像の処理対象範囲の先頭の添字 29 @param length (int) : 対象範囲の長さ 30 @return number (int) : 画像の処理対象範囲の先頭の添字 31 @return part_height (int) : 処理後の画像の配列 32 """ 33 endPioint = number + length if number + length < len(img) else len(img) - 1 34 part_height = img[ number : endPioint-1 ] 35 for width in part_height: 36 for pixel in width: 37 gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11) 38 pixel[0] = gray 39 pixel[1] = gray 40 pixel[2] = gray 41 return number, part_height 42 43def mulchProcess(useCPU: int, step: int): 44 """ 45 マルチコアでプロセスを生成して実行させる処理 46 @param useCPU (int) : 使用するCPUのコア数 47 @param step (int) : 画像の高さをコア数で割った数 48 """ 49 index_list = [ i for i in range(0, len(img), step) if i < len(img) ] 50 with concurrent.futures.ProcessPoolExecutor(max_workers=useCPU) as executer: 51 fs = [ executer.submit(changeToGray, i, step) for i in index_list ] 52 for future in concurrent.futures.as_completed(fs): 53 line_number = future.result()[0] 54 part_height = future.result()[1] 55 for i, height in zip( range(line_number, line_number+len(part_height)), part_height ): 56 img[i] = height 57 58if __name__ == '__main__': 59 main()

試したこと

横1列ごとのピクセルで処理していたのを「横*(高さ/使用するコア数)」ごとに処理をさせるようにして通信時間を使用するコア数に削減させたつもりです....

補足情報(FW/ツールのバージョンなど)

[実行環境]
MacBook Pro (15-inch, 2016)
プロセッサ : 2.6 GHz Intel Core i7 ( 4コア8スレッド )
メモリ : 16 GB 2133 MHz LPDDR3
Python 3.6.4

[実行時間]
使用画像 : 5000px*5025px (15.1MB)
修正前のコード : 44.79758310317993秒
修正後のコード : 45.401297092437744秒

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

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

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

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

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

mkgrei

2018/07/24 14:12

ボトルネックとおっしゃるので、とりあえず今の処理でどこでどれほどの時間をかけているのかの情報を追記していただけませんか?
reishisu

2018/07/24 14:18

申し訳ございません、何処でどれほどの時間とは変えた部分の実行速度でよろしいでしょうか?
退会済みユーザー

退会済みユーザー

2018/07/24 14:50

全部ですよ
退会済みユーザー

退会済みユーザー

2018/07/24 14:51

それぞれの行にどのくらいの時間がかかっているかです
reishisu

2018/07/24 14:57

では、mulchProcessより前はほとんど処理が同じなので入ってから終わるまでの実行時間を追記させて頂きますね。
reishisu

2018/07/24 15:02

使用画像とコードごとの実行時間について追記させていただきました。
umyu

2018/07/24 15:23 編集

@reishisuさんへ 前の質問を見る限りではグレースケール化したいだけじゃないですよね。実際のコードとかけ離れたコードをチューニングしても無意味なのでグレースケール以外にもしている実際の処理を載せてくださいな
reishisu

2018/07/24 15:47 編集

実際にはこれだけでグレースケール化をしておりますが、、、前の質問ではまだプロトタイプ版で尚且つ質問の趣旨が違うので確か前のコードと少し違うかもしれないですがほとんど時間計測の処理と標準出力の部分を削除しているだけですよ。
reishisu

2018/07/24 15:44 編集

あとは、前の質問でもあった通りスレッドの部分は必要がないとのことでしたので実際にも使わないのと、こちらで投稿するときに要らないところまで書くと見辛いので削除しております。
reishisu

2018/07/24 15:51

回答になっておりますか?もし、別のところでしたらどちらの部分か教えて頂けるとありがたいです。
umyu

2018/07/24 15:56

前も似たようなコメントをしたのですが、ProcessPoolExecutorを使わずともcv2.cvtColor(img, cv2.COLOR_RGB2GRAY)を使うとグレースケール化は0.046秒で終わるんですよね。
umyu

2018/07/24 15:58 編集

ようするに、グレースケール化以外にも処理をしたいから、ProcessPoolExecutorを使うという話を仰られてたじゃないですか。でもこれはグレースケール部分のコードしかないので、グレースケール化以外部分がボトルネックにもしもなっていても、このコードからはわかんないという話です。
reishisu

2018/07/24 16:00

確かに、それを使えば早いのはOpenCV使ってるのでそれに関しては100の承知ですが。今回はそれを踏まえた上で並列処理を利用してどれだけ逐次処理処理と比べて高速化できるかという所をやろうとしております....
umyu

2018/07/24 16:00

例えば、numpyなら1バイト単位にループで演算しなくてもベクトル演算が使えると思いますし
umyu

2018/07/24 16:05

@reishisuさんへ なんとなくやりたいことが理解できました。コメント欄お邪魔して失礼致しました。
guest

回答3

0

ベストアンサー

基本的に numpy の配列に対する演算は numpy で完結させたほうが速いです。

提示の「直したソースコード」は動かないので**「元のソースコード」**で、
changeToGray を numpy で完結する処理に変更した例を示します。

python

1def changeToGray( number: int, width: np.ndarray ): 2 """ 3 並列化する処理 ( グレースケールに変換する ) 4 @param number (int) : このプロセスの番号 5 @param width (np.ndarray) : 横1行の配列[ [R, G, B], ・・・・ ,[R, G, B] ] 6 @return number (int) : このプロセスの番号 7 @return width (np.ndarray) : 引数で受け取った配列をグレースケールに変換した配列 8 """ 9 return number, np.tile((width * [0.3, 0.59, 0.11]).astype(np.int).sum(axis=1), (3, 1)).T

環境はだいぶ違いますが、Windows 10 (Core i5)、4 コア(論理プロセッサの最大)で比較したところ
5472x3648 ピクセルの画像は、約 130 秒が 5 秒に、
1920x1200 ピクセルの画像は、約 9 秒が 2 秒に短縮しました。


[以降追記]
まずは、NumPy についてです。

NumPy - Wikipedia

目的
Pythonは動的型付け言語であるため、プログラムを柔軟に記述できる一方で、純粋にPythonのみを使って数値計算を行うと、ほとんどの場合C言語やJavaなどの静的型付き言語で書いたコードに比べて大幅に計算時間がかかる。そこでNumPyは、Pythonに対して型付きの多次元配列オブジェクト (numpy.ndarray) と、その配列に対する多数の演算関数や操作関数を提供することにより、この問題を解決しようとしている。NumPyの内部はC言語 (およびFortran)によって実装されているため非常に高速に動作する。したがって、目的の処理を、大きな多次元配列(ベクトル・行列など)に対する演算として記述できれば(ベクトル化できれば)、計算時間の大半はPythonではなくC言語によるネイティブコードで実行されるようになり大幅に高速化する。

したがって、以下のように配列の 1 要素ごとに Python で演算を行っていたのでは NumPy を活かすことができません。

Python

1def changeToGray( number: int, width: np.ndarray ): 2 for pixel in width: 3 gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11) 4 pixel[0] = gray 5 pixel[1] = gray 6 pixel[2] = gray 7 return number, width

np.tile((width * [0.3, 0.59, 0.11]).astype(np.int).sum(axis=1), (3, 1)).T は、
“配列に対する多数の演算関数や操作関数”を使用するように置き換えたものです。

NumPy では +* などの演算子は、配列同士の演算ができるように定義されています。
以下のように、配列 ab に対して、a + b を行った結果は、同じ位置の要素同士を加算したものになります。

Python

1>>> a = np.arange(6).reshape(2, 3) 2>>> a 3array([[0, 1, 2], 4 [3, 4, 5]]) 5 6>>> b = np.ones((2, 3)) 7>>> b 8array([[1., 1., 1.], 9 [1., 1., 1.]]) 10 11>>> a + b 12array([[1., 2., 3.], 13 [4., 5., 6.]])

また、「ブロードキャスト」といって、長さが異なる配列の演算は、不足している要素を、ルールにしたがって自動的に補完してくれます。詳しくは「NumPy ブロードキャスト」などで検索してください。

Python

1>>> c = np.array([3, 5, 7]) 2>>> c 3array([3, 5, 7]) 4 5>>> a + c 6array([[ 3, 6, 9], 7 [ 6, 9, 12]]) 8 9>>> d = np.array([5]) 10>>> d 11array([5]) 12 13>>> a + d 14array([[ 5, 6, 7], 15 [ 8, 9, 10]])

つまり、width * [0.3, 0.59, 0.11] は、以下の処理と等価です。

Python

1for pixel in width: 2 gray = pixel[0]*0.3 + pixel[1]*0.59 + pixel[2]*0.11

この結果に対して、.astype(np.int) は配列の int 変換を、.sum(axis=1) は 1 次元の要素同士の合計(つまりR+G+B)を求めています。
そして、その合計を np.tile によって、元の長さに戻しています。

Python

1>>> e = np.tile(np.arange(5), (3, 1)) 2>>> e 3array([[0, 1, 2, 3, 4], 4 [0, 1, 2, 3, 4], 5 [0, 1, 2, 3, 4]])

ただし、このままだと、0次元目と1次元目が入れ替わってしまっているため、.T で転置しています。

Python

1>>> e.T 2array([[0, 0, 0], 3 [1, 1, 1], 4 [2, 2, 2], 5 [3, 3, 3], 6 [4, 4, 4]])

投稿2018/07/25 15:07

編集2018/07/26 14:05
copepoda

総合スコア324

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

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

reishisu

2018/07/25 16:24

大変申し訳ありません、引数や名前を色々修正できておりませんでした。。。 現在は修正して実行の確認もOKです。。。。 ファッ!これはとても凄いですね!! 私の環境では8コアで8000×8000の処理が5秒で終わりました(*_*; お手数ではございますが私の理解力が足りないのでどうか戻り値の部分解説いただけないでしょうか><
reishisu

2018/07/25 17:28

ちなみに修正した「直したソースコード」のところを for width in part_height: width = np.tile((width * [0.3, 0.59, 0.11]).astype(np.int).sum(axis=1), (3, 1)).T # for pixel in width: # gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11) # pixel[0] = gray # pixel[1] = gray # pixel[2] = gray のように修正したら、何も実行されずに終了しました、、、
copepoda

2018/07/26 14:25 編集

回答に解説を追記しました。 「直したソースコード」の方は、width 経由で part_height の中を更新する必要があるので、 width[...] = np.tile((width * [0.3, 0.59, 0.11]).astype(np.int).sum(axis=1), (3, 1)).T です。 width 自体を置き換えるのではなく、width[...] で中身を置き換えます。
reishisu

2018/07/26 15:28

丁寧な解説ありがとうございました!! Python始めたばかりでなんでわざわざ関数呼び出しなんてしてるんだろうと思っていたらNumpyはCで実装されていたんですね>< Numpyはとても奥深いのを知ったのと線形代数をプログラミングで使うの初めてでちょっと慣れるまで時間がかかりそうです笑 疑問なのですが、Pythonって動的型付け言語なのでwidthで出来そうなのですが何故width[...]じゃ無いとできないのでしょうか??
reishisu

2018/07/26 15:29

あ、あとフォロー返していただきありがとうございます!!
reishisu

2018/07/26 15:44 編集

度々質問ばかりで申し訳ないですが、解説を読ませていただいて「直したコード」のところにあるmulchProcess関数の最後のところで for i, height in zip( range(line_number, line_number+len(part_height)), part_height ): img[i] = height と書いており、line_numberで画像の高さの初めの位置を保存しており戻り値の配列をそのまま先頭から代入しております。 img[ line_number : line_number+len(part_height) ] = part_height forを消してこのように出来ればと考えておりますが、シンタックスエラー出てしまいます。 ですので、どの様に実装すれば良いか教えて頂けないでしょうか....?
reishisu

2018/07/26 16:00 編集

大変申し訳ありません、カッコの数が多いだけでした。 おかげさまで8000×8000の画像が「元のコード」でも5秒でかなり高速化されていたのに「直したコード」で実行するとさらに1.7秒まで大幅に短縮することが出来ました!!! 本当にありがとうございました!
guest

0

測定したい時間は以下のものです。

python

1def changeToGray( number: int, width: np.ndarray ): 2 ココから 3 本来のコード 4 ココまで 5 return number, part_height

普通に並列処理させたら、普通に速くなりました。

「直したソースコード」が誤っていますが、実行すべきコードを実行できていますか?


私の手元では以下のコードのuseCPUを変えると正常にスケールします。

python

1import concurrent.futures 2import numpy as np 3import time 4 5img = np.ones(shape=(500,5000,3)) 6 7def main(): 8 useCPU = 4 9 step = int(len(img) / useCPU ) 10 mulchProcess(useCPU=useCPU, step=step) 11 12def changeToGray(number: int, length: int): 13 s = time.time() 14 endPioint = number + length if number + length < len(img) else len(img) - 1 15 part_height = img[number:endPioint-1] 16 for width in part_height: 17 for pixel in width: 18 gray = int(pixel[0]*0.3) + int(pixel[1]*0.59) + int(pixel[2]*0.11) 19 pixel[0] = gray 20 pixel[1] = gray 21 pixel[2] = gray 22 t = time.time() - s 23 return number, part_height, t 24 25def mulchProcess(useCPU: int, step: int): 26 index_list = [i for i in range(0, len(img), step)] 27 with concurrent.futures.ProcessPoolExecutor(max_workers=useCPU) as executer: 28 fs = [executer.submit(changeToGray, i, step) for i in index_list] 29 for future in concurrent.futures.as_completed(fs): 30 line_number = future.result()[0] 31 part_height = future.result()[1] 32 t = future.result()[2] 33 print(t) 34 for i, height in zip(range(line_number, line_number+len(part_height)), part_height): 35 img[i] = height 36 37s = time.time() 38main() 39print(time.time()-s)

時間は以下のようになります。
手元のPCは4coreなので、8個使おうとしても一度に4つずつしか実行できません。
それらがキューに乗せられて順番に実行されるわけではなく、スイッチしながら実行されるので、プロセスあたり2倍の時間がかかり、結局全体の時間は同じになります。
むしろスイッチした分だけオーバーヘッドがあって全体の実行時間が長くなっています。

useCPU = 1 3.6020491123199463 total 4.26886773109436 useCPU = 2 1.7662358283996582 1.7804999351501465 total 2.166551113128662 useCPU = 4 0.9876649379730225 0.9972729682922363 1.0097663402557373 1.0141608715057373 total 1.2486088275909424 useCPU = 8 0.9724259376525879 0.9717621803283691 0.973222017288208 0.97617506980896 0.9814469814300537 0.9806389808654785 0.9841761589050293 0.9821760654449463 0.015022039413452148 total 1.2429370880126953

投稿2018/07/25 06:39

編集2018/07/25 21:57
mkgrei

総合スコア8560

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

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

reishisu

2018/07/25 16:32 編集

大変申し訳ありません、引数や名前を色々修正できておりませんでした。。。 修正しましたので、実行結果を貼らせて頂きます>< ーー 8コア ーー finish time : 41.220848083496094 finish time : 3.910064697265625e-05 finish time : 41.38852524757385 finish time : 41.44024705886841 finish time : 41.493447065353394 finish time : 41.56655478477478 finish time : 41.57504391670227 finish time : 41.58701515197754 finish time : 41.611690044403076
reishisu

2018/07/26 08:50 編集

今気が付いたのですが、何故か2行目の結果がおかしい事を確認しました.... 5000*5000では謎の行が出て来て4000*3000では正しく出力されるので現在原因を調査中でございます
guest

0

何処がボトルネックになっているか

a,numpyはループでピクセル単位に処理を行うとものすごーく時間がかかるので。
あまりテストできてませんが。

How can I convert an RGB image into grayscale in Python?を参考に。

Python

1from time import perf_counter 2 3def changeToGray_beta( number: int, length :int): 4 st_time = perf_counter() 5 endPioint = number + length if number + length < len(img) else len(img) - 1 6 part_height = img[ number : endPioint-1 ] 7 gray = np.ceil(np.dot(part_height[..., :3], [0.3, 0.59, 0.11])) 8 # 1ch => 3ch 9 part_height = np.stack((gray,) * 3, -1) 10 # 戻り値に実行時間を追加 11 return number, part_height, perf_counter() - st_time

diff

1-for future in concurrent.futures.as_completed(fs): 2- line_number = future.result()[0] 3- part_height = future.result()[1] 4+for future in concurrent.futures.as_completed(fs): 5+ line_number, part_height, exe_time = future.result() 6+ print(exe_time)

※numpyマニアな方なツッコミ待ちです。

b, あとはグレースケール変換部分をcv2.cvtColorを使ったこのような形にでも。

Python

1gray = cv2.cvtColor(part_height, cv2.COLOR_RGB2GRAY)

あと思いつく点としては、changeToGrayの処理で戻り値として3次元(RGB)データを返していますが。
グレースケールデータは1次元で十分なのでas_completed呼び出し側で一部の処理を行えば返す必要も無い気がします。
これに関しては受け渡しするpickleのデータ量とas_completed呼び出し側のCPU時間のトレードオフになるのではないかと、プロファイリングを複数回採ってみた方がよいと思います。


テスト中に気づいたのですが、質問文の元コードだと白色のグレースケール値が254になります。

こんな感じのハッシュ関数を作ってテストコードに入れておくのをお勧め致します。

Python

1def image_hash(img): 2 from hashlib import sha384 3 x = np.ascontiguousarray(img, dtype=np.uint8) 4 print(sha384(x).hexdigest())

◇参考情報
Fast way to Hash Numpy objects for Caching

投稿2018/07/24 18:38

編集2018/07/25 05:05
umyu

総合スコア5846

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

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

mkgrei

2018/07/25 11:31

全く関係ない雑談ですが。 一枚が処理終わるまでの時間を減らさなければならないのでないならば、画像ごとに並列化した方がミスも少なくて良いと思います。 今回のようなケースでは、①並列化すると時間が短縮されるのは自明で、②pythonのマルチプロセスを試すのよい題材でもなく、③opencvライブラリの処理を使うと圧倒的に速く、④numpyのベルトル処理を使っても容易に速くなることを踏まえて、やりたいのことの意図を掴みかねているのですが。 段階的にコードの高速化を実現した、という実績が欲しいのでなければ何も想像力が働かない次第。 ちなみにnp.ceilのデフォルトだと整数型にならないので、場合によっては困るかも。プロットする時にたまに整数を必要とする。 .astype('i')だけでも良いような。±1は誤差でしょ。 マルチプロセスで変数をまるごと共有するとコピーが発生して遅いので、プロセスに渡す時点で画像を先に分割しておくと、少し高速化が望めるかも。 今回の場合、プロセス間でシェア変数にしてもバグらないはず。 ちょっとトリックが必要になりそうですが。
umyu

2018/07/25 12:46

@mkgreiさんへ .astype('i')の件、参考になりました。 指摘ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問