概要 | |
---|---|
目的 | メモ化を用いたプログラム高速化技法の紹介 part 1 |
プログラミング言語 | Python 3.6.9 (Google Colabのデフォルト) |
実行環境 | Google Colab |
前提 | Pythonのdecorators の概念を知っていること |
参考 | Memoization with Decorators |
1. Memoizationとは?
memorization
ではなくMemoization
(メモ化)であることに注意。Memoizationはto be remembered
を意味する。プログラム高速化を目的としてよく用いられる。
とある関数について、一度呼ばれたinputsとそのinputsに対応するoutputをメモに記録し(メモ化し)、のちに同じinputsでcallされたときにわざわざもう一度計算するのではなく、メモからそのinputsに対応するoutputsを返すことでプログラムの実行時間の短縮を実現する技法がmemoizationです。メモのデータ構造としては、Pythonでは辞書型がよく用いられる。
メモ化は関数の時間コストを領域コストに交換して、時間コスト(実行回数)を低減させる手段です。なので、「メモ化された関数は速度の面で最適化され、記憶装置の領域という面ではより多く消費する」というトレードオフに留意が必要です。実行回数を減らすことができるため、maximum recursion depth制約を回避する手法として用いることができます。
2. Memoizationの実装 in Python
以下のようなフィボナッチrecursive functionを考える:
1
2
3
4
5
6
7
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
これをmemoization機能を加味して書き直すと、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def memoize(f):
memo = {}
def helper(x):
if x not in memo:
memo[x] = f(x)
return memo[x]
return helper
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
fib = memoize(fib)
decoratorを用いて表現すると
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def memoize(f):
memo = {}
def helper(x):
if x not in memo:
memo[x] = f(x)
return memo[x]
return helper
@memoize
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
実行時間の比較
実行環境はGoogle Colabとする。Test codeは以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import time
# Function that computes Fibonacci
# numbers without memoization
def fib_without_cache(n):
if n < 2:
return n
return fib_without_cache(n-1) + fib_without_cache(n-2)
def memoize(f):
memo = {}
def helper(x):
if x not in memo:
memo[x] = f(x)
return memo[x]
return helper
# Function that computes Fibonacci
# numbers with memoization
@memoize
def fib_with_cache(n):
if n < 2:
return n
return fib_with_cache(n-1) + fib_with_cache(n-2)
# Execution start time
begin = time.time()
fib_without_cache(30)
# Execution end time
end = time.time()
print("Time taken to execute the function without memoization is", end-begin)
# Execution start time
begin = time.time()
fib_with_cache(30)
# Execution end time
end = time.time()
print("Time taken to execute the function with memoization is", end-begin)
実行結果は
1
2
Time taken to execute the function without memoization is 0.3091275691986084
Time taken to execute the function with memoization is 9.918212890625e-05
3. Exercise: Ackermann関数とMemoization
ここではメモ化を用いてAckermann関数を実装してみる。Ackermann関数とは
\[A(m, n) =\begin{cases} n + 1 & \text{ if } m = 0\\ A(m-1, 1) & \text{ if } m>0, n = 0\\ A(m-1, A(m, n-1)) & \text{ if } m>0, n > 0 \end{cases}\]で定義されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import time
# Function that computes Ackermann
# numbers without memoization
def ackermann_without_cache(m, n):
if m < 0 or n < 0:
raise ValueError("m, n must be non-negative integers")
elif m == 0:
return n + 1
elif m > 0 and n == 0:
return ackermann_without_cache(m-1, 1)
elif m > 0 and n > 0:
return ackermann_without_cache(m-1, ackermann_without_cache(m, n-1))
def memoize(f):
memo = {}
def helper(m, n):
if (m, n) not in memo:
memo[(m, n)] = f(m, n)
return memo[(m, n)]
return helper
# Function that computes Ackermann
# numbers with memoization
@memoize
def ackermann_with_cache(m, n):
if m < 0 or n < 0:
raise ValueError("m, n must be non-negative integers")
elif m == 0:
return n + 1
elif m > 0 and n == 0:
return ackermann_with_cache(m-1, 1)
elif m > 0 and n > 0:
return ackermann_with_cache(m-1, ackermann_with_cache(m, n-1))
# Execution start time
begin = time.time()
print(ackermann_without_cache(3, 6))
# Execution end time
end = time.time()
print("Time taken to execute the function without memoization is", end-begin)
# Execution start time
begin = time.time()
print(ackermann_with_cache(3, 6))
# Execution end time
end = time.time()
print("Time taken to execute the function with memoization is", end-begin)
実行結果は
1
2
3
4
509
Time taken to execute the function without memoization is 0.03899407386779785
509
Time taken to execute the function with memoization is 0.002206802368164062
(注意:GitHub Accountが必要となります)