Advanced Recurrent Layers (Dropout, Stacking, Bidirectional)
A single LSTM(32) on 5,000 IMDB reviews scores 0.8260. Three refinements are the
standard next moves: regularise it, stack it, run it in both directions. All three were
measured here on the identical model, data and seed, at eight epochs.
| Change | Best validation accuracy | Against the baseline | Seconds |
|---|---|---|---|
baseline LSTM(32) | 0.8260 | — | 129.5 |
dropout=0.3 | 0.8364 | +0.0104 | 141.3 |
recurrent_dropout=0.3 | 0.8180 | −0.0080 | 259.2 |
| both at 0.3 | 0.8240 | −0.0020 | 284.3 |
| 2 stacked layers | 0.8204 | −0.0056 | 362.5 |
| 3 stacked layers | 0.8188 | −0.0072 | 557.4 |
Bidirectional (32+32) | 0.8448 | +0.0188 | 225.3 |
Bidirectional (16+16, matched width) | 0.8284 | +0.0024 | 262.7 |
One of the three helped. One cost twice the wall-clock to lose accuracy. One helped mostly because it doubled the model’s width.
What you’ll learn
Section titled “What you’ll learn”- The difference between
dropoutandrecurrent_dropout— different tensors, different masks, very different cost. - Why
recurrent_dropoutdoubled the training time: it drops off TensorFlow’s fused kernel. - The
return_sequencesrule that makes stacking possible, and why depth lost here. - What
Bidirectionalactually costs, and how to tell a real gain from a width gain. - The one setting a forecaster must never use, and the leak that makes it look good.
Two dropout arguments, two different tensors
Section titled “Two dropout arguments, two different tensors”dropoutmasks the input at each step.recurrent_dropoutmasks the previous state — inside the loop.
Both use the same mask at every timestep (a “variational” mask). That matters: a fresh
mask per step would resample the noise 200 times along one sequence and destroy anything
the state was carrying. A Dropout layer placed between two recurrent layers does
exactly that resampling — which is fine there, because it sits outside the recurrence.
The cost nobody mentions
Section titled “The cost nobody mentions”recurrent_dropout took 259.2 seconds against 129.5 — exactly 2.0×. The reason is
implementation, not mathematics: cuDNN and TensorFlow’s fused CPU kernels implement the
whole recurrence as one optimised operation, and applying a dropout mask inside the
loop forces Keras to fall back to the generic step-by-step path.
The same is true of mask_zero=True, which every page in this phase uses. The two
together are why this module was the slowest in the phase.
| Setting | Fused kernel | Measured cost |
|---|---|---|
plain LSTM | yes | 1.0× |
dropout=0.3 | yes | 1.1× |
recurrent_dropout=0.3 | no | 2.0× |
If you want regularisation and speed, use dropout plus a Dropout layer after the
recurrent layer, and reach for recurrent_dropout only when the gap says you need it.
Stacking
Section titled “Stacking”Every recurrent layer except the last needs return_sequences=True, or the next one
receives a vector where it expects a sequence:
model = keras.Sequential([
keras.layers.Input((200,)),
keras.layers.Embedding(10000, 32, mask_zero=True),
keras.layers.LSTM(32, return_sequences=True), # (batch, 200, 32)
keras.layers.LSTM(32), # (batch, 32)
keras.layers.Dense(1, activation="sigmoid"),
])| Layers | Parameters | Best validation accuracy | Seconds |
|---|---|---|---|
| 1 | 328,353 | 0.8260 | 188.8 |
| 2 | 336,673 | 0.8204 | 362.5 |
| 3 | 344,993 | 0.8188 | 557.4 |
Stacking is genuinely useful when the task has structure at several timescales — speech (phonemes → words), music, long documents. It is not a general-purpose capacity dial, and on this task it was a pure loss: 3× the training time for −0.0072 accuracy.
Bidirectional
Section titled “Bidirectional”Bidirectional runs two copies of the layer — one forward, one over the reversed
sequence — and concatenates their outputs. Reading a review backwards is not a worse way
to read it: the backward model alone scored 0.8180 against the forward model’s
0.8260.
| Setup | Parameters | Best validation accuracy | Against forward |
|---|---|---|---|
forward LSTM(32) | 328,353 | 0.8260 | — |
backward LSTM(32) | 328,353 | 0.8180 | −0.0080 |
Bidirectional(LSTM(32)) | 336,705 | 0.8448 | +0.0188 |
Bidirectional(LSTM(16)) | 326,305 | 0.8284 | +0.0024 |
Always include the matched-width control. Any wrapper that doubles a layer’s output width will look good against the un-doubled version; the question is whether the structure helped, and here it was worth 0.0024, not 0.0188.
flowchart TB A["is the train-validation gap large?"] -->|"no"| B["regularisation will not help
— add capacity or data"] A -->|"yes"| C["dropout=0.3
+0.0104, gap 0.1702 → 0.1280"] C --> D{"still overfitting?"} D -->|"yes"| E["recurrent_dropout
— but it costs 2.0x wall-clock"] D -->|"no"| F["done"] G["is the task hierarchical
in time?"] -->|"yes"| H["stack, with
return_sequences=True"] G -->|"no"| I["one layer
— depth lost 0.0072 here"] J["will the whole sequence
exist at prediction time?"] -->|"yes"| K["Bidirectional
— check matched width"] J -->|"no (forecasting)"| L["forward only
— the backward pass reads the future"]
The one that is a bug, not a trade-off
Section titled “The one that is a bug, not a trade-off”For classification, Bidirectional is a legitimate choice: the whole review exists
before you classify it. For forecasting, it is a leak. The backward pass at timestep
has consumed timesteps , and at prediction time those have not
happened.
The failure mode is nasty because it validates beautifully. Your offline metric improves, because your offline data contains the future; production then fails silently. Exercise 5 demonstrates it directly: on a task where the answer sits at the last timestep and must be reported from the first, a forward model cannot exceed chance and a bidirectional one solves it.
The same reasoning covers every “look-ahead” mistake in the phase — shuffled splits, scalers fitted on all the data, and target-derived features.
Pitfalls
Section titled “Pitfalls”- Reaching for
recurrent_dropoutfirst. It cost 2.0× the wall-clock and lost 0.0080 here, while plaindropoutgained 0.0104 for 1.1×. - Judging regularisation by accuracy alone. The useful signal was the gap: 0.1702 →
0.1280 under
dropout, essentially unchanged (0.1708) underrecurrent_dropout. - Stacking as a capacity dial. Three layers cost 3× the time for −0.0072. Stack when the task is hierarchical in time, not when you want “more model”.
- Forgetting
return_sequences=Trueon all but the last layer. It raises a shape error at build time, which is the good case; in a functional model with aReshapeit can silently mean something else. - Quoting a
Bidirectionalgain without a matched-width control. +0.0188 became +0.0024 once the widths were equalised. - Using
Bidirectionalin a forecaster. The backward pass reads timesteps that do not exist at prediction time, and it validates better for exactly that reason. - Assuming backward is worse than forward. 0.8180 against 0.8260 — a different reading order, not a broken one.
dropoutmasks the input,recurrent_dropoutmasks the previous state, and both hold one mask fixed across all timesteps.- Measured:
dropout=0.3gave the only real win (+0.0104, gap 0.1702 → 0.1280);recurrent_dropout=0.3lost 0.0080 at 2.0× the wall-clock because it disables the fused kernel. - Stacking cost accuracy at every depth (0.8260 → 0.8204 → 0.8188) and roughly 180 seconds per layer.
Bidirectionalscored 0.8448 against 0.8260 — but only +0.0024 once width was matched.- Backward-only scored 0.8180: a different reading order, not a worse one.
- No forecaster may use
Bidirectional; the backward pass reads the future.
Recurrence has now been measured from every angle, including the angles where it loses. One angle remains, and it is the bridge to the next phase: the fixed-size state that a recurrent encoder hands to its decoder, and the mechanism invented to get around it. Attention Before Transformers (Additive and Bahdanau).
-
What is the difference between dropout and recurrent_dropout on an LSTM?
Measured: 141.3 seconds for dropout against 259.2 for recurrent_dropout, on the identical model.
pch.quizShowAnswer
B — dropout masks the input x_t; recurrent_dropout masks the previous state h_{t-1} inside the recurrence — and only the second one disables the fused kernel — Measured: 141.3 seconds for dropout against 259.2 for recurrent_dropout, on the identical model.
-
recurrent_dropout=0.3 left the train-validation gap at 0.1708 and lost 0.0080 accuracy, while dropout=0.3 cut the gap to 0.1280 and gained 0.0104. What do you conclude?
The gap is the diagnostic. A regulariser that does not move the gap is not regularising, whatever it does to the score.
pch.quizShowAnswer
B — On this task plain dropout is the better first move — it regularised more and cost half as much; recurrent_dropout is what you try when the gap is still too large — The gap is the diagnostic. A regulariser that does not move the gap is not regularising, whatever it does to the score.
-
Stacking went 0.8260 to 0.8204 to 0.8188 while training time went 189s to 362s to 557s. When is stacking still the right move?
Sentiment on short reviews is not hierarchical in time, so the second layer added optimisation difficulty and nothing else.
pch.quizShowAnswer
B — When the task has structure at several timescales — speech, music, long documents — so a second layer has something to build on — Sentiment on short reviews is not hierarchical in time, so the second layer added optimisation difficulty and nothing else.
-
Bidirectional scored 0.8448 against a forward LSTM's 0.8260. Why is that not a +0.0188 result?
Any wrapper that doubles width beats the un-doubled version. The matched-width control is what isolates the structural effect.
pch.quizShowAnswer
B — Because it also doubled the output width — two 16-unit directions at matched parameters scored 0.8284, a gain of only +0.0024 — Any wrapper that doubles width beats the un-doubled version. The matched-width control is what isolates the structural effect.
-
Why must a forecasting model never use Bidirectional?
Same family as a shuffled split or a scaler fitted on the full series: information at training time that will not exist at inference time.
pch.quizShowAnswer
B — The backward pass consumes later timesteps, which at prediction time are the future — so the model validates better precisely because it is leaking — Same family as a shuffled split or a scaler fitted on the full series: information at training time that will not exist at inference time.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading