Compute scaled dot-product attention
Attention lets each token collect information from the other tokens, taking more from the ones that matter to it. Every token plays three parts. Its query says what it is looking for. Its key says what it has to offer. Its value is what it hands over. A token compares its query with every key, turns the match scores into shares that add up to 1, and takes that share of each value. In a GPT a token may only look at itself and the tokens before it.
Learn it
Words to know
- attention
- The step in which each token gathers information from other tokens, taking more from those that match what it is looking for.
- query, key, value
- The three roles in attention: a query is what a token looks for, a key is what it can be matched on, a value is what it passes on.
- causal mask
- The rule that a token may attend only to itself and earlier tokens, never to later ones.
- softmax
- The function that turns a list of scores into probabilities: all positive, adding up to 1.
How it works
- Score: the dot product of one query with each key, divided by
sqrt(d), wheredis the width of a query: how many numbers it holds. - Weights: the softmax of those scores. They are positive and add up to 1.
- Output for that query: each value row times its weight, added together.
- Causal: for query number
i, use only keys and values 0 toi. Drop the later ones before the softmax, so the remaining weights still add up to 1.
Worked example
Input Q = [[1, 0], [0, 1]] K = [[1, 0], [0, 1]] V = [[10, 0], [0, 10]] causal = False Output [[6.69762, 3.30238], [3.30238, 6.69762]]
| Query | Scores | Weights | Output |
|---|---|---|---|
| [1, 0] | 0.7071, 0 | 0.6698, 0.3302 | [6.698, 3.302] |
| [0, 1] | 0, 0.7071 | 0.3302, 0.6698 | [3.302, 6.698] |
| key 0 | key 1 | key 2 | key 3 | |
|---|---|---|---|---|
| query 0 | yes | - | - | - |
| query 1 | yes | yes | - | - |
| query 2 | yes | yes | yes | - |
| query 3 | yes | yes | yes | yes |
- may use
- hidden: a later token
Each query matches its own key best and takes about two thirds of that value. With the causal mask, the first query could see only key 0 and its output would be [10, 0].
Why it matters
This one calculation is the centre of every transformer. The next two lessons run several copies of it side by side and wrap it into a block.
Your turn
Compute scaled dot-product attention, with an optional causal mask.
Q- the queries: one row per token
K- the keys: one row per token, the same width as the queries
V- the values: one row per token
causal- when true, query
imay use only keys and values 0 toi - Returns
- One output row per query: the value rows mixed with weights
softmax(scores), where each score isdot(query, key) / sqrt(d)anddis the width of a query.
Example 1
Input Q = [[1, 0], [0, 1]] K = [[1, 0], [0, 1]] V = [[10, 0], [0, 10]] causal = False Output [[6.69762, 3.30238], [3.30238, 6.69762]]
Each query matches its own key best, with weights 0.67 and 0.33, so it takes most of its own value.
Example 2
Input Q = [[1, 0], [0, 1]] K = [[1, 0], [0, 1]] V = [[10, 0], [0, 10]] causal = True Output [[10, 0], [3.30238, 6.69762]]
The same input with the causal mask. Query 0 can see only key 0, so it takes all of value 0. Query 1 can see both, as before.
Example 3
Input Q = [[0], [0]] K = [[0], [0]] V = [[2], [6]] causal = False Output [[4], [4]]
All scores are 0, so the weights are equal and each output is the average of 2 and 6.
Example 4
Input Q = [[0], [0]] K = [[0], [0]] V = [[2], [6]] causal = True Output [[2], [4]]
Causal again: query 0 sees only the value 2. Query 1 sees both and averages them to 4.
Example 5
Input Q = [[2, 0, 0, 0], [0, 0, 0, 0]] K = [[1, 0, 0, 0], [0, 0, 0, 0]] V = [[8], [0]] causal = False Output [[5.84847], [4]]
Each query is 4 wide, so the dot product 2 is divided by sqrt(4), not by the square root of the 2 tokens. The weights are 0.73 and 0.27, and the second query averages 8 and 0.
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Where to start
Handle one query at a time. For each one you need a list of scores, then a list of weights, then one output row.
Hint 2: The key step
For query
i, decide how many keys it may use:i + 1whencausalis true, all of them otherwise. Compute scores for only those keys, then apply softmax as in chapter 1.Hint 3: The outline
For each query: take
countkeys; score each asdot(q, k) / math.sqrt(len(q)); subtract the largest score and raiseeto each; divide by the total to get weights. Each output column is the sum ofweight * V[j][column]over the allowed rows.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Step by step
- A causal query sees only keys 0 to i. Leaving the later keys out before the softmax means the weights of the remaining keys still add up to 1.
- Score each visible key against the query. A large dot product means the query and the key point the same way. Dividing by
sqrt(d)keeps scores from growing as the vectors get wider. - Softmax turns the scores into weights that add up to 1. The same stable softmax as in chapter 1: subtract the largest score, raise
eto each, divide by the total. - Mix the value rows with those weights. Each output number is a weighted average of one column of the values, over the rows this query could see.
The same numbered steps are the comments in the solution.
Python
import math
def solve(Q, K, V, causal):
output = []
for i, q in enumerate(Q):
# 1. A causal query sees only keys 0 to i
count = i + 1 if causal else len(K)
# 2. Score each visible key against the query
scores = []
for k in K[:count]:
dot = sum(a * b for a, b in zip(q, k))
scores.append(dot / math.sqrt(len(q)))
# 3. Softmax turns the scores into weights that add up to 1
top = max(scores)
exps = [math.exp(score - top) for score in scores]
total = sum(exps)
weights = [e / total for e in exps]
# 4. Mix the value rows with those weights
row = []
for column in range(len(V[0])):
row.append(sum(w * V[j][column] for j, w in enumerate(weights)))
output.append(row)
return outputWatch out
- Apply the mask before the softmax. Setting weights to 0 afterwards leaves the rest adding up to less than 1.
- Divide by the square root of the query width,
len(q), not by the number of tokens.
Check yourself
What do the query, the key and the value each do?
The query says what a token is looking for, the key says what a token offers to be matched on, and the value is what it hands over.
Why must the causal mask be applied before the softmax?
So the weights of the visible tokens add up to 1. Removing weights afterwards would leave a sum below 1.