array-operations: 配列操作ライブラリ

概要

このページでは配列操作ライブラリであるarray-operationsについて簡単に説明します。ANSI標準の配列操作については第15章「配列」を参照してください。

array-operationsは以下の状態でロードされていることとします。
(asdf:load-system :array-operations)
; => T

なお、公式ドキュメントではパッケージのニックネームがaoとなっていますが、ソースコードではaopsであるため、このページでもaopsを使用します。
(package-nicknames :array-operations)
; => ("AOPS")

一般的なオペレータ

array-operationsには汎用的なオペレータがいくつか定められています。

dims総称関数: 配列のディメンションの取得

ANSI Common Lispにおけるarray-dimensions関数に相当するのがdims総称関数です。
(aops:dims #2a((1 2 3)(4 5 6)))
; => (2 3)

この場合、2行・3列の行列(2次元配列)であることを意味します。

dim総称関数: 配列の特定のディメンションの取得

配列の特定の次元における要素数を取得したければdim総称関数を用います。ANSI標準のarray-dimension関数に相当します。
(aops:dim #2a((1 2 3)(4 5 6)) 1)
; => 3

行列では0が行、1が列を表す軸番号です。

rank総称関数: 配列のランクの取得

一般的にN次元配列と呼ぶ場合のNを取得します。ANSI標準のarray-rank関数に相当します。
(aops:rank #2a((1 2 3)(4 5 6)))
; => 2

nrow, ncol総称関数: 行数、列数の取得

配列の中でも頻繁に利用される2次元配列(行列)の行数を取得するにはnrowを、列数を取得するにはncolを使用するのが最も簡単です。
(aops:nrow #2a((1 2 3)(4 5 6)))
; => 2

(aops:ncol #2a((1 2 3)(4 5 6)))
; => 3

matrix?, square-matrix?関数: 行列・正方行列の判定

配列が行列であるかどうかを判定するにはmatrix?述語関数を、正方行列(行数と列数が等しい2次元配列)であるかどうかを判定するにはsquare-matrix?述語関数を使用します。
(aops:matrix? #2a((1 2 3)(4 5 6)))
; => T

(aops:square-matrix? #2a((1 2 3)(4 5 6)))
; => NIL

Common Lispのコーディングスタイルでは述語関数にはpを付けるのが好まれますが、SchemeというLispの他の方言では疑問符が好まれます。ここではSchemeスタイルが使用されています。

次元に関するオペレータ

array-operationsでは配列の次元を変更してベクトルにしたり、変形したりするオペレータが定められています。

displace関数: 参照配列の作成

ANSI Common Lisp標準のmake-array関数には標準仕様入門では紹介していない2つのキーワード引数があります。:displaced-to:displaced-index-offsetです。

これらのキーワード引数はdisplaced arrayを作るために使用されます。これは訳が難しいのですが、あえて付けるとすれば「参照配列」でしょうか。個別の配列ではなく、他の配列への参照を格納した配列です。つまり、元の配列の値が変われば自動的に参照配列も変わります。

array-operationsのdisplace関数は前述の2つのキーワード引数を使って参照配列を作るものです。以下の例を参照してください。
;; 実体のある通常の配列を生成し、束縛しておく
;;   2次元, 8要素 = 2 * 4
(defparameter *a* #2a((1 2 3 4)(5 6 7 8)))
; => *A*

;; 1次元への変更
;;  (1次元, 8要素)
(aops:displace *a* '(8))
; => #(1 2 3 4 5 6 7 8)

;; 3次元への変更
;;  (3次元, 8要素 = 2 * 2 * 2)
(aops:displace *a* '(2 2 2))
; => #3A(((1 2) (3 4)) ((5 6) (7 8)))

これだけだと単に次元を変更しているだけのように見えますが、返り値である参照配列は*a*へのポインタを含んでいるため、*a*を変更すると値が変わります。
;; 1次元に変更した参照配列を*b*に束縛する
(defparameter *b* (aops:displace *a* '(8)))
; => *B*

*b*
; => #(1 2 3 4 5 6 7 8)

;; 参照先である*a*を変更する
(setf (aref *a* 0 1) 20)
; => 20

;; *b*も変更されている
*b*
; => #(1 20 3 4 5 6 7 8)

このように元の配列の要素への参照を使いながら別の次元を持つ配列を作るのがANSI標準のmake-array関数における:displaced-toキーワード引数の役割であり、それを簡単に利用するのがarray-operationsのdisplace関数です。

ちなみに、もう一つのキーワード引数である:displaced-index-offsetは配列における参照の始点を指定するのに使用します。例えば、3のある位置から4つの要素を参照して2行2列の参照配列を作るには、以下のようにします。
;; 2次元への変更(オフセットあり)
;;  (2次元, 4要素 = 2 * 2, 参照起点 = 2 (3番目の要素))
(aops:displace *a* '(2 2) 2)
; => #2A((3 4) (5 6))

flatten関数: ベクトル化

flatten関数はdisplace関数の特殊ケースで、常に全ての要素への参照を持つ1次元の参照配列(ベクトル)を生成します。
(aops:flatten #2a((1 2)(3 4)))
; => #(1 2 3 4)

split関数: ランクの切り下げ

split関数もdisplace関数の特殊ケースですが、元の配列の構造に近い重層構造を保ちながら、配列の次元を切り下げるという関数です。

例えば行列(2次元配列)をベクトル(1次元配列)に切り下げると、通常は素の構造が失われます。
;; displaceで1次元に切り下げる場合
(aops:displace #2a((1 2)(3 4)) '(4))
; => #(1 2 3 4)

;; flattenで1次元に切り下げる場合
(aops:flatten #2a((1 2)(3 4)))
; => #(1 2 3 4)

しかし、split関数で次元を切り下げると、多次元配列から「多重配列」のような形式に切り替わります(ネイティブで多次元配列がサポートされないC言語のようなプログラミング言語では、このような配列を「多次元配列」と呼びますが、実際には「配列の配列」であるため、「多重配列」という言葉の方がふさわしいと思います)。
;; 1次元への変更(多重配列化)
(aops:split #2a((1 2)(3 4)) 1)
; => #(#(1 2) #(3 4))

このように多重配列にして何が嬉しいかというと、データの一部を直接「シーケンス」として扱うことができるようになるという点です。
(reduce #'+ (svref * 0))
; => 3

上の例では、多重配列の1行目(添字番号0)のデータを全て合計しています(1 + 2)。なお*というのは処理系において直前の評価結果を取得するスペシャル変数です。標準仕様入門の第25章「周辺分野」で説明しています。

実際には行ではなく列の和や平均、分散を求めることが多いため、行列を転置してから扱うことが一般的です。そのような処理は同じくarray-operationsのmargin関数を使う方が便利です。

なお、ランク(一般的な意味での次元)に0または元の配列と同じランクを指定した場合、元の配列がそのまま返り値となります。

subアクセッサ: 配列の部分取得

配列の部分取得は一般的にはcl-sliceを利用すべきですが、参照配列の機能を使って部分的に取得する場合はarray-operationsのsubアクセッサを使用することができます。
(aops:sub #2a((1 2)(3 4)) 0)
; => #(1 2)

(aops:sub #2a((1 2)(3 4)) 1)
; => #(3 4)

(aops:sub #2a((1 2)(3 4)) 0 0)
; => 1

(aops:sub #2a((1 2)(3 4)) 0 1)
; => 2

(aops:sub #2a((1 2)(3 4)) 1 0)
; => 3

N次元配列においてN個の部分指定を行うとANSI標準のarefアクセッサと同じ動作を行います。

このsubはアクセッサとして定義されていますので、setfマクロと共に使用することができます。
(defparameter *a* #2a((1 2)(3 4)))
; => *A*

(setf (aops:sub *a* 1) #(30 40))
; => #(30 40)

*a*
; => #2A((1 2) (30 40))

partitionアクセッサ: 配列の範囲取得

行列に対してsubアクセッサを使用する場合は特定の行または要素にしか使用できませんが、partitionアクセッサを使用すると行を範囲で指定することができます。
(aops:partition #2a((1 2)(3 4)(5 6)(7 8)) 1 3)
; => #2A((3 4) (5 6))

cl-sliceを使用する場合は以下と等価です。
;; cl-sliceをロードしておく
(asdf:load-system :cl-slice)
; => T

;; 切り取る範囲を指定する
(cl-slice:slice #2a((1 2)(3 4)(5 6)(7 8)) '(1 . 3) t)
; => #2A((3 4) (5 6))

partition(setf partition)関数が同時に定義されているのでアクセッサであり、setfマクロと共に使用することが可能です。

なお、cl-sliceのslice総称関数を使用する場合は行だけでなく列も個別に指定できるため、より複雑な切り取りが可能です。
;; 2次元配列の2〜3行目の2列目を取得する
(cl-slice:slice #2a((1 2)(3 4)(5 6)(7 8)) '(1 . 3) 1)
; => #(4 6)

subvecアクセッサ: ベクトル専用のpartition

配列が1次元(ベクトル)である場合はpartitionよりもsubvecを使用した方が余計な処理がないため高速です。
(aops:subvec #(1 2 3 4 5) 1 3)
; => #(2 3)

;; 結果は同じだが、配列の次元の取得など余計な処理がある
(aops:partition #(1 2 3 4 5) 1 3)
; => #(2 3)

combine関数: ランクの切り上げ

split関数とは逆に、低い次元の多重配列から高次元の多次元配列を生成するのがcombine関数です。
(aops:combine #(#(1 2) #(3 4)))
; => #2A((1 2) (3 4))

reshape, reshape-col, reshape-row関数: 自動調整機能付きdisplace

reshape関数はdisplace関数をより便利にしたものです。

多次元配列の次元(ディメンション)は、要素の数が特定されていればフル指定しなくても自動的に計算して求めることができます。例えば、要素数が6の2次元配列の場合、行数が2と決まれば列数は自動的に3と決まります。一般的に言えば、要素数が決まっている状態でN次元配列のディメンションはN-1個分かれば残りの1つは自動的に計算されます。

displace関数にこの機能を追加したのがreshape関数です。例えば、3行・2列の行列を2行・3列に変更するには、displaceの場合、ディメンションを明示する必要があります。
(aops:displace #2a((1 2)(3 4)(5 6)) '(2 3))
; => #2A((1 2 3) (4 5 6))

しかし、reshapeの場合は行または列のどちらかを指定すれば構いません。
(aops:reshape #2a((1 2)(3 4)(5 6)) '(2 t))
; => #2A((1 2 3) (4 5 6))

(aops:reshape #2a((1 2)(3 4)(5 6)) '(t 3))
; => #2A((1 2 3) (4 5 6))

tを指定した部分が自動で計算されます。

reshape-row関数とreshape-col関数は行列に特化したreshapeで、1行N列またはN行1列に変更するものです。
;; 行優先(1行N列)
(aops:reshape-row #2a((1 2)(3 4)(5 6)))
; => #2A((1 2 3 4 5 6))

;; 列優先(N行1列)
(aops:reshape-col #2a((1 2)(3 4)(5 6)))
; => #2A((1) (2) (3) (4) (5) (6))

処理に関するオペレータ

この節では配列に対する処理を行うオペレータを紹介します。

generate, generate*関数: 関数を使った配列の生成

generate関数は高階関数を使って配列を生成します。第1引数には高階関数を、第2引数には次元(ディメンション)を指定します。第3引数は高階関数に渡す引数の決め方を指定しますが、オプショナル引数であるため、無指定の場合は高階関数に引数が渡されません。
(aops:generate #'(lambda () (random 10)) '(2 3))
; => #2A((1 3 6) (5 1 5))

第3引数に:positionキーワードを指定すると、行優先で数えた場合のインデックスが渡されます。
(aops:generate #'1+ '(2 3) :position)
; => #2A((1 2 3) (4 5 6))

:subscriptsキーワードを指定すると、要素を示す添字指定子がリストで渡されます。行列の場合は、行インデックスと列インデックスです。
(aops:generate #'identity '(2 3) :subscripts)
; => #2A(((0 0) (0 1) (0 2)) ((1 0) (1 1) (1 2)))

:position-and-subscriptsキーワードを指定すると、両方が渡されます(つまり引数は2つです)。
;; format関数の使い方については当サイト標準仕様入門第22章を参照のこと。
(defun format-position (pos sub)
  (format nil "~:r=(~{~a~^, ~})" pos sub)) 
; => FORMAT-POSITION

(aops:generate #'format-position '(2 3) :position-and-subscripts)
; => #2A(("zeroth=(0, 0)" "first=(0, 1)" "second=(0, 2)")
;        ("third=(1, 0)" "fourth=(1, 1)" "fifth=(1, 2)"))

generateを使うと九九を表す行列も簡単に作れます。
(aops:generate #'(lambda (sub) (reduce #'* (mapcar #'1+ sub))) '(9 9) :subscripts)
; => #2A((1 2 3 4 5 6 7 8 9)
;        (2 4 6 8 10 12 14 16 18)
;        (3 6 9 12 15 18 21 24 27)
;        (4 8 12 16 20 24 28 32 36)
;        (5 10 15 20 25 30 35 40 45)
;        (6 12 18 24 30 36 42 48 54)
;        (7 14 21 28 35 42 49 56 63)
;        (8 16 24 32 40 48 56 64 72)
;        (9 18 27 36 45 54 63 72 81))

単位行列も簡単です。
(aops:generate #'(lambda (sub) (if (apply #'= sub) 1 0)) '(9 9) :subscripts)
; => #2A((1 0 0 0 0 0 0 0 0)
;        (0 1 0 0 0 0 0 0 0)
;        (0 0 1 0 0 0 0 0 0)
;        (0 0 0 1 0 0 0 0 0)
;        (0 0 0 0 1 0 0 0 0)
;        (0 0 0 0 0 1 0 0 0)
;        (0 0 0 0 0 0 1 0 0)
;        (0 0 0 0 0 0 0 1 0)
;        (0 0 0 0 0 0 0 0 1))

なお、generate*は第1引数に要素の型を指定できるバージョンで、第2引数以降はgenerateの第1引数以降と同じです。

each, each*関数: 要素毎の関数適用

each関数はその名の通り、全ての要素に高階関数を適用します。
(aops:each #'print #2a((1 2)(3 4)))
; 
; 1 
; 2 
; 3 
; 4 
; => #2A((1 2) (3 4))

複数の配列を指定した場合は、同一位置の要素を同時に引数としながら高階関数を適用します。
(aops:each #'+ #2a((1 2)(3 4)) #2a((10 20)(30 40)))
; => #2A((11 22) (33 44))

each*generate*と同様に第1引数に要素の型を指定します。

margin, margin*関数: 行単位、列単位の計算

margin関数は複雑ですが、一般的には以下のような使い方をします。

read-csvで使った試験結果の表 があるとして、列単位の平均点を求める場合は以下の通りです。
(aops:margin #'alexandria:mean #2a((66 78)(72 62)(85 75)) 0)
; => #(223/3 215/3)

行単位の場合は1にします。
(aops:margin #'alexandria:mean #2a((66 78)(72 62)(85 75)) 1)
; => #(72 67 80)

0 件のコメント :

コメントを投稿