はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、8.1.5項「Decoderの改良③」の内容です。時系列データに対応したAttentionレイヤの処理を解説して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
8.1.5.2 Time Attentionレイヤの実装
$T$個のAttentionレイヤを組み合わせて時系列データに対応したTime Attentionレイヤを実装します。
# 8.1.5項で利用するライブラリ import numpy as np
・処理の確認
Time Attentionレイヤの処理を確認していきます。
・順伝播の計算
データとパラメータの形状に関する値を設定して、「Encoderの隠れ状態$\mathbf{hs}^{(\mathrm{Enc})} = (\mathbf{h}_0^{(\mathrm{Enc})}, \cdots, \mathbf{h}_{T-1}^{(\mathrm{Enc})})$」と「Decoderの隠れ状態$\mathbf{hs}^{(\mathrm{Dec})} = (\mathbf{h}_0^{(\mathrm{Dec})}, \cdots, \mathbf{h}_{T-1}^{(\mathrm{Dec})})$」を簡易的に作成しておきます。
# 変数の形状に関する値を指定 N = 3 # バッチサイズ(入力する文章数) T_enc = 4 # Encoderの時系列サイズ(入力する単語数) T_dec = 7 # Decoderの時系列サイズ(入力する単語数) H = 5 # 隠れ状態のサイズ(LSTMレイヤの中間層のニューロン数) # (簡易的に)EncoderのT個の隠れ状態を作成 hs_enc = np.random.randn(N, T_enc, H) print(hs_enc.shape) # (簡易的に)DecoderのT個の隠れ状態を作成 hs_dec = np.random.randn(N, T_dec, H) print(hs_dec.shape)
(3, 4, 5)
(3, 7, 5)
Attentionレイヤのインスタンスを作成して順伝播を計算します。計算結果をコンテキスト$\mathbf{cs} = (\mathbf{c}_0, \cdots, \mathbf{c}_{T-1})$とします。
順伝播メソッドの入力は、「Encoderの$T$個の隠れ状態hs_enc
」と「Decoderの$t$番目の隠れ状態hs_dec[:, t, :]
」です。出力をcs
に、使用したインスタンスはlayers
に格納していきます。また、インスタンス変数attention_weight
として保存されているAttentionの重みもattention_weights
に格納します。これをT_dec
回繰り返します。
# T個のAttentionレイヤの受け皿を初期化 layers = [] # T個のAttentionの重みの受け皿を初期化 attention_weights = [] # T個のコンテキストの受け皿を初期化 cs = np.empty((N, T_dec, H)) # Time Attentionレイヤの処理 for t in range(T_dec): # t番目のAttentionレイヤを作成 layer = Attention() # t番目のコンテキストを計算 cs[:, t, :] = layer.forward(hs_enc, hs_dec[:, t, :]) # t番目のAttentionレイヤを格納 layers.append(layer) # t番目のAttentionの重みを格納 attention_weights.append(layer.attention_weight)
$\mathbf{cs}$を確認します。
# T個のコンテキストを確認 print(np.round(cs[0], 2)) # 0番目のコンテキスト print(cs.shape)
[[ 0.54 -0.08 1.23 0.17 -0.95]
[ 0.31 -0.38 0.75 0.5 -1.69]
[ 0.59 0.48 -0.02 0.43 -0.25]
[ 0.48 -0.43 1.67 0.09 -1.33]
[ 0.54 -0.15 0.73 0.15 -0.5 ]
[ 0.48 -0.19 0.94 0.25 -1.02]
[ 0.74 1.12 0.12 0.47 -0.08]]
(3, 7, 5)
$T$個のAttentionレイヤが格納されたリストlayers
と$T$個のAttentionの重み$\mathbf{a}_t$を格納したリストattention_weights
を確認します。
# リストを確認 print(len(layers)) print(len(attention_weights)) # Attentionの重みを確認 print(np.round(attention_weights[0], 2)) print(np.sum(attention_weights[0], axis=1)) print(attention_weights[0].shape)
7
7
[[0.15 0.12 0.67 0.06]
[0.16 0.1 0.15 0.59]
[0.2 0.06 0.67 0.07]]
[1. 1. 1.]
(3, 4)
layers
には$T$個のインスタンス変数が、attention_weights
には$T$個のNumPy配列が格納されています。
以上が順伝播の処理です。続いて、逆伝播の処理を確認します。
・逆伝播の計算
Time Affineレイヤからコンテキストの勾配$\frac{\partial L}{\partial \mathbf{cs}} = \Bigl( \frac{\partial L}{\partial \mathbf{c}_0}, \cdots, \frac{\partial L}{\partial \mathbf{c}_{T-1}} \Bigr)$が入力します。ここでは、$\frac{\partial L}{\partial \mathbf{cs}}$を簡易的に作成します。
# (簡易的に)T個のコンテキストの勾配を作成 dcs = np.ones((N, T_dec, H)) print(dcs.shape)
(3, 7, 5)
layers
から1つずつレイヤを取り出して、逆伝播を計算します。各レイヤの出力は、「Encoderの$T$個の隠れ状態の勾配$\frac{\partial L}{\partial \mathbf{hs}^{(\mathrm{Enc})}}$」と「Encoderの$t$番目の隠れ状態の勾配$\frac{\partial L}{\partial \mathbf{h}_t^{(\mathrm{Dec})}}$」です。それぞれdhs
、dh
とします。
$\mathbf{hs}^{(\mathrm{Enc})}$は分岐して全てのAttentionレイヤに入力したので、各Attentionレイヤで求まるdhs
をdhs_enc
に足し合わせていきます(Repeatノード)。$\mathbf{hs}^{(\mathrm{Dec})}$はそれぞれ同じ時刻のAttentionレイヤに入力したので、各Attentionレイヤで求まるdh
をdhs_dec
に格納していきます。
# EncoderのT個の隠れ状態の勾配を初期化 dhs_enc = 0 # DecoderのT個の隠れ状態の勾配を初期化 dhs_dec = np.empty_like(dcs) # Time Attentionレイヤの処理 for t in range(T_dec): # t番目のAttentionレイヤを取得 layer = layers[t] # EncoderとDecoderの隠れ状態の勾配を計算 dhs, dh = layer.backward(dcs[:, t, :]) # EncoderのT個の隠れ状態の勾配を加算 dhs_enc += dhs # Decoderのt番目の隠れ状態の勾配を格納 dhs_dec[:, t, :] = dh
処理に影響しないため、layers
内のレイヤを逆順に処理する必要はありません。
$\frac{\partial L}{\partial \mathbf{hs}^{(\mathrm{Enc})}}$と$\frac{\partial L}{\partial \mathbf{hs}^{(\mathrm{Dec})}}$を確認します。
# EncoderのT個の隠れ状態の勾配を確認 print(dhs_enc.shape) # DecoderのT個の隠れ状態の勾配を確認 print(dhs_dec.shape)
(3, 4, 5)
(3, 7, 5)
以上がTime Attentionレイヤの処理です。
・実装
処理の確認ができたので、Time Attentionレイヤをクラスとして実装します。
# Time Attentionレイヤの実装 class TimeAttention: # 初期化メソッド def __init__(self): # 他のレイヤに対応するための空のリストを作成 self.params = [] # パラメータ self.grads = [] # 勾配 # Attentionの重みの受け皿を初期化 self.attention_weights = None # 順伝播メソッド def forward(self, hs_enc, hs_dec): # 変数の形状に関する値を取得 N, T, H = hs_dec.shape # T個のコンテキストの受け皿を初期化 out = np.empty_like(hs_dec) # 受け皿を初期化 self.layers = [] self.attention_weights = [] # Attentionレイヤごとに処理 for t in range(T): # t番目のAttentionレイヤを作成 layer = Attention() # t番目のコンテキスト(隠れ状態の重み付き和)を計算 out[:, t, :] = layer.forward(hs_enc, hs_dec[:, t, :]) # t番目のレイヤとAttentionの重みを格納 self.layers.append(layer) self.attention_weights.append(layer.attention_weight) return out # 逆伝播メソッド def backward(self, dout): # 変数の形状に関する値を取得 N, T, H = dout.shape # EncoderとDecoderの隠れ状態の勾配を初期化 dhs_enc = 0 dhs_dec = np.empty_like(dout) # Attentionレイヤごとに処理 for t in range(T): # t番目のAttentionレイヤを取得 layer = self.layers[t] # EncoderとDecoderの隠れ状態の勾配を計算 dhs, dh = layer.backward(dout[:, t, :]) # EncoderのT個の隠れ状態の勾配を加算 dhs_enc += dhs # Decoderのt番目の隠れ状態の勾配を格納 dhs_dec[:, t, :] = dh return dhs_enc, dhs_dec
$T$個分のAttentionの重みを格納するインスタンス変数self.attention_weights
は、変数名の最後にs
が付いているのに注意してください。
実装したクラスを試してみましょう。
Encoderの隠れ状態$\mathbf{hs}^{(\mathrm{Enc})}$とDecoderの隠れ状態$\mathbf{hs}^{(\mathrm{Dec})}$を簡易的に作成して、Time Attentionレイヤのインスタンスを作成します。
# (簡易的に)EncoderのT個の隠れ状態を作成 hs_enc = np.random.randn(N, T_enc, H) print(hs_enc.shape) # (簡易的に)Decoderの隠れ状態を作成 hs_enc = np.random.randn(N, T_dec, H) print(hs_enc.shape) # インスタンスを作成 time_attention_layer = TimeAttention()
(3, 4, 5)
(3, 7, 5)
順伝播を計算します。
# T個のコンテキストを計算 cs = time_attention_layer.forward(hs_enc, hs_enc) print(np.round(cs, 2)) print(cs.shape)
[[[-1.47 -0.71 -1.7 -1.03 0.93]
[-2.17 0.52 0.48 1.49 -0.47]
[-0.86 -0.63 -0.74 -0.67 0.27]
[-1.11 -0.22 -0.26 -0.12 -0.6 ]
[ 0.68 -2.36 1.59 1.36 2.16]
[-0.17 2.91 1.69 1.5 -0.24]
[ 0.04 1.51 0.89 0.88 -0.42]]
[[ 1.39 -0.5 0.47 -0.43 1.2 ]
[ 0.56 1.04 0.12 0.32 0.17]
[-1.3 1.04 -1.21 -1.89 0.32]
[-1.61 1.04 -1.55 -1.92 0.43]
[ 1.24 1.33 -0.64 -0.1 -1.03]
[ 0.44 0.85 0.61 1.64 -0.1 ]
[ 0.52 1.11 0.71 1.87 -0.12]]
[[ 1.02 0.81 1.08 -0.37 -1.28]
[ 0.19 0.5 -0.74 -0.31 -0.43]
[-1.06 0.26 -0.91 -0.45 -0.7 ]
[ 2.24 1.04 -0.46 0.95 0.96]
[-0.47 0.09 -1.02 0.27 -0.86]
[ 0.72 0.97 -0.49 -0.76 0.12]
[-0.76 0.31 -0.9 -0.19 -0.7 ]]]
(3, 7, 5)
この出力はTime Affineレイヤに入力します。
インスタンス変数として保存されている$T$個のAttentionの重みを確認します。
# Attentionの重みを取得 attention_weights = time_attention_layer.attention_weights print(np.round(attention_weights[0], 2)) # 0番目のAttentionの重み print(np.sum(attention_weights[0], axis=1)) print(np.array(attention_weights[0]).shape)
[[0.99 0. 0. 0. 0. 0. 0. ]
[0.95 0.03 0.01 0. 0.01 0.01 0.01]
[0.95 0.01 0. 0.02 0. 0.02 0. ]]
[1. 1. 1.]
(3, 7)
2次元方向の和が1になっているのを確認できました。
$T$個のコンテキストの勾配(Time Affineレイヤの出力)$\frac{\partial L}{\partial \mathbf{cs}}$を簡易的に作成して、逆伝播を計算します。
# (簡易的に)逆伝播の入力を作成 dcs = np.random.randn(N, T_dec, H) print(dcs.shape) # 逆伝播を計算 dhs_enc, dhs_dec = time_attention_layer.backward(dcs) print(dhs_enc.shape) print(dhs_dec.shape)
(3, 7, 5)
(3, 7, 5)
(3, 7, 5)
dhs_enc
はEncoderのTime LSTMレイヤに、dhs_dec
はDecoderのTime LSTMレイヤに入力します。
8.1節では、Time Attentionレイヤを実装しました。8.2節では、Attention付きのseq2seqを実装します。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.
おわりに
これで8章のメインパーツが完成ですね。あとちょっと!
【次節の内容】