RNN と LSTM のしくみ:隠れ状態・勾配消失・ゲートで長く覚える
文章・音声・株価・センサーの記録のように、順番に意味があるデータ(系列データ)を扱うために作られたのが RNN(再帰型ニューラルネットワーク)と、その改良版の LSTM です。今の大規模言語モデルは Transformer で作られていますが、2010 年代半ばまでは、機械翻訳も音声認識も文字の予測も、LSTM が主役でした。
アテンションと Transformer のしくみ では、RNN の弱点として「遠くの単語の情報が薄れる」とだけ書きました。この記事では、その理由を計算で確かめ、LSTM がどうやってそれを解決したのかを説明します。
結論を先に書くと、次のようになります。
- RNN は、それまでの内容を 1 本のベクトル(隠れ状態)にまとめ、同じ重みで 1 ステップずつ更新する
- 学習では、勾配が時間を逆向きにさかのぼるたびに同じ行列を掛けられる。その倍率が 1 より小さければ勾配は指数的に消え、大きければ爆発する
- LSTM は、足し算で更新される「セル」を別に持ち、ゲートで「忘れる・書き込む・読み出す」を調整する。セルを通る勾配は忘却ゲートの値を掛けられるだけなので、ゲートが開いていれば遠くまで届く
- 忘却ゲートのバイアスの初期値が大きく効く。計算例では、バイアスを 0 から始めた LSTM は 30 ステップ前の値を覚えられず、5 から始めると 100 ステップ前の値も覚えられた
説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。
環境
計算例を動かした環境です。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| NumPy | 2.5 |
系列データと RNN
順番があるデータの例
| データ | 1 ステップの入力 | 予測するもの |
|---|---|---|
| 文章 | 1 つのトークン | 次のトークン、文の感情 |
| 音声 | 10 ミリ秒ごとの周波数の強さ | 話している文字 |
| 売上 | 1 日の売上 | 翌週の売上 |
| センサー | 1 秒ごとの値 | 故障の前ぶれ |
ふつうのニューラルネットワークは、入力の大きさが決まっています。系列データは長さがまちまちで、しかも「前に何が来たか」で次の意味が変わります。
隠れ状態を持ち回る
RNN は、隠れ状態 h というベクトルを持ち、入力 x を 1 つ読むたびに、次の式で更新します。
h_t = tanh(W_x × x_t + W_h × h_{t−1} + b)
ポイントは 2 つです。
- 今の隠れ状態は、今の入力と、1 つ前の隠れ状態から決まる。1 つ前の隠れ状態はさらにその前の入力を含んでいるので、h_t には、それまでのすべての入力の情報がまとめて入っている
- どのステップでも同じ重み W_x・W_h を使う。長さ 10 の系列も長さ 1000 の系列も、同じ重みで処理できる
最後の隠れ状態から答えを出せば「系列全体の分類」、ステップごとの隠れ状態から答えを出せば「ステップごとの予測」(次の単語の予測など)になります。
勾配消失と勾配爆発
時間をさかのぼる誤差逆伝播
RNN を学習させるときは、系列を時間方向に展開すると、同じ重みを使った層が T 段重なった深いネットワークとみなせます。このネットワークで誤差逆伝播法を行うのが、時間方向の誤差逆伝播法(BPTT)です。
最後の損失から t ステップ前の隠れ状態への勾配は、連鎖律により、1 ステップ分の微分(ヤコビ行列)を t 回掛けたものになります。
∂h_t / ∂h_{t−1} = diag(1 − h_t²) × W_h
∂h_T / ∂h_0 = (∂h_T / ∂h_{T−1}) × … × (∂h_1 / ∂h_0)
同じ W_h を何度も掛けるので、W_h が「ベクトルを何倍に伸ばすか」で結果が大きく変わります。活性化関数がなければ、倍率が 0.9 のとき 50 ステップで 0.9⁵⁰ ≒ 0.005、1.1 のとき 1.1⁵⁰ ≒ 117 です。
勾配の大きさを計算してみる
隠れ状態が 16 次元の RNN で、W_h を「回転だけをする行列」に倍率を掛けたものにして、∂h_t / ∂h_0 の大きさ(行列がベクトルを最大で何倍に伸ばすか)を計算します。
import numpy as np
rng = np.random.default_rng(0)
d_h = 16
Q, _ = np.linalg.qr(rng.normal(size=(d_h, d_h))) # 回転だけの行列(大きさを変えない)
def grad_norms(scale, steps=50):
W_h = scale * Q
h = rng.normal(0, 0.1, d_h)
J = np.eye(d_h) # ∂h_t/∂h_0
out = {}
for t in range(1, steps + 1):
h = np.tanh(W_h @ h + rng.normal(0, 0.5, d_h))
J = np.diag(1 - h**2) @ W_h @ J # 連鎖律:1 ステップ分のヤコビ行列を掛ける
if t in (1, 10, 20, 50):
out[t] = np.linalg.norm(J, 2)
return out
for scale in (0.5, 1.0, 3.0):
print(scale, {t: f"{v:.2e}" for t, v in grad_norms(scale).items()})
# 線形(活性化関数なし)なら倍率の t 乗
for scale in (0.9, 1.1):
print("linear", scale, f"{scale**50:.2e}")
実行結果:
0.5 {1: '4.99e-01', 10: '3.47e-04', 20: '8.98e-08', 50: '6.05e-19'}
1.0 {1: '9.97e-01', 10: '1.53e-01', 20: '5.45e-03', 50: '1.83e-06'}
3.0 {1: '2.97e+00', 10: '3.50e+01', 20: '2.38e+02', 50: '2.38e+05'}
linear 0.9 5.15e-03
linear 1.1 1.17e+02
- 倍率 0.5 では、50 ステップ前への勾配は 10⁻¹⁹ 倍で、実質的に 0 です。50 ステップ前の入力を変えたら損失がどう変わるか、という情報が届かないので、遠くの入力との関係を学習できません(勾配消失)
- 倍率 1.0 でも、tanh の微分 1 − h² が 1 以下なので、少しずつ縮んで 50 ステップで 10⁻⁶ 倍になりました
- 倍率 3.0 では、50 ステップで 10⁵ 倍に膨らみます(勾配爆発)。tanh が飽和する(h が ±1 に近づき微分が 0 に近くなる)ため、線形の場合ほどは膨らみませんが、重みの更新が大きくなりすぎて学習が壊れます
この問題は、1991 年に Hochreiter が修士論文で、1994 年に Bengio らが論文で詳しく分析しました。
勾配爆発には「クリッピング」
勾配爆発は、勾配の大きさに上限を決めて、超えたら縮める 勾配クリッピング でおおむね防げます(Pascanu ら, 2013)。向きは変えずに大きさだけを抑えるので、学習が 1 回で壊れることはなくなります。
一方、勾配消失は、勾配を大きくしても解決しません。0 に近い値は、もともと情報がほとんど残っていないからです。そこで、ネットワークの形そのものを変えて、勾配が縮まずに届く道を作る のが LSTM です。
LSTM:セルとゲートで長く覚える
足し算で更新される「セル」
LSTM(Hochreiter と Schmidhuber, 1997)は、隠れ状態 h とは別に、セル c というベクトルを持ちます。セルは掛け算で上書きされるのではなく、足し算で少しずつ書き換わる のが特徴です。どれだけ書き換えるかは、0〜1 の値を出す 3 つの ゲート で決めます。
f_t = σ(W_f × [x_t, h_{t−1}] + b_f) 忘却ゲート:セルの内容をどれだけ残すか
i_t = σ(W_i × [x_t, h_{t−1}] + b_i) 入力ゲート:新しい内容をどれだけ書き込むか
o_t = σ(W_o × [x_t, h_{t−1}] + b_o) 出力ゲート:セルの内容をどれだけ読み出すか
g_t = tanh(W_g × [x_t, h_{t−1}] + b_g) 書き込む候補
c_t = f_t ⊙ c_{t−1} + i_t ⊙ g_t
h_t = o_t ⊙ tanh(c_t)
σ はシグモイド関数、⊙ は要素ごとの掛け算です。忘却ゲートが 1、入力ゲートが 0 なら、セルの値はそのまま次のステップに渡ります。「覚えておくべき値が来たら書き込み、関係のない入力が続く間は閉じておく」ことを、ゲートが学習します。
セルを通る勾配は縮みにくい
c_t = f_t ⊙ c_{t−1} + … なので、セルからセルへの勾配は、ほぼ 忘却ゲートの値を掛けるだけ です。
∂c_t / ∂c_{t−1} ≒ f_t
RNN のように同じ行列 W_h と tanh の微分を毎回掛けるのではなく、学習で決まる 0〜1 の値を掛けるだけなので、忘却ゲートが 1 に近ければ、勾配は何十ステップも縮まずに届きます。この「勾配が縮まずに流れる道」の考え方は、のちに画像認識の ResNet の残差接続(画像分類 AI のしくみ)にも受け継がれました。
忘却ゲートのバイアスの初期値
ただし、学習を始めた直後の忘却ゲートの値は、バイアス b_f の初期値で決まります。ゲートの入力がおよそ 0 だとすると、f ≒ σ(b_f) です。この値を何ステップも掛けたときに勾配がどれだけ残るかを計算すると、次のようになります。
| b_f の初期値 | σ(b_f) | 30 ステップ掛けると | 100 ステップ掛けると |
|---|---|---|---|
| 0 | 0.5 | 9.3 × 10⁻¹⁰ | 7.9 × 10⁻³¹ |
| 1 | 0.731 | 8.3 × 10⁻⁵ | 2.5 × 10⁻¹⁴ |
| 5 | 0.993 | 0.82 | 0.51 |
バイアスを 0 から始めると、LSTM でも最初は RNN と同じように勾配が消えてしまいます。そのため、忘却ゲートのバイアスは 1 以上から始めるのが定番になっています(Gers ら, 2000、Jozefowicz ら, 2015)。
「最初の値を覚える」問題で比べる
問題の設定
RNN と LSTM が、どれだけ前の入力を覚えられるかを比べます。問題は次のとおりです。
- 長さ T の系列の 最初の値 は +1 か −1。2 番目以降は、平均 0・標準偏差 0.5 のノイズ
- 最後のステップで、最初の値が +1 だったかを答える
答えに関係があるのは最初の値だけで、それを T − 1 ステップのノイズの間ずっと覚えていなければなりません。T を大きくするほど、勾配消失の影響がはっきり出ます。
実装
RNN と LSTM の順伝播と逆伝播を NumPy で書きます。重みは、入力と 1 つ前の隠れ状態をつなげたベクトルに掛ける 1 つの行列 W にまとめています(LSTM では 4 つのゲートの分を横に並べています)。逆伝播は、数値微分と一致することを確かめてあります。最適化には 最適化手法のしくみ で説明した Adam を使い、勾配の値は ±1 でクリッピングします。
import numpy as np
def make_data(rng, n, T):
"""最初の値が +1 か −1 かを、最後に答える問題。2 番目以降はノイズ"""
first = rng.choice([-1.0, 1.0], size=n)
xs = rng.normal(0, 0.5, (n, T))
xs[:, 0] = first
return xs[:, :, None], (first > 0).astype(float)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
class Adam:
def __init__(self, params, lr=0.01):
self.p, self.lr, self.t = params, lr, 0
self.m = {k: np.zeros_like(v) for k, v in params.items()}
self.v = {k: np.zeros_like(v) for k, v in params.items()}
def step(self, g):
self.t += 1
for k in self.p:
gk = np.clip(g[k], -1, 1) # 勾配クリッピング
self.m[k] = 0.9 * self.m[k] + 0.1 * gk
self.v[k] = 0.999 * self.v[k] + 0.001 * gk**2
mh, vh = self.m[k] / (1 - 0.9**self.t), self.v[k] / (1 - 0.999**self.t)
self.p[k] -= self.lr * mh / (np.sqrt(vh) + 1e-8)
def init(rng, d_in, d_h, gates):
s = 1 / np.sqrt(d_h)
return {"W": rng.normal(0, s, (d_in + d_h, gates * d_h)), "b": np.zeros(gates * d_h),
"v": rng.normal(0, s, d_h), "c": np.zeros(1)}
RNN の順伝播と逆伝播です。逆伝播では、時間を後ろから前にたどりながら、隠れ状態への勾配 dh に tanh の微分と W を掛けていきます。
def rnn_forward_backward(p, xs, y):
n, T, _ = xs.shape
d_h = p["v"].size
h = np.zeros((n, d_h)); hs, zs = [h], []
for t in range(T):
z = np.concatenate([xs[:, t], h], 1)
h = np.tanh(z @ p["W"] + p["b"]); zs.append(z); hs.append(h)
prob = sigmoid(h @ p["v"] + p["c"])
loss = -np.mean(y * np.log(prob + 1e-9) + (1 - y) * np.log(1 - prob + 1e-9))
g = {k: np.zeros_like(v) for k, v in p.items()}
dlogit = (prob - y) / n
g["v"] = h.T @ dlogit; g["c"] = np.array([dlogit.sum()])
dh = np.outer(dlogit, p["v"])
for t in reversed(range(T)):
da = dh * (1 - hs[t + 1] ** 2)
g["W"] += zs[t].T @ da; g["b"] += da.sum(0)
dh = (da @ p["W"].T)[:, 1:]
return loss, prob, g
LSTM の順伝播と逆伝播です。セルへの勾配 dc は、1 ステップさかのぼるたびに忘却ゲート f を掛けるだけになっています。
def lstm_forward_backward(p, xs, y):
n, T, _ = xs.shape
d_h = p["v"].size
h = np.zeros((n, d_h)); c = np.zeros((n, d_h)); cache = []
for t in range(T):
z = np.concatenate([xs[:, t], h], 1)
a = z @ p["W"] + p["b"]
f = sigmoid(a[:, :d_h]); i = sigmoid(a[:, d_h:2*d_h])
o = sigmoid(a[:, 2*d_h:3*d_h]); g_ = np.tanh(a[:, 3*d_h:])
c_prev = c
c = f * c + i * g_ # セル:忘れる分を掛けて、書き込む分を足す
h = o * np.tanh(c)
cache.append((z, f, i, o, g_, c_prev, c))
prob = sigmoid(h @ p["v"] + p["c"])
loss = -np.mean(y * np.log(prob + 1e-9) + (1 - y) * np.log(1 - prob + 1e-9))
g = {k: np.zeros_like(v) for k, v in p.items()}
dlogit = (prob - y) / n
g["v"] = h.T @ dlogit; g["c"] = np.array([dlogit.sum()])
dh = np.outer(dlogit, p["v"]); dc = np.zeros_like(dh)
for t in reversed(range(T)):
z, f, i, o, g_, c_prev, c = cache[t]
tc = np.tanh(c)
do = dh * tc
dc = dc + dh * o * (1 - tc**2)
df, di, dg = dc * c_prev, dc * g_, dc * i
da = np.concatenate([df * f * (1 - f), di * i * (1 - i), do * o * (1 - o), dg * (1 - g_**2)], 1)
g["W"] += z.T @ da; g["b"] += da.sum(0)
dh = (da @ p["W"].T)[:, 1:]
dc = dc * f # セルを通る勾配は、忘却ゲートを掛けるだけ
return loss, prob, g
隠れ状態は 8 次元にして、64 件ずつのミニバッチで 1500 回更新し、別に作った 2000 件で正解率を測ります。乱数の種を変えて 5 回ずつ学習させ、正解率が 0.95 を超えた回数を数えます。
def train(kind, T, seed=0, steps=1500, d_h=8, forget_bias=1.0):
rng = np.random.default_rng(seed)
p = init(rng, 1, d_h, 1 if kind == "rnn" else 4)
if kind == "lstm":
p["b"][:d_h] = forget_bias # 忘却ゲートのバイアス(大きいほど最初は忘れにくい)
fb = rnn_forward_backward if kind == "rnn" else lstm_forward_backward
opt = Adam(p)
for _ in range(steps):
xs, y = make_data(rng, 64, T)
_, _, g = fb(p, xs, y)
opt.step(g)
xs, y = make_data(np.random.default_rng(123), 2000, T)
_, prob, _ = fb(p, xs, y)
return np.mean((prob > 0.5) == y)
runs = [("RNN", "rnn", 1.0), ("LSTM(バイアス 0)", "lstm", 0.0), ("LSTM(バイアス 5)", "lstm", 5.0)]
for T in (10, 30, 100):
for name, kind, fb in runs:
accs = [train(kind, T, seed=s, forget_bias=fb) for s in range(5)]
print(f"T={T:3d} {name}: 解けた回数 {sum(a > 0.95 for a in accs)}/5, 正解率 {[round(float(a), 2) for a in accs]}")
結果
実行結果:
T= 10 RNN: 解けた回数 5/5, 正解率 [1.0, 1.0, 1.0, 1.0, 1.0]
T= 10 LSTM(バイアス 0): 解けた回数 5/5, 正解率 [1.0, 1.0, 1.0, 1.0, 1.0]
T= 10 LSTM(バイアス 5): 解けた回数 5/5, 正解率 [1.0, 1.0, 1.0, 1.0, 1.0]
T= 30 RNN: 解けた回数 4/5, 正解率 [0.5, 1.0, 1.0, 1.0, 1.0]
T= 30 LSTM(バイアス 0): 解けた回数 0/5, 正解率 [0.5, 0.5, 0.5, 0.5, 0.5]
T= 30 LSTM(バイアス 5): 解けた回数 5/5, 正解率 [1.0, 1.0, 1.0, 1.0, 1.0]
T=100 RNN: 解けた回数 2/5, 正解率 [0.5, 0.5, 1.0, 0.5, 1.0]
T=100 LSTM(バイアス 0): 解けた回数 0/5, 正解率 [0.5, 0.5, 0.5, 0.5, 0.5]
T=100 LSTM(バイアス 5): 解けた回数 4/5, 正解率 [1.0, 0.97, 0.9, 1.0, 1.0]
正解率 0.5 は、当て推量と同じで「何も覚えていない」状態です。
- 10 ステップなら、どれも解ける。この程度の距離なら、RNN でも勾配は十分に届きます
- RNN は、距離が延びるほど失敗が増える。30 ステップで 5 回中 1 回、100 ステップで 5 回中 3 回、まったく学習できませんでした。解けるかどうかが初期値の運しだいになっています
- 忘却ゲートのバイアスを 0 から始めた LSTM は、30 ステップでも 1 回も解けない。上の表のとおり、最初の忘却ゲートは 0.5 なので、セルの内容も勾配も 1 ステップごとに半分になり、RNN より早く消えてしまいます
- バイアスを 5 から始めた LSTM は、100 ステップでも 5 回中 4 回解けた。最初から「ほとんど忘れない」状態で学習を始めるので、最初の値の情報と勾配が最後まで残ります
LSTM は形を変えただけで自動的に長く覚えられるわけではなく、勾配が届く状態から学習を始めること が大事だと分かります。
GRU:ゲートを 2 つにした簡単な版
GRU(Cho ら, 2014)は、LSTM のセルと隠れ状態を 1 つにまとめ、ゲートを 2 つに減らした形です。
z_t = σ(W_z × [x_t, h_{t−1}]) 更新ゲート:新しい内容にどれだけ入れ替えるか
r_t = σ(W_r × [x_t, h_{t−1}]) リセットゲート:候補を作るときに過去をどれだけ使うか
h̃_t = tanh(W × [x_t, r_t ⊙ h_{t−1}])
h_t = (1 − z_t) ⊙ h_{t−1} + z_t ⊙ h̃_t
h_t が「古い値と新しい値の重み付き平均」になっているので、LSTM と同じように、z_t が 0 に近ければ値も勾配もそのまま次に渡ります。重みの数が LSTM の 4 分の 3 で済み、多くの問題で LSTM と同じくらいの成績になることが報告されています(Chung ら, 2014)。
RNN の使い方の形
| 形 | 入力と出力 | 例 |
|---|---|---|
| 多対一 | 系列を読んで、最後に 1 つ答える | 文の感情の分類、上の「最初の値を覚える」問題 |
| 多対多(同じ長さ) | 各ステップで答える | 次の文字の予測、音声のフレームごとの文字の予測(音声認識 の CTC) |
| 系列から系列へ(seq2seq) | エンコーダーで読んだ内容を、デコーダーが別の長さの系列にする | 機械翻訳、要約 |
| 双方向 | 前から読む RNN と後ろから読む RNN の隠れ状態をつなげる | 品詞の付与、固有表現の抽出(文全体が先に分かっている問題) |
seq2seq(Sutskever ら, 2014)は、原文を読み終えたときの隠れ状態 1 本に文全体の意味を詰め込む必要があり、長い文で訳が崩れました。これを解決するために生まれたのが、訳語を出すたびに原文のすべての隠れ状態を見直す アテンション(Bahdanau ら, 2014)で、それがやがて RNN をなくした Transformer(2017 年)につながります。
Transformer に置き換わった理由と、その後
Transformer は、系列のすべての位置どうしをアテンションで直接つなぐので、遠くの情報も 1 段で届き、全位置を並列に計算できます。大量のデータを GPU で学習させるには、1 ステップずつ順番に計算する RNN より圧倒的に向いていました。
一方で、Transformer は系列が長くなると、アテンションの計算量が長さの 2 乗で増え、生成中も過去のすべての位置の情報(KV キャッシュ)を持ち続ける必要があります。RNN は、どれだけ長い系列でも、固定の大きさの隠れ状態だけを持てば次を計算できます。この長所を生かしつつ、学習を並列にできるように工夫した 状態空間モデル(Mamba など)や、RNN として動かせる言語モデル(RWKV など)の研究が、2020 年代に入って再び盛んになっています。
RNN と LSTM の手法の流れ
この記事で紹介した手法を年代順に並べます。
| 年 | 手法・出来事 | ポイント |
|---|---|---|
| 1986 | ジョーダン・ネットワーク | 出力を次のステップの入力に戻す、初期の再帰型のネットワーク |
| 1990 | エルマン・ネットワーク | 隠れ状態を次のステップに戻す。今の RNN の基本の形 |
| 1991・1994 | 勾配消失の分析(Hochreiter、Bengio ら) | 時間をさかのぼる勾配が指数的に消える・爆発することを示した |
| 1997 | LSTM | 足し算で更新されるセルで、勾配が遠くまで届くようにした |
| 1997 | 双方向 RNN | 前から読む RNN と後ろから読む RNN を組み合わせる |
| 2000 | 忘却ゲート(Gers ら) | セルの内容を消せるようにした。今の LSTM の形 |
| 2013 | 勾配クリッピング(Pascanu ら) | 勾配の大きさに上限を設けて、勾配爆発を防ぐ |
| 2014 | GRU・seq2seq・アテンション | ゲートを減らした RNN、系列から系列への変換、原文を見直すしくみ |
| 2017 | Transformer | RNN をなくし、アテンションだけで系列を扱う |
| 2023 | Mamba など | 並列に学習でき、RNN のように少ないメモリーで生成できる状態空間モデル |
まとめ
- RNN は、隠れ状態を同じ重みで 1 ステップずつ更新し、それまでの入力の情報を 1 本のベクトルにまとめる
- 時間方向の誤差逆伝播では、同じ行列と tanh の微分を何度も掛けるので、勾配が指数的に消えたり爆発したりする。爆発は勾配クリッピングで防げるが、消失は防げない
- LSTM は、足し算で更新されるセルと、忘れる・書き込む・読み出すを決める 3 つのゲートを持つ。セルを通る勾配は忘却ゲートの値を掛けるだけなので、遠くまで届く
- 忘却ゲートのバイアスの初期値が大事。0 から始めると LSTM でも 30 ステップ前の値を覚えられず、5 から始めると 100 ステップ前の値も覚えられた
- GRU はゲートを 2 つにした簡単な形。RNN の弱点を補うアテンションが Transformer につながり、その後、RNN の長所を生かす状態空間モデルの研究も進んでいる
参考
- M. I. Jordan, "Serial Order: A Parallel Distributed Processing Approach", ICS Report 8604, 1986
- J. L. Elman, "Finding Structure in Time", Cognitive Science 1990
- S. Hochreiter, "Untersuchungen zu dynamischen neuronalen Netzen", Diploma thesis, TU München 1991
- Y. Bengio, P. Simard and P. Frasconi, "Learning Long-Term Dependencies with Gradient Descent is Difficult", IEEE Transactions on Neural Networks 1994
- S. Hochreiter and J. Schmidhuber, "Long Short-Term Memory", Neural Computation 1997
- M. Schuster and K. K. Paliwal, "Bidirectional Recurrent Neural Networks", IEEE Transactions on Signal Processing 1997
- F. A. Gers, J. Schmidhuber and F. Cummins, "Learning to Forget: Continual Prediction with LSTM", Neural Computation 2000
- R. Pascanu, T. Mikolov and Y. Bengio, "On the difficulty of training recurrent neural networks", ICML 2013(arXiv:1211.5063)
- K. Cho et al., "Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation", EMNLP 2014(arXiv:1406.1078)
- J. Chung et al., "Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling", NIPS 2014 Workshop(arXiv:1412.3555)
- I. Sutskever, O. Vinyals and Q. V. Le, "Sequence to Sequence Learning with Neural Networks", NIPS 2014(arXiv:1409.3215)
- D. Bahdanau, K. Cho and Y. Bengio, "Neural Machine Translation by Jointly Learning to Align and Translate", ICLR 2015(arXiv:1409.0473)
- R. Jozefowicz, W. Zaremba and I. Sutskever, "An Empirical Exploration of Recurrent Network Architectures", ICML 2015
- A. Gu and T. Dao, "Mamba: Linear-Time Sequence Modeling with Selective State Spaces", 2023(arXiv:2312.00752)
- B. Peng et al., "RWKV: Reinventing RNNs for the Transformer Era", Findings of EMNLP 2023(arXiv:2305.13048)