からっぽのしょこ

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

6.b:拡張版多層ニューラルネットワークの実装【ゼロつく1のノート(実装)】

はじめに

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

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、6章「学習に関するテクニック」全体に関わる内容になります。Batch NormレイヤとDropoutレイヤを組み込んだ多層ニューラルネットワーククラスをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

6.b 拡張版多層ニューラルネットワーククラスの実装

 前項のニューラルネットワークのクラスに、Batch NormalizationとDropoutの機能を追加して実装します。前項と併せて読んでください。

・インスタンスの作成時の引数:__init__

 次の3つの引数を追加します。


・レイヤの作成:__init__

 「Affineレイヤ」→「Batch Normレイヤ」→「活性化レイヤ」→「Dropoutレイヤ」→「Affineレイヤ」→・・・→「最終層のAffineレイヤ」→「ソフトマックス関数と損失関数レイヤ」の順番に構成します。

 Batch NormレイヤとDropoutレイヤは、それぞれ引数にTrueが指定されたときのみ作成します。

・推論(順伝播)メソッド:predict

 Batch NormalizationとDropoutは、訓練時のみ行う処理です。そこで訓練なのかどうかを引数train_flgTrueFalseかで管理します。そのため順伝播メソッド.forward()train_flg引数を持つBatch Normレイヤまたは(or)Dropoutレイヤと、持たないAffineレイヤ・活性化レイヤとで条件分岐します。

・誤差逆伝播法による勾配計算メソッド:gradient

 Batch Normalizationのハイパーパラメータに関する微分の計算処理を追加します。最終層(第hidden_layer_num + 1層)にはBatch Normレイヤがないので、use_batchnorm引数がTrueかつ(and)i < hidden_layer_num + 1のときのみ処理します。Batch Normレイヤの逆伝播については、「Batch Normレイヤの逆伝播【ゼロつく1のノート(数学)】 - からっぽのしょこ」を確認してください。

・多層ニューラルネットワーククラスの実装

 これが最後です!

# 多層ニューラルネットワークの実装
class MultiLayerNetExtend:
    
    # インスタンス変数の定義
    def __init__(self, input_size, hidden_size_list, output_size,
                 activation='relu', weight_init_std='relu', weight_decay_lambda=0, 
                 use_dropout=False, dropout_ratio=0.5, use_batchnorm=False):
        self.input_size = input_size # 入力層のニューロン数
        self.output_size = output_size # 出力層のニューロン数
        self.hidden_size_list = hidden_size_list # 中間層のニューロン数のリスト
        self.hidden_layer_num = len(hidden_size_list) # 中間層の数
        self.use_dropout = use_dropout # Dropoutの可否
        self.weight_decay_lambda = weight_decay_lambda # Weight decayの係数
        self.use_batchnorm = use_batchnorm # Batch Normalizationの可否
        
        # パラメータの初期値を設定
        self.params = {} # 初期化
        self.__init_weight(weight_init_std) # 初期化メソッド
        
        ## ニューラルネットワークを生成
        
        # レイヤ作成用にディクショナリ変数に活性化関数を格納
        activation_layer = {'sigmoid': Sigmoid, 'relu': Relu}
        
        # 1層ずつインスタンスをディクショナリ変数に格納
        self.layers = OrderedDict() # 順番付き辞書を作成(初期化)
        for idx in range(1, self.hidden_layer_num + 1): # (`+1`は未満に対応するため)
            # Affineレイヤ
            self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)], self.params['b' + str(idx)])
            
            # Batch Normレイヤ
            if self.use_batchnorm:
                self.params['gamma' + str(idx)] = np.ones(hidden_size_list[idx-1]) # 調整用の標準偏差
                self.params['beta' + str(idx)] = np.zeros(hidden_size_list[idx-1]) # 調整用の平均
                self.layers['BatchNorm' + str(idx)] = BatchNormalization(self.params['gamma' + str(idx)], self.params['beta' + str(idx)])
            
            # 活性化レイヤ
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()
            
            # Dropoutレイヤ
            if self.use_dropout:
                self.layers['Dropout' + str(idx)] = Dropout(dropout_ratio)

        # 最終層のAffineレイヤ
        idx = self.hidden_layer_num + 1
        self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)], self.params['b' + str(idx)])
        
        # 最終層の活性化レイヤ:(不要な場合があるので分けておく)
        self.last_layer = SoftmaxWithLoss()
    
    # 重みの初期化メソッドの定義
    def __init_weight(self, weight_init_std):
        # 全層のニューロン数のリストを作成
        all_size_list = [self.input_size] + self.hidden_size_list + [self.output_size]
        
        # 各層のパラメータを設定
        for idx in range(1, len(all_size_list)):
             # 重みの初期値の標準偏差の設定:引数に数値を指定した場合はこのまま
            scale = weight_init_std
            
            # 特定の初期値を指定した場合の処理:(6.2節)
            if str(weight_init_std).lower() in ('relu', 'he'): # 'ReLU'か'He'を指定した場合
                scale = np.sqrt(2.0 / all_size_list[idx-1]) # Heの初期値を使用
            elif str(weight_init_std).lower() in ('sigmoid', 'xavier'): # `Sigmoid`か`Xavier`を指定した場合
                scale = np.sqrt(1.0 / all_size_list[idx-1]) # Xavierの初期値を使用
            
            # 値を生成
            self.params['W' + str(idx)] = scale * np.random.randn(all_size_list[idx-1], all_size_list[idx])
            self.params['b' + str(idx)] = np.zeros(all_size_list[idx])

    # 推論(順伝播)メソッドの定義
    def predict(self, x, train_flg=False):
        # 1層ずつ順伝播メソッドによる処理
        for key, layer in self.layers.items():
            if "Dropout" in key or "BatchNorm" in key: # DropoutレイヤかBach Normレイヤのとき
                x = layer.forward(x, train_flg)
            else: # Affineレイヤか活性化レイヤのとき
                x = layer.forward(x)

        return x

    # 損失関数メソッドの定義
    def loss(self, x, t, train_flg=False):
        # ニューラルネットワークの処理(最終層の活性化抜き)
        y = self.predict(x, train_flg)

        # 荷重減衰:(6.4節)
        weight_decay = 0 # 値を初期化
        for idx in range(1, self.hidden_layer_num + 2):
            # 重みを複製
            W = self.params['W' + str(idx)]
            
            # Weight decay(荷重減衰)を計算
            weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)

        # 交差エントロピー誤差にWeight decayを加算
        return self.last_layer.forward(y, t) + weight_decay

    # 認識精度の測定メソッドの定義
    def accuracy(self, x, t):
        # ニューラルネットワークの処理(最終層の活性化抜き)
        y = self.predict(x, train_flg=False)
        
        # 推論結果を抽出(最大値のインデックスを取得)
        if y.ndim == 2: # 2次元配列(バッチデータ)のとき
            y = np.argmax(y, axis=1)
            t = np.argmax(t, axis=1)
        elif y.ndim == 1: # 1次元配列(データが1つ)のとき
            y = np.argmax(y)
            t = np.argmax(t)
        
        # 正解率を計算
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # 誤差逆伝播法による勾配計算メソッドの定義
    def gradient(self, x, t):
        # 順伝播(交差エントロピー誤差)を計算
        self.loss(x, t, train_flg=True)

        # 逆伝播(勾配)を計算
        dout = 1 # 逆伝播の入力
        dout = self.last_layer.backward(dout) # 最終層
        layers = list(self.layers.values()) # その他の層を後ろから処理
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 計算結果をディクショナリ変数に格納
        grads = {}
        for idx in range(1, self.hidden_layer_num + 2): # (`+2`は未満に対応するためと最終レイヤの分)
            grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.params['W' + str(idx)]
            grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db

            if self.use_batchnorm and idx != self.hidden_layer_num + 1: # 最終層以外のとき
                grads['gamma' + str(idx)] = self.layers['BatchNorm' + str(idx)].dgamma
                grads['beta' + str(idx)] = self.layers['BatchNorm' + str(idx)].dbeta

        return grads

 これまで組んだプログラミングが問題なく回れば、これで6章の内容の全て実装完了です!お疲れ様でした。

 以上でニューラルネットワークの内容は終了です。次は畳み込みニューラルネットワークを扱います。

参考文献

おわりに

 5章は数式の行間埋めで大変でしたが、6章は解説の穴埋めで大変でした。もう少し本文に説明を載せてほしかった。あでも載ってない手法も実装されてるのはありがたかったです。
 それとは別に色々あってモチベが低水準です。勉強仲間ほしい。

【次節の内容】

www.anarchive-beta.com