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

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

#2.RNNLM
　まず、PTBデータセットの語順を学習する下記のコード、ch05/train_custom_loop.py を実行してみます。

```python
import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm

# ハイパーパラメータの設定
batch_size = 10
wordvec_size = 100
hidden_size = 100
time_size = 5  # Truncated BPTTの展開する時間サイズ
lr = 0.1
max_epoch = 100

# 学習データの読み込み（データセットを小さくする）
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)

xs = corpus[:-1]  # 入力
ts = corpus[1:]  # 出力（教師ラベル）
data_size = len(xs)
print('corpus size: %d, vocabulary size: %d' % (corpus_size, vocab_size))

# 学習時に使用する変数
max_iters = data_size // (batch_size * time_size)
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []

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

# ミニバッチの各サンプルの読み込み開始位置を計算
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]

for epoch in range(max_epoch):
    for iter in range(max_iters):
        # ミニバッチの取得
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')
        for t in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, t] = xs[(offset + time_idx) % data_size]
                batch_t[i, t] = ts[(offset + time_idx) % data_size]
            time_idx += 1

        # 勾配を求め、パラメータを更新
        loss = model.forward(batch_x, batch_t)
        model.backward()
        optimizer.update(model.params, model.grads)
        total_loss += loss
        loss_count += 1

    # エポックごとにパープレキシティの評価
    ppl = np.exp(total_loss / loss_count)
    print('| epoch %d | perplexity %.2f'
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0

# グラフの描画
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()
```
![スクリーンショット 2020-05-18 18.32.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/1cfffcd6-4ec2-57ab-6a65-ea329cb254aa.png)
　グラフの縦軸は次に来る単語の確率を予測する**パープレキシティ**という指標で、$perplexity=e^L\  (L=-\frac{1}{N}\sum_n\sum_k t_{nk}log y_{nk})$という計算式で表わされます。パープレキシティの値は1に近くほど、予測精度が高いことになります。簡単に言うと、パープレキシティは次に来る単語の選択肢の数と言うことができます。それでは、データを準備する部分をざっと見ておきます。

![スクリーンショット 2020-05-19 10.45.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/1dfa016c-1a7a-7678-74b6-c4591833e2cc.png)

　**corpus** は PTBデータセットの先頭1,000語のみを使い、**学習用データxs** と**教師データts** は1語づらしでそれぞれ999語取得します。

　そして、`offsets` でバッチサイズ分の読み込み位置(10箇所)を決め、タイムサイズ分のデータ毎(5個)で区切ってミニバッチを作成します。`offsets + time_idx` がデータサイズの999以上になったら再び0からスタートしてデータを取得しています。

　それでは、モデルの生成を行っている、**class SimpleRnnlm** を見てみましょう。

#3.SimpleRnnlm
```python
class SimpleRnnlm:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        # 重みの初期化
        embed_W = (rn(V, D) / 100).astype('f')
        rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f')
        rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f')
        rnn_b = np.zeros(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),
            TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.rnn_layer = self.layers[1]

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

    def forward(self, xs, ts):
        for layer in self.layers:
            xs = layer.forward(xs)
        loss = self.loss_layer.forward(xs, 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.rnn_layer.reset_state()
```
![スクリーンショット 2020-05-17 13.58.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/a4da3087-b76d-5662-8e92-3d0a130bd774.png)

　**class SimpleRnnlm**は、**TimeEmbedding, Time RNN, Time Affine, Time Softmax With Loss** という４つの**Timeレイヤ**を重ねたものです。では、Timeレイヤを順番に見て行きましょう。

#4.TimeEmbeddingレイヤ

```python
class TimeEmbedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.layers = None
        self.W = W

    def forward(self, xs):
        N, T = xs.shape
        V, D = self.W.shape

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

        for t in range(T):
            layer = Embedding(self.W)
            out[:, t, :] = layer.forward(xs[:, t])
            self.layers.append(layer)

        return out

    def backward(self, dout):
        N, T, D = dout.shape

        grad = 0
        for t in range(T):
            layer = self.layers[t]
            layer.backward(dout[:, t, :])
            grad += layer.grads[0]

        self.grads[0][...] = grad
        return None

``` 
![スクリーンショット 2020-05-21 09.21.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/1603c7e0-e529-4b7c-e961-099eae6af43c.png)


　**Time Embeddingレイヤ**は、xsから1列づつデータを切り出し**Embeddingレイヤ**に入力して、その出力を**out(N, T, D)**に溜め込む、という動作をforループでT回繰り返すというものです。

#5.RNN レイヤ
　TimeRNN レイヤをを見る前に、TimeRNN レイヤに使われている RNN レイヤから見て行きます。

```python
class RNN:
    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):
        Wx, Wh, b = self.params
        t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
        h_next = np.tanh(t)

        self.cache = (x, h_prev, h_next)
        return h_next
```
![スクリーンショット 2020-05-21 10.53.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/4064e07f-b9bb-0429-ac92-c86b3187aadb.png)

　RNNレイヤは、２つの重みを持っています。それは、入力x_tと内積(MatMul)を取る重み**W_x**と入力h_prevと内積(MatMUl)を取る重み**W_h**です。

```python
    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev, h_next = self.cache

        dt = dh_next * (1 - h_next ** 2)
        db = np.sum(dt, axis=0)
        dWh = np.dot(h_prev.T, dt)
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt, Wx.T)

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

        return dx, dh_prev
```
![スクリーンショット 2020-05-21 10.54.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/19a3800d-3b64-913e-9ae4-71afde545a8e.png)



　逆伝播はこんな形。Affineの変形版なので、特に複雑なところはありません。

#6.TimeRNNレイヤ

```python
class TimeRNN:
    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.dh = None, None
        self.stateful = stateful

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

        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')

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

        return hs
```

![スクリーンショット 2020-05-21 10.56.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/e67a733a-b14f-7226-dbb8-0a9159fc9d41.png)


　**TimeRNNレイヤ**は、T個のRNNレイヤを連結したネットワークです。ブロック間の状態hを引き継ぐかどうかを`stateful`という引数で調整できるようにします。

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

　つまり、TimeRNNレイヤは、RNNレイヤの入出力にデータ切り出しとデータまとめの機能を付け加えたものです。

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

        dxs = np.empty((N, T, D), dtype='f')
        dh = 0
        grads = [0, 0, 0]
        for t in reversed(range(T)):
            layer = self.layers[t]
            dx, dh = layer.backward(dhs[:, t, :] + dh)  # 合算した勾配
            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):
        self.h = h

    def reset_state(self):
        self.h = None
```

![スクリーンショット 2020-05-19 10.27.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/98463a6e-1549-5bd2-7501-f550f4e80920.png)

　TimeRNNの順伝播は出力が2つあるので、逆伝播のときはその２つが合算された $dh_t+dh_{next}$が入力されます。

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

```python
class TimeAffine:
    def __init__(self, W, b):
        self.params = [W, b]
        self.grads = [np.zeros_like(W), np.zeros_like(b)]
        self.x = None

    def forward(self, x):
        N, T, D = x.shape
        W, b = self.params

        rx = x.reshape(N*T, -1)
        out = np.dot(rx, W) + b
        self.x = x
        return out.reshape(N, T, -1)

    def backward(self, dout):
        x = self.x
        N, T, D = x.shape
        W, b = self.params

        dout = dout.reshape(N*T, -1)
        rx = x.reshape(N*T, -1)

        db = np.sum(dout, axis=0)
        dW = np.dot(rx.T, dout)
        dx = np.dot(dout, W.T)
        dx = dx.reshape(*x.shape)

        self.grads[0][...] = dW
        self.grads[1][...] = db

        return dx
```
![スクリーンショット 2020-05-21 10.56.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/793ab60b-3bc3-e0c3-f1f4-0d1d6e083705.png)


　**Time Affineレイヤ**は、**Affineレイヤ**の入出力に、時間軸方向のTに対応出来るようにそれぞれreshapeを付け加えたものです。

#8.TimeSoftmaxWithLoss

```python
class TimeSoftmaxWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None
        self.ignore_label = -1

    def forward(self, xs, ts):
        N, T, V = xs.shape

        if ts.ndim == 3:  # 教師ラベルがone-hotベクトルの場合
            ts = ts.argmax(axis=2)

        mask = (ts != self.ignore_label)

        # バッチ分と時系列分をまとめる（reshape）
        xs = xs.reshape(N * T, V)
        ts = ts.reshape(N * T)
        mask = mask.reshape(N * T)

        ys = softmax(xs)
        ls = np.log(ys[np.arange(N * T), ts])
        ls *= mask  # ignore_labelに該当するデータは損失を0にする
        loss = -np.sum(ls)
        loss /= mask.sum()

        self.cache = (ts, ys, mask, (N, T, V))
        return loss

    def backward(self, dout=1):
        ts, ys, mask, (N, T, V) = self.cache

        dx = ys
        dx[np.arange(N * T), ts] -= 1
        dx *= dout
        dx /= mask.sum()
        dx *= mask[:, np.newaxis]  # ignore_labelに該当するデータは勾配を0にする

        dx = dx.reshape((N, T, V))

        return dx
```

![スクリーンショット 2020-05-20 19.00.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/caec6ccd-73b3-f73a-e205-776882f94b0e.png)
　**Time Softmax with Loss レイヤー** は、$x_t, t_t$のSotmax with Loss をT個合算してTで割るレイヤーです。

#9.日本語版データセットの作成
　全体の理解を深めるために、日本語のデータセットでもやってみましょう。但し、単語単位でやろうとすると形態素解析が必要なので**文字単位**とします。今回は、青空文庫から[「老人と海」](https://www.aozora.gr.jp/cards/001847/card57347.html)をダウンロードして使っています。

```python
import numpy as np
import io

def load_data():
    
    # file_name の先頭から1000文字をUTF-8 形式で textに読み込み
    file_name = './data_rojinto_umi.txt'
    length = 1000
    with io.open(file_name, encoding='utf-8') as f:
        text = f.read().lower()
        text = text[:length]

    # word_to_id, id_to_ward の作成
    word_to_id, id_to_word = {}, {}
    for word in text:
        if word not in word_to_id:
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word

    # corpus の作成
    corpus = np.array([word_to_id[W] for W in text])  
    
    return text, corpus, word_to_id, id_to_word
```
　`file_name` で指定したテキストファイルの先頭から1000文字分をUTF-8形式で読み込み。text, corpus, word_to_id, id_to_word を返す関数です。ちょっと動かしてみます。

```python
text, corpus, word_to_id, id_to_word = load_data()
print('text_length = ', len(text))
print(text)
```
![スクリーンショット 2020-05-20 15.09.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/7b0c278e-6fb8-9782-4dcb-014547793dbe.png)
　`text` の長さは指定通り1000になっています。文字単位で用意すると、テキストはとてもコンパクトですね。

```python
print('vocab_size = ', len(word_to_id))
print(word_to_id)
```
![スクリーンショット 2020-05-20 15.17.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/66e1999f-776d-41a0-e807-3e96f5587e12.png)

　`word_to_id` です。`vocab_size` は236と思ったほど大きくはありません。これを使って `text` の１文字１文字をidに置き換えて、`corpus` を作成するわけです。

```python
print('corpus_length = ', len(corpus))
print(corpus[:500])
```
![スクリーンショット 2020-05-20 15.22.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/a778e5d5-06b8-ace2-f27e-198eb4c464a4.png)
　corpus です。表示は、始めから500文字分のみにしています。

```python
text2 = ''.join([id_to_word[id] for id in corpus])
print(text2)
```
![スクリーンショット 2020-05-20 15.26.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d0db54c0-a9f6-6ef6-bb8b-8f6dd290b7a8.png)

　`corpus` の id を `id_to_word` を使って文字に変換すると、この様に最初のテキストに戻ります。

　さて、せっかくなので、テスト用のデータと答えを用意して、epoch毎にどの程度予測ができているのかを確認してみたいと思います。

```python
# corpus からのサンプル
x = corpus[:50]
t = corpus[1:51]
print('x = ', x)
print('t = ', t)

# 文字による確認
text_x = ''.join([id_to_word[id] for id in x])
text_t = ''.join([id_to_word[id] for id in t])
print('text_x = ', text_x)
print('text_t = ', text_t)

# バッチ形式に変換
test_x = x.reshape(10, 5)
test_t = t.reshape(10, 5)
print(test_x)
print(test_t)
```
![スクリーンショット 2020-05-20 15.57.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/ffd5b615-373d-2119-17de-ac1ebeebae08.png)
　`corpus` から50文字分を1文字づらしで x, t を取得します。一応、文字に変換して内容を確認しています。そして、モデルで使われているシェイプ(10, 5)に変換し、テスト用の予測データ test_x と test_t　を作成しています。


```python
    def generate(self, xs):
        for layer in self.layers:
            xs = layer.forward(xs)
        return xs
```
　後、各epoch毎に予測をさせるために、simple_rnnlm.pyの最後に、このコードを追加しておきます。

#10.改造版RNNLM
　それでは、最初に実行したコード ch05/train_custom_loop.py を元に、**学習データの読み込み、テスト用データの推論実行** の２つの部分を修正追加して実行します。epoch数は、1000回です。

```python
import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm    

# ハイパーパラメータの設定
batch_size = 10
wordvec_size = 100
hidden_size = 100
time_size = 5  # Truncated BPTTの展開する時間サイズ
lr = 0.1
max_epoch = 1000  

# ----------- 学習データの読み込み -------------
text, corpus, word_to_id, id_to_word = load_data()
corpus_size = 1000
vocab_size = int(max(corpus) + 1)
# ----------------------------------------------

xs = corpus[:-1]  # 入力
ts = corpus[1:]  # 出力（教師ラベル）
data_size = len(xs)
print('corpus size: %d, vocabulary size: %d' % (corpus_size, vocab_size))

# 学習時に使用する変数
max_iters = data_size // (batch_size * time_size)  
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []

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

# ミニバッチの各サンプルの読み込み開始位置を計算
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]

for epoch in range(max_epoch):
    for iter in range(max_iters):
        # ミニバッチの取得
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')
        for t in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, t] = xs[(offset + time_idx) % data_size]
                batch_t[i, t] = ts[(offset + time_idx) % data_size]
            time_idx += 1

        # 勾配を求め、パラメータを更新
        loss = model.forward(batch_x, batch_t)
        model.backward()
        optimizer.update(model.params, model.grads)
        total_loss += loss
        loss_count += 1

    # エポックごとにパープレキシティの評価
    ppl = np.exp(total_loss / loss_count)
    print('| epoch %d | perplexity %.2f'
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0
    
    # ---------- テスト用データで予測 ------------
    pred= model.generate(test_x) 
    predict = np.argmax(pred, axis = 2) 
    print(predict)
    # ------------------------------------------------
    
# グラフの描画
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()
```
![スクリーンショット 2020-05-20 17.02.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/754ae9ba-7a83-6b19-a363-4ba4d6713d22.png)
　1000epoch後の perplexity は 1.08まで下がりました。各epoch毎に、テスト用データによる予測結果を表示しているので、1epoch後の予測結果を見ると全て5(ひらがなの「た」) です。学習したばかりの時は、こんな感じなんでしょう。これが、1000epoch後の予測結果は、結構それっぽくなりました。では、最終の予測結果を確認してみましょう。

![スクリーンショット 2020-05-20 18.42.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/66bb34a5-33c8-a965-ad8e-44870e40ff66.png)


　予測結果を答え合わせし正解だったところを赤丸で囲んでいます。24/50ですので、正答率48％で、思ったほど高くはないです。5文字の後半の正答率が高いのは、前半の文字を踏まえて予測できるためでしょうか。１行目の予測結果をモデルで表してみると、

![スクリーンショット 2020-05-20 18.36.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/7326ae8a-de85-fd7d-fae8-38a135fb1db9.png)

　なるほど。こんな感じに間違えたわけですか。