Weeks
Deep Learning and GenAIWeek 9 · Embeddings and Attention
Week 9 Notes

Deep Learning,
Week 9

Introduction to DL and GenAI, Week 9: attention. Word embeddings (static against contextual), why the single context vector fails and how attention fixes it, queries, keys and values, scaled dot-product attention and why it is scaled, self- and cross-attention, causal masking, and multi-head attention with its cost. Every number was really run, including the same copy task as Week 8 now solved with attention.

7 chapters12 worked examples~41 min read
EmbeddingsAttentionQ, K, VScaled dot-productSelf / crossMaskingMulti-headInterview prep

Chapters

  1. 01Word EmbeddingsPart 1
  2. 02From the Bottleneck to AttentionPart 2
  3. 03Scaled Dot-Product AttentionPart 3
  4. 04Self, Cross and Masked AttentionPart 4
  5. 05Multi-Head AttentionPart 5
  6. 06Interview Question BankInterview
  7. 07Week 9 Summary SheetSummary
01
Part 1

Word Embeddings

Every network so far took numbers as input. Words are not numbers, so the first step of any language model is to turn each word (or sub-word token) into a vector. The way this is done decides what the rest of the network can know about meaning.

1.1One-hot vectors know nothing

The simplest code is one-hot: a vector as long as the vocabulary, with a 1 in the word's position and 0 elsewhere. Every pair of one-hot vectors is orthogonal, so "king" is as far from "queen" as from "apple", and the vectors are enormous. An embedding instead gives each word a short, dense, learned vector. The embedding layer is nothing more than a lookup table, which is exactly a one-hot vector multiplied by a weight matrix:

main.py
import torch, torch.nn as nn
torch.manual_seed(0)
vocab = ["king", "queen", "man", "woman", "apple"]
idx = {w: i for i, w in enumerate(vocab)}
onehot = torch.eye(len(vocab))
print("one-hot dot products (king.queen, king.apple):", (onehot[idx["king"]] @ onehot[idx["queen"]]).item(), (onehot[idx["king"]] @ onehot[idx["apple"]]).item(), " <- every pair is orthogonal: no notion of similarity")

emb = nn.Embedding(len(vocab), 4)
ids = torch.tensor([idx["king"], idx["apple"]])
lookup = emb(ids)                                           # the embedding layer is a table lookup ...
matmul = onehot[ids] @ emb.weight                           # ... which equals one-hot times the weight matrix
print("embedding lookup equals one-hot @ weight matrix:", torch.allclose(lookup, matmul))
V, d = 50_000, 300
print(f"a vocabulary of {V:,} words with {d}-dimensional embeddings has {V * d:,} parameters; a one-hot vector would have {V:,} dimensions")
Output
one-hot dot products (king.queen, king.apple): 0.0 0.0  <- every pair is orthogonal: no notion of similarity
embedding lookup equals one-hot @ weight matrix: True
a vocabulary of 50,000 words with 300-dimensional embeddings has 15,000,000 parameters; a one-hot vector would have 50,000 dimensions

The dot product of one-hot king and queen is 0, as for king and apple: no similarity is encoded. The lookup equals the one-hot product (True), and a 50,000-word vocabulary with 300-dimensional embeddings has 15 million parameters, a table that training adjusts so that useful words end up close together.

1.2Static embeddings: Word2Vec and GloVe

Word2Vec (skip-gram / CBOW) learns embeddings by predicting a word from its neighbours (or neighbours from a word); GloVe factorises a global co-occurrence matrix. Both rest on one idea: "you shall know a word by the company it keeps". The result is one fixed vector per word. To see the mechanism with nothing hidden, build embeddings the classical way: count which words appear near each other in a tiny templated corpus, convert counts to PPMI (positive pointwise mutual information), and compress with an SVD to 8 dimensions.

main.py
import numpy as np
from collections import Counter

templates = [("the {} wore a crown and he ruled the kingdom", ["king", "prince"]),
             ("the {} wore a crown and she ruled the kingdom", ["queen", "princess"]),
             ("the {} went to the market and he sold bread", ["man", "boy"]),
             ("the {} went to the market and she sold bread", ["woman", "girl"]),
             ("we sat on the {} of the river near the water", ["bank"]),
             ("she kept her money at the {} to get a loan", ["bank"])]
corpus = [t.format(w).split() for t, ws in templates for w in ws for _ in range(20)]
vocab = sorted({w for s in corpus for w in s}); idx = {w: i for i, w in enumerate(vocab)}
C = np.zeros((len(vocab), len(vocab)))
for s in corpus:                                                   # co-occurrence counts within a window of 6 words either side
    for i, w in enumerate(s):
        for j in range(max(0, i - 6), min(len(s), i + 7)):
            if i != j: C[idx[w], idx[s[j]]] += 1
total = C.sum(); pw = C.sum(1, keepdims=True) / total; pc = C.sum(0, keepdims=True) / total
with np.errstate(divide="ignore"):
    ppmi = np.maximum(np.log(C / total / (pw * pc)), 0)            # positive pointwise mutual information
U, S, _ = np.linalg.svd(ppmi)
k = 8
E = U[:, :k] * np.sqrt(S[:k]); E /= np.linalg.norm(E, axis=1, keepdims=True)         # 8-dimensional word vectors, unit length
stop = {"the", "a", "of", "at", "to", "on", "we", "she", "he", "and", "her", "near", "get", "kept", "sat", "went"}
sim = lambda a, b: float(E[idx[a]] @ E[idx[b]])
nearest = lambda v, n, skip=(): [vocab[i] for i in np.argsort(-(E @ v)) if vocab[i] not in skip and vocab[i] not in stop][:n]

print("cosine similarities:  king~prince", round(sim("king", "prince"), 3), " king~queen", round(sim("king", "queen"), 3), " king~woman", round(sim("king", "woman"), 3), " king~bread", round(sim("king", "bread"), 3))
q = E[idx["king"]] - E[idx["man"]] + E[idx["woman"]]
print("king - man + woman is closest to:", nearest(q, 3, skip=("king", "man", "woman")))
print("\nthe polysemy problem: ONE vector for 'bank' although the text uses it in two senses")
print("  nearest words to 'bank':", nearest(E[idx["bank"]], 8, skip=("bank",)))
Output
cosine similarities:  king~prince 1.0  king~queen 0.997  king~woman 0.374  king~bread 0.316
king - man + woman is closest to: ['queen', 'princess', 'kingdom']

the polysemy problem: ONE vector for 'bank' although the text uses it in two senses
  nearest words to 'bank': ['river', 'money', 'water', 'loan', 'market', 'crown', 'ruled', 'queen']
manwomankingqueenwoman - manking - manidea (schematic):man to woman is about thesame step as king to queen,soking - man + woman= queenin the toy count-based embeddings of this chapter it came out first (queen, then princess, kingdom)
Figure 1. Parallel steps in embedding space: the famous analogy (schematic)

Read it carefully. The corpus is so regular that king and prince have identical contexts (similarity 1.000), and king and queen are 0.997 similar: the shared "royal" context dominates, and gender is a small direction. Yet that small direction is consistent, so the arithmetic king −- man ++ woman lands closest to queen (then princess), the famous analogy, produced by nothing but counting. (A real Word2Vec on billions of words works the same way and finds many such regularities, with plenty of exceptions: the analogy trick is a striking property, not a guarantee.)

The problem with a fixed vector

The last lines of the output show it: "bank" appears in "the river bank" and "the money bank", yet it has one vector, whose nearest neighbours are a mix of river, water, money and loan words. A static embedding cannot represent polysemy: "Apple" the company and "apple" the fruit, "bank" of a river and a bank for money, share a single point. The meaning of a word depends on its sentence.

1.3Contextual embeddings

The remedy is to make a word's vector depend on the sentence it appears in. Contextual embeddings (ELMo, BERT, GPT) do exactly that: the vector for "bank" is computed fresh from the whole sentence. The mechanism that lets one token's vector absorb its neighbours is attention, the subject of the rest of this week. A first taste (one untrained attention layer, so the numbers are arbitrary but the point is not): the same input vector for "bank" produces different outputs in the two sentences.

main.py
import torch
torch.manual_seed(0)
d = 6
vocab = ["the", "river", "money", "bank", "was", "deep", "closed"]
E = {w: torch.randn(d) for w in vocab}
Wq, Wk, Wv = (torch.randn(d, d) * 0.7 for _ in range(3))

def contextual(sentence):                                                      # one untrained self-attention layer
    x = torch.stack([E[w] for w in sentence])
    return torch.softmax((x @ Wq) @ (x @ Wk).T / d ** 0.5, -1) @ (x @ Wv)

s1, s2 = "the river bank was deep".split(), "the money bank was closed".split()
v1, v2 = contextual(s1)[s1.index("bank")], contextual(s2)[s2.index("bank")]
print("static embedding of 'bank' is one fixed vector:", E["bank"].numpy().round(2))
print("self-attention output for 'bank' in the river sentence:", v1.numpy().round(2))
print("self-attention output for 'bank' in the money sentence:", v2.numpy().round(2))
print("distance between the two contextual vectors:", round((v1 - v2).norm().item(), 3), "(0 for a static embedding)")
print("same word, same input vector; the output now depends on the neighbours, which is the idea behind contextual embeddings")
Output
static embedding of 'bank' is one fixed vector: [-0.89 -1.51  0.37  1.46  0.94  0.77]
self-attention output for 'bank' in the river sentence: [ 0.12  0.04  0.07 -0.15  0.2   1.09]
self-attention output for 'bank' in the money sentence: [ 0.22  1.62 -0.44  0.97  0.03 -0.92]
distance between the two contextual vectors: 2.841 (0 for a static embedding)
same word, same input vector; the output now depends on the neighbours, which is the idea behind contextual embeddings

The static embedding is a single fixed vector; after one attention layer the two "bank" vectors are 2.84 apart. Training would arrange for the river sentence to move "bank" toward the water sense and the money sentence toward the finance sense.

1.4Chapter summary

  • One-hot: orthogonal, huge, no similarity. Embedding: a learned lookup table (= one-hot times a weight matrix), 50,000 x 300 = 15M parameters.
  • Static (Word2Vec, GloVe): one vector per word; analogies like king −- man ++ woman ≈\approx queen emerge from co-occurrence counts, but polysemy is lost.
  • Contextual (ELMo, BERT, GPT): the vector depends on the sentence, which attention makes possible.
Next · Part 2From the Bottleneck to Attention
02
Part 2

From the Bottleneck to Attention

Week 8 ended on a measured problem: an encoder-decoder squeezes the whole input into one context vector, and copy accuracy fell from 0.997 to 0.15 as the input grew from 4 to 32 symbols. The fix, attention (Bahdanau et al., 2014), is one idea: do not throw the encoder's states away; let the decoder look back at all of them, and choose where to look at each step.

2.1The human analogy

A person translating "John loves Mary" into Hindi does not memorise the sentence and then write; while writing "John" they look at "John", while writing "loves" they look at "loves". Attention is selective focus: for each output, a weighting over the input saying how much each part matters right now.

2.2Queries, keys and values

xWqQWkKWvVscoresQ K^T / sqrt(d)softmaxweights x Vweighted averageoutquery: what am I looking for? key: what do I offer for matching? value: what do I actually carry?compare the query with every key, turn the scores into weights that sum to 1, average the values
Figure 2. Compare the query with every key, normalise to weights, average the values

The cleanest way to think about it is a soft dictionary lookup, in the notes' cookbook analogy. The query is the recipe you are looking for ("what am I looking for?"), the keys are the recipe titles ("what can I offer for matching?") and the values are the recipes themselves ("what do I actually carry?"). A normal dictionary returns the value of the one key that matches exactly. Attention returns a blend: it scores the query against every key, turns the scores into weights that sum to 1 (softmax), and returns the weighted average of the values. The recipe is:

  • Score the query against every key (a dot product: large when they point the same way).
  • Softmax the scores into weights: non-negative and summing to 1.
  • Average the values with those weights.

2.3The fix, measured

Give the Week 8 copy task an attention decoder: at every step the decoder's state is the query, the encoder states are both keys and values, and the context it uses is the weighted average. Everything else (task, hidden size 32, training length, optimiser) is identical to Week 8, so the comparison is clean:

main.py
import math, torch, torch.nn as nn
torch.set_num_threads(1)
V, BOS = 12, 10

class AttnSeq2Seq(nn.Module):
    def __init__(self, hidden):
        super().__init__()
        self.emb = nn.Embedding(V, 32)
        self.encoder = nn.GRU(32, hidden, batch_first=True)
        self.cell = nn.GRUCell(32 + hidden, hidden)
        self.out = nn.Linear(2 * hidden, V)
    def attend(self, query, enc):                                     # query: (B, h)   enc: (B, T, h) = the keys AND the values
        scores = torch.bmm(enc, query.unsqueeze(2)).squeeze(2) / math.sqrt(enc.size(2))
        weights = torch.softmax(scores, dim=1)                        # (B, T): where to look in the input
        return torch.bmm(weights.unsqueeze(1), enc).squeeze(1)        # weighted average of ALL encoder states
    def step(self, tok, state, enc):
        state = self.cell(torch.cat([self.emb(tok), self.attend(state, enc)], 1), state)
        return self.out(torch.cat([state, self.attend(state, enc)], 1)), state
    def forward(self, src, tgt_in):                                   # teacher forcing
        enc, last = self.encoder(self.emb(src)); state, outs = last[0], []
        for t in range(tgt_in.size(1)):
            o, state = self.step(tgt_in[:, t], state, enc); outs.append(o)
        return torch.stack(outs, 1)
    @torch.no_grad()
    def generate(self, src, length):                                  # free running
        enc, last = self.encoder(self.emb(src)); state = last[0]; tok = torch.full((src.size(0),), BOS); res = []
        for _ in range(length):
            o, state = self.step(tok, state, enc); tok = o.argmax(-1); res.append(tok)
        return torch.stack(res, 1)

def evaluate(length, hidden=32, seed=0, steps=2500):
    torch.manual_seed(seed); model = AttnSeq2Seq(hidden); opt = torch.optim.Adam(model.parameters(), lr=3e-3)
    batch = lambda B: (lambda s: (s, torch.cat([torch.full((B, 1), BOS), s[:, :-1]], 1)))(torch.randint(0, 10, (B, length)))
    for _ in range(steps):
        src, tgt_in = batch(64)
        loss = nn.functional.cross_entropy(model(src, tgt_in).reshape(-1, V), src.reshape(-1))
        opt.zero_grad(); loss.backward(); nn.utils.clip_grad_norm_(model.parameters(), 1.0); opt.step()
    src, tgt_in = batch(1000)
    with torch.no_grad():
        forced = (model(src, tgt_in).argmax(-1) == src).float().mean().item(); free = model.generate(src, length)
    return forced, (free == src).float().mean().item(), (free == src).all(1).float().mean().item()

print("copy task WITH attention, hidden size 32 (the same task, size and training as Week 8's plain encoder-decoder)")
print("sequence length   teacher-forced acc   free-running acc   whole sequence exact")
for length in (4, 8, 16, 32):
    a, b, c = evaluate(length)
    print(f"{length:10d}      {a:14.3f}      {b:14.3f}      {c:14.3f}")
Output
copy task WITH attention, hidden size 32 (the same task, size and training as Week 8's plain encoder-decoder)
sequence length   teacher-forced acc   free-running acc   whole sequence exact
         4               1.000               1.000               1.000
         8               1.000               1.000               1.000
        16               0.999               0.995               0.982
        32               0.988               0.894               0.738
00.510.9971.000length 40.7941.000length 80.3030.995length 160.1470.894length 32one context vectorwith attentionfree-running copy accuracy, hidden size 32, same training (chance 0.10)attention lets the decoder look back at every encoder state, so the bottleneck of Week 8 disappears
Figure 3. Same task, same size: one context vector against attention

Free-running accuracy at length 16 goes from 0.30 to 0.995, and at length 32 from 0.15 to 0.894; lengths 4 and 8 are perfect. No extra capacity was added to the hidden state: the decoder simply stopped depending on a single vector. (Length 32 is still not perfect, 0.894 free-running with 74% of whole sequences exact, so the model is not magic; this is a small network after a short training run.) This is the entire motivation for the architecture of the next chapters.

2.4Attention pooling

The weighted average of values is sometimes called attention pooling. Because the weights are non-negative and sum to 1, the output is a convex combination of the values: it always lies inside the range of the values, and the weights are a probability distribution over the positions, readable as "where the model looked". That readability is a large part of attention's appeal.

2.5Chapter summary

  • Attention lets the decoder look at all encoder states: query vs keys →\to softmax weights →\to weighted average of values.
  • Measured on the Week 8 copy task: length 16 accuracy 0.30 →\to 0.995, length 32 0.15 →\to 0.894 with no bigger hidden state.
  • The output is a convex combination of the values; the weights are a distribution over positions.
Next · Part 3Scaled Dot-Product Attention
03
Part 3

Scaled Dot-Product Attention

The attention used in Transformers is a specific, simple recipe called scaled dot-product attention. It is the formula to know cold for the end term.

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)V
  • Q∈Rnq×dkQ\in\mathbb{R}^{n_q\times d_k}: one query row per position that is asking.
  • K∈Rnk×dkK\in\mathbb{R}^{n_k\times d_k}: one key row per position that can be attended to (same width dkd_k as the queries, so a dot product exists).
  • V∈Rnk×dvV\in\mathbb{R}^{n_k\times d_v}: one value row per key (any width dvd_v).
  • QK⊤∈Rnq×nkQK^\top\in\mathbb{R}^{n_q\times n_k}: every query against every key; softmax is applied row by row; the result times VV has shape nq×dvn_q\times d_v.

3.1A worked example

The notes' example: Q=[1,0]Q=[1,0]; keys [1,0],[0,1],[1,1][1,0],[0,1],[1,1]; values 10, 20, 30; dk=2d_k=2. The scores are q⋅k/2=[0.707, 0, 0.707]q\cdot k/\sqrt2=[0.707,\,0,\,0.707] and the softmax gives weights e0.707/(2e0.707+1)=0.401e^{0.707}/(2e^{0.707}+1)=0.401, 1/(2e0.707+1)=0.1981/(2e^{0.707}+1)=0.198, 0.401:

query [1, 0] looks at three keys; each key carries a valuekey 1 = [1,0]weight 0.401value 10key 2 = [0,1]weight 0.198value 20key 3 = [1,1]weight 0.401value 300.401 x 10 + 0.198 x 20 + 0.401 x 30 = 4.01 + 3.96 + 12.03 = 20.0the output is a weighted average of the values: it must lie between 10 and 30
Figure 4. Weights 0.401, 0.198, 0.401 over values 10, 20, 30
main.py
import numpy as np
Q = np.array([[1.0, 0.0]])
K = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
V = np.array([[10.0], [20.0], [30.0]])
d_k = K.shape[1]
scores = Q @ K.T / np.sqrt(d_k)
weights = np.exp(scores) / np.exp(scores).sum()
print("scores  q.k / sqrt(2):", scores.round(3).tolist())
print("softmax weights      :", weights.round(3).tolist(), " sum =", weights.sum().round(3))
print("output = weights @ V :", (weights @ V).round(2).tolist())
print("the query [1,0] matches keys 1 and 3 equally, so they get equal weight 0.401; key 2 (orthogonal) gets 0.198")
Output
scores  q.k / sqrt(2): [[0.707, 0.0, 0.707]]
softmax weights      : [[0.401, 0.198, 0.401]]  sum = 1.0
output = weights @ V : [[20.0]]
the query [1,0] matches keys 1 and 3 equally, so they get equal weight 0.401; key 2 (orthogonal) gets 0.198

The output is 0.401×10+0.198×20+0.401×30=20.00.401\times10+0.198\times20+0.401\times30=\mathbf{20.0}, matching the notes. Keys 1 and 3 each overlap the query (a dot product of 1) and get equal weight; key 2 is orthogonal and gets less, but not zero: softmax never gives exactly zero.

3.2The same in code, and its properties

main.py
import torch, torch.nn.functional as F
torch.manual_seed(0)
n_q, n_k, d_k, d_v = 3, 5, 4, 6
Q, K, V = torch.randn(n_q, d_k), torch.randn(n_k, d_k), torch.randn(n_k, d_v)

scores = Q @ K.T / d_k ** 0.5                       # (n_q, n_k): how well each query matches each key
weights = torch.softmax(scores, dim=-1)             # each row: a probability distribution over the keys
out = weights @ V                                   # (n_q, d_v): a weighted average of the values
print("shapes: Q", tuple(Q.shape), "K", tuple(K.shape), "V", tuple(V.shape), "-> scores", tuple(scores.shape), "-> output", tuple(out.shape))
print("matches F.scaled_dot_product_attention:", torch.allclose(out, F.scaled_dot_product_attention(Q, K, V), atol=1e-6))
print("each row of weights sums to 1:", weights.sum(-1).tolist())
print("weights are non-negative:", bool((weights >= 0).all()))
inside = bool(((out >= V.min(0).values - 1e-6) & (out <= V.max(0).values + 1e-6)).all())
print("every output lies between the smallest and largest value (a convex combination of the rows of V):", inside)
print("changing the value of a key with weight ~0 barely changes the output; the attention weights say WHERE the model looked")
Output
shapes: Q (3, 4) K (5, 4) V (5, 6) -> scores (3, 5) -> output (3, 6)
matches F.scaled_dot_product_attention: True
each row of weights sums to 1: [1.0, 1.0, 1.0]
weights are non-negative: True
every output lies between the smallest and largest value (a convex combination of the rows of V): True
changing the value of a key with weight ~0 barely changes the output; the attention weights say WHERE the model looked

Four properties to quote: (1) each row of weights sums to 1; (2) all weights are non-negative; (3) so each output is a convex combination of the value rows, lying within their range; (4) the whole computation is two matrix multiplications and a softmax, so it parallelises completely over positions, unlike an RNN which must go step by step.

3.3Why divide by dk\sqrt{d_k}?

If the entries of qq and kk are independent with mean 0 and variance 1, then q⋅k=∑i=1dkqikiq\cdot k=\sum_{i=1}^{d_k}q_ik_i is a sum of dkd_k independent terms with variance 1, so it has variance dkd_k and standard deviation dk\sqrt{d_k}. Without the division the scores grow with the key dimension, and softmax of large numbers is nearly one-hot: one weight near 1, the rest near 0. There the softmax is flat, its gradient is almost zero, and learning stalls. Dividing by dk\sqrt{d_k} restores a standard deviation of 1. Measure it:

00.51416642561024key dimension d_klargest attention weight (10 keys, random q and k)without scalingone weight takes over(0.97 at d = 1024)divided by sqrt(d_k)stays at about 0.32
Figure 5. The largest attention weight against the key dimension, with and without scaling
main.py
import torch
torch.manual_seed(0)
n_keys, trials = 10, 20000
print("   d_k   std(q.k)   std(q.k / sqrt(d_k))   mean max-weight: unscaled   scaled   |   softmax gradient norm: unscaled   scaled")
for d in (4, 16, 64, 256, 1024):
    q = torch.randn(trials, 1, d); k = torch.randn(trials, n_keys, d)          # entries with mean 0 and variance 1
    raw = (q @ k.transpose(1, 2)).squeeze(1); scaled = raw / d ** 0.5
    stats = []
    for s in (raw, scaled):
        s = s.clone().requires_grad_()
        w = torch.softmax(s, -1)
        (w[:, 0]).sum().backward()                                              # how strongly does the first logit move the first weight?
        stats.append((w.max(-1).values.mean().item(), s.grad.norm(dim=-1).mean().item()))
    print(f"{d:6d}   {raw.std().item():7.2f}   {scaled.std().item():12.2f}          {stats[0][0]:21.2f}   {stats[1][0]:6.2f}   |   {stats[0][1]:25.4f}   {stats[1][1]:6.4f}")
print("a dot product of d independent terms has variance d, so without the division the softmax saturates (one weight near 1) and its gradient vanishes")
Output
   d_k   std(q.k)   std(q.k / sqrt(d_k))   mean max-weight: unscaled   scaled   |   softmax gradient norm: unscaled   scaled
     4      2.00           1.00                           0.49     0.30   |                      0.0732   0.0882
    16      4.00           1.00                           0.73     0.32   |                      0.0461   0.0884
    64      8.00           1.00                           0.87     0.32   |                      0.0251   0.0886
   256     15.98           1.00                           0.93     0.32   |                      0.0125   0.0882
  1024     31.97           1.00                           0.97     0.32   |                      0.0067   0.0887
a dot product of d independent terms has variance d, so without the division the softmax saturates (one weight near 1) and its gradient vanishes

The standard deviation of q⋅kq\cdot k is exactly dk\sqrt{d_k} (2, 4, 8, 16, 32 in the second column, from dk=4d_k=4 to 1024); after scaling it is 1 at every size. Unscaled, the average largest weight over 10 keys climbs from 0.49 to 0.97 and the softmax gradient norm falls from 0.073 to 0.007, a 10-fold collapse; scaled, the largest weight stays at about 0.32 and the gradient stays at 0.088 whatever dkd_k is. That is the whole reason for the 1/dk1/\sqrt{d_k}: it keeps the softmax in its responsive range.

3.4Chapter summary

  • softmax(QK⊤/dk)V\mathrm{softmax}(QK^\top/\sqrt{d_k})V: weights are a distribution over keys; output is a convex combination of values. Worked example: 20.0.
  • q⋅kq\cdot k has variance dkd_k; dividing by dk\sqrt{d_k} prevents saturation (largest weight 0.97 →\to 0.32 and gradient 0.007 →\to 0.088 at dk=1024d_k=1024).
  • Two matrix products and a softmax: fully parallel over positions.
Next · Part 4Self, Cross and Masked Attention
04
Part 4

Self, Cross and Masked Attention

The recipe of the last chapter does not say where QQ, KK and VV come from. The answer defines the two kinds of attention and decides whether a layer can peek at the future.

4.1Self-attention and cross-attention

SELF-attention: Q, K, V all from the same sequencet1t2t3t4every token chooses among all tokens of the same sequenceCROSS-attention: Q from the decoder, K and V from the encodere1e2e3e4d1d2encoder outputs give K and Veach decoder token (Q) looks atevery encoder position
Figure 6. Self-attention: one sequence looks at itself. Cross-attention: one sequence looks at another
  • Self-attention: QQ, KK, VV are all computed from the same sequence (each from the input by its own learned matrix: Q=XWQQ=XW^Q, K=XWKK=XW^K, V=XWVV=XW^V). Every token asks "which other tokens of this sentence matter to me?". In "The animal didn't cross the street because it was tired", self-attention lets "it" draw on "animal". Used in the Transformer encoder (and, masked, the decoder).
  • Cross-attention: QQ comes from one sequence (the decoder's current state) while KK and VV come from another (the encoder output). "While generating this word, where in the input should I look?" This is the Week 8 seq2seq attention, and it is used in the Transformer decoder.
main.py
import torch, torch.nn as nn
torch.manual_seed(0)
d, heads = 8, 2
mha = nn.MultiheadAttention(d, heads, batch_first=True)
encoder_out = torch.randn(1, 6, d)                     # 6 source tokens
decoder_in = torch.randn(1, 4, d)                      # 4 target tokens produced so far

out, w = mha(encoder_out, encoder_out, encoder_out, average_attn_weights=True)
print("SELF-attention  (Q, K, V all from the source)  : output", tuple(out.shape), " weights", tuple(w.shape), "= (batch, 6 queries, 6 keys)")
out, w = mha(decoder_in, encoder_out, encoder_out, average_attn_weights=True)
print("CROSS-attention (Q from decoder; K, V from the encoder): output", tuple(out.shape), " weights", tuple(w.shape), "= (batch, 4 queries, 6 keys)")
print("so in cross-attention each of the 4 decoder positions chooses among the 6 encoder positions; the output has the decoder's length")
Output
SELF-attention  (Q, K, V all from the source)  : output (1, 6, 8)  weights (1, 6, 6) = (batch, 6 queries, 6 keys)
CROSS-attention (Q from decoder; K, V from the encoder): output (1, 4, 8)  weights (1, 4, 6) = (batch, 4 queries, 6 keys)
so in cross-attention each of the 4 decoder positions chooses among the 6 encoder positions; the output has the decoder's length

The weight matrices show the difference: self-attention is 6×66\times6 (each of 6 source tokens looks at all 6); cross-attention is 4×64\times6 (each of 4 decoder tokens chooses among the 6 encoder tokens), and the output has the query's length, 4. The number of keys and values only has to match each other, not the number of queries.

4.2Masked attention: no peeking at the future

When a model generates text one token at a time, position tt must not see tokens after tt: in training the whole target sentence is given at once, so without protection the model would just copy the next word. The fix is a causal mask: before the softmax, set the score of every future position to −∞-\infty. Then e−∞=0e^{-\infty}=0, the weight is exactly zero, and the remaining weights are renormalised to sum to 1.

attends to (column)t1t2t3t4t5t11.00t20.050.95t30.130.250.63t40.010.010.090.89t50.010.010.110.300.58red cells: the FUTURE.their score is set to -infinitybefore the softmax, so theweight is exactly 0each row still sums to 1row t looks only at t and beforelookingtoken
Figure 7. A causal mask: the upper triangle is blocked
main.py
import torch
torch.manual_seed(0)
n, d = 5, 4
x = torch.randn(n, d)
scores = x @ x.T / d ** 0.5

causal = torch.triu(torch.ones(n, n, dtype=torch.bool), diagonal=1)          # True above the diagonal = the FUTURE
weights = torch.softmax(scores.masked_fill(causal, float("-inf")), dim=-1)    # -inf before the softmax = weight exactly 0 after it
print("causal attention weights (row = the position that is looking, column = what it looks at):")
print(weights.numpy().round(2))
print("rows still sum to 1:", weights.sum(-1).tolist())

def attend(x, mask):
    return torch.softmax((x @ x.T / d ** 0.5).masked_fill(mask, float("-inf")), -1) @ x
x2 = x.clone(); x2[3:] = torch.randn(2, d)                                    # change the FUTURE tokens 3 and 4
print("\nwith the causal mask the outputs at positions 0, 1, 2 do not change when tokens 3, 4 change:",
      torch.allclose(attend(x, causal)[:3], attend(x2, causal)[:3]))
nomask = torch.zeros(n, n, dtype=torch.bool)
print("without the mask they do change (the model could read the answer):", not torch.allclose(attend(x, nomask)[:3], attend(x2, nomask)[:3]))

pad = torch.tensor([False, False, False, True, True])                          # padding mask: positions 3 and 4 are padding
w = torch.softmax(scores.masked_fill(pad[None, :], float("-inf")), -1)
print("padding mask: weight given to the padding positions =", w[:, 3:].sum().item())
Output
causal attention weights (row = the position that is looking, column = what it looks at):
[[1.   0.   0.   0.   0.  ]
 [0.05 0.95 0.   0.   0.  ]
 [0.13 0.25 0.63 0.   0.  ]
 [0.01 0.01 0.09 0.89 0.  ]
 [0.01 0.01 0.11 0.3  0.58]]
rows still sum to 1: [1.0, 0.9999999403953552, 1.0, 1.0, 0.9999998807907104]

with the causal mask the outputs at positions 0, 1, 2 do not change when tokens 3, 4 change: True
without the mask they do change (the model could read the answer): True
padding mask: weight given to the padding positions = 0.0

The matrix is lower-triangular: token 1 can only attend to itself (weight 1.00), token 5 to all five. The test is the proof that matters: with the mask, changing tokens 3 and 4 leaves the outputs at positions 0, 1, 2 unchanged (True); without it they change, i.e. information leaks from the future. The notes' mistake-list is right: masking in the decoder is causal (autoregressive), not only for padding. A separate padding mask (the last lines) stops tokens attending to the filler positions added to make a batch rectangular; its weight on padding is exactly 0.0. The two masks are combined when both apply.

4.3Self-attention cannot tell the order

There is a catch hidden in the formula. Attention treats its inputs as a set: reordering the tokens just reorders the outputs. Take "dog bites man" and "man bites dog", with the same weights, and look at the output for "bites":

main.py
import torch
torch.manual_seed(0)
d = 6
emb = {w: torch.randn(d) for w in ("dog", "bites", "man")}
Wq, Wk, Wv = (torch.randn(d, d) * 0.5 for _ in range(3))

def self_attention(x):
    Q, K, V = x @ Wq, x @ Wk, x @ Wv
    return torch.softmax(Q @ K.T / d ** 0.5, -1) @ V

a = torch.stack([emb[w] for w in ("dog", "bites", "man")])                    # "dog bites man"
b = torch.stack([emb[w] for w in ("man", "bites", "dog")])                    # "man bites dog"
oa, ob = self_attention(a), self_attention(b)
print("output for 'bites' in 'dog bites man':", oa[1].numpy().round(3))
print("output for 'bites' in 'man bites dog':", ob[1].numpy().round(3))
print("identical without position information:", torch.allclose(oa[1], ob[1], atol=1e-6), " (self-attention sees a SET of words, not a sequence)")

pos = torch.randn(3, d)                                                        # any distinct vector per position will do for this test
oa2, ob2 = self_attention(a + pos), self_attention(b + pos)
print("after adding a position vector to each token: identical?", torch.allclose(oa2[1], ob2[1], atol=1e-6), "-> the two sentences are now told apart")
Output
output for 'bites' in 'dog bites man': [-0.72   0.958  1.767  0.24   1.691  0.924]
output for 'bites' in 'man bites dog': [-0.72   0.958  1.767  0.24   1.691  0.924]
identical without position information: True  (self-attention sees a SET of words, not a sequence)
after adding a position vector to each token: identical? False -> the two sentences are now told apart

The outputs for "bites" are identical, so a Transformer built only from attention cannot distinguish the two sentences, unlike an RNN, whose order is built into the recurrence. The cure is to add positional information to the token vectors (positional encoding, Week 10); the last line shows that once each position carries its own vector, the two sentences are told apart. It is precisely why the notes say self-attention is permutation-invariant "and that is why we add positional encoding".

4.4Chapter summary

  • Self-attention: Q,K,VQ,K,V from one sequence. Cross-attention: QQ from the decoder, K,VK,V from the encoder; output length = number of queries.
  • Causal mask: future scores =−∞=-\infty before softmax, weight exactly 0 (verified: past outputs unchanged when the future changes). Padding mask is separate.
  • Attention alone is order-blind (the "bites" outputs were identical): add positional encoding.
Next · Part 5Multi-Head Attention
05
Part 5

Multi-Head Attention

One attention pass produces one weighting per query: one way of deciding what is relevant. A word may need several at once: its grammatical subject, the previous word, a distant noun it refers to. Multi-head attention runs several attentions in parallel, each in its own smaller space, and combines them.

5.1The formula

xhead 1own Wq, Wk, Wv (d/h wide)head 2own Wq, Wk, Wv (d/h wide)head 3own Wq, Wk, Wv (d/h wide)... h headsconcatenateh * d/h = dW_Omix the headsouth attention patterns in h smaller subspaces, concatenated (not averaged), then mixed by W_Oparameters stay at 4d^2 whatever h is: more heads split the same width, they do not add weights
Figure 8. Each head attends in its own subspace; the heads are concatenated and mixed
headi=Attention(QWiQ, KWiK, VWiV),MultiHead(Q,K,V)=Concat(head1,…,headh) WO\mathrm{head}_i=\mathrm{Attention}\big(QW_i^{Q},\,KW_i^{K},\,VW_i^{V}\big),\qquad \mathrm{MultiHead}(Q,K,V)=\mathrm{Concat}(\mathrm{head}_1,\dots,\mathrm{head}_h)\,W^{O}

With model width dd and hh heads each head has width dk=dv=d/hd_k=d_v=d/h (8 heads on d=512d=512 gives 64). Each head has its own projections WiQ,WiK,WiVW_i^Q,W_i^K,W_i^V, so it can learn its own notion of "relevant". The head outputs are concatenated (not averaged, a point the notes stress), restoring width h⋅d/h=dh\cdot d/h=d, and a final matrix WOW^O mixes them. Write it by hand and compare with PyTorch:

main.py
import torch, torch.nn as nn
torch.manual_seed(0)
d, h, n = 8, 2, 5
dk = d // h                                                    # each head works in a d/h = 4 dimensional subspace
mha = nn.MultiheadAttention(d, h, batch_first=True)
x = torch.randn(1, n, d)

Wq, Wk, Wv = mha.in_proj_weight.chunk(3, 0); bq, bk, bv = mha.in_proj_bias.chunk(3, 0)
Q, K, V = x[0] @ Wq.T + bq, x[0] @ Wk.T + bk, x[0] @ Wv.T + bv                # one big projection each, then split into heads
heads = []
for i in range(h):
    sl = slice(i * dk, (i + 1) * dk)
    w = torch.softmax(Q[:, sl] @ K[:, sl].T / dk ** 0.5, -1)                  # each head has its OWN attention pattern
    heads.append(w @ V[:, sl])
concat = torch.cat(heads, dim=-1)                                              # CONCATENATE the heads (not average) ...
out = concat @ mha.out_proj.weight.T + mha.out_proj.bias                       # ... and mix them with W_O
print("hand-written multi-head attention matches nn.MultiheadAttention:", torch.allclose(out, mha(x, x, x)[0][0], atol=1e-5))

count = lambda m: sum(p.numel() for p in m.parameters())
for heads_n in (1, 2, 4, 8):
    print(f"d_model=64, heads={heads_n}: parameters {count(nn.MultiheadAttention(64, heads_n))} = 4*64*64 + 4*64 = {4 * 64 * 64 + 4 * 64}   (head size {64 // heads_n})")
print("more heads do NOT add parameters: they split the same d_model into smaller subspaces")
Output
hand-written multi-head attention matches nn.MultiheadAttention: True
d_model=64, heads=1: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 64)
d_model=64, heads=2: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 32)
d_model=64, heads=4: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 16)
d_model=64, heads=8: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 8)
more heads do NOT add parameters: they split the same d_model into smaller subspaces

The hand-written version matches nn.MultiheadAttention. And the parameter table has the most surprising fact of the chapter: with d=64d=64, going from 1 head to 8 heads does not change the parameter count (16,640 each, =4d2+4d=4d^2+4d: the four matrices WQ,WK,WV,WOW^Q,W^K,W^V,W^O plus biases). More heads do not add weights; they cut the same width into smaller subspaces, so each head sees a lower-dimensional view and the total cost stays about the same as one wide head.

What heads "specialise" in The notes say head 1 follows syntax, head 2 meaning, head 3 position. Studies of trained Transformers do find some heads with clear roles (attending to the previous token, to a matching bracket) but many heads are redundant or mixed, and some can be removed with little loss. Treat "committee of experts" as the motivation, not as something guaranteed or something you can read off the weights.

5.2The price: quadratic cost

Every token attends to every token, so the score matrix is n×nn\times n and the work is O(n2d)O(n^2d), quadratic in the sequence length (the notes' Example 4). Doubling the length quadruples the cost:

main.py
print("self-attention builds an n x n score matrix per head")
print("      n     score entries    memory (float32)    n^2 * d operations (d = 512)")
for n in (512, 2048, 8192, 32768, 131072):
    entries = n * n
    mem = entries * 4
    unit = ("MB", mem / 1e6) if mem < 1e9 else ("GB", mem / 1e9)
    print(f"{n:7d}   {entries:14.2e}    {unit[1]:8.1f} {unit[0]}        {n * n * 512:.2e}")
print("doubling the length quadruples the cost; this is why long context is expensive and why efficient attention is an active research area")
Output
self-attention builds an n x n score matrix per head
      n     score entries    memory (float32)    n^2 * d operations (d = 512)
    512         2.62e+05         1.0 MB        1.34e+08
   2048         4.19e+06        16.8 MB        2.15e+09
   8192         6.71e+07       268.4 MB        3.44e+10
  32768         1.07e+09         4.3 GB        5.50e+11
 131072         1.72e+10        68.7 GB        8.80e+12
doubling the length quadruples the cost; this is why long context is expensive and why efficient attention is an active research area

At n=512,d=512n=512,d=512 it is 134 million multiply-adds per layer (the notes' figure); at n=2048n=2048, 2.1 billion. The score matrix alone, for a single head, is 1 MB at 512 tokens and 4.3 GB at 32,768. RNNs have cost O(nd2)O(nd^2), linear in length but sequential; attention is quadratic but parallel, which on GPUs is a very good trade for the lengths used until recently, and a real constraint for books and long videos (hence "efficient attention" research).

5.3Chapter summary

  • headi=Attention(QWiQ,KWiK,VWiV)\mathrm{head}_i=\mathrm{Attention}(QW_i^Q,KW_i^K,VW_i^V); MultiHead=Concat(heads)WO\mathrm{MultiHead}=\mathrm{Concat}(\text{heads})W^O; head width d/hd/h; concatenated, not averaged.
  • Parameters 4d2+4d4d^2+4d for any number of heads (16,640 at d=64d=64 for h=1,2,4,8h=1,2,4,8): heads split the width, they do not add weights.
  • Cost O(n2d)O(n^2d): 134M at n=512n=512, 2.1B at 2048; score matrix 4.3 GB at 32,768 tokens.
Next · InterviewInterview Question Bank
06
Interview

Interview Question Bank

What is likely to be asked from Week 9, with 30-second answers, deeper explanations, a lecture-or-extra tag on each question, and code you can run.

Week 9 questions are about why attention exists, what its formula means, and how its variants differ. Each answer starts with a 30-second version. Questions are tagged From the notes or Extra.

1Definition

One clear sentence: what it is.

2Intuition or tiny example

Why it works, in one picture or three lines.

3The failure it prevents

What goes wrong without it.

6.1Interview heat-map: what is most likely

Very likely7 topics
Q, K, V intuition"Explain queries, keys and values with an intuition."Notes 2
Scaled dot-product attention"Write the attention formula. Properties of the weights?"Notes 3
Why scale by root d_k"Why divide by sqrt(d_k)?"Notes 3
Self-attention versus cross-attention"Self-attention versus cross-attention?"Notes 2
Multi-head attention"Explain multi-head attention and its parameter count."Notes 2, 3
The RNN bottleneck and attention"Why was attention invented?"Notes 2
Masked self-attention"Why is the decoder self-attention masked?"Notes 2
Likely4 topics
Static versus contextual embeddings"Static versus contextual embeddings?"Notes 1
Positional information"Why does self-attention need positional encoding?"Notes 2, Ex. 3
Cost of attention"What is the complexity of self-attention?"Notes Ex. 4
One-hot versus embedding"Why not use one-hot vectors for words?"Notes 1
Sometimes1 topics
Attention weights as explanation"Do attention weights explain the model?"Extra

6.2Embeddings and motivation

Interview 1Why use embeddings instead of one-hot encodings? How is the embedding layer implemented?
Week 9LikelyNotes 1From the lecture

One-hot knows nothing; embeddings are dense and learned.

Prints
one-hot dot products (king.queen, king.apple): 0.0 0.0  <- every pair is orthogonal: no notion of similarity
embedding lookup equals one-hot @ weight matrix: True
a vocabulary of 50,000 words with 300-dimensional embeddings has 15,000,000 parameters; a one-hot vector would have 50,000 dimensions
Answer
Say this first (30 seconds)

One-hot vectors are as long as the vocabulary, sparse, and mutually orthogonal: no similarity ("king" is as far from "queen" as from "apple"). Embeddings are short, dense vectors learned so that related words are close. The embedding layer is a lookup table of size V×dV\times d, equivalent to multiplying a one-hot vector by a weight matrix (verified), with VdVd parameters.

Go deeper
  • 50,000 words x 300 dimensions = 15,000,000 parameters.
Follow-up they may askWhat is a sub-word token and why are they used?
Interview 2Compare static (Word2Vec, GloVe) and contextual (BERT, GPT) embeddings. Give an example where static fails.
Week 9LikelyNotes 1From the lecture

One vector per word versus one per occurrence.

Prints
cosine similarities:  king~prince 1.0  king~queen 0.997  king~woman 0.374  king~bread 0.316
king - man + woman is closest to: ['queen', 'princess', 'kingdom']

the polysemy problem: ONE vector for 'bank' although the text uses it in two senses
  nearest words to 'bank': ['river', 'money', 'water', 'loan', 'market', 'crown', 'ruled', 'queen']
Prints
distance between the two contextual vectors: 2.841 (0 for a static embedding)
same word, same input vector; the output now depends on the neighbours, which is the idea behind contextual embeddings
Answer
Say this first (30 seconds)

Static embeddings give each word one fixed vector learned from co-occurrence, so they cannot represent polysemy: "bank" (river) and "bank" (money) share a point. Contextual embeddings compute the vector for each occurrence from the whole sentence (via attention), so "bank" differs by context. Static ones are still cheap and useful (and show analogies such as king −- man ++ woman ≈\approx queen).

Go deeper
  • Measured: one self-attention layer made the two "bank" vectors 2.84 apart; the toy count-based embedding put river and money words around the single "bank".
Follow-up they may askName another polysemous word.
Interview 3What problem of RNN seq2seq does attention solve and how?
Week 9Very likelyNotes 2From the lecture

One context vector is too small.

Prints
sequence length   teacher-forced acc   free-running acc   whole sequence exact
         4               1.000               1.000               1.000
         8               1.000               1.000               1.000
        16               0.999               0.995               0.982
        32               0.988               0.894               0.738
Answer
Say this first (30 seconds)

A plain encoder-decoder passes the whole input through one fixed-size context vector, which loses information as the input grows. Attention keeps all encoder states and lets the decoder, at every step, compute a weighted average of them (query = decoder state, keys = values = encoder states), so it can look at the relevant input positions directly. It also shortens the path for gradients between output and input.

Go deeper
  • Measured with hidden size 32: length-16 copy accuracy 0.30 without attention, 0.995 with; length 32: 0.15 against 0.894.
Follow-up they may askWhat does the attention weight matrix of a translation model show?

6.3The attention formula

Interview 4Explain queries, keys and values with an intuition. What do you do with them?
Week 9Very likelyNotes 2From the lecture

A soft dictionary lookup.

Prints
scores  q.k / sqrt(2): [[0.707, 0.0, 0.707]]
softmax weights      : [[0.401, 0.198, 0.401]]  sum = 1.0
output = weights @ V : [[20.0]]
the query [1,0] matches keys 1 and 3 equally, so they get equal weight 0.401; key 2 (orthogonal) gets 0.198
Answer
Say this first (30 seconds)

A query is what a position is looking for, a key is what each position advertises for matching, a value is the content it actually contributes (cookbook: the recipe you want, the recipe titles, the recipes). Compare the query with every key (dot product) to get scores, softmax them into weights that sum to 1, and return the weighted sum of the values. It is a dictionary lookup that returns a blend instead of one exact match.

Go deeper
  • In self-attention all three come from the same input by three learned matrices.
Follow-up they may askWhy are keys and values separate?
Interview 5Write the scaled dot-product attention formula and the shapes. What are the properties of the weights?
Week 9Very likelyNotes 3From the lecture

softmax(QKT over root dk) V.

Prints
shapes: Q (3, 4) K (5, 4) V (5, 6) -> scores (3, 5) -> output (3, 6)
matches F.scaled_dot_product_attention: True
each row of weights sums to 1: [1.0, 1.0, 1.0]
weights are non-negative: True
every output lies between the smallest and largest value (a convex combination of the rows of V): True
changing the value of a key with weight ~0 barely changes the output; the attention weights say WHERE the model looked
Answer
Say this first (30 seconds)

Attention(Q,K,V)=softmax ⁣(QK⊤/dk)V\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\!\big(QK^\top/\sqrt{d_k}\big)V, with Q:nq×dkQ:n_q\times d_k, K:nk×dkK:n_k\times d_k, V:nk×dvV:n_k\times d_v, scores nq×nkn_q\times n_k, output nq×dvn_q\times d_v. Softmax is per row, so the weights are non-negative and sum to 1: a probability distribution over positions, and the output is a convex combination of the value rows. Worked example: weights [0.401,0.198,0.401][0.401,0.198,0.401] on values 10,20,3010,20,30 give 20.0.

Go deeper
  • Verified against F.scaled_dot_product_attention; rows sum to 1.
Follow-up they may askWhat would the output be if all scores were equal?
Interview 6Why do we divide the scores by dk\sqrt{d_k}?
Week 9Very likelyNotes 3From the lecture

Variance of a dot product is d_k.

Prints
   d_k   std(q.k)   std(q.k / sqrt(d_k))   mean max-weight: unscaled   scaled   |   softmax gradient norm: unscaled   scaled
     4      2.00           1.00                           0.49     0.30   |                      0.0732   0.0882
    16      4.00           1.00                           0.73     0.32   |                      0.0461   0.0884
    64      8.00           1.00                           0.87     0.32   |                      0.0251   0.0886
   256     15.98           1.00                           0.93     0.32   |                      0.0125   0.0882
  1024     31.97           1.00                           0.97     0.32   |                      0.0067   0.0887
a dot product of d independent terms has variance d, so without the division the softmax saturates (one weight near 1) and its gradient vanishes
Answer
Say this first (30 seconds)

For independent entries with mean 0 and variance 1, q⋅kq\cdot k has variance dkd_k, so scores grow like dk\sqrt{d_k}. Large scores push the softmax into saturation (nearly one-hot), where its gradient is almost zero and training stalls. Dividing by dk\sqrt{d_k} restores unit variance and keeps the softmax in its responsive region.

Go deeper
  • Measured at d_k=1024: largest weight 0.97 unscaled against 0.32 scaled; gradient norm 0.007 against 0.088.
Follow-up they may askWhat if you divide by dkd_k instead of its root?

6.4Variants and costs

Interview 7What is the difference between self-attention and cross-attention, and where is each used?
Week 9Very likelyNotes 2From the lecture

Same sequence or two sequences.

Prints
SELF-attention  (Q, K, V all from the source)  : output (1, 6, 8)  weights (1, 6, 6) = (batch, 6 queries, 6 keys)
CROSS-attention (Q from decoder; K, V from the encoder): output (1, 4, 8)  weights (1, 4, 6) = (batch, 4 queries, 6 keys)
so in cross-attention each of the 4 decoder positions chooses among the 6 encoder positions; the output has the decoder's length
Answer
Say this first (30 seconds)

Self-attention: Q,K,VQ,K,V all come from the same sequence, so each token gathers context from the other tokens of its own sentence (e.g. "it" attends to "animal"). Used in the Transformer encoder and, with a causal mask, in the decoder. Cross-attention: QQ comes from the decoder, K,VK,V from the encoder output, so each output token looks at the input; the attention matrix is ndec×nencn_{dec}\times n_{enc}. Used in the decoder of encoder-decoder models.

Go deeper
  • Shapes: self 6x6, cross 4x6; the output has the length of the queries.
Follow-up they may askIs cross-attention present in GPT?
Interview 8Why does the Transformer decoder use masked self-attention, and how is the mask applied?
Week 9Very likelyNotes 2From the lecture

No peeking at the future.

Prints
causal attention weights (row = the position that is looking, column = what it looks at):
[[1.   0.   0.   0.   0.  ]
 [0.05 0.95 0.   0.   0.  ]
 [0.13 0.25 0.63 0.   0.  ]
 [0.01 0.01 0.09 0.89 0.  ]
 [0.01 0.01 0.11 0.3  0.58]]
rows still sum to 1: [1.0, 0.9999999403953552, 1.0, 1.0, 0.9999998807907104]

with the causal mask the outputs at positions 0, 1, 2 do not change when tokens 3, 4 change: True
without the mask they do change (the model could read the answer): True
Answer
Say this first (30 seconds)

When generating, token tt must not depend on later tokens, but in training the whole target is fed at once, so without a mask the model could copy the answer. The causal mask sets the scores of all future positions to −∞-\infty before the softmax, so their weights are exactly 0 and the rest renormalise to 1. This makes training match the left-to-right generation at inference. It is causal, not just for padding; a padding mask is separate.

Go deeper
  • Verified: with the mask, changing tokens 3-4 left outputs 0-2 unchanged; without it they changed.
Follow-up they may askIs the encoder's self-attention masked?
Interview 9Write the multi-head attention formula. Why several heads, and how many parameters does it have?
Week 9Very likelyNotes 2, 3From the lecture

Concatenate h small attentions, then mix.

Prints
hand-written multi-head attention matches nn.MultiheadAttention: True
d_model=64, heads=1: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 64)
d_model=64, heads=2: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 32)
d_model=64, heads=4: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 16)
d_model=64, heads=8: parameters 16640 = 4*64*64 + 4*64 = 16640   (head size 8)
more heads do NOT add parameters: they split the same d_model into smaller subspaces
Answer
Say this first (30 seconds)

headi=Attention(QWiQ,KWiK,VWiV)\mathrm{head}_i=\mathrm{Attention}(QW_i^Q,KW_i^K,VW_i^V), MultiHead=Concat(head1..headh)WO\mathrm{MultiHead}=\mathrm{Concat}(\mathrm{head}_1..\mathrm{head}_h)W^O, each head of width d/hd/h. Several heads let the model attend to different relations (position, syntax, reference) in different subspaces at once. The heads are concatenated, not averaged. Parameters: four d×dd\times d matrices, 4d24d^2 (plus biases), independent of the number of heads.

Go deeper
  • Verified by hand against nn.MultiheadAttention; d=64 has 16,640 parameters for 1, 2, 4 and 8 heads.
Follow-up they may askIf d=512 and h=8, what are dkd_k and the shape of WOW^O?
Interview 10Why is positional information needed with self-attention?
Week 9LikelyNotes 2, Ex. 3From the lecture

Attention treats the tokens as a set.

Prints
output for 'bites' in 'dog bites man': [-0.72   0.958  1.767  0.24   1.691  0.924]
output for 'bites' in 'man bites dog': [-0.72   0.958  1.767  0.24   1.691  0.924]
identical without position information: True  (self-attention sees a SET of words, not a sequence)
after adding a position vector to each token: identical? False -> the two sentences are now told apart
Answer
Say this first (30 seconds)

Self-attention is permutation-equivariant: reordering the inputs just reorders the outputs, so "dog bites man" and "man bites dog" give the same output for "bites". Order must be injected by adding a position-dependent vector (positional encoding) to each token embedding. An RNN gets order for free from recurrence; a Transformer must be given it.

Go deeper
  • Measured: identical outputs without positions, different with them.
Follow-up they may askWhat does "equivariant" mean here?
Interview 11What is the time and memory complexity of self-attention in the sequence length? Why does it matter?
Week 9LikelyNotes Ex. 4From the lecture

Quadratic.

Prints
self-attention builds an n x n score matrix per head
      n     score entries    memory (float32)    n^2 * d operations (d = 512)
    512         2.62e+05         1.0 MB        1.34e+08
   2048         4.19e+06        16.8 MB        2.15e+09
   8192         6.71e+07       268.4 MB        3.44e+10
  32768         1.07e+09         4.3 GB        5.50e+11
 131072         1.72e+10        68.7 GB        8.80e+12
doubling the length quadruples the cost; this is why long context is expensive and why efficient attention is an active research area
Answer
Say this first (30 seconds)

The score matrix is n×nn\times n, so time is O(n2d)O(n^2d) and memory O(n2)O(n^2) per head: quadratic in the length. For n=512,d=512n=512,d=512: 5122×512=134512^2\times512=134M operations; n=2048n=2048: 2.1B. Long contexts become expensive (4.3 GB for one head's scores at 32,768 tokens), motivating efficient and sparse attention. RNNs are O(nd2)O(nd^2) but sequential.

Go deeper
  • Computed: 1.0 MB of scores at n=512, 16.8 MB at 2048, 4.3 GB at 32,768.
Follow-up they may askWhy is attention still preferred to RNNs despite the quadratic cost?
Interview 12Can attention weights be read as an explanation of what the model used?
Week 9SometimesNotes -Extra

Useful, but not a proof.

Answer
Say this first (30 seconds)

The weights are a probability distribution showing where the model looked, which makes attention more inspectable than most layers. But a high weight does not prove that position caused the output: the values may carry little, other heads and layers also contribute, and the residual path bypasses attention. Treat them as a hint, not a faithful explanation.

Go deeper
  • Reading attention maps is common in translation visualisations.
Follow-up they may askWhat other methods explain a prediction?
Next · SummaryWeek 9 Summary Sheet
07
Summary

Week 9 Summary Sheet

Week 9 on one page: chapter glance, the formulas, the pairs to tell apart, a shapes table, a checklist and corrections to the source notes.

7.1Week 9 at a glance

C1Embeddings

One-hot: orthogonal. Embedding = lookup table (V x d). Static (Word2Vec, GloVe) has one vector per word; contextual (BERT, GPT) depends on the sentence.

C2Why attention

A single context vector is a bottleneck (copy accuracy 0.30 at length 16); attention lifts it to 0.995 at the same size.

C3Scaled dot-product

softmax(QK⊤/dk)V\mathrm{softmax}(QK^\top/\sqrt{d_k})V. Weights sum to 1; output is a convex combination. Scale: variance of q⋅kq\cdot k is dkd_k.

C4Variants

Self: Q,K,V from one sequence. Cross: Q from decoder, K,V from encoder. Causal mask: future = −∞-\infty. Attention alone cannot see order.

C5Multi-head

Concat of hh heads of width d/hd/h, then WOW^O. Parameters 4d24d^2 for any hh. Cost O(n2d)O(n^2d).

7.2Shapes

ObjectShapeNote
Qnq×dkn_q\times d_kqueries
Knk×dkn_k\times d_ksame width as Q
Vnk×dvn_k\times d_vone per key
scores / weightsnq×nkn_q\times n_ksoftmax per row
outputnq×dvn_q\times d_vlength of the queries
head widthd/hd/h64 for d=512, h=8
MHA parameters4d24d^2+ biases; same for any h

7.3Key terms

C1-C2
embedding
learned dense vector per token.
polysemy
one word, several meanings.
context vector
the single summary of the input.
attention
weighted average of values.
C3-C4
query / key / value
ask / match / content.
scaling
divide by dk\sqrt{d_k}.
causal mask
future blocked.
padding mask
ignore filler.
C5
head
one attention in a subspace.
W^O
mixes the concatenated heads.
quadratic
cost grows as n2n^2.
equivariant
reorder in, reorder out.

7.4The pairs you must tell apart

staticone vector per word
VS
contextualdepends on the sentence
self-attentionQ,K,V same sequence
VS
cross-attentionQ decoder; K,V encoder
causal maskhides the future (autoregressive)
VS
padding maskhides filler tokens
concatenate headscorrect
VS
average headswrong
more headssame parameters
VS
more layersmore parameters
RNNO(nd2)O(nd^2), sequential
VS
attentionO(n2d)O(n^2d), parallel

7.5Last-minute checklist

C1-C2
  • embedding = lookup = one-hot @ W
  • static fails on polysemy
  • bottleneck motivates attention
C3-C4
  • softmax(QK^T / sqrt(dk)) V
  • scale because variance is dk
  • causal mask before the softmax
  • attention needs positional encoding
C5
  • heads concatenated, then W_O
  • parameters 4 d^2 for any h
  • cost quadratic in n

7.6Corrections and additions to the source notes

WhereThe source saysBetter
Transformer parameters"per layer ∼8d2\sim8d^2" (with attention 4d24d^2 and FFN 8d28d^2)the notes' own parts add to 4d2+8d2=12d24d^2+8d^2=12d^2; we verify the count with PyTorch in Week 10
Attention heads"head 1 syntax, head 2 semantics, head 3 position"a motivation; real heads are often mixed or redundant
Word analogies"king - man + woman = queen"a striking property of some embeddings; in the toy count-based embedding queen came first, princess second
RNN bottleneck"compresses everything: information loss"measured: 0.30 at length 16 against 0.995 with attention (hidden 32)
Masking"mask future positions with −∞-\infty"correct and it must happen before the softmax; padding is a separate mask
Attention weightsimplied as explanationsa hint of where the model looked, not a proof of what it used
Revision overview
Embeddingsone-hot vs dense
static vs contextual
polysemy
Why attentioncontext-vector bottleneck
look at all states
0.30 to 0.995
FormulaQ, K, V
softmax(QK^T/sqrt dk) V
why scale
Week 9Attention
Variantsself vs cross
causal mask
order-blind
Multi-headh heads of d/h
concat, W_O
4 d^2 parameters
CostO(n^2 d)
134M at n=512
4.3 GB at 32k
Revision: all of Week 9 on one page
Back to top