【问题标题】:MATLAB: How to vector-multiply two arrays of matrices?MATLAB:如何对两个矩阵数组进行向量乘法?
【发布时间】:2011-09-28 16:02:33
【问题描述】:

我有两个 3 维数组,前两个维表示矩阵,最后一个维数通过参数空间计算,举个简单的例子

A = repmat([1,2; 3,4], [1 1 4]);

(但假设每个jA(:,:,j) 都不同)。如何轻松执行两个这样的矩阵数组AB 的每j 矩阵乘法?

C = A; % pre-allocate, nan(size(A,1), size(B,2)) would be better but slower
for jj = 1:size(A, 3)
  C(:,:,jj) = A(:,:,jj) * B(:,:,jj);
end

当然可以,但如果第三维更像 1e3 个元素,这将非常慢,因为它不使用 MATLAB 的矢量化。那么,有没有更快的方法呢?

【问题讨论】:

  • 你真的给循环计时了吗?对于最新的 Matlab 版本,它可能会非常快。您期望“矢量化”版本比蜜蜂快多少?谢谢
  • @eat: 对于 1000 个参数,它是 7 的因数(MATLAB R2010a),我在优化循环中使用它,所以这很重要 - 我现在找到了解决方案,我会发布它午饭后
  • @TobiasKienzler:我假设您正在预分配矩阵C??

标签: arrays matlab matrix vectorization matrix-multiplication


【解决方案1】:

我现在做了一些时序测试,结果证明 2x2xN 最快的方法是计算矩阵元素:

C = A;
C(1,1,:) = A(1,1,:).*B(1,1,:) + A(1,2,:).*B(2,1,:);
C(1,2,:) = A(1,1,:).*B(1,2,:) + A(1,2,:).*B(2,2,:);
C(2,1,:) = A(2,1,:).*B(1,1,:) + A(2,2,:).*B(2,1,:);
C(2,2,:) = A(2,1,:).*B(1,2,:) + A(2,2,:).*B(2,2,:);

在一般情况下,事实证明 for 循环实际上是最快的(但不要忘记预先分配 C!)。

如果已经将结果作为矩阵的单元阵列,使用cellfun 是最快的选择,它也比遍历单元元素更快:

C = cellfun(@mtimes, A, B, 'UniformOutput', false);

但是,对于 3d 数组的情况,必须先调用 num2cell (Ac = num2cell(A, [1 2])) 和 cell2mat 会浪费太多时间。


这是我为一组随机的 2 x 2 x 1e4 做的一些时间安排:

 array-for: 0.057112
 arrayfun : 0.14206
 num2cell : 0.079468
 cell-for : 0.033173
 cellfun  : 0.025223
 cell2mat : 0.010213
 explicit : 0.0021338

显式是指使用直接计算 2 x 2 矩阵元素,见下文。 对于新的随机数组,结果类似,如果之前不需要num2cellcellfun 是最快的,并且对 2x2xN 没有限制。对于一般的 3d 数组,在第三维上循环确实是最快的选择。这是计时码:

n = 2;
m = 2;
l = 1e4;

A = rand(n,m,l);
B = rand(m,n,l);

% naive for-loop:
tic
%Cf = nan(n,n,l);
Cf = A;
for jl = 1:l
    Cf(:,:,jl) = A(:,:,jl) * B(:,:,jl);
end;
disp([' array-for: ' num2str(toc)]);

% using arrayfun:
tic
Ca = arrayfun(@(k) A(:,:,k)*B(:,:,k), 1:size(A,3), 'UniformOutput',false);
Ca = cat(3,Ca{:});
disp([' arrayfun : ' num2str(toc)]);

tic
Ac = num2cell(A, [1 2]);
Bc = num2cell(B, [1 2]);
disp([' num2cell : ' num2str(toc)]);

% cell for-loop:
tic
Cfc = Ac;
for jl = 1:l
    Cfc{jl} = Ac{jl} * Bc{jl};
end;
disp([' cell-for : ' num2str(toc)]);

% using cellfun:
tic
Cc = cellfun(@mtimes, Ac, Bc, 'UniformOutput', false);
disp([' cellfun  : ' num2str(toc)]);

tic
Cc = cell2mat(Cc);
disp([' cell2mat : ' num2str(toc)]);

tic
Cm = A;
Cm(1,1,:) = A(1,1,:).*B(1,1,:) + A(1,2,:).*B(2,1,:);
Cm(1,2,:) = A(1,1,:).*B(1,2,:) + A(1,2,:).*B(2,2,:);
Cm(2,1,:) = A(2,1,:).*B(1,1,:) + A(2,2,:).*B(2,1,:);
Cm(2,2,:) = A(2,1,:).*B(1,2,:) + A(2,2,:).*B(2,2,:);
disp([' explicit : ' num2str(toc)]);

disp(' ');

【讨论】:

  • 确实很聪明。您可能确实需要稍后接受您自己的答案;)。谢谢
  • 别被 CELLFUN 骗了,里面有一个隐藏的循环......所以写起来真的很简单:C = arrayfun(@(k) A(:,:,k)*B(:,:,k), 1:size(A,3), 'UniformOutput',false); C = cat(3,C{:});。两者都不比原来的 for 循环好!
  • @Amro:你说得对,我现在做了计时测试。 arrayfun 几乎和 num2cell + cellfun + cell2mat 一样快/慢,事实证明原来的 for 循环确实是最快的(是的,我预先分配了 C),除非你已经有单元格
  • @TobiasKienzler:我发布了一些我自己的基准测试......正如预期的那样,FOR 循环非常快,尤其是在最新版本的 MATLAB 中对即时 (JIT) 加速器进行了改进
【解决方案2】:

这是我的基准测试,比较了@TobiasKienzler 答案中提到的方法。我正在使用TIMEIT 函数来获得更准确的计时。

function [t,v] = matrixMultTest()
    n = 2; m = 2; p = 1e5;
    A = rand(n,m,p);
    B = rand(m,n,p);

    %# time functions
    t = zeros(5,1);
    t(1) = timeit( @() func1(A,B,n,m,p) );
    t(2) = timeit( @() func2(A,B,n,m,p) );
    t(3) = timeit( @() func3(A,B,n,m,p) );
    t(4) = timeit( @() func4(A,B,n,m,p) );
    t(5) = timeit( @() func5(A,B,n,m,p) );

    %# check the results
    v = cell(5,1);
    v{1} = func1(A,B,n,m,p);
    v{2} = func2(A,B,n,m,p);
    v{3} = func3(A,B,n,m,p);
    v{4} = func4(A,B,n,m,p);
    v{5} = func5(A,B,n,m,p);
    assert( isequal(v{:}) )
end

%# simple FOR-loop
function C = func1(A,B,n,m,p)
    C = zeros(n,n,p);
    for k=1:p
        C(:,:,k) = A(:,:,k) * B(:,:,k);
    end
end

%# ARRAYFUN
function C = func2(A,B,n,m,p)
    C = arrayfun(@(k) A(:,:,k)*B(:,:,k), 1:p, 'UniformOutput',false);
    C = cat(3, C{:});
end

%# NUM2CELL/FOR-loop/CELL2MAT
function C = func3(A,B,n,m,p)
    Ac = num2cell(A, [1 2]);
    Bc = num2cell(B, [1 2]);
    C = cell(1,1,p);
    for k=1:p
        C{k} = Ac{k} * Bc{k};
    end;
    C = cell2mat(C);
end

%# NUM2CELL/CELLFUN/CELL2MAT
function C = func4(A,B,n,m,p)
    Ac = num2cell(A, [1 2]);
    Bc = num2cell(B, [1 2]);
    C = cellfun(@mtimes, Ac, Bc, 'UniformOutput', false);
    C = cell2mat(C);
end

%# Loop Unrolling
function C = func5(A,B,n,m,p)
    C = zeros(n,n,p);
    C(1,1,:) = A(1,1,:).*B(1,1,:) + A(1,2,:).*B(2,1,:);
    C(1,2,:) = A(1,1,:).*B(1,2,:) + A(1,2,:).*B(2,2,:);
    C(2,1,:) = A(2,1,:).*B(1,1,:) + A(2,2,:).*B(2,1,:);
    C(2,2,:) = A(2,1,:).*B(1,2,:) + A(2,2,:).*B(2,2,:);
end

结果:

>> [t,v] = matrixMultTest();
>> t
t =
      0.63633      # FOR-loop
      1.5902       # ARRAYFUN
      1.1257       # NUM2CELL/FOR-loop/CELL2MAT
      1.0759       # NUM2CELL/CELLFUN/CELL2MAT
      0.05712      # Loop Unrolling

正如我在 cmets 中所解释的,一个简单的 FOR 循环是最好的解决方案(在最后一种情况下缺少 loop unwinding,这仅适用于这些小的 2×2 矩阵)。

【讨论】:

  • 恐怕你刚刚被Ali's answer 盗用了 MMX 工具箱的复选标记,2012 年之前还没有这个工具箱...
  • @TobiasKienzler 啊,没关系。毕竟,很难打败 C 代码!我查看了 MMX 工具箱的源代码,它基本上是在创建线程(与处理器一样多),每个线程在分配的矩阵切片上调用一个矩阵乘法函数。如果您在编译时启用了优化,它将使用dgemm BLAS 例程(来自 MATLAB 附带的英特尔 MKL 库)来执行矩阵乘法,这与 MATLAB 内部使用的例程相同。
  • ... 也就是说,对于小型 2x2 矩阵,您应该注意超额订阅(MATLAB 附带的 MKL 本身是多线程的,同时 MMX 工具箱从多个线程调用它)。通过使用针对小型 mat-mult 优化的库,您实际上可能会获得更好的性能(BLAS 确实适用于大型矩阵)。你可以在阿里的时间里看到这个事实; MMX 与循环展开版本的时间几乎相同。现在想象一下用 C 实现的相同代码! IMO 问题是内存限制而不是 CPU 限制,线程在这里效率较低,这都是关于良好的缓存重用。
【解决方案3】:

我强烈推荐你使用 matlab 的MMX toolbox。它可以尽可能快地乘以 n 维矩阵。

MMX的优点是:

  1. 易于使用。
  2. n维矩阵相乘(实际上可以将二维矩阵数组相乘)
  3. 它执行其他矩阵运算(转置、二次乘法、Chol 分解等)
  4. 它使用C编译器多线程计算来加速。

对于这个问题,你只需要写这个命令:

C=mmx('mul',A,B);

我在@Amro 的回答中添加了以下功能

%# mmx toolbox
function C=func6(A,B,n,m,p)
    C=mmx('mul',A,B);
end

我得到了n=2,m=2,p=1e5 的这个结果:

    1.6571 # FOR-loop
    4.3110 # ARRAYFUN
    3.3731 # NUM2CELL/FOR-loop/CELL2MAT
    2.9820 # NUM2CELL/CELLFUN/CELL2MAT
    0.0244 # Loop Unrolling
    0.0221 # MMX toolbox  <===================

我使用@Amro 的代码运行基准测试。

【讨论】:

  • 很好地更新了我的一个老问题 :-) 当然,该工具箱在 2012 年之前并不存在......
【解决方案4】:

一种技术是创建一个 2Nx2N 稀疏矩阵并在对角线上嵌入 A 和 B 的 2x2 矩阵。对稀疏矩阵进行乘积,并通过稍微巧妙的索引获取结果并将其重塑为 2x2xN。

但我怀疑这会比简单的循环更快。

【讨论】:

  • 好主意,尽管您的怀疑可能是正确的。如果您有兴趣,我找到了使用 cellfun 的解决方案
【解决方案5】:

根据我的经验,一种更快的方法是在三维矩阵上使用点乘和求和。下面的函数 z_matmultiply(A,B) 将两个具有相同深度的三维矩阵相乘。点乘以尽可能并行的方式完成,因此您可能需要检查此函数的速度,并在大量重复后将其与其他函数进行比较。

function C = z_matmultiply(A,B)

[ma,na,oa] = size(A);
[mb,nb,ob] = size(B);

%preallocate the output as we will do a loop soon
C = zeros(ma,nb,oa);

%error message if the dimensions are not appropriate
if na ~= mb || oa ~= ob
    fprintf('\n z_matmultiply warning: Matrix Dimmensions Inconsistent \n')
else

% if statement minimizes for loops by looping the smallest matrix dimension 
if ma > nb
    for j = 1:nb
        Bp(j,:,:) = B(:,j,:);
        C(:,j,:) = sum(A.*repmat(Bp(j,:,:),[ma,1]),2);
    end
else
    for i = 1:ma
        Ap(:,i,:) = A(i,:,:);
        C(i,:,:) = sum(repmat(Ap(:,i,:),[1,nb]).*B,1);
    end 
end

end

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-05
  • 2021-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-24
相关资源
最近更新 更多