πŸ“š Study Notes / Home / Neural Nets / Session 15
Session 15 Β· The Transformer

The Transformer, from the dot product up

Last session we taught a recurrent network to translate by reading one word at a time and leaning on an attention "spotlight." Today we take a bold step: we throw away the recurrence entirely and keep only the attention. The result β€” the Transformer β€” is the architecture behind essentially every modern language model. We'll build it from scratch and stay honest about the math: real matrices, real softmax, a numeric worked example you can check by hand, and a small slab of PyTorch-style code. (Our sibling GenAI course covers this at an intuition level; here we go deeper, because this is the machine-learning class.)

⏱ 21 min readπŸ“– 5 topics

1 Why drop recurrence? "Attention is all you need"


Explain like I'm 5

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.

PropertyRNN / LSTMSelf-attention
Sequential operations (per layer)O(n) β€” must go in orderO(1) β€” all positions at once
Max path between any two tokensO(n) β€” distance = hopsO(1) β€” direct connection
Compute per layerO(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.

The big idea

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.

Concrete example β€” the speed win

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.

What we keep from Session 14

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.

Recap RNNs are slow (O(n) sequential, can't parallelise) and forgetful (O(n) path length, vanishing gradients). Self-attention fixes both: O(1) sequential operations and O(1) path length between any two tokens, at the cost of O(nΒ²) compute. "Attention is all you need" β€” drop recurrence, keep attention, process all tokens in parallel.

2 Self-attention & Q/K/V


Explain like I'm 5

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:

The 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).

Worked numeric example (do it by hand)

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.

"Self" vs "cross" attention

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).

Why √d_k and not just d_k?

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.

Recap Self-attention projects each token into a Query, Key, and Value. It scores every query against every key with a dot product, scales by √d_k, softmaxes each row into weights that sum to 1, and uses those weights to blend the values. Compactly: softmax(QKᵀ/√d_k)V. The output is a context-aware re-mix of the inputs, computed for all tokens at once.

3 Multi-head attention


Explain like I'm 5

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/h features, the concatenation is back to width d_model.
  • Project with a final learned matrix Wᴼ of shape (d_model, d_model), which lets the heads' outputs mix into one coherent vector.
Concrete dimensions (the original Transformer)

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.

What do heads actually learn?

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.

Key takeaway

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.

Recap Multi-head attention splits d_model into h smaller subspaces, runs an independent scaled dot-product attention in each (its own WQ/WK/WV), concatenates the h outputs, and applies a final projection Wᴼ. Different heads specialise in different relationships, giving the model many simultaneous "views" of the sequence at roughly the cost of a single full-width attention.

4 Positional encoding


Explain like I'm 5

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.

Why sinusoids? The relative-position trick

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.

Learned positional embeddings

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.

Gotcha β€” add, don't concatenate

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.

Recap Attention is order-blind (permutation-equivariant), so we add a positional encoding to each embedding. The classic choice is sinusoids of geometrically increasing wavelength β€” even dims use sin, odd dims use cos β€” which let the model reason about relative offsets via linear relationships. Learned position embeddings are a simpler, common alternative that can't extrapolate beyond trained lengths.

5 The full Transformer block


Explain like I'm 5

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

SublayerEncoder blockDecoder block
1Multi-head self-attention (sees whole input)Multi-head masked self-attention (sees only past)
2Feed-forwardMulti-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

πŸ”€
Embed + PE
tokens β†’ vectors + position
β†’
🧩
Encoder Γ—N
self-attn + FFN
β†’
🎭
Decoder Γ—N
masked self-attn + cross-attn + FFN
β†’
πŸ“Š
Linear + softmax
β†’ next-token probabilities

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.

Encoder-only, decoder-only, encoder–decoder

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.

Minimal PyTorch-style code β€” scaled dot-product attention
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.

Key takeaway

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.

Recap The full Transformer stacks N encoder blocks (self-attention + FFN) and N decoder blocks (masked self-attention + cross-attention + FFN), each sublayer wrapped in a residual connection and LayerNorm. Masked attention enforces left-to-right generation by sending future scores to −∞ before softmax. Embeddings + positional encodings go in; a linear layer + softmax over the vocabulary comes out. Encoder-only (BERT) and decoder-only (GPT) variants drop the parts they don't need.

β˜… 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