Dot Products as Similarity Scores
Attention, at its core, answers one question over and over: when processing this word, how much should I pay attention to that other word? The answer is a number. That number comes from a dot product. Everything else in the attention formula — the softmax, the value vectors, the scaling factor — is plumbing around this one operation.
So it's worth getting the dot product into your fingers, not just your notes.
1. The mechanic
Given two vectors of the same length, multiply them element-wise and add up the results.
For and :
3 × 4 = 12
4 × 3 = 12
----
24
That's it. In Python, without NumPy:
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
dot([3, 4], [4, 3]) # 24
Two vectors in → one scalar out. Note that the output is a single number no matter how long the vectors are. A 768-dimensional dot product still collapses to one score. That's exactly what you want for "how relevant is this token to that one."
2. Why the number means "similar direction"
The definition above is the computational one. There's an equivalent geometric one:
where is the length of (i.e. , Pythagoras generalized) and is the angle between the two vectors.
You don't need to derive this, but you do need to trust it, so here's the intuition. Imagine shining a light straight down onto and seeing where 's shadow falls. That shadow — the projection of onto — has length . The dot product is that shadow length, scaled by how long is.
- If and point the same way, , , the shadow is as long as itself. Dot product is maximal.
- If they're perpendicular, , , casts no shadow at all. Dot product is zero.
- If they point opposite ways, , . Dot product is maximally negative.
Check that against , :
3 × (-4) = -12
4 × 3 = 12
----
0
Zero — and indeed those two vectors are at right angles (rotate by 90° and you get ).
3. Where normalization comes in
The geometric formula has a nuisance in it: . A long vector produces big dot products with everything, regardless of direction. If you're ranking candidates by similarity, a merely-adjacent-but-very-long vector can outscore a perfectly-aligned-but-short one.
Normalizing removes that confound. To normalize, divide a vector by its own length:
has length , so . Check: . ✓
Now, if both vectors have length 1, the geometric formula collapses:
The dot product of two normalized vectors is the cosine of the angle between them. It lives in , and it depends on nothing but direction. That's the sentence to say to your colleague:
A bigger dot product between unit vectors means a smaller angle between them — the length is factored out, so the only thing left that the number can be reporting is alignment.
This quantity has a name you'll see everywhere: cosine similarity.
| | Angle | Reading | |---|---|---| | 1.0 | 0° | identical direction | | ~0.9 | ~25° | strongly related | | 0.0 | 90° | unrelated / orthogonal | | −1.0 | 180° | opposite |
4. Worked example: a toy attention score
Forget learned embeddings for a moment. Suppose we hand-build a 4-dimensional space where the axes mean:
[ animal-ness, machine-ness, edible-ness, emotion-ness ]
Here are five word vectors, all chosen with length exactly 5 to keep the arithmetic clean:
| word | vector | length | |---|---|---| | puppy (our query) | | | | kitten | | | | engine | | | | steak | | | | algorithm | | |
Compute puppy · each key by hand:
puppy · kitten
4×3 = 12
0×0 = 0
0×0 = 0
3×4 = 12
---
24
puppy · engine
4×0 = 0
0×5 = 0
0×0 = 0
3×0 = 0
---
0
puppy · steak
4×3 = 12
0×0 = 0
0×4 = 0
3×0 = 0
---
12
puppy · algorithm
4×0 = 0
0×3 = 0
0×0 = 0
3×(-4) = -12
----
-12
Since every vector here has length 5, the cosine similarity is just the dot product divided by :
| key | dot | cosine | angle | |---|---|---|---| | kitten | 24 | 0.96 | ≈ 16° | | steak | 12 | 0.48 | ≈ 61° | | engine | 0 | 0.00 | 90° | | algorithm | −12 | −0.48 | ≈ 119° |
The ranking matches intuition: kitten shares both animal-ness and emotion-ness with puppy, so it's nearly aligned. Steak shares animal-ness only — related, but at a wide angle. Engine shares nothing; it lives on an axis puppy has no component along, so it's exactly orthogonal. Algorithm is anti-aligned on the emotion axis, which drags the score below zero.
This is, structurally, one row of an attention score matrix. A real transformer computes puppy's query vector and every other token's key vector, dot-products the query against all the keys, and gets exactly this kind of ranked list of numbers — which a softmax then turns into weights that sum to 1.
Verify the arithmetic:
import numpy as np
puppy = np.array([4, 0, 0, 3])
keys = {
"kitten": np.array([3, 0, 0, 4]),
"engine": np.array([0, 5, 0, 0]),
"steak": np.array([3, 0, 4, 0]),
"algorithm": np.array([0, 3, 0, -4]),
}
for name, k in keys.items():
raw = puppy @ k
cos = raw / (np.linalg.norm(puppy) * np.linalg.norm(k))
print(f"{name:10s} dot={raw:4d} cos={cos:+.2f} angle={np.degrees(np.arccos(cos)):.0f}°")
5. One honest caveat to keep in your pocket
Transformers do not normalize queries and keys before taking the dot product. They compute raw , then divide by (the square root of the vector dimension). So the scores aren't strictly cosines — magnitude does contribute.
That's fine, and we'll unpack the later in the week. The reason to learn the normalized case first is that it isolates the why: it proves the dot product is fundamentally an alignment measure, with magnitude riding along as a separate, optional signal. If a colleague asks "why does multiplying and summing tell you anything about meaning?", the unit-vector story is the answer.
Try it yourself
Using the same 4-axis space ([animal, machine, edible, emotion]), take the query vector
and these three keys:
- Compute all three dot products by hand, showing the element-wise products.
- Compute and each , then convert each dot product to a cosine similarity.
- Notice that drone gets a lower raw dot product than one of the others but a competitive cosine — or vice versa. Which key benefits most from not normalizing, and why?
- Write two sentences you'd actually say out loud to a colleague explaining why a large dot product between unit vectors means "similar direction." Don't use the word "cosine" in the first sentence.
Then check yourself with NumPy.