Self-attention is the beating heart of the transformer. This section builds it from first principles: the idea (a context-dependent weighted average), the concrete recipe (scaled dot-product attention), the PyTorch code, and an interactive lab you can poke at.
The idea: a context-dependent weighted average
Self-attention self-attention Attention where the queries, keys, and values all come from the same sequence; each output embedding is a context-dependent weighted average of the sequence. defined in ch. 3 — open in glossary lets each token’s representation depend on the whole sequence instead of being fixed. Given token embeddings , it produces new embeddings where each is a weighted combination of all the inputs:
Reading it term by term:
- — the updated embedding for token (same dimension as ).
- — the original embedding of token (one term per token in the sequence).
- — the attention weight attention weights Normalized coefficients w_ji (each row sums to 1) that say how much each token contributes to another token's updated embedding. defined in ch. 3 — open in glossary : how much token contributes to token ‘s new representation.
- — the weights over form a proper average (they’re non-negative and sum to one).
Intuition — “a fancy form of averaging”
A word’s meaning is coloured by its neighbours. When you read “time flies like an arrow”, the word flies is a verb; in “fruit flies like a banana” it’s a noun. Self-attention builds a representation of “flies” by mixing in the other tokens — heavily weighting “time” and “arrow” in the first sentence, “fruit” and “banana” in the second. The result is a contextualized embedding contextualized embedding A token representation that incorporates information from its surrounding context, so homonyms like 'flies' get different vectors in different sentences. defined in ch. 3 — open in glossary : same word, different vector, because different context.Worked example. Suppose “flies” attends with weights over [time, flies, like, an, arrow]. Then its updated embedding is
— mostly “arrow” with a dash of its own identity. Those weights aren’t invented; the rest of this section is about how to compute them.
Scaled dot-product attention
There are several ways to compute those weights; the standard one — from Attention Is All You Need — is scaled dot-product attention scaled dot-product attention The standard self-attention recipe: score queries against keys with a dot product, scale by 1/√d_k, softmax to get weights, then take the weighted sum of values. defined in ch. 3 — open in glossary , in four steps:
- Project each embedding into three vectors: a query query The learned projection of a token embedding that does the 'asking' — matched against every key to decide how much to attend. defined in ch. 3 — open in glossary , a key key The learned projection of a token embedding that is 'matched against' by queries via a similarity (dot product). defined in ch. 3 — open in glossary , and a value value The learned projection of a token embedding that carries the content mixed into the output according to the attention weights. defined in ch. 3 — open in glossary .
- Score every query against every key with a dot product (the similarity function). For tokens this yields an matrix of attention scores attention scores The raw n×n dot products of queries with keys, before scaling and softmax turn them into attention weights. defined in ch. 3 — open in glossary .
- Scale and softmax the scores into weights that sum to 1.
- Weighted-sum the value vectors using those weights.
Step 2 — score with the dot product
The similarity between query and key is their dot product. Stacking all queries into a matrix and all keys into , every pairwise score is a single matrix multiply:
- — the queries, shape (one row per token).
- — the keys, shape ; is its transpose .
- — the score matrix, shape . is how strongly token ‘s query matches token ‘s key.
Intuition — similarity as overlap
A dot product is large when two vectors point the same way and near zero when they’re unrelated. So a query “lights up” the keys it resembles. In our toy numbers, the query for flies is aligned with the key for arrow, so is the biggest score in that row.Worked example. With and , the score is . Against it is . “flies” strongly prefers “arrow”.
Step 3 — scale, then softmax
Raw dot products can get large, which pushes the softmax into a region where its gradients vanish. So we divide by first, then normalize:
- — the key/query dimension. Dividing by keeps the variance of the scores around 1 regardless of dimension.
- — turns a row of real scores into positive weights that sum to 1.
- — token ‘s attention distribution over all keys.
Intuition — why divide by √dₖ?
If the entries of and have unit variance, their dot product over dimensions has variance — it grows with the vector size. Left unchecked, one score dwarfs the others and softmax becomes a hard argmax with almost no gradient. Dividing by rescales it back to variance ≈ 1, keeping the softmax soft and trainable.Worked example. The “flies” scores scaled by become . Softmax gives — exactly the weights from the averaging example above. Without the scaling, the raw would softmax to a spikier .
Step 4 — weighted sum of values
Finally, mix the value vectors with those weights. In matrix form the whole mechanism is one tidy expression:
- The softmax term is the weight matrix .
- is the values, shape .
- is the output, shape : row is .
Intuition — retrieve, softly
The supermarket analogy: your recipe’s ingredients are queries; each shelf label is a key; the product is a value. Instead of grabbing the single best match, self-attention takes every value in proportion to how well its key matches the query. “A dozen eggs” might return 10 eggs, an omelette, and a chicken wing.Try it: the attention lab
Everything above happens live below. Pick a query token and step through the four operations. Watch flies collapse onto arrow at the softmax step.
The output is the value vectors averaged by those weights.
| key token j | k(j) | q·k | ÷√4 | weight wⱼ | v(j) |
|---|---|---|---|---|---|
| time | 0 | 0 | 0.009 | ||
| flies | 6.00 | 3.00 | 0.178 | ||
| like | 0 | 0 | 0.009 | ||
| an | 0 | 0 | 0.009 | ||
| arrow | 9.00 | 4.50 | 0.796 |
weights sum to 1.00. “flies” attends most to arrow.
And here is the whole weight matrix at once — every row is one token’s attention distribution. Notice function words (“like”, “an”) spread their attention, while “flies” concentrates on “arrow”.
| key (attended to) → | |||||
| time | flies | like | an | arrow | |
| query↓time | .65 | .09 | .09 | .09 | .09 |
| flies | .18 | .80 | |||
| like | .06 | .06 | .42 | .42 | .06 |
| an | .06 | .06 | .42 | .42 | .06 |
| arrow | .10 | .27 | .10 | .10 | .44 |
Each row is one query token's attention distribution over all keys — it sums to 1. Darker = more attention. Hover a cell for the exact weight.
Implementing it in PyTorch
First tokenize the text into input ids (excluding the [CLS]/[SEP] special
tokens for simplicity):
inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False)
inputs.input_ids
# tensor([[ 2051, 10029, 2066, 2019, 8612]])
Then look up a dense embedding for each id with an nn.Embedding layer:
from torch import nn
from transformers import AutoConfig
config = AutoConfig.from_pretrained(model_ckpt)
token_emb = nn.Embedding(config.vocab_size, config.hidden_size) # Embedding(30522, 768)
inputs_embeds = token_emb(inputs.input_ids)
inputs_embeds.size()
# torch.Size([1, 5, 768]) -> [batch_size, seq_len, hidden_dim]
Now score, scale, softmax, and mix. Keeping query = key = value = inputs_embeds
for simplicity:
import torch
from math import sqrt
import torch.nn.functional as F
query = key = value = inputs_embeds
dim_k = key.size(-1) # 768
scores = torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)
scores.size() # torch.Size([1, 5, 5])
weights = F.softmax(scores, dim=-1)
weights.sum(dim=-1) # tensor([[1., 1., 1., 1., 1.]])
attn_outputs = torch.bmm(weights, value)
attn_outputs.shape # torch.Size([1, 5, 768])
That’s the entire mechanism — two matrix multiplies and a softmax. Wrapped up as a reusable function:
def scaled_dot_product_attention(query, key, value):
dim_k = query.size(-1)
scores = torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)
weights = F.softmax(scores, dim=-1)
return torch.bmm(weights, value)
Follow the shapes end to end:
Click an operation to see what it does. A highlighted dimension is one that just changed.
Note — PyTorch ↔ TensorFlow
We use PyTorch, but the steps are analogous in TensorFlow/Keras. The classes used in this chapter:nn.Linear ↔ keras.layers.Dense,
nn.LayerNorm ↔ keras.layers.LayerNormalization,
nn.Embedding ↔ keras.layers.Embedding,
nn.GELU ↔ keras.activations.gelu,
torch.bmm ↔ tf.matmul (batched matmul).Why three separate vectors?
With query = key = value set equal, a token’s query matches itself
perfectly — the dot product of a vector with itself is large — so every token
attends mostly to its own position. But a word’s meaning is usually better
informed by its neighbours than by a second copy of itself: “flies” is
clarified by “time” and “arrow”, not by “flies” again.
The fix is to give the layer freedom: apply three different learned linear projections to each embedding, producing distinct query, key, and value vectors that can specialize. Stacking several such projections in parallel gives multi-headed attention — the subject of the next section.
Check yourself: scaled dot-product attention
1.In self-attention, what is each updated embedding x'ᵢ?
2.Why is the dot product a sensible similarity function for query and key vectors?
3.Why are the attention scores divided by √dₖ before the softmax?
4.Predict the shape: with inputs_embeds of shape [1, 5, 768], what is the shape of `scores = bmm(Q, Kᵀ)/√dₖ`?
5.In the AttentionLab, the query “flies” ends up attending ~0.80 to “arrow”. At which step does that concentration appear?
6.The book first sets query = key = value = the embeddings. What problem does that cause, and how is it fixed?