Key ideas
- Recurrence: an RNN reads a sequence one step at a time and keeps a hidden state that summarizes everything seen so far. The new state is computed directly from just two things, the current input and the previous state1, yet through it depends on all earlier inputs. In d2l's row-vector notation , usually with , and the output is 2.
- Weight sharing: the same parameters are used at every time step1, so the parameter count does not grow with the sequence length. Unrolling over 5 or 500 steps adds no parameters (stacked layers, by contrast, each have their own weights).
- Shapes: input size , hidden size , output size give , , (with column vectors, , the matrices are transposed, e.g. is ). For a language model is the vocabulary size: one score per possible next token, turned into probabilities by a softmax2.
- Patterns8: one-to-one (an ordinary network), one-to-many (image captioning: CNN features start an RNN that emits the caption word by word), many-to-one (text classification), many-to-many or sequence-to-sequence (translation). By where the loss sits1: an acceptor (encoder) is supervised only on its final output, e.g. a sentiment label; a transducer is supervised at every position and sums the per-position losses, e.g. part-of-speech tagging.
- Backpropagation through time (BPTT) runs backprop on the unrolled graph. The gradient picks up powers of : components along eigenvalues below 1 in magnitude vanish and those above 1 explode3. Truncated BPTT stops the sum after steps, which can be done by detaching the gradient after a fixed number of steps3, e.g. by detaching the hidden state at the start of every batch.
- Gradient clipping rescales the gradient of all parameters together (one long vector) to norm at most , keeping its direction; it tames exploding gradients but does nothing against vanishing ones2.
- LSTM4: a memory cell with three sigmoid gates. The forget gate keeps or erases old memory, the input gate decides how much of the tanh "input node" is written into the cell, and the output gate decides how much of becomes ; the outer tanh is needed because the cell, a running sum, can leave . The additive cell update lets gradients flow across many steps, easing the vanishing-gradient problem4. Gates are soft versions of a 0/1 mask that chooses, per component, between new input and old memory1.
- GRU5: two sigmoid gates and no separate cell. The reset gate controls how much of enters the candidate (short-term dependencies); the update gate mixes old state and candidate, and copies the state forward while ignoring the input (long-term dependencies)5.
- Bidirectional RNN: one RNN reads left to right, another right to left, and their states are concatenated at each step1, giving features (or if the sizes differ). It needs the whole sequence up front, so it suits tagging and encoding, but not next-token generation, which may only use the leftward context6.
- Seq2seq7: an encoder RNN reads the source, a decoder RNN generates the target. In training the decoder gets
<bos>plus the true target shifted by one (teacher forcing), and padding positions are masked out of the loss; at test time it is fed its own previous prediction until it emits<eos>7. d2l's decoder starts from the encoder's final state, which requires equal numbers of layers and hidden units7; otherwise a learned layer can map one state to the other. The two halves need not even be the same kind of network: captioning pairs a CNN encoder with an RNN decoder8. - Generation: always taking the most likely token tends to loop through the same phrases; sampling from the predicted distribution with a temperature (higher means flatter and more varied) works better8. Each step multiplies -vectors by matrices, so steps cost , and step needs , so the steps cannot run in parallel.
Formulas
Parameters per block of the form : . A vanilla RNN layer has 1 block, a GRU 3, an LSTM 4 (cost per step scales the same way). PyTorch stores two bias vectors per block, so it reports per block.
Worked example
Scalar RNN , , inputs :
- , , .
- .
Every step multiplies by , so the influence of shrinks geometrically. Even a per-step factor of 0.9 (a linear RNN with ) leaves the first of 50 inputs a weight of only . Counting parameters for , (one bias per block): a block has , so RNN 620, GRU 1860, LSTM 2480.
In code
import torch
from torch import nn
for cls in (nn.RNN, nn.GRU, nn.LSTM):
m = cls(input_size=10, hidden_size=20, batch_first=True)
print(cls.__name__, sum(p.numel() for p in m.parameters())) # RNN 640, GRU 1920, LSTM 2560
out, (h, c) = nn.LSTM(10, 20, batch_first=True)(torch.zeros(4, 7, 10))
print(out.shape, h.shape) # torch.Size([4, 7, 20]) torch.Size([1, 4, 20])
bi = nn.GRU(10, 20, batch_first=True, bidirectional=True)
print(bi(torch.zeros(4, 7, 10))[0].shape) # torch.Size([4, 7, 40])
Common pitfalls
- An RNN does not have separate weights per time step; the parameter count is independent of sequence length.
- The hidden-to-hidden matrix is (here ), not input-sized.
- Gradient clipping fixes exploding gradients only; gated cells (LSTM/GRU) are the answer to vanishing ones. Switching from tanh to ReLU does not make clipping unnecessary: on active units nothing damps the powers of 3.
- GRU gates are sigmoids too; both cells mix sigmoid gates with tanh candidates. An LSTM has 3 gates and a cell state, a GRU 2 gates and none.
- In the GRU, keeps the old state and takes the candidate, not the other way round.
- Bidirectional RNNs cannot be used for left-to-right generation, because the backward pass needs future tokens.
- Detaching the hidden state between batches (truncated BPTT) keeps its value but cuts the gradient; it does not reset the state to zero.
- If a classifier reads only the output at the last position (
out[:, -1]), pad short sequences on the left so that the last step comes right after the real tokens; in seq2seq, exclude<pad>targets from the loss7.
References
- Natural Language Processing with Deep Learning (Ruhr University Bochum, 2025/26) — Lecture 6 slides, frames 'Define RNN recursively — Computing current state', 'Graphical visualization of abstract RNN (unrolled)', 'RNN as acceptor or encoder', 'RNN as transducer', 'Bi-directional RNNs', 'How to provide more controlled memory access?'
- Dive into Deep Learning — Sec. 9.5 Recurrent Neural Network Implementation from Scratch (9.5.1 RNN Model, 9.5.2 RNN-Based Language Model, 9.5.3 Gradient Clipping)
- Dive into Deep Learning — Sec. 9.7 Backpropagation Through Time (9.7.1.2 Truncating Time Steps, 9.7.2 Backpropagation Through Time in Detail)
- Dive into Deep Learning — Sec. 10.1 Long Short-Term Memory (LSTM) (introduction, 10.1.1 Gated Memory Cell)
- Dive into Deep Learning — Sec. 10.2 Gated Recurrent Units (GRU) (10.2.1 Reset Gate and Update Gate, 10.2.2 Candidate Hidden State, 10.2.3 Hidden State)
- Dive into Deep Learning — Sec. 10.4 Bidirectional Recurrent Neural Networks (introduction, 10.4.3 Summary)
- Dive into Deep Learning — Sec. 10.7 Sequence-to-Sequence Learning for Machine Translation (10.7.1 Teacher Forcing, 10.7.3 Decoder, 10.7.5 Loss Function with Masking, 10.7.7 Prediction)
- AI for Beginners — Lesson: Generative networks (introduction: RNN architecture patterns; Soft text generation and temperature)