Attention Is All You Need
Lesson 3Attention
The core operation: which tokens should attend to which
Attention, query, key, value, weighted sum
Recall from the last lesson: every token is now a vector, and we built
Z = token embedding + positional vector. The transformer's first move is to apply three separate linear maps toZ, producing Q, K, V. This lesson is what happens next, what those three matrices actually do.
Attention is a way to look up information by content, not position. You hold a query (“what am I looking for?”) and you have a collection of keys (labels for what's available) and values (the actual content behind each label). The query is compared against every key, the comparison scores are turned into weights, and the answer is a weighted sum of the values.
This single mechanism, and the variants built from it, replaces the recurrence in RNNs with a parallel, content-addressable read. The rest of the transformer is plumbing around this idea.
Query, current focus
Key, label for matching
Value, content retrieved
The "aha": resolving it
Before the math, see why content-addressing matters. In the sentence below, what does it refer to, the animal or the street? A human knows instantly; a bag-of-positions model cannot. Attention lets the token it send out a query that scores highly against animal, so it pulls meaning from the right place. Click the it row.
"it" distributes its attention across all tokens. Brighter cell = more attention. Click any token to switch.
The it row dumps most of its weight on animal, a little on street (the plausible distractor), and almost nothing elsewhere. That is content-addressing: the connection is by meaning, not by how many words apart the two tokens sit.
A library analogy
Imagine a library where every book has two parts: a catalogue card describing it (the key) and the book itself (the value). You walk in with a search query.
- The librarian compares your query against every catalogue card in parallel.
- Cards that match well get a high score; mismatched cards get a low score.
- The librarian doesn't hand you one book, she hands you a blend of all the books, with each book contributing in proportion to how well its card matched.
That blend is the output of attention. You never asked for book #3; you described what you wanted, and the relevant content surfaced.
Scaled dot-product attention
For a single query vector q and a set of key-value pairs (kⱼ, vⱼ) with :
In matrix form, when Q is a stack of queries (one row per query token):
Three operations, in order:
- Score,
QKᵀproduces an(n_q × n_k)matrix of raw similarity scores. Each entry is a single dot product. - Normalize, divide by
√d_k, then softmax along each query's row to get weights that are positive and sum to 1. - Aggregate, multiply by
Vto get a(n_q × d_v)output: each query's answer is a convex combination of the value vectors.
Worked example
Take d_k = 4 (the width of queries and keys) and d_v = 2 (the width of values), three keys, three values, one query.
q = [1, 0, 1, 0] ← d_k = 4
k_1 = [1, 0, 1, 0] v_1 = [10, 0] (close match to q)
k_2 = [0, 1, 0, 1] v_2 = [ 0, 5] (orthogonal, no match)
k_3 = [1, 0, 0, 0] v_3 = [ 2, 2] (partial match)
└─ d_v = 2Step 1, raw scores. q · k_j:
q · k_1 = 1·1 + 0·0 + 1·1 + 0·0 = 2
q · k_2 = 1·0 + 0·1 + 1·0 + 0·1 = 0
q · k_3 = 1·1 + 0·0 + 1·0 + 0·0 = 1Predict first: which key will get the most weight, and roughly how much?
k_1 scored highest (2), so it wins, but softmax is gentle: k_2 and k_3 are not zeroed out. A common wrong guess is "k_1 gets ~100%". Hold your estimate, then check it against the exact numbers below. (Answer: ≈ 0.51, far from 1.0.)
Step 2, scale by √d_k = 2.
scaled = [2/2, 0/2, 1/2] = [1.0, 0.0, 0.5]Step 3, softmax. exp(1.0)=2.72, exp(0.0)=1.00, exp(0.5)=1.65, sum ≈ 5.37:
α = [2.72/5.37, 1.00/5.37, 1.65/5.37] ≈ [0.506, 0.186, 0.308]The weights are positive, sum to 1, and concentrate mass on k_1 (the best match) without ignoring the others.
Step 4, weighted sum of values.
output = 0.506 · [10, 0] + 0.186 · [0, 5] + 0.308 · [2, 2]
≈ [5.06 + 0 + 0.62, 0 + 0.93 + 0.62]
≈ [5.68, 1.55]The output borrows mostly from v_1, a touch from v_3, and almost nothing from v_2. No discrete choice was made, the answer is differentiable, so gradients flow back into Q, K, V projections during training.
Now stop reading and operate it. The widget below starts at exactly this query; drag the query vector and watch every step recompute. Then use the d_k slider underneath to feel why the √d_k divisor exists.
Run scaled dot-product attention
Drag the query and watch score → scale → softmax → weighted sum recompute end to end. The defaults reproduce the worked example above; change the query to make a different key win.
Your query q
Drag each dimension. The whole pipeline below updates live.
q[0]
1
q[1]
0
q[2]
1
q[3]
0
| key | value v | q·k | ÷√d_k | softmax weight α |
|---|---|---|---|---|
| k₁ [1, 0, 1, 0] | [10, 0] | 2.00 | 1.00 | 0.506 |
| k₂ [0, 1, 0, 1] | [0, 5] | 0.00 | 0.00 | 0.186 |
| k₃ [1, 0, 0, 0] | [2, 2] | 1.00 | 0.50 | 0.307 |
weighted sum of values = output
[5.68, 1.55]
Σ αⱼ vⱼ · weights sum to 1.00 (always 1)
Notice: no key is ever "chosen". Every value contributes in proportion to its weight, which is exactly what makes the whole operation differentiable.
Why divide by √d_k? See it collapse.
Five keys with the same relative match. As d_k grows, raw scores grow with it (variance ≈ d_k), so the unscaled softmax saturates toward one-hot and gradients on the other keys die. The scaled version stays sharp-but-soft.
Unscaled: softmax(QKᵀ)
top weight 73%
Scaled: softmax(QKᵀ / √d_k)
stays balanced at every d_k
Push d_k past ~100 with scaling off: one bar pins to ~100% and the rest flatline. That flatline is a dead gradient. The √d_k divisor is what prevents it.
Your turn: compute one step by hand
New query q = [0, 1, 0, 1], same keys as above. The raw scores are:
q · k_1 = 0 q · k_2 = 2 q · k_3 = 0
Which value vector should now dominate the output, and why? Work it out before expanding.
Reveal answer
k_2 scores highest (2), so after scaling and softmax α ≈ [0.21, 0.58, 0.21] and the output leans toward v_2 = [0, 5]: the weighted sum works out to ≈ [2.54, 3.30] (the second component is now the largest). The query "matches" a different key purely by content, the mechanism never changed. Verify by setting the query to [0, 1, 0, 1] in the widget above.
Why divide by √d_k?
If q and k have entries with mean 0 and variance 1, the dot product q · k = Σᵢ qᵢ kᵢ has variance d_k. As d_k grows, scores grow with it. Large positive scores push softmax toward a one-hot distribution; one weight saturates at ≈ 1 and the rest collapse to ≈ 0, which kills gradients on the suppressed terms.
Dividing by √d_k rescales the variance back to ≈ 1, keeping softmax in a region where it's sharp but not saturated, confident enough to focus, soft enough to learn.
Intuition cheat-sheet
- High
q · k→ large weight on thatv. - Softmax → weights are non-negative and sum to 1 (a convex combination).
- No argmax → fully differentiable; the network can learn what to attend to.
√d_kscale → keeps logits from saturating asd_kgrows.
Next: when Q, K, V are all derived from the same sequence, every token attends to every other token in that sequence. That's self-attention.
Check your understanding
1 / 11The output of attention for a single query is:
Practice it yourself
Build attention one step at a time — score, weight, aggregate. Real Python, hidden tests, no setup.