Queries, keys and values
The three projections behind attention.
Create a free account Try part of it here
Why it matters
The last lesson's attention could only link words that were alike. Real attention does more. Each token makes three vectors from its own embedding. Its query says what it's looking for. Its key says what it can be found by. Its value is what it hands over when it's picked. Training learns how to make all three. Most of attention's skill lives here, and these are the Q, K and V you'll see in every transformer diagram and model card.
What you'll be able to do
- Explain the three roles a token plays in attention: query, key and value
- Compute Q, K and V with learned projection matrices
- Show how projections let attention express relations that plain similarity can't
- Connect attention to retrieval as a soft, differentiable lookup
What it covers
- Three jobs for every token
- Making the three vectors
- Scoring and mixing
- Worked example: an adjective finds its noun
- Why separate projections matter
- All tokens at once
- A soft search inside the model
Three jobs for every token
Think about searching a recipe website. You type "quick vegetarian dinner". That's your query: what you want. Each recipe carries a few tags, like "vegan", "20 minutes" or "pasta". Those tags are its key: what it can be found by. When a recipe matches, you don't get its tags back. You get the recipe itself. That's the value: what it hands over.
The three are different things. The tags you match on are not the meal you cook. And what you search for is not how you'd describe yourself.
In attention, every token plays all three parts at once:
| blank | Role | On a recipe site |
|---|---|---|
| Query | what this token is looking for | the words you type in the search box |
| Key | what this token can be found by | each recipe's tags |
| Value | what this token passes along when picked | the recipe you get back |
The last lesson's attention skipped this split. It used each word's embedding for all three jobs. That's like a site where your search words, the tags and the recipe must all be the same text.
Show the answer
The content you get back when a token is matched
Queries are matched against keys. The matched tokens' values are what gets blended into the output.
Making the three vectors
Each token starts as a vector, a list of numbers. Call it . To make the token's query, the model multiplies by a matrix called , the query weights. A matrix is a grid of numbers, as in the matrix multiplication lesson. Keys use a second matrix, . Values use a third one, . The query that comes out is called . The key is . The value is .
In plain words: the same token goes through three different sets of weights. It comes out as three different vectors: one for searching, one for being found, one for handing over.
Multiplying by a learned matrix like this is called a projection. The three matrices are weights, so training tunes them like any others. Every token in the text uses the same three.
Here's a worked example with small numbers. Take the phrase "warm jacket". Give each word two features: is it an adjective, and is it a noun? So "warm" is [1, 0] and "jacket" is [0, 1]. Now pick a query matrix:
W_Q = [[0, 2],
[0, 0]]
Here's how a row of numbers times a matrix works. Each number in says how much of one row of the matrix to take. Then you add up what you took. "warm" is [1, 0], so it takes all of row one and none of row two. Its query is [0, 2]. "jacket" is [0, 1], so it takes row two: [0, 0].
Read the query [0, 2] as "I'm looking for a noun, strongly". A blank query like [0, 0] means "I'm not looking for anything in particular". Here is the same step in Python:
x_warm, x_jacket = [1, 0], [0, 1] # features: [adjective, noun] W_Q = [[0, 2], [0, 0]] def times(x, W): # a row of numbers times a matrix return [x[0] * W[0][c] + x[1] * W[1][c] for c in range(2)] print("warm's query: ", times(x_warm, W_Q)) print("jacket's query:", times(x_jacket, W_Q))
Both lines match the hand calculation. In NumPy, the whole step is x @ W_Q.
x = [0, 1] # a noun W_K = [[1, 0], [0, 3]] print([x[0] * W_K[0][c] + x[1] * W_K[1][c] for c in range(2)])
Show the answer
[0, 3]
x is [0, 1], so it takes none of row one and all of row two. Row two is [0, 3].
Scoring and mixing
Now each token has a query, a key and a value. Attention runs the same three steps as in the last lesson: score, normalize, mix. What changes is which vector goes where.
Two letters keep track of positions. is the token doing the looking. is any token it might look at, itself included. Token 's score for token is written :
In plain words: to score token , take 's query and 's key. Multiply the matching numbers and add them up. That's the dot product. A high score means is what is looking for.
Next, a softmax turns the scores into weights. They're all positive, and they add up to 1. The output for token is a blend of every token's value, mixed by those weights:
The symbol (a Greek capital sigma) means "add up". Here it adds over every token . The little on the softmax means it runs across all of token 's scores together.
In plain words: turn the scores into shares that add up to 100%. Then build the output by taking that share of each token's value. Queries and keys decide who to listen to. Values decide what gets passed on.
Back to "warm jacket". To keep the numbers easy, let and leave vectors unchanged. (In a real model they reshape them too.) So "warm" has key [1, 0] and value [1, 0]. "jacket" has key [0, 1] and value [0, 1].
"warm" has the query [0, 2]. Its two scores are:
- against its own key [1, 0]: 0 × 1 + 2 × 0 = 0;
- against jacket's key [0, 1]: 0 × 0 + 2 × 1 = 2.
The softmax raises , a fixed number about 2.718, to the power of each score. and is about 7.39. The total is 8.39. Divide each by the total to get the weights: 1 ÷ 8.39 ≈ 0.12 and 7.39 ÷ 8.39 ≈ 0.88. The output is 0.12 × [1, 0] plus 0.88 × [0, 1], which is [0.12, 0.88]. The new vector for "warm" is now mostly about the jacket.
"jacket" has a blank query, so both of its scores are 0. Equal scores give equal weights: 0.5 and 0.5.
Think of a library search: your query matches against labels (keys), and you take home a blend of the contents (values).
Worked example: an adjective finds its noun
Here is the same calculation for "warm" in Python. Read it top to bottom: score, normalize, mix.
import math q_warm = [0, 2] # warm's query: "I want a noun" keys = {"warm": [1, 0], "jacket": [0, 1]} values = {"warm": [1, 0], "jacket": [0, 1]} scores = {w: sum(a * b for a, b in zip(q_warm, k)) for w, k in keys.items()} total = sum(math.exp(s) for s in scores.values()) weights = {w: math.exp(s) / total for w, s in scores.items()} out = [sum(weights[w] * values[w][i] for w in values) for i in range(2)] print("scores: ", scores) print("weights:", {w: round(p, 2) for w, p in weights.items()}) print("output: ", [round(o, 2) for o in out])
The scores are 0 and 2. The softmax turns them into 0.12 and 0.88. The output takes 12% of warm's value and 88% of jacket's: [0.12, 0.88]. Now change q_warm to [0, 0] and run it again. The weights become 0.5 each, which is what "jacket" gets with its blank query.
Show the answer
The values
Queries and keys only make the scores. The softmax weights from those scores then blend the values.
Why separate projections matter
The last lesson used plain similarity: queries, keys and values were all the raw embeddings. That has two limits.
- It's symmetric. A's score for B always equals B's score for A. A dot product gives the same answer either way around.
- Tokens favor themselves. When vectors are of similar length, a token usually scores itself highest.
Language isn't symmetric. An adjective looks for the noun it describes. The noun doesn't need the adjective in the same way. Separate query and key projections allow that.
Here's a hand-built example with four words. Each word gets three features: is it an adjective, a noun, or neither?
import numpy as np words = ["warm", "jacket", "for", "winter"] X = np.array([[1.0, 0.0, 0.0], # adjective [0.0, 1.0, 0.0], # noun [0.0, 0.0, 1.0], # neither [0.0, 1.0, 0.0]]) # noun def attention(Q, K): s = Q @ K.T e = np.exp(s - s.max(axis=1, keepdims=True)) return e / e.sum(axis=1, keepdims=True) print("plain similarity (Q = K = X):") print(np.round(attention(X, X), 2)) W_Q = np.array([[0, 3, 0], # an adjective's query asks for "noun" [0, 0, 0], # nouns and other words ask for nothing in particular [0, 0, 0]], dtype=float) W_K = np.eye(3) # keys simply advertise each word's type print("\nwith projections (Q = X W_Q, K = X W_K):") print(np.round(attention(X @ W_Q, X @ W_K), 2))
Each printed table has one row per word, in order: warm, jacket, for, winter. A row shows how that word spreads its attention over the four words, and each row adds up to 1.
With plain similarity, "warm" gives its biggest share, 0.48, to itself. With the projections, "warm" sends 0.48 to each noun, "jacket" and "winter". Only 0.02 is left for itself. The other words have blank queries, so they spread their attention evenly: 0.25 each.
Nobody writes these matrices by hand in a real model. Training finds projections like this, because they help predict the next token.
Go deeper Why plain similarity is symmetric
The dot product ignores order: . So if queries and keys are the same vectors, A's score for B is the same number as B's score for A.
A token also tends to win against itself. If all vectors have the same length, a vector's dot product with itself is the largest it can get with any of them.
Separate matrices break both rules. In the example, warm's query is [0, 3, 0]. Against jacket's key [0, 1, 0], that scores 3. Jacket's query is [0, 0, 0]. Against warm's key [1, 0, 0], that scores 0. Same pair of words, two different scores.
Show the answer
Yes: A's query against B's key is a different score from B's query against A's key
With Q = K = X the scores are symmetric. Separate learned projections break that symmetry, as the warm/jacket example showed.
All tokens at once
A real model handles every token, and several texts, in one go. The projections become linear layers. nn.Linear with bias=False is a learned matrix and nothing else.
(torch here is the course's in-browser teaching subset of PyTorch. The code is the same as real PyTorch. It runs on the CPU only, much more slowly, and its seeded random numbers differ.)
The sizes in the code are:
B = 2: two texts processed together, called a batch;T = 5: five tokens in each text;d_model = 16: the numbers in each token's vector;d_k = 8: the numbers in each query, key and value.
import torch import torch.nn as nn torch.manual_seed(0) B, T, d_model, d_k = 2, 5, 16, 8 # 2 sequences, 5 tokens each, 16 features per token x = torch.randn(B, T, d_model) W_q = nn.Linear(d_model, d_k, bias=False) W_k = nn.Linear(d_model, d_k, bias=False) W_v = nn.Linear(d_model, d_k, bias=False) q, k, v = W_q(x), W_k(x), W_v(x) # each (2, 5, 8) scores = q @ k.transpose(-2, -1) # (2, 5, 8) @ (2, 8, 5) → (2, 5, 5): every query vs every key weights = torch.softmax(scores, dim=-1) # each row sums to 1 out = weights @ v # (2, 5, 5) @ (2, 5, 8) → (2, 5, 8) print("q, k, v:", tuple(q.shape), " scores:", tuple(scores.shape), " out:", tuple(out.shape)) print("rows sum to 1:", torch.allclose(weights.sum(dim=-1), torch.ones(B, T))) print("parameters in the three projections:", 3 * d_model * d_k)
Each projection turns 16 numbers per token into 8. So q, k and v each have the shape (2, 5, 8). k.transpose(-2, -1) turns each text's keys on their side, like .T in NumPy. That lets every query meet every key.
The scores come out as (2, 5, 5). For each text, that's a 5 × 5 grid with one score for every (query token, key token) pair. Its size depends on the number of tokens, not on the vector length. Each projection matrix holds 16 × 8 = 128 weights, so the three hold 384.
Show the answer
(10, 10)
There's one score for every (query token, key token) pair: 10 × 10. The 64 numbers are used up inside each dot product.
A soft search inside the model
This is the retrieval idea from Phase 1's RAG lessons, built into the model. RAG compares a query with an index of documents and returns the top few matches. Attention changes that in two ways.
- It's soft. It doesn't pick the top results. It blends every value, weighted by how well its key matched.
- It's differentiable. Every step is smooth math, so training can use gradients. That's how it tunes what gets searched for and what gets returned.
Still missing
Two details come in the next lessons. First, the scores get divided by , the square root of . This stops the softmax from saturating, which means piling almost all the weight on one token, as the vectors get longer. Second, a mask stops tokens in a language model from attending to the future.
Practice what you just read
- 4 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
Token i's attention score for token j is computed from which two vectors?