Recurrent networks, LSTMs and vanishing gradients
Before this lesson: lesson 03 (backpropagation, the adjoint ), lesson 04 (, , saturation), and eigenvalues and powers of a matrix in linear-algebra lesson 05. Norms of matrices are in linear-algebra lesson 11. Time: about 70 minutes, plus the notebook.
Why this matters #
An RNN is the simplest network that carries a state through time. Its training problem is the clearest example of a general fact: depth multiplies Jacobians, and a product of many matrices grows or shrinks exponentially. The same fact explains careful initialization (lesson 04), residual connections (lesson 05) and why state-space models control the eigenvalues of their state matrix (lesson 16).
LSTMs were the standard model for translation and speech recognition until about 2017. Attention was first invented to help an RNN translate long sentences. Today, linear recurrences are back in Mamba, RWKV and other state-space models. Next-token prediction, tokens and embeddings, which this lesson introduces with RNNs, are exactly the setup of every large language model. In ML for science, a learned time stepper (a weather emulator, for example) is an RNN, and training it on multi-step rollouts is BPTT. Interviews ask: "why do RNN gradients vanish?", "write the LSTM equations" and "why did transformers replace RNNs?"
1. Predict first #
2. The picture #
From text to vectors: tokens and embeddings #
A network works with numbers, so we first cut a text into tokens. A token is a small unit of text: a character, a word, or a piece of a word. The list of all possible tokens is the vocabulary ; each token gets an integer token id in . Real language models use subword tokenizers such as byte-pair encoding; the LLMs track covers them. In this lesson we use characters, because they are easy to see.
Example. With the vocabulary and the ids , , , , the word "hello" becomes the sequence of ids .
An id is a label, not a quantity: token 3 is not "three times" token 1. So we give each token a learned vector. An embedding matrix has one row per token, and the embedding of token is row of . In matrix form it is a one-hot row vector times :
The product picks one row (linear-algebra lesson 01: a one-hot vector selects a column or a row). Libraries never form the one-hot vector; they read the row directly. The rows are trained by backpropagation like any other weights.
Next-token prediction. A language model reads tokens and outputs a
probability distribution over the vocabulary for the next token
. The training loss at step is the cross-entropy : the
negative log-probability that the model gave to the true next token. For "hello" the inputs are
h e l l and the targets are the same text shifted by one: e l l o.
Teacher forcing. During training, the input at step is the true token , not the token the model would have sampled. So all targets are known in advance, and one pass over the text gives training signals. At generation time there is no true text: the model samples a token and feeds it back as the next input (this is called autoregressive generation).
The recurrent cell, unrolled #
An RNN keeps a hidden state (a row vector, as in PyTorch). At every step it mixes the old state with the new input, using the same weights at every step:
Draw one box per time step and connect to : this is the unrolled network. It is a deep network with layers, one per step, and every layer uses the same , and . Training it is ordinary backpropagation on this graph. The only new point is that a shared weight collects one gradient term from each step, and these terms are added.
Step through BPTT. Press Forward step three times: each cell computes its blue state from the cell on its left. Then press Backward step: the orange gradient moves to the left, and each arrow multiplies it by . The defaults are the worked example of section 4. Now set : the factors stay below 1 because large states saturate tanh. Set and a small pulse : the factors are above 1 (magenta) and the gradient grows.
Memory and gradient are the same number #
Start with the simplest case: one number, no nonlinearity, . Unroll it:
The input of step reaches step with the weight . The derivative is the same number: . So memory (how much of the past is still in the state) and gradient flow (how much a loss at step can change step ) are one quantity:
- : old information and its gradient decay exponentially (vanishing);
- : both grow exponentially (exploding);
- : the boundary case; they neither decay nor grow.
The matrix case: eigenvalues and singular values #
With a state vector, gives . If is diagonalizable, and : each eigen-direction is scaled by . In the long run the largest wins (this is power iteration, linear-algebra lesson 05). This number is the spectral radius .
The spectral radius decides the long run, but not the first steps. If the eigenvectors of are far from orthogonal (a non-normal matrix), can grow for many steps before it decays, even with . Each single step is bounded by the largest singular value .
With the tanh, every step also multiplies by . A saturated unit (one with near 1) has near 0 and blocks the gradient.
How far back can a gradient travel? The curves show against the lag , on a log axis, for a hidden state of size 16. The dashed orange curve is the linear RNN, the solid one the tanh RNN; the grey line is and the dotted line is the bound . Move across 1 and watch the linear curve leave the blue (vanishing) region for the magenta (exploding) one. Then raise the input scale : saturation pulls the tanh curve down, even when . Switch to the random : now , and the linear curve first rises before it falls.
A scalar linear RNN . What is ?
, so the derivative is the product of 50 equal factors, . Derivatives along a chain multiply; they do not add. The weight is the same at each step, but its effect compounds.
A tanh RNN has . Its inputs are large, so most hidden units sit near . What happens to for large ?
Each step multiplies by . Saturated units make the diagonal factor tiny, and this can beat . The spectral radius decides only for the linear RNN. A bounded state does not mean a bounded gradient.
3. Definitions and main results #
Embeddings and the next-token loss #
Let be a sequence of token ids. The model reads (teacher forcing) and predicts .
- Embedding: , the row of . It has parameters.
- Logits and probabilities: , .
- Loss: and . Libraries usually report the mean ; the factor does not change anything below.
- Gradient at the logits: , as for any softmax with cross-entropy (lesson 03).
- Gradient of the embedding: . Only the rows of tokens that appear in the sequence receive a gradient, and a token that appears several times collects one term per appearance.
The vanilla RNN and BPTT #
Inputs , hidden states , given (usually ):
with , and . Many papers write column vectors, ; then . Norms and eigenvalues are the same for and , so all results below hold in both conventions.
BPTT. Let be the total gradient (a row vector, like ) and . Go from down to , with :
The first term is the loss at step ; the second is everything that happens after step , passed back through . The weights are used at every step, so their gradients sum over time:
Each term has the shape of a linear-layer gradient, input transposed times adjoint, exactly as in lesson 03.
Theorem (BPTT is backpropagation with tied weights). Give each step its own copy of , and evaluate at for all . Then .
Proof.
- depends on only through the copies: with every .
- The map has the derivative "copy". By the multivariable chain rule, the derivative of is the sum of the partial derivatives with respect to each copy.
- Each copy is used once, in . Its gradient is the usual linear-layer gradient .
Cost. BPTT costs about as much as the forward pass, , but it must store all states : memory . Truncated BPTT cuts the graph every steps (the state is passed forward but treated as a constant). Memory drops to , but the gradient no longer sees dependencies longer than steps.
Where the trouble is. Unroll the recursion for one loss term and an earlier state (). With the one-step Jacobian
the chain rule gives
This is a product of matrices. Pascanu et al. call the terms with large the long-term contributions. The total gradient is a sum of short-term and long-term contributions.
Vanishing and exploding gradients #
Theorem (Pascanu, Mikolov, Bengio 2013). Let ( for tanh, for the logistic sigmoid ). Write and . If , then for all
so the long-term contributions vanish exponentially, whatever the inputs and states are.
Proof.
- The 2-norm is submultiplicative: , and .
- The 2-norm of a diagonal matrix is its largest absolute entry, so .
- Hence for every .
- Submultiplicativity again: .
Turning the proof around: the long-term contributions can explode only if . This is a necessary condition for exploding, not a sufficient one: saturation can still make them vanish. In the linear case ( = identity, ) the exact condition is the spectral radius: if and only if , and grows exponentially if .
An RNN uses the logistic sigmoid () and has . What does the theorem say?
The condition is on , so every one-step Jacobian has norm at most 0.75. A sigmoid RNN vanishes much faster than a tanh RNN, because at every step. alone does not imply exploding.
Gradient clipping #
Global-norm clipping (Pascanu et al., Algorithm 1): let be the gradient of all parameters, stacked into one vector, and a threshold. Replace
It keeps the direction and caps the step length at times the learning rate. Why this helps: when gradients explode, the loss surface has a steep "wall". A normal gradient step at the wall throws the parameters far away, and training diverges. A clipped step stays short.
Clipping does not fix vanishing gradients. A small gradient is left unchanged. And when the gradient is large, the whole sum is rescaled by one number: the long-term contributions stay tiny next to the short-term ones. Clipping by value (each entry separately, to ) is a different rule: it changes the direction of the gradient.
Two ways to clip. Drag the orange gradient . Clipping by norm (solid orange) moves it back to the dashed circle along the same line, so the direction is kept. Clipping by value (magenta) moves it into the square ; the readout shows the angle it turned. Try a short gradient: neither rule changes it, so clipping cannot make a vanishing gradient larger.
The LSTM #
The long short-term memory (LSTM; Hochreiter and Schmidhuber 1997, with the forget gate of Gers et al. 2000) keeps two states: the hidden state and the cell state . Three gates, numbers between 0 and 1 for each unit, control the cell. With the logistic sigmoid and the elementwise product:
- The forget gate decides what to keep of the old cell state.
- The input gate decides how much of the candidate (often written ) to write.
- The output gate decides what part of the cell to show as .
There are four blocks of weights, each acting on , so one layer has parameters. PyTorch keeps two bias vectors ( and ), so it reports . PyTorch stores the four blocks in the order .
Why the cell state helps.
- The cell update is additive: . Hold the gates fixed. Then the direct path from to has the Jacobian .
- Over many steps this path gives : no weight matrix and no factor. (The other paths go through and the gates; they behave like a vanilla RNN.)
- The network sets from the data. With near 1, the memory and its gradient last about steps; with near 0 the cell erases its content on purpose. With and , the cell copies its value forever: the "constant error carousel" of the original LSTM.
- It is a residual connection in time, with a gate (compare lesson 05).
Forget-gate bias. A common choice is at initialization (Jozefowicz, Zaremba, Sutskever
2015), so the cell starts by keeping its content. Note that : a memory of only about
4 steps. Training must push toward 1 when long memory is needed. PyTorch initializes all biases
uniformly at random, so you set by hand (the slice [d_h:2*d_h] of the bias).
The GRU (Cho et al. 2014) uses the same idea with fewer parts: one update gate mixes the old state and a candidate, . It has no separate cell state and 3 weight blocks instead of 4. (Papers and libraries differ on which term gets .)
The LSTM keeps a gradient alive through the forget gate. The top shows the cell at step ; the small blue bars are the values of the 8 units of each gate, and the yellow box is the forget gate. The chart shows in orange: solid for all paths, dashed for the cell path alone. Try , and . The mean forget gate moves from about 0.5 to 0.74 to 0.95, and after 50 steps the gradient moves from about to to about 1.
Why does a forget-gate bias of at initialization help an LSTM learn long-range dependencies?
Along the cell path the gradient is multiplied by at every step. With it halves every step; with it keeps about after 50 steps. The cell is not constant: and the input gate still writes. The bias is only a starting point; training adapts .
Why attention and SSMs replaced RNNs for long sequences #
| RNN / LSTM | Self-attention | Linear RNN / SSM | |
|---|---|---|---|
| Training over the sequence | sequential: dependent steps | parallel | parallel (FFT convolution or scan) |
| Path between tokens and | nonlinear steps | 1 step | linear steps, eigenvalues controlled |
| Cost per generated token | constant (fixed-size state) | grows with (KV cache) | constant (fixed-size state) |
- Training. needs , so an RNN cannot spread the time steps over the GPU. Attention (lesson 09) computes all positions at once with large matrix products.
- Path length. Between distant tokens, an RNN gradient passes Jacobians. Attention connects them directly, so there is no long product to vanish.
- The price of attention: training cost and a cache of keys and values that grows with during generation.
- Linear RNNs and state-space models (S4, Mamba, LRU) keep the update linear in the state: (the S4 paper names the state and the input ). This unrolls to a convolution or an associative scan, so training is parallel; generation keeps a constant-size state. They also control the spectrum: with and puts every eigenvalue of inside the unit circle. The price: a fixed-size state must compress the whole past, so exact recall is weaker than with attention. Lesson 16 develops this.
Answers to section 1. (1) : closer to . (2) No: clipping only shrinks large gradients. (3) million parameters, and zero multiplications: the lookup reads one row.
4. Worked example #
(a) BPTT by hand. A scalar tanh RNN with , , , inputs and loss .
Forward.
- ;
- ;
- .
Backward, with and . There is no loss at steps 1 and 2, so there:
- , ;
- , ;
- , ;
- .
Parameter gradients (sums over time, one term per use of the weight):
- ;
- .
Each backward step multiplies the gradient by . The only input sits at step 1, so is small: it went through the whole product.
(b) Eigenvalues against singular values. Take (a Jordan block). Its eigenvalues are , so . But , so the theorem (with ) says nothing. In the linear case,
because . The norms are , and : the norm stays above 1 for two steps, then takes over. The norm condition is sufficient, not necessary.
5. In code #
PyTorch's autograd checks the worked example; then the Jacobian products, the LSTM equations and clipping. The notebook has these cells and the exercises.
import torch
# (a) the worked example with autograd
w = torch.tensor(0.5, requires_grad=True)
u = torch.tensor(1.0, requires_grad=True)
h = [torch.tensor(0.0, requires_grad=True)] # h_0
for x_t in [1.0, 0.0, 0.0]:
h.append(torch.tanh(w * h[-1] + u * x_t))
h[3].backward() # L = h_3
print([round(v.item(), 4) for v in h]) # [0.0, 0.7616, 0.3634, 0.1797]
print(round(w.grad.item(), 4), round(u.grad.item(), 4), round(h[0].grad.item(), 4)) # 0.6715 0.0882 0.0441
Jacobian products for steps and (column convention, ). The linear RNN follows ; the tanh RNN obeys the bound, and saturation can make it vanish even when :
from torch.autograd.functional import jacobian
torch.manual_seed(0)
H, D, T = 32, 8, 50
xs, U, W0 = torch.randn(T, D), torch.randn(H, D) / D**0.5, torch.randn(H, H)
norm2 = lambda M: torch.linalg.matrix_norm(M, ord=2).item()
def tanh_rnn_last(h0, W):
h = h0
for x in xs:
h = torch.tanh(W @ h + U @ x)
return h
# linear RNN: dh_T/dh_0 = W^T, ruled by rho(W)
for rho in [0.9, 1.0, 1.1]:
W = W0 * rho / torch.linalg.eigvals(W0).abs().max()
print("linear, rho", rho, f"{norm2(torch.linalg.matrix_power(W, T)):.0e}")
# linear, rho 0.9 8e-03 / linear, rho 1.0 2e+00 / linear, rho 1.1 2e+02
# tanh RNN with ||W||_2 = 0.9: the bound gives at most 0.9^50 = 5e-3
W = W0 * 0.9 / torch.linalg.matrix_norm(W0, ord=2)
print(f"{norm2(jacobian(lambda h0: tanh_rnn_last(h0, W), torch.zeros(H))):.0e}") # 6e-25
# tanh RNN with rho(W) = 1.5: the linear part would explode, but saturation wins here
W = W0 * 1.5 / torch.linalg.eigvals(W0).abs().max()
print(f"{norm2(jacobian(lambda h0: tanh_rnn_last(h0, W), torch.zeros(H))):.0e}") # 5e-05
The LSTM equations against nn.LSTMCell, the effect of the forget-gate bias, and clipping:
torch.manual_seed(0)
H, D, T = 32, 8, 50
xs = torch.randn(T, D)
# the LSTM by hand matches nn.LSTMCell (gate order in the weights: i, f, g, o)
cell = torch.nn.LSTMCell(D, H)
x, h, c = torch.randn(1, D), torch.randn(1, H), torch.randn(1, H)
z = x @ cell.weight_ih.T + cell.bias_ih + h @ cell.weight_hh.T + cell.bias_hh
i, f, g, o = z.chunk(4, dim=1)
c_new = torch.sigmoid(f) * c + torch.sigmoid(i) * torch.tanh(g)
h_new = torch.sigmoid(o) * torch.tanh(c_new)
h_ref, c_ref = cell(x, (h, c))
print(torch.allclose(h_new, h_ref, atol=1e-6), torch.allclose(c_new, c_ref, atol=1e-6)) # True True
# ||dc_T/dc_0|| after 50 steps for three forget-gate biases
def cell_last(c0):
h, c = torch.zeros(1, H), c0[None]
for x in xs:
h, c = cell(x[None], (h, c))
return c[0]
with torch.no_grad():
cell.bias_hh.zero_()
for b_f in [0.0, 1.0, 3.0]:
with torch.no_grad():
cell.bias_ih[H:2 * H] = b_f # the forget-gate block of the bias
J = jacobian(cell_last, torch.zeros(H))
print(b_f, f"{torch.linalg.matrix_norm(J, ord=2).item():.0e}") # 2e-10, 1e-03, 2e+01
# global-norm clipping keeps the direction and caps the length
p = torch.nn.Parameter(torch.zeros(3))
p.grad = torch.tensor([30.0, -40.0, 0.0]) # norm 50
total = torch.nn.utils.clip_grad_norm_([p], max_norm=1.0)
print(total.item(), [round(v, 3) for v in p.grad.tolist()]) # 50.0 [0.6, -0.8, 0.0]
With the forget gate is about ; the gradient survives only when is close to 1. At it even grows slightly: the paths through and the gates add to the direct cell path.
6. Where it shows up #
- Language and speech. LSTMs were the standard for machine translation and speech recognition in the mid-2010s; Google's 2016 neural translation system was a deep LSTM. Attention was first added to an RNN encoder–decoder (Bahdanau, Cho, Bengio 2015) and later replaced the recurrence.
- Language models. Tokens, embeddings, teacher forcing and the next-token cross-entropy of this lesson are the training setup of every GPT-style model; only the network in the middle changed.
- Linear recurrences return. Mamba, RWKV, LRU and other state-space models are designed around the problems of this lesson: linear updates, controlled eigenvalues, parallel training.
- ML for science. A learned time stepper (weather and climate emulators) is an RNN without input. Training on multi-step rollouts backpropagates through it exactly like BPTT. If has eigenvalues with magnitude above 1, long rollouts blow up; this is the role of the amplification factor in a von Neumann stability analysis. The adjoint method for neural ODEs is BPTT in continuous time. Earthquake phase pickers such as EQTransformer first shorten the waveform with convolutions and only then apply LSTMs and attention, because a raw record has thousands of steps.
- Practice. Global-norm clipping at 1.0 is a common default in transformer training as well
(nanoGPT uses
grad_clip = 1.0).
7. Common mistakes #
- Using eigenvalues in the sufficient condition. The proof needs . When the diagonal factors change from step to step, does not bound the product of a tanh RNN.
- "Clipping fixes vanishing gradients." It only shrinks large gradients.
- "LSTMs solve vanishing gradients." They give a path that does not vanish when . With the gradient still decays like ; this is intended forgetting.
- Forgetting the sum over time. is used at every step, so adds terms.
- Forgetting . A sigmoid RNN vanishes much faster than a tanh RNN.
- Expecting PyTorch to set the forget bias to 1. It does not; set
bias_ih[d_h:2*d_h]by hand. - Truncated BPTT with window and long dependencies. The forward state carries information further than steps, but the gradient cannot teach the model to use it.
- Shifting the targets the wrong way. The target at position is the token at . A model trained with inputs and targets aligned learns to copy its input.
8. Check yourself #
- A sigmoid RNN has . Does Pascanu's sufficient condition for vanishing hold?
Answer
Yes. , so : the long-term contributions shrink at least like . - How many parameters does one LSTM layer with inputs and hidden units have (one
bias per gate)? How many does PyTorch report?
Answer
. PyTorch has two bias vectors: . - A forget gate stays at . What fraction of the cell-path gradient survives 100 steps?
Compare a vanilla RNN whose per-step factor is 0.9.
Answer
against . The memory time is about steps. - Why does clipping not help with vanishing gradients?
Answer
It changes only when , and then it rescales the whole sum by one number. The long-term contributions are tiny next to the short-term ones, and rescaling does not change their ratio. - Why is a sum over time steps?
Answer
The same is used at every step (tied weights). By the multivariable chain rule, the gradient adds the contribution of each use: . - For the text "hello" with character tokens, write the inputs and the targets used with teacher forcing.
Answer
Inputs `h e l l`, targets `e l l o`. At each position the target is the next character, and the input is always the true previous character, not the model's own prediction.
Write the vanilla RNN update (row-vector convention).
, with the same weights at every step.
What is backpropagation through time (BPTT)?
Ordinary backpropagation on the RNN unrolled over steps: a -layer network with shared weights, whose weight gradients are sums over the steps.
Give the one-step Jacobian of a tanh RNN.
(in the column convention : ).
A scalar linear RNN has . What is ?
. Memory and gradient are the same number, .
What does the spectral radius decide for an RNN?
The long-run behaviour of a linear RNN: if and only if . Single steps are bounded by , which can be larger.
State Pascanu's sufficient condition for vanishing gradients.
If and , then .
Write global-norm gradient clipping.
over all parameters at once. It keeps the direction and caps the length at .
Write the LSTM cell-state update and say why it helps gradients.
. The direct path has Jacobian : no weight matrix and no , so gradients survive when .
Why are RNNs slow to train on long sequences?
needs , so the steps must run one after another; the GPU cannot process the time steps in parallel.
How does an embedding layer compute the vector of token ?
: it reads row of . No multiplications are needed.
9. Interview angle #
- "Why do RNN gradients vanish or explode?" — The gradient over steps is a product of Jacobians , bounded by ; in the linear case decides.
- "Write the LSTM and say why it helps." — Gates and candidate ; additive cell update; the direct path has Jacobian .
- "LSTM or GRU?" — Similar quality on most tasks; the GRU has 3 weight blocks instead of 4 and no separate cell state, so it is a little cheaper.
- "How would you initialize ?" — Orthogonal (all singular values 1), or the identity for ReLU RNNs (Le, Jaitly, Hinton 2015); set the LSTM forget bias to about 1.
- "What is teacher forcing, and what is its weakness?" — Training with the true previous tokens as inputs. At generation time the model sees its own outputs, which can contain errors it never saw in training (exposure bias).
- "Why did transformers replace RNNs, and what came back?" — Parallel training and a path of length 1 between any two tokens; linear RNNs and SSMs came back for constant-memory generation.
10. Practice #
The notebook for this lesson runs in Colab (NumPy and a small PyTorch model on the CPU). It has five exercises, each with a check cell and a folded solution:
- Write the forward pass and BPTT of a vanilla RNN in NumPy and check the gradients against autograd.
- Measure for several spectral radii and compare it with and with Pascanu's bound.
- Write one LSTM step by hand and match
nn.LSTMCell(gate order ). - Implement global-norm clipping and check it against
torch.nn.utils.clip_grad_norm_. - Build inputs and targets for next-token prediction, write the loss, then train a tiny character-level RNN on a short text and sample from it.
11. Watch and read #
- Pascanu, Mikolov, Bengio, On the difficulty of training recurrent neural networks (2013): §2.1 (the conditions with ), §2.3 (the "wall" picture), §3.2 (norm clipping, Algorithm 1).
- Dive into Deep Learning, §9.7 Backpropagation through time and §10.1 LSTM.
- Christopher Olah, Understanding LSTM Networks: the standard diagrams of the gates.
- Goodfellow, Bengio, Courville, Deep Learning, chapter 10: sequence modeling, BPTT, long-term dependencies and gated RNNs.
- Andrej Karpathy, The Unreasonable Effectiveness of Recurrent Neural Networks: character-level language models in practice.
- Jozefowicz, Zaremba, Sutskever, An Empirical Exploration of Recurrent Network Architectures (2015): the forget-gate bias of 1, and LSTM against GRU.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.