📊

[解析手法] - RDBを用いた行列計算の手法

2024/07/04に公開

はじめに

リレーショナルデータベース(RDB)の元となっている Edgar.F.Codd の理論「関係代数」(論文:A Relational Model of Data for Large Shared Data Banks)は 「集合論」 をその土台として直接的に用いています。
実際、SQLで行うデータ操作は一部の例外を除き集合論においての集合演算そのものです。
現代数学の大部分が集合論の上に成り立っていることを考えれば、集合の要素を格納でき、集合操作を行うことができる RDB は「データを格納する」ということ以上に、出来ることが数多くあると考えられ、そしてそれは真です。

「データを格納して検索して引き出す」という用途にだけ利用するのであれば、今流行りの Redis などに代表される NoSQL の方が優っているところはあるのも事実だと思いますが、RDBの真骨頂はそんなところではない と私は思っています。

今回はそんな数多くある RDB の応用利用のうち、「RDBを用いた行列計算」を書かせて頂こうと思います。

動機

なぜこんなことを考えたのかを記載しておきます。


私が以前携わっていた業務において、顧客に対してリコメンドを出す仕組みを作る機会がありました。
個別の顧客に対してその顧客に最適な購買確率が高い商品を推薦するというものです。
今回の SQLで行列計算を行うことを思いついたのは、この機会においての現実的な制約を突破する為でした。

いろいろなリコメンド手法を検討した結果、現実的な方法として「協調フィルタリング」のアルゴリズムを採用することになりました。
協調フィルタリングでは類似度を計算する必要があります。
この時はcos距離を採用しました。
このcos距離を計算するために行列計算が必要になります。
対象となる顧客と製品は、数百万人の顧客に対して数千の商品です。
そのため、巨大行列計算を行う必要がありました。
しかし困ったことに、現実的な時間内で顧客毎の推薦リストを計算することが出来るサーバーマシンが存在しませんでした。
当時は3大クラウドが企業に本格的に受け入れられる前でしたので、計算リソースと言えば社内のオンプレミスのサーバー群だけでした。
また、行列のスパース性を用いても社内のサーバーではとても現実的な時間では終わらなかったのです。

その時の社内計算リソースとして最も高速であったのが、データベースとして利用していた Oracle社の Exadata というデータベースアプライアンスでした。
(大変高速かつ大容量のデータベースアプライアンスです)

そこで私は巨大な行列計算をこの Exadata にまかせることを思いつきました。
そして、今回の手法を用いることで、プログラムからは計算の指示やリストの整形などの指示だけを出す形にして、数分以内に推薦リストの作成計算を終わらせることに成功しました。


以上が、私が行列計算をSQLで行おうと思った動機です。
昔語りが長くなりましたが、この手法は使用場所によっては大変有用な手法だと思います。
実際、RDBの集合計算は自前でプログラムを行うより大変高速です。
データの管理とデータに関する計算はRDB内でDBMSに行わせ、プログラム側は出来た結果だけを受け取る、というのはある意味理にかなっており効率が良いと私はこの時感じました。

是非みなさんも機会があればこのようにシステムを作ってみてはいかがでしょうか。
「推薦」いたします。

今回の結論

先に今回の結論を以下に記載しておきます。

以下の3つの行列演算について記述します。

  • 定数倍

尚、記載しているSQL文は、SQLite にて動作を検証しています。
(こちらの DB Browser for SQLite を使うと便利です。)

また、関係代数に関する数式に関しては、こちら をご参照ください。

定数倍

対象の行列を A、定数を \alpha とします。
この定数倍を関係代数理論に則った集合論の記号で記述すれば、

\begin{aligned} \alpha \cdot A := \lbrace \alpha \cdot a \mid a \in A \rbrace \end{aligned}

SQLに即して詳しく書けば以下。

\begin{aligned} \alpha \cdot A := \lbrace a_{[row, col]} \cup \alpha \cdot a_{[value]} \mid a \in A \rbrace \end{aligned}

SQLで記述すれば、

SELECT row, col, α * value as value
FROM A; 

となります。

対象の行列を A, B とします。
ただし、A, B は同一の行列型であるとします。

数式はこちら。

A + B := \lbrace {a_{i,j}}_{[row,col]} \cup {a_{i,j}}_{[value]} + {b_{k,l}}_{[value]} \mid a_{i,j} \in A \wedge b_{k,l} \in B \wedge i=k \wedge j=l \rbrace

ここで添字 i, k は行の添字、 j, l 列の添字です。

SQLは以下。

SELECT A.row, A.col, A.value + B.value as value
FROM A, B
WHERE A.row = B.row AND A.col = B.col;

A.row, A.colA.row, B.col,B.row, A.col, B.row, B.col でも構いません。

ここで行番号と列番号の組(row, col)に欠損がある場合(COO形式の場合)は、上のSQLでは間違った結果が返ります。
これを回避するための修正版は以下になります。

SELECT D.row, D.col,  ifnull(A.value, '0') + ifnull( B.value, '0') as value
FROM (
	SELECT A.row, A.col FROM A
		UNION
	SELECT B.row, B.col FROM B
	) AS D
LEFT OUTER JOIN A
		ON D.row = A.row AND D.col = A.col 
LEFT OUTER JOIN B
		ON D.row = B.row AND D.col = B.col ;

これに対する数式は以下

\begin{aligned} T1 &= \lbrace t_{[row, col]} \mid t \in A \vee t \in B \rbrace \\ T2 &= \lbrace t \cup a_{[value]} \mid t \in T1 \vee a \in A \wedge t_{[row, col]}= a_{[row,col]} \rbrace \\ T3 &= \lbrace t \cup b_{[value]} \mid t \in T2 \vee b \in B \wedge t_{[row, col]}= b_{[row,col]} \rbrace \\ \varphi(x) &= (x=null) \rightarrow 0 \\ \\ A + B &:= \lbrace t_{[row,col]} \cup \varphi({t_{a}}_{[value]}) + \varphi({t_{b}}_{[value]}) \mid t \in T3 \rbrace \end{aligned}

となります。

同じく対象の行列を A, B とします。
ただし行列 A,BA, B の順で積演算が可能な行列型であるとします。

数式はこちら。

\begin{aligned} T &= \lbrace a_{[row, col]} \cup b_{[row,col]} \cup a_{[value]} \times b_{[value]} \mid a \in A \wedge b \in B \wedge a_{[col]} = b_{[row]} \rbrace \\ \\ A \cdot B &:= _{a_{[row]}, b_{[col]}} G_{\sum(a_{[value]} \times b_{[value]} )} (T) \end{aligned}

SQLは

SELECT Arow as row, Bcol as col, sum(value) as value
FROM (
	SELECT A.row as Arow, A.col as Acol, B.row as Brow, B.col as Bcol, A.value * B.value as value
	FROM A, B
	WHERE A.col = B.row
    )
GROUP BY Arow, Bcol;

となります。

解説

以下、上記の結論を解説していきます。

準備

例示に使用する行列

今回の説明で例示に使用する3つの行列を以下とします。

\begin{aligned} A &= \begin{pmatrix} 5 & 4 & 5 \\ 0 & 8 & 9 \end{pmatrix},\\ B &= \begin{pmatrix} 6 & 0 & 2 \\ 4 & 5 & 0 \end{pmatrix},\\ C &= \begin{pmatrix} 5 & 4 & 0 \\ 1 & 0 & 6 \\ 3 & 6 & 8 \end{pmatrix} \end{aligned}

もちろん、例示に用いるだけで、今回の手法がこれらの行列や行列型に限定されるわけではありません。

RDB上の行列表現

RDB上には、行列を以下のように表現して格納します。

row col value
1 1 a_{11}
1 2 a_{12}
\vdots \vdots \vdots
i j a_{ij}
\vdots \vdots \vdots
m n a_{mn}

上記の例示行列 A, B, C で例示すると、

A

row col value
1 1 5
1 2 4
1 3 5
2 1 0
2 2 8
2 3 9

B

row col value
1 1 6
1 2 0
1 3 2
2 1 4
2 2 5
2 3 0

C

row col value
1 1 5
1 2 4
1 3 0
2 1 1
2 2 0
2 3 6
3 1 3
3 2 6
3 3 8

よく言われるデータの「縦持ち形式」です。
今回記載している演算方法を使用するには、この縦持ち形式の形で行列がRDBに格納されている必要があります。

上の形式をみるとデータに 0 が入っている箇所があります。
この箇所はレコードごと削除してしまって構いません。
例えば B であれば、以下のようにRDBに格納しても構いません。

B

row col value
1 1 6
1 3 2
2 1 4
2 2 5

この形式は Coordinate(COO)形式 として知られている形式です。
並列計算に向いていないと言われているようですが、今回は計算はRDBに任せてしまうので関係ありません。

このCOO形式で格納した場合、行列の中の値に 0 の代わりに NULL が入ることと同義となるので、NULLの処理を考えないといけません。
定数倍と積は変わりませんが、和の場合は少々複雑になります。
どちらを使うかは、ケースによって変わるかと思います。

各演算の解説

以下、各演算の詳細を記載します。

定数倍

定数倍は全ての要素に一定値をかける演算なので、全ての要素を取り出して精数倍

\begin{aligned} \alpha \cdot A := \lbrace \alpha \cdot a \mid a \in A \rbrace \end{aligned}

関係代数としてちゃんとわかるように、変化するカラム名としないカラム名も明記して、

\begin{aligned} \alpha \cdot A := \lbrace a_{[row, col]} \cup \alpha \cdot a_{[value]} \mid a \in A \rbrace \end{aligned}

とかけます。
これをSQLで実現するには、value の行を全て定数倍してやればよいです。

SELECT row, col, α * value as value
FROM A; 

これで行列の定数倍が実行されます。

行列の和は、同じ場所にある要素同士を足せば良いです。

\begin{aligned} A + B &= \begin{pmatrix} 5 & 4 & 5 \\ 0 & 8 & 9 \end{pmatrix} + \begin{pmatrix} 6 & 0 & 2 \\ 4 & 5 & 0 \end{pmatrix} \\ &= \begin{pmatrix} 11 & 4 & 7 \\ 4 & 13 & 9 \end{pmatrix} \end{aligned}

よって、行と列それぞれの添字が同じ要素同士を足し合わせることで実現できます。

A + B := \lbrace {a_{i,j}}_{[row,col]} \cup {a_{i,j}}_{[value]} + {b_{k,l}}_{[value]} \mid a_{i,j} \in A \wedge b_{k,l} \in B \wedge i=k \wedge j=l \rbrace

これをSQLにすると、

SELECT A.row, A.col, A.value + B.value as value
FROM A, B
WHERE A.row = B.row AND A.col = B.col;

です。ここで、A.row, A.colA.row, B.col,B.row, A.col, B.row, B.col でも構いません。


全ての行と列の添字が row,col 列にある場合はこれで良いですが、COO形式を使用して 0 の部分のレコードを削除している場合、このSQLを使用するとどちらか一方の行列添字の値が 0 の箇所が足し合わせされず 0 をとなってしまいます。

\begin{aligned} A + B &= \begin{pmatrix} 5 & 4 & 5 \\ 0 & 8 & 9 \end{pmatrix} + \begin{pmatrix} 6 & 0 & 2 \\ 4 & 5 & 0 \end{pmatrix} \\ &\neq \begin{pmatrix} 11 & 0 & 7 \\ 0 & 13 & 0 \end{pmatrix} \end{aligned}

※ 実行結果がこのような間違った行列を返してきます。

NULL の処理を行いつつ正しい結果を出すには工夫が必要です。

まず、A, B 両方にある行列の添字を全て集めます。

T1 = \lbrace t_{[row, col]} \mid t \in A \vee t \in B \rbrace \\
SELECT A.row, A.col FROM A
UNION
SELECT B.row, B.col FROM B ;

次に、行列Aを外部結合します。

T2 = \lbrace t \cup a_{[value]} \mid t \in T1 \vee a \in A \wedge t_{[row, col]}= a_{[row,col]} \rbrace \\
SELECT *
FROM (
  SELECT A.row, A.col FROM A
  UNION
  SELECT B.row, B.col FROM B
) as D
LEFT OUTER JOIN A
		ON D.row = A.row AND D.col = A.col ;

さらに行列Bを外部結合します。

T3 = \lbrace t \cup b_{[value]} \mid t \in T2 \vee b \in B \wedge t_{[row, col]}= b_{[row,col]} \rbrace \\
SELECT *
FROM (
	SELECT A.row, A.col FROM A
		UNION
	SELECT B.row, B.col FROM B
	) AS D
LEFT OUTER JOIN A
		ON D.row = A.row AND D.col = A.col 
LEFT OUTER JOIN B
		ON D.row = B.row AND D.col = B.col ;

値の場所がNULLだった場合、0 に置換する関数

\varphi(x) = (x=null) \rightarrow 0 \\
ifnull(value, '0')

を各値に適用して、足し算を実行します。

A + B := \lbrace t_{[row,col]} \cup \varphi({t_{a}}_{[value]}) + \varphi({t_{b}}_{[value]}) \mid t \in T3 \rbrace
SELECT D.row, D.col,  ifnull(A.value, '0') + ifnull( B.value, '0') as value
FROM (
	SELECT A.row, A.col FROM A
		UNION
	SELECT B.row, B.col FROM B
	) AS D
LEFT OUTER JOIN A
		ON D.row = A.row AND D.col = A.col 
LEFT OUTER JOIN B
		ON D.row = B.row AND D.col = B.col ;

これで行列の和が実行されます。

行列の積は、左行列の列と右行列の行を互いに掛け合わせて和をとります。

\begin{aligned} A \cdot C &= \begin{pmatrix} 5 & 4 & 5 \\ 0 & 8 & 9 \end{pmatrix} \cdot \begin{pmatrix} 5 & 4 & 0 \\ 1 & 0 & 6 \\ 3 & 6 & 8 \end{pmatrix} \\ &= \begin{pmatrix} 5 \cdot 5 + 4 \cdot 1 + 5 \cdot 3 & 5 \cdot 4 + 4 \cdot 0 + 5 \cdot 6 & 5 \cdot 0 + 4 \cdot 6 + 5 \cdot 8 \\ 0 \cdot 5 + 8 \cdot 1 + 9 \cdot 3 & 0 \cdot 4 + 8 \cdot 0 + 9 \cdot 6 & 0 \cdot 0 + 8 \cdot 6 + 9 \cdot 8 \end{pmatrix} \\ &= \begin{pmatrix} 44 & 50 & 64 \\ 35 & 54 & 120 \end{pmatrix} \end{aligned}

その為、左行列の列番号と右行列の行番号を内部結合してテーブルを作り、その後掛け合わせ、足し合わせを実行してやればよいです。

ここでは対象の例示行列は A, C であることに注意して、まずは AC を内部結合してやります。
そして、同時に value 同士を掛け合わせます。

T = \lbrace a_{[row, col]} \cup c_{[row,col]} \cup a_{[value]} \times c_{[value]} \mid a \in A \wedge c \in C \wedge a_{[col]} = c_{[row]} \rbrace \\
SELECT A.row as Arow, A.col as Acol, C.row as Crow, C.col as Ccol, A.value * C.value as value
FROM A, C
WHERE A.col = C.row ;

または

SELECT A.row as Arow, A.col as Acol, B.row as Brow, B.col as Bcol, A.value * B.value as value
FROM A INNER JOIN B
ON A.col = B.row ;

ここではCOO形式を使用していたとしても、行列の和演算の時のようなNULLの処理をしてやる必要がありません。
なぜならNULLの部分は 0 に該当するので、掛け合わせると 0 になり、この後の足し算に影響しなくなるからです。
正直これはとても嬉しい性質でした。

次に A の行番号と C の列番号を組にして、その組毎に和を取ります。

A \cdot C := _{a_{[row]}, c_{[col]}} G_{\sum(a_{[value]} \times c_{[value]} )} (T)
SELECT Arow as row, Ccol as col, sum(value) as value
FROM (
	SELECT A.row as Arow, A.col as Acol, C.row as Crow, C.col as Ccol, A.value * C.value as value
	FROM A, C
	WHERE A.col = C.row
    )
GROUP BY Arow, Ccol;

これで行列の積が実行されます。


これで行列の 定数倍・和・積 が計算できるようになりました。

おわりに

行列の 定数倍・和・積 が計算できるようになったので、ベクトル演算や内積計算もRDBで行うことができます。
またさらに演算を工夫して、今度はテンソル計算までできるように挑戦してみようかと思っています。

リコメンドシステムや、特定のバッチデータ解析システムなどにおいて、サーバーでゴリゴリ計算を行うのではなく、行列演算をRDBに任せるようなシステムを組んでみても面白いと思います。
ぜひ感想など聞かせてください。

Discussion