からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

行列の積【ゼロつく1のノート(数学)】

はじめに

 「機械学習・深層学習」学習初手『ゼロから作るDeep Learning』民のための数学攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 NumPy関数を使って実装できてしまう計算について、数学的背景を1つずつ確認していきます。

 この記事は、主に3.3節「多次元配列の計算」を補足するための内容になります。行列同士の掛け算の数学上の定義を説明し、NumPyで実際に計算します。

【関連する記事】

www.anarchive-beta.com

【他の記事一覧】

www.anarchive-beta.com

【この記事の内容】

・行列の積

 1次元配列と2次元配列(行列)の計算について、数学上の定義とNumPyでの扱い方を説明します。
 要素を並べた配列に対して、要素が1つのものをスカラーと呼びます。

 多くの人(私も)がよく詰まる分野です。1次元配列を飛ばして、2次元配列同士の計算の雰囲気だけ掴めば先の話にもついていけます。完全に理解しなくとも進められますし、進めていくうちに段々と理解もついてくるものです(プログラムから理論(数式)を理解するのもこの本(そしてこのノートでも)のコンセプトです)。気にせず進めましょう!

・1次元配列

 これまでにも登場しましたが、$x_1, x_2, \cdots, x_N$のように$x$に関する要素が$N$個あったとしましょう。これをいちいち全部書いていては面倒なだけでなく、注目すべき内容に集中できませんよね(いやこの書き方でも既に一部省略していますが)。これらをまとめたものを小文字の太字を使って書くことにします。

$$ \boldsymbol{\mathrm{x}} = \begin{pmatrix} x_1 & x_2 & \cdots & x_N \end{pmatrix} $$

 このような要素が$N$個の1次元配列(べクトル)のことを$N$次元ベクトルと呼ぶこともあるので、注意する必要があります。本では要素数が$N$の1次元配列(あるいは単に1次元配列)と表現されます。また要素を横に並べるか縦に並べるのかも本来は重要ですが、本では特に区別せずに扱われます。これはNumPy配列として扱うと、縦ベクトルか横ベクトルかを意識せずとも処理されるからでもあります。なのでここでも、主に横に並べたもののみを取り上げます。

 1次元配列の変数は、np.array()を使って作れます。(何度も使ってますね。)

# 1次元配列を作成
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(x)
[ 1  2  3  4  5  6  7  8  9 10]

 1次元配列$\boldsymbol{\mathrm{x}}$とスカラー$k$の計算は、$\boldsymbol{\mathrm{x}}$の各要素と$k$との計算になります。

 まずは足し算を数式で表すと次のように書けます。

$$ k + \boldsymbol{\mathrm{x}} = \begin{pmatrix} k + x_1 & k + x_2 & \cdots & k + x_N \end{pmatrix} $$

 掛け算も同様です。

$$ k \boldsymbol{\mathrm{x}} = \begin{pmatrix} k x_1 & k x_2 & \cdots & k x_N \end{pmatrix} $$

 引き算の場合は$k$を$-k$、割り算の場合は$k$を$\frac{1}{k}$と解釈すればこの式のまま対応できます。

 Pythonでの計算も直感通りに行えます。

# 定数kを指定
k = 3

# スカラーと1次元配列の足し算
k + x
array([ 4,  5,  6,  7,  8,  9, 10, 11, 12, 13])


# スカラーと1次元配列の掛け算
k * x
array([ 3,  6,  9, 12, 15, 18, 21, 24, 27, 30])

 続いて、1次元配列同士の計算です。

$$ \boldsymbol{\mathrm{a}} = \begin{pmatrix} a_1 & a_2 & \cdots & a_N \end{pmatrix} ,\ \boldsymbol{\mathrm{b}} = \begin{pmatrix} b_1 & b_2 & \cdots & b_N \end{pmatrix} $$


 ベクトル$\boldsymbol{\mathrm{a}}$と$\boldsymbol{\mathrm{b}}$の足し算は、同じ位置の要素を足します。

$$ \boldsymbol{\mathrm{a}} + \boldsymbol{\mathrm{b}} = \begin{pmatrix} a_1 + b_1 & a_2 + b_2 & \cdots & a_N + b_N \end{pmatrix} $$

 つまり、要素数が同じでないと計算できないことに注意してください。

 この計算も式通りに行えます。

# 1次元配列を作成
a = np.array([1, 3, 5, 7, 9])
b = np.array([2, 4, 6, 8, 10])

# 1次元配列同士の足し算
a + b
array([ 3,  7, 11, 15, 19])

 1次元配列同士の積は、同じ位置の要素を掛けたものを全て足したものとなります。

$$ \boldsymbol{\mathrm{a}} \boldsymbol{\mathrm{b}} = a_1 b_1 + a_2 b_2 + \cdots + a_N b_N = \sum_{i=1}^N a_i b_i $$

 この計算も要素数が同じでないと計算できません。また計算結果がスカラーとなることにも注意してください。

 直感に反する計算方法ですね。しかし既にこの計算をしたことがあります。

 パーセプトロンの計算式(3.4)は次の式でした。

$$ a = b + w_1 x_1 + w_2 x_2 \tag{3.4} $$

 この入力信号と重みを

$$ \boldsymbol{\mathrm{x}} = \begin{pmatrix} x_1 & x_2 \end{pmatrix} ,\ \boldsymbol{\mathrm{w}} = \begin{pmatrix} w_1 & w_2 \end{pmatrix} $$

とすると、2つの積にバイアス$b$を加えた$a$は

$$ \begin{align} a &= b + \boldsymbol{\mathrm{x}} \boldsymbol{\mathrm{w}} \\ &= b + \sum_{i=1}^2 x_i w_i \\ &= b + w_1 x_1 + w_2 x_2 \tag{3.4} \end{align} $$

式(3.4)と全く同じになりますね。

 この計算をNumPyで行うには、掛け算の結果の和をとればいいのでした。

# 行列の積?
print(a * b)

# 行列の積
print(np.sum(a * b))
[ 2 12 30 56 90]
190

 直感に反した計算が受け入れがたければ、重み付き和の計算をすっきりと表現できる計算方法として定義された別の計算体系なんだと思いましょう。

・2次元配列

 続いて、要素を縦と横に並べた2次元配列の計算です。

$$ \boldsymbol{\mathrm{W}} = \begin{pmatrix} w_{1,1} & w_{1,2} & \cdots & w_{1,N} \\ w_{2,1} & w_{2,2} & \cdots & w_{2,N} \\ \vdots & \vdots & \ddots & \vdots \\ w_{M,1} & w_{M,2} & \cdots & w_{M,N} \end{pmatrix} $$

 これを$M \times N$の行列(マトリクス)といいます。本では、2行2列目の要素を$w_{22}$と書きますが、分かりやすさのため$,$(カンマで)区切って$w_{2,2}$と表現します(どちらも同じものです)。行列は太字の大文字で書くことで区別されます。

 NumPy配列として作成するには、np.array()の引数に渡すリストを入れ子にします。

W = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
print(W)
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]

 外側の括弧を二重にします。入れ子になっているリストとは、大きなリストの中に更にリストが入っている状態のことです。

 この例だと、1つのリストの中に3つのリストが入っている状態です。3つのリストの1つ目が行列の1行目の要素になります。2つ目のリストは2行目の要素に対応します。print(W)の結果が数式の行列のように表示されていますね。

 定数$k$と行列との計算は次のように行います。

 まずは足し算を数式で表すと

$$ k + \boldsymbol{\mathrm{W}} = \begin{pmatrix} k + w_{1,1} & k + w_{1,2} & \cdots & k + w_{1,N} \\ k + w_{2,1} & k + w_{2,2} & \cdots & k + w_{2,N} \\ \vdots & \vdots & \ddots & \vdots \\ k + w_{M,1} & k + w_{M,2} & \cdots & k + w_{M,N} \end{pmatrix} $$

です。掛け算も同様です。

$$ k \boldsymbol{\mathrm{W}} = \begin{pmatrix} k w_{1,1} & k w_{1,2} & \cdots & k w_{1,N} \\ k w_{2,1} & k w_{2,2} & \cdots & k w_{2,N} \\ \vdots & \vdots & \ddots & \vdots \\ k w_{M,1} & k w_{M,2} & \cdots & k w_{M,N} \end{pmatrix} $$


 Pythonでの計算も素直に行えます。

# スカラーを作成
k = 2

# スカラーと2次元配列の足し算
k + W
array([[ 3,  4,  5,  6],
       [ 7,  8,  9, 10],
       [11, 12, 13, 14]])


# スカラーと2次元配列の掛け算
k * W
array([[ 2,  4,  6,  8],
       [10, 12, 14, 16],
       [18, 20, 22, 24]])

 では、行列同士ではどうでしょうか。2つの$3 \times 3$の行列で説明します。

$$ \boldsymbol{\mathrm{A}} = \begin{pmatrix} a_{1,1} & a_{1,2} & a_{1,3} \\ a_{2,1} & a_{2,2} & a_{2,3} \\ a_{3,1} & a_{3,2} & a_{3,3} \end{pmatrix} ,\ \boldsymbol{\mathrm{B}} = \begin{pmatrix} b_{1,1} & b_{1,2} & b_{1,3} \\ b_{2,1} & b_{2,2} & b_{2,3} \\ b_{3,1} & b_{3,2} & b_{3,3} \end{pmatrix} $$


 2つの行列$\boldsymbol{\mathrm{A}},\ \boldsymbol{\mathrm{B}}$の足し算はこれまで通りに行えます。

$$ \boldsymbol{\mathrm{A}} + \boldsymbol{\mathrm{B}} = \begin{pmatrix} a_{1,1} + b_{1,1} & a_{1,2} + b_{1,2} & a_{1,3} + b_{1,3} \\ a_{2,1} + b_{2,1} & a_{2,2} + b_{2,2} & a_{2,3} + b_{2,3} \\ a_{3,1} + b_{3,1} & a_{3,2} + b_{3,2} & a_{3,3} + b_{3,3} \end{pmatrix} $$


 まずは行列の足し算を試してみましょう。

# 2次元配列を作成
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A)
print(B)
[[1 2]
 [3 4]]
[[5 6]
 [7 8]]
# 行列の足し算
A + B
array([[ 6,  8],
       [10, 12]])

 続いて行列同士の積です。これも通常のイメージとは異なります。

$$ \boldsymbol{\mathrm{A}} \boldsymbol{\mathrm{B}} = \begin{pmatrix} a_{1,1} b_{1,1} + a_{1,2} b_{2,1} + a_{1,3} b_{3,1} & a_{1,1} b_{1,2} + a_{1,2} b_{2,2} + a_{1,3} b_{3,2} & a_{1,1} b_{1,3} + a_{1,2} b_{2,3} + a_{1,3} b_{3,3} \\ a_{2,1} b_{1,1} + a_{2,2} b_{2,1} + a_{2,3} b_{3,1} & a_{2,1} b_{1,2} + a_{2,2} b_{2,2} + a_{2,3} b_{3,2} & a_{2,1} b_{1,3} + a_{2,2} b_{2,3} + a_{2,3} b_{3,3} \\ a_{3,1} b_{1,1} + a_{3,2} b_{2,1} + a_{3,3} b_{3,1} & a_{3,1} b_{1,2} + a_{3,2} b_{2,2} + a_{3,3} b_{3,2} & a_{3,1} b_{1,3} + a_{3,2} b_{2,3} + a_{3,3} b_{3,3} \end{pmatrix} $$

 1つ1つの要素がゴチャゴチャしていて分かりづらいですね。$\sum$を使って次のように各項の足し算をまとめて書くこともできます。

$$ \boldsymbol{\mathrm{A}} \boldsymbol{\mathrm{B}} = \begin{pmatrix} \sum_{k=1}^3 a_{1,k} b_{k,1} & \sum_{k=1}^3 a_{1,k} b_{k,2} & \sum_{k=1}^3 a_{1,k} b_{k,3} \\ \sum_{k=1}^3 a_{2,k} b_{k,1} & \sum_{k=1}^3 a_{2,k} b_{k,2} & \sum_{k=1}^3 a_{2,k} b_{k,3} \\ \sum_{k=1}^3 a_{3,k} b_{k,1} & \sum_{k=1}^3 a_{3,k} b_{k,2} & \sum_{k=1}^3 a_{3,k} b_{k,3} \end{pmatrix} $$

 どちらの方が分かりやすいですかね?

 行列の積も*では行えません。

# 行列の積?
A * B
array([[ 5, 12],
       [21, 32]])

 図3-11の計算結果と異なりますね。2次元配列同士を*で計算すると、同じ位置の要素を掛けた値が返ってきます。

 行列の掛け算は、np.dot()を使います。

# 行列の積
np.dot(A, B)
array([[19, 22],
       [43, 50]])

 図3.11と同じ計算結果が返ってきました!

 では、この$3 \times 3$の行列の掛け算を一般化(どんな形の行列にも対応できるように拡張)します。

$$ \boldsymbol{\mathrm{A}} = \begin{pmatrix} a_{1,1} & a_{1,2} & \cdots & a_{1,N} \\ a_{2,1} & a_{2,2} & \cdots & a_{2,N} \\ \vdots & \vdots & \ddots & \vdots \\ a_{M,1} & a_{M,2} & \cdots & a_{M,N} \end{pmatrix} ,\ \boldsymbol{\mathrm{B}} = \begin{pmatrix} b_{1,1} & b_{1,2} & \cdots & b_{1,L} \\ b_{2,1} & b_{2,2} & \cdots & b_{2,L} \\ \vdots & \vdots & \ddots & \vdots \\ b_{N,1} & b_{N,2} & \cdots & b_{N,L} \end{pmatrix} $$

 $\boldsymbol{\mathrm{A}}$は$M \times N$の行列、$\boldsymbol{\mathrm{B}}$は$N \times L$の行列です。表示上は同じ形状の行列に見えますが、別の形をしていることに注意してください!

 この2つの行列の積は

$$ \boldsymbol{\mathrm{A}} \boldsymbol{\mathrm{B}} = \begin{pmatrix} \sum_{k=1}^N a_{1,k} b_{k, 1} & \sum_{k=1}^N a_{1,k} b_{k, 2} & \cdots & \sum_{k=1}^N a_{1,k} b_{k, L} \\ \sum_{k=1}^N a_{2,k} b_{k, 1} & \sum_{k=1}^N a_{2,k} b_{k, 2} & \cdots & \sum_{k=1}^N a_{2,k} b_{k, L} \\ \vdots & \vdots & \ddots & \vdots \\ \sum_{k=1}^N a_{M,k} b_{k, 1} & \sum_{k=1}^N a_{M,k} b_{k, 2} & \cdots & \sum_{k=1}^N a_{M,k} b_{k, L} \end{pmatrix} $$

になります。(下に行くと$a$の1つ目の添字が増えて、右に行くと$b$の2つ目の添字が増えるわけです。)

 2行2列目の要素($2,2$成分とも言います)を確認します。

$$ \sum_{k=1}^N a_{2,k} b_{k, 2} = a_{2,1} b_{1, 2} + a_{2,2} b_{2, 2} + \cdots + a_{2,N} b_{N, 2} $$

 (全ての要素で、$a$の2つ目の添字と$b$の1つ目の添字は1から順番に増えていきます。)
 $\boldsymbol{\mathrm{A}}$の行と$\boldsymbol{\mathrm{B}}$列が対応しています。この関係は図3.11で視覚的に説明されています。$\boldsymbol{\mathrm{A}}$の行の各要素と$\boldsymbol{\mathrm{B}}$列の各要素をそれぞれ掛けて、全ての和をとるということです。

 重要なのは次の2点です。

 $\boldsymbol{\mathrm{A}}$の列数と$\boldsymbol{\mathrm{B}}$の行数が一致している必要があります。要素数が同じでないと計算できません

 また、$\boldsymbol{\mathrm{A}} \boldsymbol{\mathrm{B}}$と$\boldsymbol{\mathrm{B}} \boldsymbol{\mathrm{A}}$の計算結果は異なります。順番を入れ替えた積の計算結果が異なるどころか、計算できないことがあります(というより、2つとも正方行列(列数と行数が同じ行列のこと)のときのみ計算できます)。

# 順番を入れ替えてみる
print(np.dot(A, B))
print(np.dot(B, A))
[[19 22]
 [43 50]]
[[23 34]
 [31 46]]


 1次元配列同士の積について少しだけ補足しておきます。(Python上は困らないので)先ほどは(本でも)やんわりとごまかしましたが、要素数$N$の1次元配列を$N \times 1$の行列あるいは$1 \times N$の行列とみることができますね。$\boldsymbol{\mathrm{a}},\ \boldsymbol{\mathrm{b}}$をそれぞれの表現で書くと

$$ \boldsymbol{\mathrm{a}} = \begin{pmatrix} a_1 & a_2 & \cdots & a_N \end{pmatrix} ,\ \boldsymbol{\mathrm{b}} = \begin{pmatrix} b_1 \\ b_2 \\ \vdots \\ b_N \end{pmatrix} $$

となります。$\boldsymbol{\mathrm{a}}$を横ベクトル、$\boldsymbol{\mathrm{b}}$を縦ベクトルと呼びます。

 数学的には横ベクトル同士では計算できません(行と列の要素数が一致しないからですね!)。図3-13の$\boldsymbol{\mathrm{B}}$を$2 \times 1$、$\boldsymbol{\mathrm{C}}$を$3 \times 1$と捉えれば、図3-12と同じことだと分かります。

 また$\boldsymbol{\mathrm{a}} \boldsymbol{\mathrm{b}}$の計算式が、$\boldsymbol{\mathrm{A}} \boldsymbol{\mathrm{B}}$の1行1列目の要素の計算式が同じものだと分かります。

 以上で線形代数の基礎のキ終了です!お疲れ様でした。3章までが本全体における基礎編です。あとちょっと踏ん張ろう!

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.
  • 加藤公一『機械学習のエッセンス』SBクリエイティブ,2018年.

おわりに

 ド直球の数学ネタなので少し緊張します。ネタ被りがすごいだろうし、その分優良記事との差だとか色々と考えてしまいます。あとは単純に間違ってないだろうかとか。
 ビビりながら書いた分、頭に残った気がします(n回目の線形代数入門)。一応勉強会で教えるために作っている資料なのですが、結局参加者の誰よりも自分の勉強になってますね。

 それに、世にいくつあろうがまだ私のブログにはないので。私のブログに必要な記事なので書きます。

 以前Twitterで流れてきた言葉の意訳です。この言葉を見てから気持ちが楽になりました。皆さんも是非学んだことを臆せずブログに吐き出して、その結果私の学習が少しでも楽になることを・・・

【元の記事】

www.anarchive-beta.com