PostgreSQLで行列演算関数を作ってみた

<!--
#広告: ブログやってます！遊びに来てね！<a href="https://912.hateblo.jp/">https://912.hateblo.jp/</a>
-->


※PostgreSQLでニューラルネットワークの実装が目的なら、行列演算を実装する必要はないかもしれない。
matlab, pythonのように高速で行列を演算できるアルゴリズムがあるならともかく、ここで扱っているのは高速化は一切考えていない。
スキーマをクラスに見立てて「パーセプトロンスキーマ」でも作り、インスタンスを呼び出しながら計算するほうが実は速いのかもしれない。

----

`numeric`型の2次元配列を行列として演算(乗算、行列入れ替え)をできるように型、関数を定義してみた。
行列式、余因子行列、逆行列は、この記事にいいねがたくさんついたらやるかもだ。

例えば`{{1,2,3,4},{5,6,7,8}}`を

```math
\left(
\begin{array}{1}
1&2&3&4 \\
5&6&7&8
\end{array}
\right)

```

とみなせるようにする。

# サイズ情報の取得

```sql
CREATE FUNCTION sizeof(matrix numeric[][]) RETURNS int[] AS
$cf$
    DECLARE first int; second int;
    BEGIN
        SELECT array_length(matrix, 1) INTO first;
        SELECT array_length(matrix, 2) INTO second;
        return array[first, second];
    END;
$cf$ LANGUAGE plpgsql;
```
実行してみよう。

```
matrix=# SELECT sizeof('{{1,2,3,4},{5,6,7,8}}');
 sizeof
--------
 {2,4}
(1 行)
```

# 行と列の入れ替え

nullを使って正則化し、対角線対称に入れ替えていく。

```sql:nullを使った正則化
CREATE OR REPLACE FUNCTION holomize_by_null(matrix numeric[][]) RETURNS numeric[][] AS
$cf$
    DECLARE
        size int[];
    BEGIN
        size := sizeof(matrix);
        if size[1] < size[2] THEN matrix := add_null_row(matrix, size[2]-size[1]); END IF;
        if size[2] < size[1] THEN matrix := add_null_col(matrix, size[1]-size[2]); END IF;
        return matrix;
    END
$cf$ LANGUAGE plpgsql;
    CREATE OR REPLACE FUNCTION add_null_row(matrix numeric[][], gap int) RETURNS numeric[][] AS
    $cf$
        DECLARE null_row numeric[]; size int[];
        BEGIN
            size := sizeof(matrix);
            IF gap < 0 THEN RAISE EXCEPTION '引数gapが異常'; END IF;
            IF gap = 0 THEN return matrix; END IF;
            IF gap > 0 THEN
                null_row :=  array_fill(null::numeric, (sizeof(matrix))[2:2]);
                return add_null_row(matrix || array[null_row], gap-1);
            END IF;
        END;
    $cf$ LANGUAGE plpgsql;
    CREATE OR REPLACE FUNCTION add_null_col(matrix numeric[][], gap int) RETURNS numeric[][] AS
    $cf$
        DECLARE size int[]; new_matrix numeric[][] := array[]::numeric[][]; each_row numeric[];
        BEGIN
            size := sizeof(matrix);
            IF gap < 0 THEN RAISE EXCEPTION '引数gapが異常'; END IF;
            IF gap = 0 THEN return matrix; END IF;
            IF gap > 0 THEN
                FOREACH each_row SLICE 1 IN ARRAY matrix LOOP
                    each_row := each_row || null::numeric;
                    new_matrix := new_matrix || array[each_row];
                END LOOP;
                return add_null_col(new_matrix, gap-1);
            END IF;
        END;
    $cf$ LANGUAGE plpgsql;
```

試してみる

```
matrix=# SELECT holomize_by_null('{{1,2,3,4},{5,6,7,8}}');
                         holomize_by_null
-------------------------------------------------------------------
 {{1,2,3,4},{5,6,7,8},{NULL,NULL,NULL,NULL},{NULL,NULL,NULL,NULL}}
(1 行)


matrix=# SELECT holomize_by_null('{{1,2},{5,6}}');
 holomize_by_null
------------------
 {{1,2},{5,6}}
(1 行)


matrix=# SELECT holomize_by_null('{{1},{5}}');
  holomize_by_null
---------------------
 {{1,NULL},{5,NULL}}
```

行数と列数が異なる行列を渡したときnullを加えることで正則化し、行数と列数が同じときは何もせずに行列を返却していることがわかる。


```sql:対角線対称に入れ替えていく
CREATE OR REPLACE FUNCTION exchange(matrix numeric[][]) RETURNS numeric[][] AS
$cf$
    DECLARE size int[] := sizeof(matrix); each_row numeric[]; row int; tmp numeric;
    BEGIN
        IF size[1]!=size[2] THEN RETURN kill_null(exchange(holomize_by_null(matrix))); END IF;
        row := 1;
        FOREACH each_row SLICE 1 IN ARRAY matrix LOOP
            FOR col IN 1 .. row LOOP
                tmp := matrix[row][col];
                matrix[row][col] := matrix[col][row];
                matrix[col][row] := tmp;
            END LOOP;
            row := row + 1;
        END LOOP; 
        RETURN matrix;
    END;           
$cf$ LANGUAGE plpgsql;

    CREATE OR REPLACE FUNCTION kill_null(matrix numeric[][]) RETURNS numeric[][] AS
    $cf$
        DECLARE size int[]; row numeric[]; i int; null_max_idx int := null; is_all_null boolean; tmp numeric[]; new_mat numeric[][] := array[]::numeric[][];
        BEGIN
            size := sizeof(matrix);

            --まず、下の1行がオールヌルでないか確認
            is_all_null := true;
            FOR i IN 1..size[2] LOOP
                IF matrix[size[1]][i] IS NOT NULL THEN is_all_null := false; EXIT; END IF;
            END LOOP;
            IF is_all_null THEN return kill_null(matrix[1:(size[1]-1)]); END IF;
                             
            --次に、右の1列がオールヌルでないか確認
            is_all_null := true;
            FOR i IN 1..size[1] LOOP
                IF matrix[i][size[2]] IS NOT NULL THEN is_all_null := false; EXIT; END IF;
            END LOOP;
            IF is_all_null THEN 
                FOR i IN 1 .. size[1] LOOP
                    new_mat := new_mat || matrix[i:i][1:(size[2]-1)]; 
                    --ポスグレの罠のような仕様に注意。高次元を[i]とすると[1:i]とみなされる。
                END LOOP;
                return kill_null(new_mat); 
            END IF;

            --右にも下にもオールヌルなし
            return matrix;
        END;
    $cf$ LANGUAGE plpgsql;
    
```

試してみる

```
matrix=# select exchange('{{1,2,3},{5,6,7}}');
      exchange
---------------------
 {{1,5},{2,6},{3,7}}
(1 行)


matrix=# select exchange('{{1,5},{2,6},{3,7}}');
     exchange
-------------------
 {{1,2,3},{5,6,7}}
(1 行)


matrix=# select exchange('{{1,5}}');
 exchange
-----------
 {{1},{5}}
(1 行)


matrix=# select exchange('{{1},{5}}');
 exchange
----------
 {{1,5}}
(1 行)


matrix=# select exchange('{{1}}');
 exchange
----------
 {{1}}
(1 行)

```

#加算と減算は、foreach文を2重に回して容易に実現するのでわざわざ解説しない。

#乗算

行列の積ABは、Aの列数とBの行数が同じときのみ計算できる。
(それ以上の細かい説明はほかに譲る)

```sql:行列の積
CREATE OR REPLACE FUNCTION matrix_mult(A numeric[][], B numeric[][]) RETURNS numeric[][] AS
$cf$
    DECLARE max_row int; max_col int; size int[]; dot numeric; A_col int; B_row int; each_row numeric[]; rtn numeric[][];
    BEGIN
        size := sizeof(A); max_row := size[1]; A_col := size[2];
        size := sizeof(B); max_col := size[2]; B_row := size[1];
        IF A_col != B_row THEN RAISE EXCEPTION '与えられた2つの行列の順序対では行列の積が定義できません'; END IF;
        
        rtn := array[]::numeric[][];
        FOR row IN 1..max_row LOOP
            each_row := array[]::numeric[];
            FOR col IN 1..max_col LOOP
                dot := 0;
                FOR idx IN 1..A_col LOOP
                    dot := dot + A[row][idx]*B[idx][col];
                END LOOP;
                each_row := each_row || dot;
            END LOOP;
RAISE NOTICE '%', each_row;
            rtn := array[rtn] || each_row;
        END LOOP;
        return rtn;
    END;
$cf$ LANGUAGE plpgsql;
```

確かめてみよう。

```
matrix=# SELECT matrix_mult('
matrix'# {
matrix'#     {1,2,3},
matrix'#     {4,5,6}
matrix'# }','
matrix'# {
matrix'#     { 7, 8, 9,10},
matrix'#     {11,12,13,14},
matrix'#     {15,16,17,18}
matrix'# }');
            matrix_mult
-----------------------------------
 {{74,80,86,92},{173,188,203,218}}
(1 行)


matrix=# --↑matlabと同じ結果になった。
matrix=#
matrix=# SELECT matrix_mult('
matrix'# {
matrix'#     {1,2,3,100},
matrix'#     {4,5,6,200}
matrix'# }','
matrix'# {
matrix'#     { 7, 8, 9,10},
matrix'#     {11,12,13,14},
matrix'#     {15,16,17,18}
matrix'# }');
ERROR:  与えられた2つの行列の順序対では行列の積が定義できません
CONTEXT:  PL/pgSQL 関数 matrix_mult(numeric[],numeric[]) の6行目 - RAISE
matrix=# --↑Aの列数とBの行数が異なる。
matrix=#
matrix=# SELECT matrix_mult('
matrix'# {
matrix'#     {1,0,0},
matrix'#     {0,1,0},
matrix'#     {0,0,1}
matrix'# }','
matrix'# {
matrix'#     { 7, 8, 9},
matrix'#     {11,12,13},
matrix'#     {15,16,17}
matrix'# }');
           matrix_mult
---------------------------------
 {{7,8,9},{11,12,13},{15,16,17}}
(1 行)


matrix=# --↑IB=B
matrix=#
matrix=# SELECT matrix_mult('
matrix'# {
matrix'#     {1,2,3},
matrix'#     {4,5,6},
matrix'#     {7,8,9}
matrix'# }','
matrix'# {
matrix'#     {1,0,0},
matrix'#     {0,1,0},
matrix'#     {0,0,1}
matrix'# }');
        matrix_mult
---------------------------
 {{1,2,3},{4,5,6},{7,8,9}}
(1 行)


matrix=# --↑AI=A
```

## おまけ: ベクトルの内積


ベクトルの内積$\boldsymbol{a}\dot{}\boldsymbol{b}$は、$\boldsymbol{a}$を1行の行列(行ベクトル)、$\boldsymbol{b}$を1列の行列(列ベクトル)とみなすとき、行列の積により計算される。
(ただし、1行1列の行列`{{x}}`をスカラ`x`と考える必要がある)

```sql:ベクトルの内積

CREATE OR REPLACE FUNCTION dot(a numeric[], b numeric[]) RETURNS numeric AS
$cf$ SELECT (matrix_mult(array[$1], exchange(array[$2])))[1][1]; $cf$ LANGUAGE SQL;

```

試してみよう

```
matrix=# SELECT dot('{1,2,3}', '{4,5,6}');
 dot
-----
  32
(1 行)

```