はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、7.3.1項「Encoderクラス」の内容です。seq2seqの入力側のRNNであるEncoderの処理を解説して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
7.3.1 Encoderクラス
入力側のRNNであるEncoder(エンコーダー)を実装します。Encoderは、時系列データを入力して隠れ状態にエンコードします。
# 7.3.1項で利用するライブラリ import numpy as np
Encoderの実装には、RNNで用いるレイヤを利用します。そのため、各レイヤのクラス定義を再実行するか、次の方法で実装済みのクラスを読み込む必要があります。各レイヤのクラスは、「common」フォルダ内の「time_layers.py」ファイルに実装されています。各レイヤについては、5.4.2.1項、6.3.1項を参照してください。
# 実装済みクラスの読み込み用の設定 import sys sys.path.append('C://Users//「ユーザー名」//Documents//・・・//deep-learning-from-scratch-2-master') # 実装済みのレイヤを読み込み from common.time_layers import TimeEmbedding # 5.4.2.1項 from common.time_layers import TimeLSTM # 6.3.1項
「deep-learning-from-scratch-2-master」フォルダにパスを設定しておく必要があります。
・処理の確認
図7-6・7-9を参考にして、Encoderで行う処理を確認していきます。
・ネットワークの設定
まずは、RNNを構築します。
データとパラメータの形状に関する値を設定して、入力データを簡易的に作成します。
# データとパラメータの形状に関する値を指定 N = 6 # バッチサイズ T = 7 # 時系列サイズ V = 13 # 単語の種類数 D = 15 # 単語ベクトル(Embedレイヤの中間層)のサイズ H = 10 # 隠れ状態(LSTMレイヤの中間層)のサイズ # (簡易的に)入力データを作成 xs = np.random.randint(low=0, high=V, size=(N, T)) print(xs) print(xs.shape)
[[ 5 2 9 4 5 6 12]
[ 4 5 12 5 6 5 5]
[12 2 7 5 12 0 3]
[ 8 3 9 3 12 12 4]
[ 3 9 7 3 10 4 6]
[ 9 1 1 6 1 4 8]]
(6, 7)
Encoderの入力データは、Tine Embedレイヤの入力$\mathbf{xs} = (x_{0,0}, \cdots, x_{N-1,T-1})$です。$n$番目のバッチデータの時刻$t$の入力データ$x_{n,t}$は、単語の種類(語彙)を示す単語IDです。単語IDは、語彙数を$V$として0から$V-1$の整数です。
足し算データセットの場合は、文字の種類を示す文字IDであり$V = 13$です。また、Encoder側の時系列サイズは$T = 7$です。
各レイヤの重みとバイアスの初期値をランダムに生成します。
# Time Embeddingレイヤのパラメータを初期化 embed_W = (np.random.randn(V, D) * 0.01) # Time LSTMレイヤのパラメータを初期化 lstm_Wx = (np.random.randn(D, 4 * H) / np.sqrt(D)) lstm_Wh = (np.random.randn(H, 4 * H) / np.sqrt(H)) lstm_b = np.zeros(4 * H)
各パラメータの形状については各レイヤの記事を、初期値の設定については1巻の6.2節を参照してください。
作成したパラメータを渡して、各レイヤのインスタンスを作成します。
# Time Embeddingレイヤのインスタンスを作成 embed_layer = TimeEmbedding(embed_W) # Time LSTMレイヤのインスタンスを作成 lstm_layer = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False)
以上でRNNを構築できました。次は順伝播の処理を確認します。
・順伝播の計算
各レイヤの順伝播を計算します。
# Time Embeddingレイヤの順伝播を計算 xs = embed_layer.forward(xs) print(xs.shape) # Time LSTMレイヤの順伝播を計算 hs = lstm_layer.forward(xs) print(hs.shape) # T-1番目の隠れ状態をDecoderに出力 h = hs[:, -1, :] print(np.round(h, 3)) print(h.shape)
(6, 7, 15)
(6, 7, 10)
[[-0.003 -0.001 0.005 -0.001 -0.005 0.001 0.002 0.004 0.001 0.008]
[-0.003 0.001 0.004 0.006 0.002 0.001 0.004 0.001 0.001 0.001]
[-0.003 -0.001 -0.002 -0.004 0.001 -0.001 -0.004 0.001 -0.002 -0.004]
[ 0. -0.002 0.001 0.001 -0.004 -0.002 -0.004 0.002 0.002 -0.001]
[-0.004 0.001 0.004 -0.002 0.004 0.002 -0.001 -0.002 -0.001 -0. ]
[-0.002 0.002 0.001 -0.001 0.001 0.002 0.003 -0.002 -0.001 -0.004]]
(6, 10)
Time Embedレイヤの出力は、単語ベクトル(の集合)$\mathbf{xs} = (x_{0,0,0}, \cdots, x_{N-1,T-1,D-1})$です。
Time LSTMレイヤの出力は、隠れ状態(の集合)$\mathbf{hs} = (h_{0,0,0}, \cdots, h_{N-1,T-1,H-1})$です。
「0から数えてT番目(最後の時刻)の隠れ状態$\mathbf{h}_{T-1} = (h_{0,0}^{(T-1)}, \cdots, h_{N-1,H-1}^{(T-1)})$」を「Decoderの0番目のLSTMレイヤ」に入力します。
以上が順伝播の処理です。続いて、逆伝播の処理を確認します。
・逆伝播の計算
「Decoderの0番目のLSTMレイヤ」から「Encoderの$T - 1$番目(最後の時刻)のLSTMレイヤ」に$\mathbf{h}_{T-1}$の勾配$\frac{\partial L}{\partial \mathbf{h}_{T-1}} = \Bigl( \frac{\partial L}{\partial h_{0,0}^{(T-1)}}, \cdots, \frac{\partial L}{\partial h_{N-1,H-1}^{(T-1)}} \Bigr)$が入力します。
$\mathbf{hs}$の勾配$\frac{\partial L}{\partial \mathbf{hs}} = \Bigl( \frac{\partial L}{\partial \mathbf{h}_0}, \cdots, \frac{\partial L}{\partial \mathbf{h}_{T-1}} \Bigr)$を全ての要素を0として作成して、入力した$\frac{\partial L}{\partial \mathbf{h}_{T-1}}$を代入します。ここでは、$\frac{\partial L}{\partial \mathbf{h}_{T-1}}$を簡易的に作成します。
# (簡易的に)Decoderからの入力する隠れ状態の勾配を作成 dh = np.ones((N, H)) print(dh.shape) # 隠れ状態を初期化 dhs = np.zeros_like(hs) dhs[:, -1, :] = dh print(dhs.shape)
(6, 10)
(6, 7, 10)
代入時のスライスに最後の要素を表す-1
を指定しています。値を明示的に示すのであればT-1
です。
各レイヤの逆伝播を逆順に計算します。
# Time LSTMレイヤの逆伝播を計算 dout = lstm_layer.backward(dhs) print(dout.shape) # Time Embeddingレイヤの逆伝播を計算 dout = embed_layer.backward(dout) print(dout)
(6, 7, 15)
None
Time LSTMレイヤの出力は、単語ベクトルの勾配$\frac{\partial L}{\partial \mathbf{xs}} = \Bigl( \frac{\partial L}{\partial x_{0,0,0}}, \cdots, \frac{\partial L}{\partial x_{N-1,T-1,D-1}} \Bigr)$です。
Time Embedレイヤより先はないので、None
を返します。入力データの勾配は返しません。
以上がEncoderで行う処理です。
・実装
処理の確認ができたので、Encoderをクラスとして実装します。
# エンコーダーの実装 class Encoder: # 初期化メソッド 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) * 0.01).astype('f') lstm_Wx = (np.random.randn(D, 4 * H) / np.sqrt(D)).astype('f') lstm_Wh = (np.random.randn(H, 4 * H) / np.sqrt(H)).astype('f') lstm_b = np.zeros(4 * H).astype('f') # レイヤのインスタンスを作成 self.embed = TimeEmbedding(embed_W) self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False) # パラメータと勾配をリストに格納 self.params = self.embed.params + self.lstm.params # パラメータ self.grads = self.embed.grads + self.lstm.grads # 勾配 # LSTMレイヤの中間変数を初期化 self.hs = None # 順伝播メソッド def forward(self, xs): # 各レイヤの順伝播を計算 xs = self.embed.forward(xs) hs = self.lstm.forward(xs) # 逆伝播用に隠れ状態を保存 self.hs = hs # T-1番目の隠れ状態をDecoderに出力 return hs[:, -1, :] # 逆伝播メソッド def backward(self, dh): # 隠れ状態を初期化 dhs = np.zeros_like(self.hs) dhs[:, -1, :] = dh # Decoderから入力 # 各レイヤの逆伝播を逆順に計算 dout = self.lstm.backward(dhs) dout = self.embed.backward(dout) return dout
TimeEmbed
の逆伝播メソッドbackward()
はNone
を返します。よって、Encoder
の逆伝播メソッドbackward()
もNone
を返します。
実装したクラスを試してみましょう。
簡易的な入力データ$\mathbf{xs}$と、Encoderのインスタンスを作成します。
# (簡易的に)入力データを作成 xs = np.random.randint(low=0, high=V, size=(N, T)) print(xs) print(xs.shape) # Encoderのインスタンスを作成 encoder = Encoder(V, D, H)
[[ 9 9 7 2 1 7 11]
[ 5 9 1 5 0 8 3]
[ 0 0 6 11 9 11 4]
[ 3 8 3 2 3 6 1]
[11 3 6 9 2 12 2]
[ 4 12 6 2 4 12 11]]
(6, 7)
順伝播を計算します。
# 順伝播を計算 h = encoder.forward(xs) print(np.round(h, 3)) print(h.shape)
[[-0.001 -0.011 -0.003 -0.002 0.004 -0.001 0.004 -0.003 0. -0.001]
[ 0.007 -0.001 -0.006 -0.001 -0.002 0.001 0.005 -0.005 0. 0. ]
[-0.004 -0.005 -0.004 -0.001 0. -0.001 0.001 -0. -0. -0.002]
[ 0.002 -0.004 -0.002 0.003 0.002 -0. 0.005 0.001 0. -0. ]
[ 0.002 -0.004 -0. -0.001 0.001 0.003 0.004 0.008 -0. -0.003]
[ 0.001 -0.007 0. -0.003 -0.001 -0.001 0.003 0.001 0.001 0.001]]
(6, 10)
入力データ$\mathbf{xs}$が、パラメータによって重み付けされて隠れ状態$\mathbf{h}_{T-1}$にエンコードされました。
逆伝播の入力(Decoderの出力)を簡易的に作成して、逆伝播の計算を行います。
# (簡易的に)Decoderからの入力する隠れ状態の勾配を作成 dh = np.ones((N, H)) print(dh.shape) # 逆伝播を計算 dout = encoder.backward(dh) print(dout)
(6, 10)
None
インスタンス内に各レイヤのパラメータの勾配が保存されます。確率的勾配降下法により、それぞれ勾配を用いてパラメータを更新します。
以上でEncoderを実装できました。次項では、Decoderを実装します。
参考文献
おわりに
seq2seqです。翻訳とかのヤツらしいくらいの認識でしたが、やってみると面白いですね。
ところで、内容のない目次ページなどを除くとこの記事が201記事目だと思います。 2年と3か月半で200記事を越えました!漠然とした目標の年100記事ペースは下回ってますが、中々なのでは?まぁ暇なだけなんですがね。
【次節の内容】