Kiru Lab / Language Models / Anatomy of a Transformer
Self-Attention, Built From Scratch
Queries, keys, values — a differentiable lookup table. You already know every operation involved.
Hands-On Lab · about 50 minutes
Each position produces three vectors from its current representation: a query (what am I looking for?), a key (what do I offer?), and a value (what will I pass along if selected?). Every query is compared against every key by dot product — the alignment measure from track two — and the resulting scores, softmaxed into weights, determine a weighted sum of values.
import numpy as np
def softmax(x, axis=-1):
x = x - x.max(axis=axis, keepdims=True)
e = np.exp(x)
return e / e.sum(axis=axis, keepdims=True)
def attention(X, Wq, Wk, Wv, causal=True):
Q, K, V = X @ Wq, X @ Wk, X @ Wv # each (seq, d_k)
scores = Q @ K.T / np.sqrt(Q.shape[-1]) # (seq, seq)
if causal:
mask = np.triu(np.ones_like(scores), k=1).astype(bool)
scores[mask] = -np.inf # a position may not read the future
weights = softmax(scores) # (seq, seq), rows sum to 1
return weights @ V, weightsThree details that matter
- The sqrt(d_k) division keeps dot products from growing with dimension and driving the softmax into saturation, where gradients vanish.
- The causal mask is what makes the model autoregressive. Remove it and you have a bidirectional encoder instead.
- Multiple heads run this in parallel with different projections, so different heads can specialize — one tracking syntax, another tracking a repeated name.
Attention weights are widely treated as explanations. They are not. A weight tells you where information was read from, not what was computed with it or whether it mattered to the output. The interpretability track replaces this intuition with causal tests that can actually distinguish the two.
Hold on to
- Attention is a differentiable, content-addressed lookup
- The causal mask is what makes generation autoregressive
- Attention weights show routing, not causation
Work through
Try each one before opening the solution. Getting it wrong first is most of where the learning happens.
-
Implement `attention` and verify each row of the weight matrix sums to 1 and respects the mask.
Hint
Softmax rows sum to 1; the causal mask should leave zeros strictly above the diagonal.
Solution
Each row sums to 1 because softmax normalizes across the key axis. Above the diagonal every entry is exactly 0, because -inf maps to exp(-inf) = 0 — that is the mask working. Row i therefore distributes all its weight across positions 0 through i, which is precisely what "may not read the future" means mechanically.
import numpy as np rng = np.random.default_rng(0) X = rng.normal(size=(6, 32)) Wq, Wk, Wv = (rng.normal(size=(32, 32)) for _ in range(3)) out, weights = attention(X, Wq, Wk, Wv, causal=True) assert np.allclose(weights.sum(axis=-1), 1.0) assert np.allclose(np.triu(weights, k=1), 0.0)Check your work
Paste this after your own code. If it runs without raising, you have it.
import numpy as np assert np.allclose(weights.sum(axis=-1), 1.0) assert np.allclose(np.triu(weights, k=1), 0.0) assert out.shape == (6, 32) print("ok") -
Run it on a sentence with a repeated noun and visualize the weight matrix. Describe what the pattern does and does not tell you.
Hint
Look at the row for the second occurrence of the repeated noun.
Solution
You typically see the second occurrence attending strongly to the first, and most positions attending heavily to the first token — a well-documented "attention sink" where models park unused attention mass. What the map does tell you: where information was read from. What it does not tell you: what was computed with it, whether it affected the output, or whether an ablation would change anything. Distinguishing those two lists is the entire point of the exercise.
In the Bio Mirror
Attention NetworksSign in to track your progress through the lab.