26

Suppose I have an AxBxC matrix X and a BxD matrix Y.

Is there a non-loop method by which I can multiply each of the C AxB matrices with Y?

Amro
  • 123,847
  • 25
  • 243
  • 454
Jacob
  • 34,255
  • 14
  • 110
  • 165
  • 5
    Why would you bother? I look at Gnovice's (correct) solution and it would take me a significant amount of time to understand what that does. I then look at Zaid's and understand instantly. *If* there is a performance difference, there is a maintance cost to consider also. – MatlabDoug Nov 17 '09 at 13:38
  • 2
    This isn't about performance or readability - just mere curiosity since I knew it was possible to operate on each 3D matrix individually but couldn't figure out how. I know that Gnovice's solution will be much slower than Zaid's "solution" and Amro's solution but, as I said, that's not the point. – Jacob Nov 17 '09 at 13:47
  • 1
    Now you've totally lost me... what is it that you're after? – Zaid Nov 17 '09 at 15:14
  • A non-loop method by which I can multiply each of the C AxB matrices with Y, e.g. Amro's & GNovice's solutions. – Jacob Nov 17 '09 at 15:33
  • 6
    @Jacob: 1. the solution by gnovice IS NOT slower then that of amro. 2. The solution of gnovice uses cellfun which is a wrapper around a loop. So you can make a function from Zaid's solution, call it prod3D.m and voilà, you have a non-loop method for multiplying X and Y. 3. Do not forget that 80% of software cost is maintenance. – Mikhail Poda Nov 18 '09 at 10:01
  • Do not forget that 80% of software cost is maintenance. - You're wonderful, @Mikhail! –  Jan 08 '14 at 23:21

10 Answers10

17

As a personal preference, I like my code to be as succinct and readable as possible.

Here's what I would have done, though it doesn't meet your 'no-loops' requirement:

for m = 1:C

    Z(:,:,m) = X(:,:,m)*Y;

end

This results in an A x D x C matrix Z.

And of course, you can always pre-allocate Z to speed things up by using Z = zeros(A,D,C);.

Zaid
  • 36,680
  • 16
  • 86
  • 155
  • 4
    -1 : because this is not a real solution regardless of your disclaimer. If you have any opinions on succinctness or readability, please leave them as comments. – Jacob Nov 17 '09 at 13:51
  • 6
    +1 because it's also faster than gnovice and amro's fine solutions. – Ramashalanka Mar 09 '10 at 00:28
  • +1 for readability - but please pre-allocate Z with `Z = zeros([A D C]);`! – Floris Jul 09 '13 at 15:44
15

You can do this in one line using the functions NUM2CELL to break the matrix X into a cell array and CELLFUN to operate across the cells:

Z = cellfun(@(x) x*Y,num2cell(X,[1 2]),'UniformOutput',false);

The result Z is a 1-by-C cell array where each cell contains an A-by-D matrix. If you want Z to be an A-by-D-by-C matrix, you can use the CAT function:

Z = cat(3,Z{:});



NOTE: My old solution used MAT2CELL instead of NUM2CELL, which wasn't as succinct:

[A,B,C] = size(X);
Z = cellfun(@(x) x*Y,mat2cell(X,A,B,ones(1,C)),'UniformOutput',false);
gnovice
  • 125,304
  • 15
  • 256
  • 359
  • 2
    With this solution the loop is inside cellfun. But it is nevertheless 10% faster then solution provided by amro (on large matrces, shortly before MATLAB runs out of memory). – Mikhail Poda Nov 17 '09 at 11:37
  • I'm curious as to the 2 downvotes I got. Whether or not you *like* the answer, it *does* answer the question by avoiding explicit use of a for loop. – gnovice Nov 17 '09 at 15:01
  • 2
    Man, who would've thought a simple question like this would be so controversial? – Jacob Nov 17 '09 at 15:42
  • 1
    @Jacob: Yeah, it appears to have spawned some debate. Since I had seen you answering MATLAB questions before, I figured you already knew how to do this using loops (the most straight-forward way). I just assumed you were asking the question out of curiosity for what other ways it could also be done. – gnovice Nov 17 '09 at 15:51
  • @gnovice: You figured correctly. I was looking for a `bsxfun`-ish way of doing this and your `cellfun` implementation fit the bill. I had assumed the "non-loop" criterion would've been enough to make my intentions clear: guess not! – Jacob Nov 17 '09 at 15:56
8

Here's a one-line solution (two if you want to split into 3rd dimension):

A = 2;
B = 3;
C = 4;
D = 5;

X = rand(A,B,C);
Y = rand(B,D);

%# calculate result in one big matrix
Z = reshape(reshape(permute(X, [2 1 3]), [A B*C]), [B A*C])' * Y;

%'# split into third dimension
Z = permute(reshape(Z',[D A C]),[2 1 3]);

Hence now: Z(:,:,i) contains the result of X(:,:,i) * Y


Explanation:

The above may look confusing, but the idea is simple. First I start by take the third dimension of X and do a vertical concatenation along the first dim:

XX = cat(1, X(:,:,1), X(:,:,2), ..., X(:,:,C))

... the difficulty was that C is a variable, hence you can't generalize that expression using cat or vertcat. Next we multiply this by Y:

ZZ = XX * Y;

Finally I split it back into the third dimension:

Z(:,:,1) = ZZ(1:2, :);
Z(:,:,2) = ZZ(3:4, :);
Z(:,:,3) = ZZ(5:6, :);
Z(:,:,4) = ZZ(7:8, :);

So you can see it only requires one matrix multiplication, but you have to reshape the matrix before and after.

Amro
  • 123,847
  • 25
  • 243
  • 454
  • Thanks! I was hoping for a solution along the lines of `bsxfun` but this looks interesting – Jacob Nov 17 '09 at 00:20
  • there was no need. As you can see from the explanation I added, it was only a matter of preparing the matrix by rearranging its shape, so that a simple multiplication would suffice. – Amro Nov 17 '09 at 17:18
  • Nice solution but it can produce a memory overflow because of the reshaping – gaborous Jun 14 '14 at 16:36
  • 1
    @user1121352: as was mentioned by the OP in the comments, the motivation here was to explore alternative solutions (for fun) rather than producing faster or more readable code... In production code, I would stick with the straightforward for-loop :) – Amro Jun 14 '14 at 16:52
6

I'm approaching the exact same issue, with an eye for the most efficient method. There are roughly three approaches that i see around, short of using outside libraries (i.e., mtimesx):

  1. Loop through slices of the 3D matrix
  2. repmat-and-permute wizardry
  3. cellfun multiplication

I recently compared all three methods to see which was quickest. My intuition was that (2) would be the winner. Here's the code:

% generate data
A = 20;
B = 30;
C = 40;
D = 50;

X = rand(A,B,C);
Y = rand(B,D);

% ------ Approach 1: Loop (via @Zaid)
tic
Z1 = zeros(A,D,C);
for m = 1:C
    Z1(:,:,m) = X(:,:,m)*Y;
end
toc

% ------ Approach 2: Reshape+Permute (via @Amro)
tic
Z2 = reshape(reshape(permute(X, [2 1 3]), [A B*C]), [B A*C])' * Y;
Z2 = permute(reshape(Z2',[D A C]),[2 1 3]);
toc


% ------ Approach 3: cellfun (via @gnovice)
tic
Z3 = cellfun(@(x) x*Y,num2cell(X,[1 2]),'UniformOutput',false);
Z3 = cat(3,Z3{:});
toc

All three approaches produced the same output (phew!), but, surprisingly, the loop was the fastest:

Elapsed time is 0.000418 seconds.
Elapsed time is 0.000887 seconds.
Elapsed time is 0.001841 seconds.

Note that the times can vary quite a lot from one trial to another, and sometimes (2) comes out the slowest. These differences become more dramatic with larger data. But with much bigger data, (3) beats (2). The loop method is still best.

% pretty big data...
A = 200;
B = 300;
C = 400;
D = 500;
Elapsed time is 0.373831 seconds.
Elapsed time is 0.638041 seconds.
Elapsed time is 0.724581 seconds.

% even bigger....
A = 200;
B = 200;
C = 400;
D = 5000;
Elapsed time is 4.314076 seconds.
Elapsed time is 11.553289 seconds.
Elapsed time is 5.233725 seconds.

But the loop method can be slower than (2), if the looped dimension is much larger than the others.

A = 2;
B = 3;
C = 400000;
D = 5;
Elapsed time is 0.780933 seconds.
Elapsed time is 0.073189 seconds.
Elapsed time is 2.590697 seconds.

So (2) wins by a big factor, in this (maybe extreme) case. There may not be an approach that is optimal in all cases, but the loop is still pretty good, and best in many cases. It is also best in terms of readability. Loop away!

cspence
  • 18
  • 3
Nolan Conaway
  • 2,639
  • 1
  • 26
  • 42
1

Nope. There are several ways, but it always comes out in a loop, direct or indirect.

Just to please my curiosity, why would you want that anyway ?

Rook
  • 60,248
  • 49
  • 165
  • 242
  • 2
    Why would I want to do it without a loop? Just old habits. MATLAB is supposed to be loop-optimized now with JITA, but I try to avoid them whenever I can - and I have a strong feeling it is possible to solve this without loops. – Jacob Nov 16 '09 at 23:02
  • 1
    yes, ok, i can understand that. (on the opposite, i sometimes do stuff which can be done without a loop in a loop, since i find it easier to read <-- :( old habits, too :) – Rook Nov 16 '09 at 23:38
1

To answer the question, and for readability, please see:

  • ndmult, by ajuanpi (Juan Pablo Carbajal), 2013, GNU GPL

Input

  • 2 arrays
  • dim

Example

 nT = 100;
 t = 2*pi*linspace (0,1,nT)’;

 # 2 experiments measuring 3 signals at nT timestamps
 signals = zeros(nT,3,2);
 signals(:,:,1) = [sin(2*t) cos(2*t) sin(4*t).^2];
 signals(:,:,2) = [sin(2*t+pi/4) cos(2*t+pi/4) sin(4*t+pi/6).^2];

 sT(:,:,1) = signals(:,:,1)’;
 sT(:,:,2) = signals(:,:,2)’;
   G = ndmult (signals,sT,[1 2]);

Source

Original source. I added inline comments.

function M = ndmult (A,B,dim)
  dA = dim(1);
  dB = dim(2);

  # reshape A into 2d
  sA = size (A);
  nA = length (sA);
  perA = [1:(dA-1) (dA+1):(nA-1) nA dA](1:nA);
  Ap = permute (A, perA);
  Ap = reshape (Ap, prod (sA(perA(1:end-1))), sA(perA(end)));

  # reshape B into 2d
  sB = size (B);
  nB = length (sB);
  perB = [dB 1:(dB-1) (dB+1):(nB-1) nB](1:nB);
  Bp = permute (B, perB);
  Bp = reshape (Bp, sB(perB(1)), prod (sB(perB(2:end))));

  # multiply
  M = Ap * Bp;

  # reshape back to original format
  s = [sA(perA(1:end-1)) sB(perB(2:end))];
  M = squeeze (reshape (M, s));
endfunction
1

I highly recommend you use the MMX toolbox of matlab. It can multiply n-dimensional matrices as fast as possible.

The advantages of MMX are:

  1. It is easy to use.
  2. Multiply n-dimensional matrices (actually it can multiply arrays of 2-D matrices)
  3. It performs other matrix operations (transpose, Quadratic Multiply, Chol decomposition and more)
  4. It uses C compiler and multi-thread computation for speed up.

For this problem, you just need to write this command:

C=mmx('mul',X,Y);

here is a benchmark for all possible methods. For more detail refer to this question.

    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  <===================
Ali Mirzaei
  • 1,496
  • 2
  • 16
  • 27
1

I would like to share my answer to the problems of:

1) making the tensor product of two tensors (of any valence);

2) making the contraction of two tensors along any dimension.

Here are my subroutines for the first and second tasks:

1) tensor product:

function [C] = tensor(A,B)
   C = squeeze( reshape( repmat(A(:), 1, numel(B)).*B(:).' , [size(A),size(B)] ) );
end

2) contraction: Here A and B are the tensors to be contracted along the dimesions i and j respectively. The lengths of these dimensions should be equal, of course. There's no check for this (this would obscure the code) but apart from this it works well.

   function [C] = tensorcontraction(A,B, i,j)
      sa = size(A);
      La = length(sa);
      ia = 1:La;
      ia(i) = [];
      ia = [ia i];

      sb = size(B);
      Lb = length(sb);
      ib = 1:Lb;
      ib(j) = [];
      ib = [j ib];

      % making the i-th dimension the last in A
      A1 = permute(A, ia);
      % making the j-th dimension the first in B
      B1 = permute(B, ib);

      % making both A and B 2D-matrices to make use of the
      % matrix multiplication along the second dimension of A
      % and the first dimension of B
      A2 = reshape(A1, [],sa(i));
      B2 = reshape(B1, sb(j),[]);

      % here's the implicit implication that sa(i) == sb(j),
      % otherwise - crash
      C2 = A2*B2;

      % back to the original shape with the exception
      % of dimensions along which we've just contracted
      sa(i) = [];
      sb(j) = [];
      C = squeeze( reshape( C2, [sa,sb] ) );
   end

Any critics?

Dan
  • 51
  • 4
  • Thank you very much for sharing these codes. I tested the first one and it works magically well (I compared it with a code using nested for loops!). Could you explain please the logic behind that first code? Why that transposition at the end? Thanks again! – Shahram Khazaie Oct 12 '22 at 13:03
  • 1
    Well, the transposition in the end is for aligning dimensions of both vectors. We don't apply matrix multiplication here (i.e. * operation), we employ element-wise multiplication (i.e. .* operation), so our dimensions should be exactly the same; in this case we multiply two rows of the same length. B(:) represents a column, hence the transposition to make it a row as well. – Dan Dec 29 '22 at 16:59
0

I would think recursion, but that's the only other non- loop method you can do

Kevin
  • 19
  • 1
  • 3
0

You could "unroll" the loop, ie write out all the multiplications sequentially that would occur in the loop

µBio
  • 10,668
  • 6
  • 38
  • 56