BPTT’s verdict: the gradient k steps back scales as \rho^k: vanishing or exploding. Clipping fixes explosion; vanishing needs architecture.
The fix that stuck: multiplicative gating (Hochreiter & Schmidhuber, 1997).
LSTM: a protected memory cell + three learned gates.
GRU: the streamlined two-gate version.
Depth and direction, compressed to their surviving lessons.
The same gate primitive lives on in SwiGLU MLPs, Mamba, xLSTM — and in every linear recurrence of this chapter.
The gate
The only recurrence whose Jacobian is exactly the identity is an accumulator: \mathbf{S}_t = \mathbf{S}_{t-1} + (\textrm{new}). But it never forgets. Memory needs decisions: write? clear? reveal?
Direct path (gates held fixed): \partial \mathbf{C}_t/\partial \mathbf{C}_{t-1} = \textrm{diag}(\mathbf{F}_t): hold \mathbf{F} \approx 1, \mathbf{I} \approx 0 and the cell (and its gradient) survives: the constant error carousel.
The total derivative adds gate paths through \mathbf{H}_{t-1}: an additive route is supplied, not a guarantee.
\mathbf{O}_t lets a cell accumulate silently, then reveal.
From scratch: parameters
Four heads, one triple() factory each; num_inputs is the embedding dimension:
Walk the sequence, carry (\mathbf{H}, \mathbf{C}):
@d2l.add_to_class(LSTMScratch)def forward(self, inputs, H_C=None):if H_C isNone:# Initial state with shape: (batch_size, num_hiddens) H = d2l.zeros((inputs.shape[1], self.num_hiddens), device=inputs.device) C = d2l.zeros((inputs.shape[1], self.num_hiddens), device=inputs.device)else: H, C = H_C outputs = []for X in inputs: I = d2l.sigmoid(d2l.matmul(X, self.W_xi) + d2l.matmul(H, self.W_hi) +self.b_i) F = d2l.sigmoid(d2l.matmul(X, self.W_xf) + d2l.matmul(H, self.W_hf) +self.b_f) O = d2l.sigmoid(d2l.matmul(X, self.W_xo) + d2l.matmul(H, self.W_ho) +self.b_o) C_tilde = d2l.tanh(d2l.matmul(X, self.W_xc) + d2l.matmul(H, self.W_hc) +self.b_c) C = F * C + I * C_tilde H = O * d2l.tanh(C) outputs.append(H)return outputs, (H, C)
Same recipe as the vanilla RNN (50k windows of 32 BPE tokens, batch 1024, emb 64, hidden 128, 10 epochs, clip 1), so the numbers are directly comparable:
data = d2l.TimeMachine(batch_size=1024, num_steps=32, num_train=50000, num_val=5000)
Three quarters of the LSTM’s recurrent parameters and the best perplexity in this section’s runs: fewer gates converge faster on a short budget. Its 2020s legacy: strip the gates to input-only functions and the recurrence turns linear → minGRU, LRU, SSMs.
Depth and direction, briefly
Layer l reads layer l{-}1 at the same step and itself at the previous step; a one-argument change with num_layers:
Two stacked recurrent layers over three time steps.
No use unchanged as a causal decoder: at sampling time the future does not exist; “next-token training” hands the model the answer. Use for tagging/encoding (ELMo → BERT); fine inside generative systems as the encoder.
Gates everywhere
Where
The gate
Controls
LSTM
forget \mathbf{F}_t
cell-state decay
GRU
update \mathbf{Z}_t
copy vs. overwrite
Highway nets
transform gate
depth routing
GLU/SwiGLU MLPs
\sigma/Swish branch
channel selection
Mamba
step size \Delta_t
linear-state decay
Griffin / mLSTM
recurrence gates
linear-cell decay
SwiGLU’s payoff: measured in the transformer chapter’s matched-parameter sweep.
xLSTM: exponential gates (log-space stabilized) revive the classic cell; its matrix sibling mLSTM joins the family table two sections ahead.
Bottom rows: gates from the input only → linear recurrence → parallel training. The rest of the chapter rides that step.
Recap
Vanishing gradients are architectural; the cure is an additive state path plus learned multiplicative gates.
LSTM: \mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t is the constant error carousel (the protected direct path).
GRU: two gates, convex blend, cheaper, and the section’s best perplexity: it beats the vanilla RNN clearly.
The LSTM needs budget and init care before its machinery pays: architecture and optimization are judged together.