からっぽのしょこ

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

5.7.2-3:誤差逆伝播法に対応したニューラルネットワークの実装【ゼロつく1のノート(実装)】

はじめに

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

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

 この記事は、5.7.2項「誤差逆伝播法に対応したニューラルネットワークの実装」と5.7.3項「誤差逆伝播法の勾配確認」の内容です。誤差逆伝播法を用いた2層のニューラルネットワークをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

5.7.2 誤差逆伝播法に対応したニューラルネットワークの実装

 5章で実装してきたレイヤを組み合わせて、2層のニューラルネットワークを実装します。レイヤごとに解析的に微分を計算することで、誤差逆伝播法により各パラメータの勾配を効率よく計算できます。2層のニューラルネットワークの計算過程については「4.5.1:2層ニューラルネットワークのクラス【ゼロつく1のノート(実装)】 - からっぽのしょこ」を参照してください。

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

# 5.7.2項で利用するライブラリ
import numpy as np
from collections import OrderedDict # 順番付きディクショナリ


 またこの節では、5章で実装した各レイヤのクラスと、「4.4.0:勾配【ゼロつく1のノート(数学)】 - からっぽのしょこ」の(バッチ対応版)数値微分により勾配を計算する関数numerical_gradient()を利用します。そのため、クラス定義・関数定義を再度実行しておく必要があります。
 または次の方法で、「deep-learning-from-scratch-master」フォルダ内の「common」フォルダの「layers.py」ファイルから実装済みのクラスを読み込むこともできます。ファイルの読み込みについては「3.6.1:MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ」を参照してください。

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

# 実装済み関数を読み込み
from common.layers import * # 各レイヤ:5章
from common.gradient import numerical_gradient # 数値微分による勾配の計算:4.4節


・処理の確認

 2層のニューラルネットワークで行う処理を確認します。処理(計算)の流れは4.5.1項と同様です。そちらも参考にしてください。

・順番付きディクショナリ

 4.5.2項で実装したニューラルネットワークのクラスとの主な違いは、レイヤごとにクラスを使って計算する点です。その各レイヤのクラスのインスタンスは、OrdereDictライブラリの順番付きディクショナリを使います。まずはこの機能と使い方を確認しましょう。
 ただし、Python 3.7以降は、通常のディクショナリでも順番を保存するようになったようで、このライブラリを使う理由がなくなりました。一応実装の通りにやってみます。

 OrderedDict()(または{})で順番付きディクショナリ型のオブジェクトを作成します。

# (空の)順番付きディクショナリを作成
dic_123 = OrderedDict()
#dic_123 = {}
print(dic_123.items())
odict_items([])

 引数に何も指定しないと、空のディクショナリを作成します。
 items()メソッドで、ディクショナリに格納されているキーと値を取り出せます。

 通常のディクショナリと同様に、変数名['キー'] = 値で新たにキーを指定して値(パラメータの配列やレイヤのインスタンス)を格納します。

# キーを指定して値を格納
dic_123['One'] = '構って欲しくて'
dic_123['Two'] = '触って欲しくて'
dic_123['Three'] = '笑って欲しくて'

# 中身を確認
print(dic_123.keys())   # キー
print(dic_123.values()) # 値
odict_keys(['One', 'Two', 'Three'])
odict_values(['構って欲しくて', '触って欲しくて', '笑って欲しくて'])

 通常のディクショナリ変数と同様に、キーと値をセットで指定します。
 keys()メソッドでキーを、values()メソッドで値を取り出せます。

 格納した順番に並んでいるのを確認できました。

 keys()の返り値を用いて、for文で格納した値を順番に取り出します。

# 順番にデータを取り出して処理
for key in dic_123.keys():
    # 値を取得
    val = dic_123[key]
    print(key)
    print(val)
One
構って欲しくて
Two
触って欲しくて
Three
笑って欲しくて

 順伝播では、このように取り出したval(レイヤのインスタンス)を使って処理(計算)します。

 逆伝播では、逆順に取り出す必要があります。

 keys()でキーを取り出して、list()でリスト型のオブジェクトに変換します。
 さらに、reverse()メソッドでリストの要素(キー)を逆順に並べ替えます。

# キーを取り出してリスト型に変換
key_list = list(dic_123.keys())
print(key_list)

# 要素を逆順に変換
key_list.reverse()
print(key_list)
['One', 'Two', 'Three']
['Three', 'Two', 'One']

 逆順にキーを持つリストが得られました。

 これを使って、先ほどと同様に処理します。

# 逆順でデータを取り出して処理
for key in key_list:
    # 値を取得
    val = dic_123[key]
    print(key)
    print(val)
Three
笑って欲しくて
Two
触って欲しくて
One
構って欲しくて

 逆順でval(レイヤのインスタンス)を取り出せました。

 この処理は「ネットワークの作成」で利用します。

・入力を作成

 この例ではMNISTデータセットの代わりに、簡易的に入力データと教師データを作成します。

 各層のニューロン数(入力データと出力データの形状に関する値)を指定します。

# バッチサイズを指定
batch_size = 100

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

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

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

 バッチサイズ(1試行当たりのデータ数)$N$をbatch_sizeとして値を指定します。
 入力データの要素数(入力層のニューロン数)$D$をinput_sizeとして値を指定します。手書き数字では、ピクセル数の784です。
 中間層のニューロン数$H$をhidden_sizeとして値を指定します。
 分類するクラス数(出力データの要素数・出力層のニューロン数)$K$をoutput_sizeとして値を指定します。手書き数字認識では、数字の種類数の10です。

 指定したサイズに従って、入力データ$\mathbf{X} = (x_{0,0}, \cdots, x_{N-1,D-1})$と教師データ$\mathbf{T} = (t_{0,0}, \cdots, t_{N-1,K-1})$を作成します。

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

# (仮の)教師データ(正解ラベル)を作成
t = np.random.multinomial(n=1, pvals=np.repeat(1 / output_size, output_size), size=batch_size)
print(t.shape)
(100, 784)
(100, 10)

 入力データxは0から1の一様乱数np.random.rand()で、教師データtは多項分布に従う乱数np.random.multinomial()で作成します(今後は登場しない関数なので解説は省略します)。

・ネットワークの作成

 2層のニューラルネットワークを構築します。この例では、活性化関数にReLU関数を用いて、レイヤの構成を「Affineレイヤ」-「ReLUレイヤ」-「Affineレイヤ」-「Softmax-with-Lossレイヤ」とします。

 レイヤのインスタンスを作成する前に、パラメータを作成します。

 第1層の重み$\mathbf{W}^{(1)} = (w_{0,0}, \cdots, w_{D-1,H-1})$とバイアス$\mathbf{b}^{(1)} = (b_0, \cdots, b_{H-1})$、第2層の重み$\mathbf{W}^{(2)} = (w_{0,0}, \cdots, w_{H-1,K-1})$とバイアス$\mathbf{b}^{(2)} = (b_0, \cdots, b_{K-1})$を作成して、ディクショナリ変数paramsに格納します。

# 重みの初期値の標準偏差を指定
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層のバイアス
print(params['W1'].shape)
print(params['b1'].shape)
print(params['W2'].shape)
print(params['b2'].shape)
(784, 50)
(50,)
(50, 10)
(10,)

 重みの初期値は、平均0・標準偏差weight_init_stdのガウス分布(正規分布)に従いランダムに生成します。重みの初期値については6.2節を参照してください。
 バイアスの初期値は、全ての要素を0とします。

 ニューラルネットワークで利用するレイヤのインスタンスを作成して、ディクショナリ変数layersに格納します。

# レイヤ格納用のディクショナリを作成
layers = OrderedDict() # 順番付きディクショナリ

# レイヤのインスタンスをディクショナリに格納
layers['Affine1'] = Affine(params['W1'], params['b1']) # 第1層のAffineレイヤ
layers['Relu1'] = Relu() # 第1層のReLUレイヤ
layers['Affine2'] = Affine(params['W2'], params['b2']) # 第2層のAffineレイヤ
print(layers.keys())
print(layers.values())
odict_keys(['Affine1', 'Relu1', 'Affine2'])
odict_values([<common.layers.Affine object at 0x0000020978F66788>, <common.layers.Relu object at 0x0000020978F667C8>, <common.layers.Affine object at 0x0000020978F66888>])

 values()の返り値(レイヤのインスタンス)に関して、クラス定義を実行した場合はレイヤ名、「common」フォルダから読み込んだ場合はcommon.layers.クラス名と表示されます。

 Softmax-with-Lossレイヤは、学習時のみ利用します。推論(画像認識)や精度測定では、正規化と損失の計算は必要ありません。
 そこで、SoftmaxWithLossクラスのインスタンスは、lastLayerとしてlayersとは別に作成します。

# Softmax-with-Lossレイヤのインスタンスを作成
lastLayer = SoftmaxWithLoss()


 ニューラルネットワークを構築できました。続いて、ニューラルネットワークを用いた処理を確認していきます。

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

 作成したニューラルネットワークを使って、順伝播を計算します。この計算を推論処理とも呼びます。

 layersから順番にレイヤ(のインスタンス)を取り出して順伝播メソッドを実行します。

# 各レイヤを順番に処理
for layer in layers.values():
    # 順伝播を計算
    x = layer.forward(x)
    
    # 確認
    print(type(layer)) # レイヤのインスタンス
    print(x.shape) # 出力
<class 'common.layers.Affine'>
(100, 50)
<class 'common.layers.Relu'>
(100, 50)
<class 'common.layers.Affine'>
(100, 10)

 順伝播メソッドfoward()を実行する度に配列xが置き換わり、次のレイヤの順伝播メソッドの引数になります。

 実装では、この処理をまとめて推論メソッドpredict()とします。また、predict()メソッドの返り値をyとします。
 そこで、ここでは分かりやすいように、最後の配列xyとして複製しておくことにします。

# スコアを計算(ニューラルネットワークの処理)
y = x.copy()

 この出力yは最終層の活性化(正規化)前の出力なので、図3-20などでは$\mathbf{a}$を使って表記しています。

 ニューラルネットワークの出力yを用いて、以降の計算を行います。

・損失の計算

 学習時に行う計算を確認します。

 SoftmaxWithLossクラス(lastLayer)の順伝播メソッドで損失(交差エントロピー誤差)を計算します。

# 損失を計算
loss = lastLayer.forward(y, t)
print(loss) # 返り値
print(lastLayer.loss) # インスタンス変数
2.3025296472415793
2.3025296472415793

 損失の値はインスタンス変数lossとしても保存されます(5.6.3項)が、ここでは使いません。

・認識精度の計算

 認識精度として正解率を求めます。

 出力yの行(データ)ごとに最大値のインデックスを抽出します。

# 元の配列を確認
print(np.round(y[:5], 3))

# 推論結果を抽出
y = np.argmax(y, axis=1)
print(y[:5])
[[-0.     0.009  0.008  0.004 -0.003  0.013  0.004  0.004 -0.004 -0.006]
 [-0.001  0.011  0.005  0.014  0.002  0.007 -0.002  0.01  -0.009 -0.009]
 [ 0.004 -0.    -0.     0.002 -0.009  0.008  0.003 -0.001 -0.013 -0.01 ]
 [ 0.001  0.006  0.005  0.008  0.002  0.008  0.001  0.005 -0.011 -0.006]
 [ 0.012  0.004  0.009  0.011 -0.005  0.005  0.006  0.011 -0.009 -0.003]]
[5 3 5 3 0]

 列番号が分類されたクラスに対応します。つまり、予測(分類)した数字を示します。

 同様に、教師データtから正解のラベルの値を抽出します。

# 元の配列を確認
print(t[:5])

# 正解ラベルを抽出
if t.ndim != 1: # one-hot表現の場合
    t = np.argmax(t, axis=1)
print(t[:5])
[[0 0 0 0 0 0 0 0 0 1]
 [0 0 0 0 1 0 0 0 0 0]
 [0 0 0 0 0 1 0 0 0 0]
 [0 1 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 1 0 0]]
[9 4 5 1 7]

 教師データtの各要素は01の値をとるので、1が最大値であり、そのインデックスがラベルの値(書かれている数字)を示します。
 ただし、one-hot表現でない場合はこの処理は不要です。

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

# 正解率を計算
accuracy = np.sum(y == t) / float(x.shape[0]) # (正解数) / (バッチサイズ)
print(accuracy)
0.1

 正解数np.sum(y == t)をデータ数x.shape[0]で割って正解率を求めます。分母に関して、float()で浮動小数点数に変換しています。

・勾配の計算

 続いて、逆伝播の計算を行います。誤差逆伝播法により勾配を計算します。数値微分による計算については、4.5.1項と同じなので省略します。

 ニューラルネットワークの逆伝播の入力(Softmax-with-Lossレイヤの逆伝播の入力)$\frac{\partial L}{\partial L} = 1$をdoutとして作成します。

# 逆伝播の入力を作成
dout = 1


 doutSoftmaxWithLossクラス(lastLayer)の逆伝播メソッドに入力して、$\frac{\partial L}{\partial \mathbf{a}}$を計算します。

# Softmax-with-Lossレイヤの逆伝播を計算
dout = lastLayer.backward(dout)

 順伝播の計算時のxと同様に、doutを更新していきます。

 layersに格納されているレイヤ(のインスタンス)を逆順に取り出して、逆伝播メソッドを実行します。

# ディクショナリからレイヤを取り出してリストに変換
layers_lt = list(layers.values())

# 逆順に並び替え
layers_lt.reverse()

# 各レイヤの逆伝播を逆順に計算
for layer in layers_lt:
    # 逆伝播を計算
    dout = layer.backward(dout)
    
    # 確認
    print(type(layer)) # レイヤのインスタンス
    print(dout.shape) # 出力
<class 'common.layers.Affine'>
(100, 50)
<class 'common.layers.Relu'>
(100, 50)
<class 'common.layers.Affine'>
(100, 784)

 最後の出力doutは、ニューラルネットワークの入力の勾配$\frac{\partial L}{\partial \mathbf{X}}$です。

 各レイヤのインスタンス変数として保存されている勾配(の配列)を取り出して、ディクショナリ変数gradsに格納します。

# 勾配格納用のディクショナリを作成
grads = {}

# 勾配をディクショナリに格納
grads['W1'] = layers['Affine1'].dW # 第1層の重みの勾配
grads['b1'] = layers['Affine1'].db # 第1層のバイアスの勾配
grads['W2'] = layers['Affine2'].dW # 第2層の重みの勾配
grads['b2'] = layers['Affine2'].db # 第2層のバイアスの勾配

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

 layersは、各レイヤのインスタンスを格納したディクショナリでした。layers['Affine1']で、第1層のAffineレイヤのインスタンスを取り出しています。さらに、layers['Affine1'].dWで、第1層のAffineレイヤの重みの勾配を取り出しています。

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

・実装

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

# 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層のバイアス
        
        # レイヤ格納用のディクショナリを作成
        self.layers = OrderedDict() # 順番付きディクショナリ
        
        # レイヤのインスタンスをディクショナリに格納
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1']) # 第1層のAffineレイヤ
        self.layers['Relu1'] = Relu() # 第1層のReLUレイヤ
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2']) # 第2層のAffineレイヤ
        
        # Softmax-with-Lossレイヤのインスタンスを作成
        self.lastLayer = SoftmaxWithLoss()
        
    # 推論メソッド
    def predict(self, x):
        # 各レイヤを順番に処理
        for layer in self.layers.values():
            # 順伝播を計算
            x = layer.forward(x)
        return x
    
    # 損失メソッド
    def loss(self, x, t):
        # スコアを計算(ニューラルネットワークの処理)
        y = self.predict(x)
        
        # 損失を計算
        return self.lastLayer.forward(y, t)
    
    # 認識精度メソッド
    def accuracy(self, x, t):
        # スコアを計算(ニューラルネットワークの処理)
        y = self.predict(x)
        
        # 推論結果を抽出
        y = np.argmax(y, axis=1)
        
        # 正解ラベルを抽出
        if t.ndim != 1: # one-hot表現の場合
            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
    
    # 誤差逆伝播法による勾配メソッド
    def gradient(self, x, t):
        # 損失を計算:(結果はインスタンス変数に保存される)
        self.loss(x, t)
        
        # 逆伝播の入力を作成
        dout = 1
        
        # Softmax-with-Lossレイヤの逆伝播を計算
        dout = self.lastLayer.backward(dout)
        
        # ディクショナリから全てのレイヤを取り出してリストに変換
        layers = list(self.layers.values())
        
        # 逆順に並び替え
        layers.reverse()
        
        # 各レイヤの逆伝播を逆順に計算
        for layer in layers:
            # 逆伝播を計算
            dout = layer.backward(dout)
        
        # 勾配格納用のディクショナリを作成
        grads = {}
        
        # 勾配をディクショナリに格納
        grads['W1'] = self.layers['Affine1'].dW # 第1層の重みの勾配
        grads['b1'] = self.layers['Affine1'].db # 第1層のバイアスの勾配
        grads['W2'] = self.layers['Affine2'].dW # 第2層の重みの勾配
        grads['b2'] = self.layers['Affine2'].db # 第2層のバイアスの勾配
        return grads

 パラメータをディクショナリ型のインスタンス変数params、レイヤをディクショナリ型のインスタンス変数layers、ただし最終層のレイヤはlastLayerとして保存します。

 勾配メソッドgradient()を実行すると、内部で損失メソッドloss()が実行されます。また、loss()の内部で推論メソッドpredict()が実行されます。
 これにより、逆伝播の計算で用いる中間変数が各レイヤのインスタンス変数に保存されます。

 実装したクラスを試してみましょう。

 入力データと教師データを作成します。

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

# (仮の)教師データ(正解ラベル)を作成
t = np.random.multinomial(n=1, pvals=np.repeat(1 / output_size, output_size), size=batch_size)
print(t.shape)
(100, 784)
(100, 10)

 処理の確認中にxの中身が変更されているので、再度作成する必要があります。

 TwoLayerNetクラスのインスタンスを作成します。

# 2層のニューラルネットワークのインスタンスを作成
network = TwoLayerNet(input_size, hidden_size, output_size)

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

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

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

# ニューラルネットワークの計算
y = network.predict(x)
print(y.shape)
(100, 10)

 yは、正規化していないニューラルネットワーク出力です。

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

# 損失を計算
loss = network.loss(x, t)
print(loss)
2.3031789315861535


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

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


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

# 勾配を計算
grads = network.gradient(x, t)

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

 各パラメータの勾配が得られました。この勾配を用いて、損失が小さくなるように各パラメータを更新します。

 以上で、ニューラルネットワークを実装できました。学習を行う前に、誤差逆伝播法による勾配の計算が正しく組めているのかを確認します。

5.7.3 誤差逆伝播法の勾配確認

 数値微分で求めた勾配(の値)と誤差逆伝播法により解析的に求めた勾配を比較して、正しく実装できているのかを確認します。これを勾配確認と言います。

・データセットの用意

 まずは、利用するデータセットを読み込みます。

 MNISTデータセットの読み込みに利用する関数load_mnist()を読み込んで、データセットを取得します。詳しくは「3.6.1:MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ」を参照してください。

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

# MNISTデータセット読み込み関数を読み込む
from dataset.mnist import load_mnist

# 画像データを読み込む
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=True)
print(x_train.shape)
print(t_train.shape)
(60000, 784)
(60000, 10)

 load_mnist()を使って、手書き数字の画像データとラベルデータを読み込みます。この例では、正規化normalize、1次元化flatten、one-hot表現化one_hot_labelの全ての引数をTrueとします。

 読み込んだデータセットの一部を取り出します。

# バッチサイズを指定
batch_size = 3

# バッチデータを抽出
x_batch = x_train[:batch_size]
t_batch = t_train[:batch_size]
print(x_batch)
print(x_batch.shape)
print(t_batch)
print(t_batch.shape)
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]
(3, 784)
[[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]]
(3, 10)

 この例では、3枚分のデータを使うことにします。

・勾配の確認

 TwoLayerNetクラスのインスタンスを作成して、数値微分による勾配計算メソッドnumerical_gradient()と、誤差逆伝播法による勾配計算メソッドgradient()で勾配を求めます。

# 2層のニューラルネットワークのインスタンスを作成
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 数値微分により勾配を計算
grad_numerical = network.numerical_gradient(x_batch, t_batch)
print(grad_numerical.keys())

# 誤差逆伝播法により勾配を計算
grad_backprop = network.gradient(x_batch, t_batch)
print(grad_backprop.keys())
dict_keys(['W1', 'b1', 'W2', 'b2'])
dict_keys(['W1', 'b1', 'W2', 'b2'])

 各パラメータの勾配がディクショナリに格納されています。

 数値微分により求めた勾配grad_numericalと、誤差逆伝播法により求めた勾配grad_backpropの値を比較します。

 試しに、第1層のバイアスに関する勾配の差をみてみましょう。

# 差を確認
print(np.round(grad_numerical['b1'] - grad_backprop['b1'], 10))
[-2.7e-09  0.0e+00  0.0e+00  6.2e-09  0.0e+00  6.7e-09 -1.9e-09 -3.0e-10
  3.9e-09  2.1e-09 -5.1e-09  0.0e+00  0.0e+00  4.1e-09  2.5e-09  0.0e+00
 -2.2e-09  1.9e-09  1.4e-09 -2.3e-09 -5.8e-09  1.7e-09  0.0e+00  5.6e-09
  1.5e-09 -7.0e-10 -2.0e-09  3.9e-09  9.0e-10 -3.0e-09 -4.0e-10  0.0e+00
  0.0e+00 -2.7e-09 -6.9e-09  2.0e-10  9.9e-09 -3.5e-09 -6.0e-10  1.6e-09
  8.5e-09 -2.7e-09  0.0e+00  0.0e+00  2.1e-09 -2.0e-10  0.0e+00 -5.0e-10
  0.0e+00 -2.1e-09]

 2つの方法による勾配の差がほぼ0です。プログラム上の誤差もありますが、数値微分はそもそも誤差を必ず含む求め方なので、0にはなりません。丸め込みの桁数を変更すると、0ではないことが確認できます。

 では、全てのパラメータで誤差を確認しましょう。

 要素ごとの誤差はプラスにもマイナスにもなるため、そのまま和をとると相殺されてしまします。なので、np.abs()で絶対値をとってからnp.average()で平均を求めます。

# パラメータごとに誤差を計算
for key in grad_numerical.keys():
    # 差を計算
    diff = np.average(np.abs(grad_numerical[key] - grad_backprop[key]))
    print(key + ' : ' + str(np.round(diff, 10)))
W1 : 4e-10
b1 : 2.2e-09
W2 : 5e-09
b2 : 1.399e-07


 以上で、誤差逆伝播法による勾配計算に問題がないことを確認できました。次項では、勾配降下法による(4章での処理に時間がかかる問題を回避した)学習を行います。

参考文献

github.com

おわりに

 パーツを作って、組み立てたのなら、次は実践ですね!ここで止めるとクラスを再実行したりと面倒なので、このままラストまでやっちゃいましょう。

 今日の挿入歌♪♪

 8年前の曲ですが、現メンバーも4人います。当時は全然知りませんでした。

 そ・れ・と、2020年8月7日はハロプログループ「BEYOOOOONDS」メジャーデビュー1周年!おめでとおおおおおございます!!!!!!!!!!!


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


【次節の内容】

www.anarchive-beta.com