「ゼロから作るDeep Learning」自習メモ（その10）MultiLayerNet クラス

「ゼロから作るDeep Learning」(斎藤 康毅　著　オライリー・ジャパン刊)を読んでいる時に、参照したサイト等をメモしていきます。　[その９](https://qiita.com/slow_learner/items/16b4cad06782abeb98d5) ←　→ [その１１](https://qiita.com/slow_learner/items/d4c36e3701e16c5a9e77)

５章でレイヤでの実装を説明した後、６章以降ではプログラム自体の説明をあまりやらなくなる。
プログラム例は、最初にダウンロードしたファイルにあるから、自分で実行して、内容を確認しなさい、ということなのだろうが、初心者にはけっこう大変。

まあ、ぼちぼちいきます。

###６章の MultiLayerNet クラスの内容を確認してみる

３章でニューラルネットの基本的な説明があり、４章で２層ニューラルネットワークのクラスTwoLayerNet　が実装されました。その後、いろんな説明があって、MultiLayerNetクラスになったわけです。すごく複雑になったように見えますが、基本部分は TwoLayerNet と変わってません。このクラスが参照しているライブラリ layers.py の内容を見ると、TwoLayerNetクラスで使っているものと同じです。
複雑そうに見えるのは、
プログラムの汎用性を高めるためにレイヤ単位の実装にした
活性化関数、パラメータ更新手法、重みの初期値等を選択できるようにした
からのようです。

プログラムを理解したいときは、１行ずつ手動トレースしていくのが確実。

というわけで、P192のプログラムをトレースしてみます。

###ニューラルネットオブジェクトnetwork を生成する

```python
weight_decay_lambda = 0.1

network = MultiLayerNet(input_size=784, 
                        hidden_size_list=[100, 100, 100, 100, 100, 100],
                        output_size=10,
                        weight_decay_lambda=weight_decay_lambda)
```

input_size=784 というのは、要素数784個のMNISTデータを使うということ
output_size=10　というのは、認識した結果は10通りになるということ
で
hidden_size_list=[100, 100, 100, 100, 100, 100]
によって、networkオブジェクトの中が、どのようになるかというと

multi_layer_net.py の MultiLayerNet の定義にある、初期化で

```python
    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
        self.params = {}

        # 重みの初期化
        self.__init_weight(weight_init_std)
```

オブジェクト生成のところでは省略していたけれど
activation='relu'　活性化関数は relu を使う
weight_init_std='relu' 重みの初期値は relu と相性がいい Heの初期値 を使う
self.hidden_layer_num = len(hidden_size_list)　リストhidden_size_listの要素の数だけ隠れ層のレイヤを作る、
ということになっています。

###レイヤを生成する
ということで、要素の数だけ forループします

```python
        # レイヤの生成
        activation_layer = {'sigmoid': Sigmoid, 'relu': Relu}
        self.layers = OrderedDict()
        for idx in range(1, self.hidden_layer_num+1):
            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]()
```
これの最後に 出力層last_layer として
SoftmaxWithLoss
が付け加わります。

```python
        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()
```
つまり、隠れ層が６つ＋出力層１つで、７層のネットワークになります。
リスト layers の内容は、次のようになります。
>
OrderedDict([
('Affine1', Affine(params[W1],params[b1])),
('Activation_function1', Relu),
('Affine2', Affine(params[W2],params[b2])),
('Activation_function2', Relu),
('Affine3', Affine(params[W3],params[b3])),
('Activation_function3', Relu),
('Affine4', Affine(params[W4],params[b4])),
('Activation_function4', Relu),
('Affine5', Affine(params[W5],params[b5])),
('Activation_function5', Relu),
('Affine6', Affine(params[W6],params[b6])),
('Activation_function6', Relu),
('Affine7', Affine(params[W7],params[b7]))
])

レイヤ単位で実装したことで、hidden_size_listの要素数で、隠れ層の数を指定できるようになっているのがわかります。６層くらいなら、TwoLayerNetクラスのようにプログラム内で層を増やしていくこともできますが、これが100とかになったら、まずムリ。

###学習させてみる

このネットワークオブジェクトにMNISTデータを与えて学習させます。

```python
optimizer = SGD(lr=0.01)

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

    grads = network.gradient(x_batch, t_batch)
    optimizer.update(network.params, grads)
```
ミニバッチのループの中の
grads = network.gradient(x_batch, t_batch)
で、勾配を求めています
grads の中身は、こんな感じ
>
{
 'W1': array([[-0.00240062, -0.01276378,  0.00096349, ..., 0.0054993 ],
        [-0.00232299, -0.0022137 ,  0.0036697 , ..., -0.00693252],
        ...,
        [-0.00214929,  0.00358515, -0.00982791, ...,  0.00342722]]),
 'b1': array([-4.51501921e-03,  5.25825778e-03, ..., -8.60827293e-03]),
 'W2': array([[ 0.00394647, -0.01781943,  0.00114132, ...,  0.0029042 ],
        [-0.00551014,  0.00238989,  0.01442266, ..., 0.00171659],
        ...,
       [ 0.00279524,  0.01496588,  0.01859664, ..., -0.02194152]]),
 'b2': array([ 2.08738753e-03, -8.28071395e-03, ...,  1.22945079e-02]),
 'W3': array([[         ..., ]]),
 'b3': array([ ..., ]),
 'W4': array([[         ..., ]]),
 'b4': array([ ..., ]),
 'W5': array([[         ..., ]]),
 'b5': array([ ..., ]),
 'W6': array([[         ..., ]]),
 'b6': array([ ..., ]),
 'W7': array([
[ 6.72420338e-02,3.36979669e-04,1.26773417e-02,-2.30916938e-03, -4.84414774e-02, 
         -2.58458587e-02,-5.26754173e-02,3.61136740e-02,-4.29689699e-03, -2.85799599e-02],
        [ ...],
        [-1.68008362e-02,  6.87882255e-03, -3.15578291e-03, -8.00362948e-04,  8.81555008e-03, 
         -9.23032804e-03,-1.83337109e-02,  2.17933554e-02, -6.52331525e-03, 1.50930257e-02]
]),
 'b7': array([ 0.11697053, -0.02521648,  0.03697393, -0.015763  , -0.0456317 ,
        -0.03476072, -0.05961871,  0.0096403 ,  0.03581566, -0.01840983])
}

最後のgrads['W7']の内容には、softmax関数で出力した数字0 ～ 9 のどれであるかの確率を10個の要素のリストにして、読み込ませた​訓練データの行数分並んでいます。
そして

```python
    optimizer.update(network.params, grads)
```
commonフォルダにあるライブラリ optimizer.py の関数SGDのupdateメソッドで、パラメータparamsの内容からgradsの内容を引いて更新します。上の例では、SGD手法で更新しています。ライブラリにはSGDのほかにMomentum、AdaGrad、Adam、RMSpropとかが定義されています。

更新した結果のparamsは、次のバッチ処理に引き継いで使うので、バッチがループする分、学習が進んでいきます。

###gradientメソッドは何をしているか

じゃあ、このgradientメソッドは何をしているかというと、重みパラメータの勾配を誤差逆伝播法で求めています。
まず、順方向で損失関数の値を計算し、それから、networkオブジェクトを創生したときに設定したレイヤを、逆方向に辿って勾配を求めます。

```python
    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        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):
            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
```

最初に、
self.loss(x, t)
とあるのが、よくわかりませんでした。
関数を実行していますが、その結果を次に利用してるように見えませんから。
なので、その中身をトレースしてみました。
実行しているのは、multi_layer_net.pyで定義している関数 loss です。

###損失関数lossをトレースしてみた

```python
network.loss(x_batch, t_batch)
```

>62.09479496490768

```python
    def loss(self, x, t):
        y = self.predict(x)
        weight_decay = 0
        for idx in range(1, self.hidden_layer_num + 2):
            W = self.params['W' + str(idx)]
            weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)

        return self.last_layer.forward(y, t) + weight_decay
```
loss関数の中では、predictで、入力データから結果 y を予測させています。この中で、Affine1からAffine7までのレイヤのforwardメソッドが実行されていきます。

```python
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x
```
過学習を防ぐために重み(params['W1']等）から weight_decay　を計算し、これを加えて
出力します。

```python
weight_decay
```
>59.84568388277881

```python
network.last_layer.forward(y, t_batch)
```
>2.2491110821288687

self.last_layer.forward(y, t)　は、MultiLayerNetクラスの初期化のところで、

```python
self.last_layer = SoftmaxWithLoss()
```
と定義してあるので、　実際に実行しているのはlayers.pyで定義している　SoftmaxWithLoss()のforwardメソッドです。

```python
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
```

で、このforwardメソッドの中で、交差エントロピー誤差（cross entropy error）を計算して返しています。

```python
network.last_layer.loss
```
>2.2491110821288687

```python
from common.functions import *
cross_entropy_error(network.last_layer.y, network.last_layer.t)
```
>2.2491110821288687

と言う事で、self.loss(x, t)　で何を参照して、何をやっているかはわかりました。

で、

SoftmaxWithLoss関数は、この後、勾配を求めるために誤差逆伝播法で backwardメソッドを使うことになるわけです。その中で self.y とか self.t とかを参照していますが、これらは forwardメソッドを実行したときにセットされる変数です。
つまり、**最初にself.loss(x, t)とあるのは、損失関数を求めているのではなく、誤差逆伝播法で backwardメソッドを使うための準備だった**、ということです。

後ろに戻るためには、先に前に進んでおかなければいけないと言う、まあ、分かって見れば、当たり前の話なんですが。

###backward で勾配を求める

self.loss(x, t)　を実行して、入力したデータからの予測値等をセットしたら、誤差逆伝播法で勾配を求めます。

```python
        # backward
        dout = 1
        dout = self.last_layer.backward(dout)
```
self.last_layer.backward(dout) はSoftmaxWithLoss.backward()のことです。
doutには、予測値y と、教師ラベルt の差のリストが返されます。
[y1 - t1, y2 - t2, y3 - t3, ・・・ , y100 - t100] 

```python
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)
```
layers.reverse()で、積み重ねたレイヤを逆順にして、dout = layer.backward(dout)を繰り返して、勾配を求めています。繰り返しを展開すると、こうなります。

```python
dout = layers[0].backward(dout)  #Affine7
dout = layers[1].backward(dout)  #Activation_function6 Relu
dout = layers[2].backward(dout)  #Affine6
dout = layers[3].backward(dout)  #Activation_function5 Relu
dout = layers[4].backward(dout)  #Affine5
dout = layers[5].backward(dout)  #Activation_function4 Relu
dout = layers[6].backward(dout)  #Affine4
dout = layers[7].backward(dout)  #Activation_function3 Relu
dout = layers[8].backward(dout)  #Affine3
dout = layers[9].backward(dout)  #Activation_function2 Relu
dout = layers[10].backward(dout) #Affine2
dout = layers[11].backward(dout) #Activation_function1 Relu
dout = layers[12].backward(dout) #Affine1
```

各Affineレイヤ内で参照する self.x self.W は、forwardメソッドを実行したときにセットされたものを使っています。

```python
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
```
各レイヤで求めたdw、dbを使って、各レイヤの重みとバイアスの勾配をセットして、関数の値として返します。

```python
        # 設定
        grads = {}
        for idx in range(1, self.hidden_layer_num+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
```

返された勾配で、パラメータを更新して、ミニバッチの処理が１回終わります。

```python
    grads = network.gradient(x_batch, t_batch)
    optimizer.update(network.params, grads)
```

```python
class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr
        
    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key] 
```
lr はlearning rate（ 学習係数）
この例では、0.01が設定されている。

###MultiLayerNetExtendクラス
multi_layer_net_extend.py にあるMultiLayerNetExtendクラスは、レイヤ生成のところで　Dropout、Batch Normalization　に対応していますが、基本的なところはMultiLayerNetと同じです。


　[その９](https://qiita.com/slow_learner/items/16b4cad06782abeb98d5) ← 　→ [その１０の２](https://qiita.com/slow_learner/items/d2e2e2094080d67e3e01) 　→ [その１１](https://qiita.com/slow_learner/items/d4c36e3701e16c5a9e77)
メモの目次等はこちらから　[読めない用語集](https://qiita.com/slow_learner/items/03eb3367119e98b5a22c)