Code Architecture — Learning Phrase Representations using RNN Encoder–Decoder (GRU)
This notebook implements a GRU cell from scratch using raw matrix operations (no nn.GRU), builds both a GRU-based and a vanilla-RNN-based recurrent network, trains both on a sequence-memorisation task, and compares them by plotting gradient norms over time steps — directly demonstrating the GRU’s advantage in preserving information over long sequences.
Section 1 — Setup and imports
What it does: Installs and imports PyTorch, matplotlib, numpy. Seeds all RNGs for reproducibility.
Key packages: torch, torch.nn, matplotlib, numpy
Section 2 — GRU cell from scratch
What it does: Implements the exact GRU equations from the paper (Eqs. 5–8) using raw nn.Linear layers and element-wise operations — no nn.GRU or nn.GRUCell.
Class: GRUCellScratch(nn.Module)
- Reset gate:
r = σ(W_r·x + U_r·h_{t-1})—nn.Linearfor input projection (W_r) and hidden projection (U_r), then sigmoid. - Update gate:
z = σ(W_z·x + U_z·h_{t-1})— same structure, separate parameters. - Candidate:
h̃ = tanh(W·x + U·(r ⊙ h_{t-1}))— the reset gate gates the previous hidden state before the candidate is computed. - New state:
h = z·h_{t-1} + (1−z)·h̃— linear interpolation.
Shapes:
- Input
x:(batch, input_dim) - Hidden
h:(batch, hidden_dim) - All weight matrices:
(hidden_dim, input_dim)or(hidden_dim, hidden_dim) - Output
h:(batch, hidden_dim)
This is a faithful reproduction of Equations 5–8 from the paper, with the paper’s convention that the update gate z keeps the old state (i.e. h = z·h_old + (1−z)·h_new). Note: some frameworks (e.g. PyTorch’s nn.GRU) use the opposite convention; we follow the paper exactly.
Section 3 — Vanilla RNN cell from scratch
What it does: Implements a plain tanh RNN cell for comparison.
Class: VanillaRNNCell(nn.Module)
h = tanh(W·x + U·h_{t-1} + b)— the simplest possible recurrent unit with no gating.
Section 4 — Full RNN wrapper
What it does: Wraps a cell (GRU or vanilla) into a full sequence-processing network that unrolls over time steps.
Class: RecurrentNetwork(nn.Module)
- Takes a cell and processes a sequence of length T, maintaining a running hidden state.
- Input:
(batch, seq_len, input_dim)→ Output:(batch, seq_len, hidden_dim) - At each time step t:
h_t = cell(x_t, h_{t-1}) - Stores all intermediate hidden states for gradient analysis.
Section 5 — Sequence memorisation task
What it does: Creates a synthetic task where the model must reproduce a sequence of random values after reading it — a pure memorisation task that stresses long-range memory.
Task design:
- Input: a sequence of T random vectors (each a one-hot or continuous vector).
- Target: the same sequence (copy task) — the model must output x_t at position t after reading the full sequence.
- This task directly tests whether the recurrent unit can preserve information across T time steps.
Key function: generate_memorisation_data(n_samples, seq_len, input_dim)
Section 6 — Training loop
What it does: Trains both the GRU-based and vanilla-RNN-based networks on the memorisation task with the same hyperparameters, using MSE loss and Adam optimiser.
Key variables:
SEQ_LEN: sequence length (varied: 5, 10, 20, 40)HIDDEN_DIM: 64INPUT_DIM: 16LR: 0.01NUM_EPOCHS: 200
Key function: train_model(cell_type, seq_len, epochs)
Section 7 — Gradient norm analysis
What it does: After training, runs a forward pass and backpropagates loss to the first time step. Measures the gradient norm of the loss with respect to the hidden state at each time step — this is the classic vanishing/exploding gradient diagnostic.
Why this matters: The paper’s motivation for the GRU was precisely to combat the vanishing gradient problem in plain RNNs. By plotting ‖∂L/∂h_t‖ vs. time step t for both architectures, we see that:
- Vanilla RNN: gradients shrink exponentially with time step (vanishing gradient).
- GRU: gradients remain much more stable across time steps thanks to the update gate’s linear interpolation (gradient highway).
Key function: compute_gradient_norms(model, seq_len)
Section 8 — Comparison plots
8a — Loss convergence
Plots training loss vs. epoch for GRU and vanilla RNN at different sequence lengths, showing the GRU converges faster and to lower loss especially for longer sequences.
8b — Gradient norm vs. time step
Plots ‖∂L/∂h_t‖ for each time step t, on a log scale, for both architectures. This is the key figure demonstrating the GRU’s resistance to vanishing gradients.
8c — Accuracy by sequence length
Bar chart comparing final memorisation accuracy for GRU vs. vanilla RNN at sequence lengths 5, 10, 20, 40.
Data flow summary
random sequence (batch, seq_len, input_dim)
↓
┌──────┴──────┐
│ GRU cell │ vs. │ Vanilla RNN cell │
│ unrolled T │ │ unrolled T │
│ times │ │ times │
└──────┬──────┘ └───────┬───────────┘
↓ ↓
hidden states h_1...h_T hidden states h_1...h_T
↓ ↓
output projection output projection
↓ ↓
MSE loss vs. target MSE loss vs. target
↓ ↓
backprop → gradient backprop → gradient
norms per time step norms per time step
↓ ↓
┌──────┴──────┐
│ Plot: │
│ loss curve │
│ grad norms │
│ accuracy │
└─────────────┘
Deliberate simplifications vs. full paper
| Paper | Notebook |
|---|---|
| English→French SMT, 348M words, 15k vocab | Synthetic sequence memorisation task |
| 1000 hidden units, rank-100 embeddings | 64 hidden units, 16-dim inputs |
| Encoder–Decoder for phrase-pair scoring | Standalone GRU vs. vanilla RNN comparison |
| Adadelta optimizer | Adam optimizer |
| BLEU evaluation on WMT’14 | MSE loss + gradient norm analysis |
| Trained for days on GPU | ~3 minutes on Kaggle GPU |
The notebook isolates the paper’s core architectural contribution (the GRU cell with reset/update gates) and demonstrates its advantage over a vanilla RNN through gradient analysis — the exact motivation given in Section 2.3 of the paper. The full encoder–decoder and SMT integration are abstracted away to keep the notebook self-contained and fast.