kaeken(嘉永島健司)のTech探究ブログ

主に情報科学/情報技術全般に関する知見をポストします。(最近は、特にData Science、機械学習、深層学習、統計学、Python、数学、ビッグデータ)

5章 誤差逆伝播法(ごさぎゃくでんぱほう)Backpropagation『ゼロから作るDeep Learning』

5章誤差逆伝播

いよいよ誤差逆伝播法まできた。

数式より計算グラフの方が理解しやすいらしい。

詳しくは本書参照。

まずはレイヤ概念の導入。

レイヤ(層)とは:NNにおける機能の単位を指す。レイヤ単位で実装することで、ブロックのように積み上げていける。

まず、乗算レイヤの実装と、加算レイヤの実装について。

# cat layer_naive.py


# coding: utf-8


class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y
        out = x * y

        return out

    def backward(self, dout): #dout微分
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy


class AddLayer:
    def __init__(self):
        pass

    def forward(self, x, y):
        out = x + y

        return out

    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy

つぎに、活性化関数レイヤ(ReLUレイヤ、Sigmoidレイヤ)の実装について。

# cat common/layers.py

# coding: utf-8
import numpy as np
from common.functions import *
from common.util import im2col, col2im


class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx


class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

つぎに、Affineレイヤの実装について。

Affineとは:行列の内積を行うレイヤ。幾何学で「アフィン変換」と呼ばれる。

class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b

        self.x = None
        self.original_x_shape = None
        # 重み・バイアスパラメータの微分
        self.dW = None
        self.db = None

    def forward(self, x):
        # テンソル対応
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)

        dx = dx.reshape(*self.original_x_shape)  # 入力データの形状に戻す(テンソル対応)
        return dx

つぎに、Softmaxレイヤの実装について。

Softmaxレイヤは学習時のみ必要で推論時には必要ない。

サンプルでは、Softmax関数と交差エントロピー誤差のレイヤSoftmax-with-Lossを定義している

導出過程が複雑なので、付録Aをあとで参照。

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None # softmaxの出力
        self.t = None # 教師データ

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 教師データがone-hot-vectorの場合
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size

        return dx

最後に、誤差逆伝播法の実装について。

以下の学習4ステップのうち、ステップ2勾配の算出で登場

学習の4ステップ

ステップ1ミニバッチ:ミニバッチをランダムに選択
ステップ2勾配算出:ミニバッチの損失関数を減らす勾配を算出
ステップ3パラメータ更新:重みパラメータを勾配方向に微小量だけ更新
ステップ4反復:ステップ1−3を反復
# cat two_layer_net.py


# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
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):
        # 重みの初期化
        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):
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    # x:入力データ, t:教師データ
    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 : 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):
        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):
        # 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とは:誤差逆伝播法と数値微分の結果を比較し、ほぼ近似できることで誤差逆伝播法の正しさを確認すること。

# cat deep-learning-from-scratch/ch05/gradient_check.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# データの読み込み
(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))

勾配確認のサンプルコードを実行してみる:

# py gradient_check.py
W2:9.77654477536e-13
W1:2.37797639143e-13
b2:1.2012613404e-10
b1:7.43019632366e-13

ほぼ誤差がないことが分かった。

そして、誤差逆伝播法を訓練データを学習させてみる。

# py train_neuralnet.py

0.102266666667 0.1016
0.90125 0.9073
0.9205 0.9221
0.936033333333 0.9349
0.945816666667 0.9461
0.9512 0.9504
0.957433333333 0.955
0.961133333333 0.959
0.965266666667 0.962
0.967566666667 0.9636
0.970366666667 0.9637
0.97065 0.9635
0.974466666667 0.9674
0.974416666667 0.9671
0.976816666667 0.968
0.97775 0.9698
0.978783333333 0.971

認識精度がかなり向上していることがわかる。

5章まとめ問題

・◯◯1を用いれば計算過程を視覚的に把握できる
・◯◯2は局所的な計算によって全体の計算を構成する
・◯◯1の◯◯3は通常の計算を行い、◯◯4によって各ノードの微分が求まる
・◯◯5とは、NNの構成要素を◯◯6で実装することで勾配計算を効率化する
・◯◯6とは、◯◯5と◯◯7の結果を比較して◯◯5の実装が誤っていないことが確認できる