
こんにちは。清野(@historid1)です。
今回はBDA定期勉強会の『ゼロから作るディープラーニング』シリーズの第4回を担当しましたので、それをまとめます。
現在、私たちBDARC学生勉強会では『ゼロから作るディープラーニング』を教科書として輪読会を進めています。
今回はp53-71です。「3.3多次元配列」の計算から「3.5出力層の設計」までです。
ここではNumPyの使い方と計算を主にまとめます。行列の基本的な計算方法を身に着けましょう。
1次元配列
まずは一次元配列を作ります。
import numpy as np
A = np.array([1, 2, 3, 4])
print(A)
[1 2 3 4]
とくに問題はないですね。np.array()
でNumPy配列を作っています。
# 行列Aの次元を確認する。
np.ndim(A)
# 行列Aの形状を確認する。
A.shape
1
(4, )
A.shape
の返り値はタプルです。例えば2行3列の配列であれば(2, 3)
と表示されます。
ではなぜ行列Aでは(1, 4)
と表示されないのでしょうか。
返り値である(4, )
は0番目の次元に4個の要素があることを示しているためです。0番目の次元とはつまり1次元のことです。Pythonではインデックス番号は0から始まるので注意してください。
同じように(2, 3)では、0番目の次元に2つ、1番目の次元に3つの要素があるという意味になります。つまり2行3列の行列というわけです。
2次元配列
1次元配列と同じように2次元配列も作ることができます。
$$
\left(
\begin{matrix}
1 & 2 \\
3 & 4 \\
5 & 6 \\
\end{matrix}
\right)
$$
上記のような行列をNumPy配列で定義します。
# 2次元配列の定義
B = np.array([[1,2], [3,4], [5,6]])
print(B)
[[1 2]
[3 4]
[5 6]]
配列の中に配列が入れ子になっているわけですね。
次数や形状の確認も同じようにできます。
# 次数の確認
np.ndim(B)
# 形状の確認
B.shape
このように2次元の配列をとくに行列(matrix)といいます。
横方向の並びを行(row)、縦方向の並びを列(column)といいます。
データサイエンスの場合、基本的には1つの行が1つの標本を表します。列は説明変数であることが多いです。そのため「200行ある」といったら200個の標本があると考え、10列といったら変数が10個あるのだと思ってください。
では行列の積(ドット積)を計算しましょう。これもNumPyの機能で行うことができます。
2 x 2 の行列同士の積
まずは行列の積の計算方法をおさらいしましょう。
$$
\left(\begin{matrix} 1 & 2 \\ 3 & 4 \end{matrix}\right)
\left(\begin{matrix} 5 & 6 \\ 7 & 8 \end{matrix}\right)
=
\left(\begin{matrix} 19 & 22 \\ 43 & 50 \end{matrix}\right)
$$
どの要素同士を掛け算するか覚えていますか?ちょっと一部分だけ抜き出してみましょう。
$$
\left(\begin{matrix} 1 & 2 \end{matrix}\right)
\left(\begin{matrix} 5 \\ 7 \end{matrix}\right)
= 19
$$
このように、左のマトリクスの行と右のマトリクスの列をかけ合わせます。
$$
(1 \times 5) + (2 \times 7) = 19
$$
この例で示すように、行列の積は左行列の行と右行列の列の要素ごとの積とその和によって計算されます。
したがって、左側の行列の行数と右側の行列の列数は一致していなければなりません。
# 行列の積
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
np.dot(A, B)
プログラムではこのように書きます。
例で示したように、行列の計算は被演算子(行列A, Bのこと)の順番が異なると結果が異なります。
形状の異なる行列の積
さきほどは同じ形状の行列の積を計算しました。次は異なる形状を試しましょう。
繰り返しになりますが、左側の行列の行数と右側の行列の列数が一致していないと計算はできません。
$$
\left(\begin{matrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{matrix}\right)
\left(\begin{matrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{matrix}\right)
=
\left(\begin{matrix} 22 & 28 \\ 49 & 64 \end{matrix}\right)
$$
A = np.array([[1, 2, 3], [4, 5, 6]])
B = np.array([[1, 2], [3, 4], [5, 6]])
np.dot(A, B)
ではニューラルネットワークを実装しましょう。
ニューラルネットワークの基本
$$Y=XW$$
この方程式がニューラルネットワークの基本です。
今はバイアスと活性化関数は省略してあります。
X = np.array([1, 2])
W = np.array([[1, 3, 5], [2, 4, 6]])
Y = np.dot(X, W)
ニューラルネットワークのグラフは以下のようになります。
$$
X=\left( \begin{matrix} x_1 & x_2 \end{matrix} \right)
=\left( \begin{matrix} 1 & 2 \end{matrix} \right)
$$
Xは上記のように(1, 2)であり、重みはそれぞれの矢印に乗っているイメージです(イラストには描いてありません)。
したがって、ネットワークの計算は以下のようになります。一部分だけを取り出してみます。
$$\begin{align}
y_1 &= (x_1 \times w_1) + (x_2 \times w_4)\\
&= (1 \times 1) + (2 \times 2)\\
&= 5
\end{align}$$
全体としては以下のようになります。
$$\begin{align}
Y &= XW \\
&= \left( \begin{matrix} x_1 & x_2 \end{matrix} \right)
\left( \begin{matrix} w_1 & w_2 & w_3 \\ w_4 & w_5 & w_6 \end{matrix} \right) \\
&= \left( \begin{matrix} 1 & 2 \end{matrix} \right)
\left( \begin{matrix} 1 & 3 & 5 \\ 2 & 4 & 6 \end{matrix} \right) \\
&=\left( \begin{matrix} 5 & 11 & 17 \end{matrix} \right)
\end{align}$$
いかがでしょうか。行列の計算にすれば、ネットワークの計算をうまく表すことができますよね。
では3層のニューラルネットワークを実装していきましょう。
3層といっていますが、0層からはじまるので数としては4層あります。
0層目は入力層、1層目は中間層あるいは隠れ層、2層目は出力層と呼ばれます。
まずは記号の意味を確認しましょう。これは一般的な記号ではなく、教科書内での記述法なので今だけ覚えてください。
$$w^{(1)}_{12}$$
まず(1)
は第1層に付加される重みであることを意味します。つぎに12
は、2番目の要素から1番目の要素へ向かう、という意味です。21
ではない点に注意してください。先に来る数字(十の位)が行き先です。
記号の意味を確認しながら、バイアスを追加してネットワークを実装していきましょう。
バイアスを加えての確認
実装するネットワークは以下のイラストのようになります。

$$b^{(1)}_1$$
b
はバイアス(bias)のb
です。(1)
は第1層に加えられる重みということです。バイアスは第1層にすべて足されるので、次層の1番目の要素へ向かうことを示す1
だけ(十の位)が残ります。
そしてa_1
を求める計算は以下のようになります。
$$a_1 = x_1w^{(1)}_{11} + x_2w^{(1)}_{12} + b^{(1)}_1$$
$$a_2 = x_1w^{(1)}_{21} + x_2w^{(1)}_{22} + b^{(1)}_2$$
$$a_3 = x_1w^{(1)}_{31} + x_2w^{(1)}_{32} + b^{(1)}_3$$
じっくり見てください。そんなに難しくはないはずです。
計算全体を行列として表す
これらの計算を行列計算としてまとめてみます。
$$A=XW+B$$
$$\begin{align}
A&=\left( \begin{matrix} a_1 & a_2 & a_3\end{matrix} \right),
X=\left( \begin{matrix} x_1 & x_2\end{matrix} \right),\\
B&=\left( \begin{matrix} b^{(1)}_1 & b^{(1)}_2 & b^{(1)}_3\end{matrix} \right),
W=\left( \begin{matrix} w^{(1)}_{11} & w^{(1)}_{21} & w^{(1)}_{31} \\
w^{(1)}_{12} & w^{(1)}_{22} & w^{(1)}_{32}\end{matrix} \right)
\end{align}$$
ではPythonで実装してみます。
X = np.array([1.0, 0.5])
W = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B = np.array([0.1, 0.2, 0.3])
A = np.dot(X, W) + B
# 結果
# array([0.3, 0.7, 1.1])
なおこのコードで実装した重みなどの数値は適当な値です。
活性化関数を加える
ニューラルネットワークでは、実際には活性化関数が付加されています。
さきほどの例ではa_1
が活性化関数へ入力されます。活性化関数はh(a)
で表されます。
$$ z = h(a)$$
今回は活性化関数にシグモイド関数を用います。シグモイド関数は以下のように実装されています。
$$sigmoid(x) = \frac{1}{1+e^{-x}}$$
def sigmoid(x):
return 1 / (1 + np.exp(-x))
シグモイド関数にさきほど計算した行列Aを代入します。
Z = sigmoid(A)
# 結果
# array([0.57444252, 0.66818777, 0.75026011])
第0層から第1層のネットワークのまとめ
つぎに第1層から第2層への入力を実装していきます。
その前に第0層から第1層の入力をまとめておきます。さきほどとはちょっと書き方を変えていますが、意味はわかるはずです。
# 第0層から第1層への入力のまとめ
# sigmoid関数の定義
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# 行列の定義
X1 = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
# ニューロンへの入力
A1 = np.dot(X1, W1) + B1
#活性化関数への入力
Z1 = sigmoid(A1)
これで第0層から第1層の実装がまとめられました。同じように第1層から第2層のネットワークを実装してみましょう。
第1層のニューロンは3つで、第2層のニューロンは2つあります。したがって重みはニューロンの組合せの数だけ必要なので6個ですね。
またこのイラストでは、あえて活性化関数を省略しています。各ニューロンの内部では、a_1
が活性化関数(シグモイド関数)に入力されz_1
が出力されます。
こうして第1層のニューロンには行列Z1が格納されるです。
第1層から第2層のネットワークを実装
第0層から第1層で得られた出力は行列Z1に格納されています。
さきほどと同じようにネットワークを実装します。
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
# A2
# array([0.51615984, 1.21402696])
# Z2
# array([0.62624937, 0.7710107 ])
出力層の実装
ではいよいよ出力層を実装します。
ごちゃごちゃしないように矢印の数を減らしていますが、実際には各ニューロンはつながっています。
今は黄色の円まで実装が終わっています。これから黄色(第2層)から紫色(第3層)への処理を実装していきます。
出力層の活性化関数は恒等関数です。入力をそのまま出力します。
def identity_function(x):
return x
恒等関数なのでわざわざ書かなくてもOKです。教科書では、これまでの流れに沿ってidentity_function(x)
を実装しています。
では第2層の重みとバイアスを定義して、ネットワークを実装していきましょう。
# 重みとバイアスの定義
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])
# 前層からの入力
A3 = np.dot(Z2, W3) + B3
# 活性化関数
Y = identity_function(A3)
# A3
# array([0.31682708, 0.69627909])
# Y
# array([0.31682708, 0.69627909])
流れとしてはこれまでと同じですね。次の項目でこれまでの実装をすべてまとめていきます。
これまでの実装を1つにします。
関数を使って書き直していますが、中身は変わらないことが分かるはずです。
# モジュールのインポート
import numpy as np
# シグモイド関数の定義
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# 恒等関数の定義
def identity_function(x):
return x
# ネットワークの重みとバイアスの定義
def init_network():
network = {}
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['b2'] = np.array([0.1, 0.2])
network['b3'] = np.array([0.1, 0.2])
return network
# 順伝播の定義
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3)
return y
# ネットワークの処理
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
# 結果
# array([0.31682708, 0.69627909])

出力層の設計ってなに?
ニューラルネットワークの出力は、回帰か分類です。
回帰は具体的な数値を出力し、分類ではクラス変数(標識変数)を出力します。
一般的に、回帰問題では恒等関数、分類問題ではソフトマックス関数を使います。
恒等関数は入力をそのまま出力するので、何も難しいところはないでしょう。
ソフトマックス関数は以下の式で表されます。
$$ \begin{align}
y_k &= \frac{\exp(a_k)}{\sum_{i=1}^{n}\exp(a_i)} \\
&= \frac{e^{a_k}}{\sum_{i=1}^{n}e^{a_i}}
\end{align}$$
これは出力層のニューロンがn個ある中で、k番目のニューロンの出力を示しています。
ソフトマックス関数の出力は、前層のすべてのニューロンからの入力に影響を受けます。シグマ記号が分母にあるのでそれがわかります。
ソフトマックス関数の実装
では先ほどの数式を実装してみましょう。
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
return exp_a / sum_exp_a
ここでもう一度ソフトマックス関数を見てみてください。
$$ y_k = \frac{\exp(a_k)}{\sum_{i=1}^{n}\exp(a_i)} $$
ここでk=10
のとき、分子は20,000を超えます。k=100
では0が40個以上並ぶ大きな数になってしまいます。
あまりに大きな数の場合、プログラムの結果はinf
という無限大を表す返り値になってしまいます。
そのため、inf
にならないように巨大な定数を引くという手法をとります。以下のように式変形します。
$$ \begin{align}
y_k &= \frac{\exp(a_k)}{\sum_{i=1}^{n}\exp(a_i)} \\
&= \frac{C\exp(a_k)}{C\sum_{i=1}^{n}\exp(a_i)} \\
&= \frac{\exp(a_k + \log C)}{\sum_{i=1}^{n}\exp(a_i + \log C)} \\
&= \frac{\exp(a_k + C’)}{\sum_{i=1}^{n}\exp(a_i + C’)} \\
\end{align}$$
まず、分子と分母に定数Cをかけています。その後は対数に変換して指数関数の中に入れます。
この式変形からいえることは、ソフトマックス関数の指数関数の中身に対して、どんな定数を足しても結果は変わらない、ということです。つまりC'
が何でもOKということです。
そのため、exp(a)
はinf
になる可能性がありましたが、exp(a + C')
であればC'
の値を調整することによってオーバーフローを防ぐことができます。
定数C’の決定
では定数C’をどのように決めればよいのでしょうか。
まずは定数C’なしの場合にどうなるかを見てみましょう。
a = np.array([1010, 1000, 990])
softmax(a)
# 結果
# RuntimeWarning: overflow encountered in exp
# array([nan, nan, nan])
nan
はNot a Numberのことで、非数とも書かれます。つまり計算できなかったことを意味します。exp(a)
がオーバーフローしたわけですね。
では次に定数C’を使って解決策を提示します。
a = np.array([1010, 1000, 990])
# 定数C'の決定
c = np.max(a)
softmax(a - c)
# 結果
# array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
これでオーバーフローを回避できました!!
ついでにソフトマックス関数の定義も書き換えておきます。
def softmax(a):
c = np.max(a) # 定数Cの定義
exp_a = np.exp(a-c) # オーバーフロー対策
sum_exp_a = np.sum(exp_a)
return exp_a / sum_exp_a
これでソフトマックス関数の定義はOKです。
ここでソフトマックス関数の重要な特徴を見ていきましょう。
それはソフトマックス関数の出力は確率とみなせるということです。
適当な数値で確認してみましょう。
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
# y
# array([0.01821127, 0.24519181, 0.73659691])
ここでy
を合計してみましょう。
np.sum(y)
# 結果
# 1.0
そうです。ソフトマックス関数の出力の和は1.0になるのです。
ソフトマックス関数を使わなくても結果はわかる
さてさきほどの例で示したように、ソフトマックスの出力は確率としてみなすことができると説明しました。
ここでもう一度、入力と出力を見比べてみましょう。
# 入力
[0.3, 2.9, 4.0]
# 出力
[0.01821127, 0.24519181, 0.73659691]
気付きましたか? 入力と出力の大小関係は変わっていないのです。
そのため、具体的な確率値が必要でないなら、わざわざソフトマックス関数を通さなくても、確率の大小関係は分かるのです。
さて、ソフトマックス関数の特徴は理解できたでしょうか。
まず回帰問題には恒等関数、分類問題にはソフトマックス関数を出力層の活性化関数に使います。
そして分類問題において、出力層のニューロンの数は分類クラス数に対応しています。
上記のイラストでは、出力層のニューロンは9個です。
もう一度、ソフトマックス関数の式を確認しましょう。
$$y_k = \frac{\exp(a_k)}{\sum_{i=1}^{n}\exp(a_i)}$$
これを上のイラストにあてはめた場合、出力層の0番目のニューロンの出力は以下のようになります。
$$y_0 = \frac{\exp(a_0)}{\sum_{i=1}^{9}\exp(a_i)}$$
同じように1番目のニューロンは
$$y_1 = \frac{\exp(a_1)}{\sum_{i=1}^{9}\exp(a_i)}$$
となります。
このように1つずつ書けば、出力層のニューロン数と分類クラス数が一致することが理解できますね。

長くなりましたが、私の担当はここまでです。
ニューラルネットワークは理解できそうですか?
今回はここまでです。この部分は時間をかけて何度も読み返してください。
重みやバイアスとは何か、具体的にどのように計算しているのか、ということはニューラルネットワークを実装する上でこの先ずっと関わってきます。
なぜ分類問題にはソフトマックス関数を使うのか、なぜ確率とみなすことができるのかも説明できるレベルになるのが望ましいです。
ではまた!!