はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、5.5.1項「RNNLMの実装」の内容です。RNNLMを解説して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
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レイヤの逆伝播は出力しないように実装しています。
各レイヤのパラメータと勾配を確認しましょう。各レイヤのパラメータは、そのレイヤのインスタンスのメンバ変数に保存されています。また参照コピーしているparams
とgrads
からもアクセスできます。
# 確認 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を実装できました。次項では、モデルの評価について考えます。
参考文献
おわりに
これができたらほぼ完了です。
【次節の内容】