2013年6月10日月曜日

matlab: セル配列で困っている人へ

matlab に慣れてきた人でも,「セル配列」をすっきり理解しきれた気がしない,という人は多いかもしれない.そうした向きへセル配列の解説を試みる.

まとめ 兼 目次

  • セル配列とは「セル(=1×1のセル配列)」を並べた配列である.
  • 「セル」は,任意のデータを中括弧で囲むと出来る.B = {A} など.
    •  どんなタイプのものも中括弧 {} で囲むとセルになるということ.
  • セル配列の作り方は,根源的には 1) データを中括弧 {} で囲む(大括弧 [] 同様,複数のデータを一度に囲っても良い),2) cell 関数を使って空のセル配列を作る,3) struct2cell を使って構造体から変換する,の3通りで,この他に num2cell, mat2cell, cellstr という変換関数を使う方法もある.もちろん,個々のセルを配列として並べて大括弧 [] で囲んでも良い.
  • 小括弧 () を用いてアクセスすると,通常の配列と同様,部分配列を参照できる.
    • 取り出された部分配列もセル配列.
  • 中括弧 {} を用いてアクセスすると,セルの中身を取り出せる.
    • インデクス(=サブスクリプト)の解釈(参照される要素がどれになるか)は小括弧と同様.
    • 複数要素を参照すると,複数の文(式)を次々に実行(評価)したのと同じ結果になる.実行順序はリニアインデックス順.
  • 大事なのでもう一度.中括弧 {} アクセスは,複数文相当の表現!
  • セル配列に代入するには,次のいずれかの方法をとる.
    • 左辺を小括弧指定,右辺をサイズの適合するセル配列にする.例: a(1:3) = {'foo', 100, true};
    • 左辺を大括弧+中括弧指定,右辺を複数文相当の表現にする.例:[a{:}] = deal(100, 'strings', true);
  • 複数文相当の表現の使い道
    • 小括弧 () 受け:サブスクリプト,関数に入力引数を渡す
    • 中括弧 {} 受け:セル配列のサブスクリプト
    • 大括弧 [] 受け:連結,関数の出力引数を受け取る

注意


では,始めよう.



セル配列の定義


ざっくりいこう.matlab におけるセル配列とは,ひとつひとつの「セル」を並べた配列である.ひとつのセルとは,データ(形式は問わない!)を中括弧 {} ではさんだものである.
本来 matlab の配列とは同じクラスのものを箱型(多次元配列なら超箱型)に並べたものだ.そこで,異なるクラスのものを配列に仕立てるために,各々を「セル」という同じクラスのコンテナに入れてから並べている,と理解すればよい.

セルとは


データに中括弧を付けるとセルができる.
>> a = {100}
a =
    [100]
どんなタイプのデータでも,中括弧をつけるとセルが出来る.
clear b
b.comment = 'this is a struct';
b.score = 3;
c = {b};

>> c
c =
    [1x1 struct]
本当にセルが出来たのだろうか?データの種類を class で確認してみよう.
>> class(a)
ans =
cell
>> class(c)
ans =
cell
さらに,whos でも確認しておこう.
>> whos a c
  Name      Size            Bytes  Class    Attributes
  a         1x1               120  cell              
  c         1x1               504  cell 
>> help class として情報を見ると,class が 'cell' という文字列を返す場合,そのデータ(オブジェクト)はセル配列だと分かる.つまり,上の a や c はセル配列になっている.
なお,セル配列 a や c をさらにセルに入れることも出来る.{a} や {c} を試してみると良い.
実は,(配列でない)「ひとつのセル」というのは概念上の話でしかない.matlab では基本すべてのデータは配列なので,「ひとつのセル」とは,「1×1のセル配列」に他ならない.

セルの中身の取り出し方(参照の仕方)


a がひとつのセル(=1×1のセル配列)だとすると,a の中身を取り出すには,{1} をつけて
>> a{1}
とすればよい.作成と取り出しを行ってみれば,
>> a = 100
a =
   100
>> b = {100}
b =
    [100]
>> c = b{1}
c =
   100
>> isequal(a,c) % セルに入れた a とセルから取り出した c は等しいか?
ans =
     1
>> whos a b c
  Name      Size            Bytes  Class     Attributes
  a         1x1                 8  double             
  b         1x1               120  cell               
  c         1x1                 8  double             
となる.

いかにも配列の第1要素を参照している風情だが,セル配列の参照法,中身の取り出し方の詳細は後の節に回す.次のステップではセル配列の作り方をより一般に見ておこう.

セル配列の作成


matlab に用意されているセル配列の作成法を挙げてみよう.
  1. セル配列はセルの配列であるから,作ったセルを並べればよい.
    • a = [ {100}, {'strings'}, {true} ];
    • >> a
    • a =
          [100]    'strings'    [1]
    • >> whos a
        Name      Size            Bytes  Class    Attributes
        a         1x3               359  cell  
  2. 中括弧 {} の機能を使うと,より簡単に書ける.結果は方法 1 と全く同じになる.カンマ(, comma)やセミコロン(; semicolon)の用法も大括弧 [] と同じ
    • a = { 100, 'strings', true };
  3. 関数 cell を使って,「各セルの中身が空」のセル配列を作る.いわば数値配列における zeros(M, N) と同じイメージ.
    • a = cell(4, 3, 2);
    • >> a
      a(:,:,1) =
           []     []     []
           []     []     []
           []     []     []
           []     []     []
      a(:,:,2) =
           []     []     []
           []     []     []
           []     []     []
           []     []     []
    • >> whos a
        Name      Size             Bytes  Class    Attributes
        a         4x3x2              192  cell              
  4.  何か別のものからセル配列に変換する(各々の詳細はリンク箇所で後述).
    1. 数値配列からセル配列に変換する.num2cell を用いる.
    2. 任意の配列からセル配列に変換する.mat2cell を用いる.
    3. 構造体配列から変換する.struct2cell を用いる.struct2cell は built-in function である.
    4. 文字列から変換する.cellstr を用いる.
基本的な作成法は以上である.num2cell.m, mat2cell.m, cellstr.m は内部で中括弧 {} を使っているため,根源的な作成法は,関数 cell の使用,中括弧 {} の使用,struct2cell の使用,の3通りだけである(少なくとも R2013a の時点では).

セル配列の参照と代入:「小括弧 () は部分配列」編


セル配列は配列であるから,一部を取り出すには小括弧 () でアクセスすればよい.小括弧 () で参照した結果は依然としてセル配列である.
>> a = [ {100}, {'strings'}, {true} ]
a =
    [100]    'strings'    [1]
>> whos a
  Name      Size            Bytes  Class    Attributes
  a         1x3               359  cell              
>> b = a(2:3)
b =
    'strings'    [1]
>> whos a b
  Name      Size            Bytes  Class    Attributes
  a         1x3               359  cell              
  b         1x2               239  cell              
次に代入を行ってみよう.上で作った a の第2・第3要素を上書きする.
>> a(2:3) = [ {false}, {'oops!'} ]
a =
    [100]    [0]    'oops!'
小括弧 () でアクセスする場合,通常の配列と全く同様の振る舞いである.各要素がセルというだけ.

セル配列の参照と代入:「中括弧 {} は中身の出し入れ」編


ついに来た!中括弧 {} を用いることで,セル配列の中身を柔軟に参照する方法を述べる.ここが本記事の本丸といっても過言ではない.

(ひとつの)セルの中身を取り出すには {1} をつける,と前に述べた.では,通常の配列の参照で複数の要素に一度にアクセスするように,多数のセルの中身を一度に取り出すことが出来るだろうか?もちろん出来る.そのためには,通常の配列を (1:10, 1:2:end) などとして参照するのと同様,セル配列に {1:10, 1:2:end} などとしてアクセスすればよい.

実際にやってみよう.まずは準備.
% 2 x 3 のセル配列を作る.
>> a = { 100, 'strings', true; [1,2,3], {'a', 'bc'}, [] }
a =
    [       100]    'strings'     [1]
    [1x3 double]    {1x2 cell}     []

% 小括弧 () によるアクセスで,(1,1), (2,1), (1,3), (2,3) 要素を並べた部分セル配列ができる.
>> a(1:2, [1, 3])
ans =
    [       100]    [1]
    [1x3 double]     []
ここで,小括弧 () でなく,中括弧{} を使ってセルの中身に一度にアクセスしてみよう.
% 中括弧 {} によるアクセス (1,1), (2,1), (1,3), (2,3) 要素の中身を一度に取り出す
>> a{1:2, [1, 3]}
ans =
   100
ans =
     1     2     3
ans =
     1
ans =
     []
小括弧のときとの結果の違いに気づいただろうか?そう,ans が複数,参照した要素(セル)の数だけあるのだ.この結果は,
>> a{1,1}, a{2,1}, a{1,3}, a{2,3}
の結果と全く同じ.つまり,セル配列を中括弧 {} で参照する操作とは,参照した要素のリニアインデクス順(※)に,セルの中身を順番に,かつ別々に取り出す操作なのである.一度に取り出せてはいる.しかし,一文について一要素ずつ取り出すのを,複数の文連続して行っている状態だ.これが,あくまでも一文でしかない通常の配列の参照に対し,本質的に異なる点だ.

Linear indexing - Matrix Indexing - MATLAB & Simulink (mathworks website)
通し番号順.1次元目から順にカウントアップしていく.

中括弧 {} による参照結果を受け取る方法
この中括弧 {} による参照結果をうまく受け取るには,少し工夫がいる.まずは,何の工夫も無く受け取ろうとしてみよう.
>> b = a{1:2, [1, 3]}
b =
   100
んん?どうやら最初の一要素しか受け取らなかったようだ.ここで思い出すのは,関数の出力引数の受け取り規約だ.MATLAB では,「複数の出力引数を返す関数の出力を,一つの変数だけで受け取ると,最初の出力引数のみ渡される」という仕様になっている.例えば,ベクトルに対し関数 min を使うと,第一出力引数には最小値が入り,第二出力引数には最小だった要素の番号が入る.
% min の出力を変数2つで受け取る
>> [v, k] = min( [100, 200, 50] )
v =
    50
k =
     3
このとき,変数一つのみで受け取ると,最初の出力引数つまり最小値が割り当てられる.
% min の出力を変数1つで受け取る
>> v = min( [100, 200, 50] )
k =
    50
これと同じことがセル配列の参照・受け取りにも起こっているのだ.したがって,複数のセルの中身を受け取るには,セルの数だけの変数を用意すればよい.関数の出力引数の受け取り方を思い出して,大括弧 [] を左辺に使う.
% セル配列の4つの要素の中括弧参照をを変数4つで受け取る
>> [b, c, d, e] = a{1:2, [1, 3]}
b =
   100
c =
     1     2     3
d =
     1
e =
     []
見事,これでそれぞれ受け取ることが出来た.

ここで,である.左辺をよく見てみると,「大括弧 [] で4つの文 b c d e を囲った」 ように見えないだろうか?そう見立てたとき,セル配列の中括弧参照が「複数の文を連続評価する」ことを思い出さないだろうか?これを応用すると,f をセル配列として,左辺に大括弧入り中括弧展開セル配列 [f{:}] を配せば,b c d e の例と同様のことがセル配列 f を受け皿として出来そうである.やってみよう.
% 複数の出力引数をセル配列で一度に受け取る
>> f = cell(4, 1);
>> [f{:}] = a{1:2, [1, 3]};
>> f{:}
ans =
   100
ans =
     1     2     3
ans =
     1
ans =
     []
完全に狙い通りである.左辺でも右辺でも,「複数の文をまとめた体裁」をとっている箇所には,「中括弧 {} によるセル配列アクセス」をあてがうことが出来る.これがセル配列参照の極意の一つといっていいだろう.では,「複数の文をまとめた体裁」はどこに出てくるか?については後ろに述べる.

中括弧 {} による指定対象に代入する方法
これは前記「受け取る方法」 ですでに垣間見えた.セル配列 f の各セルに何かを代入したいときには,
[f{:}] =セル配列の要素数と同数の出力引数を返す表現
とすればよい.この右辺にどんなパターンが可能かというと,

  • 複数の出力引数を返す関数
例えば,関数 max は,最大値とその位置を返す.その他,size, find から textscan (strread) にいたるまで,該当例は多数あり,実用上もセル配列で受けると便利なことが多い.

>> r = rand(4, 3)
r =
    0.6381    0.1359    0.5193
    0.8755    0.9457    0.2041
    0.5850    0.5873    0.3957
    0.7167    0.0371    0.1791
>> f = cell(1, 2);
>> [f{:}] = max(r);
>> f{:}
ans =
    0.8755    0.9457    0.5193
ans =
     2     2     1
 % a{1} は各列中の最大値を,a{2} はそれらが各列内で何番目に位置するかを格納.
  • 特に, 入出力の整理に特化した関数 deal
% deal は,入出力の整理・分配に便利.
>> f = cell(1, 3);
>> [f{:}] = deal(100, 'strings', true);
>> f
f =
    [100]    'strings'    [1]
  • セル配列の中括弧参照
直前に見た通り.
>> a = { 100, 'strings', true; [1,2,3], {'a', 'bc'}, [] };
>> b = cell(size(a));
>> b(:) = a(:);
>> c = cell(size(a));
>> [c{:}] = a{:};
>> isequal(a,b,c)
ans =
     1
  •  構造体配列の「フィールド」参照と「小括弧+フィールド」参照
これを知っていればかなりの Matlab 通?当ブログの記事「matlab: 構造体配列の参照法とその周辺」にも関連.

% 構造体配列を作る
>> s = struct('name', {'Fredrikson', 'Juksu'}, 'occupation', {'inventor','idler'});
>> s(1),s(2)
ans =
          name: 'Fredrikson'
    occupation: 'inventor'
ans =
          name: 'Juksu'
    occupation: 'idler'

% 構造体の「フィールド」参照は例の「複数の文」に該当する!
>> s.name
ans =
Fredrikson
ans =
Juksu
>> s(:).name
ans =
Fredrikson
ans =
Juksu

% そこで,大括弧入り中括弧展開セル配列で受け取れる.
>> names = cell(size(s));
>> [names{:}] = s.name
names =
    'Fredrikson'    'Juksu'
といったあたりが代表的だろう.

ちなみに,渡すものが足りないとエラーが出る. 渡すものが多すぎる場合は右辺によってまちまち.
% 2つの入れ物に1つしか渡さないとエラー
>> f = cell(1, 2);
>> [f{:}] = 100
??? Too many output arguments.

複数文相当の表現の使い道

セル配列への中括弧 {} アクセスが,複数文の評価に相当することはここまで見てきた.では,これは一体何に使えるのだろうか?実は,MATLAB では,無意識のうちに複数文相当の表現になっている箇所が多数あるのだ.これらは特に括弧 (), {}, [] の中に出現する.では見ていこう.

小括弧 ()

小括弧 () 自体の説明の前に,カンマ , (コンマ)について説明したい.小括弧 () の中にカンマ , で何かを並べるのはよく見たり使ったりするところである.MATLAB のヘルプ(>>help punct)から抜粋すると

   ,    カンマ。行列のサブスクリプトや関数の引数を分離するために使います。
       複数のステートメントの各ステートメントを区別するためにも使います。
       この場合、カンマの画面表示を抑制するセミコロンに置き換えることも
       できます。
  ,   Comma.  The comma is used to separate matrix subscripts
      and arguments to functions.  It is also used to separate
      statements in multi-statement lines. In this situation,
      it may be replaced by a semicolon to suppress printing.

とのことである.機能を理解するには,まず何よりもこのカンマ , は文を区切るものだと考えると分かりやすい.すると,小括弧 () 内のサブスクリプトや関数引数がカンマ , で区切られているのは,複数の文を与えていると解釈できる.そう,ここにはセル配列への中括弧 {} アクセスが利用できるのである.

使いどころA1.関数に一度に複数の引数を与える.
% サイズの異なる行列をまとめてセル配列を作る.
A = {rand(2,3), rand(1,2), rand(4,2)};

% 各々をブロック対角要素にした行列を作成.
B = blkdiag(A{:});

% 今のは次と同値.
% B = blkdiag(A{1}, A{2}, A{3});

% こんなふうになります.
>> B
B =
    0.1644    0.0430    0.6070         0         0         0         0
    0.9793    0.1186    0.9908         0         0         0         0
         0         0         0    0.5879    0.7948         0         0
         0         0         0         0         0    0.2584    0.5236
         0         0         0         0         0    0.9823    0.6158
         0         0         0         0         0    0.5148    0.0140
         0         0         0         0         0    0.2452    0.1137
使いどころA2.サブスクリプト(インデクス)を一度に指定する.
% 多次元配列を作る.
sz = 10:-1:2
A = rand(sz);

% 第 2,2,2,2,2,2,2,2,2 番の要素を抜き出す.
indc = repmat( {2}, [1, length(sz)] ); % セルを並べてセル配列を作る.
% indc = num2cell(repmat(2, [1, length(sz)])); %としても同じ.
B = A(indc{:})

% 今のは次と同じ.大幅に楽でしょ?
% B = A(2, 2, 2, 2, 2, 2, 2, 2, 2);

% これは通し番号(リニアインデクス)で何番にあたるのか?
li = sub2ind(sz, indc{:}); % 関数へ渡すのは,使いどころA1で見た通り.
C = A(li)

% あってる?
isequal(B, C)
もちろん代入側(左辺)にも使える.やってみて下さい.
ただ,インデクスをセル配列展開で指定する方法では始点終点なしのコロン : や end はそのままでは使えない.興味のある方は subsref, subsasgn のヘルプを参照されたし.

使いどころA3.varargin の引渡し.

仕組みは使いどころ1と同じ.関数 m ファイル内で,
function varargout = myfunc(varargin)
[varargout{:}] = someotherfunc(varargin{:});
のようにすると,someotherfunc のラッパーが簡単に書ける.上の場合,myfunc を呼び出すのと someotherfunc を呼び出すのは,ほぼ全く同じ働きをする.
b = varargin{1} * 3;
a = someotherfunc(varargin{2:end});
など,いろいろな展開もできる.(参考:matlab: セル配列の参照法とその周辺)

中括弧 {}

ここまで見たとおり,中括弧 {} はセル配列の参照と作成に使われる.セル配列以外の用途は筆者の知る限り存在しない(MATLAB で >>help paren としても,他の用途は出てこない.オブジェクト指向で演算子オーバーロードすれば別?)

大括弧 []
大括弧 [] は通常は連結のために使われる.関数 cat の親類のような働きをする.カンマ , や空白文字で区切って並べられてものは横,つまり2次元目に,セミコロン ; で並べられたものは縦,つまり 1 次元目に並ぶ.セル配列の中括弧 {} 参照による展開は,カンマ , で並べるのと同値となる.
代入文の左辺では,これまで見てきた「大括弧入り中括弧展開セル配列」の形で,複数の出力引数を一度に格納できる.

使いどころC1. 横に連結.
% セル配列を作る.このセル配列のサイズはこの例では重要ではない.
A = { rand(2, 1); ones(2, 3); rand(2, 2)};
% 横に連結してみよう.中括弧 {} でコロン : 参照すると,セルはリニアインデクス順に評価される. 大括弧内に入れると,[ A{1}, A{2}, A{3} ] と同値になる.
B = [ A{:} ];
>> A, B
A =
    [2x1 double]
    [2x3 double]
    [2x2 double]
B =
    0.8491    1.0000    1.0000    1.0000    0.6787    0.7431
    0.9340    1.0000    1.0000    1.0000    0.7577    0.3922
% これは,cat で2次元目を連結するのと同じである.
C = cat(2, A{:});
>> isequal(B, C)
ans =
     1
もちろん,cat(1, A{:}) とすれば縦にも連結できる.

使いどころC2. 縦横に連結.
% セル配列を作る.このセル配列のサイズはこの例では重要ではない.
A = { rand(2, 1), ones(2, 3), rand(3, 2), zeros(3, 2)};
% 縦横に連結してみよう.[ A{1}, A{2}; A{3}, A{4} ] を簡単に.
B = [ A{1:2}; A{3:4} ];
>> A, B
A =
    [2x1 double]    [2x3 double]    [3x2 double]    [3x2 double]
B =
    0.0971    1.0000    1.0000    1.0000
    0.8235    1.0000    1.0000    1.0000
    0.6948    0.0344         0         0
    0.3171    0.4387         0         0
    0.9502    0.3816         0         0
使いどころC3. 大括弧入り中括弧展開セル配列による複数出力の格納

かなり便利な用法.前記「中括弧 {} による指定対象に代入する方法」ですでに述べた.
% svd の3つの出力引数を格納する.
A = cell(1, 3);
B = rand(10, 100);
[A{:}] = svd(B);
% とすれば,B = A{1} * A{2} * A{3}' となる.各々のサイズは
>> A
A =
    [10x10 double]    [10x100 double]    [100x100 double]

最後に


いかがだっただろうか.ひとたび使い方を覚えれば,非常に便利に使えるのがこのセル配列である. セル配列を想定した個々の関数等については,また個別の記事にする予定.

他の言語に慣れている人には,ポインタ配列と言えば分かりよいかもしれない.() ではポインタ自体が,{} ではポインタの指すものが意味される.

疑問質問等はコメントで頂ければお答えできるかもしれない(あまり期待はしないでください).

0 件のコメント:

コメントを投稿