假设我们有一个点p,例如(1, 2, 3)
,我们要对其应用 N 次线性变换。如果变换由矩阵 A 表示,则最终变换将由A^N . p
给出。矩阵乘法的成本很高,我假设特征分解后对角化会加快整个过程。但令我惊讶的是,这种所谓的改进方法需要更多时间。我在这里错过了什么?
import timeit
mysetup = '''
import numpy as np
from numpy import linalg as LA
from numpy.linalg import matrix_power
EXP = 5 # no. of time linear transformation is applied
LT = 10 # range from which numbers are picked at random for matrices and points.
N = 100 # dimension of the vector space
A_init = np.random.randint(LT, size=(N, N))
A = (A_init + A_init.T)/2
p = np.random.randint(LT, size=N)
def run_sim_1():
An = matrix_power(A, EXP)
return An @ p
def run_sim_2():
λ, V = LA.eig(A)
Λ = np.diag(λ)
Λ[np.diag_indices(N)] = λ ** EXP
An = V @ Λ @ V.T
return An @ p
'''
# code snippet whose execution time is to be measured
# naive implementation
mycode_1 = '''run_sim_1()'''
print(timeit.timeit(setup = mysetup, stmt = mycode_1, number = 1000))
# time taken = 0.14894760597962886
# improved code snippet whose execution time is to be measured
# expecting this to take much less time.
mycode_2 = '''run_sim_2()'''
# timeit statement
print(timeit.timeit(setup = mysetup, stmt = mycode_2, number = 1000))
# time taken = 8.035318267997354
这有点难以权威地回答。矩阵乘法和特征分解的标准实现都是 O(n^3(,因此没有先验的理由期望一个比另一个更快。有趣的是,我的经验是特征分解通常比单个矩阵乘法慢得多,所以这个结果并不完全让我感到惊讶。
因为在这种情况下,矩阵幂运算涉及二十次乘法,所以我可以理解为什么你可能认为它比特征分解慢。但是如果你看一下源代码,就会发现这个有趣的花絮:
# Use binary decomposition to reduce the number of matrix multiplications.
# Here, we iterate over the bits of n, from LSB to MSB, raise `a` to
# increasing powers of 2, and multiply into the result as needed.
z = result = None
while n > 0:
z = a if z is None else fmatmul(z, z)
n, bit = divmod(n, 2)
if bit:
result = z if result is None else fmatmul(result, z)
所以事实上,它并没有真正做 20 次乘法!它使用了一种分而治之的方法,减少了这个数字。在仔细考虑了这个算法之后,它真的非常优雅,我相信它永远不会为给定的幂p
做超过2*log(p)
次乘法。当p
的所有位均为 1 时,即当p
小于 2 的幂时,将达到此最大值。
结果是,尽管特征分解在理论上可能比重复矩阵乘法更快,但它带有恒定的开销,使其效率降低,直到p
变得非常大——可能大于任何实际价值。
我应该补充一点:直接乘以向量不会比将矩阵提高到幂更快吗?二十个向量乘法仍然是O(n^2)
,不是吗?但也许你真正想做的是在 10k 向量上执行此操作,在这种情况下,矩阵幂方法显然更胜一筹。
my_code_1
和my_code_2
都只包含一个def
语句。 因此,您对timeit
的调用只是计时定义函数所需的时间;从不调用这些函数。
将函数定义移动到设置代码中,并仅用调用适当的函数替换要计时的语句,例如
mycode_1 = '''
run_sim_1()
'''
然后,您应该降低(降低很多(传递给timeit
的number
值。 然后,您必须修复run_sim_2()
以执行正确的计算:
def run_sim_2():
λ, V = LA.eig(A)
Λ = np.diag(λ)
Λ[np.diag_indices(N)] = λ ** 20
An = V @ Λ @ V.T
return An @ p
进行这些更改后,您仍然会发现run_sim_1()
更快。 请参阅@senderle的答案以了解可能的原因。