6.6. 誤差伝播法の実装

ニューラルネットワークは「重み」と「バイアス」を訓練データに適応するように調整する必要がある。 これが学習フェーズ。

  1. (ミニバッチ)訓練データからランダムに、一部のデータを選ぶ

  2. (勾配の算出) 重みパラメーターに関する損失関数の勾配を求める (どういう重みを設定すればよいのか)

  3. (パラメータの更新) 重みパラメーターを勾配方向に微小量だけ更新する (答えに近づくために、勾配から見てパラメーターを調整する)

  4. 繰り返す

6.6.1. 実装

MNISTデータを使ったニューラルネットワークのため、
2つのレイヤーを持つニューラルネットワークを作ります
[1]:
# coding: utf-8
import sys, os
sys.path.append(os.path.abspath(os.path.join('..', 'sample')))
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
        """
        重みとバイアスを初期化します

        Parameters
        ----------
        input_size :
            入力層のニューロン数
        hidden_size :
            隠れ層のニューロン数
        output_size :
            出力層のニューロン数
        weight_init_std :
            重み初期化時のガウス分布のスケール

        Attributes
        ----------
        params :
            ニューラルネットワークの重みとバイアス
        layers :
            レイヤーとなる関数。
            辞書として引くのではなく、計算順序が大事になる
        lastLayer :
            出力層
        """
        # 重みの初期化
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

        # レイヤの生成
        self.layers = OrderedDict() # 順序付き辞書
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])

        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        """
        推論をします

        Parameters
        ----------
        x : numpy.array
            入力データの画像

        Returns
        -------
        x : numpy.array
            計算結果
        """
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    # x:入力データ, t:教師データ
    def loss(self, x, t):
        """
        損失関数の値を求めます

        Parameters
        ----------
        x : numpy.array
            入力データの画像
        t :
            正解ラベル

        Returns
        -------
        accuracy :
            損失
        """
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

    def accuracy(self, x, t):
        """
        認識制度を求めます

        Parameters
        ----------
        x : numpy.array
            入力データの画像
        t :
            正解ラベル

        Returns
        -------
        accuracy :
            認識制度
        """
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    # x:入力データ, t:教師データ
    def numerical_gradient(self, x, t):
        """
        重みパラメータに対する勾配を、数値微分で求めます
        (処理が重い)

        Parameters
        ----------
        x : numpy.array
            入力データの画像
        t :
            正解ラベル

        Returns
        -------
        grads :
            勾配
        """
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

        return grads

    def gradient(self, x, t):
        """
        重みパラメータに対する勾配を、誤差逆伝播法で求めます

        Parameters
        ----------
        x : numpy.array
            入力データの画像
        t :
            正解ラベル

        Returns
        -------
        grads :
            勾配
        """
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

数値微分は、実装が簡単だがその計算は重い。一方誤差伝搬法は実装が複雑だが計算が軽い。

というわけで、誤差伝搬法の計算が本当に正しいのかを検証するために、勾配確認(gradient check) を行う。

[2]:
from dataset.mnist import load_mnist

# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

x_batch = x_train[:3]
t_batch = t_train[:3]

grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)

for key in grad_numerical.keys():
    diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
    # print(key + ":" + str(diff))
    print(key + ":" + str(diff) + "->" + f'{diff:f}')
W1:4.690990345148965e-10->0.000000
b1:2.8160295458966487e-09->0.000000
W2:6.563031654817776e-09->0.000000
b2:1.3939825101727532e-07->0.000000

6.6.2. 学習させてみよう!

[3]:
# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 勾配
    #grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)

    # 更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)

print("実験終了!")
0.13013333333333332 0.1259
0.9023833333333333 0.9067
0.9237166666666666 0.9251
0.9335333333333333 0.9328
0.9443666666666667 0.9428
0.9490833333333333 0.9473
0.9538166666666666 0.949
0.9593833333333334 0.9544
0.9618333333333333 0.9579
0.9639166666666666 0.9592
0.96775 0.9609
0.9700833333333333 0.9604
0.97255 0.9624
0.97375 0.9649
0.975 0.9656
0.9765166666666667 0.9663
0.9776166666666667 0.9682
実験終了!

6.7. まとめ

  • 誤差伝搬法により、数値微分よりも速く学習をすることができる

    • ただし複雑

  • 計算グラフによって、一部の計算だけをピックアップし、計算過程を視覚的に把握することができる

  • 計算グラフの順伝播は通常の計算、逆伝播は微分(偏微分)を求められる

  • 計算グラフから、誤差伝播法の実装方法がわかる

  • 誤差伝搬法の実装が、数値微分の実装結果とほぼ変わらないのかを確認するのに、勾配を見る