【问题标题】:How can I express this large number of computations without for loops?如何在没有 for 循环的情况下表达如此大量的计算?
【发布时间】:2019-02-21 19:56:54
【问题描述】:

我主要在 MATLAB 中工作,但我认为答案应该不会太难从一种语言转移到另一种语言。

我有一个维度为[n, p, 3] 的多维数组X。 我想计算下面的多维数组。

T = zeros(p, p, p)
for i = 1:p
for j = 1:p
for k = 1:p
T(i, j, k) = sum(X(:, i, 1) .* X(:, j, 2) .* X(:, k, 3));
end
end
end

总和是长度-n 向量的元素。任何帮助表示赞赏!

【问题讨论】:

  • 为了便于携带,您可以简单地使用循环。在 Matlab 中,Jit 编译器越来越好,Python 也有这样的方法,例如。 stackoverflow.com/a/50890174/4045774 。在 C 或 Fortran 中,您还可以使用简单的循环。但请记住数组顺序(Matlab、Fortran、Julia -> 列主要顺序,C、Python 通常是行主要顺序)。如果您翻转 T 的数组 dims,您还可以改进 Matlab 版本(您将其编码为 C 或 Python 函数)
  • @max9111 实际上,正如 Cris 所建议的那样,通过矢量化内循环,我得到了很好的速度提升。 (我正在使用n = 6500p = 300。)我会尝试切换尺寸。

标签: matlab multidimensional-array vectorization matrix-multiplication


【解决方案1】:

你只需要一些维度的排列和单例扩展的乘法:

T = sum(bsxfun(@times, bsxfun(@times, permute(X(:,:,1), [2 4 5 3 1]), permute(X(:,:,2), [4 2 5 3 1])), permute(X(:,:,3), [4 5 2 3 1])), 5);

从 R2016b 开始,这可以更容易地写成

T = sum(permute(X(:,:,1), [2 4 5 3 1]) .* permute(X(:,:,2), [4 2 5 3 1]) .* permute(X(:,:,3), [4 5 2 3 1]), 5);

【讨论】:

  • 我喜欢矢量化,但由于某种原因,for 循环快了大约 4 倍。
  • @OpenSeason:循环在 MATLAB 中不再慢。矢量化仍然节省了一些开销,但差异并不像以前那么大。此矢量化解决方案使用permute,它以不同的顺序复制数据。复制的开销比循环的开销更重要。我建议您继续使用您的循环,并对它们不再是曾经的速度损失感到满意。也许您可以只为了一点点优势而对内部循环进行vercorize?
  • @OpenSeason:我错了,矢量化似乎确实在我的系统上产生了很大的加速。我看到 Luis 的解决方案比使用 n=50, p=100 的原始代码快 20 倍。如果您发现这变慢了,那么您的阵列可能会大得多。我已经发布了一个仅矢量化内部循环的替代解决方案。
  • @CrisLuengo 完美地完成了您的回答。我的答案完全出局了。只是为了争论,您有没有办法将张量符号转换为您的置换公式或任何参考文章?
  • @NPE 不是真的。诀窍只是移动维度,以便如果您需要涉及某些原始维度的所有元组,则将这些维度移动到不同的维度索引(因此这里的原始 2nd dim 移动到 1st、2nd 和 3rd)。然后您执行所需的计算,其中涉及单例扩展以隐式创建元组和一些操作(此处为求和),通常将某些维度(此处为第 5 个)减少为单例。最后,您可能需要再次移动维度以将这些单例向右移动(此处不需要,因为它已在第一步中完成)
【解决方案2】:

正如我在a comment 中提到的,矢量化不再是一个巨大的优势。因此,有一些矢量化方法可以减慢代码速度而不是加快速度。您必须始终为您的解决方案计时。矢量化通常涉及创建大型临时数组或复制大量数据,这些在循环代码中被避免。如果这样的解决方案要更快,这取决于架构、输入的大小以及许多其他因素。

尽管如此,在这种情况下,矢量化方法似乎可以产生很大的加速。

关于原始代码,首先要注意的是X(:, i, 1) .* X(:, j, 2) 在内部循环中被重新计算,尽管它在那里是一个常量值。重写内部循环,这样可以节省时间:

Y = X(:, i, 1) .* X(:, j, 2);
for k = 1:p
   T(i, j, k) = sum(Y .* X(:, k, 3));
end

现在我们注意到内循环是一个点积,可以写成:

Y = X(:, i, 1) .* X(:, j, 2);
T(i, j, :) = Y.' * X(:, :, 3);

Y 上的.' 转置不会复制数据,因为Y 是一个向量。接下来,我们注意到X(:, :, 3) 被重复索引。让我们把它移出外循环。现在我剩下以下代码:

T = zeros(p, p, p);
X1 = X(:, :, 1);
X2 = X(:, :, 2);
X3 = X(:, :, 3);
for i = 1:p
   for j = 1:p
      Y = X1(:, i) .* X2(:, j);
      T(i, j, :) = Y.' * X3;
   end
end

删除j 上的循环很可能同样容易,这将在i 上留下一个循环。但这就是我停下来的地方。

这是我看到的时间安排(R2017a,3 岁的 4 核 iMac)。对于n=10, p=20

original:                     0.0206
moving Y out the inner loop:  0.0100
removing inner loop:          0.0016
moving indexing out of loops: 7.6294e-04
Luis' answer:                 1.9196e-04

对于更大的数组,n=50, p=100

original:                     2.9107
moving Y out the inner loop:  1.3488
removing inner loop:          0.0910
moving indexing out of loops: 0.0361
Luis' answer:                 0.1417

“路易斯的回答”是this one。对于小型阵列,它是迄今为止最快的,但对于较大的阵列,它显示了排列的成本。将第一个乘积的计算移出内部循环可以节省一半以上的计算成本。但是删除内部循环会大大降低成本(我没有预料到,我认为单矩阵产品可以比许多小的元素产品更好地使用并行性)。然后,我们通过减少循环内的索引操作量来进一步减少时间。

这是计时码:

function so()
n = 10; p = 20;
%n = 50; p = 100; 
X = randn(n,p,3);

T1 = method1(X);
T2 = method2(X);
T3 = method3(X);
T4 = method4(X);
T5 = method5(X);

assert(max(abs(T1(:)-T2(:)))<1e-13)
assert(max(abs(T1(:)-T3(:)))<1e-13)
assert(max(abs(T1(:)-T4(:)))<1e-13)
assert(max(abs(T1(:)-T5(:)))<1e-13)

timeit(@()method1(X))
timeit(@()method2(X))
timeit(@()method3(X))
timeit(@()method4(X))
timeit(@()method5(X))

function T = method1(X)
p = size(X,2);
T = zeros(p, p, p);
for i = 1:p
   for j = 1:p
      for k = 1:p
         T(i, j, k) = sum(X(:, i, 1) .* X(:, j, 2) .* X(:, k, 3));
      end
   end
end

function T = method2(X)
p = size(X,2);
T = zeros(p, p, p);
for i = 1:p
   for j = 1:p
      Y = X(:, i, 1) .* X(:, j, 2);
      for k = 1:p
         T(i, j, k) = sum(Y .* X(:, k, 3));
      end
   end
end

function T = method3(X)
p = size(X,2);
T = zeros(p, p, p);
for i = 1:p
   for j = 1:p
      Y = X(:, i, 1) .* X(:, j, 2);
      T(i, j, :) = Y.' * X(:, :, 3);
   end
end

function T = method4(X)
p = size(X,2);
T = zeros(p, p, p);
X1 = X(:, :, 1);
X2 = X(:, :, 2);
X3 = X(:, :, 3);
for i = 1:p
   for j = 1:p
      Y = X1(:, i) .* X2(:, j);
      T(i, j, :) = Y.' * X3;
   end
end

function T = method5(X)
T = sum(permute(X(:,:,1), [2 4 5 3 1]) .* permute(X(:,:,2), [4 2 5 3 1]) .* permute(X(:,:,3), [4 5 2 3 1]), 5);

【讨论】:

    【解决方案3】:

    您提到您对其他语言持开放态度,并且 NumPy 的语法非常接近 MATLAB,因此我们将尝试在此基础上提供基于 NumPy 的解决方案。

    现在,这些与张量相关的减和,特别是矩阵乘法很容易表示为einstein-notation,幸运的是,NumPy 有一个与np.einsum 相同的功能。在幕后,它在C 中实现并且非常高效。最近它被进一步优化以利用基于 BLAS 的矩阵乘法实现。

    因此,将所述代码翻译到 NumPy 领域,记住它遵循基于 0 的索引,并且轴的可视化方式与使用 MATLAB 的维度不同,将是 -

    import numpy as np
    
    # X is a NumPy array of shape : (n,p,3). So, a random one could be
    # generated with : `X = np.random.rand(n,p,3)`.
    
    T = np.zeros((p, p, p))
    for i in range(p):
        for j in range(p):
            for k in range(p):
                T[i, j, k] = np.sum(X[:, i, 0] * X[:, j, 1] * X[:, k, 2])
    

    einsum 的解决方法是 -

    np.einsum('ia,ib,ic->abc',X[...,0],X[...,1],X[...,2])
    

    要利用 matrix-multiplication,请使用 optimize 标志 -

    np.einsum('ia,ib,ic->abc',X[...,0],X[...,1],X[...,2],optimize=True)
    

    时机(大尺寸)

    In [27]: n,p = 100,100
        ...: X = np.random.rand(n,p,3)
    
    In [28]: %%timeit
        ...: T = np.zeros((p, p, p))
        ...: for i in range(p):
        ...:     for j in range(p):
        ...:         for k in range(p):
        ...:             T[i, j, k] = np.sum(X[:, i, 0] * X[:, j, 1] * X[:, k, 2])
    1 loop, best of 3: 6.23 s per loop
    
    In [29]: %timeit np.einsum('ia,ib,ic->abc',X[...,0],X[...,1],X[...,2])
    1 loop, best of 3: 353 ms per loop
    
    In [31]: %timeit np.einsum('ia,ib,ic->abc',X[...,0],X[...,1],X[...,2],optimize=True)
    100 loops, best of 3: 10.5 ms per loop
    
    In [32]: 6230.0/10.5
    Out[32]: 593.3333333333334
    

    600x 附近加速!

    【讨论】:

    • 公平地说,我相信您应该执行与@CrisLuengo 在for 解决方案中的答案相同的操作,以与einsum 进行比较。
    • @NPE 我相信 CrisLuengo 做了和 OP 一样的操作。我正在执行与 OP 相同的操作,唯一不同的是 NumPy 在 MATLAB 中根据维度处理轴的方式。
    • 只是说 T = np.zeros((p, p, p)) X1 = X[:, : ,0] X2 = X[:, :, 1] X3 = X[:, :, 2] for i in range(p): for j in range(p): tmp = X1[:, i] * X2[:, j] T[i,j,:] = np.dot(np. transpose(tmp), X3[:], ) return T 实际上更像是 250 ms 而不是 6s,与没有 BLAS 的 einsum 相比。然而使用 BLAS 的 optimize=True 是神奇的!很棒的 np.dot 函数可以为你做什么。
    猜你喜欢
    • 2021-01-29
    • 1970-01-01
    • 2015-02-12
    • 2020-07-25
    • 1970-01-01
    • 1970-01-01
    • 2016-10-31
    • 2017-10-25
    • 1970-01-01
    相关资源
    最近更新 更多