S107 · Attention Intuition — Why RNNs Failed, Why Attention Won
The 2017 paper that killed RNNs. What 'attention' actually means, the bottleneck it fixes in seq2seq, and why parallelism made Transformers eat NLP.
🎯 Understand attention as a soft dictionary lookup — WHY it exists, WHAT problem it fixes in RNN encoder-decoders, and HOW it scales to a full Transformer.
Why this session exists
"Attention Is All You Need" (2017) is the single most-cited ML paper of the decade. It killed RNNs for NLP in three years and became the foundation of every LLM, image generator, and multimodal model that followed. But the paper is dense math, and most tutorials jump straight to Q/K/V equations without explaining WHY attention was invented or what it replaces. This session builds the intuition first — you'll see attention as a soft dictionary lookup that fixes a specific bottleneck in seq2seq translation. The math (Session S108) makes sense once you feel the problem.
- Explain the seq2seq bottleneck — why the last LSTM hidden state can't carry a 100-word sentence.
- Describe attention as a weighted average, and 'soft dictionary lookup' as its unifying metaphor.
- Draw an attention pattern between an English sentence and its French translation.
- Explain WHY attention parallelises across the sequence axis when RNNs cannot.
- Distinguish 'attention' (as an add-on to RNNs, Bahdanau 2014) from 'self-attention' (the Transformer's core, 2017).
Prerequisites
- S103 · RNNs & LSTMs — you MUST understand seq2seq and the vanishing gradient to appreciate what attention fixes.
- S104 · Embeddings — attention operates on vector representations of tokens.
- S106 · Tokenization — the input tokens attention sees.
(a) Intuition · 5 min
You're translating a French sentence to English. Do you memorise the entire French sentence, then look away and generate the English from memory? Of course not — you glance back at the French for each English word you write. Sometimes the current English word depends on French word 3; sometimes on word 15; sometimes on a pair of words across the sentence.
The glancing is the trick. You dynamically decide, for each output word, which input words matter right now.
Before 2014, seq2seq models did the opposite: encode the whole source sentence into ONE fixed vector, then decode from that vector alone. This is the 'memorise and look away' approach. It works for 10-word sentences and fails miserably for 30-word ones — the vector can't carry that much information.
Attention gives the decoder the ability to glance back. At each decoder step, it computes a weighted average of ALL the encoder hidden states, with weights determined by 'which source words are most relevant right now.' The bottleneck disappears.
The three ideas that make attention work
- Soft dictionary lookup — think of attention as a 'fuzzy hash table.' You have a query (what you want to know), and a set of key-value pairs. Instead of matching one exact key, you compute similarity with every key and return a weighted average of the values.
- Content-based routing — the weights come from the CONTENT of the query and keys (usually a dot product), not from fixed positional rules. This is why attention can handle any pair of positions equally well.
- Parallelism — all query-key comparisons can be done in parallel via a single matrix multiplication. RNNs must process token 1 before token 2 before token 3; attention does all N² comparisons at once.
A short history
- 2014Seq2Seq · Sutskever et al.Encoder-decoder LSTMs for translation. Works well up to ~20 words, degrades sharply beyond.
- 2014Attention · Bahdanau et al.Bolt attention onto seq2seq. Fixes the long-sentence bottleneck. BLEU score jumps.
- 2015Luong attentionSimpler, faster variants. Multiplicative attention becomes the standard.
- 2017Attention Is All You Need · Vaswani et al.Drop the RNN. Use self-attention. Parallelisable, faster to train, wins WMT translation benchmarks.
- 2018BERT · GoogleTransformer encoder pretrained on masked language modelling. NLP is transformed.
- 2020GPT-3 · OpenAI175B-parameter Transformer decoder shows in-context learning. LLM era begins.
(b) Visual walkthrough · 15 min
The seq2seq bottleneck
The red box is the entire information bottleneck. Everything the encoder learned about a 30-word input must fit in one d-dimensional vector. It doesn't — long sentences translate badly.
The fix: attention over all encoder states
At every decoder step, the decoder queries ALL encoder states. Softmax over similarity scores gives attention weights (which sum to 1). Weighted sum of encoder states = the context vector for THIS decoder step. Bottleneck gone.
The three roles: query, key, value
The three flavours: cross-attention, self-attention, causal self-attention
Query from A, keys/values from B
- Original Bahdanau attention
- Decoder queries encoder outputs
- Used in translation, T5, Whisper
- Q = decoder state, K/V = encoder outputs
Q, K, V all from same sequence
- Transformer's core innovation
- Every token attends to every other token
- Bidirectional — used in BERT encoders
- Q, K, V = 3 projections of same input
Self-attention with future masked
- Used in GPT-style decoders
- Token t can only see tokens 1..t
- Enables next-token prediction
- Mask = upper triangle of attention matrix set to -∞
Anatomy of one attention head
How attention actually computes
What an attention pattern actually looks like
Different attention heads learn different patterns: some look at nearby tokens (like a CNN would), some look at syntactic parents, some look at co-referring entities. In a 12-layer Transformer with 12 heads, you have 144 different learned attention patterns.
"Attention weights tell me what the model is looking at. If I visualise them, I get an explanation of the prediction."
Attention weights are a routing distribution over values, not an attribution of importance. A head can place 90% of its mass on a token whose value vector contributes almost nothing to the output, and the residual stream carries information around attention entirely. Attention maps are a useful debugging signal; they are not an explanation.
Because the myth is true in the case everyone meets first: seq2seq translation attention, where a single head genuinely aligned source and target words and the picture was beautiful and correct. That image is what made attention famous. In a 32-layer, 32-head transformer, no single head owns a concept — the computation is distributed across heads and layers, and the softmax normalisation forces mass somewhere even when the head has nothing to say (hence attention sinks on the first token).
Show that high attention weight does not imply high output contribution: scale a value vector to zero and watch the output barely move while the weight stays high.
import torch
T, d = 6, 8
Q = torch.randn(T, d); K = torch.randn(T, d); V = torch.randn(T, d)
A = torch.softmax(Q @ K.T / d**0.5, dim=-1)
out = A @ V
V2 = V.clone(); V2[0] = 0 # kill the value of token 0
out2 = A @ V2
print('attn mass on tok0:', A[:, 0].mean().item())
print('output change :', (out - out2).norm().item())
# high mass, and yet the delta is bounded entirely by |V[0]|, not by AWhy divide by √dk before the softmax? Everyone repeats "to stop gradients vanishing" — derive it.
- 1Take query and key components as roughly independent, zero-mean, unit-variance after normalisation. The score is a dot product of two d-dimensional vectors.forced by · this is what LayerNorm plus standard initialisation actually gives you at the start of training
- 2A sum of d independent zero-mean unit-variance products has variance d, hence standard deviation
√d.forced by · variances add for independent terms; that is the whole content of the step - 3So raw scores grow as
√d. At d=128 that is a spread of roughly ±11 between typical scores, and larger for outliers.forced by · the score magnitude is set by dimension, not by anything semantic - 4Softmax over logits separated by ~10 is numerically one-hot: the largest logit takes essentially all the mass.forced by · e^10 is about 22,000, so a 10-unit gap gives a 22,000:1 ratio
- 5The gradient of softmax is p(1−p) in the diagonal term. As p approaches 1 or 0, that goes to zero — the head stops learning where to look, and it is stuck wherever random initialisation pointed it.forced by · a saturated softmax has vanishing Jacobian, so no gradient reaches Q and K
Therefore dividing by √dk restores unit-variance logits at initialisation, keeping the softmax in its responsive regime. It is a variance-normalisation, not a magic constant.
And note what this predicts: (1) the scaling matters most at initialisation — a trained model learns Q/K magnitudes that keep logits sane, which is why alternative scalings can still train, just worse and slower; (2) anything else that inflates logit variance — very long sequences, unnormalised inputs, fp16 overflow in the score matrix — reproduces the same saturation. That is exactly why production kernels compute the softmax in fp32 and subtract the row max.
Every token emits a query ("what do I need?"), advertises a key ("what do I have?"), and offers a value ("here is the thing"). A hard dictionary would return the one value whose key matches. Attention returns a weighted blend of all values, weights given by key–query similarity.
The blend is the entire point: hard lookup has no gradient, so nothing could learn what to look up. Softmax is what turns a lookup into something trainable.
- Q, K, V are three different learned projections of the same input. Separating them lets "what I search for" differ from "what I expose" and from "what I hand over".
- The output for each position is a convex combination of value vectors — so attention can only mix values, never invent information outside their span.
- Cost is O(T²) in time and, if materialised, O(T²) in memory. That single fact drives every long-context technique you will meet later.
- Attention is permutation-invariant by construction. Order exists only because positional information was added to the inputs.
Fire this model the moment you see: an attention map being presented as an explanation · a question about why context length is expensive · a model that mysteriously ignores token order · a masked-attention bug where the future leaks · anyone proposing to "just remove the softmax".
You need a sequence model for a task with long-range dependencies. Attention, recurrence, or convolution?
Attention won because it traded memory for path length, and path length is what gradient descent actually needs: one layer between any two tokens means the credit-assignment signal does not have to survive a hundred sequential multiplications. Everything since — sparse attention, linear attention, FlashAttention, state-space hybrids — is an attempt to keep the short path while paying less than O(T²).
In practice the senior read is: use attention, and treat the quadratic term as a budget line you manage (chunking, retrieval, caching) rather than an architecture you replace. Replace it only when the sequence length is genuinely unbounded.
(c) Hands-on · 25 min
Implement a minimal attention layer in PyTorch and visualise the attention pattern on a real sentence. This is the exact code that becomes multi-head attention in Session S109.
# mini_attention.py — a from-scratch self-attention layer with visualisation.
# Run: uv run mini_attention.py
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class SingleHeadAttention(nn.Module):
"""One head of self-attention. Q, K, V from same input; attention weights returned."""
def __init__(self, d_model: int, causal: bool = False):
super().__init__()
self.d_model = d_model
self.causal = causal
# Three linear projections — no bias, following the Transformer paper.
self.W_q = nn.Linear(d_model, d_model, bias=False)
self.W_k = nn.Linear(d_model, d_model, bias=False)
self.W_v = nn.Linear(d_model, d_model, bias=False)
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
# x: [B, T, d]. Project to Q, K, V.
B, T, D = x.shape
Q = self.W_q(x) # [B, T, D]
K = self.W_k(x) # [B, T, D]
V = self.W_v(x) # [B, T, D]
# Compute scaled dot-product attention scores.
# scores[b, i, j] = how much token i attends to token j.
scores = Q @ K.transpose(-2, -1) # [B, T, T]
scores = scores / math.sqrt(D) # scale — see Session S108 for why
# Causal mask: token i cannot see token j > i.
if self.causal:
mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
scores = scores.masked_fill(mask, float("-inf"))
# Softmax across the KEYS (last dim).
weights = F.softmax(scores, dim=-1) # [B, T, T]
# Weighted sum of values.
output = weights @ V # [B, T, D]
return output, weights
def demo() -> None:
torch.manual_seed(42)
# A tiny vocabulary + embedding for demonstration.
sentence = ["The", "cat", "sat", "on", "the", "mat"]
vocab = {word: i for i, word in enumerate(sorted(set(sentence)))}
ids = torch.tensor([[vocab[w] for w in sentence]]) # [1, 6]
d_model = 16
embed = nn.Embedding(len(vocab), d_model)
attn = SingleHeadAttention(d_model, causal=False)
x = embed(ids) # [1, 6, 16]
out, weights = attn(x)
print(f"input shape: {x.shape}")
print(f"output shape: {out.shape}")
print(f"weights shape: {weights.shape} (should be [1, 6, 6])")
# Pretty-print the attention matrix.
w = weights[0].detach().numpy()
print("\nAttention pattern (rows = query token, cols = key token):")
print(f" {' '.join(w_.rjust(4) for w_ in sentence)}")
for i, word in enumerate(sentence):
row = " ".join(f"{v:.2f}" for v in w[i])
print(f" {word:<7} {row}")
# Compare causal vs non-causal.
print("\n--- Causal (GPT-style) ---")
attn_causal = SingleHeadAttention(d_model, causal=True)
_, weights_c = attn_causal(x)
w = weights_c[0].detach().numpy()
for i, word in enumerate(sentence):
row = " ".join(f"{v:.2f}" for v in w[i])
print(f" {word:<7} {row}")
print("(Notice the upper-right triangle is 0 — the future is masked.)")
if __name__ == "__main__":
demo()Anatomy of the script
What the interesting lines do
import time
T = 512
x = torch.randn(1, T, 16)
# LSTM baseline
lstm = nn.LSTM(16, 16, batch_first=True)
t0 = time.time()
for _ in range(50): _ = lstm(x)
print(f"LSTM 50 iters: {time.time() - t0:.3f}s")
# Attention
attn = SingleHeadAttention(16, causal=True)
t0 = time.time()
for _ in range(50): _ = attn(x)
print(f"Attention 50 iters: {time.time() - t0:.3f}s")On CPU LSTM wins for small T. On GPU with larger T (say 2048), attention is 5-10× faster — this is the entire reason Transformers replaced RNNs.
(d) Production reality · 15 min
Google's 2016 NMT system used 8-layer stacked LSTMs with Bahdanau attention. It shipped and dramatically improved translation quality. But engineers noticed inference was slow and hard to scale — the attention had to be computed at every decoder step, which added ~15% latency, AND the LSTM stacks couldn't be parallelised.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
If you can't teach these three without notes, redo the session:
- What is the seq2seq bottleneck, and how does attention fix it?
- What are Q, K, V — and what does the metaphor 'soft dictionary lookup' mean?
- Why can attention parallelise across the sequence axis when RNNs can't?
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.