1 Why drop recurrence? "Attention is all you need"
Imagine a line of people passing a secret down a row by whispering. Person 50 can only hear what person 49 whispers β and the message gets garbled and slow by the time it travels all the way down. Now imagine instead that everyone can talk to everyone at once, in one big room. No waiting in line, and nobody forgets the start of the story. That switch β from a whisper-chain to a everyone-talks-at-once room β is the whole idea behind the Transformer.
In Session 14 we used a recurrent neural network (RNN)
β and its gated cousins the LSTM and GRU β to process sequences. An RNN reads a sequence
x₁, x₂, β¦, xₙ one step at a time, carrying a
hidden state hₜ forward:
hₜ = f(hₜ₋₁, xₜ) # step t depends on step t-1
That little recurrence β "step t needs step t−1" β is elegant, but it causes two serious problems for long sequences.
Problem 1 β recurrence is inherently sequential (slow)
Because hₜ depends on hₜ₋₁,
you cannot compute step 5 before step 4. The work forms a chain of length n,
so even on a GPU with thousands of cores, an RNN's forward pass takes O(n) sequential steps
β the hardware sits idle waiting. Modern GPUs love doing thousands of independent multiplications at once;
recurrence refuses to give them that.
Problem 2 β long-range dependencies get forgotten
To connect word 1 with word 100, the signal must survive 99 hops through the hidden state. Each hop multiplies gradients by a factor; do that 99 times and the gradient either shrinks to nothing (vanishing gradient) or blows up (exploding gradient). LSTMs soften this with gates, but the fundamental issue remains: the path length between two positions grows with their distance.
| Property | RNN / LSTM | Self-attention |
|---|---|---|
| Sequential operations (per layer) | O(n) β must go in order | O(1) β all positions at once |
| Max path between any two tokens | O(n) β distance = hops | O(1) β direct connection |
| Compute per layer | O(n Β· dΒ²) | O(nΒ² Β· d) |
| Parallel on GPU? | Poorly (chain) | Yes (one big matmul) |
Read that table carefully β it is the entire argument for the Transformer. Self-attention makes the path between any two tokens length 1 (every token looks directly at every other), and it does the work as one big matrix multiply with no sequential dependency. The price is the O(nΒ²) term: every token attends to every other, so cost grows with the square of the sequence length. For typical sentence lengths that trade is a bargain.
In 2017, Vaswani et al. published "Attention Is All You Need." The provocative claim: you can delete recurrence and convolutions entirely and build a state-of-the-art sequence model out of attention alone. It worked β and it became the foundation of GPT, BERT, Llama, Claude, and basically everything since.
Translate a 50-token sentence. An RNN does 50 sequential hidden-state updates β
step 50 waits for all 49 before it. A Transformer layer instead forms one
50 Γ 50 attention matrix in a single parallel operation, then
another, etc. On a GPU the Transformer's layer finishes in roughly constant wall-clock time
regardless of which token you're looking at β there is nothing to wait for.
The idea of attention β letting an output position weight all input positions β is exactly the spotlight from last session. The Transformer's twist is to apply attention within a single sequence (a token attending to its own neighbours), called self-attention, and to use it as the only mixing mechanism rather than as a bolt-on to an RNN.
2 Self-attention & Q/K/V
Think of a classroom doing a group project. Each kid holds up a question card ("I need help with glue") and also wears a name tag describing what they're good at ("I'm the glue expert"). Everyone glances around, matches their question to the best name tags, and then borrows help from those kids β more from the great matches, less from the bad ones. The question card is the query, the name tag is the key, the help is the value. Self-attention is just every token doing this matching at once.
Self-attention takes a sequence of token vectors and produces a new sequence of the same length, where each output vector is a weighted blend of all the input vectors. The weights are learned from the content itself. Here is the machinery, built up piece by piece.
Step 1 β three roles per token: Query, Key, Value
Start with the input as a matrix X of shape
(n, d_model): n tokens, each a d_model-dimensional vector
(the embedding from the previous layer). From X we create three new matrices by
multiplying with three learned weight matrices:
Q = X Β· WQ # queries β "what am I looking for?" K = X Β· WK # keys β "what do I offer / contain?" V = X Β· WV # values β "what information do I pass on?"
- Query (Q) β each token's "search request." Shape
(n, d_k). - Key (K) β each token's "advertised label." Shape
(n, d_k). - Value (V) β each token's "payload" to be mixed into others. Shape
(n, d_v).
The matrices WQ, WK, WV are the only learned parameters here β training
decides what each token should ask for, advertise, and carry.
Step 2 β scores: how well does each query match each key?
We measure match with a dot product. The dot product of two vectors is large when they point the same way (similar) and small/negative when they don't. Compute every query against every key in one shot:
scores = Q Β· Kᵀ # shape (n, n): scores[i][j] = how much token i attends to token j
Step 3 β scale by √d_k
Here's a subtle but important detail. If d_k (the dimension of each query/key)
is large, the dot products grow large in magnitude too β roughly proportional to d_k.
Large inputs push the upcoming softmax into a region where its gradients are tiny (it saturates), which
stalls learning. The fix is to divide by √d_k to keep the variance around 1:
scaled = (Q Β· Kᵀ) / √d_k
Step 4 β softmax into attention weights
Apply softmax along each row so each query's scores become a probability distribution over the keys (all positive, each row sums to 1). Row i now says "how much should token i draw from each other token."
Step 5 β weighted sum of values
Multiply those weights by V to blend the payloads. Putting all five steps
together gives the celebrated scaled dot-product attention formula:
Attention(Q, K, V) = softmax( Q·Kᵀ / √d_k ) · V
Read it left to right: match queries to keys (Q·Kᵀ), scale (÷√d_k), turn into weights (softmax), blend the values (·V).
Take 3 tokens with tiny d_k = d_v = 2. Suppose after the
linear projections we have:
Q = [[1, 0], K = [[1, 0], V = [[10, 0], [0, 1], [0, 1], [ 0,10], [1, 1]] [1, 1]] [ 5, 5]]
Focus on token 1, whose query is q₁ = [1, 0].
(a) Scores = q₁ · each key:
q₁·k₁ = (1)(1)+(0)(0) = 1 q₁·k₂ = (1)(0)+(0)(1) = 0 q₁·k₃ = (1)(1)+(0)(1) = 1 scores = [1, 0, 1]
(b) Scale by √d_k = √2 ≈ 1.414:
[0.707, 0, 0.707].
(c) Softmax. Exponentiate: e^0.707≈2.028, e^0≈1.000, e^0.707≈2.028. Sum = 5.056. Divide:
weights ≈ [0.401, 0.198, 0.401] # sums to 1.0 β
(d) Blend the values β weighted sum of the V rows:
out₁ = 0.401·[10,0] + 0.198·[0,10] + 0.401·[5,5]
= [4.01, 0] + [0, 1.98] + [2.005, 2.005]
≈ [6.02, 3.98]
So token 1's new representation is mostly built from tokens 1 and 3 (the ones its query matched),
with a smaller dose of token 2. That blended vector β not the original β is what flows onward. You can
repeat the same four steps for q₂ and q₃ to fill out the full (3, 2)
output matrix.
When Q, K, and V all come from the same sequence, it's self-attention (a token attends to its own sentence). When the queries come from one sequence but keys/values from another β e.g. the decoder querying the encoder's output in translation β it's cross-attention. Same formula, different sources. We'll see both in the full block (Topic 5).
If query and key entries are independent with mean 0 and variance 1, then their dot product over
d_k dimensions has variance d_k, hence standard
deviation √d_k. Dividing by √d_k renormalises the spread back to ~1 so
softmax stays in its responsive range. Forget this and large models train poorly.
3 Multi-head attention
Reading a sentence, you notice many things at once: who-did-what, which word rhymes, what the mood is. Imagine sending several friends to read the same sentence, each told to watch for one thing β one tracks grammar, one tracks topic, one tracks tone β and then you combine all their notes. That's multi-head attention: several attention "readers" running in parallel, each looking at a different kind of relationship, then merged.
A single attention computation can only learn one way of relating tokens. But language has many simultaneous relationships (subject–verb agreement, coreference, adjacency, semantic similarityβ¦). Multi-head attention runs several attention functions in parallel, each in its own learned subspace, then combines the results.
The recipe
Pick h heads (the original paper used h = 8). Split the model
dimension among them: d_k = d_v = d_model / h. Give each head i its
own projection matrices WQᵢ, WKᵢ, WVᵢ:
headᵢ = Attention(XΒ·WQᵢ, XΒ·WKᵢ, XΒ·WVᵢ) # each head: shape (n, d_v) MultiHead(X) = Concat(head₁, β¦, headₕ) Β· Wᴼ # concat then project back to d_model
Two moves to notice:
- Concatenate the h head outputs side by side. Since each head produces
d_v = d_model/hfeatures, the concatenation is back to widthd_model. - Project with a final learned matrix
Wᴼof shape(d_model, d_model), which lets the heads' outputs mix into one coherent vector.
d_model = 512, h = 8 heads, so each head works in
a 512 / 8 = 64-dimensional subspace (d_k = d_v = 64).
Eight heads each output a (n, 64) matrix; concatenating gives
(n, 512); the final Wᴼ of shape
(512, 512) mixes them back into (n, 512).
Total compute is about the same as one full-width head β because we shrank each head's
dimension by the same factor we multiplied heads. We get diversity essentially for free.
When researchers visualise trained heads, they find specialists: one head links pronouns to their referents ("it" → "animal"), another attends to the immediately preceding token, another tracks syntactic dependencies, another to sentence-delimiting punctuation. Nobody assigns these jobs β they emerge from training because dividing labour across subspaces is a useful thing to do.
One head = one viewpoint. h heads = h viewpoints in parallel subspaces, each cheap (dimension d_model/h), concatenated and projected back. This is what gives attention its expressive richness without blowing up the parameter count.
4 Positional encoding
Suppose you dump all the words of a sentence into a bag and hand them to a friend. They know the words but not the order β "dog bites man" and "man bites dog" look the same! So before bagging them, we stamp a little seat number on each word: "you're word #1, you're word #2β¦" Now order survives the bag. Positional encoding is that seat-number stamp.
Here's the catch with attention: it is permutation-equivariant β shuffle the
input tokens and the outputs just shuffle the same way. The formula
softmax(QKᵀ/√d_k)V contains no notion of position;
token 3 and token 30 are treated identically. But word order carries meaning, so we must inject it.
The fix β add a position signal to each embedding
Before the first attention layer, we add a positional encoding vector
PE(pos) to each token's embedding:
inputₜ = Embedding(tokenₜ) + PE(pos = t)
Both have width d_model, so they add elementwise. Now each vector carries
"what I mean" + "where I sit."
Sinusoidal positional encoding (the original choice)
Vaswani et al. used fixed sine and cosine waves of different frequencies. For position
pos and dimension index i (out of d_model):
PE(pos, 2i) = sin( pos / 10000^(2i/d_model) ) PE(pos, 2i+1) = cos( pos / 10000^(2i/d_model) )
So even dimensions get sines, odd dimensions get cosines, and the wavelength grows as you move to higher dimensions: low dimensions wiggle fast (encode fine position), high dimensions wiggle slowly (encode coarse position). It's like representing a number in many "bases" at once β the same trick as the hands of a clock (seconds, minutes, hours) telling you position at multiple scales.
A neat property: for any fixed offset k, PE(pos+k)
can be written as a linear function of PE(pos) (it's just a
rotation, from the sine/cosine addition formulas). That means the model can learn to attend "3 tokens
back" as a consistent linear operation, regardless of the absolute position β handy for generalising to
sentence lengths longer than any seen in training.
Many later models (BERT, GPT) instead use a learned positional embedding: just a lookup table with one trainable vector per position, learned like any other parameter. Simpler, works great β but it can't extrapolate past the maximum position seen in training, whereas the sinusoidal version (in principle) can. Both approaches are common; newer models also use rotary (RoPE) and relative schemes, which you may meet in later reading.
Positional info is added to the embedding, not stuck on the side. This keeps the width at d_model and lets every later layer freely mix content and position. It works because the embedding space has room to encode both signals in different directions.
5 The full Transformer block
Think of an assembly line where each station does a small fix and then hands the work back plus its improvement (so nothing good ever gets lost), and a supervisor keeps measurements tidy after each station. Stack a few of these stations and you've built something powerful out of simple, repeated parts. The Transformer block is exactly that: attention + a little neural net, wrapped in "keep what you had" shortcuts and tidying steps, repeated N times.
We now assemble the parts. A Transformer has an encoder stack and a
decoder stack, each made of N identical blocks (the paper used
N = 6). Each block has two or three sublayers, and every
sublayer is wrapped in the same two helpers.
The two wrappers: residual connection + LayerNorm
- Residual connection β add the sublayer's input back to its output:
x + Sublayer(x). This gives gradients a direct highway back through the network (recall the same skip-connection idea from ResNets), so very deep stacks still train. The sublayer only has to learn a correction to its input, not rebuild it. - Layer normalisation (LayerNorm) β renormalise each token's vector to mean
0, variance 1 (then scale/shift with learned parameters). It keeps activations stable across the deep
stack. So every sublayer computes
LayerNorm(x + Sublayer(x)).
The feed-forward sublayer
After attention mixes information across tokens, each block has a
position-wise feed-forward network (FFN) that processes each token
independently: two linear layers with a non-linearity (ReLU/GELU) between, typically expanding
to d_ff = 2048 then back to d_model = 512:
FFN(x) = max(0, xΒ·W₁ + b₁) Β· W₂ + b₂ # same weights applied to every position
Encoder block vs decoder block
| Sublayer | Encoder block | Decoder block |
|---|---|---|
| 1 | Multi-head self-attention (sees whole input) | Multi-head masked self-attention (sees only past) |
| 2 | Feed-forward | Multi-head cross-attention (queries from decoder, keys/values from encoder) |
| 3 | β | Feed-forward |
Masked attention β no peeking at the future
The decoder generates output one token at a time, so when predicting token t it must
not see tokens t+1, t+2, β¦ (they don't exist yet at inference, and letting it cheat at
training would be useless). We enforce this with a causal mask: before the
softmax, set the scores for all future positions to −∞, so softmax
drives their weights to 0.
scores = (QΒ·Kᵀ)/√d_k scores = scores + mask # mask[i][j] = -inf if j > i, else 0 weights = softmax(scores) # future positions now get weight 0
The whole pipeline, end to end
The encoder reads the whole input and builds rich representations. The decoder generates the output autoregressively: its masked self-attention looks at what it has produced so far, its cross-attention pulls relevant information from the encoder, the FFN refines, and a final linear layer + softmax gives a probability over the vocabulary for the next token.
The original Transformer is encoder–decoder (built for translation). Later families specialise: BERT is encoder-only (great for understanding/classification); GPT is decoder-only (great for generation β just the masked-self-attention + FFN stack, no cross-attention). We'll explore these variants in Session 16.
import torch import torch.nn.functional as F def scaled_dot_product_attention(Q, K, V, mask=None): # Q, K, V shapes: (batch, heads, seq_len, d_k) d_k = Q.size(-1) # 1) scores: (batch, heads, seq_len, seq_len) scores = torch.matmul(Q, K.transpose(-2, -1)) / d_k ** 0.5 # 2) causal / padding mask: set masked positions to -inf if mask is not None: scores = scores.masked_fill(mask == 0, float("-inf")) # 3) softmax over the last dim (keys) β attention weights weights = F.softmax(scores, dim=-1) # 4) weighted sum of values output = torch.matmul(weights, V) # (batch, heads, seq_len, d_k) return output, weights class MultiHeadAttention(torch.nn.Module): def __init__(self, d_model, h): super().__init__() assert d_model % h == 0 self.h, self.d_k = h, d_model // h self.W_q = torch.nn.Linear(d_model, d_model) self.W_k = torch.nn.Linear(d_model, d_model) self.W_v = torch.nn.Linear(d_model, d_model) self.W_o = torch.nn.Linear(d_model, d_model) # final projection def forward(self, x, mask=None): B, n, _ = x.shape # project, then split into h heads β (B, h, n, d_k) split = lambda t: t.view(B, n, self.h, self.d_k).transpose(1, 2) Q, K, V = split(self.W_q(x)), split(self.W_k(x)), split(self.W_v(x)) out, _ = scaled_dot_product_attention(Q, K, V, mask) # concat heads back to (B, n, d_model), then final projection out = out.transpose(1, 2).contiguous().view(B, n, -1) return self.W_o(out)
That's the heart of a Transformer in ~30 lines. A full encoder block wraps the
MultiHeadAttention output as
x = LayerNorm(x + attn(x)), then
x = LayerNorm(x + FFN(x)), and you stack N of them.
A Transformer block = (multi-head attention) + (position-wise FFN), each wrapped in
LayerNorm(x + Sublayer(x)). Stack N blocks for the encoder; the decoder adds a
masked self-attention (causal) and a cross-attention into the encoder. Residuals keep gradients
flowing; LayerNorm keeps them stable; the mask stops the decoder from peeking at the future.
β Putting it all together
We replaced the slow, forgetful recurrence of Session 14 with pure attention β and built the architecture that powers modern AI. Here's the one-paragraph story tying the five topics together:
RNNs are sequential and forgetful, so the Transformer drops recurrence and processes all tokens in parallel. Its engine is self-attention: each token forms a Query, Key, and Value, and we compute softmax(QKᵀ/√d_k)V to blend every token's value by how well its query matches each key. We run this in several parallel heads (each a small subspace) and concatenate-then-project. Because attention is order-blind, we add a positional encoding (sinusoidal or learned) to every embedding. We wrap attention and a feed-forward sublayer in residual connections + LayerNorm, stack N of them into an encoder and a decoder (whose self-attention is masked so it can't see the future and which cross-attends to the encoder), and finish with a linear + softmax over the vocabulary. That's the whole machine.
Quick self-check
Give the two concrete reasons recurrence was dropped.
(1) It's sequential β step t needs step t−1, so a layer takes O(n) sequential operations and can't be parallelised on a GPU. (2) Long-range dependencies travel O(n) hops through the hidden state, causing vanishing/exploding gradients. Self-attention makes both O(1).
Write the scaled dot-product attention formula and say what each piece does.
Attention(Q,K,V) = softmax(QKᵀ/√d_k)V. QKᵀ matches every query to every key (scores), ÷√d_k keeps the variance ~1 so softmax doesn't saturate, softmax turns each row into weights summing to 1, and ·V blends the values by those weights.
Why divide by √d_k specifically?
A dot product over d_k dimensions of unit-variance entries has variance d_k, i.e. standard deviation √d_k. Dividing by √d_k renormalises the scores to ~unit variance so the softmax stays in a region with useful gradients instead of saturating.
What does multi-head attention buy you, and why isn't it much more expensive?
Each head attends in its own subspace, so different heads can learn different relationships (grammar, coreference, adjacencyβ¦) simultaneously. It's cheap because each head's dimension is d_model/h, so h heads cost about the same as one full-width attention.
Self-attention has no sense of order. How is order injected, and how is it combined?
Via positional encodings β sinusoids of increasing wavelength (or learned per-position vectors). They are added (not concatenated) to the token embeddings before the first layer, so each vector carries both meaning and position.
What is the decoder's causal mask and why is it needed?
Before softmax, scores for all future positions (j > i) are set to −∞ so their attention weight becomes 0. It stops the decoder from "peeking" at tokens it hasn't generated yet, which is required for left-to-right autoregressive generation.
What are the two helpers wrapped around every sublayer, and what does each do?
A residual connection (x + Sublayer(x)) gives gradients a direct path so deep stacks train, and LayerNorm renormalises each token vector to stable mean/variance. Together each sublayer is LayerNorm(x + Sublayer(x)).
π References & Further Reading
Class material
- SST Deep Learning handout (Session 15) β your course handout for this session, covering the Transformer architecture, self-attention, and multi-head attention.
Papers, docs & deep dives
- Attention Is All You Need (Vaswani et al., 2017) β the original paper that introduced the Transformer; read it to see the exact formulas and architecture firsthand.
- The Illustrated Transformer (Jay Alammar) β the best visual, step-by-step walkthrough of Q/K/V, multi-head attention, and the full stack.
- The Annotated Transformer (Harvard NLP) β the paper re-implemented line-by-line in runnable PyTorch; perfect for connecting the math to real code.
- GenAI Session 1 β Introduction to GenAI β our sibling course's intuitive, math-free treatment of attention and transformers; a gentle complement to today's deeper dive.