Skip to content

Norms, Dot Products, and Similarity

ab
Rotation θ = 45°
a · b = 0.707
cos θ = 0.707
‖a‖ = 1, ‖b‖ = 1.00

The dot product is positive.

Dot product on a fixed −1 to +1 scale

−10+1
A unit vector rotates around a fixed reference. Blue when the dot product is positive, amber at orthogonal, red when negative. That bar underneath is the number attention runs on.

In the last post we talked about vectors: ordered lists of numbers that represent things. A word embedding is a vector. An activation inside a neural network is a vector. A row in a weight matrix is a vector.

To work with those vectors, we need ways to measure and compare them:

  1. How big is this vector? (Its magnitude, or norm.)
  2. How similar are these two vectors? (Some kind of comparison.)

We'll build up from the L2 norm to the dot product to cosine similarity. These operations let us measure activation scale, compute attention scores, and compare embeddings.


How big is a vector?

Let's start simple. You have a vector v=[3,4]\mathbf{v} = [3, 4]. How "big" is it?

The Pythagorean theorem gives the distance. The vector [3,4][3, 4] is a point in 2D space, and its distance from the origin is 32+42=9+16=25=5\sqrt{3^2 + 4^2} = \sqrt{9 + 16} = \sqrt{25} = 5.

That distance-from-the-origin measurement is called the L2 norm (also called the Euclidean norm, also written ∥v∥\|\mathbf{v}\|). The general formula for any number of dimensions is:

∥v∥=v12+v22+…+vn2\|\mathbf{v}\| = \sqrt{v_1^2 + v_2^2 + \ldots + v_n^2}

So for a 768-dimensional word embedding, the L2 norm is just "square every component, add them up, take the square root." That is its distance from the origin.

For activations, the norm measures signal scale. Dividing by that norm is different from layer normalization, which centers and rescales features before a learned affine step.

One special case: a unit vector is any vector with norm equal to 1. You can turn any nonzero vector into a unit vector by dividing it by its norm:

v^=v∥v∥\hat{\mathbf{v}} = \frac{\mathbf{v}}{\|\mathbf{v}\|}

Dividing a nonzero vector by its norm preserves its direction and sets its length to one.

Other ways to measure "big"

A norm is any function that measures "how big" a vector is while obeying a short list of rules (positive, scales cleanly, triangle inequality). The L2 norm is one specific choice. Two other common choices are L1 and L∞.

The L1 norm is the sum of absolute values, ∥v∥1=∑i∣vi∣\|\mathbf{v}\|_1 = \sum_i |v_i|. Think of it as taxicab distance: you can only move along the axes, so the length of the trip is the total number of blocks. L1 is what regularizers like Lasso use, and it's what encourages sparsity in the weights. Under the right optimizer and objective it can drive many of them all the way to zero.

The L∞ norm is the largest absolute component, ∥v∥∞=max⁡i∣vi∣\|\mathbf{v}\|_\infty = \max_i |v_i|. It tells you about the biggest single spike in the vector, regardless of the others. Useful for bounding worst-case behavior, and it's exactly what gradient clipping on a per-element basis is doing.

The cleanest way to see the difference is to look at the unit ball for each norm: the set of all vectors whose norm is at most 1. Its boundary, where the norm equals exactly 1, is the unit sphere. Each norm gives that ball a different shape, and the shape tells you everything about what that norm rewards and punishes.

L1L2L∞(0.70, 0.60)
three ways to measure this vector
L1
1.300
outside
L2
0.922
inside
L∞
0.700
inside
The vertical tick marks the "length 1" line. Drag the point. Each norm has its own idea of what size-1 means, which is why the three unit balls have different shapes.
Drag the point around. The three shapes are the unit spheres for each norm: the boundary of each unit ball, where norm = 1. A point like (0.5, 0.5) sits on L1's diamond (sum = 1), safely inside L2's circle (√0.5 ≈ 0.71), and well inside L∞'s square (max = 0.5). Same vector, three different lengths.

In deep learning you'll use the L2 norm most of the time: attention scores, embedding distances, layer-norm statistics, gradient magnitudes. But when you see a paper talking about "L1 regularization" or a framework clipping gradients "by value" (L∞) versus "by norm" (L2), the difference is exactly which of these pictures applies.


The dot product

Okay, so we can measure how big a single vector is. But the more interesting question is: given two vectors, how related are they? Are they pointing in roughly the same direction? Are they perpendicular? Are they pointing in opposite directions?

The dot product answers this. Given two vectors a\mathbf{a} and b\mathbf{b}, their dot product is:

a⋅b=a1b1+a2b2+…+anbn=∑i=1naibi\mathbf{a} \cdot \mathbf{b} = a_1 b_1 + a_2 b_2 + \ldots + a_n b_n = \sum_{i=1}^n a_i b_i

Multiply corresponding components, then add the products.

Take a=[3,4]\mathbf{a} = [3, 4] and b=[4,1]\mathbf{b} = [4, 1]:

a⋅b=(3)(4)+(4)(1)=12+4=16\mathbf{a} \cdot \mathbf{b} = (3)(4) + (4)(1) = 12 + 4 = 16

The result is a scalar, 16. We can also compute it from the vectors' lengths and the angle between them:

a⋅b=∥a∥ ∥b∥cos⁡θ\mathbf{a} \cdot \mathbf{b} = \|\mathbf{a}\| \, \|\mathbf{b}\| \cos\theta

where θ\theta is the angle between the two vectors.

The dot product is telling you three things at once: how big a\mathbf{a} is, how big b\mathbf{b} is, and how much they're pointing in the same direction (cos⁡θ\cos\theta).

When two vectors point in exactly the same direction, θ=0°\theta = 0° and cos⁡θ=1\cos\theta = 1. For fixed vector lengths, the dot product is at its maximum: ∥a∥∥b∥\|\mathbf{a}\| \|\mathbf{b}\|. When they're perpendicular, θ=90°\theta = 90° and cos⁡θ=0\cos\theta = 0. The dot product is zero. When they point in opposite directions, θ=180°\theta = 180° and cos⁡θ=−1\cos\theta = -1. The dot product is negative.

So the sign and magnitude of the dot product give you a quick read on alignment:

  • Positive: the angle is less than 90°
  • Zero: vectors are orthogonal, meaning this dot-product comparison sees no linear alignment between them.
  • Negative: the angle between nonzero vectors is greater than 90°

Drag either vector and watch both sides of the equation compute themselves in real time. The coordinate formula on the left only knows about a1b1+a2b2a_1 b_1 + a_2 b_2. The geometry formula on the right only knows about magnitudes and angle. They always land on the same number.

θab
From coordinates
a · b = (3.0)(4.0) + (4.0)(1.0)
     = 12.00 + 4.00
     = 16.00
From geometry
|a| · |b| · cos θ
     = 5.00 · 4.12 · 0.776
     (θ ≈ 39.1°)
     = 16.00
Drag either tip. Both formulas always give the same number.a · b is positive
Drag the tips to compare the coordinate and geometric calculations.

Starting position is our running example: a=[3,4]\mathbf{a} = [3, 4], b=[4,1]\mathbf{b} = [4, 1]. The dot product is 16, which is positive but less than the maximum possible value of ∥a∥∥b∥=5×17≈20.6\|\mathbf{a}\| \|\mathbf{b}\| = 5 \times \sqrt{17} \approx 20.6. Drag a\mathbf{a} until it points roughly away from b\mathbf{b} and watch the number flip to negative.

Rotating one unit vector around another makes the dot product vary from +1+1 (aligned) through 00 (perpendicular) to −1-1 (opposite).

ab
Rotation θ = 45°
a · b = 0.707
cos θ = 0.707
‖a‖ = 1, ‖b‖ = 1.00

The dot product is positive.

Dot product on a fixed −1 to +1 scale

−10+1
Drag the slider or hit the preset angles. Because both vectors have length 1, the dot product here is exactly cos θ. Notice how the sign flips at 90° and 270°, and how the whole picture wraps around once per full rotation.

Projection: "how much of a points in b's direction?"

There's another way to think about the dot product that becomes critical in attention mechanisms. You can ask: if I shine a flashlight straight down onto the line defined by b\mathbf{b}, where does the shadow of a\mathbf{a} fall?

That shadow is called the projection of a\mathbf{a} onto b\mathbf{b}. There are actually two different objects here, and it helps to keep the names straight. For nonzero b\mathbf{b}, the signed length of the shadow (the scalar projection) is:

compb(a)=a⋅b∥b∥\text{comp}_{\mathbf{b}}(\mathbf{a}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{b}\|}

And the actual projection vector (the shadow itself, lying along b\mathbf{b}) is:

projb(a)=a⋅bb⋅b b\text{proj}_{\mathbf{b}}(\mathbf{a}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\mathbf{b} \cdot \mathbf{b}} \, \mathbf{b}

Let's compute it for our vectors. We already know a⋅b=16\mathbf{a} \cdot \mathbf{b} = 16 and b⋅b=42+12=17\mathbf{b} \cdot \mathbf{b} = 4^2 + 1^2 = 17. So compb(a)=16/17≈3.88\text{comp}_{\mathbf{b}}(\mathbf{a}) = 16/\sqrt{17} \approx 3.88, and projb(a)=(16/17) [4,1]≈[3.76,0.94]\text{proj}_{\mathbf{b}}(\mathbf{a}) = (16/17)\,[4, 1] \approx [3.76, 0.94].

ba
scalar projection
a·b / |b|
= 3.88
signed distance along b
projection vector
(3.76, 0.94)
purple segment, along b
perpendicular part
|a − proj| = 3.15
the piece of a that b can't see
Drag either tip. Watch the shadow on b's line grow, shrink, and flip sign as a rotates past 90°.
The purple segment is a's shadow on b. Drag a perpendicular to b and the shadow collapses to zero. Drag it past 90° from b and the shadow flips sign. The 'perpendicular part' on the right is everything b can't see about a.

In attention, when a query vector q\mathbf{q} is dotted with a key vector k\mathbf{k}, it's essentially asking: "how much does this query align with this key?" The score depends on both angle and length. Holding other scores and masks fixed, increasing it raises that token's softmax attention weight.


Cosine similarity: stripping out magnitude

Here's a problem with using raw dot products to measure similarity. Consider these two vectors:

  • a=[1,0]\mathbf{a} = [1, 0]
  • b=[1000,0]\mathbf{b} = [1000, 0]

Their dot product is 1×1000+0×0=10001 \times 1000 + 0 \times 0 = 1000. And the dot product of a\mathbf{a} with c=[0.5,0.866]\mathbf{c} = [0.5, 0.866] (which points 60 degrees away) is 1×0.5+0×0.866=0.51 \times 0.5 + 0 \times 0.866 = 0.5.

So the dot product says a\mathbf{a} is way more "similar" to b\mathbf{b} (score of 1000) than to c\mathbf{c} (score of 0.5). But a\mathbf{a} and b\mathbf{b} point in exactly the same direction, while a\mathbf{a} and c\mathbf{c} point in different directions. The dot product is correct in the sense that b\mathbf{b} really is more aligned, but the scores are being dominated by magnitude, not direction.

For an embedding model trained to use directional similarity, normalizing removes length differences from the comparison.

The picture below makes the disagreement concrete. Same query, same three candidates, two metrics. The "long" candidate is 20° off-axis and longer; the "aligned" one is short but pointing right at the query. Watch the rankings shuffle when the metric flips.

ranking by
cosine similaritydot product
three candidates against the same query q. dot product rewards long vectors; cosine doesn't.
longalignedtiltedqdashed circle = unit length
long
4.32
aligned
1.40
tilted
1.91
q · v · bars: 0–5
Switch metrics to compare the ranking of the long off-axis vector with the shorter aligned vector.

Cosine similarity fixes this by normalizing:

cos⁡θ=a⋅b∥a∥ ∥b∥\cos\theta = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \, \|\mathbf{b}\|}

You take the dot product and divide by the product of the norms. This gives you a number between −1-1 and 11:

  • 11 means identical direction
  • 00 means perpendicular
  • −1-1 means opposite direction

For our example: cos⁡(a,b)=1000/(1×1000)=1.0\cos(\mathbf{a}, \mathbf{b}) = 1000 / (1 \times 1000) = 1.0 (identical direction, as expected). And cos⁡(a,c)=0.5/(1×1)=0.5\cos(\mathbf{a}, \mathbf{c}) = 0.5 / (1 \times 1) = 0.5 (60 degrees apart).

There's an equivalent way to think about this: cosine similarity is just the dot product of the unit vectors. Normalize both vectors first, then dot them:

cos⁡θ=a^⋅b^\cos\theta = \hat{\mathbf{a}} \cdot \hat{\mathbf{b}}

Cosine similarity is undefined for a zero vector. Libraries may skip such vectors or stabilize the denominator with epsilon; check that behavior when comparing results.

Cosine is a good default when vector magnitude is accidental or doesn't carry signal you care about. But it's not always the right metric. If the embedding model was trained so that length encodes something meaningful (popularity, frequency, confidence), then raw dot product is the metric you actually want, because cosine throws that information away. The right move is to check how the embedding model was trained and what its docs recommend. For unit-length embeddings, dot product, cosine similarity, and Euclidean nearest-neighbor ranking agree: ∥a−b∥2=2−2(a⋅b)\|a-b\|^2 = 2-2(a\cdot b).


A worked example

Vectors: a=[3,4]\mathbf{a} = [3, 4] and b=[4,1]\mathbf{b} = [4, 1]

Norms: ∥a∥=9+16=25=5\|\mathbf{a}\| = \sqrt{9 + 16} = \sqrt{25} = 5 ∥b∥=16+1=17≈4.12\|\mathbf{b}\| = \sqrt{16 + 1} = \sqrt{17} \approx 4.12

Dot product: a⋅b=(3)(4)+(4)(1)=16\mathbf{a} \cdot \mathbf{b} = (3)(4) + (4)(1) = 16

Cosine similarity: cos⁡θ=165×4.12≈1620.6≈0.776\cos\theta = \frac{16}{5 \times 4.12} \approx \frac{16}{20.6} \approx 0.776

Angle: θ=arccos⁡(0.776)≈39°\theta = \arccos(0.776) \approx 39°

Projection of a onto b: compb(a)=1617≈3.88\text{comp}_{\mathbf{b}}(\mathbf{a}) = \frac{16}{\sqrt{17}} \approx 3.88 (the signed length of the shadow) projb(a)=1617 [4,1]≈[3.76,0.94]\text{proj}_{\mathbf{b}}(\mathbf{a}) = \frac{16}{17}\,[4, 1] \approx [3.76, 0.94] (the shadow as a vector)

So these two vectors are about 39 degrees apart. They're reasonably aligned (cosine similarity of 0.776 out of a maximum of 1.0), and most of a\mathbf{a}'s magnitude projects onto b\mathbf{b}'s direction.

Now imagine doing this in 768 dimensions instead of 2. The formulas are identical. You're still just multiplying components and summing. There are more terms in each sum, but the same identities apply.


Where this lives in the stack

Norms, dot products, and cosine similarity appear in several parts of an LLM:

Attention. Attention starts by computing dot products between query and key vectors. In transformers it's scaled dot-product attention: those raw scores get divided by dk\sqrt{d_k}, optionally masked, and then passed through softmax to become weights over the value vectors. So the dot product isn't the whole attention mechanism, but it's the primitive compatibility score that everything else is built on. The machinery around it gets its own post later in the series.

The hand-tuned vectors below let you rank a tiny vocabulary by cosine similarity. Change the query to see which neighbors move up or down.

query
bananacat
Hand-built vectors, ranked by cosine similarity (0–1 here)
cat
0.000
kitten
0.994
dog
0.981
tiger
0.964
car
0.194
truck
0.189
bike
0.279
apple
0.197
banana
0.267
mango
0.213
chair
0.310
cos(q, w) = (q · w) / (‖q‖ ‖w‖)
Bars show cosine similarity to the selected query; rows reorder by rank.

Embedding retrieval. When you search a vector database, you encode the query as an embedding and compare it against the stored embeddings using cosine similarity, normalized dot product, raw inner product, or L2 distance, depending on how the embedding model and index are configured. At small scale you can brute-force the comparison against every vector. At large scale, libraries such as FAISS use indexing structures and approximate nearest neighbor algorithms (IVF, HNSW, product quantization) to avoid scanning the whole corpus on every query. The metric is still a dot product or a distance; the work is in not computing all of them.

Linear layers. Every nn.Linear(in, out) in PyTorch computes xWT+b\mathbf{x} W^T + \mathbf{b}. Each output neuron is the dot product of the input with one row of the weight matrix. A linear layer with 768 inputs and 3072 outputs computes 3072 dot products in parallel.


Implementation

import numpy as np
import torch
import torch.nn.functional as F
 
a = np.array([3.0, 4.0])
b = np.array([4.0, 1.0])
 
# Norms
np.linalg.norm(a)                        # 5.0
torch.linalg.vector_norm(torch.tensor(a))              # 5.0
 
# Dot product
np.dot(a, b)                             # 16.0
torch.dot(torch.tensor(a), torch.tensor(b))  # 16.0
 
# Cosine similarity
cos_sim = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 0.776...
 
# Compare along the default feature dimension, dim=1
F.cosine_similarity(
    torch.tensor(a).unsqueeze(0),
    torch.tensor(b).unsqueeze(0)
)  # tensor([0.7761])

A dot product on two 768-dimensional vectors requires 768 multiplications and 767 additions. Attention computes one for every query–key pair, so the score matrix grows quadratically with sequence length. Batching these products into matrix multiplications uses GPUs efficiently, but the total work still grows. FlashAttention, KV caches, and serving systems address different parts of that cost.


Common misconceptions

"Cosine similarity measures semantic meaning." It measures the angle between vectors. Its usefulness for semantic comparison depends on the representation: a model trained to bring related texts together may produce useful cosine rankings, while arbitrary vectors need not.

"A bigger dot product always means more similar." For a fixed query, dot-product ranking agrees with directional similarity when the candidate vectors have equal nonzero norms. Otherwise length affects the score. For example, [1000,0][1000,0] has dot product 1000 with [1,0][1,0], zero with [0,1][0,1], and −1000 with [−1,0][-1,0]. A longer vector amplifies the signed projection; it does not make every score positive or large.

"Euclidean distance and cosine similarity always agree." They don't, at least not for unnormalized vectors. Two vectors can be close in Euclidean distance but point in very different directions (imagine two short vectors near the origin, pointing away from each other). And two vectors can have high cosine similarity but be far apart in Euclidean space (imagine two very long vectors pointing the same way but with different magnitudes). However, if you normalize all your vectors to unit length first, then Euclidean distance and cosine similarity become monotonically related. This is why many vector databases normalize embeddings at index time: it lets you use either metric interchangeably.

What's next

The next post covers probability distributions: turning a vector of scores into next-token probabilities with softmax.


Additional reading (and watching)

  • Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS 2017. The paper that introduced scaled dot-product attention as softmax(QKT/dk)V\mathrm{softmax}(QK^T/\sqrt{d_k})V, which is why every attention score is a dot product.
  • Alammar, J. (2019). The Illustrated Word2vec. A visual walkthrough of how embedding spaces learn directional meaning through training.
  • Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, Chapter 2: Linear Algebra. MIT Press. Covers the relationship between norms, distances, and inner products at the level needed for deep learning.
  • Kusupati, A., et al. (2022). Matryoshka Representation Learning. NeurIPS 2022. Introduced nested embeddings that let you train selected vector prefixes to remain useful at different embedding sizes.
  • Johnson, J., Douze, M., & Jégou, H. (2017). Billion-scale similarity search with GPUs. IEEE Transactions on Big Data. The FAISS paper; made approximate nearest-neighbor search practical at web scale.