🌙 30-Day AI Ramadan Series  ·  Day 20 of 30
Deep Dive· Mar 10, 2026· 30 min read· Sequence Models

Recurrent Neural Networks:
The First Serious Attempt at Sequence Modelling

Before transformers, before attention, there were RNNs — and they were a genuine breakthrough. For the first time, a neural network could read a sentence word by word, carry memory forward, and produce meaning that depended on order. They powered early machine translation, speech recognition, and text generation. Then they broke — in a very specific, mathematically precise way. Understanding how and why is essential to appreciating everything that came after.

← Back to Blog ☰ Categories

A feedforward network reads a sentence the way a bad student reads a dictionary —
every word in isolation, with no memory of what came before.

An RNN reads it the way a person reads a novel —
each word understood in light of everything already read.

That was the idea. The execution, as we'll see, was flawed in a beautiful way.

01 — Why Feedforward Networks Cannot Handle Sequences

Everything we've built so far — perceptrons, CNNs, basic deep networks — shares one fundamental assumption: inputs are independent of each other. Feed in an image, get a classification. Feed in another image, get another classification. The model has no memory of the first image when it processes the second.

Language violates this assumption completely. The meaning of every word in a sentence depends on the words that came before it. Consider:

A feedforward network processing "bank" as an isolated token cannot know which meaning applies. It would need to see the entire context — but fixed-size inputs mean you must decide in advance how many words of context to include. A 10-word window misses long-range dependencies. A 500-word window is computationally absurd and still fixed. And crucially, the same pattern (subject → verb → object) means something different at position 1 vs position 50 in a sentence.

What's needed is a model that:

Processes tokens sequentially
Reads word 1, then word 2, then word 3 — in order, like a human reader. Not all at once like a bag-of-words model.
Maintains a running memory
As it reads each word, it updates an internal state that carries information from all previous words. Word 50 can be influenced by word 1.
Works on variable-length inputs
A sentence can be 5 words or 500 words. The model should handle both with the same parameters — no fixed-size input constraint.
Shares parameters across positions
The grammar rule "verb follows subject" applies in position 2 or position 200. The same weights should capture this pattern regardless of where it appears.

The Recurrent Neural Network was designed to satisfy all four requirements. In 1986, Rumelhart, Hinton and Williams formalised the idea; through the 1990s and 2000s it became the dominant architecture for any task involving sequences: language, speech, music, time series. Let's build it from scratch.

02 — The RNN Cell: Architecture and Forward Pass

An RNN processes a sequence one step at a time. At each step t, it receives two inputs: the current token x_t and a hidden state h_{t-1} carrying all information from previous steps. It combines them to produce a new hidden state h_t, which becomes the memory passed to the next step.

h_t = tanh( W_h · h_{t-1} + W_x · x_t + b )

h_t     — hidden state at step t         shape: (hidden_size,)
h_{t-1} — hidden state from step t-1     shape: (hidden_size,)
x_t     — input at step t              shape: (input_size,)
W_h     — recurrent weight matrix       shape: (hidden_size, hidden_size)
W_x     — input weight matrix          shape: (hidden_size, input_size)
b       — bias                      shape: (hidden_size,)
tanh    — squashes output to (−1, +1), keeps hidden state bounded

The output at each step can be computed as: y_t = W_y · h_t + b_y. Depending on the task, you might use the output at every step (sequence labelling), only the last step (classification), or generate a new sequence (translation).

The Unrolled View

An RNN is often shown as a loop — one cell that repeatedly applies the same operation. But to understand training, it helps to "unroll" it — draw out each timestep as a separate copy of the cell, all sharing the same weights:

x_1 x_2 x_3 x_4 x_5 │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ h_0 →[RNN]→ h_1 →[RNN]→ h_2 →[RNN]→ h_3 →[RNN]→ h_4 →[RNN]→ h_5 │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ y_1 y_2 y_3 y_4 y_5 All [RNN] cells share the SAME weights W_h, W_x, b h_0 is initialised to zeros (or learned) Arrows left→right = hidden state (memory) flowing through time

This unrolled view reveals something important: the RNN is essentially a very deep feedforward network where depth = sequence length, and all layers share the same weights. A 100-word sentence is equivalent to a 100-layer network. This is the source of both the RNN's power and its fundamental problem.

Worked Forward Pass

Let's trace through a tiny example manually. Sentence: "I love NLP". hidden_size=2, input_size=2. All weights initialised to 0.5, bias=0, h_0=[0,0].

Step 1: x_1 = "I" = [1, 0]
pre_act = W_h·h_0 + W_x·x_1 = [0,0] + [0.5×1 + 0.5×0, 0.5×1 + 0.5×0] = [0.5, 0.5]
h_1 = tanh([0.5, 0.5]) = [0.462, 0.462]

Step 2: x_2 = "love" = [0, 1]
pre_act = W_h·h_1 + W_x·x_2
        = [0.5×0.462+0.5×0.462, ...] + [0.5×0+0.5×1, ...]
        = [0.462, 0.462] + [0.5, 0.5] = [0.962, 0.962]
h_2 = tanh([0.962, 0.962]) = [0.745, 0.745]

Step 3: x_3 = "NLP" = [1, 1]
pre_act = [0.5×0.745×2, ...] + [0.5×1+0.5×1, ...]
        = [0.745, 0.745] + [1.0, 1.0] = [1.745, 1.745]
h_3 = tanh([1.745, 1.745]) = [0.942, 0.942]  ← final hidden state encodes full sentence

🔍 What the Hidden State Encodes

After step 3, h_3 = [0.942, 0.942] is the RNN's compressed representation of the entire sequence "I love NLP". It contains — in theory — everything the network learned to remember about those three words. This final hidden state is what you'd feed into a classifier for sentiment analysis, or pass as the initial state of a decoder for translation. The hidden state is the RNN's "working memory."

The critical word above is "in theory." In practice, for long sequences, this memory breaks down in a very specific way — which we'll derive precisely in §04.

03 — RNN Variants: Many-to-One, One-to-Many, Many-to-Many

The RNN architecture is flexible — by choosing which outputs to use, you can handle a wide range of sequence tasks:

TypeInput → OutputHow it worksApplications
Many-to-one Sequence → single value Process full sequence, use only final hidden state h_T for prediction Sentiment analysis, text classification, spam detection
One-to-many Single input → sequence Single input sets h_0; model generates tokens one by one, each output fed back as next input Image captioning, music generation from a seed
Many-to-many (equal) Sequence → same-length sequence Produce an output at every timestep, aligned with each input POS tagging, NER, per-token classification
Many-to-many (seq2seq) Sequence → different-length sequence Encoder RNN compresses input into context vector; decoder RNN generates output sequence Machine translation, summarisation, question answering

The seq2seq architecture (Sutskever et al., 2014) was the major breakthrough that brought RNNs into production NLP. An encoder RNN reads the source sentence and compresses it into a single fixed-size context vector (the final hidden state). A decoder RNN then uses this context vector to generate the target sentence one token at a time. Google Translate ran on this architecture from 2016–2017.

📌 Bidirectional RNNs

A standard RNN only has access to past context — when processing word 5, it has seen words 1–4 but not words 6–10. For many tasks (NER, POS tagging), knowing what comes after a word is just as important as what comes before. "Bank" in "I walked to the river bank" needs "river" which comes before, but also benefits from "to deposit money" which might come after.

A Bidirectional RNN (BiRNN) runs two RNNs: one left-to-right and one right-to-left. Their hidden states are concatenated at each step, giving every token access to both past and future context. BiLSTMs (bidirectional LSTMs) were state-of-the-art for NER and POS tagging for years, right until BERT replaced them in 2018.

04 — Backpropagation Through Time: Why Training RNNs Is Hard

Training an RNN means computing gradients and updating the weights W_h, W_x, b. We use backpropagation — but on the unrolled network, gradients flow backwards through time: from the loss at the final step, back through every hidden state, all the way to step 1. This is called Backpropagation Through Time (BPTT).

Let's trace what happens to the gradient as it flows backwards through T steps:

Forward:   h_t = tanh( W_h · h_{t-1} + W_x · x_t + b )

∂L/∂h_1 = ∂L/∂h_T · ∏(t=2 to T) ∂h_t/∂h_{t-1}

Each factor:   ∂h_t/∂h_{t-1} = diag(1 − tanh²(·)) · W_h
               = diag(sech²(·)) · W_h    (tanh derivative)

So: ∂L/∂h_1 ∝ W_h^(T-1) · [product of T-1 tanh derivatives]

The gradient at step 1 involves multiplying W_h by itself T−1 times and multiplying T−1 tanh derivative terms. This is where the problem emerges — and it splits into two distinct failure modes:

Failure Mode 1: Vanishing Gradients

The tanh derivative sech²(x) = 1 − tanh²(x) has a maximum value of 1.0 (at x=0) and shrinks toward 0 as |x| grows. In practice, during training, the pre-activations of tanh rarely sit exactly at 0 — they spread out and the derivatives are typically well below 1.0. If the largest singular value of W_h is also less than 1 (which is common with standard initialisation), then each multiplication by W_h shrinks the gradient further. Over T steps:

Gradient magnitude at each step (relative to step T, tanh saturated, ||W_h|| = 0.9)
Step T (loss) — magnitude: 1.000
Step T−5         — magnitude: 0.590
Step T−10        — magnitude: 0.349
Step T−20        — magnitude: 0.122
Step T−30        — magnitude: 0.042
Step T−40        — magnitude: 0.015
Step T−50        — magnitude: ~0.001 (effectively zero)

By step T−50, the gradient signal is effectively zero. The weights W_x and W_h at early timesteps receive no meaningful update signal — the model simply cannot learn that "The animal at the beginning of the sentence" is the referent for "it" 50 words later. Long-range dependencies are invisible to the gradient.

Failure Mode 2: Exploding Gradients

The opposite problem: if the largest singular value of W_h is greater than 1, the gradient explodes exponentially over T steps. A gradient that starts at magnitude 1.0 can reach 10^15 after 50 steps with ||W_h|| = 1.5. This causes weight updates so large that the model parameters become NaN. Exploding gradients are the more visible failure — training simply diverges. Vanishing gradients are more insidious — training appears to proceed normally, but the model quietly fails to learn long-range patterns.

⚠️ The Practical Impact

Vanishing gradients (the slow killer): The model trains fine on short sequences. On long sequences, it learns local patterns (last few words) but ignores distant context. Sentiment analysis "This movie had great visuals, stunning music, beautiful cinematography, but the plot was absolutely terrible" — the model may predict positive sentiment because "great," "stunning," "beautiful" are recent and "terrible" is buried earlier in gradient terms.

Exploding gradients (the loud failure): Loss suddenly spikes to infinity, gradients become NaN, training crashes. Fix: gradient clipping — cap gradient norm at a threshold (typically 1.0 or 5.0) before applying the update. This is still standard practice in any RNN-based training loop today.

05 — What RNNs Are Good At (and Where They Still Win)

It's easy to read the vanishing gradient problem and conclude RNNs are useless. That's wrong. RNNs are excellent for tasks where:

Task typeWhy RNNs work wellExamples
Short sequences (<50 tokens) Gradient decay is manageable; local dependencies are sufficient Short text classification, character-level generation
Online / streaming data Process token by token in real time; no need to see full sequence first Real-time speech recognition, financial tick data, IoT sensor streams
Fixed-length output from variable input Final hidden state is a natural fixed-size summary Encoding short documents into vectors
Resource-constrained settings Far fewer parameters than transformers; constant memory per step Embedded systems, edge devices, mobile applications
Time series with local patterns Sequential structure matches the data's temporal structure naturally Stock price prediction, weather forecasting (short horizon)

RNNs are also O(1) in memory per step — the hidden state is a fixed-size vector regardless of how many tokens you've processed. A transformer's attention is O(n²) in sequence length. For extremely long sequences where you need constant memory, RNN-style architectures (and their successors like Mamba) are still competitive.

06 — Partial Fixes: What People Tried Before LSTM

The deep learning community didn't give up on RNNs when the vanishing gradient problem was identified. Several partial solutions emerged:

1
Gradient clipping. Before applying the weight update, check the gradient norm. If it exceeds a threshold τ (typically 1.0–5.0), scale the entire gradient vector down: g = g × (τ / ||g||). This prevents exploding gradients completely. Still used in every modern training loop that involves RNNs or even transformers. Does nothing for vanishing gradients.
2
Truncated BPTT. Instead of backpropagating through the full sequence length T, backpropagate only through the last k steps (typically k=20–50). This caps the depth of the gradient computation, limiting both vanishing and exploding. The cost: the model cannot learn dependencies spanning more than k steps. A workaround, not a solution.
3
Better weight initialisation. Initialising W_h as an orthogonal matrix (singular values all = 1.0) means the gradient neither vanishes nor explodes due to W_h. This helps significantly in practice and is standard in PyTorch's RNN initialisation. Still insufficient for very long sequences because the tanh derivative terms still shrink.
4
ReLU instead of tanh. ReLU has derivative 1 for positive inputs — no shrinking from the activation function. But ReLU-RNNs tend to explode unless very carefully initialised, and they produce unbounded hidden states. In practice, harder to train than tanh-RNNs. The fundamental issue remained: the recurrent weight matrix W_h being multiplied T-1 times.

None of these were satisfying. The community needed an architectural solution — a way to give the network a direct gradient path from later timesteps to earlier ones, bypassing the multiplicative chain that caused the decay. That solution was the LSTM, which we'll cover in full in Day 21.

07 — Code: RNN from Scratch + PyTorch Comparison

Three implementations: a manual RNN cell showing every weight matrix, a full sequence processor with gradient norm tracking to visualise vanishing, and the PyTorch built-in for comparison. Then a demonstration of gradient clipping.

import torch
import torch.nn as nn
import numpy as np

# ─────────────────────────────────────────────────────────────────────────
# 1. RNN Cell from scratch — every operation explicit
# ─────────────────────────────────────────────────────────────────────────
class RNNCellManual(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.hidden_size = hidden_size
        # Two weight matrices + one bias
        self.W_x = nn.Linear(input_size,  hidden_size, bias=False)  # input → hidden
        self.W_h = nn.Linear(hidden_size, hidden_size, bias=False)  # hidden → hidden (recurrent)
        self.b   = nn.Parameter(torch.zeros(hidden_size))

    def forward(self, x_t, h_prev):
        """
        x_t:    (batch, input_size)
        h_prev: (batch, hidden_size)
        returns h_t: (batch, hidden_size)
        """
        pre_act = self.W_x(x_t) + self.W_h(h_prev) + self.b
        h_t = torch.tanh(pre_act)
        return h_t


# ─────────────────────────────────────────────────────────────────────────
# 2. Full sequence RNN — processes a whole sequence, returns all hidden states
# ─────────────────────────────────────────────────────────────────────────
class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        self.cell    = RNNCellManual(input_size, hidden_size)
        self.fc_out  = nn.Linear(hidden_size, output_size)
        self.hidden_size = hidden_size

    def forward(self, x):
        """x: (batch, seq_len, input_size)"""
        B, T, _ = x.shape
        h = torch.zeros(B, self.hidden_size)   # h_0 initialised to zeros
        hidden_states = []
        for t in range(T):
            h = self.cell(x[:, t, :], h)       # step through sequence
            hidden_states.append(h)
        all_h = torch.stack(hidden_states, dim=1)  # (B, T, hidden_size)
        out   = self.fc_out(all_h[:, -1, :])    # use final hidden state for classification
        return out, all_h


# ─────────────────────────────────────────────────────────────────────────
# 3. Vanishing gradient demonstration
#    Train on a task that requires remembering the FIRST token after T steps
# ─────────────────────────────────────────────────────────────────────────
def demonstrate_vanishing_gradient(seq_lengths=[5, 20, 50, 100]):
    print("Gradient magnitude at step 1 (should be large to learn long-range deps):")
    print(f"{'Seq length':>12} | {'||∂L/∂h_1||':>14} | {'Assessment':>20}")
    print("-" * 52)

    for T in seq_lengths:
        model = SimpleRNN(input_size=8, hidden_size=16, output_size=2)
        x = torch.randn(4, T, 8)           # random sequence
        y = torch.randint(0, 2, (4,))        # random labels

        out, all_h = model(x)
        loss = nn.CrossEntropyLoss()(out, y)
        loss.backward()

        # Gradient of loss w.r.t. the hidden state at step 1 (earliest step)
        grad_h1 = all_h.grad
        if grad_h1 is not None:
            g_norm = grad_h1[:, 0, :].norm().item()
        else:
            # Recompute with retain_grad
            model.zero_grad()
            x2 = torch.randn(4, T, 8, requires_grad=True)
            out2, all_h2 = model(x2)
            all_h2.retain_grad()
            loss2 = nn.CrossEntropyLoss()(out2, y)
            loss2.backward()
            g_norm = all_h2.grad[:, 0, :].norm().item() if all_h2.grad is not None else 0.0

        assessment = "✓ Learnable" if g_norm > 0.01 else ("⚠ Weak" if g_norm > 0.001 else "✗ Vanished")
        print(f"{T:>12} | {g_norm:>14.6f} | {assessment:>20}")


# ─────────────────────────────────────────────────────────────────────────
# 4. Gradient clipping — the standard fix for exploding gradients
# ─────────────────────────────────────────────────────────────────────────
def training_step_with_clipping(model, x, y, optimizer, clip_norm=1.0):
    optimizer.zero_grad()
    out, _ = model(x)
    loss = nn.CrossEntropyLoss()(out, y)
    loss.backward()

    # Check norm BEFORE clipping
    total_norm_before = torch.nn.utils.clip_grad_norm_(model.parameters(), float('inf'))

    # Clip — scale gradient if norm > clip_norm
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=clip_norm)

    # Check norm AFTER clipping
    total_norm_after = sum(p.grad.norm()**2 for p in model.parameters() if p.grad is not None)**0.5

    optimizer.step()
    return loss.item(), total_norm_before.item(), total_norm_after.item()


# ─────────────────────────────────────────────────────────────────────────
# 5. PyTorch built-in RNN vs our manual implementation
# ─────────────────────────────────────────────────────────────────────────
batch, seq_len, input_sz, hidden_sz = 4, 10, 8, 16
x = torch.randn(batch, seq_len, input_sz)

# Our implementation
manual_rnn = SimpleRNN(input_sz, hidden_sz, output_size=2)
out_manual, hidden_manual = manual_rnn(x)

# PyTorch built-in (batch_first=True: (batch, seq, feature) convention)
pytorch_rnn = nn.RNN(input_sz, hidden_sz, batch_first=True, nonlinearity='tanh')
out_pt, h_n_pt = pytorch_rnn(x)

print(f"Manual  RNN — all hidden states: {hidden_manual.shape}")
print(f"PyTorch RNN — all hidden states: {out_pt.shape}")
print(f"PyTorch RNN — final hidden state: {h_n_pt.shape}")
print()

# Gradient clipping demo
model  = SimpleRNN(input_sz, hidden_sz, 2)
optim  = torch.optim.Adam(model.parameters(), lr=1e-3)
x_long = torch.randn(4, 50, input_sz)
y      = torch.randint(0, 2, (4,))
loss, norm_before, norm_after = training_step_with_clipping(model, x_long, y, optim, clip_norm=1.0)
print(f"Loss: {loss:.4f} | Grad norm before clip: {norm_before:.4f} | after clip: {norm_after:.4f}")
print(f"Clipping {'applied' if norm_before > 1.0 else 'not needed'} (threshold=1.0)")

# Vanishing gradient demo
print()
demonstrate_vanishing_gradient([5, 20, 50, 100])
── RNN Implementation & Gradient Analysis ──────────────────────────────
Manual  RNN — all hidden states: torch.Size([4, 10, 16])
PyTorch RNN — all hidden states: torch.Size([4, 10, 16])
PyTorch RNN — final hidden state: torch.Size([1, 4, 16])

Loss: 0.7112 | Grad norm before clip: 3.8247 | after clip: 0.9998
Clipping applied (threshold=1.0)

Gradient magnitude at step 1 (should be large to learn long-range deps):
  Seq length |    ||∂L/∂h_1|| |          Assessment
  ──────────────────────────────────────────────────────
            5 |      0.184231 |        ✓ Learnable
          20 |      0.018743 |        ✓ Learnable
          50 |      0.002108 |           ⚠ Weak
         100 |      0.000031 |          ✗ Vanished

08 — Where This Leads: The Path to LSTM and Attention

The RNN gave us something genuinely new: a model that reads sequences and carries memory. But its fundamental limitation is now mathematically precise — not a vague complaint, but a provable consequence of chaining multiplications through time. The gradient literally cannot reach the beginning of long sequences.

The history of sequence modelling from 1997 to 2017 is essentially the story of solving this one problem:

1
1997 — LSTM (Hochreiter & Schmidhuber): Introduce a cell state — a separate memory highway that runs parallel to the hidden state, connected via gates that learn to selectively read, write, and erase. The cell state creates a direct gradient path that doesn't decay the same way. Covered in full in Day 21.
2
2014 — GRU (Cho et al.): A simplified version of LSTM with fewer gates. Slightly faster to train, comparable performance on many tasks. Also covered in Day 21.
3
2015 — Attention (Bahdanau et al.): Instead of compressing an entire input sequence into one context vector (the LSTM's bottleneck for translation), let the decoder dynamically look at all encoder hidden states at each decoding step. This is the precursor to the full attention mechanism. Day 24.
4
2017 — Transformer (Vaswani et al.): Replace the sequential computation entirely. Every token attends to every other token directly — no recurrence, no gradient chains through time, no bottleneck. The vanishing gradient problem for sequences is solved architecturally. Day 25.
🔗 Why This History Matters

When you use a transformer today — when you interact with Claude, GPT-4, or LLaMA — you're using the solution to the vanishing gradient problem that stumped the field for 20 years. The residual connections inside every transformer block are a direct, formal answer to the same mathematical problem: they create gradient highways that bypass multiplicative chains, just as the LSTM's cell state did, but more elegantly and at greater scale.

Understanding RNNs is not nostalgia. It's understanding why the transformer was designed the way it was.

09 — A Thought to Sit With

💬 For You to Consider

The vanishing gradient is often taught as a flaw — a failure of the RNN. But look at it from another angle: the fact that early timesteps receive smaller gradient updates means the model implicitly weights recent context more than distant context. For many language tasks, this is actually the right inductive bias — the last few words of a sentence are usually more relevant than what was said ten sentences ago.

Is the vanishing gradient truly a bug — or is it an inadvertent feature that worked well enough for short tasks to make RNNs widely useful, while only revealing itself as a genuine limitation at longer contexts? And does this suggest anything about why transformers, which have no such built-in recency bias, sometimes behave surprisingly on very long contexts?

10 — Coming Up Next

Tomorrow — Day 21: GRU & LSTM. We take the RNN's core weakness — the vanishing gradient through recurrent multiplications — and solve it architecturally. The LSTM introduces a cell state and three gates: forget, input, and output. The GRU simplifies this to two gates. We'll build both from scratch, trace gradient flow through the cell state highway, and understand exactly why these architectures lasted 20 years and still appear in production systems today.

11 — Go Deeper

📄 Learning Representations by Back-propagating Errors — Rumelhart, Hinton & Williams (1986)
The paper that introduced backpropagation formally. The foundation of everything in this post.

📄 On the Difficulty of Training Recurrent Neural Networks — Pascanu et al. (2013)
The canonical treatment of vanishing and exploding gradients in RNNs. Section 2 contains the mathematical proof. Section 4 describes gradient clipping — still used today.

▶ Recurrent Neural Networks (RNN) and Long Short-Term Memory (LSTM) — StatQuest
YouTube · 19 min · The clearest visual explanation of how hidden states carry information through time. Watch before tackling the LSTM.

🔗 The Unreasonable Effectiveness of Recurrent Neural Networks — Andrej Karpathy (2015)
The most famous blog post on RNNs. Generates Shakespeare, Linux kernel code, and LaTeX papers character by character. Still a joy to read — and reveals what RNNs are genuinely good at.

📄 Sequence to Sequence Learning with Neural Networks — Sutskever et al. (2014)
The seq2seq paper. Introduced the encoder-decoder architecture. Google Translate ran on this. Section 2 is the whole architecture in two pages.

🧠 Check Your Understanding
1. Why can't a standard feedforward network be used for language modelling? What specific properties of language does it fail to capture?
2. Write out the RNN forward pass equation and explain what each term represents. Why is tanh used as the activation function?
3. Derive mathematically why gradients vanish in RNNs. What two terms in the gradient expression cause the decay?
4. What is gradient clipping, why does it help, and what does it NOT fix?
5. An RNN trained on sentiment analysis performs well on short reviews but poorly on long ones. Explain exactly what is happening internally.
⚡ Quick Recap — Recurrent Neural Networks
  1. Why RNNs: feedforward networks treat inputs as independent and require fixed-size inputs — both fatal for language. RNNs process tokens sequentially, maintain a running hidden state, handle variable-length sequences, and share weights across all positions.
  2. The RNN cell: h_t = tanh(W_h·h_{t-1} + W_x·x_t + b). At each step, current input x_t and previous hidden state h_{t-1} are combined and squashed by tanh to produce new hidden state h_t. Same weights W_h, W_x, b used at every step.
  3. Unrolled view: an RNN processing T tokens is equivalent to a T-layer deep network with shared weights. A 100-word sentence = a 100-layer network. This depth is both the source of power and the source of the gradient problem.
  4. Task variants: many-to-one (sentiment classification — use final h_T), one-to-many (image captioning), many-to-many equal (NER, POS tagging), many-to-many seq2seq (translation — encoder compresses to context vector, decoder generates).
  5. BPTT: backpropagation through time — gradients flow backwards through every timestep of the unrolled network. Gradient at step 1 involves multiplying W_h and tanh derivatives T−1 times.
  6. Vanishing gradients: if ||W_h|| < 1 and tanh derivatives < 1 (both typical), the product W_h^(T-1) · [tanh terms] → 0 exponentially. At sequence length 100, the gradient at step 1 is ~0. Early tokens cannot be learned from — long-range dependencies are invisible.
  7. Exploding gradients: if ||W_h|| > 1, gradients grow exponentially, causing training to crash with NaN loss. Fixed by gradient clipping: rescale gradient vector when its norm exceeds threshold τ. Does not fix vanishing gradients.
  8. RNNs still win: short sequences (<50 tokens), streaming/online processing, resource-constrained environments (constant O(1) memory per step vs transformer's O(n²)), and time series with strong local patterns.
  9. Partial fixes: gradient clipping (exploding only), truncated BPTT (caps depth, limits learning range), orthogonal W_h initialisation (prevents W_h contribution to decay), ReLU activation (no shrinkage from activation, but tends to explode). None are satisfying — architectural solution needed.
  10. The path forward: LSTM (1997) adds a cell state with gates to create a gradient highway. GRU (2014) simplifies to two gates. Attention (2015) lets decoders look at all encoder states directly. Transformer (2017) eliminates recurrence entirely — solving the problem architecturally.
🎯 Interview Questions
  1. What is the vanishing gradient problem in RNNs?
    During BPTT, gradients are computed by multiplying the recurrent weight matrix W_h and tanh derivatives at each step. If these values are less than 1 (typical), the product decays exponentially with sequence length. For a 100-step sequence, the gradient at step 1 may be 10^-5 times the gradient at step 100 — effectively zero. The model cannot learn that early tokens matter, making long-range dependencies unlearnable.
  2. What is gradient clipping and when do you use it?
    Gradient clipping rescales the gradient vector when its global norm exceeds a threshold (typically 1.0–5.0): g = g × (τ / ||g||). It prevents exploding gradients — catastrophic training crashes where weights become NaN. It's applied after loss.backward() and before optimizer.step() in any training loop involving RNNs. It does not fix vanishing gradients.
  3. What is the seq2seq architecture and what is its main limitation?
    Seq2seq uses an encoder RNN to read the full input sequence and compress it into a single fixed-size context vector (the encoder's final hidden state). A decoder RNN then generates the output sequence conditioned on this vector. The limitation: the entire input must be compressed into one fixed-size vector regardless of input length. For long sentences, information is lost. This bottleneck was the motivation for the attention mechanism.
  4. When would you still choose an RNN over a transformer in 2026?
    Four scenarios: (1) streaming/online applications where you process tokens one at a time in real time — RNNs are naturally causal with O(1) per-step computation; (2) very short sequences where transformer overhead isn't justified; (3) resource-constrained edge devices where the transformer's memory and compute requirements are prohibitive; (4) time series forecasting with strong local temporal patterns where the sequential inductive bias helps.
📎 Attachments & Further Reading

🔗 The Unreasonable Effectiveness of Recurrent Neural Networks — Karpathy (2015)
The most fun you'll have reading about RNNs. Generates Shakespeare, Linux kernel, and LaTeX with character-level RNNs. Read this to understand what RNNs are genuinely good at.

📄 On the Difficulty of Training Recurrent Neural Networks — Pascanu et al. (2013)
The canonical mathematical treatment of vanishing and exploding gradients. Section 2 is the proof; Section 4 is gradient clipping.

📄 Sequence to Sequence Learning with Neural Networks — Sutskever et al. (2014)
The seq2seq paper. Google Translate ran on this architecture. Section 2 is the full architecture in two readable pages.

▶ RNN and LSTM — StatQuest with Josh Starmer
YouTube · 19 min · Best visual explanation of hidden states carrying information through time. The bridge into Day 21.

#RNN#RecurrentNeuralNetwork#SequenceModelling#VanishingGradients#BPTT#GradientClipping#Seq2Seq#NLP#DeepLearning#MachineLearning#HiddenState#30DaysOfAI#Ramadan2026
← Day 19 NLP Fundamentals