2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

深層学習/ゼロから作るDeep Learning2 第5章メモ

Last updated at Posted at 2020-05-21

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

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

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
 グラフの縦軸は次に来る単語の確率を予測するパープレキシティという指標で、$perplexity=e^L\ (L=-\frac{1}{N}\sum_n\sum_k t_{nk}log y_{nk})$という計算式で表わされます。パープレキシティの値は1に近くほど、予測精度が高いことになります。簡単に言うと、パープレキシティは次に来る単語の選択肢の数と言うことができます。それでは、データを準備する部分をざっと見ておきます。

スクリーンショット 2020-05-19 10.45.53.png

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

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

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

#3.SimpleRnnlm

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

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

#4.TimeEmbeddingレイヤ

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

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

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

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

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

    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

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

#6.TimeRNNレイヤ

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

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

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

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

    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

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

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

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

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

#8.TimeSoftmaxWithLoss

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
 Time Softmax with Loss レイヤー は、$x_t, t_t$のSotmax with Loss をT個合算してTで割るレイヤーです。

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

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 を返す関数です。ちょっと動かしてみます。

text, corpus, word_to_id, id_to_word = load_data()
print('text_length = ', len(text))
print(text)

スクリーンショット 2020-05-20 15.09.17.png
 text の長さは指定通り1000になっています。文字単位で用意すると、テキストはとてもコンパクトですね。

print('vocab_size = ', len(word_to_id))
print(word_to_id)

スクリーンショット 2020-05-20 15.17.08.png

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

print('corpus_length = ', len(corpus))
print(corpus[:500])

スクリーンショット 2020-05-20 15.22.04.png
 corpus です。表示は、始めから500文字分のみにしています。

text2 = ''.join([id_to_word[id] for id in corpus])
print(text2)

スクリーンショット 2020-05-20 15.26.55.png

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

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

# 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
 corpus から50文字分を1文字づらしで x, t を取得します。一応、文字に変換して内容を確認しています。そして、モデルで使われているシェイプ(10, 5)に変換し、テスト用の予測データ test_x と test_t を作成しています。

    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 を元に、学習データの読み込み、テスト用データの推論実行 の2つの部分を修正追加して実行します。epoch数は、1000回です。

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
 1000epoch後の perplexity は 1.08まで下がりました。各epoch毎に、テスト用データによる予測結果を表示しているので、1epoch後の予測結果を見ると全て5(ひらがなの「た」) です。学習したばかりの時は、こんな感じなんでしょう。これが、1000epoch後の予測結果は、結構それっぽくなりました。では、最終の予測結果を確認してみましょう。

スクリーンショット 2020-05-20 18.42.27.png

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

スクリーンショット 2020-05-20 18.36.00.png

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

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?