How each token decides which tokens to attend to
On this page
Attention computes a new vector for every token as a weighted average of all the tokens' vectors. The weights say how much each token attends to, or uses, each other token. Training learns how to set those weights. Several copies of attention, called heads, run side by side in one layer, each with its own learned weights. Attention runs in every layer on every forward pass, over all the tokens at once in training.
Older sequence models like RNNs (recurrent neural networks) read one token at a time, a token being a word or part of a word, so they cannot run in parallel. If the word that decides the meaning showed up 50 words ago, an RNN carries it through 50 sequential steps. Every step is a chance to forget.
CNNs (convolutional neural networks, which slide a small learned filter across the input) see only a local neighborhood, so many layers must stack before one end of a sentence can reach the other.
In "the cat sat on the mat because it was tired", attention lets "it" look straight at "cat" in one step. That is self-attention.
In "the cat sat on the mat", when the model processes "sat", which tokens should it attend to? Probably "cat" more than "the". Attention is the model learning those attention weights by itself. Each token gets a query, a key and a value; stacked over tokens they form the matrices Q, K and V. The query is what the token looks for, a key is what a token offers to be matched on, and a value is what it hands over when it is picked, like search text, page titles and the pages themselves.
All three come from the input embedding matrix X: one row per token, each row a learned vector of 512 numbers. One learned weight matrix per role turns X into queries, keys and values. Queries and keys need the same length, because each query is multiplied entry by entry with each key and summed (the dot product of Section 3). With 6 tokens and a query length of 64:
Attention turns each token's query into one output vector in three moves: score it against every key, turn the scores into weights, and take the weighted average of the values. The score of a query against a key is their dot product: multiply the two vectors entry by entry and add up. A softmax turns each query's row of scores into weights that are positive and sum to 1. The token's output is the average of the values under those weights.
The attention equation in PyTorch, starting from queries, keys and values that are already projected: six tokens go in, one output row per token comes out, and the three lines inside the function carry the idea.
import torch
def attention(Q, K, V):
d_k = Q.size(-1)
S = Q @ K.transpose(-2, -1) / d_k ** 0.5 # (n, n)
A = S.softmax(dim=-1)
return A @ V # (n, d_v)
n, d_k, d_v = 6, 64, 32
Q, K = torch.randn(n, d_k), torch.randn(n, d_k)
V = torch.randn(n, d_v)
print(attention(Q, K, V).shape) # torch.Size([6, 32])
Before the softmax, the scores are divided by √dk, where dk is the length of each query and key. A dot product adds one term per dimension, so scores grow with dk. Big scores make the softmax nearly one-hot (one weight near 1, the rest near 0), and the gradients, the signals training uses to adjust the weights, become tiny. The paper's authors give this reasoning as a likely cause, not a proof.
If the entries of a query and a key are independent with mean 0 and variance 1 (variance is the average squared distance from the mean), their dot product has variance dk, and dividing by √dk brings it back to 1; the variance snippet after this paragraph checks it on 10,000 random pairs at dk = 64. Look at the two printed variances, about dk before the division and about 1 after.
import torch
NUM_PAIRS, d_k = 10_000, 64
q = torch.randn(NUM_PAIRS, d_k)
k = torch.randn(NUM_PAIRS, d_k)
dots = (q * k).sum(dim=-1)
print(dots.var()) # about 64, which is d_k
print((dots / d_k ** 0.5).var()) # about 1
Real models split text into subword pieces, parts of a word, with 512 numbers per token, and add position information to each vector so the model knows where the token sits; Positional Encoding and Masking explains it.
Figure 6. A toy layer on "the cat sat" with illustrative values; step through to see which token "sat" weights most.
One set of attention weights captures one type of relationship. But "cat" relates to "the" by grammar and to "sat" by meaning. One head can't do both well.
Multi-head attention runs several copies of attention side by side, called heads, each with its own projections into a smaller subspace, a slice of the model's dimensions. The 2017 transformer uses a model width dmodel of 512 and h = 8 heads, so each head works in 64 dimensions.
Each head runs the attention line from Section 3 on its own projections:
Multi-head attention in PyTorch: one projection per role, cut into h heads, then the built-in scaled_dot_product_attention runs the attention equation of Section 3 in every head.
import torch
import torch.nn as nn
import torch.nn.functional as F
B, T, d, h = 2, 6, 512, 8 # batch, tokens, width, heads
d_k = d // h
X = torch.randn(B, T, d)
W_Q, W_K, W_V, W_O = (nn.Linear(d, d, bias=False) for _ in range(4))
def split(M):
# (B, T, d) to (B, h, T, d_k): column block i goes to head i
return M.view(B, T, h, d_k).transpose(1, 2)
Qh, Kh, Vh = split(W_Q(X)), split(W_K(X)), split(W_V(X))
H = F.scaled_dot_product_attention(Qh, Kh, Vh)
H = H.transpose(1, 2).reshape(B, T, d) # concat the heads
out = W_O(H) # (B, T, d)
Because each head scores in its own subspace, two heads can attend to different tokens. In "The animal didn't cross the street because it was too tired", "it" needs coreference, finding which noun a pronoun refers to, and the local clause around it.
Figure 8. With illustrative weights, Head A puts its weight on "animal"; Head B stays on the clause around "it".
More heads do not mean more parameters: with model width d (short for dmodel), each head's slice dk is d divided by the head count h.
More heads, same parameter count
Figure 9. Sixteen 32-dim heads or one 512-dim head: the slices change, the parameter count does not.
Every head on this page takes its queries, keys and values from the same sequence, which makes it self-attention. When the queries come from one stream and the keys and values from another, say text asking about an image, it becomes cross-attention, covered in Cross-Attention.
One self-attention layer on a padded batch, first as PyTorch's module, then by hand with the module's own projection weights and the fused function, so out2 matches out.
import torch
import torch.nn as nn
import torch.nn.functional as F
B, n, d_model, h = 2, 6, 512, 8
x = torch.randn(B, n, d_model)
pad = torch.tensor([[False] * 6, [False] * 4 + [True] * 2]) # True = padding
mha = nn.MultiheadAttention(d_model, h, batch_first=True)
out, _ = mha(x, x, x, key_padding_mask=pad, need_weights=False)
d_k = d_model // h
q, k, v = F.linear(x, mha.in_proj_weight, mha.in_proj_bias).chunk(3, dim=-1)
q, k, v = (t.view(B, n, h, d_k).transpose(1, 2) for t in (q, k, v))
keep = ~pad[:, None, None, :] # True = may attend
heads = F.scaled_dot_product_attention(q, k, v, attn_mask=keep)
out2 = mha.out_proj(heads.transpose(1, 2).reshape(B, n, d_model))
keep is True where attending is allowed.out_proj, eq card in Section 5, lines 3 and 4.| Self-attention | Every layer, every forward pass, over all n tokens at once; nothing is kept between passes; n2 scores per head. |
|---|---|
| Multi-head | All h heads run side by side inside each attention layer, 4d2 weights per layer whatever h is. |
| h | Cuts dmodel into h slices of dk = dmodel/h. More heads give more attention patterns from thinner slices, fewer give fewer wider ones; weights stay 4d2. At 1, one pattern does everything; at dmodel, each head scores with one number. The base Transformer uses 8. |
| √dk | Divides every score (Section 3 explains why). With no divisor a large dk gives one winner per row; a much larger divisor flattens the softmax toward a plain average. The base Transformer uses dk = 64, a divisor of 8, and PyTorch scales by 1/√dk by default. |
| Code | nn.MultiheadAttention, F. (PyTorch docs). |
batch_first defaults to False, meaning (sequence, batch, feature).need_weights=False is needed for the fused path; returned weights are averaged over heads.key_padding_mask, True hides a key.attn_mask, True keeps a key.dropout_p only during training.Attention as built here has no idea which token came first. How does it learn order, and how does a decoder avoid seeing later tokens? Positional Encoding and Masking.