LoginSignup
14
11

ゼロから作るDeep Learningで素人がつまずいたことメモ:8章

Last updated at Posted at 2020-02-22

はじめに

ふと思い立って勉強を始めた「ゼロから作るDeep LearningーーPythonで学ぶディープラーニングの理論と実装」の8章で私がつまずいたことのメモです。

実行環境はmacOS Mojave + Anaconda 2019.10、Pythonのバージョンは3.7.4です。詳細はこのメモの1章をご参照ください。

(このメモの他の章へ:1章 / 2章 / 3章 / 4章 / 5章 / 6章 / 7章 / 8章 / まとめ

この記事は個人で作成したものであり、内容や意見は所属企業・部門見解を代表するものではありません。

8章 ディープラーニング

この章は、層を深くしたディープなニューラルネットワークの説明です。

8.1 ネットワークをより深く

これまでに学んだことを使って、ディープなネットワークでMNISTの手書き文字認識の実装に挑戦します。残念ながらこの章はソースの解説が全くないので大変です。

前章までで学んだDropoutやAdamは実装をサボっていたのですが、今回使うのでここから片付けます。

(1)Dropoutレイヤーの実装

Dropoutレイヤーは本の「6.4.3 Droput」に実装の解説があるので、それを見ながら実装しました。

dropout.py
# coding: utf-8
import numpy as np


class Dropout:
    def __init__(self, dropout_ratio=0.5):
        """Dropoutレイヤー
        
        Args:
            dropout_ratio (float): 学習時のニューロンの消去割合、デフォルトは0.5。
        """
        self.dropout_ratio = dropout_ratio              # 学習時のニューロンの消去割合
        self.valid_ratio = 1.0 - self.dropout_ratio     # 学習時に生かしていた割合
        self.mask = None                                # 各ニューロンの消去有無を示すフラグの配列

    def forward(self, x, train_flg=True):
        """順伝播
        
        Args:
            x (numpy.ndarray): 入力
            train_flg (bool, optional): 学習中ならTrue、デフォルトはTrue。
        
        Returns:
            numpy.ndarray: 出力
        """
        if train_flg:
            # 学習時は消去するニューロンを決めるマスクを生成
            self.mask = np.random.rand(*x.shape) > self.dropout_ratio

            # 出力を算出
            return x * self.mask
        
        else:
            # 認識時はニューロンは消去しないが、学習時の消去割合を加味した出力に調整する
            return x * self.valid_ratio

    def backward(self, dout):
        """逆伝播
        
        Args:
            dout (numpy.ndarray): 右の層から伝わってくる微分値
        
        Returns:
            numpy.ndarray: 微分値(勾配)
        """
        # 消去しなかったニューロンのみ右の層の微分値を逆伝播
        assert self.mask is not None, '順伝播なしに逆伝播が呼ばれた'
        return dout * self.mask

(2)Adamの実装

最適化に使うAdamは、本の「6.1.6 Adam」に簡単な解説があるのですが、簡単すぎてこれだけでは実装できません。また、本のソースを見てもアルゴリズムが良く分かりませんでした。そこでまず、 @omiita さんの 【2020決定版】スーパーわかりやすい最適化アルゴリズム -損失関数からAdamとニュートン法- で大まかな仕組みを理解しました。そして、本で紹介されている 原著論文のPDF (本の参考文献[8]のサイト Adam: A Method for Stochastic Optimization の右上からダウンロードできます)のP.2の「Algorithm 1」の説明を見ながら実装しました。英語ですが擬似コードによる20行程度の説明なので、英語が苦手な私でもなんとかなりました。パラメータの初期値も、この論文の推奨値通りにしてみました。

adam.py
# coding: utf-8
import numpy as np


class Adam:

    def __init__(self, alpha=0.001, beta1=0.9, beta2=0.999):
        """Adamによるパラメーターの最適化
        
        Args:
            alpha (float, optional): 学習係数、デフォルトは0.001。
            beta1 (float, optional): Momentumにおける速度の過去と今の按分の係数、デフォルトは0.9。
            beta2 (float, optional): AdaGradにおける学習係数の過去と今の按分の係数、デフォルトは0.999。
        """
        self.alpha = alpha
        self.beta1 = beta1
        self.beta2 = beta2

        self.m = None   # Momentumにおける速度
        self.v = None   # AdaGradにおける学習係数
        self.t = 0      # タイムステップ

    def update(self, params, grads):
        """パラメーター更新
        
        Args:
            params (dict): 更新対象のパラメーターの辞書、keyは'W1''b1'など。
            grads (dict): paramsに対応する勾配の辞書
        """
        # mとvの初期化
        if self.m is None:
            self.m = {}
            self.v = {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)

        # 更新
        self.t += 1     # タイムステップ加算
        for key in params.keys():

            # mの更新、Momentumにおける速度の更新に相当
            # 過去と今の勾配を beta1 : 1 - beta1 で按分する
            self.m[key] = \
                self.beta1 * self.m[key] + (1 - self.beta1) * grads[key]

            # vの更新、AdaGradにおける学習係数の更新に相当
            # 過去と今の勾配を beta2 : 1 - beta2 で按分する
            self.v[key] = \
                self.beta2 * self.v[key] + (1 - self.beta2) * (grads[key] ** 2)

            # パラメーター更新のためのmとvの補正値算出
            hat_m = self.m[key] / (1.0 - self.beta1 ** self.t)
            hat_v = self.v[key] / (1.0 - self.beta2 ** self.t)

            # パラメーター更新、最後の1e-7は0除算回避
            params[key] -= self.alpha * hat_m / (np.sqrt(hat_v) + 1e-7)

(3)畳み込み層とプーリング層の出力サイズの計算

今回は層が多く、畳み込み層とプーリング層の出力サイズの計算が何度も出てきます。そのため、それぞれconv_output_sizepool_output_sizeという関数としてfunctions.pyに追加しました。他の関数は前章までのままです。

functions.py
# coding: utf-8
import numpy as np


def softmax(x):
    """ソフトマックス関数
    
    Args:
        x (numpy.ndarray): 入力
    
    Returns:
        numpy.ndarray: 出力
    """
    # バッチ処理の場合xは(バッチの数, 10)の2次元配列になる。
    # この場合、ブロードキャストを使ってうまく画像ごとに計算する必要がある。
    # ここでは1次元でも2次元でも共通化できるようnp.max()やnp.sum()はaxis=-1で算出し、
    # そのままブロードキャストできるようkeepdims=Trueで次元を維持する。
    c = np.max(x, axis=-1, keepdims=True)
    exp_a = np.exp(x - c)  # オーバーフロー対策
    sum_exp_a = np.sum(exp_a, axis=-1, keepdims=True)
    y = exp_a / sum_exp_a
    return y


def cross_entropy_error(y, t):
    """交差エントロピー誤差の算出
    
    Args:
        y (numpy.ndarray): ニューラルネットワークの出力
        t (numpy.ndarray): 正解のラベル
    
    Returns:
        float: 交差エントロピー誤差
    """

    # データ1つ場合は形状を整形(1データ1行にする)
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 誤差を算出してバッチ数で正規化
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size


def conv_output_size(input_size, filter_size, pad, stride):
    """畳み込み層の出力サイズ算出
    
    Args:
        input_size (int): 入力の1辺のサイズ(縦横は同値の前提)
        filter_size (int): フィルターの1辺のサイズ(縦横は同値の前提)
        pad (int): パディングのサイズ(縦横は同値の前提)
        stride (int): ストライド幅(縦横は同値の前提)
    
    Returns:
        int: 出力の1辺のサイズ
    """
    assert (input_size + 2 * pad - filter_size) \
        % stride == 0, '畳み込み層の出力サイズが割り切れない!'
    return int((input_size + 2 * pad - filter_size) / stride + 1)


def pool_output_size(input_size, pool_size, stride):
    """プーリング層の出力サイズ算出
    
    Args:
        input_size (int): 入力の1辺のサイズ(縦横は同値の前提)
        pool_size (int): プーリングのウインドウサイズ(縦横は同値の前提)
        stride (int): ストライド幅(縦横は同値の前提)
    
    Returns:
        int: 出力の1辺のサイズ
    """
    assert (input_size - pool_size) % stride == 0, 'プーリング層の出力サイズが割り切れない!'
    return int((input_size - pool_size) / stride + 1)

(4)ディープなCNNの実装

これで必要なパーツの実装が終わったので、いよいよネットワークの実装です。

まず、今回のネットワークにおける入出力の整理です。

レイヤー 入出力の形状 実装時の形状
(バッチサイズN,チャンネル数CH,
画像の高さH,幅W)
(100,1,28,28)
[1]Convolution#1
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,16,28,28)
[2]ReLU#1
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,16,28,28)
[3]Convolution#2
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,16,28,28)
[4]ReLU#2
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,16,28,28)
[5]Pooling#1
(バッチサイズN,フィルター数FN,

出力の高さOH,幅OW)
(100,16,14,14)
[6]Convolution#3
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,32,14,14)
[7]ReLU#3
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,32,14,14)
[8]Convolution#4
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,32,16,16)
[9]ReLU#4
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,32,16,16)
[10]Pooling#2
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,32,8,8)
[11]Convolution#5
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,64,8,8)
[12]ReLU#5
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,64,8,8)
[13]Convolution#6
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,64,8,8)
[14]ReLU#6
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,64,8,8)
[15]Pooling#3
(バッチサイズN,フィルター数FN,
出力の高さOH,幅OW)
(100,64,4,4)
[16]Affine#1
(バッチサイズN,隠れ層のサイズ) (100,50)
[17]ReLU#7
(バッチサイズN,隠れ層のサイズ) (100,50)
[18]Dropout#1
(バッチサイズN,隠れ層のサイズ) (100,50)
[19]Affine#2
(バッチサイズN,隠れ層のサイズ) (100,10)
[20]Dropout#2
(バッチサイズN,隠れ層のサイズ) (100,10)
[21]Softmax
(バッチサイズN,最終出力サイズ) (100,10)

壮大な表になりましたが、これを1層ずつ実装します。

本のコードはループを使ってシンプルにまとめられていますが、各層の入出力サイズの計算が混乱しそうなので、私は1層ずつパラメーターの初期化とレイヤーの生成を実装しました。かなり泥臭いコードになっています。なお、パラメーターは「Heの初期値」で初期化しています。

deep_conv_net.py
# coding: utf-8
import numpy as np
from affine import Affine
from convolution import Convolution
from dropout import Dropout
from functions import conv_output_size, pool_output_size
from pooling import Pooling
from relu import ReLU
from softmax_with_loss import SoftmaxWithLoss


class DeepConvNet:

    def __init__(
        self, input_dim=(1, 28, 28),
        conv_param_1={
            'filter_num': 16, 'filter_size': 3, 'pad': 1, 'stride': 1
        },
        conv_param_2={
            'filter_num': 16, 'filter_size': 3, 'pad': 1, 'stride': 1
        },
        conv_param_3={
            'filter_num': 32, 'filter_size': 3, 'pad': 1, 'stride': 1
        },
        conv_param_4={
            'filter_num': 32, 'filter_size': 3, 'pad': 2, 'stride': 1
        },
        conv_param_5={
            'filter_num': 64, 'filter_size': 3, 'pad': 1, 'stride': 1
        },
        conv_param_6={
            'filter_num': 64, 'filter_size': 3, 'pad': 1, 'stride': 1
        },
        hidden_size=50, output_size=10
    ):
        """ディープな畳み込みニューラルネットワーク
        
        Args:
            input_dim (tuple, optional): 入力データの形状、デフォルトは(1, 28, 28)。
            conv_param_1 (dict, optional): 畳み込み層1のハイパーパラメーター、
                デフォルトは{'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1}。
            conv_param_2 (dict, optional): 畳み込み層2のハイパーパラメーター、
                デフォルトは{'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1}。
            conv_param_3 (dict, optional): 畳み込み層3のハイパーパラメーター、
                デフォルトは{'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1}。
            conv_param_4 (dict, optional): 畳み込み層4のハイパーパラメーター、
                デフォルトは{'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1}。
            conv_param_5 (dict, optional): 畳み込み層5のハイパーパラメーター、
                デフォルトは{'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1}。
            conv_param_6 (dict, optional): 畳み込み層6のハイパーパラメーター、
                デフォルトは{'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1}。
            hidden_size (int, optional): 隠れ層のニューロンの数、デフォルトは50。
            output_size (int, optional): 出力層のニューロンの数、デフォルトは10。
        """
        assert input_dim[1] == input_dim[2], '入力データは高さと幅が同じ前提!'

        # パラメーターの初期化とレイヤー生成
        self.params = {}    # パラメーター
        self.layers = {}    # レイヤー(Python 3.7からは辞書の格納順が保持されるので、OrderedDictは不要)
    
        # 入力サイズ
        channel_num = input_dim[0]                          # 入力のチャンネル数
        input_size = input_dim[1]                           # 入力サイズ

        # [1] 畳み込み層#1 : パラメーター初期化とレイヤー生成
        filter_num, filter_size, pad, stride = list(conv_param_1.values())
        pre_node_num = channel_num * (filter_size ** 2)     # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W1', 'b1'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(filter_num, channel_num, filter_size, filter_size)
        )
        self.params[key_b] = np.zeros(filter_num)

        self.layers['Conv1'] = Convolution(
            self.params[key_w], self.params[key_b], stride, pad
        )

        # 次の層の入力サイズ算出
        channel_num = filter_num
        input_size = conv_output_size(input_size, filter_size, pad, stride)

        # [2] ReLU層#1 : レイヤー生成
        self.layers['ReLU1'] = ReLU()
   
        # [3] 畳み込み層#2 : パラメーター初期化とレイヤー生成
        filter_num, filter_size, pad, stride = list(conv_param_2.values())
        pre_node_num = channel_num * (filter_size ** 2)     # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W2', 'b2'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(filter_num, channel_num, filter_size, filter_size)
        )
        self.params[key_b] = np.zeros(filter_num)

        self.layers['Conv2'] = Convolution(
            self.params[key_w], self.params[key_b], stride, pad
        )

        # 次の層の入力サイズ算出
        channel_num = filter_num
        input_size = conv_output_size(input_size, filter_size, pad, stride)

        # [4] ReLU層#2 : レイヤー生成
        self.layers['ReLU2'] = ReLU()
        
        # [5] プーリング層#1 : レイヤー生成
        self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
        
        # 次の層の入力サイズ算出
        input_size = pool_output_size(input_size, pool_size=2, stride=2)

        # [6] 畳み込み層#3 : パラメーター初期化とレイヤー生成
        filter_num, filter_size, pad, stride = list(conv_param_3.values())
        pre_node_num = channel_num * (filter_size ** 2)     # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W3', 'b3'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(filter_num, channel_num, filter_size, filter_size)
        )
        self.params[key_b] = np.zeros(filter_num)

        self.layers['Conv3'] = Convolution(
            self.params[key_w], self.params[key_b], stride, pad
        )

        # 次の層の入力サイズ算出
        channel_num = filter_num
        input_size = conv_output_size(input_size, filter_size, pad, stride)

        # [7] ReLU層#3 : レイヤー生成
        self.layers['ReLU3'] = ReLU()
   
        # [8] 畳み込み層#4 : パラメーター初期化とレイヤー生成
        filter_num, filter_size, pad, stride = list(conv_param_4.values())
        pre_node_num = channel_num * (filter_size ** 2)     # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W4', 'b4'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(filter_num, channel_num, filter_size, filter_size)
        )
        self.params[key_b] = np.zeros(filter_num)
        
        self.layers['Conv4'] = Convolution(
            self.params[key_w], self.params[key_b], stride, pad
        )

        # 次の層の入力サイズ算出
        channel_num = filter_num
        input_size = conv_output_size(input_size, filter_size, pad, stride)

        # [9] ReLU層#4 : レイヤー生成
        self.layers['ReLU4'] = ReLU()
        
        # [10] プーリング層#2 : レイヤー生成
        self.layers['Pool2'] = Pooling(pool_h=2, pool_w=2, stride=2)
        
        # 次の層の入力サイズ算出
        input_size = pool_output_size(input_size, pool_size=2, stride=2)

        # [11] 畳み込み層#5 : パラメーター初期化とレイヤー生成
        filter_num, filter_size, pad, stride = list(conv_param_5.values())
        pre_node_num = channel_num * (filter_size ** 2)     # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W5', 'b5'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(filter_num, channel_num, filter_size, filter_size)
        )
        self.params[key_b] = np.zeros(filter_num)

        self.layers['Conv5'] = Convolution(
            self.params[key_w], self.params[key_b], stride, pad
        )

        # 次の層の入力サイズ算出
        channel_num = filter_num
        input_size = conv_output_size(input_size, filter_size, pad, stride)

        # [12] ReLU層#5 : レイヤー生成
        self.layers['ReLU5'] = ReLU()
   
        # [13] 畳み込み層#6 : パラメーター初期化とレイヤー生成
        filter_num, filter_size, pad, stride = list(conv_param_6.values())
        pre_node_num = channel_num * (filter_size ** 2)     # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W6', 'b6'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(filter_num, channel_num, filter_size, filter_size)
        )
        self.params[key_b] = np.zeros(filter_num)
        
        self.layers['Conv6'] = Convolution(
            self.params[key_w], self.params[key_b], stride, pad
        )

        # 次の層の入力サイズ算出
        channel_num = filter_num
        input_size = conv_output_size(input_size, filter_size, pad, stride)

        # [14] ReLU層#6 : レイヤー生成
        self.layers['ReLU6'] = ReLU()
        
        # [15] プーリング層#3 : レイヤー生成
        self.layers['Pool3'] = Pooling(pool_h=2, pool_w=2, stride=2)
        
        # 次の層の入力サイズ算出
        input_size = pool_output_size(input_size, pool_size=2, stride=2)

        # [16] Affine層#1 : パラメーター初期化とレイヤー生成
        pre_node_num = channel_num * (input_size ** 2)      # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W7', 'b7'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(channel_num * (input_size ** 2), hidden_size)
        )
        self.params[key_b] = np.zeros(hidden_size)

        self.layers['Affine1'] = Affine(self.params[key_w], self.params[key_b])
 
        # 次の層の入力サイズ算出
        input_size = hidden_size

        # [17] ReLU層#7 : レイヤー生成
        self.layers['ReLU7'] = ReLU()

        # [18] Dropout層#1 : レイヤー生成
        self.layers['Drop1'] = Dropout(dropout_ratio=0.5)

        # [19] Affine層#2 : パラメーター初期化とレイヤー生成
        pre_node_num = input_size                           # 1ノードに対する前層の接続ノード数
        key_w, key_b = 'W8', 'b8'                           # 辞書格納時のkey
        self.params[key_w] = np.random.normal(
            scale=np.sqrt(2.0 / pre_node_num),              # Heの初期値の標準偏差
            size=(input_size, output_size)
        )
        self.params[key_b] = np.zeros(output_size)

        self.layers['Affine2'] = Affine(self.params[key_w], self.params[key_b])

        # [20] Dropout層#2 : レイヤー生成
        self.layers['Drop2'] = Dropout(dropout_ratio=0.5)

        # [21] Softmax層 : レイヤー生成
        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x, train_flg=False):
        """ニューラルネットワークによる推論
        
        Args:
            x (numpy.ndarray): ニューラルネットワークへの入力
            train_flg (Boolean): 学習中ならTrue(Dropout層でニューロンの消去を実施)
        
        Returns:
            numpy.ndarray: ニューラルネットワークの出力
        """
        # レイヤーを順伝播
        for layer in self.layers.values():
            if isinstance(layer, Dropout):
                x = layer.forward(x, train_flg)  # Dropout層の場合は、学習中かどうかを伝える
            else:
                x = layer.forward(x)
        return x

    def loss(self, x, t):
        """損失関数の値算出
        
        Args:
            x (numpy.ndarray): ニューラルネットワークへの入力
            t (numpy.ndarray): 正解のラベル

        Returns:
            float: 損失関数の値
        """
        # 推論
        y = self.predict(x, True)   # 損失は学習中しか算出しないので常にTrue

        # Softmax-with-Lossレイヤーの順伝播で算出
        loss = self.lastLayer.forward(y, t)

        return loss

    def accuracy(self, x, t, batch_size=100):
        """認識精度算出
        batch_sizeは算出時のバッチサイズ。一度に大量データを算出しようとすると
        im2colでメモリを食い過ぎてスラッシングが起きてしまい動かなくなるため、
        その回避のためのもの。

        Args:
            x (numpy.ndarray): ニューラルネットワークへの入力
            t (numpy.ndarray): 正解のラベル(one-hot)
            batch_size (int), optional): 算出時のバッチサイズ、デフォルトは100。
        
        Returns:
            float: 認識精度
        """
        # 分割数算出
        batch_num = max(int(x.shape[0] / batch_size), 1)

        # 分割
        x_list = np.array_split(x, batch_num, 0)
        t_list = np.array_split(t, batch_num, 0)

        # 分割した単位で処理
        correct_num = 0  # 正答数の合計
        for (sub_x, sub_t) in zip(x_list, t_list):
            assert sub_x.shape[0] == sub_t.shape[0], '分割境界がずれた?'
            y = self.predict(sub_x, False)  # 認識精度は学習中は算出しないので常にFalse
            y = np.argmax(y, axis=1)
            t = np.argmax(sub_t, axis=1)
            correct_num += np.sum(y == t)
        
        # 認識精度の算出
        return correct_num / x.shape[0]

    def gradient(self, x, t):
        """重みパラメーターに対する勾配を誤差逆伝播法で算出
        
         Args:
            x (numpy.ndarray): ニューラルネットワークへの入力
            t (numpy.ndarray): 正解のラベル
        
        Returns:
            dictionary: 勾配を格納した辞書
        """
        # 順伝播
        self.loss(x, t)     # 損失値算出のために順伝播する

        # 逆伝播
        dout = self.lastLayer.backward()
        for layer in reversed(list(self.layers.values())):
            dout = layer.backward(dout)

        # 各レイヤーの微分値を取り出し
        grads = {}
        layer = self.layers['Conv1']
        grads['W1'], grads['b1'] = layer.dW, layer.db
        layer = self.layers['Conv2']
        grads['W2'], grads['b2'] = layer.dW, layer.db
        layer = self.layers['Conv3']
        grads['W3'], grads['b3'] = layer.dW, layer.db
        layer = self.layers['Conv4']
        grads['W4'], grads['b4'] = layer.dW, layer.db
        layer = self.layers['Conv5']
        grads['W5'], grads['b5'] = layer.dW, layer.db
        layer = self.layers['Conv6']
        grads['W6'], grads['b6'] = layer.dW, layer.db
        layer = self.layers['Affine1']
        grads['W7'], grads['b7'] = layer.dW, layer.db
        layer = self.layers['Affine2']
        grads['W8'], grads['b8'] = layer.dW, layer.db

        return grads

(5)学習の実装

学習は前の章のコードとほとんど変わりません。本のコードに合わせて Trainer クラスを実装しようかと思っていたのですが、もう最後の章になってしまって今回の実装で終わりなので従来のままにしています。

更新回数は12,000(20エポック)にしてみました。

mnist.py
# coding: utf-8
import os
import sys
import matplotlib.pylab as plt
import numpy as np
from adam import Adam
from deep_conv_net import DeepConvNet
sys.path.append(os.pardir)  # パスに親ディレクトリ追加
from dataset.mnist import load_mnist


# MNISTの訓練データとテストデータ読み込み
(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, flatten=False, one_hot_label=True)

# ハイパーパラメーター設定
iters_num = 12000           # 更新回数
batch_size = 100            # バッチサイズ
adam_param_alpha = 0.001    # Adamのパラメーター
adam_param_beta1 = 0.9      # Adamのパラメーター
adam_param_beta2 = 0.999    # Adamのパラメーター

train_size = x_train.shape[0]  # 訓練データのサイズ
iter_per_epoch = max(int(train_size / batch_size), 1)    # 1エポック当たりの繰り返し数

# ディープな畳み込みニューラルネットワーク生成
network = DeepConvNet()

# オプティマイザー生成、Adamを使用
optimizer = Adam(adam_param_alpha, adam_param_beta1, adam_param_beta2)

# 学習前の認識精度の確認
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_loss_list = []            # 損失関数の値の推移の格納先
train_acc_list = [train_acc]    # 訓練データに対する認識精度の推移の格納先
test_acc_list = [test_acc]      # テストデータに対する認識精度の推移の格納先
print(f'学習前 [訓練データの認識精度]{train_acc:.4f} [テストデータの認識精度]{test_acc:.4f}')

# 学習開始
for i in range(iters_num):

    # ミニバッチ生成
    batch_mask = np.random.choice(train_size, batch_size, replace=False)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 勾配の計算
    grads = network.gradient(x_batch, t_batch)

    # 重みパラメーター更新
    optimizer.update(network.params, grads)
    
    # 損失関数の値算出
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    # 1エポックごとに認識精度算出
    if (i + 1) % 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(
            f'[エポック]{(i + 1) // iter_per_epoch:>2} '
            f'[更新数]{i + 1:>5} [損失関数の値]{loss:.4f} '
            f'[訓練データの認識精度]{train_acc:.4f} [テストデータの認識精度]{test_acc:.4f}'
        )

# 損失関数の値の推移を描画
x = np.arange(len(train_loss_list))
plt.plot(x, train_loss_list, label='loss')
plt.xlabel('iteration')
plt.ylabel('loss')
plt.xlim(left=0)
plt.ylim(0, 2.5)
plt.show()

# 訓練データとテストデータの認識精度の推移を描画
x2 = np.arange(len(train_acc_list))
plt.plot(x2, train_acc_list, label='train acc')
plt.plot(x2, test_acc_list, label='test acc', linestyle='--')
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.xlim(left=0)
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

(6)実行結果

以下、実行結果です。私の環境では半日くらいかかりました。

学習前 [訓練データの認識精度]0.0975 [テストデータの認識精度]0.0974
[エポック] 1 [更新数]  600 [損失関数の値]1.0798 [訓練データの認識精度]0.9798 [テストデータの認識精度]0.9811
[エポック] 2 [更新数] 1200 [損失関数の値]0.8792 [訓練データの認識精度]0.9881 [テストデータの認識精度]0.9872
[エポック] 3 [更新数] 1800 [損失関数の値]0.9032 [訓練データの認識精度]0.9884 [テストデータの認識精度]0.9890
[エポック] 4 [更新数] 2400 [損失関数の値]0.8012 [訓練データの認識精度]0.9914 [テストデータの認識精度]0.9906
[エポック] 5 [更新数] 3000 [損失関数の値]0.9475 [訓練データの認識精度]0.9932 [テストデータの認識精度]0.9907
[エポック] 6 [更新数] 3600 [損失関数の値]0.8105 [訓練データの認識精度]0.9939 [テストデータの認識精度]0.9910
[エポック] 7 [更新数] 4200 [損失関数の値]0.8369 [訓練データの認識精度]0.9920 [テストデータの認識精度]0.9915
[エポック] 8 [更新数] 4800 [損失関数の値]0.8727 [訓練データの認識精度]0.9954 [テストデータの認識精度]0.9939
[エポック] 9 [更新数] 5400 [損失関数の値]0.9640 [訓練データの認識精度]0.9958 [テストデータの認識精度]0.9935
[エポック]10 [更新数] 6000 [損失関数の値]0.8375 [訓練データの認識精度]0.9953 [テストデータの認識精度]0.9925
[エポック]11 [更新数] 6600 [損失関数の値]0.8500 [訓練データの認識精度]0.9955 [テストデータの認識精度]0.9915
[エポック]12 [更新数] 7200 [損失関数の値]0.7959 [訓練データの認識精度]0.9966 [テストデータの認識精度]0.9932
[エポック]13 [更新数] 7800 [損失関数の値]0.7778 [訓練データの認識精度]0.9946 [テストデータの認識精度]0.9919
[エポック]14 [更新数] 8400 [損失関数の値]0.9212 [訓練データの認識精度]0.9973 [テストデータの認識精度]0.9929
[エポック]15 [更新数] 9000 [損失関数の値]0.9046 [訓練データの認識精度]0.9974 [テストデータの認識精度]0.9934
[エポック]16 [更新数] 9600 [損失関数の値]0.9806 [訓練データの認識精度]0.9970 [テストデータの認識精度]0.9924
[エポック]17 [更新数]10200 [損失関数の値]0.7837 [訓練データの認識精度]0.9975 [テストデータの認識精度]0.9931
[エポック]18 [更新数]10800 [損失関数の値]0.8948 [訓練データの認識精度]0.9976 [テストデータの認識精度]0.9928
[エポック]19 [更新数]11400 [損失関数の値]0.7936 [訓練データの認識精度]0.9980 [テストデータの認識精度]0.9932
[エポック]20 [更新数]12000 [損失関数の値]0.8072 [訓練データの認識精度]0.9984 [テストデータの認識精度]0.9939

スクリーンショット 2020-02-19 23.34.13.png
スクリーンショット 2020-02-19 23.34.34.png

最終的な認識精度は99.39%でした。前の章のCNNは98.60%だったので、0.79ポイント アップです。層を深くすることの可能性を感じさせる結果になりました。

なお、前の章の結果と比べて認識精度に対する損失関数の値が大きいのですが、これはDropoutの影響かと思います。認識精度はすべてのニューロンを使っていますが、損失関数の算出時は半分(Dropoutのレートを0.5で実行したので)のニューロンが削除状態のためです。

この本で実装するのはここまでですが、さらに認識精度を高めるために、アンサンブル学習やData Augmentationなどの手法が紹介されています。また、層を深くすることのメリットについてもまとめられています。

8.2 ディープラーニングの小歴史

ディープラーニングのトレンドの紹介です。いずれも、ここまでに学んだCNNが基本であることが理解できました。

8.3 ディープラーニングの高速化

高速化についての説明です。興味深かったのは、ディープラーニングだと単精度浮動小数点では精度が高すぎてもったいないので、半精度の浮動小数点が注目されているという点です。これまで私が使ってきた開発言語では半精度浮動小数点型というのを聞いたことがなかったのですが、NumPyにはfloat16という型があることを知りました。

8.4 ディープラーニングの実用例

物体検出、セグメンテーション、画像のキャプション生成と、面白そうなことがすでに実現されていることが分かりました。ただ、その仕組みについては、これまでに学んだレベルではまだまだ理解しきれません。

8.5 ディープラーニングの未来

画像の生成や自動運転、強化学習など、研究中の分野の紹介です。ディープラーニングの可能性を感じますね。

8.6 まとめ

なんとか最後の実装も終えました。本の通りの精度が出せて一安心です。ディープラーニングの可能性についても学ぶことができました。

この章は以上です。誤りなどありましたら、ご指摘いただけますとうれしいです。

(このメモの他の章へ:1章 / 2章 / 3章 / 4章 / 5章 / 6章 / 7章 / 8章 / まとめ

14
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11