How to unroll a cell array into a column vector?

Viewed 725

I have a cell array, where each cell is a matrix of different size. I want to concatenate every elements of all the matrices into one column vector. So

X1=rand(2,3);  % Total 6 elements.
X2=rand(3,4);  % Total 12 elements.
X = {X1, X2};  % Total 18 elements in a 2-cell array.

% How to unroll everything from X into one giant column vector of size 18x1 ?

% Edit: The above example only shows two matrices, X1 and X2, but there could be n such matrices in the cell array.
X = {X1, X2, ... , Xn};

I can do this with a loop, but curious if there is a faster way. I looked into cell2mat and reshape, but can't get them to do this (dimension mismatch errors). Searches on web don't seem to help.

This is my solution with a for loop:

unrolled_X=[];
for i=1:length(X)
  unrolled_X = [unrolled_X; X{i}(:)];
end

Edit 2: Thanks for the answers. I learned something new about perf. I benchmarked the 3 solutions by @HansHirse, @lucien-xhh, and @wolfie. A bit surprising results. Note I am actually running Octave (version 5.2.0.).

So the solution without cell2fun was fastest. Other 2 solutions both use cellfun, but was surprisingly close to the fastest, while the other was double of the fastest. Code and results follow.

Code


function run_benchmarks()
  X={};
  for i=1:5
    X{i}=rand(1000,1000);
  end
  
  fprintf("unroll_with_cellfun: %f\n", benchmark(@()unroll_with_cellfun(X), 100));
  fprintf("unroll_with_cellfun2: %f\n", benchmark(@()unroll_with_cellfun2(X), 100));
  fprintf("unroll_with_vertcat: %f\n", benchmark(@()unroll_with_vertcat(X), 100));
  
end

function unrolled_X = unroll_with_cellfun(X)
  unrolled_X = cell2mat(cellfun(@(x) x(:), X, 'UniformOutput', false).');
end

function unrolled_X = unroll_with_cellfun2(X)
  unrolled_X = cell2mat(cellfun(@(x) x(:).', X, 'UniformOutput', false)).';
end

function unrolled_X = unroll_with_vertcat(X)
  unrolled_X = cell(length(X),1);
  for ii = 1:length(X)
    unrolled_X{ii} = X{ii}(:);
  end
  unrolled_X = vertcat( unrolled_X{:} );
end


function elapsed_time_in_seconds = benchmark(f, N)
  % benchmark runs the function 'f' N times and returns the elapsed time in seconds.

  timeid = tic;
  for i=1:N
    output = f();
  end
  elapsed_time_in_seconds = toc(timeid);
end

Results:

octave:161> run_benchmarks
unroll_with_cellfun: 1.240324
unroll_with_cellfun2: 0.606957   <-- Close to fastest.
unroll_with_vertcat: 0.597657    <-- FASTEST

Surprised to see cellfun2 is almost same as the fastest solution, and that cellfun takes 2x time even it's almost same as cellfun2.

4 Answers

Preallocating your loop will be more performant and better practise

unrolled_X = cell(length(X),1);
for ii = 1:length(X)
  unrolled_X{ii} = X{ii}(:);
end
unrolled_X = vertcat( unrolled_X{:} );

Any shorthand like cellfun is basically this loop in disguise, and cell2mat uses a loop for concatenation under the hood but has additional checks so may actually cause a minor slow-down.

You could use cellfun to flatten all matrices using an anonymous function. Then, feed the modified cell array as "column vector" to cell2mat, i.e. transposing the modified cell arry beforehand.

Here's an example, in which all that becomes a one-liner (tested with MATLAB Online):

X1 = rand(2, 3);
X2 = rand(3, 4);
X3 = rand(1, 5);
X = {X1, X2, X3}

unrolled_X = cell2mat(cellfun(@(x) x(:), X, 'UniformOutput', false).')

Some examplary output (generated with Octave 6.1.0):

X =
{
  [1,1] =
     0.2781   0.3303   0.7424
     0.3314   0.4878   0.6254

  [1,2] =
     0.567344   0.848374   0.035421   0.171656
     0.359233   0.482265   0.327617   0.188834
     0.088272   0.771683   0.763845   0.181979

  [1,3] =
     0.9843   0.7817   0.9399   0.5453   0.3310
}

unrolled_X =

   0.278085
   0.331438
   0.330314
   0.487774
   0.742395
   0.625360
   0.567344
   0.359233
   0.088272
   0.848374
   0.482265
   0.771683
   0.035421
   0.327617
   0.763845
   0.171656
   0.188834
   0.181979
   0.984326
   0.781678
   0.939857
   0.545296
   0.331043

In fact, using cellfun with an anonymous function is something like a loop in disguise, but it should be still more efficient than your loop, since you're appending to an array.

One Solution: Try X = {[X1(:); X2(:)]} , then use cell2mat

Two Solution:

clear
X1 = rand(2,3);  
X2 = rand(3,4);
X3 = rand(4,5);
X = {X1, X2, X3};
XX = cellfun(@(x) x(:)', X, 'UniformOutput', false);
cell2mat(XX)

If you are using Octave you can use cellindexmat:

unrolled_X = vertcat(cellindexmat(X, ':'){:});
Related