代码之家  ›  专栏  ›  技术社区  ›  TayTay

用numpy高效求和复杂矩阵积

  •  8
  • TayTay  · 技术社区  · 7 年前

    我有一个矩阵, X ,我正在计算中间矩阵乘积的加权和。下面是一个最小的可复制示例:

    import numpy as np
    
    random_state = np.random.RandomState(1)
    n = 5
    p = 10
    
    X = random_state.rand(p, n) # 10x5
    X_sum = np.zeros((n, n)) # 5x5
    
    # The length of weights are not related to X's dims,
    # but will always be smaller
    y = 3
    weights = random_state.rand(y)
    
    for k in range(y):
        X_sum += np.dot(X.T[:, k + 1:],
                        X[:p - (k + 1), :]) * weights[k]
    

    这很好,产生了我期望的结果。但是,由于 n 和 y 成长(成百上千),这变得非常昂贵,因为重复计算矩阵产品并不完全有效…

    然而,在计算产品的方式上有一个明显的模式:

    First iteration

    Second iteration

    您可以看到,随着迭代的进行,开始的列切片 Xt 右移,而结束行位于 X 向上移动。下面是第n个迭代的样子:

    Nth iteration

    这实际上意味着相同值的一个子集被重复地相乘(参见 编辑2 这在我看来可能有机会利用…(也就是说,如果我要一次手动计算产品)。

    但我希望不必手动做任何事情,而且可能有一个很好的方法可以用numpy更优雅地完成整个循环。

    编辑1

    一组真实的数字:

    n = 400
    p = 2000
    y = 750
    

    编辑2

    要处理评论:

    你能解释一下什么值是反复乘以的吗?

    考虑以下数组:

    n = p = 5
    X = np.arange(25).reshape(p, n)
    

    为了 k=0 ,第一个产品将介于 A 和 B :

    k = 0
    A = X.T[:, k + 1:]
    B = X[:p - (k + 1), :]
    >>> A
    array([[ 5, 10, 15, 20],
           [ 6, 11, 16, 21],
           [ 7, 12, 17, 22],
           [ 8, 13, 18, 23],
           [ 9, 14, 19, 24]])
    >>> B
    array([[ 0,  1,  2,  3,  4],
           [ 5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14],
           [15, 16, 17, 18, 19]])
    

    什么时候 k=1 :

    k = 1
    >>> A
    array([[10, 15, 20],
           [11, 16, 21],
           [12, 17, 22],
           [13, 18, 23],
           [14, 19, 24]])
    >>> B
    array([[ 0,  1,  2,  3,  4],
           [ 5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14]])
    

    所以每一个后续的矩阵积都是前一个积的一个子集,如果这是合理的话。

    2 回复  |  直到 7 年前
        1
  •  4
  •   Charles Drotar    7 年前

    我会选择@parfait使用 test_gen_sum 基于各种价值的基准 n , p 和 y . 为了延续,把旧答案留在这里 .

    评估如何 n , 磷 , Y 影响算法选择

    此分析是使用@parfait的函数来确定 一 最佳解决方案,或者如果有一系列基于 n , 磷 和 Y .

    import numpy as np
    import pytest # This code also requires the pytest-benchmark plugin
    
    def test_for_sum(n, p, y):
        random_state = np.random.RandomState(1)
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        weights = random_state.rand(y)
    
        for k in range(y):
            X_sum += np.dot(X.T[:, k + 1:],
                        X[:p - (k + 1), :]) * weights[k]
    
        return X_sum
    
    
    def test_list_sum(n, p, y):
        random_state = np.random.RandomState(1)
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        weights = random_state.rand(y)
    
        matrix_list = [np.dot(X.T[:, k + 1:],
                          X[:p - (k + 1), :]) * weights[k] for k in range(y)]
    
        X_sum = np.sum(matrix_list, axis=0)
    
        return X_sum
    
    
    def test_reduce_sum(n, p, y):
        random_state = np.random.RandomState(1)
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        weights = random_state.rand(y)
    
        matrix_list = [(X.T[:, k + 1:] @
                    X[:p - (k + 1), :]) * weights[k] for k in range(y)]
    
        X_sum = reduce(lambda x,y: x + y, matrix_list)
    
        return X_sum
    
    
    def test_concat_sum(n, p, y):
        random_state = np.random.RandomState(1)
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        weights = random_state.rand(y)
    
        x_mat = np.concatenate([np.matmul(X.T[:, k + 1:],
                                      X[:p - (k + 1), :]) for k in range(y)])
    
        wgt_mat = np.concatenate([np.full((n,1), weights[k]) for k in range(y)])
    
        mul_res = x_mat * wgt_mat        
        X_sum = mul_res.reshape(-1, n, n).sum(axis=0)
    
        return X_sum
    
    
    def test_matmul_sum(n, p, y):
        random_state = np.random.RandomState(1)
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        weights = random_state.rand(y)
        # Use list comprehension and np.matmul 
        matrices_list = [np.matmul(X.T[:, k + 1:],
                               X[:p - (k + 1), :]) * weights[k] for k in range(y)]
    
        # Sum matrices in list of matrices to get the final result   
        X_sum = np.sum(matrices_list, axis=0)
    
        return X_sum
    
    
    def test_gen_sum(n, p, y):
        random_state = np.random.RandomState(1)
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        weights = random_state.rand(y)
    
        matrix_gen = (np.dot(X.T[:, k + 1:],
                         X[:p - (k + 1), :]) * weights[k] for k in range(y))
    
        X_sum = sum(matrix_gen)
    
        return X_sum
    
    parameters = [
        pytest.param(400, 800, 3)
        ,pytest.param(400, 2000, 3)
        ,pytest.param(400, 800, 750)
        ,pytest.param(400, 2000, 750)
    ]
    
    @pytest.mark.parametrize('n,p,y', parameters)
    def test_test_for_sum(benchmark, n, p, y):
        benchmark(test_for_sum, n=n, p=p, y=y)
    
    @pytest.mark.parametrize('n,p,y', parameters)
    def test_test_list_sum(benchmark, n, p, y):
         benchmark(test_list_sum, n=n, p=p, y=y)
    
    @pytest.mark.parametrize('n,p,y', parameters)
    def test_test_reduce_sum(benchmark, n, p, y):
        benchmark(test_reduce_sum, n=n, p=p, y=y)
    
    @pytest.mark.parametrize('n,p,y', parameters)
    def test_test_concat_sum(benchmark, n, p, y):
        benchmark(test_concat_sum, n=n, p=p, y=y)
    
    @pytest.mark.parametrize('n,p,y', parameters)
    def test_test_matmul_sum(benchmark, n, p, y):
        benchmark(test_matmul_sum, n=n, p=p, y=y)
    
    @pytest.mark.parametrize('n,p,y', parameters)
    def test_test_gen_sum(benchmark, n, p, y):
        benchmark(test_gen_sum, n=n, p=p, y=y)
    
    • n=400 , p=800 , y=3 (100次迭代)

      • 获胜者: TestGyn和 enter image description here
    • n=400 , p=2000 , Y=3 (100次迭代)

      • 获胜者: TestGyn和 enter image description here
    • n=400 , P=800 , y=750 (10次迭代)

      • 获胜者: TestGyn和 enter image description here
    • n=400 , P=2000 , Y=750 (10次迭代)

      • 获胜者: TestGyn和 enter image description here

    旧答案

    更小的 Y 价值观

    我肯定会用 np.matmul 而不是 np.dot 这将给您带来最大的性能提升,实际上还有 np.dot 会指引你去 NP.Matul 用于代替 NPT点 .

    我测试了两个 NPT点 和 NP.Matul 有无清单理解和 pytest-benchmark 结果如下:

    y=3

    顺便说一句 Pytest基准 它相当圆滑,我强烈建议在这种时候验证一种方法是否真正有效。

    仅仅使用列表理解对 NP.Matul 结果和负面影响 NPT点 (尽管这是更好的形式)在事物的图式中,但是两种变化的结合产生了最好的结果。我警告说,使用列表理解往往会提高运行时的标准偏差,因此您可能会看到运行时性能的范围比仅使用 NP.Matul .

    代码如下:

    import numpy as np
    
    def test_np_matmul_list_comprehension():
        random_state = np.random.RandomState(1)
        n = p = 1000
        X = np.arange(n * n).reshape(p, n)
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        y = 3
        weights = [1, 1, 1]
        # Use list comprehension and np.matmul 
        matrices_list = [np.matmul(X.T[:, k + 1:],
                                 X[:p - (k + 1), :]) * weights[k] for k in range(y)]
    
        # Sum matrices in list of matrices to get the final result   
        X_sum = np.sum(matrices_list, axis=0)
    

    更大的 Y 价值观

    对于更大的值 Y 最好不要使用列表理解。平均/中值运行时间对两者都比较大 NPT点 和 NP.Matul 在这两种情况下。这里是 pytest-benchmark 结果( n=500 , p=5000 , Y=750 ):

    enter image description here

    这可能有点过分了,但我宁愿犯错,因为我太乐于助人了。)

        2
  •  3
  •   Parfait    7 年前

    考虑与中的迭代和调用相比,以下重新考虑因素的版本 for 循环。新版本使用 reduce ,发电机,和 np.concatenate 结果略快,但仍与 对于 循环。各奔东西 n = 400, p = 800, y = 750 .

    OP原始版本

    import numpy as np
    
    def test_for_sum():
        random_state = np.random.RandomState(1)
        n= 400
        p = 800
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        y = 750
        weights = random_state.rand(y)
    
        for k in range(y):
            X_sum += np.dot(X.T[:, k + 1:],
                            X[:p - (k + 1), :]) * weights[k]
    
        return X_sum
    

    用np.dot列出理解

    def test_list_sum():
        random_state = np.random.RandomState(1)
        n= 400
        p = 800
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        y = 750
        weights = random_state.rand(y)
    
        matrix_list = [np.dot(X.T[:, k + 1:],
                              X[:p - (k + 1), :]) * weights[k] for k in range(y)]
    
        X_sum = sum(matrix_list)
    
        return X_sum
    

    发电机版本

    def test_gen_sum():
        random_state = np.random.RandomState(1)
        n= 400
        p = 800
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        y = 750
        weights = random_state.rand(y)
    
        matrix_gen = (np.dot(X.T[:, k + 1:],
                             X[:p - (k + 1), :]) * weights[k] for k in range(y))
    
        X_sum = sum(matrix_gen)
    
        return X_sum
    

    还原版 (使用新的 @ 运算符--句法糖--代替 np.matmul )

    from functools import reduce
    
    def test_reduce_sum():
        random_state = np.random.RandomState(1)
        n= 400
        p = 800
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        y = 750
        weights = random_state.rand(y)
    
        matrix_list = [(X.T[:, k + 1:] @
                        X[:p - (k + 1), :]) * weights[k] for k in range(y)]
    
        X_sum = reduce(lambda x,y: x + y, matrix_list)
    
        return X_sum
    

    连接版本

    def test_concat_sum():
        random_state = np.random.RandomState(1)
        n= 400
        p = 800
    
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        y = 750
        weights = random_state.rand(y)
    
        x_mat = np.concatenate([np.matmul(X.T[:, k + 1:],
                                          X[:p - (k + 1), :]) for k in range(y)])
    
        wgt_mat = np.concatenate([np.full((n,1), weights[k]) for k in range(y)])
    
        mul_res = x_mat * wgt_mat        
        X_sum = mul_res.reshape(-1, n, n).sum(axis=0)
    
        return X_sum
    

    用np.matmul列出理解

    def test_matmul_sum():
        random_state = np.random.RandomState(1)
        n = 400
        p = 800
        X = random_state.rand(p, n)
        X_sum = np.zeros((n, n))
    
        # The length of weights are not related to X's dims,
        # but will always be smaller
        y = 750
        weights = random_state.rand(y)
        # Use list comprehension and np.matmul 
        matrices_list = [np.matmul(X.T[:, k + 1:],
                                   X[:p - (k + 1), :]) * weights[k] for k in range(y)]
    
        # Sum matrices in list of matrices to get the final result   
        X_sum = np.sum(matrices_list, axis=0)
    
        return X_sum
    

    计时

    import time
    
    start_time = time.time()
    res_for = test_for_sum()
    print("SUM: {} seconds ---".format(time.time() - start_time))
    
    start_time = time.time()
    res_list = test_list_sum()
    print("LIST: {} seconds ---".format(time.time() - start_time))
    
    start_time = time.time()
    res_gen = test_gen_sum()
    print("GEN: {} seconds ---".format(time.time() - start_time))
    
    start_time = time.time()
    res_reduce= test_reduce_sum()
    print("REDUCE: {} seconds ---".format(time.time() - start_time))
    
    start_time = time.time()
    res_concat = test_concat_sum()
    print("CONCAT: {} seconds ---".format(time.time() - start_time))
    
    start_time = time.time()
    res_matmul = test_matmul_sum()
    print("MATMUL: {} seconds ---".format(time.time() - start_time))
    

    相等性测试

    print(np.array_equal(res_for, res_list))
    # True
    print(np.array_equal(res_for, res_gen))
    # True
    print(np.array_equal(res_for, res_reduce))
    # True
    print(np.array_equal(res_for, res_concat))
    # True
    print(np.array_equal(res_for, res_matmul))
    # True
    

    首次运行

    # SUM: 21.569773197174072 seconds ---
    # LIST: 23.576102018356323 seconds ---
    # GEN: 21.385253429412842 seconds ---
    # REDUCE: 21.426464080810547 seconds ---
    # CONCAT: 21.059731483459473 seconds ---
    # MATMUL: 23.57494807243347 seconds ---
    

    二次跑

    # SUM: 21.6339168548584 seconds ---
    # LIST: 19.767740488052368 seconds ---
    # GEN: 23.86947798728943 seconds ---
    # REDUCE: 19.880712032318115 seconds ---
    # CONCAT: 20.761067152023315 seconds ---
    # MATMUL: 23.55513620376587 seconds ---
    

    第三跑

    # SUM: 22.764745473861694 seconds ---
    # LIST: 19.953850984573364 seconds ---
    # GEN: 24.37714171409607 seconds ---
    # REDUCE: 22.54508638381958 seconds ---
    # CONCAT: 21.20585823059082 seconds ---
    # MATMUL: 22.303589820861816 seconds ---