深層学習／ゼロから作るDeep Learning２　第６章メモ

#1.はじめに
　名著、**「ゼロから作るDeep Learning２」**を読んでいます。今回は６章のメモ。
　コードの実行はGithubからコード全体をダウンロードし、ch06の中で jupyter notebook にて行っています。

#2.LSTMモデル
　PTBデータセットの語順を学習する、ch06/train_rnnlm.py のコードです。５章のシンプルなRNNモデルでは、学習データセットの先頭1000語のみを学習しましたが、今回は学習データセット全体約90万語を学習します。

　time_size = 35, batch_size = 20, word_size = hidden_size = 100, max_eopch = 4 で実行します。

```python
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity
from dataset import ptb
from rnnlm import Rnnlm

# ハイパーパラメータの設定
batch_size = 20
wordvec_size = 100
hidden_size = 100  # RNNの隠れ状態ベクトルの要素数
time_size = 35  # RNNを展開するサイズ
lr = 20.0
max_epoch = 4
max_grad = 0.25

# 学習データの読み込み
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_test, _, _ = ptb.load_data('test')
vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

# モデルの生成
model = Rnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

# 勾配クリッピングを適用して学習
trainer.fit(xs, ts, max_epoch, batch_size, time_size, max_grad,
            eval_interval=20)
trainer.plot(ylim=(0, 500))

# テストデータで評価
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)

# パラメータの保存
model.save_params()
```
![スクリーンショット 2020-05-22 19.20.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/9bc9e525-2a8b-d86a-a54c-616d36e189e7.png)

　私のMacbookAirでは終了するまでに33分掛かりました。４epoch後の perplexityは111.47, テストデータによる perplexityは136.3 でした。次の単語を予測した場合の選択肢が136語ということですね。５章でやったのとは全然条件が違うので比較はできませんが、思ったより大きいです。

　５章の単純RNNモデルでは、勾配爆発が起こりやすいため、LSTMモデルではそれを抑制するためにtrainerクラスに、**勾配クリッピング**( -threshold < ||$\hat{g}$|| < threshold とする) 機能が含まれています。　
　
#3.Rnnlm

```python
import sys
sys.path.append('..')
from common.time_layers import *
from common.base_model import BaseModel


class Rnnlm(BaseModel):
    def __init__(self, vocab_size=10000, wordvec_size=100, hidden_size=100):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        # 重みの初期化
        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * H).astype('f')
        affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
        affine_b = np.zeros(V).astype('f')

        # レイヤの生成
        self.layers = [
            TimeEmbedding(embed_W),
            TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.lstm_layer = self.layers[1]

        # すべての重みと勾配をリストにまとめる
        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def predict(self, xs):
        for layer in self.layers:
            xs = layer.forward(xs)
        return xs

    def forward(self, xs, ts):
        score = self.predict(xs)
        loss = self.loss_layer.forward(score, ts)
        return loss

    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout

    def reset_state(self):
        self.lstm_layer.reset_state()
```
![スクリーンショット 2020-05-22 13.31.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/19b35dea-8cc6-35f1-da5f-9849395053c3.png)
　**Rnnlm**のレイヤ構成は、５章のモデルの Time RNNレイヤ を Time LSTMレイヤに置き換えただけです。Time LSTMを見る前に、そこで使われているLSTMレイヤをまず見てみましょう。

#4.LSTMレイヤ

```python
class LSTM:
    def __init__(self, Wx, Wh, b):

        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None

    def forward(self, x, h_prev, c_prev):
        Wx, Wh, b = self.params
        N, H = h_prev.shape

        A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b

        f = A[:, :H]
        g = A[:, H:2*H]
        i = A[:, 2*H:3*H]
        o = A[:, 3*H:]

        f = sigmoid(f)
        g = np.tanh(g)
        i = sigmoid(i)
        o = sigmoid(o)

        c_next = f * c_prev + g * i
        h_next = o * np.tanh(c_next)

        self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
        return h_next, c_next
```
![スクリーンショット 2020-05-22 16.33.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/50ad42de-d43d-31b9-37ae-67fdb102999a.png)

　**LSTMレイヤ**の順伝播の部分です。セルへの記憶と３つのゲートの順伝播の式は、いずれも同じ形をしているので、重み$W_x,W_h,b$はそれぞれまとめてAを計算し、その後Aをスライシングして活性化関数を通すのが効率的です。 

```python
    def backward(self, dh_next, dc_next):
        Wx, Wh, b = self.params
        x, h_prev, c_prev, i, f, g, o, c_next = self.cache

        tanh_c_next = np.tanh(c_next)

        ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2)

        dc_prev = ds * f

        di = ds * g
        df = ds * c_prev
        do = dh_next * tanh_c_next
        dg = ds * i

        di *= i * (1 - i)
        df *= f * (1 - f)
        do *= o * (1 - o)
        dg *= (1 - g ** 2)

        dA = np.hstack((df, dg, di, do))

        dWh = np.dot(h_prev.T, dA)
        dWx = np.dot(x.T, dA)
        db = dA.sum(axis=0)

        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db

        dx = np.dot(dA, Wx.T)
        dh_prev = np.dot(dA, Wh.T)

        return dx, dh_prev, dc_prev
```
![スクリーンショット 2020-05-22 13.16.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/b6f18d87-a46f-d7b0-0458-73eac744f4ef.png)
　逆伝播は、df, dg, di, doをそれぞれ求め hstach で連結すれば dA が求まります。後は、MatMulの逆伝播なので、dWx, dWh, db, dx, dh_prev が求まります。
 
#5.TimeLSTM

```python
class TimeLSTM:
    def __init__(self, Wx, Wh, b, stateful=False):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.layers = None

        self.h, self.c = None, None
        self.dh = None
        self.stateful = stateful

    def forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = xs.shape
        H = Wh.shape[0]

        self.layers = []
        hs = np.empty((N, T, H), dtype='f')

        if not self.stateful or self.h is None:
            self.h = np.zeros((N, H), dtype='f')
        if not self.stateful or self.c is None:
            self.c = np.zeros((N, H), dtype='f')

        for t in range(T):
            layer = LSTM(*self.params)
            self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
            hs[:, t, :] = self.h

            self.layers.append(layer)

        return hs
```
![スクリーンショット 2020-05-22 18.06.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/1792a937-3a62-e163-e9a1-5f26665a7f42.png)

　基本的には、TimeRNNの場合と同様です。**TimeLSTMレイヤ**は、T個のLSTMレイヤを連結したネットワークです。ブロック間の状態hを引き継ぐかどうかをstatefulという引数で調整できるようにします。また、記憶を保存するセルCがあります。

　順伝播では、まず出力用の容器 hs(N, T, H) を用意します。そして、forループを回しながら、xs[:, t, :]によってt番目のデータを切り出して、通常のLSTMに入力し、出力はhs[:, t, :]で用意した容器の指定位置に格納して行くと共に、layersにレイヤーを登録して行きます。

```python
    def backward(self, dhs):
        Wx, Wh, b = self.params
        N, T, H = dhs.shape
        D = Wx.shape[0]

        dxs = np.empty((N, T, D), dtype='f')
        dh, dc = 0, 0

        grads = [0, 0, 0]
        for t in reversed(range(T)):
            layer = self.layers[t]
            dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
            dxs[:, t, :] = dx
            for i, grad in enumerate(layer.grads):
                grads[i] += grad

        for i, grad in enumerate(grads):
            self.grads[i][...] = grad
        self.dh = dh
        return dxs

    def set_state(self, h, c=None):
        self.h, self.c = h, c

    def reset_state(self):
        self.h, self.c = None, None
```
　TimeLSTMの順伝播は出力が３つあるので、逆伝播のときはその３つが合算された$dh_t+dh_{next}+dc_{next}$が入力されます。

　まず、下流に流す容器dxsを作り、順伝播とは逆順でLSTMレイヤのbackward()で各時刻の勾配dxを求め、dxsの該当するインデッックスに代入します。重みパラメータは、各レイヤの重み勾配を加算し最終結果をself.gradsに上書きします。

#6.改良版LSTMモデル 
　さて、先程の改良モデル train_better_rnnlm.py を動かしてみましょう。さすがに、これはCPUだけではつらいので、windowsマシンにcupyをインストールしました。

　time_size = 35, batch_size = 20, **word_size = hidden_size = 650, max_epoch = 40** で実行します。 

```python
import sys
sys.path.append('..')
from common import config
# GPUで実行する場合は下記のコメントアウトを消去（要cupy）
# ==============================================
config.GPU = True
# ==============================================
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity, to_gpu
from dataset import ptb
from better_rnnlm import BetterRnnlm

# ハイパーパラメータの設定
batch_size = 20
wordvec_size = 650
hidden_size = 650
time_size = 35
lr = 20.0
max_epoch = 40
max_grad = 0.25
dropout = 0.5

# 学習データの読み込み
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_val, _, _ = ptb.load_data('val')
corpus_test, _, _ = ptb.load_data('test')

if config.GPU:
    corpus = to_gpu(corpus)
    corpus_val = to_gpu(corpus_val)
    corpus_test = to_gpu(corpus_test)

vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

model = BetterRnnlm(vocab_size, wordvec_size, hidden_size, dropout)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

best_ppl = float('inf')
for epoch in range(max_epoch):
    trainer.fit(xs, ts, max_epoch=1, batch_size=batch_size,
                time_size=time_size, max_grad=max_grad)

    model.reset_state()
    ppl = eval_perplexity(model, corpus_val)
    print('valid perplexity: ', ppl)

    if best_ppl > ppl:
        best_ppl = ppl
        model.save_params()
    else:
        lr /= 4.0
        optimizer.lr = lr

    model.reset_state()
    print('-' * 50)

# テストデータでの評価
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)
```
![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/5bcb5a1e-f5e0-7527-f119-089f71fcf6a3.png)
　windowsマシン(GTX1060)で３時間弱で完了しました。40eopch後のテストデータによる評価はperplexity=76〜79まで改善しています。

　このコードでは、1epch毎にテストデータのperplexityを計算し、その値が悪化した場合のみ学習係数 lrを下げるという工夫がされています。それでは、BetterRnnlmを見てみましょう。

#7.BetterRnnlm

```python
import sys
sys.path.append('..')
from common.time_layers import *
from common.np import *  # import numpy as np
from common.base_model import BaseModel

class BetterRnnlm(BaseModel):
    def __init__(self, vocab_size=10000, wordvec_size=650,
                 hidden_size=650, dropout_ratio=0.5):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b1 = np.zeros(4 * H).astype('f')
        lstm_Wx2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b2 = np.zeros(4 * H).astype('f')
        affine_b = np.zeros(V).astype('f')

        self.layers = [
            TimeEmbedding(embed_W),
            TimeDropout(dropout_ratio),
            TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True),
            TimeDropout(dropout_ratio),
            TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True),
            TimeDropout(dropout_ratio),
            TimeAffine(embed_W.T, affine_b)  # weight tying!!
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.lstm_layers = [self.layers[2], self.layers[4]]
        self.drop_layers = [self.layers[1], self.layers[3], self.layers[5]]

        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def predict(self, xs, train_flg=False):
        for layer in self.drop_layers:
            layer.train_flg = train_flg

        for layer in self.layers:
            xs = layer.forward(xs)
        return xs

    def forward(self, xs, ts, train_flg=True):
        score = self.predict(xs, train_flg)
        loss = self.loss_layer.forward(score, ts)
        return loss

    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout

    def reset_state(self):
        for layer in self.lstm_layers:
            layer.reset_state()
```
![スクリーンショット 2020-05-23 16.17.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/254c6004-ae48-ff43-a409-edd820d86dd0.png)
　BetterRnnlmのレイヤ構成です。特徴は、LSTMレイヤの多層化(2個)、Dropoutの使用(2個)、Time Embedding とTime Affine の重み共有、の３つです。

　LSTMの多層化で表現力を上げる一方で、過学習抑制のためにTimeDropout(中身は基本的にDropoutと同一です）を入れています。重みの共有化は、Time Embeddinhレイヤの重みは(V, H)、Affineレイヤの重みは(H, v)なので、Time Embeddingレイヤの重みの転置をAffineレイヤの重みとすることで学習パラメータを減らし、過学習を抑制すると共に学習を容易にしています。