丸井綜研

バイクとコンピュータで遊んでいるよ

行列の転置(Common Lisp)

音のデータ構造

Common Lispで音を扱うときに、どのようなデータ構造で保持しておくのが良いのか少し考えました。

Matlabと同様にサンプル数×チャンネル数という2次元配列で保持することもできますし、例えば、1チャンネル分のサンプル列をvectorとしておき、それを複数あつめたリストにするという方法もあります。後者の場合はvectorごとに異なるサンプル数になったり、要素のデータ型が異なる可能性もあるので、データの利用時に注意が必要になります。前者のほうが単純に扱えそうなので、とりあえずはMatlab同様に、Common Lisp内蔵のarrayを使って2次元配列にすることにしました。

さて、Common Lispの配列では#2A((1 2) (3 4) (5 6))とすることで、Matlabでいうところの[1 2 ; 3 4 ; 5 6]と同様の3×2行列になります。配列要素にアクセスするときには、たとえば(aref #2A((1 2) (3 4) (5 6)) 1 0)のようにして1行目・0列目の指定をします(添字は0はじまりで、このarefの結果は3です)。また(row-major-aref #2A((1 2) (3 4) (5 6)) 4)とすると、各行を一列に並べたときの何番目の要素というのが取れます(ここでは5が戻ります)。column-major-arefというのはなさそうなので、おそらく配列は各行を一列に並べた状態でメモリに確保されているのでしょう*1。各チャンネルのデータを個別に扱うことが多いのであればチャンネル数×サンプル数としたほうが良いのかもしれませんが、複数チャンネルが同期しているほうが好都合なのであればサンプル数×チャンネル数とするほうが良いということになりそうです。

さて、オレオレ音ライブラリに使う最終的なデータ構造をどうするかはもう少し悩むことにして*2、サンプル数×チャンネル数とチャンネル数×サンプル数を行ったり来たりできるようにしておきたいと思います。つまりは、転置行列を作りたいということです。

転置行列

与えられた行列がm×nであればn×mの配列を作って、対応する行列の要素をコピーするだけですので、それほど複雑ではありません。以下のように作ってみました。

(defun matrix-transpose (mtrx)
  "Transpose a matrix (two-dimensional array)."
  (assert (= (array-rank mtrx) 2))
  (let ((res (make-array (reverse (array-dimensions mtrx)))))
    (loop for r from 0 to (1- (array-dimension mtrx 0)) do
      (loop for c from 0 to (1- (array-dimension mtrx 1)) do
        (setf (aref res c r) (aref mtrx r c))))
    res))

こんなかんじになります。

(matrix-transpose #2A((1 2) (3 4)))
;;=> #2A((1 3) (2 4))

(matrix-transpose #2A((1 2) (3 4) (5 6)))
;;=> #2A((1 3 5) (2 4 6))

車輪の再発明してないでnumclを使えばいいんですが、なんとなく楽しくて……。

github.com

ベクトルも転置したい(2022/12/12追記)

ベクトルは1×Nの行列ではなく1次元配列#(1 2 3 ... N)です。それをN×1行列に転置しようとすると、Common Lispでは2次元配列#2A((1) (2) (3) ... (N))にしなくてはなりません。それも加味して、入力が1次元配列のときと2次元配列のときで場合分けをした関数にしてみました。

(defun matrix-transpose (mtrx)
"Transpose a matrix (two-dimensional array). If the input is a vector
this function creates an output matrix with size of input vector
length by 1."
  (assert (<= (array-rank mtrx) 2))
  (if (= (array-rank mtrx) 1)
      (let ((res (make-array `(,(length mtrx) 1))))
        (loop for r from 0 to (1- (length mtrx))
              do (setf (aref res r 0) (aref mtrx r)))
        res)
      (let ((res (make-array (reverse (array-dimensions mtrx)))))
        (loop for r from 0 to (1- (array-dimension mtrx 0))
              do (loop for c from 0 to (1- (array-dimension mtrx 1))
                       do (setf (aref res c r) (aref mtrx r c))))
        res)))

これでベクトルも転置することができます。ただ、N×1行列を転置してもベクトルには戻らず、2次元配列のままだという副作用?はあります。しばらく使ってみて、困るようだったらN×1行列の転置がベクトルになるように書き換えます。

(matrix-transpose #(1 2 3))               ;=> #2A((1) (2) (3))
(matrix-transpose #2A((1) (2) (3)))       ;=> #2A((1 2 3))

*1:C/C++などが代表的なrow-major orderingです。一方でMatlab、R、Juliaといった科学計算に強い言語はcolumn-major orderingになっていることが多い気がします。

*2:標本化周波数などのメタデータも保持したいので構造体かCLOSを使うことになるかと思います。