何时不对重复线性变换进行特征分解

When not to do eigendecomposition for repeated linear transformation

假设我们有一个点 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

这个比较难回答。 matrix multiplication and eigendecomposition 的标准实现都是 O(n^3),因此没有先验理由期望一个比另一个更快。有趣的是,我的经验是特征分解通常比单个矩阵乘法慢得多,所以这个结果并不完全让我感到惊讶。

因为本例中的矩阵幂运算涉及 20 次乘法,我明白为什么您可能认为它比特征分解慢。但是,如果您查看 source code,就会发现这个有趣的花絮:

# 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_1my_code_2 都只包含一个 def 语句。因此,您对 timeit 的调用只是计算定义函数所需的时间;永远不会 调用函数 .

将函数 definitions 移动到设置代码中,并将要计时的语句替换为仅调用适当的函数,例如

mycode_1 = ''' 
run_sim_1()
'''

那么你应该降低(很多)传递给 timeitnumber 的值。然后你必须修复 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 的回答。