ゼロから作るDeep Learning2 Ch1

[ゼロから作るDeep Learning2](https://www.oreilly.co.jp/books/9784873118369/)の勉強日誌です。不定期更新。

基本的にコードの実装方針・中身を確認しつつ自分で思いつく実験などを自由に行っていきます。
誤りなどを見つけた場合はコメントなどでお知らせいただけると助かります。

適当なディレクトリに https://github.com/oreilly-japan/deep-learning-from-scratch-2　の内容をクローンします。

# 1.4.3 を参考に TwoLayerNet の挙動を確認

必要なライブラリを読み込み
```python:train_custom_loop.py
# coding: utf-8
import sys
sys.path.append('..')  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from dataset import spiral
import matplotlib.pyplot as plt

# local で作成されたファイル
from common.optimizer import SGD # 最適化関数を一覧で定義している
from two_layer_net import TwoLayerNet
```

ハイパーパラメータを設定して、学習データを読み込み、モデルのインスタンスを作成
```python:train_custom_loop.py
# ハイパーパラメータの設定
max_epoch = 300
batch_size = 30
hidden_size = 10
learning_rate = 1.0

# 学習用データの読み込み
x, t = spiral.load_data()
# モデルのインスタンスを作成
model = TwoLayerNet(input_size=2, hidden_size=hidden_size, output_size=3)
# 最適化関数に学習率を設定
# 確率的勾配降下法（Stochastic Gradient Descent） を利用する
optimizer = SGD(lr=learning_rate)
```

common ディレクトリに入っている python ファイルでそれぞれ定義されている中身の挙動を確認していきたい。

### モデル内で設定されている各層
```python
# 設定されているレイヤーを確認
model.layers

#[<common.layers.Affine at 0x1376f73a0>,
# <common.layers.Sigmoid at 0x130385ac0>,
# <common.layers.Affine at 0x1376a0fd0>]
```

これは ch1/two_layer_net.py の イニシャライザないで次のように定義されていることに由来している.
```python:two_layer_net.py
# レイヤの生成
self.layers = [
    Affine(W1, b1),
    Sigmoid(),
    Affine(W2, b2)
]
```

他にもイニシャライザ内では損失関数と初期値のパラメータを格納している
```python
# 損失関数を確認
print(model.loss_layer)
# SoftmaxWithLossが使われている
# <common.layers.SoftmaxWithLoss object at 0x13746bb50>

# 初期値のパラメータを確認
model.params
# イニシャライザで定義される W1,W2,b1,b2 の値がまとめられている.

# 勾配の値を確認
print(model.grads)
# すべての値が 0 で埋められている
```

学習で使用する変数を設定
```python
# 学習で使用する変数
data_size = len(x)
max_iters = data_size // batch_size
total_loss = 0
loss_count = 0
loss_list = []
```
## レイヤークラスの確認
common/layers.py で定義されている各レイヤーはイニシャライザ、`forward()`, `backward()`メソッドが定義されている。これらは順番に実行する必要がある.
例えば `Affine`クラスでは順方向の計算を行うには行列とベクトルのパラメータがそれぞれ必要であり、インスタンス作成時に引数でこれらが渡されインスタンス変数として内部で保持している.
```python:layers.py
class Affine:
    def __init__(self, W, b):
        self.params = [W, b] # パラメータを保持
        self.grads = [np.zeros_like(W), np.zeros_like(b)] # 勾配を保持
        self.x = None
```
forwardメソッドでは順方向の伝搬の計算を行うと同時に、入力した値(`x`)を内部で保持するようになっている。これは逆伝搬の計算で`x`を利用するためである.
イニシャライザで保持しているパラメータを呼び出して順方向の計算を実施している.
```python:layers.py
def forward(self, x):
    W, b = self.params
    out = np.dot(x, W) + b
    self.x = x
    return out
```
最後に`backward()`メソッドでは逆伝搬の計算を行う.`dx`,`dW`,`db`は勾配計算
```python:layers.py
def backward(self, dout):
    W, b = self.params
    dx = np.dot(dout, W.T)
    dW = np.dot(self.x.T, dout)
    db = np.sum(dout, axis=0)

    self.grads[0][...] = dW
    self.grads[1][...] = db
    return dx
```
このとき`backward`内で勾配の値を上書きしているが、TwoLayerNetのイニシャライザ内で
```python:two_layer_net.py
self.params, self.grads = [], []
for layer in self.layers:
    self.params += layer.params
    self.grads += layer.grads
```
~~というように参照渡しをしているので、自動的に `model.grads`の値が置き換わる.
参照渡しについては例えばこちらを参照~~

https://qiita.com/NeoNuc2001/items/9fd895d1c7f882f34f92

こちらコメントでご指摘がありましたが性格には参照の値渡し(インスタンスを渡している)が正確でした。

`self.params`に対して各 layer のパラメータを参照の値渡しを行っているため`layer.params`の値が変わると同時に`model.grads`の値が置き換わる.参照の値渡しについては例えばこちらを参照

https://note.com/crefil/n/n7a0d2dec929b

## TwoLayerNet のメソッドの挙動を確認

### `model.predict(x)`の挙動
 model.layers で定義されているレイヤーに対して`x`を代入して値を更新し、次のレイヤーに渡す。これを繰り返しているのが predict 
```python
# インプットの学習データのコピー
tmp_x = x
for layer in model.layers:
    # 各レイヤーに対してループ計算.
    # 順次tmp_xを代入して forward を計算して
    # tmp_x を更新していく
    tmp_x = layer.forward(tmp_x)

# 値が一致している加どうかを確認
(tmp_x == model.predict(batch_x)).all()
```

### `model.forward(x,t)`の挙動
```python:two_layer_net.py
# 各レイヤーの順方向の計算
score = self.predict(x)
# 損失関数の計算
loss = self.loss_layer.forward(score, t)
```
順方向の計算で現時点での損失関数の値を計算

### 勾配計算とパラメータの更新
backward()メソッドを呼び出すと, `loss_layer` と各層の `backward`メソッドが呼び出され、勾配計算が行われる. このとき必ず `forward`メソッドがあらかじめ呼び出されていないと計算に必要な値が適切に格納されていないことになるので注意.
レイヤークラスの確認部分でも述べた通り, layerの勾配と TwoLayerNet の勾配(`model.grads`)は参照渡しされているため値は自動的に同期されている.
```python:train_custom_loop.py
# 誤差逆伝搬を使って勾配を計算
# 計算結果はmodel.layersの各レイヤーの中で保持される
model.backward()
# モデルのパラメータを誤差逆伝搬で計算した勾配をもとに自動更新する
optimizer.update(model.params, model.grads)
```


### バッチ学習

ここからはバッチ学習を行う。 
- データをランダム化
- 学習データからバッチサイズだけ取り出す
- 順方向計算
- 逆伝搬を計算
- 逆伝搬の結果モデルパラメータを更新

がloop の中身になっている.

### 学習過程のプロット
損失関数の値がバッチ学習を経ることで減っていくことを確認する.
イテレーションが100を超えたところで次第にサチュレーションしていき学習が進みづらくなることが分かる.
```
# 学習結果のプロット
plt.plot(np.arange(len(loss_list)), loss_list, label='train')
plt.xlabel('iterations (x10)')
plt.ylabel('loss')
plt.show()
```