RNN 逆伝播
ゼロから作るDeep Learning ❷ ―自然言語処理編 斎藤康毅 (著) の読書メモです。
記法
本ドキュメントは以下の記法で統一します。
| 記号 | 意味 |
|---|---|
| $\cdot$ | 行列積(ドット積) |
| $\odot$ | 要素積(アダマール積、element-wise product) |
前提
- corpus size: 1000
- V( vocab_size ):コーパスから重複を排除した単語 ID の数(語彙数)
- ソースコードは 418
- N:ミニバッチサイズ
- ソースコードは 10
- T:系列長(sequence length)
- ソースコードは 5
- E( wordvec_size ):単語の分散表現の次元数(要素数)
- ソースコードは 100
- H( hidden_size ): RNNの隠れ状態の次元数(要素数)
- ソースコードは 100
※ ソースコードの E, H はともに 100 ですが異なる値を設定できます。
RNN レイヤ


RNN レイヤの逆伝播
Deep Learning における勾配は損失 L に関する勾配 $\frac{\partial L}{\partial a_t}$ 、 $\frac{\partial L}{\partial W_x}$ 、 $\frac{\partial L}{\partial W_h}$ 、 $\frac{\partial L}{\partial h_{t-1}}$ 、 $\frac{\partial L}{\partial b}$ です。
逆伝播は 2 ステップで考ます。
Step 1. $\frac{\partial L}{\partial a_t}$ を求める(要素積 1 のみ):
\[\frac{\partial L}{\partial a_t} = \frac{\partial L}{\partial h_{t}} \odot \frac{\partial h_t}{\partial a_t} = dh_t \odot (1 - h_t \odot h_t) \quad \text{shape: } (N, H)\]| 記号 | 意味 | shape |
|---|---|---|
| $\odot$ | 要素積(element-wise) | — |
| $dh_t$ | 上流からの勾配 | $(N, H)$ |
| $h_t \odot h_t$ | $h_t$ の各要素を2乗 | $(N, H)$ |
| $1$ | 全要素が1の行列 | $(N, H)$ |
Step 2. $\frac{\partial L}{\partial a_t}$ を起点に各勾配を求める(ドット積):
| 勾配 | 式 | shape確認 |
|---|---|---|
| $\frac{\partial L}{\partial W_x}$ | $x_{t}^\top \cdot \frac{\partial L}{\partial a_t}$ | $(E,N)(N,H) = (E,H)$ |
| $\frac{\partial L}{\partial W_h}$ | $h_{t-1}^\top \cdot \frac{\partial L}{\partial a_t}$ | $(H,N)(N,H) = (H,H)$ |
| $\frac{\partial L}{\partial x_t}$ | $\frac{\partial L}{\partial a_t} \cdot W_x^\top$ | $(N,H)(H,E) = (N,E)$ |
| $\frac{\partial L}{\partial h_{t-1}}$ | $\frac{\partial L}{\partial a_t} \cdot W_h^\top$ | $(N,H)(H,H) = (N,H)$ |
| $\frac{\partial L}{\partial b}$ | $\sum_{n=1}^{N} \frac{\partial L}{\partial a_t}$ | $(N,H) \to (H,)$ 数学的には $(1,H)$ だがコードの実装上は $(H,)$。以下 $(H,)$ と記載する |
※ 数学的には行列積の定義より $(E, N)(N, H) = (E, H)$ が必ず成り立ちますが、真ん中の $N$ が消える(縮約される)意味をミニバッチ全員分の損失を重みに集約しているからと解釈すると理解が深まります。
パターン:重み行列の勾配は「forward で掛けた相手を転置して左から掛ける」、
入力・隠れ状態の勾配は「forward で掛けた重み行列を転置して右から掛ける」。
$\frac{\partial L}{\partial a_t}$
\[\frac{\partial L}{\partial a_t} = \frac{\partial L}{\partial h_t} \odot \frac{\partial h_t}{\partial a_t} = dh_t \odot (1 - h_t \odot h_t) \quad \text{shape: } (N, H)\]このとき $ \frac{\partial L}{\partial h_t}$ は以下の勾配の和です。
- 各時刻の出力層からの勾配
- 時刻をまたぐ再帰的な勾配
TimeRNN.backward() ではdh = dhs[:, t, :] + dh_nextとして両者を加算しいます。
$\frac{\partial L}{\partial W_x}$
\[\frac{\partial L}{\partial W_x} = x_{t}^\top \cdot \frac{\partial L}{\partial a_t}\]- $\frac{\partial a_t}{\partial W_x} = x$ : $a_t = h_{t-1} \cdot W_h + x_{t} \cdot W_x + b$ を $W_x$ で微分
- forward で $x$ を左から掛けていたので、転置して $x_{t}^\top$ を左から掛ける
NOTE
なぜ $x_{t}^\top$ を左から掛けるのかforward では:
$a_t = x \cdot W_x \quad (N,E)(E,H) = (N,H)$
逆伝播の原則は「forward で左にいた行列を転置して左から掛ける」です。
forward で $x$ は $W_x$ の左にいたので $x_{t}^\top$ を左から掛けます。$\frac{\partial L}{\partial W_x} = x_{t}^\top \cdot da \quad (E,N)(N,H) = (E,H)$
$\frac{\partial L}{\partial W_h}$
\[\frac{\partial L}{\partial W_h} = h_{t-1}^\top \cdot \frac{\partial L}{\partial a_t}\]- $\frac{\partial a_t}{\partial W_h} = h_{t-1}$ : $a_t = h_{t-1} \cdot W_h + x \cdot W_x + b$ を $W_h$ で微分
- forward で $h_{t-1}$ を左から掛けていたので、転置して $h_{t-1}^\top$ を左から掛ける
$\frac{\partial L}{\partial x_t}$
\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial a_t} \cdot W_x^\top\]- $\frac{\partial a_t}{\partial x} = W_x$ : $a_t = h_{t-1} \cdot W_h + x \cdot W_x + b$ を $x$ で微分
- forward で $W_x$ を右から掛けていたので、転置して $W_x^\top$ を右から掛ける
$\frac{\partial L}{\partial h_{t-1}}$
\[\frac{\partial L}{\partial h_{t-1}} = \frac{\partial L}{\partial a_t} \cdot W_h^\top\]- $\frac{\partial a_t}{\partial h_{t-1}} = W_h$ : $a_t = h_{t-1} \cdot W_h + x \cdot W_x + b$ を $h_{t-1}$ で微分
- forward で $W_h$ を右から掛けていたので、転置して $W_h^\top$ を右から掛ける
- この勾配が1つ前の時刻 $t-1$ の RNN へ伝播することで BPTT が実現されます。
$\frac{\partial L}{\partial b}$
\[\frac{\partial L}{\partial b} = \sum_{n=1}^{N} \frac{\partial L}{\partial a_t} \quad \text{shape: } (N,H) \to (H,)\]- $\frac{\partial a_t}{\partial b} = 1$ : $a_t = h_{t-1} \cdot W_h + x \cdot W_x + b$ を $b$ で微分
- $b$ はミニバッチ $N$ 個全員に加算されるため、逆伝播では $N$ 方向に合算する
ブロードキャストと数学的根拠
順伝播で $b$ が各データに加算されるのは:
$a_n = h_{t-1}^{(n)} W_h + x^{(n)} W_x + b \quad n = 1, \ldots, N$
$b$ は $N$ 個の式すべてに同一の値として登場します。
これは数学的に「$b$ は $N$ 個の出力すべてに依存している」ことを意味します。
全データをまとめると:
$A = \begin{pmatrix} a_1 \ a_2 \ \vdots \ a_N \end{pmatrix} \quad \text{shape: } (N, H)$
各 $a_n$ は横ベクトル $(1, H)$ で、それが $N$ 行積み重なった行列が $A$ です。
バイアス $b$ の逆伝播
$L$ は全データの損失の合計なので、$b$ による偏微分は全データ分の寄与を足し合わせます:
\[\frac{\partial L}{\partial b} = \sum_{n=1}^{N} \frac{\partial L}{\partial a_n} \cdot \frac{\partial a_n}{\partial b} = \sum_{n=1}^{N} \frac{\partial L}{\partial a_n} \cdot 1 = \sum_{n=1}^{N} \frac{\partial L}{\partial a_t}\]これは多変数の連鎖律そのもの。
ブロードキャストとの関係
| 内容 | |
|---|---|
| 数学 | $b$ が $N$ 個の式に共有されるので勾配を総和する |
| ブロードキャスト | $(1,H)$ の $b$ を $(N,H)$ に自動拡張して計算する実装上の仕組み |
ブロードキャストは「数学的に同じ値を $N$ 個使う」という操作を効率的に実装したもの。
NumPy がなくても数学的な根拠は成立しています。
BPTT(Backpropagation Through Time)
以下の RNN を時間方向に展開したネットワーク全体に対する逆伝播アルゴリズムそ BPTT(Backpropagation Through Time)と呼びます。
\[\frac{\partial L}{\partial h_t} = \underbrace{\left(dh_t^{(\text{出力層})} + dh_t^{(\text{次時刻})}\right)}_{dh_t \;(\text{2経路の和})}\]各時刻の勾配
各時刻の RNN セルに届く勾配 $\frac{\partial L}{\partial h_t}$ は 2 つの経路の和です。
| 勾配 | 経路 | 意味 |
|---|---|---|
dhs[:,t,:] |
出力層(TimeAffine)から | 時刻 $t$ の予測誤差 |
dh_next |
未来時刻 $t+1$ から | $t+1$ 以降の損失から再帰的に伝播してきた勾配 |
dh_next には $t+1$ の損失だけでなく $t+2, t+3, \ldots$ の損失から再帰的に伝播してきた
勾配も含まれています。これが「Through Time」の意味です。
TimeRNN.backward() の流れ
dh_next = 0 # t=4 の次時刻からの勾配は 0 で初期化
for t in reversed(range(T)): # t = 4, 3, 2, 1, 0 の順
dh = dhs[:, t, :] + dh_next # 2経路の勾配を加算
dh_next = rnn_layer.backward(dh) # t-1 へ渡す勾配を計算
dh_next を次のループへ引き渡すことで、勾配が時刻をさかのぼって伝播します。
Truncated BPTT
実装では time_size = 5 のため、1 回の逆伝播で遡れるのは 5 ステップ分のみです。
これを Truncated BPTT(打ち切り BPTT)と呼びます。
| 順伝播 | 逆伝播 | |
|---|---|---|
stateful=True |
前バッチの文脈を引き継ぐ | 勾配は time_size ステップのみ遡る |
stateful=False |
毎回ゼロ初期化 | 同上 |
なぜ打ち切るのか
全時刻分の勾配を遡ると計算量が膨大になります。
time_size で打ち切ることで計算量を一定に保ちながら、stateful=True による隠れ状態の引き継ぎで長期の文脈は順伝播側で保持します。
勾配消失・爆発
BPTT では勾配が時刻を遡るたびに tanh の微分($1 - h_t^2$)が掛け算されます。
t=4 → × (1 - h4²)
t=3 → × (1 - h3²)
t=2 → × (1 - h2²)
...
$\tanh$ の微分は最大でも 1 のため、ステップ数が増えるほど勾配が小さくなり勾配消失が起きやすくなります。これが素の RNN の限界であり、LSTM・GRU が導入された背景です。
補足
stateful=True による文脈の引き継ぎ
この実装では TimeRNN を以下のように生成しています。
TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True)
stateful=True の場合、順伝播は前回のミニバッチの最後の隠れ状態を次回のミニバッチへ引き継ぎます。
コードでは TimeRNN.forward() の冒頭で以下の処理が行われます。
if not self.stateful or self.h is None:
self.h = np.zeros((N, H), dtype='f')
stateful=True の場合は self.h が保持されるため、次回の forward() 呼び出し時にも前回の隠れ状態が利用されます。
ミニバッチ内での状態遷移
1つのミニバッチ( T = 5 )の内部では以下のように隠れ状態が伝播します。
通常の RNN と同じく、各時刻の隠れ状態が次時刻へ渡されます。
ミニバッチ間での状態遷移
さらに stateful=True では、最後の隠れ状態 h_4 が次のミニバッチの初期状態として利用されます。
Batch2 の最初の計算は以下のとおりです。
\[h_{0}^{(\mathrm{Batch2})} = tanh(h_4^{(\mathrm{Batch1})} \cdot W_h + x_5 \cdot W_x + b)\]概念的には以下のようになります。
\[h_{init}^{(\mathrm{Batch2})} = h_4^{(\mathrm{Batch1})}\]なぜ必要なのか
本サンプルでは time_size = 5 なので、1 回の順伝播では 5 単語分しか RNN を展開しません。
しかし stateful=True によって隠れ状態が保持されるため、実際には前のミニバッチで学習した文脈を次のミニバッチへ引き継げます。
例えば
Batch1
the stock market crashed
Batch2
because investors feared ...
のような場合でも、Batch2 は Batch1 の文脈を含んだ隠れ状態から開始できます。
そのため、RNN は time_size より長い文脈を利用した言語モデルとして振る舞います。
※ 順伝播では長い文脈を保持できるが、勾配は time_size ステップ分しか遡りません。
stateful=False の場合
stateful=False の場合は毎回隠れ状態をゼロ初期化します。 $h_0 = 0$ のため各ミニバッチは独立して扱われて前回の文脈は引き継がれません。
計算グラフ:加算ノード
順伝播(forward)
A ──┐
├─→ [+] ──→ Y = A + B
B ──┘
単純に Y = A + B を計算するだけです。
逆伝播(backward)
加算ノードの局所的な微分は、
\[\frac{\partial Y}{\partial A} = 1, \qquad \frac{\partial Y}{\partial B} = 1\]なので、上流から流れてきた勾配 dY(= d(A+B))に 1 をかけるだけ、つまりそのままの値が両方の入力ノードに流れます。
dA = dY × 1 = dY
dB = dY × 1 = dY
図で描くとこうです:
dA ←──┐
├──── dY
dB ←──┘
「情報が含まれる」の中身を分解する
\[dh_t = \frac{\partial L}{\partial h_{t+1}} \times \underbrace{(1-\tanh^2(s))}_{s に x_{t+1} の値が入っている} \times W_h\]ここでの「$x_{t+1}$ の情報」とは、
- $x_{t+1}$ の勾配(dx_{t+1}) ではなく
- $x_{t+1}$ の(順伝播で計算された)数値そのものが $s$ を経由して $\tanh’(s)$ というスカラー係数の中に埋め込まれている
という意味です。つまり dh_t の値の大きさは $x_{t+1}$ が何であったかによって変わりますが、dh_t という勾配の計算グラフ上の経路には $x_{t+1}$ の逆伝播枝(dv, dx_{t+1})は一切含まれていません。
たとえるなら
$\tanh’(s)$ は「ゲート(門)の開き具合」のようなものです。
- $s$ の値(=順伝播の結果、$h_t$ と $x_{t+1}$ の両方が影響)がゲートの開き具合を決める
- そのゲートを通して $dh_{t+1}$ が流れ、$dh_t$ になる
- でも「ゲートを開ける量」を決めているのは $s$ という一つの数値であって、そこに「$h_t$ 由来の開き具合」と「$x_{t+1}$ 由来の開き具合」が別々に分かれて入っているわけではない
なので x_{t+1} は「$dh_t$ という勾配が最終的にどれくらいの大きさになるか」に間接的に影響する情報として関与しているが、「x_{t+1} 自身の勾配計算の一部が dh_t に流れ込む」わけではない、という理解で完全に合っています。
-
アダマール積とも呼びます ↩