今回はPythonの処理速度を数倍にするmultiprocessing(並列化)について解説していきます。
Pythonはライブラリなどを使うことで、複雑な処理が出来るのがメリットですが、デメリットとして処理が遅いという部分があります。
それを解決する方法をここでは紹介していきます。
1枚の画像を処理するプログラム
今回実行するコードとしては、画像を処理するプログラムになります。
まず比較のために1枚の画像を1ピクセルずつ読みこませてRGBの値を合計するプログラムを組んでいきます。
コードはこちらです。
from PIL import Image
import time
import os
def load_single_img(img_path):
img = Image.open(img_path).convert("RGB")
width, height = img.size
pixels = img.load()
total = 0
for y in range(height):
for x in range(width):
r, g, b = pixels[x, y]
total += r + g + b
print(f"画像サイズ: {width} x {height}")
print(f"合計値: {total}")
print(f"{img_path}:完了")
if __name__ == "__main__":
img_folder = 'C:/Users/toshi/multiprocess/img'
start = time.time()
for img_file in os.listdir(img_folder):
img_path = os.path.join(img_folder, img_file)
load_single_img(img_path)
end = time.time()
print(f"処理時間: {end - start:.4f} 秒")
この画像を読みこませるためにpillowというライブラリを使用するので、インストールする必要があります。
インストールはpipで出来ます。
pip install pillow コードがやっている内容としては、
- 変数img_folderに画像が入っているフォルダーパスを格納
- os.listdirで画像のファイル名を取得してfor文で回す
- os.path.joinで画像のフルパスを作成
- load_single_img関数へ引数として画像のフルパスを渡して実行
- pillowで画像を読みこむ
- サイズを取得して、for文でサイズの回数繰り替えす
- RGBの各値をtotalへ足していく
こういった処理になります。
例えば画像のサイズが4000×3000なのであれば、1200万回処理をするという感じです。
そのため1枚の画像でも数秒の時間がかかる処理となります。
実際に4枚の画像の画素数としては、
- 4608 x 3456
- 5184 x 3211
- 6000 x 3364
- 6000 x 3364
こちらで実行すると処理をするのに約13秒かかります。
並列化して時間短縮
これだと時間がかかりすぎてしまうので、これを並列で処理させて実行時間を短縮させていきます。
その時に使うのが、multiprocessingというライブラリになります。
ちなみにこれはPythonの標準ライブラリなので、特にpipなどでインストールせずに使用できます。
from multiprocessing import Pool
from PIL import Image
import time
import os
def load_multi_img(img_path):
img = Image.open(img_path).convert("RGB")
width, height = img.size
pixels = img.load()
total = 0
for y in range(height):
for x in range(width):
r, g, b = pixels[x, y]
total += r + g + b
print(f"画像サイズ: {width} x {height}")
print(f"合計値: {total}")
print(f"{img_path}:完了")
return total
if __name__ == "__main__":
img_folder = 'C:/Users/toshi/multiprocess/img'
# 画像ファイル一覧
img_path_list = []
for img_name in os.listdir(img_folder):
img_path_list.append(os.path.join(img_folder, img_name))
start = time.time()
cpu_cnt = 4
# CPUコア数で並列処理
with Pool(cpu_cnt) as p:
results = p.map(load_multi_img, img_path_list)
end = time.time()
print(f"処理時間: {end - start:.4f} 秒")
load_multi_img関数は、1枚ずつ処理させたload_single_img関数と全く同じものです。
違いは関数を実行する際の、この部分になります。
if __name__ == "__main__":
img_folder = 'C:/Users/toshi/multiprocess/img'
# 画像ファイル一覧
img_path_list = []
for img_name in os.listdir(img_folder):
img_path_list.append(os.path.join(img_folder, img_name))
start = time.time()
cpu_cnt = 4
# CPUコア数で並列処理
with Pool(cpu_cnt) as p:
results = p.map(load_multi_img, img_path_list)
end = time.time()
print(f"処理時間: {end - start:.4f} 秒")
やっている内容として
- img_path_listというリストに画像のファイルパスの一覧を格納
- いくつの並列処理をするかを決定する→上記は4
- with Poolを使用して並列化の準備
- p.mapを使用して並列化で処理をする
p.mapの中に渡す引数としては、
- 実行する関数名
- 関数に渡す引数をリスト
これを渡すことでリストの値を順番に渡して関数を実行することが出来ます。
その時に今回はcpu_cntに4を入れているいるので、4つの処理を同時に実行します。
これを実行すると、約4秒ほどで4枚の画像の処理が終わります。
multiprocessingの注意点
こうやって並列化をして同一の処理を同時に実行することで、処理を何倍も早く終わらせることが出来ます。
ただし注意点として、
- あまり並列の値を増やし過ぎない
- 処理時間が短いものを長いものを一緒にやらない
この2点は注意が必要です。
並列化の数
僕のPCの場合には、10コアのCPUが積んであるので一応理論上は10並列まで可能です。
ただしこのmultiprocessingを使う際には、そもそもが複雑な処理の場合が多いです。
それを限界値までやってしまうとプログラムが落ちてしまう可能性があります。
なので安定して動かしたい場合には、並列の数としては、自分のCPUの数よりもやや少なめがいいと思います。
僕の場合には、だいたい6並列くらいが多いです。
処理時間の差
このmultiprocessingによる並列化というのは、仮に4並列の場合には、
- 4つの処理を実行
- その4つの処理が終わるのを待機
- 次の4つの処理に移行
という形で動いていきます。
そのため処理時間が短い処理と長い処理を混同させてしまうと、恩恵が受けられないです。
極端な例としては、
100×100の画像3枚と10000×100000の画像を1枚処理みたいな時です。
この場合には、100×100の画像の処理が終了したとしても10000×100000の画像の処理が終了するまで次の処理に移行できないです。
なので、multiprocessingで並列化をする場合には、処理時間が一定になるような部分を考えてやってみてください。
ということで、今回はPythonのmultiprocessingを使った並列処理について解説してきました。
Pythonは複雑な処理が出来る一方、複雑な処理に時間がかかるデメリットもあります。
そのデメリットを失くす1つの方法になるので、ぜひ覚えてくださいね。