为什么这种带有记忆功能的LCS(最长公共子序列)Python实现的性能很差?
我正在学习动态规划,偶然发现了LCS(最长公共子序列)算法 我已经用Python实现了它的几个版本,以了解实现之间的差异以及它们的性能 代码如下:为什么这种带有记忆功能的LCS(最长公共子序列)Python实现的性能很差?,python,recursion,dynamic-programming,memoization,lcs,Python,Recursion,Dynamic Programming,Memoization,Lcs,我正在学习动态规划,偶然发现了LCS(最长公共子序列)算法 我已经用Python实现了它的几个版本,以了解实现之间的差异以及它们的性能 代码如下: import time import sys sys.setrecursionlimit(50000) def current_milli_time(): return time.time() * 1000 def memoize_decorator(fn): cache = {} def inner_fn(*args):
import time
import sys
sys.setrecursionlimit(50000)
def current_milli_time(): return time.time() * 1000
def memoize_decorator(fn):
cache = {}
def inner_fn(*args):
if args in cache:
return cache[args]
ret = fn(*args)
cache[args] = ret
return ret
inner_fn.__name__ = fn.__name__
inner_fn.__doc__ = fn.__doc__
return inner_fn
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
class TestORedAssertValue:
def __init__(self, *ored_assert_values):
self.__values = ored_assert_values
def assert_value(self, value):
for self_value in self.__values:
if self_value == value:
return True
return False
def values(self):
return list(self.__values)
def test_assert(label, value, assertValue, *args):
ok = False
assertToPrint = assertValue
if isinstance(assertValue, TestORedAssertValue):
ok = assertValue.assert_value(value)
assertToPrint = " OR ".join(map(str, list(assertValue.values())))
elif value == assertValue:
ok = True
if ok:
print('#', label, ' - ', bcolors.OKGREEN, 'ok', bcolors.ENDC, sep='')
else:
print('#', label, ' - args: ', ", ".join(map(str, args)), ', expected: ', assertToPrint,
', got: ', value, ' - ', bcolors.FAIL, 'fail', bcolors.ENDC, sep='')
def measure_time_decorator(fn):
def inner(*args):
start = current_milli_time()
ret = fn(*args)
end = current_milli_time()
print('time: ', end - start, ' ms', sep='', end=' - ')
return ret
inner.__name__ = fn.__name__
inner.__doc__ = fn.__doc__
return inner
def test(fn):
print()
print("Testing:", fn.__name__)
fn = measure_time_decorator(fn)
A = ['A', 'C', 'B', 'D', 'E', 'G', 'C', 'E', 'D', 'B', 'G']
B = ['B', 'E', 'G', 'C', 'F', 'E', 'U', 'B', 'K']
assertRes = ['B', 'E', 'G', 'C', 'E', 'B']
res = fn(A, B)
test_assert('testcase 1', res, assertRes, A, B)
A = ['A', 'B']
B = []
assertRes = []
res = fn(A, B)
test_assert('testcase 2', res, assertRes, A, B)
A = []
B = []
assertRes = []
res = fn(A, B)
test_assert('testcase 3', res, assertRes, A, B)
A = [1, 2]
B = [1, 2]
assertRes = [1, 2]
res = fn(A, B)
test_assert('testcase 4', res, assertRes, A, B)
A = [1, 2]
B = [1, 2]
assertRes = [1, 2]
res = fn(A, B)
test_assert('testcase 5', res, assertRes, A, B)
A = ['A', 'B', 'C', 'E', 'F', 'G', 'H', 'I', 'L']
B = ['A', 'B', 'C', 'E', 'F', 'G', 'H', 'I', 'L']
assertRes = ['A', 'B', 'C', 'E', 'F', 'G', 'H', 'I', 'L']
res = fn(A, B)
test_assert('testcase 6', res, assertRes, A, B)
A = [x for x in range(3000)]
B = A
assertRes = A
res = fn(A, B)
test_assert('testcase 7', res, assertRes, A, B)
A = [x for x in range(3000)]
B = list(reversed(A))
assertRes = TestORedAssertValue([0], [2999])
res = fn(A, B)
test_assert('testcase 8', res, assertRes, A, B)
def longest_common_subsequence(A, B):
N = len(A)
M = len(B)
res_matrix = [[[]] * (M + 1) for i in range(N + 1)]
for i in range(1, N + 1):
for j in range(1, M + 1):
if A[i - 1] == B[j - 1]:
res_matrix[i][j] = res_matrix[i - 1][j - 1] + [A[i - 1]]
else:
res_matrix[i][j] = res_matrix[i][j - 1] if (
len(res_matrix[i][j - 1])
>
len(res_matrix[i - 1][j])
) else res_matrix[i - 1][j]
return res_matrix[-1][-1]
def longest_common_subsequence_recursive_memoized(A, B):
N = len(A)
M = len(B)
if N <= 0 or M <= 0:
return []
res_matrix = [[[]] * M for i in range(N)]
@memoize_decorator
def recursion(i, j):
if i <= -1 or j <= -1:
return []
elif A[i] == B[j]:
res_matrix[i][j] = recursion(i - 1, j - 1) + [A[i]]
else:
prev1 = recursion(i - 1, j)
prev2 = recursion(i, j - 1)
res_matrix[i][j] = prev1 if (
len(prev1)
>
len(prev2)
) else prev2
return res_matrix[i][j]
recursion(N - 1, M - 1)
return res_matrix[-1][-1]
def longest_common_subsequence_recursive_memoized_mit(A, B):
N = len(A)
M = len(B)
if N <= 0 or M <= 0:
return []
res_matrix = [[None] * M for i in range(N)]
def lcs(i, j):
if i <= -1 or j <= -1:
return []
if res_matrix[i][j] == None:
if A[i] == B[j]:
res_matrix[i][j] = lcs(i - 1, j - 1) + [A[i]]
else:
prev1 = lcs(i - 1, j)
prev2 = lcs(i, j - 1)
res_matrix[i][j] = prev1 if (
len(prev1)
>
len(prev2)
) else prev2
return res_matrix[i][j]
return lcs(N - 1, M - 1)
if __name__ == "__main__":
test(longest_common_subsequence_recursive_memoized)
test(longest_common_subsequence_recursive_memoized_mit)
test(longest_common_subsequence)
print()
有趣的部分是testcase8
。
您可以看到,对于这个测试用例,longest\u common\u subsequence\u recursive\u memorized
执行得很差(105788.96801757812 ms~=105.8秒
),而对于其他两个函数,它只需要不超过30秒(longest\u common\u subsequence
是最好的一个,大约需要10秒完成)
我的问题是:longest\u common\u subsequence\u recursive\u memorized
为什么对testcase 8执行得如此糟糕,而实现与longest\u common\u subsequence\u recursive\u memorized\u mit
非常相似
尽管如此,它仍然使用记忆,而不是直接访问res_matrix
并从中返回值,它使用一个装饰器包装递归函数来缓存计算结果,并在需要之前已经计算过的计算时立即返回
谢谢你的关注
编辑:经过几次尝试,我发现性能问题似乎与@memoize\u decorator
功能有关
如果我重写longest\u common\u subsequence\u recursive\u memorized
函数,添加longest\u common\u subsequence\u recursive\u memorized\u mit
中使用的等效测试(mit版本):
如果我评论@memoize\u decorator
行:
def longest_common_subsequence_recursive_memoized(A, B):
N = len(A)
M = len(B)
if N <= 0 or M <= 0:
return []
res_matrix = [[None] * M for i in range(N)]
# @memoize_decorator <--- Comment
def lcs(i, j):
if i <= -1 or j <= -1:
return []
if res_matrix[i][j] == None:
if A[i] == B[j]:
res_matrix[i][j] = lcs(i - 1, j - 1) + [A[i]]
else:
prev1 = lcs(i - 1, j)
prev2 = lcs(i, j - 1)
res_matrix[i][j] = prev1 if (
len(prev1)
>
len(prev2)
) else prev2
return res_matrix[i][j]
lcs(N - 1, M - 1)
return res_matrix[-1][-1]
因此,我猜性能缺陷来自memoize\u decorator
,好像我删除了它,就获得了性能增益。奇怪的是,记忆会加速重复计算
但是在这个修改后的最长的\u公共的\u子序列\u递归的\u记忆化的
函数中,如果它的内部函数是用记忆化的
修饰符来记忆的,则实际上并不重要,此时,因为通过res\u矩阵和如果res\u矩阵[i][j]==None:
测试,最长的\u公共\u子序列\u递归\u记忆
已经单独使用了记忆
因此,我的最终诊断是,~95秒
与内部fn中的代码相关:
...
if args in cache:
return cache[args]
ret = fn(*args)
cache[args] = ret
return ret
...
另一方面,如果我将最长的\u公共的\u子序列\u递归的保留为以前的但是没有@memoize\u decorator
(将其重命名为最长的\u公共的\u子序列\u递归的
):
def longest\u common\u subsequence\u recursive(A,B):
N=len(A)
M=len(B)
如果N
我的问题是:为什么最长的\u公共的\u子序列\u递归的\u记忆
当实现非常复杂时,testcase 8的性能非常差
与最长\u公共\u子序列\u递归\u记忆\u mit类似
我发现两者之间的主要性能差异在于MIT版本具有:
res_matrix = [[None] * M for i in range(N)]
...
if res_matrix[i][j] == None:
而您的使用:
res_matrix = [[[]] * M for i in range(N)]
没有等效的测试。如果我们修改您的应用程序以合并与MIT相同的初始化和测试,并使用Python自己的rlu_cache()
从functools进行修饰,以便我们可以查询缓存,我们将得到:
CacheInfo(hits=380620, misses=17610383, maxsize=128, currsize=128)
time: 19195.7060546875 ms - #testcase 8 - ok
将缓存大小从默认值128增加可提高性能:
CacheInfo(hits=8988004, misses=9002999, maxsize=4096, currsize=4096)
time: 16645.57080078125 ms - #testcase 8 - ok
但只到了某一点,就没有什么区别了。谢谢你的回复<代码>当您使用时:。。。没有等效测试。
谢谢您的回复。然后我想到的问题是,为什么缓存中的if args:
在@memoize\u decorator(recursion)
中的最长公共子序列\u recursive\u recursive
与if res\u矩阵[i][j]的工作原理几乎相同==无:
最长\u公共\u子序列\u递归\u记忆\u mit中的条件
?即使memoizedrecusion
函数返回将返回值保存在缓存中,缓存似乎也不会提高执行速度。。。这是我仍然不明白的主要区别。你能提供一个例子说明这两种方法之间的区别吗?我再次尝试使用代码,似乎开销来自@memoize\u decorator
decorator函数。即使我将最长的\u公共的\u子序列\u递归的\u记忆化
与MIT版本相同,如果我用@memoize\u decorator
包装内部的递归()
函数,完成testcase 8需要90秒……如果我注释@memoize\u decorator
行,然后在25秒内运行最长的\u公共\u子序列\u递归\u记忆(修改为与MIT函数相同,如果res\u矩阵[i][j]==None:
测试,则使用等效的)。。。这可能是因为memoize\u decorator
中使用了缓存
数据结构吗?
def longest_common_subsequence_recursive(A, B):
N = len(A)
M = len(B)
if N <= 0 or M <= 0:
return []
res_matrix = [[[]] * M for i in range(N)]
# @memoize_decorator <--- Without memoization
def recursion(i, j):
if i <= -1 or j <= -1:
return []
elif A[i] == B[j]:
res_matrix[i][j] = recursion(i - 1, j - 1) + [A[i]]
else:
prev1 = recursion(i - 1, j)
prev2 = recursion(i, j - 1)
res_matrix[i][j] = prev1 if (
len(prev1)
>
len(prev2)
) else prev2
return res_matrix[i][j]
recursion(N - 1, M - 1)
return res_matrix[-1][-1]
res_matrix = [[None] * M for i in range(N)]
...
if res_matrix[i][j] == None:
res_matrix = [[[]] * M for i in range(N)]
CacheInfo(hits=380620, misses=17610383, maxsize=128, currsize=128)
time: 19195.7060546875 ms - #testcase 8 - ok
CacheInfo(hits=8988004, misses=9002999, maxsize=4096, currsize=4096)
time: 16645.57080078125 ms - #testcase 8 - ok