All articles

Attention, step by step: how a transformer decides what to look at

Queries, keys and values sound abstract until you follow a few tokens through the math by hand.

LLMsIntermediate3 min read
A white robotic hand touching a glowing AI symbol on a circuit board.

Every token in a transformer asks the same question: which other tokens should I pay attention to right now? Self-attention answers it with three learned projections and one softmax. Once you have followed the numbers through once, the rest of the architecture becomes much easier to read.

Queries, keys and values

Each token starts as an embedding vector xix_i. The layer multiplies it by three learned weight matrices to produce three new vectors:

  • a query qi=xiWQq_i = x_i W_Q, describing what this token is looking for,
  • a key ki=xiWKk_i = x_i W_K, describing what this token offers to others,
  • a value vi=xiWVv_i = x_i W_V, the information it passes along if chosen.

A useful analogy is a library search. The query is what you typed, the keys are the catalogue entries, and the values are the books themselves.

Scoring every pair of tokens

To decide how much token ii should attend to token jj, take the dot product of query ii with key jj. Doing that for every pair gives a square matrix of scores. Stack the queries and keys as matrices QQ and KK and the whole computation becomes one formula:

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V

The softmax turns each row of scores into weights that are positive and sum to 1. Each output vector is then a weighted average of all the value vectors.

Here is the whole thing in a few lines of NumPy:

import numpy as np

def attention(Q, K, V):
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)
    scores -= scores.max(axis=-1, keepdims=True)  # numerical stability
    weights = np.exp(scores)
    weights /= weights.sum(axis=-1, keepdims=True)
    return weights @ V, weights

rng = np.random.default_rng(0)
Q, K, V = (rng.normal(size=(4, 8)) for _ in range(3))
out, w = attention(Q, K, V)
print(w.round(2))  # each row sums to 1

Why divide by the square root of d_k

If the entries of qq and kk are roughly independent with variance 1, their dot product has variance dkd_k. With dk=64d_k = 64, typical scores are around ±8. Softmax on numbers that large puts almost all the weight on one token, and its gradients become tiny, so learning stalls.

Dividing by dk\sqrt{d_k} brings the variance back to about 1. Attention stays spread out early in training, and the model can still learn to sharpen it where that helps.

In decoder models such as GPT, a causal mask sets the scores for future positions to −∞-\infty before the softmax. Each token can then attend only to itself and earlier tokens, which is what lets the model generate text one token at a time.

Many heads, many questions

A single attention layer can only compute one weighted average per token. Multi-head attention runs several smaller attention operations in parallel, each with its own WQW_Q, WKW_K and WVW_V. One head might learn to link pronouns to the nouns they refer to while another tracks matching brackets.

The heads’ outputs are concatenated and multiplied by one more matrix, WOW_O, to return to the model’s dimension. The original transformer used 8 heads of size 64 for a model dimension of 512.

  • Vaswani et al., Attention Is All You Need (2017), the paper that introduced the transformer.
  • Our article on tokenizers, which explains what the “tokens” in this post actually are.

Written by Thahira S

Tech futurist exploring how AI and machine learning can make everyday life healthier, more creative and more human. More about AIERA