##機械学習でも必須の配列(行列)の積
機械学習において最もシンプルなニューラルネットワークでは、重み$w_1,w_2$と入力値$x_1,x_2$の積の総和とバイアス値$b$の和 $y = w_1 x_1+w_2 x_2 +b$ を出力する計算をします。
重みと入力値が2つずつの場合は非常にシンプルですが、3つ以上、または更に多い場合、Pythonでfor loopを用いた実装を行うと、コードの実行に非常に時間がかかります。特に巨大なデータを用いる場合、ベクトル化(vectorization)を行い、重み$W$と入力値$X$の積により出力値$Y$を求めるのが効率的です($W,X,Y$は行列)。
本記事では、Python/NumPyで行列演算(特に積)を行う際に留意すべき「配列の形状」の取扱い方について説明します。
##NumPyによる行列の積の基本
PythonのNumPyでは、np.arrayにより任意の次元の配列を作成できます。
そして配列同士の積を行う際、初心者が混同しがちな演算を、簡単な例を用いて説明します。
import numpy as np
A = np.array([[1,2],[3,4]])
B = np.array([5,6])
print(A * B) #broadcastにより1次元の配列Bが2次元の配列(行列)に変形
print(np.dot(A, B))
A * B は、「要素ごとの積」を求める演算式です。
上記の場合、$\begin{bmatrix}
1 & 2 \
3 & 4 \
\end{bmatrix}$と$\begin{bmatrix}
5 & 6 \
5 & 6 \
\end{bmatrix}$
の対応する要素同士で掛け算を行い、結果は$\begin{bmatrix}
5 & 12 \
15 & 24 \
\end{bmatrix}$になります。
ここで、$\begin{bmatrix}
5 & 6 \
\end{bmatrix}$が$\begin{bmatrix}
5 & 6 \
5 & 6 \
\end{bmatrix}$に置き換わっていることに気づくと思います。これは、NumPyのbroadcast という機能によるもので、次元が異なる配列の演算を行えるように変形を行うのです。上の例の場合、1次元の配列Bが2次元の配列(=行列)に変形されています。
もう一つのnp.dot(A, B)はドット積と呼ばれる演算で、配列同士の内積を計算します。ここでの結果は$\begin{bmatrix}
17 & 39 \
\end{bmatrix}$になります。
通常、2次元の配列(=行列)同士の積では、$i \times j$ の行列$M$ と$j \times k$ の行列$N$ の積を求める際、行列$M$ の2次元目の要素数$j$ と行列$N$ の1次元目の要素数$j$ を一致させます。
上記のPythonコードでは、2次元の行列$A$ に「1次元の配列$B$」を掛ける演算を行っています。1次元の配列は、階数(rank)1の配列とも呼ばれます。この1次元の配列は行ベクトルでも列ベクトルでもないため、行列演算に慣れた身からすると、実際のところ直観的でないプロセスを辿ります。
##NumPyによる配列の形状確認
import numpy as np
A = np.array([[1,2],[3,4]])
print(A.shape) #(2, 2)
B = np.array([5,6])
print(B.shape) #(2,)
print(B.T.shape) #(2,)
print(np.dot(B, B.T)) #61
上記コードでは、各配列の形状確認を行っています。注目していただきたいのが$B$の形状です。まずB.shapeを返して配列の形状確認を行っていますが、繰り返しますように1次元配列は行列ではなく、行ベクトルでも列ベクトルでもないため、要素数2のみを返します。
次に、B.T.shapeを返して配列を転置したものの形状確認を行っていますが、これもB.shapeと同じ結果になります。
更に、BとB.Tの内積を求めると、行列ではなく1つの値を返します。
#1次元配列を避けた実装
機械学習の実装の過程で、行列や配列の形状を確認しながら丁寧に四則演算を繋げていきますが、その際に上記のような1次元配列が混在することは混乱やエラーの元となります。そのため、コーディングの際には、(n,)という配列をできるだけ避け、(n, 1)と形状を指定して列ベクトルとして作成する習慣をつけておくと分かりやすくなります。(1, n)の行ベクトルにする際には、当該ベクトルを転置すれば良いのです。
もし1次元配列として作成してしまった場合には、以下を追記することにより列ベクトルに変換できます。
B = B.reshape(2,1)
print(B.shape) #(2,1)
また、配列の形状を把握できていない場合、assert()構文を挟んで確認するのも有効です。
assert(B.shape = (2,1))
##参考文献
以上の方法は、Courseraのオンライン講座 "Deep Learning Specialization"で紹介されていた方法です。講師のAndrew Ng先生は、指導してきた学生が多くつまづくポイントとして1次元配列の混在を挙げており、それに対するアプローチとして紹介している手法です。
機械学習の初心者を始め、NumPyによる計算を多用する方にお薦めしたいtipsです。
以上、分かりにくい点や誤りがございましたらコメント等でご指摘いただければ幸いです。