からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

5.5.1:RNNLMの実装【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、5.5.1項「RNNLMの実装」の内容です。RNNLMを解説して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

5.5.1 RNNLMの実装

 これまでに実装したレイヤを組み合わせてRNNLMを実装します。RNNLMとは、時系列を保ったテキストを扱うリカレントニューラルネットワークを使った言語モデルです。

# 5.5.1項で利用するライブラリ
import numpy as np


 テキストを入力データとして整形するための関数も読み込みます。「2.3.1項」で実装したpreprocess()を使って、テキストをコーパスに変換(単語に分割)します。

# 実装済みの関数
from common.util import preprocess


・処理の確認

・データとパラメータの作成

 まずは入力データ$\mathbf{xs}$と教師ラベル$\mathbf{ts}$を作成します。テキストを設定して、preprocess()で単語に分割します。

# テキストを設定
text = 'You say goodbye and I say hello.'

# 谷後に分割
corpus, word_to_id, id_to_word = preprocess(text)
print(id_to_word)
print(corpus)
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
[0 1 2 3 4 1 5 6]


 corpusの最後以外の要素(単語)を入力データ、最初以外の要素を正解ラベルとします。バッチサイズを$N=1$として、それぞれ2次元配列に変換しておきます。また単語数を時間サイズ$T$、語彙数(単語の種類数)を$V$とします。

# 学習用のデータを作成
xs = corpus[:-1].reshape(1, -1) # 入力データ
ts = corpus[1:] .reshape(1, -1) # 教師ラベル

# データの形状に関する値を取得
batch_size, time_size = xs.shape
vocab_size = len(word_to_id)
print(batch_size) # バッチサイズ
print(time_size) # 時間サイズ(繰り返し回数)
print(vocab_size) # 語彙数(単語の種類数)
1
7
7


 続いて、各レイヤのパラメータを生成します。これまではnp.random.randn()を使って標準正規分布(平均0、標準正規1の正規分布)に従ってランダムに初期値を決めました。ここではTNNレイヤとAffineレイヤの重みに関して、Xavierの初期値を使います。これは、その重みの前の層のニューロン数(その重みの行数)を$n$としたとき、標準偏差が$\frac{1}{\sqrt{n}}$である初期値のことです。標準偏差が1の乱数に対して、任意の値を掛けることで標準偏差をその値に変更できます。詳しくは1巻の6.2節を参照してください。Embedレイヤの重みは、標準偏差を0.01にします。

# 中間層のニューロン数を指定
wordvec_size = 10
hidden_size = 15

# Time Embedレイヤのパラメータを生成
embed_W = (np.random.randn(vocab_size, wordvec_size) / 100)

# Time RNNレイヤのパラメータを生成
rnn_Wx = (np.random.randn(wordvec_size, hidden_size) / np.sqrt(wordvec_size))
rnn_Wh = (np.random.randn(hidden_size, hidden_size) / np.sqrt(hidden_size))
rnn_b = np.zeros(hidden_size)

# Time Affineレイヤのパラメータを生成
affine_W = (np.random.randn(hidden_size, vocab_size) / np.sqrt(vocab_size))
affine_b = np.zeros(vocab_size)

 これで学習に用いる変数を作成できました。

・モデルの構築

 次は、各レイヤのインスタンスを作成し手、layersに格納ます。ただしTime Sofmax with Lossレイヤに関しては、他のレイヤと処理が異なるため分けておきます。これがRNNLMです。

# RNNレイヤのインスタンスを格納
layers = [
    TimeEmbedding(embed_W), 
    TimeRNN(rnn_Wx, rnn_Wh, rnn_b), 
    TimeAffine(affine_W, affine_b)
]

# lossレイヤのインスタンスを作成
loss_layer = TimeSoftmaxWithLoss()


 各レイヤが持つパラメータと勾配をコピーして、それぞれリストに格納しておきます。

# 全てのパラメータと勾配をリストに格納
params = [] # パラメータ
grads = []  # 勾配
for layer in layers:
    params += layer.params
    grads += layer.grads

print(len(params))
print(len(grads))
6
6

 これでモデルの用意が整いました。

・順伝播の処理

 モデルを構築できたので、推論処理を行います。各レイヤの順伝播メソッドを順番に実行して、スコアを計算します。

# スコアの計算
for layer in layers:
    xs = layer.forward(xs)

print(np.round(xs, 2))
[[[ 0.    0.    0.02  0.02 -0.03  0.01  0.01]
  [ 0.04 -0.    0.01 -0.03  0.02 -0.01 -0.  ]
  [ 0.01 -0.01  0.   -0.01 -0.01 -0.01  0.03]
  [ 0.01  0.04  0.01 -0.02  0.03  0.05 -0.01]
  [-0.02  0.02  0.02  0.01 -0.   -0.03  0.02]
  [ 0.04 -0.01  0.01 -0.01  0.04 -0.02 -0.  ]
  [-0.04  0.02  0.01  0.01 -0.01 -0.04  0.01]]]

 これは、Time Affineレイヤの出力データです。それぞれ3次元方向に並ぶ要素の内、最大値のインデックスが予測単語なのでした。ここまでが推論処理です。

 Time Sofmax with Lossレイヤの順伝播メソッド実行して、損失を計算します。

# 損失を計算
loss = loss_layer.forward(xs, ts)
print(np.round(loss, 2))
1.95

 RNNLMの出力(損失)が求まりました。ここでは学習を行っていないので、損失は高い値をとります。ここまでが順伝播の処理です。

・逆伝播の処理

 最後にパラメータの更新に用いる各パラメータの勾配を求めます。逆伝播の入力は、$\frac{\partial L}{\partial L} = 1$です。layersに格納されている各レイヤのインスタンスを後ろから取り出して、逆伝播メソッドを実行しdoutを更新していきます。

# 逆伝播の入力
dout = 1

# 後のレイヤから逆伝播を実行
dout = loss_layer.backward(dout)
for layer in reversed(layers):
    dout = layer.backward(dout)

print(dout)
None

 Embedレイヤの逆伝播は出力しないように実装しています。

 各レイヤのパラメータと勾配を確認しましょう。各レイヤのパラメータは、そのレイヤのインスタンスのメンバ変数に保存されています。また参照コピーしているparamsgradsからもアクセスできます。

# 確認
for i in range(len(params)):
    print(params[i].shape)
    print(grads[i].shape)
(7, 10)
(7, 10)
(10, 15)
(10, 15)
(15, 15)
(15, 15)
(15,)
(15,)
(15, 7)
(15, 7)
(7,)
(7,)

 ここまでが逆伝播の処理です。各レイヤの勾配を用いて勾配法によりパラメータを更新します。

・実装

 処理の確認ができたので、RNNLMをクラスとして実装します。

# RNNLMの実装
class SimpleRnnlm:
    # 初期化メソッドの定義
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        # 形状に関する値を取得
        V, D, H = vocab_size, wordvec_size, hidden_size
        
        # パラメータを生成
        embed_W = (np.random.randn(V, D) / 100).astype('f')
        rnn_Wx = (np.random.randn(D, H) / np.sqrt(D)).astype('f')
        rnn_Wh = (np.random.randn(H, H) / np.sqrt(H)).astype('f')
        rnn_b = np.zeros(H).astype('f')
        affine_W = (np.random.randn(H, V) / np.sqrt(V)).astype('f')
        affine_b = np.zeros(V).astype('f')
        
        # レイヤを生成
        self.layers = [
            TimeEmbedding(embed_W), 
            TimeRNN(rnn_Wx, rnn_Wh, rnn_b), 
            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()

 (ネットワークの切断に関してはまだよく理解していません…)

 実装したクラスを試してみましょう。確認作業の間にxsが上書きされているので、作成し直す必要があります。

# 学習用のデータを作成
xs = corpus[:-1].reshape(1, -1) # 入力データ
ts = corpus[1:].reshape(1, -1)  # 教師ラベル
print(xs)
print(ts)

# RNNLMのインスタンスを作成
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)

# 順伝播の計算
loss = model.forward(xs, ts)
print(np.round(loss, 2))

# 逆伝播の計算
dout = model.backward()

# 確認
for i in range(len(params)):
    print(params[i].shape)
    print(grads[i].shape)
[[0 1 2 3 4 1 5]]
[[1 2 3 4 1 5 6]]
1.93
(7, 10)
(7, 10)
(10, 15)
(10, 15)
(15, 15)
(15, 15)
(15,)
(15,)
(15, 7)
(15, 7)
(7,)
(7,)


 以上でRNNLMを実装できました。次項では、モデルの評価について考えます。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 これができたらほぼ完了です。

【次節の内容】

www.anarchive-beta.com