Help us understand the problem. What is going on with this article?

Pythonを速くしたいときにやったこと

前置き

pythonは最近機械学習関連で非常に活躍しているのは周知の事実だと思う
最近話題なのがdeeplearningとか
重たい処理を2万とか3万とかループしたりするので非常に時間がかかったりする
そんなシステムで1回の処理を1秒も減らせれば最終的な恩恵は計り知れないものになる
そういうことを目指して試行錯誤した結果をメモ程度に残しておく

このページではjoblibとかcythonとかは触れません
あくまでライブラリとか関係ないpythonの部分だけ

追記
numbaも試した
https://qiita.com/open_cans/items/b6cde6c28f8eacba0ca1

1.while文はやめておけ!!!

ループをするときはfor文とかwhileとか使うと思う
ひとまず以下のコード実行速度を見て欲しい

whileVSfor.py
import time

start = time.time()
i=0
sumation=0
while(i<1000000):
    sumation += i
    i+=1
elapsed_time = time.time() - start
print ("while_time:{0}".format(elapsed_time) + "[sec]")

start = time.time()
i=0
sumation=0
for i in range(1000000):
    sumation+=i
elapsed_time = time.time() - start
print ("for_time:{0}".format(elapsed_time) + "[sec]")

while_time:0.13159680366516113[sec]
 for_time:0.06858682632446289[sec]

whileは遅い(断言)

2.for文の使い方に注意

2.1 参照の回数に注意

例えば以下のようなリストがあったとする
temp=[1,2,3,4,5,6,7,8,9,10]

1.py
for j in range(100000): 
    for i in temp:
        sumation += i
2.py
for j in range(100000): 
    for i in range(10):
        sumation+=temp[i]

1_time:0.06909036636352539[sec]
2_time:0.11903905868530273[sec]

考えれば当然だけど2は毎回参照しているので遅い

2.2 rangeに入れるなら一時変数

例えばこういうfor文

sample.py
for i in range(len(IMAGE)):
    for i in range(len(IMAGE[0])):

一時変数を入れた時のものと比較してみよう

temp.py
import time
import numpy

image = numpy.ones((1000,1000,3))
#1
start = time.time()
sumation=0
for i in range(len(image)): 
    for j in range(len(image[0])):
        sumation += i
elapsed_time = time.time() - start
print ("1_time:{0}".format(elapsed_time) + "[sec]")

#2
start = time.time()
sumation=0
lengthY=len(image)
lengthX=len(image[0])
for i in range(lengthY): 
    for j in range(lengthX):
        sumation += i
elapsed_time = time.time() - start
print ("2_time:{0}".format(elapsed_time) + "[sec]")

1_time:0.09444594383239746[sec]
2_time:0.0781855583190918[sec]
改善幅は軽微に見えるかもしれないが1000*1000のループで0.01秒も違うと最終的に結構違うので大事にしてほしい

2.3リストを作るなら内包表記にしよう!

[y,x]のリストを作ることを考えよう

2.3.1 基本的な内包表記

例えばこういうやつ

sample.py
list1=[]
for y in range(1000):
    for x in range(1000):
        list1.append([y,x])

こいつを内包表記で書くとこうなる

sample.py
list1 = [[y,x] for y in range(1000) for x in range(1000) ]

速度差はこんなもん
2重for文:0.4291651248931885[sec]
内包表記:0.31380796432495117[sec]

基本的に内包表記のほうが早い

2.3.2 if文がはいるとどうなるの?

sample.py
import time
import numpy

image = numpy.ones((1000,1000,3))

start = time.time()
list1=[]
for y in range(1000):
    for x in range(1000):
        if (y+x)%2==0:
            list1.append([y,x])
elapsed_time = time.time() - start
print ("2重for文time:{0}".format(elapsed_time) + "[sec]")

list1=None

start = time.time()
list1 = [[y,x] for y in range(1000) for x in range(1000) if (y+x)%2==0]
elapsed_time = time.time() - start
print ("内包表記time:{0}".format(elapsed_time) + "[sec]")

とりあえずコードを張る
比較対象は上記コードのうちの

sample.py
for y in range(1000):
    for x in range(1000):
        if (y+x)%2==0:
            list1.append([y,x])

上と下

sample.py
list1 = [[y,x] for y in range(1000) for x in range(1000) if (y+x)%2==0]

2重for文time:0.3176920413970947[sec]
内包表記time:0.2414243221282959[sec]

やっぱり内包表記がナンバーワン

3.if文の条件式にも注意

とりあえずこれを見ていただきたい

sample.py
#1
for i in range(100000):
if i%2==0 or i%(3**4**2):
    sumation+=i

#2
for i in range(100000):
    if i%(3**4**2) and i%2==0:
        sumation+=i

1_time:0.018041372299194336[sec]
2_time:0.012027978897094727[sec]

10回ぐらい計測したものの平均値を出している
ここからわかることはif文の条件式の順番が違うだけで速度に差が出るということ
or条件の時は項が複数あっても1つTrueになれば結果は同じなので最初の項でTrueなら後は見ないということかもしれない

ついでにandもやってみた
1_time:0.015639543533325195[sec]
2_time:0.0156252384185791[sec]

まぁ知ってた

3.1 and条件をor条件に変えてみる

例えば not(A and B) みたいな条件式は not(A) or not(B)で置き換えることができる(ド・モルガンの法則)
上記のor条件の仮定が正しければ
not(A and B)
not(A) or not(B)
の二つを判定する際、下の方が早くなるはずである
ということで検証用のコードをペタリ

sample.py
#1
for i in range(10000000):
    if not(i%(7**3**2)==0 and i%(3**4**2)==0):
        sumation+=i

#2
for i in range(10000000):
    if not(i%(7**3**2)==0) or not(i%(3**4**2)==0):
        sumation+=i

計測時間は以下の通り
1_time:1.3015859127044678[sec]
2_time:1.3581414222717285[sec]

なんか下の方が重い
もしかしてnotで遅くなっているのではないかと両者のnotの数を1にして計測

sample.py
#1
for i in range(10000000):
    if not(i%(7**3**2)==0 and i%(3**4**2)==0):
        sumation+=i

#2
for i in range(10000000):
    if not(i%(7**3**2)==0) or i%(3**4**2)!=0:
        sumation+=i

1_time:1.330047845840454[sec]
2_time:1.32669997215271[sec]

ほんのちょっとだけ2の方が早い
一回あたり0.3ナノ秒ぐらい

2番目にnotが一つも無い状態も計測
1_time:1.3008067607879639[sec]
2_time:1.209193468093872[sec]

どうやらorとかandよりnotの数が計測時間に影響を与えるようだ
ここから想定されることはなるべくnotの数が少なくなるように条件式を変換するのがよさそうだ

4.mapを使おう!!

sample.py
A = '010'
num = int(A)

みたいな感じで変数を変換することがあると思う
こういうときはmapを使うと早くなる

sample.py
import time
import numpy

image = numpy.ones((100000000),dtype=numpy.float32)
sumation=0
length = len(image)
start = time.time()
for i in range(length):
    image[i] = int(image[i])
elapsed_time = time.time() - start
print ("1_time:{0}".format(elapsed_time) + "[sec]")

list1=None

start = time.time()
list(map(int,image))
elapsed_time = time.time() - start
print ("2_time:{0}".format(elapsed_time) + "[sec]")

1_time:33.37748670578003[sec]
2_time:0.0[sec]
2_time:11.241594552993774[sec]
mapを使おう!

4.1 numpy.float32の配列にはmapを使うな!!

上記でmapは基本的に早いと説明したがpythonの実装ミスなのかnumpyの実装ミスなのかnumpy.float32にmapを適用しようとすると遅い。
いっそnumpy.float64に変換した方が良いほどだ

np_float32.py
import numpy as np
from time import time

check = np.arange(0.000001, 0.1, 0.000001) #生成されるデータはnumpy.float64型
change = check.astype(np.float32)
print(len(change ), type(change[0]))

start=time()
list(map(lambda x :x-1, change ))
time()-start
np_float64.py
import numpy as np
from time import time

check = np.arange(0.000001, 0.1, 0.000001)
print(len(check), type(check[0]))

start=time()
list(map(lambda x :x-1, check))
time()-start

np.arangeの出力はnumpy.float64型である。この時の実行速度は
numpy.float32 : 0.17655706405639648
numpy.float64 : 0.029918670654296875

となる。以下にnumpy.float32をnumpy.float64に変換してmapをかけ、numpy.float32に変換した場合の処理を示す。
この場合でもnumpy.float32をそのままmapを使うよりは早い

convert.py
import numpy as np
from time import time

check = np.arange(0.000001, 0.1, 0.000001)
change = check.astype(np.float32)

start=time()
convert = change.astype(np.float64)
temp = np.asarray(list(map(lambda x :x-1, convert)))
convert = temp.astype(np.float32)
convert = list(convert)
time()-start

numpy.float32 → map → numpy.float64 → listの順で実行: 0.04291200637817383

もはや意味が分からない。なお基本的な計算はnumpy.float32の方が早い。
pythonのバージョンの問題の可能性もあるので示すがPython 3.6.5でjupyter notebookでの実行結果である

5.tupleを使おう!

例えば逐次処理されるデータをlistにまとめてpandasに変換して最終結果を出力・・・というような処理の場合はtupleを使うと良い。

1.py
import numpy as np
random_list=np.random.rand(10**7)

start=time()
list_test=[ [i,i] for i in random_list]
b=pd.DataFrame(list_test)
print("list:"str(time()-start))

2.py
import numpy as np
random_list=np.random.rand(10**7)

start=time()
tuple_test=((i,i) for i in random_list)
a=pd.DataFrame(tuple_test)
print("tuple:"str(time()-start))

100回平均の実行速度は以下
list:6.02654489517212
tuple:3.84385318040848

4割近くも縮めることができる。配列のサイズによっても変わってくるのでグラフにする。
aaaaa.png
横軸は配列のサイズ、縦軸は処理時間の100回平均

配列のサイズ tuple list
10^5 0.033 0.062
10^6 0.380 0.588
10^7 3.843 6.026

配列の1サイズに対して処理時間がおおよそ決まっており積もり積もって処理時間に大きな差がうまれるようだ。

まとめ

・whileは使わない
・たくさんアクセスするなら一時変数を使おう?
・リスト作るときにfor文使うなら内包表記を使おう
・if文の条件式を見直そう
・mapを使おう
・appendが必要な時にtupleが使えるなら使おう!

if文とかマイクロ秒レベルの話になってしまったけれどきっと誰かの役に立つと信じて

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした