The problem
Attention lets every token look at the other tokens and take a weighted mix of their content (see the theory page). Each token has a query (what it looks for), a key (what it offers) and a value (what it passes on). You will write attention itself, and the causal mask that stops a token from looking at the tokens after it.
Attention in three steps
where:
- , and stack the queries, keys and values as rows, and is the size of a query or key;
- holds every query-key dot product: entry is how well query matches key . Divided by , these are the scores ;
- the softmax runs along each row, so the weights of one query add up to 1;
- multiplying by gives each query the weighted average of the values.
So:
- scores: query times key, divided by ;
- weights: softmax of each row of scores;
- output: weights times values.
The causal mask
When predicting the token at position , the model may only use positions : the later tokens are what it is trying to predict. The mask is a table of True (allowed) and False (forbidden), with True where the key position is at most the query position : a lower triangle. Before the softmax, every forbidden score is replaced by , so its weight becomes .
Example. Three tokens, . Queries and keys are the same here: , , . Values: , , .
causal_mask(3)is[[T, F, F], [T, T, F], [T, T, T]](T forTrue, F forFalse).- Scores, masked: row 0 is , row 1 is , row 2 is .
- Weights: row 0 is , row 1 is , row 2 is . Each row adds up to 1.
- Output: token 0 gets exactly its own value ; token 1 gets ; token 2 gets about .
Batches and heads
In a real model, q, k and v have extra leading axes, for the texts of the batch and the heads: shape . Attention runs independently for every batch and head, on the last two axes. NumPy's @ already works that way: on arrays with more than two axes, it multiplies the last two axes and treats the others as a batch.
What to implement
causal_mask(T): a boolean array,Truewhere attention is allowed ().attention(q, k, v, mask=None):qhas shape ,khas shape andvhas shape , with the same leading axes.mask, if given, is a boolean array that broadcasts to . Return(out, weights): the output and the weights.
Requirements
- Masked scores become before the softmax, so their weights are exactly 0. Every row of
weightsadds up to 1. - Stable softmax: subtract the row maximum.
- No Python loops.
- You can assume every row has at least one allowed position.
Hints
- To transpose only the last two axes of
k(whatever the leading axes), usenp.swapaxes(k, -1, -2). Plaink.Treverses all the axes. np.where(mask, scores, -np.inf)applies the mask;np.trilbuilds a lower triangle.