Как я могу выразить это большое количество вычислений без циклов for? - PullRequest
0 голосов
/ 17 сентября 2018

Я работаю в основном в MATLAB, но я думаю, что ответ не должен быть слишком сложным для переноса с одного языка на другой.

У меня есть многомерный массив 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

Сумма состоит из элементов вектора длины - n.Любая помощь приветствуется!

Ответы [ 3 ]

0 голосов
/ 18 сентября 2018

Как я уже упоминал в комментарии , векторизация больше не всегда является огромным преимуществом. Поэтому существуют методы векторизации, которые замедляют код, а не ускоряют его. Вы должны всегда рассчитывать свои решения. Векторизация часто включает создание больших временных массивов или копирование больших объемов данных, которых избегают в циклическом коде. Это зависит от архитектуры, размера ввода и многих других факторов, если такое решение будет быстрее.

Тем не менее, в этом случае кажется, что подходы векторизации могут дать большое ускорение.

Первое, что следует заметить в исходном коде, это то, что 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-летний iMac с 4 ядрами). Для 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

"Ответ Луиса" - , этот . Это намного быстрее для небольших массивов, но для больших массивов он показывает стоимость перестановки. Перемещение вычисления первого продукта из внутреннего цикла экономит чуть более половины стоимости вычислений. Но удаление внутреннего цикла значительно снижает стоимость (чего я не ожидал, я предполагаю, что один матричный продукт может использовать параллелизм лучше, чем многие маленькие элементные продукты). Затем мы получаем дополнительное сокращение времени за счет сокращения количества операций индексирования в цикле.

Это временной код:

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);
0 голосов
/ 18 сентября 2018

Вы упомянули, что вы открыты для других языков, и 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 ускорение там!

0 голосов
/ 18 сентября 2018

Вам понадобится лишь некоторая перестановка измерений и умножения с одноэлементным расширением:

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