からっぽのしょこ

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

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

はじめに

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

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

 この記事は、6章「学習に関するテクニック」全体に関わる内容になります。多層ニューラルネットワーククラスをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

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

 「5.7.2-3:誤差逆伝播法に対応したニューラルネットワークの実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」では、2層のニューラルネットワークを実装しました。この項では、層の数を指定してニューラルネットワークを構成できるように実装します。また6章で扱った手法も含めて実装します。

## この実装で必要なライブラリ

# これまでに実装した各種レイヤのクラス
import sys
sys.path.append('C:\\Users\\「ユーザー名」\\Documents\\・・・\\deep-learning-from-scratch-master')
from common.layers import *

# 順番付き辞書作成関数
from collections import OrderedDict

# NumPy
import numpy as np

 これまで実装したクラスを再実行できる環境であれば、common.layersを読み込む必要はありません。

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

  • input_sizeは、入力する1データのサイズです。MNISTデータセットの場合はピクセル数の784とします。
  • hidden_size_listは、隠れ層のニューロンの数です。任意の数をリスト型変数として指定します。指定した要素数のレイヤが作成されます。
  • output_sizeは、出力される1データのサイズです。MNISTデータセットの場合は数字の数の10とします。
  • activationは、最終層以外で使用する活性化関数です。ReLU関数であれば'relu'、シグモイド関数であれば'sigmoid'を指定します。
  • weight_init_stdは、重みの初期値の標準偏差です。直接標準偏差の値を指定するか、'relu'または'he'を指定した場合は「Heの初期値」、'sigmoid'または'xavier'を指定した場合は「Xavierの初期値」に従いランダムに値を生成します。詳細は「6.2:重みの初期値【ゼロつく1のノート(実装)】 - からっぽのしょこ」を確認してください。
  • weight_decay_lambdaは、Weight Decay(荷重減衰)の係数(強さ)です。詳細は「6.4.1-2:Weight decay【ゼロつく1のノート(実装)】 - からっぽのしょこ」を確認してください。


・レイヤの作成:__init__

 これまでは、2層や3層のニューラルネットワークの構造を全て書いて実装していました。この例では、それを自動化します。各層のAffineレイヤと活性化(Activation)レイヤの(5章で実装したクラスの)インスタンスを、ディクショナリ型のインスタンス変数layersに格納します。

 処理のイメージは次の通りです。

# 中間層のニューロン数を指定:(引数のイメージ)
hidden_size_list = [100, 100, 100]

# 中間層の数
layer_num = len(hidden_size_list)

# 順番付きディクショナリ変数を初期化
layers = OrderedDict()

# ディクショナリ変数に格納
for i in range(1, layer_num + 1):
    layers['レイヤ名' + str(i)] = i

# ディクショナリ変数のキーと値を表示
print(layers.items())
odict_items([('レイヤ名1', 1), ('レイヤ名2', 2), ('レイヤ名3', 3)])

 range()は、第1引数以上第2引数未満の整数列を生成します。そのためレイヤ数+1とします。

 ディクショナリ変数は、.items()でキーと値を同時に返します。

 ただし最終層については別の処理をします。

 上の処理の後に、最終層のAffineレイヤのみlayersに追加します。ソフトマックス関数による正規化が必要ない場合は、layersだけで完結させるためです。

 最終層の活性化レイヤについては損失関数レイヤと共にインスタンス変数last_layerに、SoftmaxWithLoss(5.6.3項)のインスタンスを格納します。

・パラメータの初期化メソッド:__init_weight

 これまでは、__init__()メソッド内でパラメータの初期値を定義していました。しかしこの例では定義の内容が複雑なため、パラメータ初期化メソッド__init_weight()をクラス内に定義しておき、それを__init__()メソッド内で実行することでディクショナリ型のインスタンス変数paramsを作成します。

 これまでは初期値の標準偏差を指定して、重みの値をランダムに生成しました。この例ではそれに加えて「Xavierの初期値」と「Heの初期値」を指定できるように実装します。主な仕組みと実装イメージについては、6.2節で確認しました。ここでは更に実用的に実装しています。主な変更点は次の2点です。

 'xavier''Xavier'としてもマッチするように、.lower()で小文字に変換します。

# 利用する活性化関数を指定:(引数のイメージ)
weight_init_std = "XaVieR"

# 小文字に変換
print(weight_init_std.lower())
print("xavier" == weight_init_std)
print("xavier" == weight_init_std.lower())
xavier
False
True


 次に、inを使って'Sigmoid''ReLU'を指定しても処理できるようにします。

# 利用する活性化関数を指定:(引数のイメージ)
weight_init_std = "RELU"

# 検索
print(str(weight_init_std).lower() in ('relu', 'he'))
print(str(weight_init_std).lower() in ('sigmoid', 'xavier'))
True
False

 weight_init_std引数には数値を指定することもあります。数値に対して.lower()を実行するとエラーになるので、その前にstr()で文字列型に変換する処理をしておきます。

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

 5.7.2項と同じです。最終層のAffineレイヤの出力を計算します。

 layers.values()で各レイヤのインスタンスを取り出して、forで順番に処理します。全てのクラス(レイヤ)の順伝播メソッド名をforwardとしているので、このように実装できます。

・損失関数メソッド:loss

 5.7.2項の実装に加えて、Weight decay(荷重減衰)を行います。処理の流れは、6.4.2項で確認しました。

 推論メソッド.predict()の出力yを、最終層の活性化レイヤと損失関数レイヤの順伝播メソッドlast_layer.forward()の入力とし、交差エントロピー誤差を計算します。この値と、次のWeight decayの計算結果との和が損失関数の値となります。

 (本では$\frac{1}{2} \lambda \mathbf{W}^2$と書かれていますが、)実装上は次の計算を行っています。

$$ \frac{1}{2} \lambda \sum_{l=1}^L \sum_{j=1}^{M^{(l-1)}} \sum_{k=1}^{M^{(l)}} w_{jk}^{(l)2} $$

 括弧付きの指数$(l)$は層番号を表します。$M^{(l-1)}$は1つ前の層のニューロン数で重み$\mathbf{W}^{(l)}$の行数、$M^{(l)}$は現在の層のニューロン数で$\mathbf{W}^{(l)}$の列数に対応します。要は全ての層の重みの全ての要素の和をとり、$\frac{1}{2} \lambda$で割り引きます。

・認識精度メソッド:accuracy

 5.7.2項と同じです。推論メソッドpredict()の出力と教師データを比較し、正解率を計算します。

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

 基本的な流れは5.7.2項と同じです。逆伝播の入力を1として、最終層のソフトマックス関数と損失関数レイヤの逆伝播を計算してから、各レイヤを後から順番にforでループ処理します。

 ただし、Weight decayの微分の項$\lambda \mathbf{W}$が伝播してくる点が異なります。

 Weight decayを含めた逆伝播についてもう少し詳しく式で確認すると、順伝播の式を$F$と置いて

$$ F = \frac{1}{2} \lambda \sum_{l=1}^L \sum_{j=1}^{M^{(l-1)}} \sum_{k=1}^{M^{(l)}} w_{jk}^{(l)2} $$

各層の重みの各要素で微分すると

$$ \frac{\partial F}{\partial w_{jk}^{(l)}} = \lambda w_{jk}^{(l)} $$

となります。つまり通常の逆伝播の計算結果に、その層の重みを$\lambda$で割り引いた値を加えればいいことが分かります。

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

 では実装します。(ところで、クラスの実装は書いたら即試して修正ができず大変なのでコピペで済ませたくなりますが、全文コピペするのであればマスターデータからimportすればいい訳です。そこで間をとって、5章のときに自分で実装したTwoLayerNetをコピペして書き換えていくのが良いと思います。変更点を意識しながら実装できる分、全て手打ちするよりもいいかもしれません。)

# 多層ニューラルネットワークの実装
class MultiLayerNet:
    
    # インスタンス変数の定義
    def __init__(self, input_size, hidden_size_list, output_size,
                 activation='relu', weight_init_std='relu', weight_decay_lambda=0):
        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.weight_decay_lambda = weight_decay_lambda # Weight decayの係数
        
        # パラメータの初期値を設定
        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)])
            
            # 活性化レイヤ
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()
        
        # 最終層の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):
        # 層ごとに順伝播メソッドによる処理
        for layer in self.layers.values():
            x = layer.forward(x)

        return x
    
    # 損失関数メソッドの定義
    def loss(self, x, t):
        # ニューラルネットワークの処理(最終層の活性化抜き)
        y = self.predict(x)
        
        # 荷重減衰:(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)
        
        # 推論結果を抽出(最大値のインデックスを取得)
        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)

        # 逆伝播(勾配)を計算
        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.layers['Affine' + str(idx)].W
            grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db

        return grads

 数値微分による勾配計算メソッドは省略しました。idxはインデックスのことで、iとするとイタレーション時のiと干渉するのを防ぐためです。

 これまで組んできた6章のプログラミングがそのまま動けば問題なく実装できたということです!

 多層ニューラルネットワークの本体を実装できました!次はここにBatch NormalizationとDropoutを加えます。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

 これまで使ってきた多層ニューラルネットワークのクラスをようやく実装できました。次はここに2つ機能を追加します。

【次節の内容】

www.anarchive-beta.com