はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、6.2.7項「LSTMの勾配の流れ」の内容です。LSTMレイヤの逆伝播を解説して、Pythonで再現します。
【前節の内容】
【他の節の内容】
【この節の内容】
6.2.7 LSTMの勾配の流れ
前項では、LSTMレイヤにおける順伝播の計算を解説しました。この項では、逆伝播の計算を解説します。再利用するオブジェクトなど、前回の記事の内容が実行されていることを想定しています。
各LSTMレイヤの順伝播の入力とパラメータに関する勾配を求めていきます。図6-18を参考に、順伝播のときと(概ね)逆順に説明します。
・逆伝播の入力(出力データ$\mathbf{h}_t$の勾配)
LSTMレイヤの順伝播では、隠れ状態$\mathbf{h}_t$と記憶セル$\mathbf{c}_t$の2つのデータを出力するのでした。よって逆伝播では、2つのデータが入力します。より深く伝播した$\mathbf{h}_t$に関する勾配$\frac{\partial L}{\partial \mathbf{h}_t}$から考えましょう。
$\frac{\partial L}{\partial \mathbf{h}_t}$は、「損失$L$(の計算式)」を「次のレイヤの入力$\mathbf{h}_t$」で微分したもので、「次のLSTMレイヤ」から「Lossレイヤ」までの全てのレイヤの微分の積でした(1.3.3項「チェインルール」または1巻の5.2節「連鎖律」)。
$\frac{\partial L}{\partial \mathbf{h}_t}$は既に求まっているものなので、ここでは簡易的に作成します。
# (簡易的に)逆伝播の入力(隠れ状態の勾配)を作成 dh_next = np.random.rand(*h_next.shape) * 2 print(np.round(dh_next, 2)) print(dh_next.shape)
[[1.86 1.83 1.01 1.71 0.58 1.69 0.51 1.1 0.64 0.58]
[1.21 0.53 1.38 1.97 1.79 0.26 0.03 1.03 1.76 1.42]
[1.72 1.31 0.78 0.68 1.42 1.54 1.65 0.39 0.62 0.05]
[0.42 0.85 1.6 1.87 1.6 1.27 1.13 0.35 1.45 0.62]
[1.31 1.08 0.98 1.43 0.28 0.97 0.21 0.81 1.02 1.63]]
(5, 10)
$\frac{\partial L}{\partial \mathbf{h}_t}$は、outputゲートに逆伝播します。
・outputゲート$\mathbf{o}$の勾配
outputゲート(乗算ノード)の順伝播では、次の計算をしました。
ここで$\odot$は要素ごとの積を表します。簡単に考えるために1つの要素に注目すると、$h_{nth}$は次の式で計算しています。
記憶セル$\mathbf{c}_t$をtanh関数によって活性化したものを$\mathbf{c}_t^{\mathrm{tanh}}$と表記することにします(この項でのみ用いる表記です)。乗算ノードの順伝播の入力は、$o_{nh}$と$c_{tnh}^{\mathrm{tanh}}$です。
逆伝播では、そのレイヤ(またはノード)の「順伝播の出力を入力で微分した値」と「逆伝播の入力」との積を求めるのでした。2つの入力の勾配はそれぞれ
で計算できます。他の要素も同様に計算できるので、$\mathbf{o}$と$\mathbf{c}_t^{\mathrm{tanh}}$の勾配は次の式で求められます。
$\frac{\partial L}{\partial \mathbf{c}_t^{\mathrm{tanh}}}$はtanhノードに、$\frac{\partial L}{\partial \mathbf{o}}$は$\mathbf{o}$を計算するsigmoidノードに伝播(入力)します。
# 活性化した記憶セルを保存 tanh_c_next = np.tanh(c_next) # 勾配を計算 dtanh_c_next = dh_next * o do = dh_next * tanh_c_next print(dtanh_c_next.shape) print(do.shape)
(5, 10)
(5, 10)
まずは、$\mathbf{c}_t$の勾配$\frac{\partial L}{\partial \mathbf{c}_t^{\mathrm{tanh}}}$についてさらに考えます。
tanhノードの順伝播では、$\mathbf{c}_t$を入力して$\mathbf{c}_t^{\mathrm{tanh}}$を出力します。
tanh関数の微分は、付録A.2節より次の式で計算できます。
よって逆伝播の入力$\frac{\partial L}{\partial \mathbf{c}_t^{\mathrm{tanh}}}$との積が、$\mathbf{c}_t$の勾配$\frac{\partial L}{\partial \mathbf{c}_t}$です。
$\frac{\partial L}{\partial \mathbf{c}_t}$は、もう1つの$\mathbf{c}_t$の勾配$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$に加算されます。
# tanh関数の微分 dc_next = dtanh_c_next * (1 - tanh_c_next**2) print(dc_next.shape)
(5, 10)
次に、$\mathbf{o}$の勾配$\frac{\partial L}{\partial \mathbf{o}}$をさらに考えます。
$\mathbf{o}$は、outputゲートの計算の出力でした。
この式を「sigmoid関数の計算」と「重み付き和の計算」に分けます。
「sigmoidノード」の逆伝播は、付録のA.1節より次の式で計算できます。
# 重み付き和の勾配を計算 da_o = do * o * (1 - o) print(da_o.shape)
(5, 10)
「Affine(重み付き和)ノード」の各入力の勾配は、$\frac{\partial L}{\partial \mathbf{a}^{(\mathbf{o})}}$を逆伝播の入力として次の式になります。詳しくは5.3.1項「RNNの実装」や1巻5.6.2項「バッチ版Affineレイヤ」を参考にしてください。
パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}_{\mathbf{x}}^{(\mathbf{o})}},\ \frac{\partial L}{\partial \mathbf{W}_{\mathbf{h}}^{(\mathbf{o})}},\ \frac{\partial L}{\partial \mathbf{b}^{(\mathbf{o})}}$は、それぞれパラメータの更新に用います。入力の勾配$\frac{\partial L}{\partial \mathbf{x}_t},\ \frac{\partial L}{\partial \mathbf{h}_{t-1}}$は、他で求めた勾配と加算されます。
# 各変数の勾配を計算 dx_o = np.dot(da_o, Wx_o.T) dWx_o = np.dot(x.T, da_o) dh_prev_o = np.dot(da_o, Wh_o.T) dWh_o = np.dot(h_prev.T, da_o) db_o = np.sum(da_o, axis=0) print(dx_o.shape) print(dWx_o.shape) print(dh_prev_o.shape) print(dWh_o.shape) print(db_o.shape)
(5, 12)
(12, 10)
(5, 10)
(10, 10)
(10,)
次は、記憶セルの勾配$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$の逆伝播をさらに考えます。
・逆伝播の入力(記憶セル$\mathbf{c}_t$の勾配)
LSTMレイヤの逆伝播におけるもう1つの入力が$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$です。これは、「最後($T-1$番目)のLSTMレイヤ」から「$t+1$番目のLSTMレイヤ」までの各レイヤの微分の積$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t} = \frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_{T-2}} \frac{\partial \mathbf{c}_{T-2}}{\partial \mathbf{c}_{T-3}} \cdots \frac{\partial \mathbf{c}_{t+1}}{\partial \mathbf{c}_t}$です。
$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$と「Lossレイヤから伝播してきた記憶セルの勾配$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$」との和をとります。
両辺を$\leftarrow$で繋いでいるのは、$\frac{\partial L}{\partial \mathbf{c}_t} = \frac{\partial L}{\partial \mathbf{c}_t} + \frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$とすると$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$が0でないと式が成り立たなくなるためです(それ以上の意味はありません)。
$\frac{\partial \mathbf{c}_{T-1}}{\partial \mathbf{c}_t}$も簡易的に作成して、計算します。
# (簡易的に)逆伝播の入力(記憶セルの勾配)を作成 dc_next_T = np.random.rand(*c_next.shape) * 2 # 記憶セルの勾配(分岐ノードの逆伝播)を計算 dc_next += dc_next_T print(np.round(dc_next, 2)) print(dc_next.shape)
[[1.24 1.46 1.73 1.07 0.69 1.48 0.57 1.19 0.63 1.33]
[1.06 0.55 0.12 1.5 2.19 0.48 1.16 1.25 1.04 1.5 ]
[2.41 1.62 0.31 0.54 1.89 2.35 3.29 1.44 0.16 1.89]
[0.36 0.96 1.37 2.28 1.4 1.62 1.62 0.47 1.76 1.25]
[0.94 1.08 1.64 1.56 2.17 1.2 1.64 2.11 0.19 2.27]]
(5, 10)
「このレイヤを出力する記憶セル$\mathbf{c}_t$」の勾配$\frac{\partial L}{\partial \mathbf{c}_t}$が得られたので、次は「このレイヤに入力する記憶セル$\mathbf{c}_{t-1}$」の勾配$\frac{\partial L}{\partial \mathbf{c}_{t-1}}$を求めます。
・記憶セル$\mathbf{c}_{t-1}$の勾配
$t$番目のLSTMレイヤの順伝播において、$t - 1$番目のレイヤで求めた記憶セル$\mathbf{c}_{t-1}$を入力として、$\mathbf{c}_t$を出力します。前項で行った計算をまとめると、$t$番目の記憶セル$\mathbf{c}_t$は次の式で計算します。
ここで$\mathbf{f}$はforgetゲートの出力、$\mathbf{i}$はinputゲートの出力、$\mathbf{g}$は$\mathbf{x}_t,\ \mathbf{h}_{t-1}$から抽出した新たな情報です。
この計算も1つの要素に注目して考えてみましょう。$c_{t-1nh}$の計算は
です。出力$c_{tnh}$(の計算式)を入力$c_{t-1nh}$で微分すると
となります。行列の積ではなく要素ごとの積の微分なので、他の要素についても同様に計算できます。よってこのノードの$\mathbf{c}_{t-1}$に関する微分は
となります。
$\frac{\partial \mathbf{c}_t}{\partial \mathbf{c}_{t-1}}$と「このノードの逆伝播の入力$\frac{\partial L}{\partial \mathbf{c}_t}$」との積が、$\mathbf{c}_{t-1}$の勾配$\frac{\partial L}{\partial \mathbf{c}_{t-1}}$になります。
# t-1番目のレイヤの記憶セルの勾配を計算 dc_prev = dc_next * f print(np.round(f, 2)) print(np.round(dc_prev, 2))
[[0.01 0.88 0.28 0.03 0.65 0.98 0.18 1. 0.44 0.93]
[0.01 0.27 0. 0.74 0.89 0.98 0.11 1. 0.66 0.8 ]
[0.01 0.96 0.39 0.65 0.35 0.98 0.52 1. 0.1 0.99]
[0. 0.91 0.48 0. 0.56 1. 0.69 1. 0.04 0.98]
[0. 1. 0.78 0. 0.15 1. 0.47 1. 0.3 0.87]]
[[0.01 1.29 0.49 0.04 0.45 1.45 0.1 1.18 0.28 1.25]
[0.01 0.15 0. 1.12 1.95 0.46 0.13 1.25 0.68 1.21]
[0.02 1.55 0.12 0.35 0.65 2.31 1.72 1.44 0.02 1.86]
[0. 0.87 0.65 0. 0.78 1.61 1.12 0.47 0.07 1.23]
[0. 1.08 1.28 0. 0.32 1.2 0.77 2.11 0.06 1.97]]
ところで、$\mathbf{f}$は情報の抽出率で、過去の記憶$\mathbf{c}_{t-1}$に掛けることで新たな記憶$\mathbf{c}_t$として残す情報を抽出するのでした。
逆伝播においても同様に、これまでの勾配情報$\frac{\partial L}{\partial \mathbf{c}_{t-1}}$に掛けることで、伝播する勾配情報を抽出する働きをしているのが分かります。また各要素の抽出する割合が、順伝播と逆伝播で同じなのも分かります。
他のゲートでも同じ働きをしています。
式(6.7)には、他の変数も入力していますね。各変数の勾配も、それぞれ同様にして計算できます。
それぞれ各ゲートの抽出率を計算するノード(の活性化関数)に伝播します。
# 各ゲートの出力の勾配を計算 df = dc_next * c_prev dg = dc_next * i di = dc_next * g print(df.shape) print(dg.shape) print(di.shape)
(5, 10)
(5, 10)
(5, 10)
以上で記憶セルの勾配が求まりました。次は、各ゲートの出力の勾配についてさらに考えます。
・forgetゲート$\mathbf{f}$の勾配
outputゲートのときと同様に、式(6.1)の計算を$\mathbf{f} = \sigma(\mathbf{a}^{(\mathbf{f})})$として、$\mathbf{a}^{(\mathbf{f})}$をforgetゲートの重み付き和とします。
forgetゲートのsigmoidノードには、$\frac{\partial L}{\partial \mathbf{f}}$が逆伝播してきます。また、forgetゲートのsigmoid関数の微分は
です。よって$\frac{\partial L}{\partial \mathbf{f}}$と$\frac{\partial \mathbf{f}}{\partial \mathbf{a}^{(\mathbf{f})}}$の積
が、「Affine(重み付き和)ノード」に伝播します。$\frac{\partial L}{\partial \mathbf{a}^{(\mathbf{f})}}$を逆伝播の入力として、各変数の勾配はそれぞれ次の式で計算します。
パラメータの勾配$\frac{\partial L}{\partial \mathbf{W}_{\mathbf{x}}^{(\mathbf{f})}},\ \frac{\partial L}{\partial \mathbf{W}_{\mathbf{h}}^{(\mathbf{f})}},\ \frac{\partial L}{\partial b_{h}^{(\mathbf{f})}}$はそれぞれパラメータの更新に用い、入力の勾配$\frac{\partial L}{\partial \mathbf{x}_t},\ \frac{\partial L}{\partial \mathbf{h}_{t-1}}$は他で求めた勾配と加算されます。
# 重み付き和の勾配を計算 da_f = dc_next * f * (1 - f) # 各変数の勾配を計算 dx_f = np.dot(da_f, Wx_f.T) dWx_f = np.dot(x.T, da_f) dh_prev_f = np.dot(da_f, Wh_f.T) dWh_f = np.dot(h_prev.T, da_f) db_f = np.sum(da_f, axis=0)
forgetゲートの逆伝播を計算しました。次は、同様にinputゲートの逆伝播を計算します。
・inputゲート$\mathbf{i}$の勾配
inputゲートについても同様に、式(6.5)の計算を$\mathbf{i} = \sigma(\mathbf{a}^{(\mathbf{i})})$、重み付き和を$\mathbf{a}^{(\mathbf{i})}$とおきます。
式(6.5)の逆伝播の入力$\frac{\partial L}{\partial \mathbf{i}}$と、inputゲートのsigmoid関数の微分$\frac{\partial \mathbf{i}}{\partial \mathbf{a}^{(\mathbf{i})}}$との積
が、Affine(重み付き和)ノードの入力です。よって各変数の勾配はそれぞれ次の式です。
$\frac{\partial L}{\partial \mathbf{W}_{\mathbf{x}}^{(\mathbf{i})}},\ \frac{\partial L}{\partial \mathbf{W}_{\mathbf{h}}^{(\mathbf{i})}},\ \frac{\partial L}{\partial b_{h}^{(\mathbf{i})}}$はパラメータの更新に用い、$\frac{\partial L}{\partial \mathbf{x}_t},\ \frac{\partial L}{\partial \mathbf{h}_{t-1}}$は他で求めた勾配と加算されます。
# 重み付き和の勾配を計算 da_i = dc_next * i * (1 - i) # 各変数の勾配を計算 dx_i = np.dot(da_i, Wx_i.T) dWx_i = np.dot(x.T, da_i) dh_prev_i = np.dot(da_i, Wh_i.T) dWh_i = np.dot(h_prev.T, da_i) db_i = np.sum(da_i, axis=0)
・新しい情報$\mathbf{g}$の勾配
新しい情報$\mathbf{g}$の計算は、tanh関数を使うところが3つのゲートと異なります。
式(6.4)の計算を$\mathbf{g} = \mathrm{tanh}(\mathbf{a}^{(\mathbf{g})})$とおくと、tanhノードの入力$\frac{\partial L}{\partial \mathbf{g}}$と、微分$\frac{\partial \mathbf{g}}{\partial \mathbf{a}^{(\mathbf{g})}}$との積は
です。$\frac{\partial L}{\partial \mathbf{a}^{(\mathbf{g})}}$をAffine(重み付き和)ノードの入力として、各変数の勾配はそれぞれ次の式です。
$\frac{\partial L}{\partial \mathbf{W}_{\mathbf{x}}^{(\mathbf{g})}},\ \frac{\partial L}{\partial \mathbf{W}_{\mathbf{h}}^{(\mathbf{g})}},\ \frac{\partial L}{\partial b_{h}^{(\mathbf{g})}}$はパラメータの更新に用い、$\frac{\partial L}{\partial \mathbf{x}_t},\ \frac{\partial L}{\partial \mathbf{h}_{t-1}}$は他で求めた勾配と加算されます。
# 重み付き和の勾配を計算 da_g = dc_next * (1 - g**2) # 各変数の勾配を計算 dx_g = np.dot(da_g, Wx_g.T) dWx_g = np.dot(x.T, da_g) dh_prev_g = np.dot(da_g, Wh_g.T) dWh_g = np.dot(h_prev.T, da_g) db_g = np.sum(da_g, axis=0)
次からは、LSTMレイヤの順伝播における入力の勾配を求めます。
・隠れ状態$\mathbf{h}_{t-1}$の勾配
1つ前のLSTMレイヤの隠れ状態$\mathbf{h}_{t-1}$は、forgetゲート、記憶セル、inputゲート、outputゲートに分岐して入力しています。この4つのノードで計算(出力)される勾配を全て足す必要があります。
# 1つ前の隠れ状態の勾配を計算 dh_prev = dh_prev_f + dh_prev_g + dh_prev_i + dh_prev_o print(np.round(dh_prev, 2)) print(dh_prev.shape)
[[ 0.23 0.02 -0.21 -0.56 0.3 -1.22 -0.09 1.13 1.15 0.79]
[-0.79 -2.76 1.72 2.79 -2.1 -1.19 1.9 -0.13 -0.93 -0.1 ]
[-0.13 -0.23 1.05 -0.42 -0.49 -0.47 0.52 0.5 2.06 0.41]
[-0.42 -2.56 0.51 1.2 -0.22 -1.46 0.37 0.22 0.86 0.34]
[ 0.31 0.17 0.22 -0.46 -0.06 -0.83 -0.08 1.61 0.99 0.43]]
(5, 10)
$\frac{\partial L}{\partial \mathbf{h}_{t-1}}$は、1つ前のLSTMレイヤに逆伝播します。
最後に、入力データ$\mathbf{x}_t$の勾配$\frac{\partial L}{\partial \mathbf{x}_t}$を求めます。
・入力データ$\mathbf{x}_t$の勾配
入力データ$\mathbf{x}_t$についても同様に、分岐した4つのノードで求めた勾配の総和を求めます。
# 入力データの勾配を計算 dx = dx_f + dx_g + dx_i + dx_o print(np.round(dx, 1)) print(dx.shape)
[[-0.7 -0.4 -1.6 0.1 -0.7 1.5 -0.8 0.2 -0.5 -0.4 0.5 1.9]
[ 0.4 3.9 -0.9 -2.5 -1. -2.7 -3.4 0.1 0.7 1. 2.2 -1.7]
[-1.1 0.2 -0.8 0.7 -2. -0.4 1.1 0.2 -0.6 -1.8 2.5 0.4]
[-0.9 2. -1.2 -1.2 -1.2 -0.7 -3.6 -0. 0.9 -0.6 1.3 1.1]
[-1. 0.8 -0.6 -0.7 -1.4 0.1 0.1 -0.2 0.1 -0.9 0.4 1.9]]
(5, 12)
$\frac{\partial L}{\partial \mathbf{x}_t}$は、Time Embeddingレイヤに逆伝播します。
以上がLSTMレイヤにおける逆伝播の計算です。前項とこの項でLSTMレイヤの順伝播の逆伝播を確認できました。次項では、LSTMレイヤを実装します。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.
おわりに
中々ハードでしたね。正直一週目では飛ばしてもいいと思います。私(のブログ)は無駄が性分なのでむしろ深堀してやりますがね、、
それはともかく、何とかレイヤの○伝播の入出力だとかちゃんと書かないと分からないのに書けば書くほど分かりにくくなるのを何とかしたい。ルールを決めて省略できるようにしたいけど、最初から読んでもらうことを想定するのもムリな話だしな。まぁ一通り書き終わっ(て暇になっ)たら考えます。
そんな勉強のお供にこの一曲をどうぞ♪
潔さ、、、
【次節の内容】