はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、5.4.2.1項「Time Embeddingレイヤの実装」の内容です。Time Embeddingレイヤを解説して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
5.4.2.1 Time Embeddingレイヤの実装
依存関係のある$T$個のデータ(時系列データ)を処理するために、$T$個のEmbeddingレイヤをまとめたTime Embeddingレイヤを実装します。Embedレイヤについては、4章も参照してください。
# 5.4.2.1項で利用するライブラリ import numpy as np
Time Embedレイヤの実装には、4.2.1項で実装したEmbedレイヤのクラスを利用します。クラスの定義を再実行するか、次の方法でマスターデータから読み込みます。
# 読み込み用の設定 import sys sys.path.append('C://Users//「ユーザ名」//Documents//・・・//deep-learning-from-scratch-2-master') # 実装済みのクラス from common.layers import Embedding
また、テキストを入力データとして整形するための関数も読み込みます。2.3.1項で実装したpreprocess()
を使って、テキストをコーパスに変換(単語に分割)します。
# 実装済みの関数 from common.util import preprocess
・順伝播の処理の確認
Time Embeddingレイヤで行う順伝播の処理を確認していきます。
まずは入力データと教師ラベルを作成します。テキストを設定して、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]
各要素は単語IDで、つまり1つの要素が1つの単語に対応しています。
ここから入力データ$\mathbf{xs} = (x_0, x_1, \cdots, x_{T-1})$と教師ラベル$\mathbf{ts} = (t_0, t_1, \cdots, t_{T-1})$を作成します。RNNでは、単語を順番に入力してその次の単語を予測するのでした。よって最後の単語は次の単語が存在しないため入力データには含めず、最初の単語は予測できないため教師ラベルには含めません。
# 学習用のデータを取得 xs = corpus[:-1] # 入力データ ts = corpus[1:] # 教師ラベル print(xs) print(ts) # 形状に関する値を取得 vocab_size = len(word_to_id) # 語彙数(単語の種類数) print(vocab_size)
[0 1 2 3 4 1 5]
[1 2 3 4 1 5 6]
7
語彙数$V$(vocab_size
)は、Affineレイヤでも利用します。
最終的に、RNNLMはバッチデータ(2次元配列)を扱えるように実装します。そこでここでは、xs
、ts
を簡易的に2次元配列に変換します。変換後のデータは、バッチサイズ$N = 1$と言えます。ミニバッチの処理については、5.5.3項で扱います。
またRNNレイヤの時系列データの数(繰り返し回数)$T$(time_size
)についても、簡易的にxs
の要素数(単語数)としておきます。
# バッチサイズ batch_size = 1 # 時系列サイズ(繰り返し回数) time_size = len(xs) # 2次元配列に変換 xs = xs.reshape(1, time_size) ts = ts.reshape(1, time_size) print(xs) print(ts)
[[0 1 2 3 4 1 5]]
[[1 2 3 4 1 5 6]]
RNNLMに入力される前は、各単語はスカラ(単語ID)で表現されています。
次に、Embedレイヤで用いる重みを生成します。これは4.1節における入力層の重み$\mathbf{W}_{\mathrm{in}}$のことで、行数が語彙数$V$、列数がEmbedレイヤの中間層のニューロン数$D$の2次元配列です。入力データとこの重みをEmbedレイヤで処理することで、各単語が$D$次元のベクトルに変換されます。そこでEmbedレイヤの中間層のニューロン数をwordvec_size
と呼ぶことにします。
# Embedレイヤの中間層のニューロン数を指定 wordvec_size = 10 # Embedレイヤの重みを生成 W = np.random.randn(vocab_size, wordvec_size) print(W.shape)
(7, 10)
入力データと重みを作成できたので、順伝播の処理を行います。
Embedレイヤのインスタンスを作成し、順伝播メソッドを実行し、出力をout
に格納する処理をtime_size
回繰り返します。最終的な出力データの形状は、3次元配列になります。各次元の要素数は、$(N, T, V)$です。
使用したEmbedレイヤのインスタンスは、逆伝播でも使うため、layers
に格納しておきます。
# 受け皿を初期化 out = np.empty((batch_size, time_size, wordvec_size), dtype='f') layers = [] # 繰り返し処理 for t in range(time_size): # Embedレイヤのインスタンスを作成 layer = Embedding(W) # 順伝播の計算 out[:, t, :] = layer.forward(xs[:, t]) # t番目のレイヤを格納 layers.append(layer) print(np.round(out, 2)) print(out.shape) print(len(layers))
[[[-0.23 0.79 -0.71 -0.27 1.75 1.11 0.86 -0.48 0.36 -2.33]
[-2.13 -1.13 -0.33 1.16 -0.08 1.33 -1.48 0.46 -1.72 -0.35]
[ 0.13 -1.21 2.36 1.83 0.08 -0.45 -0.43 -0.87 0.17 1.73]
[-1.24 1.46 0.49 0.04 0.89 -0.67 0.61 -0.08 1.35 -0.38]
[-0.5 -0.13 0.64 -0.35 -1.44 -0.66 0.56 0.9 -0.26 -0.02]
[-2.13 -1.13 -0.33 1.16 -0.08 1.33 -1.48 0.46 -1.72 -0.35]
[ 0.52 1.19 -1.47 0.65 2.11 -1.47 1.12 0.18 1.81 -0.13]]]
(1, 7, 10)
7
単語IDから単語ベクトルに変換され、3次元方向に$D$個拡張しました。この出力out
が、5.3.2項で実装したTime RNNレイヤの入力データ$\mathbf{xs}$です。
またlayers
に、time_size
個のEmbedding
レイヤのインスタンスが格納されているのを確認できます。
ここまでが順伝播の処理です。続いて、逆伝播の処理を確認していきます。
・逆伝播の処理の確認
layers
に格納されているレイヤの逆伝播メソッドを順番に実行して、重みの勾配$\frac{\partial L}{\partial \mathbf{W}}$を求めます。ただし、全てのEmbedレイヤで用いる重みは同じものです。つまり1つの重みが、$T$個のEmbedレイヤに分岐して入力していると言えます(1.3.4.3項「Repeatノード」)。よって、各レイヤで求めた重みの勾配$\frac{\partial L}{\partial \mathbf{W}}$を全て足す必要があります。
そこで実装上は、重みの勾配dW
の初期値を0
として、そこに各Embedレイヤで求めた値を足していきます。
Time Embedレイヤは、逆伝播において最後のレイヤであり次に伝播する必要がないため、このレイヤの入力に関する勾配を求める必要はありません。
# (結果が分かりやすいように)逆伝播の入力を作成 dout = np.ones((batch_size, time_size, wordvec_size)) # 繰り返し処理 dW = np.zeros_like(W) # 受け皿を初期化 for t in range(time_size): # t番目のレイヤを取得 layer = layers[t] # 逆伝播の計算 layer.backward(dout[:, t, :]) # 勾配を加算 dW += layer.grads[0] print(dW) print(dW.shape) print(xs)
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[2. 2. 2. 2. 2. 2. 2. 2. 2. 2.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
(7, 10)
[[0 1 2 3 4 1 5]]
dW
の形状は、W
と同じvocab_size
行wordvec_size
列になります。xs
の各要素(単語ID)に対応するdW
の行(語彙)に、逆伝播の入力dout
の値が加算されていることが分かります。詳しくは4.1.1項を参照してください。この勾配dW
を用いて、重みW
を更新します。
ここまでが逆伝播の処理です。
・実装
処理の確認ができたので、Time Embeddingレイヤをクラスとして実装します。
# Time Embeddingレイヤの実装 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): # Embedレイヤ(インスタンス)を作成 layer = Embedding(self.W) # 順伝播の計算 out[:, t, :] = layer.forward(xs[:, t]) # t番目のレイヤを格納 self.layers.append(layer) return out # 逆伝播メソッドの定義 def backward(self, dout): # 形状に関する値を取得 N, T, D = dout.shape # 繰り返し処理 dW = np.zeros_like(self.W) # 受け皿を初期化 for t in range(T): # t番目のレイヤを取得 layer = self.layers[t] # 逆伝播の計算 layer.backward(dout[:, t, :]) # 勾配を加算 dW += layer.grads[0] # 結果を格納 self.grads[0][...] = dW return None
実装したクラスを試してみましょう。
# Time Embedレイヤのインスタンスを作成 layer = TimeEmbedding(W) # 順伝播の計算 out = layer.forward(xs) print(np.round(out, 2)) print(out.shape) # 逆伝播の計算 layer.backward(dout) print(layer.grads[0]) print(layer.grads[0].shape)
[[[-0.23 0.79 -0.71 -0.27 1.75 1.11 0.86 -0.48 0.36 -2.33]
[-2.13 -1.13 -0.33 1.16 -0.08 1.33 -1.48 0.46 -1.72 -0.35]
[ 0.13 -1.21 2.36 1.83 0.08 -0.45 -0.43 -0.87 0.17 1.73]
[-1.24 1.46 0.49 0.04 0.89 -0.67 0.61 -0.08 1.35 -0.38]
[-0.5 -0.13 0.64 -0.35 -1.44 -0.66 0.56 0.9 -0.26 -0.02]
[-2.13 -1.13 -0.33 1.16 -0.08 1.33 -1.48 0.46 -1.72 -0.35]
[ 0.52 1.19 -1.47 0.65 2.11 -1.47 1.12 0.18 1.81 -0.13]]]
(1, 7, 10)
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[2. 2. 2. 2. 2. 2. 2. 2. 2. 2.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
(7, 10)
以上でTime Embeddingレイヤを実装できました。次項では、Time Affineレイヤを実装します。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.
おわりに
来年はもう少し分かりやすい解説文を書けるようになりたいなぁ。あでも、いつの間にか書くこと自体は苦じゃなくなってるのは成長です。
【次節の内容】