RNNs & LSTMs

core 12 minarchitecturesrnnlstmsequence-models

Builds on:BackpropagationActivation functions

The idea

An RNN reads a sequence one element at a time and folds it into a fixed-size hidden state — a running, lossy summary of everything seen so far:

ht=tanh⁡(Whht−1+Wxxt+b)h_t = \tanh(W_h h_{t-1} + W_x x_t + b)

The same WhW_h, WxW_x, bb are reused at every step: weight sharing across time, exactly as a CNN shares weights across space. That makes the model length-agnostic and keeps parameters at O(d2)O(d^2) regardless of sequence length — but it also means one matrix, WhW_h, sits on every path from the past to the present.

Backpropagation through time (BPTT). To train, unroll the recurrence into a deep feedforward net with one layer per step (all layers sharing weights), backpropagate, and sum each weight gradient over every step where the weight was used. The chain rule from step tt back to step kk multiplies one recurrent Jacobian per step:

∂ht∂hk=∏j=k+1t∂hj∂hj−1,∂hj∂hj−1=diag ⁣(1−hj2)Wh\frac{\partial h_t}{\partial h_k} = \prod_{j=k+1}^{t} \frac{\partial h_j}{\partial h_{j-1}}, \qquad \frac{\partial h_j}{\partial h_{j-1}} = \mathrm{diag}\!\left(1 - h_j^2\right) W_h

Why gradients vanish or explode. A product of t−kt - k near-identical matrices scales like powers of the largest singular value σmax⁡(Wh)\sigma_{\max}(W_h): at 0.90.9 per step, 50 steps leave a factor 0.950≈0.0050.9^{50} \approx 0.005; at 1.11.1, they produce 1.150≈1171.1^{50} \approx 117. So gradients from distant steps either drown (vanish — the model simply never learns long-range dependencies, and trains fine otherwise, which is what makes it insidious) or blow up (explode — loss spikes, NaNs). Tanh saturation compounds the vanishing side: the diag(1−hj2)\mathrm{diag}(1 - h_j^2) factor is at most 1 and near 0 for saturated units, dragging the effective per-step factor below σmax⁡\sigma_{\max}.

Fix for explosion: clip. Rescale the gradient when its global norm exceeds a threshold θ\theta (typically 1–5): g←θ g/∥g∥g \leftarrow \theta \, g / \|g\| if ∥g∥>θ\|g\| > \theta. Direction preserved, step size capped. Cheap, standard — and useless against vanishing, since you cannot rescale a signal that never arrived.

Fix for vanishing: the LSTM. Add a second state, the cell ctc_t, with an additive update controlled by three sigmoid gates (each a function of ht−1h_{t-1} and xtx_t):

ft=σ(Wf[ht−1,xt]+bf),it=σ(Wi[ht−1,xt]+bi),c~t=tanh⁡(Wc[ht−1,xt]+bc).\begin{aligned} f_t &= \sigma(W_f [h_{t-1}, x_t] + b_f), \\ i_t &= \sigma(W_i [h_{t-1}, x_t] + b_i), \\ \tilde{c}_t &= \tanh(W_c [h_{t-1}, x_t] + b_c). \end{aligned} ct=ft⊙ct−1+it⊙c~t,ot=σ(Wo[ht−1,xt]+bo),ht=ot⊙tanh⁡(ct).\begin{aligned} c_t &= f_t \odot c_{t-1} + i_t \odot \tilde{c}_t, \\ o_t &= \sigma(W_o [h_{t-1}, x_t] + b_o), \\ h_t &= o_t \odot \tanh(c_t). \end{aligned}

Forget gate ftf_t: what fraction of old memory to keep. Input gate iti_t: how much of the new candidate c~t\tilde{c}_t to write. Output gate oto_t: how much of the memory to expose as hth_t. The gradient payoff is the direct path through the cell: ∂ct/∂ct−1=ft\partial c_t / \partial c_{t-1} = f_t — no WhW_h, no tanh derivative. When the network holds ftf_t near 1, gradients travel back essentially unchanged: a gated highway through time. (Residual connections are the same additive trick with the gate hard-wired to 1.) Practical corollary: initialize the forget-gate bias to 1–2 so the highway starts open.

GRU: the 2-gate budget option. Merge cell and hidden state, keep an update gate ztz_t and a reset gate rtr_t:

zt=σ(Wz[ht−1,xt]),rt=σ(Wr[ht−1,xt]),h~t=tanh⁡(W[rt⊙ht−1, xt]),ht=zt⊙ht−1+(1−zt)⊙h~t.\begin{aligned} z_t &= \sigma(W_z [h_{t-1}, x_t]), \\ r_t &= \sigma(W_r [h_{t-1}, x_t]), \\ \tilde{h}_t &= \tanh(W [r_t \odot h_{t-1},\, x_t]), \\ h_t &= z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t. \end{aligned}

Three gated blocks instead of four, so about 3/4 the parameters, often matching LSTM quality. (Convention gotcha: some sources swap the roles of ztz_t and 1−zt1 - z_t — check before comparing code to a paper.)

What gating never fixed. Training is still sequential — hth_t waits for ht−1h_{t-1}, so no parallelism across positions, which is what GPUs want — and information nn steps back must still survive nn updates through a fixed-size state, so effective context stays far below nominal context. Transformers solved both: all positions train in parallel, and attention gives an O(1)O(1)-hop path between any two tokens. Recurrence remains sensible where its strengths bind: streaming/online inference (constant-size state, O(1)O(1) per step vs a growing KV cache), small data, constrained compute, and some time series. Modern state-space models (Mamba and kin) revisit exactly this trade with parallelizable training.

Worked example

One LSTM step with scalar states, so every number can be checked by hand. Previous cell ct−1=1.0c_{t-1} = 1.0, and suppose the gate pre-activations (the values of W[ht−1,xt]+bW [h_{t-1}, x_t] + b) come out as:

GatePre-activationActivationValue
forget ftf_t2.02.0σ\sigma0.8810.881
input iti_t0.00.0σ\sigma0.5000.500
candidate c~t\tilde{c}_t1.01.0tanh⁡\tanh0.7620.762
output oto_t1.01.0σ\sigma0.7310.731

(Check: σ(2)=1/(1+e−2)=1/1.135=0.881\sigma(2) = 1/(1 + e^{-2}) = 1/1.135 = 0.881; σ(0)=0.5\sigma(0) = 0.5; tanh⁡(1)=0.762\tanh(1) = 0.762; σ(1)=1/1.368=0.731\sigma(1) = 1/1.368 = 0.731.)

New cell state — keep 88.1% of the old memory, write half of the candidate:

ct=ft⋅ct−1+it⋅c~t=0.881×1.0+0.500×0.762=0.881+0.381=1.262c_t = f_t \cdot c_{t-1} + i_t \cdot \tilde{c}_t = 0.881 \times 1.0 + 0.500 \times 0.762 = 0.881 + 0.381 = 1.262

New hidden state — squash the cell, then let the output gate meter it out:

ht=ot⋅tanh⁡(ct)=0.731×tanh⁡(1.262)=0.731×0.852=0.623h_t = o_t \cdot \tanh(c_t) = 0.731 \times \tanh(1.262) = 0.731 \times 0.852 = 0.623

Note what did not happen to cc: it was never multiplied by a recurrent weight matrix or pushed through a saturating nonlinearity — only scaled by ft=0.881f_t = 0.881 and nudged additively. The backward derivative along that path is exactly 0.8810.881.

Gradient survival 20 steps back, direct path only, per-step factor to the 20th power:

ArchitecturePer-step factorAfter 20 steps
vanilla RNN (saturated tanh, factor 0.50.5)0.50.5≈10−6\approx 10^{-6}
LSTM, forget gate as above0.8810.8810.0790.079
LSTM, forget gate open (0.990.99)0.990.990.8180.818

The vanilla signal is gone; the open-gate LSTM keeps 82% of it. But note 0.88120≈0.080.881^{20} \approx 0.08: LSTMs mitigate vanishing, they do not repeal it — any forget gate below 1 still decays geometrically, just with a base the network controls.

Cheat sheet

Vanilla RNN: ht=tanh⁡(Whht−1+Wxxt+b)h_t = \tanh(W_h h_{t-1} + W_x x_t + b) — same weights every step; hidden state = fixed-size running summary.

BPTT: unroll, backprop, sum gradients over steps; ∂ht/∂hk\partial h_t / \partial h_k is a product of Jacobians diag(1−hj2) Wh\mathrm{diag}(1 - h_j^2)\, W_h, so it scales like σmax⁡(Wh)t−k\sigma_{\max}(W_h)^{t-k} (tanh saturation pushes the effective base lower).

LSTM: ft,it,ot=σ(⋅)f_t, i_t, o_t = \sigma(\cdot), c~t=tanh⁡(⋅)\tilde{c}_t = \tanh(\cdot); ct=ft⊙ct−1+it⊙c~tc_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t; ht=ot⊙tanh⁡(ct)h_t = o_t \odot \tanh(c_t). Gradient highway: ∂ct/∂ct−1=ft\partial c_t / \partial c_{t-1} = f_t.

SymptomDiagnosisFix
Loss spikes / NaNs on long sequencesexploding gradientsclip global norm, g←θg/∥g∥g \leftarrow \theta g / \Vert g \Vert, θ≈1\theta \approx 1–55
Model ignores anything more than ~10–20 steps backvanishing gradientsLSTM/GRU; forget bias init 1–2; ultimately attention
LSTM slow to pick up long dependencies early in trainingforget gate starts half-closedinitialize bf=1b_f = 1 to 22
BPTT out of memory / too slowfull-length unrolltruncated BPTT (cuts direct gradient paths at chunk boundaries; hidden state may still carry information forward)
Need whole-sequence context per tokenunidirectional statebidirectional RNN (offline tasks only)
Vanilla RNNLSTMGRU
Gates03 (f,i,of, i, o)2 (z,rz, r)
Stateshhhh and cchh only
Params per layer≈d(d+x)\approx d(d + x)4×4\times vanilla3×3\times vanilla
Long-range gradientdies through WhW_h powersadditive cell path, gatedadditive via ztz_t blend