LSTM & GRU Networks
The previous page
left a SimpleRNN(32) at 0.5856 on IMDB — behind a bag-of-words model that has no
concept of order at all. The problem is the recurrence itself: rewrites the entire state at every step, so information from step 5
has to survive 195 rewrites to reach the classifier.
Gated cells change one thing: the state is updated additively, under the control of learned gates, so information can be carried unchanged for as long as the gates say so. On the same data, same seed, same five epochs, that is worth +0.23 accuracy.
| Cell | Total parameters | Recurrent layer only | Best validation accuracy | Seconds |
|---|---|---|---|---|
SimpleRNN(32) | 322,113 | 2,080 | 0.5856 | 29.5 |
GRU(32) | 326,369 | 6,336 | 0.8156 | 52.3 |
LSTM(32) | 328,353 | 8,320 | 0.8136 | 47.9 |
Most of those parameters are the 320,000-entry embedding matrix, identical in all three. The cells differ by 6,240 parameters and 0.23 accuracy.
What you’ll learn
Section titled “What you’ll learn”- The LSTM’s four gates and the GRU’s three, written out — and re-implemented in NumPy
from a trained layer’s own weights, matching Keras to
6.56e-07. - Why a gate is exactly one more weight matrix pair: parameters scale 1 : 3 : 4.
- What the gates actually did on a real review, including a forget gate averaging 0.7275 — a two-step half-life.
- The one design decision that makes it work: the carry is scaled and added to, never overwritten.
- Where GRU and LSTM differ, and how little it mattered on sentiment (0.0020) against how much it mattered at 80 steps (MAE 0.0056 and 0.0107 against a SimpleRNN’s 0.2984).
- The same long-range experiment at a smaller budget, where every cell sat at the do-nothing baseline and would have supported the opposite conclusion.
The LSTM, exactly
Section titled “The LSTM, exactly”Four gates, each a full linear layer over and :
The second line is the whole idea. The carry is built by scaling the old value and
adding — if and then exactly, and the
information crosses the step untouched. A SimpleRNN cannot express that: every step
pushes the entire state through a matrix multiply and a tanh.
kernel, recurrent, bias = lstm_layer.get_weights() # (features, 4U), (U, 4U), (4U,)
h = np.zeros(units, "float32")
c = np.zeros(units, "float32")
for x in sequence:
z = x @ kernel + h @ recurrent + bias # all four gates in one product
i = sigmoid(z[:units]) # Keras packs them i, f, g, o
f = sigmoid(z[units:2 * units])
g = np.tanh(z[2 * units:3 * units])
o = sigmoid(z[3 * units:])
c = f * c + i * g
h = o * np.tanh(c)Keras stores all four gates in one kernel of width so four matrix multiplies
become one — which is why the weight shapes look surprising until you know to slice them.
Run this loop against the trained layer and the final prediction agrees to 6.56e-07
(0.414397 by hand against 0.414398 from Keras).
That equality is the point: the equations above are not a description of what TensorFlow does, they are what it does.
What the gates cost
Section titled “What the gates cost”| Units | SimpleRNN | GRU | LSTM |
|---|---|---|---|
| 16 | 784 | 2,400 | 3,136 |
| 32 | 2,080 | 6,336 | 8,320 |
| 64 | 6,208 | 18,816 | 24,832 |
| 128 | 20,608 | 62,208 | 82,432 |
Keras’ GRU carries a second bias vector for its reset-after variant, which is why 2,400 is a little above .
What the gates did
Section titled “What the gates did”Take the trained LSTM(32) — validation accuracy 0.8136 — and recompute every gate for
one review. This review is 68 tokens long inside a 200-step tensor, so it opens with 132
padding steps.
| Signal | Mean over the padding | Mean over the text |
|---|---|---|
| forget gate | 0.7215 | 0.7392 |
| input gate | 0.5199 | 0.5185 |
| output gate | 0.5467 | 0.5188 |
| carry state |c| | 1.3570 | 0.4186 |
| hidden state |h| | 0.4126 | 0.1786 |
Three honest observations:
- The gates barely move between padding and text (forget 0.7215 → 0.7392). The states differ because the inputs differ, not because the cell recognised anything.
- The carry is three times larger over the padding. Repeated identical input drives
the additive update to a large fixed point that the first real tokens have to undo —
the same attractor measured on the intro page, and a concrete reason to pass
mask_zero=True. - A mean forget gate of 0.7275 is not long-term memory. . This trained cell is smoothing over a few tokens, which is what sentiment needs. The capability to hold a value for hundreds of steps is there — it is what means — but this task never asked for it.
The GRU: the same idea, one fewer gate
Section titled “The GRU: the same idea, one fewer gate”| LSTM | GRU | |
|---|---|---|
| states carried | two ( and ) | one () |
| gates | 4 | 3 |
| parameters at 32 units | 8,320 | 6,336 |
| keep vs write | independent (, ) | coupled (, ) |
| measured here | 0.8136 in 47.9s | 0.8156 in 52.3s |
The coupling is the real difference: a GRU cannot both keep the old value and write the new one, because its two weights sum to 1. An LSTM can. In exchange the GRU is 24% smaller, and here it scored 0.0020 higher — comfortably inside seed noise. Try both.
flowchart LR
subgraph simple["SimpleRNN: the state is overwritten"]
A["h_{t-1}"] --> B["tanh(Wx·x + Wh·h + b)"] --> C["h_t"]
end
subgraph lstm["LSTM: the carry is scaled and added to"]
D["c_{t-1}"] --> E["× f_t"] --> F["+ i_t ⊙ g_t"] --> G["c_t"]
G --> H["h_t = o_t ⊙ tanh(c_t)"]
end
Where all three still failed
Section titled “Where all three still failed”The long-range test is the adding problem: uniform values with two marked positions — one near the start, one in the second half — and the target is the sum of the two marked values. Carrying a number the length of the sequence is the entire task, and predicting the mean sum of 1.0 is the do-nothing baseline.
At 32 units and 80 epochs, the separation is total:
| Cell | Recurrent parameters | Adding-problem MAE | Against the 0.3324 baseline |
|---|---|---|---|
SimpleRNN(32) | 1,153 | 0.2984 | +0.0340 |
GRU(32) | 3,489 | 0.0056 | +0.3268 |
LSTM(32) | 4,513 | 0.0107 | +0.3217 |
The same experiment at a smaller budget said the opposite
Section titled “The same experiment at a smaller budget said the opposite”The first version of this measurement used 16 units and 20 epochs:
| Cell | Adding-problem MAE | Baseline |
|---|---|---|
SimpleRNN(16) | 0.3333 | 0.3324 |
GRU(16) | 0.3005 | 0.3324 |
LSTM(16) | 0.3328 | 0.3324 |
All three sat at “predict the mean”. Had the page stopped there it would have concluded that gates do not help with long-range dependency — the exact opposite of what the same code shows with four times the epochs. The adding problem is slow to crack, and a cell scoring at the baseline has not been shown to be incapable, only untrained. That failure is kept here on purpose, because it is the easiest way to publish a confidently wrong result.
Drag the gate to 1.0 and the memory never decays. That is the regime the LSTM was
designed for, and it is why Keras initialises the forget-gate bias to 1 by default
(unit_forget_bias=True, Exercise 6): the cell starts out keeping its carry rather than
erasing it.
Pitfalls
Section titled “Pitfalls”- Assuming a gated cell fixes long range automatically. A measured 0.7275 forget gate is a two-step half-life. The capability exists; whether training uses it is up to the task.
- Reading a baseline-level score as “this cell cannot do it”. At 16 units and 20 epochs all three cells sat at the adding-problem baseline; at 32 units and 80 epochs the GRU scored 0.0056. The first run measured the budget.
- Choosing LSTM over GRU on principle. 0.8136 against 0.8156 here, with the GRU 24% smaller.
- Slicing the gates in the wrong order. Keras packs one kernel of width as
i, f, g, o. Get it wrong and the cell runs silently and learns nothing sensible. - Ignoring what padding does to the carry. Mean |c| was 1.3570 over the padding
against 0.4186 over the text — pass
mask_zero=True. - Paying for gates on short sequences. At 10–20 timesteps a
SimpleRNNis often fine and 3–4× cheaper. - Reporting a cell comparison without wall-clock. GRU and LSTM cost roughly 1.6× a SimpleRNN here, and on CPU that is the number that decides your iteration speed.
- The LSTM’s four gates control an additive carry, — the
mechanism a
SimpleRNNlacks. - Keras packs the gates into one kernel of width ordered
i, f, g, o; the NumPy re-implementation matched Keras to 6.56e-07. - Parameters scale 1 : 3 : 4 — 2,080 / 6,336 / 8,320 at 32 units, independent of sequence length.
- On IMDB: SimpleRNN 0.5856, GRU 0.8156, LSTM 0.8136. The gates were worth +0.23.
- On the adding problem at 80 steps: GRU 0.0056, LSTM 0.0107, SimpleRNN 0.2984 against a 0.3324 baseline — and at a quarter of the budget, all three scored at that baseline.
- Measured gate means: forget 0.7275, input 0.5192, output 0.5327; carry 1.3570 over padding against 0.4186 over the text.
- GRU couples keep and write; LSTM keeps them independent. The two scored within 0.0020.
Dropout that actually works on a recurrent layer, stacking, and reading a sequence backwards: Advanced Recurrent Layers.
-
What does the LSTM have that a SimpleRNN does not?
A SimpleRNN pushes its entire state through a matrix multiply and a tanh at every step, so nothing can pass through untouched.
pch.quizShowAnswer
B — An additive carry update, c_t = f_t·c_{t-1} + i_t·g_t — with f near 1 and i near 0 the carry crosses a step completely unchanged — A SimpleRNN pushes its entire state through a matrix multiply and a tanh at every step, so nothing can pass through untouched.
-
SimpleRNN, GRU and LSTM cost 2,080, 6,336 and 8,320 parameters at 32 units on the same input. Why that ratio?
params = G*(features*U + U^2 + U) with G = 1, 3, 4. Keras' GRU adds a second bias vector, which is why it sits slightly above 3x.
pch.quizShowAnswer
B — Because each gate is another full weight matrix pair plus a bias — one set for SimpleRNN, three for GRU, four for LSTM — params = G*(features*U + U^2 + U) with G = 1, 3, 4. Keras' GRU adds a second bias vector, which is why it sits slightly above 3x.
-
The trained LSTM's forget gate averaged 0.7275. What does that imply about its memory?
0.7275^10 = 0.0412. The capability to hold a value exists (f near 1); this task simply never needed it.
pch.quizShowAnswer
B — An untouched memory decays about 27% per step — a two-step half-life, so this cell is smoothing over a few tokens rather than storing anything long-term — 0.7275^10 = 0.0412. The capability to hold a value exists (f near 1); this task simply never needed it.
-
On the adding problem at 16 units and 20 epochs, two of three cells scored at the do-nothing baseline. What follows?
It is the same class of error as judging batch normalisation by a run too short for its moving averages to converge.
pch.quizShowAnswer
B — Nothing about capability — the budget was too small, and a baseline-level score means untrained, not incapable — It is the same class of error as judging batch normalisation by a run too short for its moving averages to converge.
-
What is the practical difference between a GRU's update gate and an LSTM's input and forget gates?
That coupling is what saves the GRU a quarter of its parameters, and here it cost nothing measurable: 0.8156 against 0.8136.
pch.quizShowAnswer
B — The GRU couples them — it interpolates with z and 1-z, so it cannot keep the old value and write a new one at once; the LSTM's f and i are independent — That coupling is what saves the GRU a quarter of its parameters, and here it cost nothing measurable: 0.8156 against 0.8136.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading