没有for循环,如何表达这么大量的计算?

时间:2018-09-17 20:42:03

标签: matlab multidimensional-array vectorization matrix-multiplication

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

我有一个维度为df %>% group_by(ID) %>% mutate(V3 = as.numeric(any(V2 == 1))) 的多维数组X。 我想计算以下多维数组。

[n, p, 3]

总和是长度{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 向量的元素。任何帮助表示赞赏!

3 个答案:

答案 0 :(得分:5)

您只需要对维度进行置换和单例扩展的乘积:

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);

答案 1 :(得分:5)

正如我在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,具有4核的3年旧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);

答案 2 :(得分:4)

您已经提到过您可以使用其他语言,并且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 附近加速!