からっぽのしょこ

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

4.5.1:2層ニューラルネットワークのクラス【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事は、4.5.1項「2層ニューラルネットワークのクラス」の内容です。2層のニューラルネットワークのクラスを実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

4.5.1 2層ニューラルネットワークのクラス

 2層のニューラルネットワークをクラスとして実装します。実装するクラスは、「パラメータの初期化」「2層のニューラルネットワークの計算(推論処理)」「損失の計算」「認識精度の計算」「勾配の計算」の5つの機能(メソッド)を持ちます。今はまだ複雑に見えるかとは思いますが、どの処理も基本的な流れはこれまでやってきたものです!
 第1層を入力層、第2層を出力層と呼びます。

 利用するライブラリを読み込みます。

# 4.5.1項で利用するライブラリ
import numpy as np


 この節では、「3.2.4:シグモイド関数の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」のシグモイド関数sigmoid()、「3.5:ソフトマックス関数の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」の(バッチ対応版)ソフトマックス関数softmax()、「4.2.2-4:交差エントロピー誤差の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」の(バッチ対応版)交差エントロピー誤差cross_entropy_error()、「4.4.0:勾配【ゼロつく1のノート(数学)】 - からっぽのしょこ」の(バッチ対応版)勾配を計算する関数numerical_gradient()を利用します。そのため、関数定義を再度実行しておく必要があります。
 または次の方法で、「deep-learning-from-scratch-master」フォルダ内の「common」フォルダの「functions.py」ファイルから実装済みの関数を読み込むこともできます。ファイルの読み込みについては「3.6.1:MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ」を参照してください。

# 読み込み用の設定
import sys
sys.path.append('../deep-learning-from-scratch-master')

# 実装済み関数を読み込み
from common.functions import sigmoid # シグモイド関数:3.2.4項
from common.functions import softmax # ソフトマックス関数:3.5節
from common.functions import cross_entropy_error # 交差エントロピー誤差:4.2.4項
from common.gradient import numerical_gradient # 勾配の計算:4.4節

 ただし、どの関数も本での実装とは多少異なります。バッチデータ(多次元配列)を処理できるようになっています。また、本では紹介していない機能を使って洗練された実装になっています。

・数式の確認

 まずは、2層のニューラルネットワークで行う計算と数式上の表記を確認します。

・入力層の計算

 ニューラルネットワークの入力(手書き数字)$\mathbf{X}$、第1層の重み$\mathbf{W}^{(1)}$とバイアス$\mathbf{b}^{(1)}$を次の形状とします。Pythonのインデックスに合わせて添字を0から割り当てています。

$$ \mathbf{X} = \begin{pmatrix} x_{0,0} & x_{0,1} & \cdots & x_{0,D-1} \\ x_{1,0} & x_{1,1} & \cdots & x_{1,D-1} \\ \vdots & \vdots & \ddots & \cdots \\ x_{N-1,0} & x_{N-1,1} & \cdots & x_{N-1,D-1} \end{pmatrix} ,\ \mathbf{W}^{(1)} = \begin{pmatrix} w_{0,0} & w_{0,1} & \cdots & w_{0,H-1} \\ w_{1,0} & w_{1,1} & \cdots & w_{1,H-1} \\ \vdots &\vdots & \ddots & \vdots \\ w_{D-1,0} & w_{D-1,1} & \cdots & w_{D-1,H-1} \end{pmatrix} ,\ \mathbf{b}^{(1)} = \begin{pmatrix} b_0 & b_1 & \cdots & b_{H-1} \end{pmatrix} $$

 ここで、$N$はバッチサイズ(1試行当たりのデータ数)、$D$は各データ$\mathbf{x}_n = (x_{n,0}, \cdots, x_{n,D-1})$の要素数(手書き数字のピクセル数)、$H$は中間層のニューロン数です。右肩の$(1)$は第1層のパラメータを表す記号で、計算上の意味はありません。

 入力$\mathbf{X}$と重み$\mathbf{W}^{(1)}$の行列の積にバイアス$\mathbf{b}^{(1)}$(を行方向に複製した$\mathbf{B}^{(1)}$)を加えて、第1層の重み付き和$\mathbf{A}^{(1)}$を計算します(3.3節)。

$$ \mathbf{A}^{(1)} = \mathbf{X} \mathbf{W}^{(1)} + \mathbf{B}^{(1)} $$

 各要素に注目すると、それぞれ次の計算をしています。

$$ a_{n,h} = \sum_{d=0}^{D-1} x_{n,d} w_{d,h} + b_h $$

 $N \times D$と$D \times H$の行列の積なので、計算結果は$N \times H$の行列になります。

$$ \mathbf{A}^{(1)} = \begin{pmatrix} a_{0,0} & a_{0,1} & \cdots & a_{0,H-1} \\ a_{1,0} & a_{1,1} & \cdots & a_{1,H-1} \\ \vdots & \vdots & \ddots & \cdots \\ a_{N-1,0} & a_{N-1,1} & \cdots & a_{N-1,H-1} \end{pmatrix} $$

 重み付き和$\mathbf{A}^{(1)}$の各要素をシグモイド関数により活性化します。

$$ z_{n,h} = \mathrm{sigmoid}(a_{n,h}) $$

 活性化関数は形状に影響しません。

$$ \mathbf{Z} = \begin{pmatrix} z_{0,0} & z_{0,1} & \cdots & z_{0,H-1} \\ z_{1,0} & z_{1,1} & \cdots & z_{1,H-1} \\ \vdots & \vdots & \ddots & \cdots \\ z_{N-1,0} & z_{N-1,1} & \cdots & z_{N-1,H-1} \end{pmatrix} $$

 $\mathbf{Z}$は、第1層の出力であり、また第2層の入力になります。

・出力層の計算

 第2層の重み$\mathbf{W}^{(2)}$とバイアス$\mathbf{b}^{(2)}$を次の形状とします。Pythonのインデックスに合わせて添字を0から割り当てています。

$$ \mathbf{W}^{(2)} = \begin{pmatrix} w_{0,0} & w_{0,1} & \cdots & w_{0,K-1} \\ w_{1,0} & w_{1,1} & \cdots & w_{1,K-1} \\ \vdots &\vdots & \ddots & \vdots \\ w_{H-1,0} & w_{H-1,1} & \cdots & w_{H-1,K-1} \end{pmatrix} ,\ \mathbf{b}^{(2)} = \begin{pmatrix} b_0 & b_1 & \cdots & b_{K-1} \end{pmatrix} $$

 ここで、$H$は中間層のニューロン数、$K$はクラス数(数字の種類数)です。右肩の$(2)$は第2層のパラメータを表す記号です。

 第2層の入力$\mathbf{Z}$と重み$\mathbf{W}^{(2)}$の行列の積にバイアス$\mathbf{b}^{(2)}$(を行方向に複製した$\mathbf{B}^{(2)}$)を加えて、第2層の重み付き和$\mathbf{A}^{(2)}$を計算します。

$$ \mathbf{A}^{(2)} = \mathbf{Z} \mathbf{W}^{(2)} + \mathbf{B}^{(2)} $$

 各要素に注目すると、それぞれ次の計算をしています。

$$ a_{n,k} = \sum_{h=0}^{H-1} z_{n,h} w_{h,k} + b_k $$

 $N \times H$と$H \times K$の行列の積なので、計算結果は$N \times K$の行列になります。

$$ \mathbf{A}^{(2)} = \begin{pmatrix} a_{0,0} & a_{0,1} & \cdots & a_{0,K-1} \\ a_{1,0} & a_{1,1} & \cdots & a_{1,K-1} \\ \vdots & \vdots & \ddots & \cdots \\ a_{N-1,0} & a_{N-1,1} & \cdots & a_{N-1,K-1} \end{pmatrix} $$

 分類問題(手書き数字認識)では、ソフトマックス関数により各データの重み付き和$\mathbf{a}_n^{(2)}$を活性化して(3.5節)、ニューラルネットワークの出力$\mathbf{y}_n$とします。

$$ \mathbf{y}_n = \mathrm{softmax}(\mathbf{a}_n^{(2)}) $$

 活性化関数は形状に影響しません。

$$ \mathbf{Y} = \begin{pmatrix} y_{0,0} & y_{0,1} & \cdots & y_{0,K-1} \\ y_{1,0} & y_{1,1} & \cdots & y_{1,K-1} \\ \vdots & \vdots & \ddots & \cdots \\ y_{N-1,0} & y_{N-1,1} & \cdots & y_{N-1,K-1} \end{pmatrix} $$

 $\mathbf{Y}$がニューラルネットワークの出力です。$n$番目のデータに関する出力$\mathbf{y}_n$は、$0 \leq y_{n,k} \leq 1$、$\sum_{k=0}^{K-1} y_{n,k} = 1$に正規化されており、$n$番目の手書き数字$\mathbf{x}_n$がどの数字(クラス$k = 0, 1, \cdots, K-1$)なのかを表す確率分布として扱えるのでした。

・損失の計算

 $N$個のデータに関する教師データ$\mathbf{T}$は、出力$\mathbf{Y}$と同じ形状です。

$$ \mathbf{T} = \begin{pmatrix} t_{0,0} & t_{0,1} & \cdots & t_{0,K-1} \\ t_{1,0} & t_{1,1} & \cdots & t_{1,K-1} \\ \vdots & \vdots & \ddots & \cdots \\ t_{N-1,0} & t_{N-1,1} & \cdots & t_{N-1,K-1} \end{pmatrix} $$

 各データの教師データ$\mathbf{t}_n$は、正解ラベルが1でそれ以外が0です。
 ニューラルネットワークの出力(推論結果)$\mathbf{Y}$と$\mathbf{T}$を用いて、(平均)交差エントロピー誤差を計算して(4.2.4項)、損失$L$とします。

$$ L = - \frac{1}{N} \sum_{n=0}^{N-1} \sum_{k=0}^{K-1} t_{n,k} \log y_{n,k} $$

 損失$L$は、スカラで0以上の値になります。値が大きいほど誤差が大きい(データへの当てはまりが悪い)ことを表します。

・勾配の計算

 損失を求めるまでの計算を1つの関数とみなして、重みの勾配$\frac{\partial L}{\partial \mathbf{W}}$とバイアスの勾配$\frac{\partial L}{\partial \mathbf{b}}$を求めます。

 第1層のパラメータ$\mathbf{W}^{(1)},\ \mathbf{b}^{(1)}$の勾配を$\frac{\partial L}{\partial \mathbf{W}^{(1)}},\ \frac{\partial L}{\partial \mathbf{b}^{(1)}}$で表します。

$$ \frac{\partial L}{\partial \mathbf{W}^{(1)}} = \begin{pmatrix} \frac{\partial L}{\partial w_{0,0}} & \frac{\partial L}{\partial w_{0,1}} & \cdots & \frac{\partial L}{\partial w_{0,H-1}} \\ \frac{\partial L}{\partial w_{1,0}} & \frac{\partial L}{\partial w_{a,1}} & \cdots & \frac{\partial L}{\partial w_{1,H-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial L}{\partial w_{D-1,0}} & \frac{\partial L}{\partial w_{D-1,1}} & \cdots & \frac{\partial L}{\partial w_{D-1,H-1}} \\ \end{pmatrix} ,\ \frac{\partial L}{\partial \mathbf{b}^{(1)}} = \begin{pmatrix} \frac{\partial L}{\partial b_0} & \frac{\partial L}{\partial b_1} & \cdots & \frac{\partial L}{\partial b_{H-1}} \end{pmatrix} $$

 同様に、第2層のパラメータ$\mathbf{W}^{(2)},\ \mathbf{b}^{(2)}$の勾配$\frac{\partial L}{\partial \mathbf{W}^{(2)}},\ \frac{\partial L}{\partial \mathbf{b}^{(2)}}$で表します。

$$ \frac{\partial L}{\partial \mathbf{W}^{(2)}} = \begin{pmatrix} \frac{\partial L}{\partial w_{0,0}} & \frac{\partial L}{\partial w_{0,1}} & \cdots & \frac{\partial L}{\partial w_{0,K-1}} \\ \frac{\partial L}{\partial w_{1,0}} & \frac{\partial L}{\partial w_{a,1}} & \cdots & \frac{\partial L}{\partial w_{1,K-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial L}{\partial w_{H-1,0}} & \frac{\partial L}{\partial w_{H-1,1}} & \cdots & \frac{\partial L}{\partial w_{H-1,K-1}} \\ \end{pmatrix} ,\ \frac{\partial L}{\partial \mathbf{b}^{(2)}} = \begin{pmatrix} \frac{\partial L}{\partial b_0} & \frac{\partial L}{\partial b_1} & \cdots & \frac{\partial L}{\partial b_{K-1}} \end{pmatrix} $$

 各パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$は、元のパラメータ$\mathbf{W},\ \mathbf{b}$と同じ形状になります。各要素は、それぞれパラメータの対応する要素の偏微分です(4.3.3項)。

・パラメータの更新

 この項では行いませんが、各パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$を用いて、勾配降下法によりパラメータ$\mathbf{W},\ \mathbf{b}$を更新します(4.4.1項)。更新後のパラメータを$\mathbf{W}^{(\mathrm{new})},\ \mathbf{b}^{(\mathrm{new})}$とすると、更新式は次の式で表せます。

$$ \begin{aligned} \mathbf{W}^{(\mathrm{new})} &= \mathbf{W} - \eta \frac{\partial L}{\partial \mathbf{W}} \\ \mathbf{b}^{(\mathrm{new})} &= \mathbf{b} - \eta \frac{\partial L}{\partial \mathbf{b}} \end{aligned} $$

 各要素に注目すると、それぞれ次の計算をしています。

$$ \begin{aligned} w_{h,k}^{(\mathrm{new})} &= w_{h,k} - \eta \frac{\partial L}{\partial w_{h,k}} \\ b_k^{(\mathrm{new})} &= b_k - \eta \frac{\partial L}{\partial b_k} \end{aligned} $$

 学習率$\eta$が適切に設定されていれば、パラメータ$\mathbf{W}^{(\mathrm{new})},\ \mathbf{b}^{(\mathrm{new})}$を使って推論を行うことで、更新前のパラメータ$\mathbf{W},\ \mathbf{b}$の時よりも損失$L$が下がります。式にほとんど違いがないため、右肩の$(1)$と$(2)$は省略しました。

 以上がニューラルネットワークの学習における計算の流れです。

・処理の確認

 次に、2層のニューラルネットワークで行う処理を確認します。ほぼほぼこれまでにやったことのある処理ですが、復習も兼ねて1つずつやってみましょう。急がば回れ!念には念を!亀になれ!

・入力データの用意

 バッチサイズ(1試行当たりのデータ数)$N$をbatch_size、各入力データ$\mathbf{x}_n$の要素数$D$をinput_size、中間層のニューロン数$H$をhidden_size、クラス数$K$をoutput_sizeとして値を指定します。

# バッチサイズ(1試行当たりのデータ数)を指定
batch_size = 100

# 入力データの要素数を指定:(固定)
input_size = 784

# 中間層のニューロン数を指定
hidden_size = 50

# クラス数を指定:(固定)
output_size = 10

 手書き数字認識の場合は、input_sizeは画像データのピクセル数の784、output_sizeは数字の種類数の10です。中間層は任意を値を指定できます。

 処理の確認用にダミーの入力データ$\mathbf{X}$と教師データ$\mathbf{T}$を作成します。

# (仮の)入力データ(手書き数字)を作成
x = np.random.rand(batch_size, input_size)
print(x[:5, :5])
print(x.shape)

# (仮の)教師データ(正解ラベル)を作成
#t = np.random.rand(batch_size, output_size)
t = np.random.multinomial(n=1, pvals=np.repeat(1 / output_size, output_size), size=batch_size)
print(t[:5, :])
print(t.shape)
[[0.86395959 0.79009712 0.45506688 0.3794739  0.43487759]
 [0.45347281 0.96578904 0.77351574 0.81360943 0.82385831]
 [0.70511108 0.83597451 0.35263811 0.74906128 0.97011319]
 [0.06375319 0.31024557 0.77170352 0.45597329 0.38104651]
 [0.87634674 0.54077677 0.2297669  0.99938191 0.0393328 ]]
(100, 784)
[[1 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 1 0]
 [0 0 0 0 0 0 0 0 0 1]
 [1 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 1 0 0 0 0 0]]
(100, 10)

 入力データxは0から1の一様乱数np.random.rand()で、教師データtは多項分布に従う乱数np.random.multinomial()で作成します。randn()ではなくrand()なのに注意してください(ただしrandn()でも処理上の問題はありません)。どちらもここでしか使わない関数なので、解説は省略します。(この例では、最大値のインデックスが得られればいいので、本のように教師データもnp.random.rand()を使っても問題はありません。)

・パラメータの初期化

 重みの初期値の標準偏差をweight_init_stdとして指定して、各層のパラメータ(重みとバイアス)$\mathbf{W},\ \mathbf{b}$を作成します。

# 重みの初期値の標準偏差を指定
weight_init_std = 0.01

# 第1層の重みを初期化
W1 = weight_init_std * np.random.randn(input_size, hidden_size)
print(W1[:5, :5])
print(W1.shape)

# 第1層のバイアスを初期化
b1 = np.zeros(hidden_size)
print(b1[:5])
print(b1.shape)

# 第2層の重みを初期化
W2 = weight_init_std * np.random.randn(hidden_size, output_size)
print(W2[:5, :5])
print(W2.shape)

# 第2層のバイアスを初期化
b2 = np.zeros(output_size)
print(b2[:5])
print(b2.shape)
[[ 0.01139733  0.01021244 -0.01439365  0.00582262 -0.00716716]
 [-0.00157202  0.00398374  0.01795173  0.02329979  0.00188542]
 [ 0.00686746  0.00980704 -0.00746827 -0.01052537  0.00715977]
 [-0.00352051  0.01174194 -0.0021613  -0.00273671  0.00129922]
 [ 0.00541454 -0.00203884  0.01440825 -0.00442578  0.01334005]]
(784, 50)
[0. 0. 0. 0. 0.]
(50,)
[[-0.00791365 -0.01042908  0.00466127  0.01602264  0.00261564]
 [-0.00181919 -0.01734508 -0.00348532 -0.00728479 -0.00910845]
 [-0.00931513 -0.01171973 -0.01263287 -0.00537094 -0.01207093]
 [ 0.01018312  0.01092938 -0.00081073  0.00521005  0.0035751 ]
 [ 0.00029071  0.00578941  0.00580856 -0.0101027   0.01139833]]
(50, 10)
[0. 0. 0. 0. 0.]
(10,)

 重みW1, W2は平均0・標準偏差1のガウス分布(標準正規分布)の乱数np.random.randn(行数, 列数)で、バイアスb1, b2は全ての要素が0の配列np.zeros(要素数)で作成します。
 np.random.randn()で生成した要素の標準偏差は約1になります。全ての要素に任意の値を掛けることで、標準偏差をその値に変更できます。重みの初期値については6.2節を参照してください。

 行列の積の計算では、行数と列数が対応していないと計算できないのでした。このように配列のサイズを指定して、各変数を自動で作成することはエラー回避の意味もあります。

 重みの初期化を行う関数として、この処理を定義します。ここで作成する関数は、クラスの実装時のメソッドに対応しています。

# パラメータを初期化する関数を定偽
def init_params(input_size, hidden_size, output_size, weight_init_std=0.01):
    # ディクショナリを初期化
    params = {}
    
    # パラメータの初期値をディクショナリに格納
    params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) # 第1層の重み
    params['b1'] = np.zeros(hidden_size) # 第1層のバイアス
    params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) # 第2層の重み
    params['b2'] = np.zeros(output_size) # 第2層のバイアス
    return params

 パラメータの形状に関する値と標準偏差を引数に指定します。
 作成した全てのパラメータをディクショナリ型の変数paramsに格納して、paramsを返します。

 作成した関数を使って、パラメータを初期化します。

# パラメータを初期化
params = init_params(input_size, hidden_size, output_size, weight_init_std)
print(params.keys())
dict_keys(['W1', 'b1', 'W2', 'b2'])

 keys()メソッドで全てのキーを確認できます。

 キーを指定することで対応する値(配列)を取り出せます。

# 第1層の重みを取得
W1 = params['W1']
print(W1[:5, :5])
print(W1.shape)

# 第1層のバイアスを取得
b1 = params['b1']
print(b1[:5])
print(b1.shape)

# 第2層の重みを取得
W2 = params['W2']
print(W2[:5, :5])
print(W2.shape)

# 第2層のバイアスを取得
b2 = params['b2']
print(b2[:5])
print(b2.shape)
[[ 0.01021691  0.00487829  0.00322041 -0.00399897  0.00746215]
 [-0.00301744 -0.01599766 -0.00722324 -0.0169931  -0.01472096]
 [ 0.00292036  0.00386149 -0.00325625  0.00561628  0.00100012]
 [ 0.02228138 -0.00925544 -0.00797982 -0.00651384 -0.00322052]
 [ 0.00734898  0.00359802 -0.00424911  0.00820627  0.00090568]]
(784, 50)
[0. 0. 0. 0. 0.]
(50,)
[[ 0.00919413  0.00861064  0.01368331  0.00895233 -0.01235799]
 [-0.00106515  0.01977561 -0.01054661  0.01011409 -0.01175837]
 [-0.00752285 -0.0029413  -0.00031399 -0.00362296  0.01075067]
 [-0.01298698  0.02424515 -0.00207621 -0.0077958  -0.0014552 ]
 [ 0.00306888 -0.01226572 -0.00126653 -0.00798172 -0.00739548]]
(50, 10)
[0. 0. 0. 0. 0.]
(10,)

 ランダムな処理を行うので、実行する度に値が変わります。

 以上で、必要な変数を用意できました。次は、ニューラルネットワークの計算を行います。

・ニューラルネットワークの計算

 第1層(入力層)の計算をします。

# 第1層の重み付き和を計算
a1 = np.dot(x, W1) + b1
print(a1[:5, :5])
print(a1.shape)

# 第1層の出力を計算(重み付き和を活性化)
z1 = sigmoid(a1)
print(z1[:5, :5])
print(z1.shape)
[[ 2.44869528e-02 -1.62509297e-01 -8.66899953e-02 -1.24985052e-01
   7.44152725e-03]
 [-4.13961324e-02 -1.28952042e-01 -1.45027296e-01 -6.34275276e-02
  -1.02723633e-01]
 [-6.59900391e-02 -3.29452339e-01 -1.55933913e-01 -1.23886112e-01
   1.49122518e-01]
 [-1.82788212e-02  3.05577815e-02 -2.36440931e-02 -1.06666352e-01
  -1.03129729e-01]
 [ 2.11201325e-04 -6.39327755e-02 -4.86086452e-02 -1.59857432e-01
   3.47058057e-02]]
(100, 50)
[[0.50612143 0.45946185 0.47834106 0.46879435 0.50186037]
 [0.48965244 0.46780659 0.46380659 0.48414843 0.47434165]
 [0.48350847 0.41837388 0.46109532 0.46906802 0.5372117 ]
 [0.49543042 0.50763885 0.49408925 0.47335867 0.47424039]
 [0.5000528  0.48402225 0.48785023 0.46012053 0.50867558]]
(100, 50)

 ニューラルネットワークの入力$\mathbf{X}$と第1層のパラメータ$\mathbf{W}^{(1)},\ \mathbf{b}^{(1)}$を用いて、第1層の重み付き和$\mathbf{A}^{(1)}$を計算します。
 また、3.2.4項で実装したシグモイド関数sigmoid()を使って、$\mathbf{A}^{(1)}$を活性化して第1層の出力$\mathbf{Z}^{(1)}$を求めます。$\mathbf{Z}^{(1)}$の各要素は、シグモイド関数によって0から1の値に変換されます。

 $\mathbf{Z}^{(1)}$を第2層の入力として、第2層(出力層)の計算をします。

# 第2層の重み付き和を計算
a2 = np.dot(z1, W2) + b2
print(a2[:5, :5])
print(a2.shape)

# 第2層の出力を計算(重み付き和を活性化)
y = softmax(a2)
print(y[:5, :5])
print(y.shape)
print(np.sum(y[:5], axis=1)) # 正規化の確認
[[-0.00493103  0.09268103  0.04567693 -0.0415461  -0.05352439]
 [-0.00261637  0.09117341  0.04634587 -0.04016851 -0.05174454]
 [-0.002865    0.09245607  0.04479108 -0.04520095 -0.05009136]
 [-0.00495586  0.09079428  0.04497274 -0.03968798 -0.05395676]
 [-0.00562854  0.09286846  0.04534943 -0.03987572 -0.04945264]]
(100, 10)
[[0.09809443 0.10815254 0.10318655 0.09456765 0.09344165]
 [0.09827712 0.1079406  0.10320874 0.09465504 0.09356563]
 [0.0982881  0.10811809 0.10308554 0.09421384 0.09375422]
 [0.09810638 0.10796451 0.10312904 0.09475743 0.09341496]
 [0.09797655 0.10811821 0.1031007  0.09467794 0.09377554]]
(100, 10)
[1. 1. 1. 1. 1.]

 第1層の出力(第2層の入力)$\mathbf{Z}^{(1)}$と第2層のパラメータ$\mathbf{W}^{(2)},\ \mathbf{b}^{(2)}$を用いて、第2層の重み付き和$\mathbf{A}^{(2)}$を計算します。
 また、3.5節で実装したソフトマックス関数softmax()を使って、$\mathbf{A}^{(2)}$を活性化してニューラルネットワークの出力$\mathbf{Y}$を求めます。$\mathbf{Y}$の各要素は、ソフトマックス関数によって0から1の値に変換され、またデータ(行)ごとの和が1になります。

 推論処理(2層のニューラルネットワークの計算)を行う関数として、この処理を定義します。

# 推論を行う関数を定義
def predict(x, params):
    # パラメータを取得
    W1, W2 = params['W1'], params['W2'] # 重み
    b1, b2 = params['b1'], params['b2'] # バイアス
    
    # 第1層の計算
    a1 = np.dot(x, W1) + b1 # 重み付き和
    z1 = sigmoid(a1) # 活性化
    
    # 第2層の計算
    a2 = np.dot(z1, W2) + b2 # 重み付き和
    y = softmax(a2) # 活性化(正規化)
    return y

 入力データxと全てのパラメータを格納したディクショナリparamsを引数に渡します。
 この例では、predict()の中でソフトマックス関数による正規化を行います。

 作成した関数を使って、ニューラルネットワークの計算を行います。

# 2層のニューラルネットワークの計算
y = predict(x, params)
print(np.round(y[:5], 4))
print(np.sum(y[:5], axis=1)) # 正規化の確認
print(np.argmax(y[:5], axis=1)) # 推論結果の抽出
[[0.0981 0.1082 0.1032 0.0946 0.0934 0.099  0.1056 0.0966 0.0955 0.1058]
 [0.0983 0.1079 0.1032 0.0947 0.0936 0.0989 0.1057 0.0964 0.0953 0.106 ]
 [0.0983 0.1081 0.1031 0.0942 0.0938 0.099  0.1057 0.0967 0.0953 0.1058]
 [0.0981 0.108  0.1031 0.0948 0.0934 0.099  0.1058 0.0966 0.0951 0.1061]
 [0.098  0.1081 0.1031 0.0947 0.0938 0.099  0.1057 0.0964 0.0952 0.106 ]]
[1. 1. 1. 1. 1.]
[1 1 1 1 1]

 この例では、入力データが偏りのない乱数なため、重みの初期値の影響のみを受けて結果が偏っています。

 以上で、推論処理を行えました。次は、この結果を使って学習の度合いを表す値を求めます。

・損失の計算

 4.2.4項で実装したcross_entropy_error()を使って、ニューラルネットワークの出力$\mathbf{Y}$と教師データ$\mathbf{T}$を用いて損失(交差エントロピー誤差)$L$を計算します。

# 交差エントロピー誤差を計算
L = cross_entropy_error(y, t)
print(L)
2.3018053423718414


 損失を計算する関数として、ここまでの処理を定義します。

# 損失を計算する関数を定義
def loss(x, params, t):
    # ニューラルネットワークの計算
    y = predict(x, params)
    
    # 交差エントロピー誤差を計算
    return cross_entropy_error(y, t)

 この関数の中で推論処理も行うことで、1つの関数で損失を計算できます。

 作成した関数を使って、損失を計算します。

# 損失を計算
L = loss(x, params, t)
print(L)
2.3018053423718414

 平均交差エントロピー誤差なので、データ数に影響されずに誤差(データへの当てはまりの度合い)を比較できます。

・認識精度の測定

 出力$\mathbf{Y}$と教師データ$\mathbf{T}$を比較して、推論結果(分類)が正しいか調べます。

# 予測ラベルを抽出
y_label = np.argmax(y, axis=1)
print(y_label[:5])

# 正解ラベルを抽出
t_label = np.argmax(t, axis=1)
print(t_label[:5])

# 値を比較
result = y_label == t_label
print(result[:5])

# 正解数を計算
print(np.sum(result))
[1 1 1 1 1]
[0 8 9 0 4]
[False False False False False]
9

 np.argmax()を使って、ニューラルネットワークの出力yと教師データtの最大値のインデックスを抽出します。インデックスがクラス番号(書かれている数字)に対応しています。
 抽出した値(ラベル)に対して、比較演算子==を使って同じ値か調べます。値が同じであればTrueとなり、正しく認識できたことを表します。値が違えばFalseとなり、認識できなかったこと表します。
 True1False0として処理されるので、データごとの結果resultの合計が正解数になります。

 ここでは学習を行っていいないので、当たるも当たらないもただの運です。

 認識精度(正解率)を計算します。

# 正解率を計算
acc = np.sum(y_label == t_label) / x.shape[0]
print(acc)
0.09

 正解数をデータ数x.shape[0]で割ることで、認識精度が求まります。

 認識精度を計算する関数として、この処理を定義します。

# 認識精度を計算する関数
def accuracy(x, params, t):
    # ニューラルネットワークの計算
    y = predict(x, params)
    
    # ラベルを抽出
    y = np.argmax(y, axis=1) # 予測
    t = np.argmax(t, axis=1) # 正解
    
    # 正解率を計算
    accuracy = np.sum(y == t) / float(x.shape[0]) # (正解数) / (画像数)
    return accuracy


 作成した関数を使って、認識精度を計算します。

# 認識精度を計算
acc = accuracy(x, params, t)
print(acc)
0.09

 当てずっぽうの10択なので大体1割になります。

 学習の進み具合を示す2つの指標を確認しました。次は、学習を行う際に利用する勾配を求めます。

・勾配の計算

 4.4.0項で実装したnumerical_gradient()を使って、各パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}$を計算します。
 ただし、numerical_gradient()の引数には、勾配の計算に用いる関数と変数を指定する必要があります。そこで、loss()の結果をそのまま返す関数を作成します。

# 損失メソッドを実行する関数を作成
f = lambda W: loss(x, params, t)

 簡単な関数なのでラムダ式で作成します。同じ関数をdefを使って作成すると次のコードになります。

# 損失メソッドを実行する関数を作成
def f(W):
    return loss(x, params, t)


 numerical_gradient()に損失を求める関数fと各パラメータを指定して、各パラメータの勾配を計算します。

# 第1層の重みの勾配を計算
dW1 = numerical_gradient(f, params['W1'])
print(dW1[:5, :5])
print(dW1.shape)

# 第1層のバイアスの勾配を計算
db1 = numerical_gradient(f, params['b1'])
print(db1[:5])
print(db1.shape)

# 第2層の重みの勾配を計算
dW2 = numerical_gradient(f, params['W2'])
print(dW2[:5, :5])
print(dW2.shape)

# 第2層のバイアスの勾配を計算
db2 = numerical_gradient(f, params['b2'])
print(db2[:5])
print(db2.shape)
[[ 2.87836330e-04  3.27081340e-04 -2.65071323e-04 -3.36469008e-05
   2.12145574e-04]
 [ 3.17229141e-04  2.85362705e-04 -2.29619317e-04  4.07053968e-05
   2.54317221e-04]
 [ 2.02243682e-04  2.77628605e-04 -1.53825606e-04  8.04985212e-05
   2.28645667e-04]
 [ 2.67658815e-04  2.65906934e-04 -1.79337742e-04 -2.40502973e-05
   2.77028924e-04]
 [ 6.97624269e-05  2.53800934e-04 -9.86010451e-05  6.50107101e-05
   2.19403675e-04]]
(784, 50)
[ 4.84404572e-04  5.36915243e-04 -4.66936325e-04  2.10456919e-05
  4.65179428e-04]
(50,)
[[-0.00115575  0.00793485  0.00188898  0.00721436 -0.03396303]
 [-0.00115071  0.00879973  0.00129269  0.00683302 -0.03204343]
 [-0.00075257  0.0079244   0.00152326  0.00675675 -0.0335608 ]
 [-0.00024765  0.00744973  0.00199949  0.0066998  -0.03135335]
 [-0.0010686   0.00882339  0.00067146  0.00747572 -0.03176842]]
(50, 10)
[-0.00176758  0.01803405  0.00313797  0.01458948 -0.06637191]
(10,)

 numerical_gradient()の内部では、指定した変数の各要素に対して順番に微小な値を加えて損失を計算することで偏微分を求めています。そのため、処理に時間がかかります。詳しくは4.4.0項と4.5.1項の最後を参照してください。

 勾配を計算する関数として、この処理を定義します。

# 勾配を計算する関数を定義
def numerical_gradient_tmp(x, W, t):
    # 損失を求める関数を作成
    loss_W = lambda W: loss(x, params, t)
    
    # 各パラメータの勾配
    grads = {}
    
    # 各パラメータの勾配をディクショナリに格納
    grads['W1'] = numerical_gradient(loss_W, params['W1']) # 第1層の重みの勾配
    grads['b1'] = numerical_gradient(loss_W, params['b1']) # 第1層のバイアスの勾配
    grads['W2'] = numerical_gradient(loss_W, params['W2']) # 第2層の重みの勾配
    grads['b2'] = numerical_gradient(loss_W, params['b2']) # 第2層のバイアスの勾配
    return grads

 関数内で損失を計算する関数loss_Wを作成しています。
 パラメータと同様に、勾配もディクショナリ型の変数に格納します。

 作成した関数を使って、各パラメータの勾配を計算します。

# 各パラメータの勾配を計算
grads = numerical_gradient_tmp(x, params, t)
print(grads.keys())
dict_keys(['W1', 'b1', 'W2', 'b2'])


 求めた勾配を確認します。

# 各パラメータの勾配を確認
for key in grads.keys():
    print(key) # キー名(パラメータ名)
    print(grads[key][0]) # 最初の値または行
    print(grads[key].shape) # 配列の形状
W1
[ 2.87836330e-04  3.27081340e-04 -2.65071323e-04 -3.36469008e-05
  2.12145574e-04 -1.56255009e-04  4.12564249e-05  3.19982485e-06
 -9.97256611e-05  2.11963758e-05  1.74199393e-04 -4.92245000e-05
 -2.70316001e-04  3.45558759e-05 -2.73253389e-04  1.86419902e-05
  1.58696787e-04  1.58066165e-04 -2.14096754e-04 -9.17215459e-05
  3.73380216e-05  1.57865023e-04  1.98447170e-05 -1.46629777e-04
 -2.73872063e-04  1.85764144e-04  6.68935218e-05 -1.40207288e-04
  5.52203616e-06 -1.38080367e-04 -4.11569809e-04 -3.24201963e-04
 -1.42450940e-05 -5.18796961e-05 -9.35124889e-05  2.89196982e-04
  3.65089594e-04  9.72865988e-05  1.88286595e-04  2.91752422e-05
  3.77544462e-05 -6.88221080e-05 -1.74864279e-04 -1.45658785e-04
  8.16193513e-05  3.28616365e-04 -6.25281604e-06 -1.69645642e-04
 -1.26728437e-04 -1.26962219e-05]
(784, 50)
b1
0.00048440457200982223
(50,)
W2
[-0.00115575  0.00793485  0.00188898  0.00721436 -0.03396303  0.03950984
 -0.01214626 -0.00123186  0.00732762 -0.01537876]
(50, 10)
b2
-0.0017675789409921094
(10,)

 for文を使って、キーを順番に変更してディクショナリに格納された値(配列)を取り出しています。

 以上が2層のニューラルネットワークで行う処理です。

・実装

 処理の確認ができたので、2層のニューラルネットワークをクラスとして実装します。

 このクラスは、インスタンスの作成時の引数に「パラメータの形状に関する3つの値」と「重みの初期値の標準偏差」をとります。初期化メソッドによって「パラメータの初期化」を行います。
 また、「推論処理(ニューラルネットワークの計算)」「損失の計算」「認識精度の計算」「勾配の計算」の4つのメソッドを持ちます。

# 2層のニューラルネットワークの実装
class TwoLayerNet:
    # 初期化メソッド
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # ディクショナリを初期化
        self.params = {}
        
        # パラメータの初期値をディクショナリに格納
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) # 第1層の重み
        self.params['b1'] = np.zeros(hidden_size) # 第1層のバイアス
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) # 第2層の重み
        self.params['b2'] = np.zeros(output_size) # 第2層のバイアス
    
    # 推論メソッド
    def predict(self, x):
        # パラメータを取得
        W1, W2 = self.params['W1'], self.params['W2'] # 重み
        b1, b2 = self.params['b1'], self.params['b2'] # バイアス
        
        # 第1層の計算
        a1 = np.dot(x, W1) + b1 # 重み付き和
        z1 = sigmoid(a1)        # 活性化
        
        # 第2層の計算
        a2 = np.dot(z1, W2) + b2 # 重み付き和
        y = softmax(a2)          # 活性化(正規化)
        return y
    
    # 損失メソッド
    def loss(self, x, t):
        # ニューラルネットワークの計算
        y = self.predict(x)
        
        # 交差エントロピー誤差を計算
        return cross_entropy_error(y, t)
    
    # 認識精度メソッド
    def accuracy(self, x, t):
        # ニューラルネットワークの計算
        y = self.predict(x)
        
        # ラベルを抽出
        y = np.argmax(y, axis=1) # 予測結果
        t = np.argmax(t, axis=1) # 正解ラベル
        
        # 精度を計算
        accuracy = np.sum(y == t) / float(x.shape[0]) # (正解数) / (画像数)
        return accuracy
    
    # 勾配メソッド
    def numerical_gradient(self, x, t):
        # 損失を求める関数を作成
        loss_W = lambda W: self.loss(x, t)
        
        # ディクショナリを初期化
        grads = {}
        
        # 各パラメータの勾配を計算してディクショナリに格納
        grads['W1'] = numerical_gradient(loss_W, self.params['W1']) # 第1層の重みの勾配
        grads['b1'] = numerical_gradient(loss_W, self.params['b1']) # 第1層のバイアスの勾配
        grads['W2'] = numerical_gradient(loss_W, self.params['W2']) # 第2層の重みの勾配
        grads['b2'] = numerical_gradient(loss_W, self.params['b2']) # 第2層のバイアスの勾配
        return grads

  パラメータをディクショナリ型のインスタンス変数として保存します。インスタンス自体がパラメータを持つことになるので、各メソッドの引数にパラメータを指定する必要がなくなります。

 メソッドの定義において、第1引数をselfにする必要があります。

 実装したクラスを試してみましょう。TwoLayerNetクラスのインスタンスを作成します。

# インスタンスを作成
net = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
net.params.keys()
dict_keys(['W1', 'b1', 'W2', 'b2'])

 インスタンスを作成した時点で、インスタンス変数としてパラメータが作成されています。

 インスタンス変数net.parmsとして保存されているパラメータ(の形状)を確認します。

# 各パラメータを確認
for key in net.params.keys():
    print(key) # キー名(パラメータ名)
    print(net.params[key].shape) # パラメータの形状
W1
(784, 50)
b1
(50,)
W2
(50, 10)
b2
(10,)


 推論メソッドpredict()を使って、ニューラルネットワークの計算を行います。

# ニューラルネットワークの計算
y = net.predict(x)
print(np.round(y[:5], 4)) # 値の確認
print(np.sum(y[:5], axis=1)) # 正規化の確認
print(np.argmax(y[:5], axis=1)) # 推論結果の確認
[[0.1023 0.1018 0.0979 0.1002 0.1093 0.0899 0.0994 0.0891 0.1031 0.1071]
 [0.1022 0.1016 0.098  0.1002 0.1092 0.0899 0.0995 0.0893 0.1031 0.107 ]
 [0.102  0.1018 0.0979 0.1003 0.1093 0.0899 0.0992 0.0893 0.1033 0.1071]
 [0.1023 0.1018 0.0978 0.1002 0.1092 0.09   0.0994 0.0892 0.1031 0.1071]
 [0.1022 0.1019 0.0981 0.1002 0.1092 0.0899 0.0991 0.0891 0.1032 0.1069]]
[1. 1. 1. 1. 1.]
[4 4 4 4 4]


 損失メソッドloss()を使って、損失(交差エントロピー誤差)を計算します。

# 損失を計算
L = net.loss(x, t)
print(L)
2.289267575488604


 認識精度メソッドaccuracy()を使って、認識精度(正解率)を測定します。

# 認識精度を計算
acc = net.accuracy(x, t)
print(acc)
0.16


 勾配メソッドnumerical_gradient()を使って、各パラメータの勾配を計算します。

# 各パラメータの勾配を計算
grasd = net.numerical_gradient(x, t)
print(grads.keys())
dict_keys(['W1', 'b1', 'W2', 'b2'])

 各パラメータの全ての要素分の数値微分の計算を行うので、処理に時間がかかります。

 勾配も確認しましょう。

# 各パラメータの勾配を確認
for key in grads.keys():
    print(key) # キー名(パラメータ名)
    print(grads[key].shape) # 勾配の形状
W1
(784, 50)
b1
(50,)
W2
(50, 10)
b2
(10,)


 以上で、2層のニューラルネットワークを実装できました。次項では、実装したクラスを使って、手書き数字に対する学習を行います。

参考文献

おわりに

 4章クライマックスです。次項は、この記事で実装したクラスを使い学習を行います。このまま進めちゃいましょう!

 あと挿入歌です!ハロー!プロジェクトのグループ「こぶしファクトリー」による「念には念を(念入りVer.)」のアカペラバージョンです。

 「急がば回れ」と「亀になれ!」もあるのですが、アルバム曲なので公式には上がってないです(残念)。

  • 2021.08.27:加筆修正しました。


【次節の内容】

www.anarchive-beta.com