← All tutorials

On this page

1 Order matters 2 Backpropagation through time unrolls the loop 3 What goes wrong when you actually train one 4 References

RNN Basics: Recurrence and Backpropagation Through Time

A recurrent neural network (RNN) reads a sequence one step at a time and carries a hidden state, its memory, from each step to the next. It learns by unrolling that loop over the whole sequence and backpropagating through every step. The same repeated multiplication that lets it remember makes its gradients fade or blow up over long sequences.

PartWhat it answers
Order matterswhy a sequence needs memory of earlier steps
One equation, reused every timestephow one cell mixes the new input with its memory
Backpropagation through time unrolls the loopwhy the gradient shrinks or blows up over many steps
Why gradients vanish or explodewhy a product of many per-step factors fades or blows up
Clipping keeps the direction and caps the sizehow to cap a gradient that blows up
What goes wrong when you actually train onewhat breaks in practice
Gated RNNs: GRU and LSTMhow gates keep memory alive across many steps

1 Order matters

A feedforward network takes one fixed-size input, multiplies it by weights and applies non-linearities (functions that bend their input, so the network can learn curved patterns). Flatten a sentence, a price series or an audio clip into that one input and which step came when is lost, and sequences of different lengths need padding (dummy steps) or truncating.

With recurrence, you process one input at a time and carry forward some memory, the hidden state h. In code, W and U are learned weight matrices shared by every step, and one more matrix V turns the memory into the prediction. Each step is one dense layer, the building block of an MLP, applied again and again:

import numpy as np

# memory starts empty
h = np.zeros(hidden_dim)
# one input at a time, in order
for x_t in sequence:
    # mix input and memory
    h = np.tanh(W @ x_t + U @ h)
# memory becomes the prediction
y = V @ h
  1. 1Starts the memory h at zero.
  2. 2Reads the inputs in order and mixes each one with the memory.
  3. 3Turns the last memory into the prediction with V.

PyTorch runs the same loop in one call. nn.RNN returns the hidden state at every step and the last one:

import torch
from torch import nn
# x: (batch, T, d_in)
rnn = nn.RNN(
    d_in, d_h,
    batch_first=True)
out, h = rnn(x)
# out: (batch, T, d_h)
# h: (1, batch, d_h)
  1. 1Builds one recurrent layer from input size d_in to hidden size d_h; batch_first=True puts the batch first and time second in x.
  2. 2Runs the whole sequence through the layer.
  3. 3out holds the hidden state at every step; h is the last one, with one row for the single layer.

1.1 One equation, reused every timestep

Each step mixes today's input with yesterday's memory and squashes the mix with tanh into the range from minus one to one. That squeeze keeps the numbers in range. In a vanilla RNN, the plain ungated kind, repeated squashing and multiplying by U make old inputs fade step by step. Here is one step as dataflow:

today's input xt × W yesterday's memory ht−1 × U + tanh ht becomes ht−1 next step
Figure 1. One vanilla RNN step. Squashing makes old inputs fade.

That washout is exactly what the gates were invented to stop (Gated RNNs: GRU and LSTM).

RNN cell t = 1 x1 y1 RNN cell t = 2 x2 y2 RNN cell t = 3 x3 y3 h0 h1 h2 h3
Figure 2. The unrolled RNN. One shared cell handles any length.
One RNN step mixes the new word with the last memory, then squashes it.
at = W·xt + U·ht−1
The mix. xt is the word entering and ht−1 the memory from the last cell; W and U are learned weights that scale each, and · means times. A learned bias also joins the mix, left off this line. at is the mix before squashing.
ht = tanh(at)
The squash. tanh squeezes the mix to between −1 and 1, and ht, the new hidden state, carries both forward. The same W and U are reused at every step, which is why one RNN handles a sentence of any length.

Each hand-off fades the past a little, so over a long sequence the early words wash out.

cell 1 t = 1 turn cell 2 t = 2 lights cell 3 t = 3 on turn turnlights turnlightson h1 h2

Figure 3. A voice command, fade exaggerated (illustrative).

The same two lines run in every cell.
at = W·xt + U·ht−1
The mix, as in the card above: the word above the cell is xt; from cell 2 on, the purple arrow into it is ht−1, and cell 1 starts from an empty memory of zeros.
ht = tanh(at)
The squash gives the new memory that each cell passes to the right.
xt the word enteringht−1 memory from the last cellat the mixht the new hidden state

2 Backpropagation through time unrolls the loop

This section builds on the backward pass in Backpropagation and Gradient Checking, where the chain rule carries a gradient back through layers, and on Cross-Entropy Loss.

In an RNN, gradients flow back through time as well as depth: backpropagation through time (BPTT). Unrolled for T steps, the RNN is a depth-T network that reuses the same weights at every layer, with a loss Lt at every step (say cross-entropy, the standard loss for picking the right class). The symbol ∂ marks a partial derivative: how much one quantity moves when another moves.

Take the voice command of Figure 3. The loss at "on" reaches the memory of "turn" through two hand-offs, so its gradient carries two factors, one per step back. The third line of the card writes that product for a sequence of any length.

In four lines, the loss adds up over time, the shared weights collect a gradient from every step, the path back through time is one long product, and each factor of that product comes from the cell line.
L = ∑t=1..T Lt
The total loss L is the per-step loss Lt summed over all T timesteps.
∂L∂U = ∑k=1..T ∂L∂U(k)
U is shared, so its gradient is the sum of the gradients through each step's copy of it, U(k) at step k. The same holds for W and V.
∂hT∂h1 = ∏k=2..T ∂hk∂hk−1
By the chain rule, how far the last memory hT moves when the first one h1 moves is a product of one factor per step, the newest on the left. ∏ multiplies over every step k from 2 to T, so T − 1 factors. The next line opens one factor up.
∂hk∂hk−1 = tanh′(ak) · U
This comes straight from the cell lines of Section 1.1. The old memory hk−1 enters the mix ak through U, then passes the tanh squash, whose slope tanh′ is at most 1. So each factor is that slope times U (for vectors, a diagonal matrix of slopes times the matrix U). Large U pushes factors above 1, a saturated tanh (one pushed to its flat ends near −1 or 1, where its slope is near 0) pulls them toward 0, and the w of Section 2.1 stands for the size of this one factor.

2.1 Why gradients vanish or explode

In the scalar model, a gradient multiplied by the same number at every step grows or shrinks exponentially.
st = wt
st is the gradient's size after it travels t steps back, and w is the size of one factor from the card above, tanh′ times U, held the same at every step; the raised t is how many times w multiplies itself. In a sequence of T steps the first timestep is T − 1 steps back from the last, so it receives wT−1: below 1 that shrinks toward 0 (vanishing), above 1 it grows without limit (exploding).
step 1step 2step 3step 4lossthe gradient flows back, × w at every stepw = 0.5, it vanishes0.1250.250.51gradientsizew = 1.5, it explodes3.42.251.51gradientsize
Figure 4. Each step multiplies the gradient by w; it fades or blows up.

Here each factor is one number w. In a real RNN each factor is a Jacobian (a matrix of partial derivatives), but the exponential dependence on T is the same.

The exploded end has a standard fix, clipping (Section 2.2).

Figure 5 calls a gradient usable while step 1 still gets between 10−2 and 102 times its size at the loss; below the band it has vanished, and above the band it has exploded. The edges are a rule of thumb for this chart (illustrative).

Figure 5. Gradient size wt on a log scale, from the loss back to step 1.

About 6% reaches step 1, still usable; longer inputs need gated RNNs.

2.2 Clipping keeps the direction and caps the size

Exploding gradients have a cheap fix, gradient clipping: if the gradient is longer than a threshold τ, scale it down to that length and keep its direction (Pascanu et al., 2013).

In a speech or language RNN, one long batch can push the factors above 1. Take w = 1.5 over 28 steps (Figure 5, illustrative): by the gradient-size card above, step 1 gets about 6 × 104 times the gradient, and one update that large throws the weights far off. With τ = 1, clipping scales that gradient back to length 1 and keeps its direction.

Clipping shrinks the whole gradient to the threshold and never stretches it.
g ← g·min(1, τ‖g‖)
g is every parameter gradient flattened into one vector, ‖g‖ is its length, and τ is the threshold. While the length is below τ the factor is 1 and nothing changes; above it every component shrinks by the same factor, so the direction holds and the length lands on τ.
Before clippingthresholdtoo longAfter clippingthresholdsame direction
Figure 6. Clipping keeps the direction and caps the length.

The section Gradient clipping of Optimizers in Practice: AdamW, SAM, Schedules and Clipping shows where the clip runs in the training step and how to pick τ.

Clipping tames the explosions. The bigger problem is vanishing gradients: with no control over what passes forward, old information washes out and the RNN forgets context.


3 What goes wrong when you actually train one

In the truncated BPTT code, rnn is the layer from Section 1, head a linear output layer, loss_fn a per-step loss, optimizer covers both, x and y are inputs and per-step targets with time on axis 1, and k is the chunk length. A longer k learns longer dependencies but stores more steps; a shorter k is cheaper, but no gradient reaches back more than k steps.

h = None
# k steps per chunk; carry h
xs = x.split(k, 1)
ys = y.split(k, 1)
for x_c, y_c in zip(xs, ys):
    out, h = rnn(x_c, h)
    pred = head(out)
    loss = loss_fn(pred, y_c)
    # backprop this chunk only
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    # keep values, cut the graph
    h = h.detach()
  1. 1Starts with no memory and cuts the inputs and targets into chunks of k steps.
  2. 2Runs one chunk forward from the carried memory and scores it.
  3. 3Backpropagates through this chunk only and updates the weights.
  4. 4Keeps the memory values for the next chunk but cuts the graph behind them.

The same chunking works unchanged for nn.GRU; for nn.LSTM, detach both parts of its (h, c) state, as in Gated RNNs: GRU and LSTM.

Clipping fixes gradients that explode, but how does a network keep information for hundreds of steps when gradients vanish? That is the question Gated RNNs: GRU and LSTM answers.

4 References

  1. Pascanu, Mikolov, Bengio (2013). On the difficulty of training recurrent neural networks. ICML. arXiv:1211.5063.