Chapter 1 · Section 1.3 · 60 min

The geometry of meaning

Once words are vectors, "how similar are these two words?" becomes "how close are these two points?". But close can mean several things, and the choice matters a great deal. Attention in a Transformer uses one notion, vector databases usually another, and a surprising amount of confusion (and many bugs) comes from mixing them up.

This section builds the three tools you'll use for the rest of the book (dot product, cosine similarity, and Euclidean distance), shows exactly how they relate, and then takes a trip into high-dimensional space, where your 3-D intuition quietly breaks. We finish with the trick that makes similarity search over millions of vectors possible, random hyperplanes.

The dot product measures alignment times magnitude

For u,v∈Rd\mathbf{u}, \mathbf{v} \in \mathbb{R}^d the dot product has two equivalent definitions, one algebraic and one geometric,

u⊤v  =  ∑j=1dujvj  =  ∥u∥ ∥v∥cos⁡θ,\mathbf{u}^\top\mathbf{v} \;=\; \sum_{j=1}^{d} u_j v_j \;=\; \lVert\mathbf{u}\rVert\,\lVert\mathbf{v}\rVert \cos\theta,

where θ\theta is the angle between the vectors (in this section only, θ\theta is an angle, not the model's parameters) and ∥u∥=u⊤u\lVert\mathbf{u}\rVert = \sqrt{\mathbf{u}^\top\mathbf{u}} is the length.

In plain words, multiply matching coordinates and add them up. Geometrically, that equals the product of the two lengths times the cosine of the angle between them.

So the dot product mixes two different things. It's large when the vectors point the same way (large cos⁡θ\cos\theta) or when they're long (large norms). It's zero when they're perpendicular and negative when they point in opposing directions.

There's a second, very useful reading. u⊤v^\mathbf{u}^\top\hat{\mathbf{v}}, where v^=v/∥v∥\hat{\mathbf{v}} = \mathbf{v}/\lVert\mathbf{v}\rVert, is the length of the shadow that u\mathbf{u} casts onto the direction of v\mathbf{v}. That shadow is called the scalar projection, and it answers "how much of u\mathbf{u} points along v\mathbf{v}?" In §1.9 we'll use exactly this to measure how much gender a word's vector carries.

A worked example. Take u=(3,4)\mathbf{u} = (3, 4) and v=(4,3)\mathbf{v} = (4, 3). The dot product is 3⋅4+4⋅3=243\cdot4 + 4\cdot3 = 24. Both lengths are 5, so cos⁡θ=24/25=0.96\cos\theta = 24/25 = 0.96 and θ≈16.3°\theta \approx 16.3°. The shadow of u\mathbf{u} on v\mathbf{v} has length 24/5=4.824/5 = 4.8. Now double u\mathbf{u} to (6,8)(6, 8). The dot product doubles to 48, the shadow doubles to 9.6, and the angle and cosine do not change at all.

How do dot product, cosine, and distance relate?
Drag the tips of u (indigo) and v (pink). Watch how the dot product mixes direction and length, while cosine only cares about direction.
Loading interactive figure…

Cosine similarity measures direction only

If we only care about direction, we divide out the lengths,

cos⁡(u,v)  =  u⊤v∥u∥ ∥v∥  =  u^⊤v^  ∈  [−1,1].\cos(\mathbf{u},\mathbf{v}) \;=\; \frac{\mathbf{u}^\top\mathbf{v}}{\lVert\mathbf{u}\rVert\,\lVert\mathbf{v}\rVert} \;=\; \hat{\mathbf{u}}^\top\hat{\mathbf{v}} \;\in\; [-1, 1].

In plain words, cosine similarity is the dot product of the two vectors after scaling each to length 1. (The range [−1,1][-1, 1] is the Cauchy–Schwarz inequality, ∣u⊤v∣≤∥u∥∥v∥\lvert\mathbf{u}^\top\mathbf{v}\rvert \le \lVert\mathbf{u}\rVert\lVert\mathbf{v}\rVert.)

It's invariant to rescaling, since cos⁡(3u,v)=cos⁡(u,v)\cos(3\mathbf{u}, \mathbf{v}) = \cos(\mathbf{u},\mathbf{v}). That's usually what you want when comparing word vectors, because the length of a word vector tends to carry other information. In word2vec, vector norms are empirically correlated with word frequency and with how consistently a word is used (Schakel & Wilson, 2015). If you used raw dot products for "most similar word", long, frequent words would win just for being long.

Euclidean distance, and the identity that ties everything together

The ordinary straight-line distance is ∥u−v∥\lVert\mathbf{u}-\mathbf{v}\rVert. Expand its square,

∥u−v∥2  =  (u−v)⊤(u−v)  =  ∥u∥2+∥v∥2−2 u⊤v.\lVert\mathbf{u}-\mathbf{v}\rVert^2 \;=\; (\mathbf{u}-\mathbf{v})^\top(\mathbf{u}-\mathbf{v}) \;=\; \lVert\mathbf{u}\rVert^2 + \lVert\mathbf{v}\rVert^2 - 2\,\mathbf{u}^\top\mathbf{v}.

(You used this in Exercise 1.1d.) Now suppose both vectors are unit length. Then ∥u∥2=∥v∥2=1\lVert\mathbf{u}\rVert^2 = \lVert\mathbf{v}\rVert^2 = 1, and

  ∥u^−v^∥2  =  2−2cos⁡(u,v)  \boxed{\;\lVert\hat{\mathbf{u}}-\hat{\mathbf{v}}\rVert^2 \;=\; 2 - 2\cos(\mathbf{u},\mathbf{v})\;}

In plain words, for unit vectors, squared distance is a decreasing function of cosine. For example, two unit vectors with cosine 0.8 are 0.4≈0.632\sqrt{0.4} \approx 0.632 apart, and two with cosine 0 are 2\sqrt 2 apart, like the one-hot vectors of §1.1.

This is one of the most practically important facts in the chapter. Once vectors are normalized, ranking by Euclidean distance, by cosine, and by dot product all give the same order. That's why vector databases usually say "normalize your embeddings, then use inner product". Maximum inner-product search on unit vectors is cosine search, which is nearest-neighbor search.

Tick "Normalize" in the playground above to check the identity numerically.

Which one should you use?

measuresensitive to length?rangewhere you'll meet it
dot product u⊤v\mathbf{u}^\top\mathbf{v}yes(−∞,∞)(-\infty,\infty)inside models, such as attention scores, softmax logits, word2vec's objective (§1.5), recommender scores
cosine u^⊤v^\hat{\mathbf u}^\top\hat{\mathbf v}no[−1,1][-1, 1]comparing embeddings, such as "most similar word", semantic search, and joint image and text spaces
Euclidean ∥u−v∥\lVert\mathbf u-\mathbf v\rVertyes[0,∞)[0,\infty)k-means, t-SNE (§1.6), metric-space indexes, and the same ranking as cosine on unit vectors

Let's put this into code. Here are a few hand-made 4-D "word vectors", with one deliberately long.

Python · cell 1

Read the table row by row. The dot product ranks "the" first (4.07, nearly three times kitten's 1.46), purely because it is long. Cosine ranks kitten first (0.987) and puts "the" third (0.738). Euclidean distance also ranks kitten first, but it is fooled in a different way, because "the" is far away for being long.

Warm-up

Cosine-similarity matrix, safely

Write cosine_sim_matrix(A, B) that returns the matrix Sij=cos⁡(ai,bj)S_{ij} = \cos(\mathbf{a}_i, \mathbf{b}_j) for A of shape (n, d) and B of shape (m, d). It must be vectorized (one matrix multiply), and zero vectors must not produce NaN. Define their similarity as 0.

Hidden tests, hints, and AI help, from $1.99 a month.
Interview

Top-k nearest neighbors in O(V) time

Given a query vector q of shape (d,) and an embedding matrix E of shape (V, d), return the indices of the k rows with the highest cosine similarity to q, sorted from most to least similar, optionally excluding some indices (like the query word itself).

Sorting all VV scores costs O(Vlog⁡V)O(V\log V). You can do better. np.argpartition finds the top kk in O(V)O(V) on average (it uses introselect), and then you sort only those kk, for O(V+klog⁡k)O(V + k\log k) in total.

Hidden tests, hints, and AI help, from $1.99 a month.

When lengths differ, inner-product search is a different problem

Recommenders often score items by a raw dot product q⊤x\mathbf{q}^\top\mathbf{x}, because the item vector's length carries meaning (popularity, quality). Finding the best item is then maximum inner-product search (MIPS), and it is not nearest-neighbor search. A long item can beat a closer short one, and a vector need not even be its own best match (x⊤x<x⊤(2x)\mathbf{x}^\top\mathbf{x} < \mathbf{x}^\top(2\mathbf{x})).

There is a neat reduction (Bachrach et al., 2014; Neyshabur & Srebro, 2015). Let M=max⁡i∥xi∥M = \max_i \lVert\mathbf{x}_i\rVert. Append one coordinate to every item, x~i=(xi,M2−∥xi∥2)\tilde{\mathbf{x}}_i = (\mathbf{x}_i, \sqrt{M^2 - \lVert\mathbf{x}_i\rVert^2}), so every augmented item has length exactly MM, and append a 0 to the query, q~=(q,0)\tilde{\mathbf{q}} = (\mathbf{q}, 0). Then

∥q~−x~i∥2=∥q∥2+M2−2 q⊤xi.\lVert\tilde{\mathbf{q}} - \tilde{\mathbf{x}}_i\rVert^2 = \lVert\mathbf{q}\rVert^2 + M^2 - 2\,\mathbf{q}^\top\mathbf{x}_i .

In plain words, the first two terms are the same for every item, so the nearest augmented item is the one with the largest inner product. Any nearest-neighbor index can now answer MIPS.

Python · cell 2

A trip into high dimensions

Embeddings live in hundreds or thousands of dimensions. Geometry there is not a scaled-up version of 3-D, and three facts will shape everything we do.

Fact 1. Random vectors are almost orthogonal

Draw u,v\mathbf{u}, \mathbf{v} with independent N(0,1)\mathcal{N}(0,1) entries. The dot product u⊤v=∑jujvj\mathbf{u}^\top\mathbf{v} = \sum_j u_j v_j is a sum of dd independent terms, each with mean 0 and variance 1. So it has mean 0 and variance dd, a typical size of d\sqrt d. Meanwhile each norm is about d\sqrt d (next fact). Therefore

cos⁡(u,v)=u⊤v∥u∥∥v∥≈O(d)d=O ⁣(1d).\cos(\mathbf{u},\mathbf{v}) = \frac{\mathbf{u}^\top\mathbf{v}}{\lVert\mathbf{u}\rVert\lVert\mathbf{v}\rVert} \approx \frac{\mathcal{O}(\sqrt d)}{d} = \mathcal{O}\!\left(\tfrac{1}{\sqrt d}\right).

In fact the variance is exactly 1/d1/d, and the derivation is short enough to carry around.

∂ Why the cosine of two random directions has variance exactly 1/d

A Gaussian vector with independent standard entries has a rotation-invariant distribution, so its direction u^\hat{\mathbf{u}} is uniform on the unit sphere and independent of its length. The cosine is u^⊤v^\hat{\mathbf{u}}^\top\hat{\mathbf{v}}. Condition on v^\hat{\mathbf{v}} and rotate the coordinates so that v^=e1\hat{\mathbf{v}} = \mathbf{e}_1. Then the cosine is just u^1\hat u_1, the first coordinate of a uniform unit vector.

By symmetry, E[u^1]=0\mathbb{E}[\hat u_1] = 0 (flipping the sign of a coordinate doesn't change the distribution) and every coordinate has the same second moment. The coordinates of a unit vector satisfy ∑j=1du^j2=1\sum_{j=1}^d \hat u_j^2 = 1, so taking expectations gives d E[u^12]=1d\,\mathbb{E}[\hat u_1^2] = 1, that is,

E[cos⁡(u,v)]=0,Var⁡[cos⁡(u,v)]=E[u^12]=1d.\mathbb{E}[\cos(\mathbf{u},\mathbf{v})] = 0, \qquad \operatorname{Var}[\cos(\mathbf{u},\mathbf{v})] = \mathbb{E}[\hat u_1^2] = \frac1d .

For large dd the distribution is close to N(0,1/d)\mathcal{N}(0, 1/d) by the central limit theorem applied to u⊤v/d\mathbf{u}^\top\mathbf{v}/\sqrt d.

In plain words, in dd dimensions, the cosine between two random directions is typically about ±1/d\pm 1/\sqrt d. At d=768d = 768 that's ±0.036\pm 0.036, and at d=4,096d = 4{,}096 it's ±0.016\pm 0.016.

Two consequences follow.

  • "Small" cosines can be big signals. A cosine of 0.3 between two 768-dimensional embeddings is about 8 standard deviations above chance (0.3/0.036≈8.30.3/0.036 \approx 8.3).
  • There's room for a huge number of almost-orthogonal directions. You can fit exponentially many (in dd) vectors whose pairwise cosines are all below any fixed ε\varepsilon. That's what lets a model with d=4096d=4096 represent far more than 4096 features by giving each a nearly orthogonal direction. This phenomenon, called superposition, is central to understanding what is inside large models, and it is one reason why the features a model learns rarely line up with single coordinates.
What happens to random vectors as the dimension grows?
Each bar counts the cosine similarities of many pairs of random Gaussian vectors (1,500 at low d). Slide the dimension d and watch the distribution squeeze toward 0.
Loading interactive figure…

Fact 2. Norms and distances concentrate

The squared norm ∥u∥2=∑juj2\lVert\mathbf{u}\rVert^2 = \sum_j u_j^2 is also a sum of dd independent terms, each with mean 1 and variance 2. So E∥u∥2=d\mathbb{E}\lVert\mathbf{u}\rVert^2 = d and Var⁡∥u∥2=2d\operatorname{Var}\lVert\mathbf{u}\rVert^2 = 2d, which means ∥u∥2/d→1\lVert\mathbf{u}\rVert^2/d \to 1. A first-order expansion of the square root gives ∥u∥≈d±12\lVert\mathbf{u}\rVert \approx \sqrt d \pm \tfrac{1}{\sqrt2}. At d=768d = 768 that is 27.7±0.7127.7 \pm 0.71. Random Gaussian vectors don't fill the ball. They live on a thin shell, whose absolute thickness stays constant while its radius grows like d\sqrt d.

Volume behaves the same way. The fraction of a dd-dimensional ball's volume that lies within radius 1−ε1 - \varepsilon of its center is (1−ε)d(1-\varepsilon)^d. For d=768d = 768 and ε=0.01\varepsilon = 0.01, that is 0.99768≈0.000440.99^{768} \approx 0.00044. More than 99.9% of the volume sits in the outer 1% of the radius.

The same thing happens to distances. For random data, the ratio between the farthest and the nearest neighbor of a query tends to 1 as dd grows (Beyer et al., 1999). The widget's "distance contrast" shows this. It's the precise sense in which nearest-neighbor search gets harder in high dimensions. A related effect, hubness, is that a few points become the nearest neighbor of a disproportionate number of others (Radovanović et al., 2010), and it plagues retrieval with high-dimensional embeddings. Real embeddings are useful only because they are not random. Their meaningful structure lives near much lower-dimensional patterns.

Python · cell 3

Fact 3. Random projections preserve distances (Johnson–Lindenstrauss)

Here's the good news. You can squash high-dimensional points into far fewer dimensions with a random matrix and barely distort their distances.

Johnson–Lindenstrauss lemma. For any 0<ε<10 < \varepsilon < 1 and any nn points x1,…,xn∈RD\mathbf{x}_1,\dots,\mathbf{x}_n \in \mathbb{R}^D, if the target dimension kk satisfies

k  ≥  4ln⁡nε2/2−ε3/3,k \;\ge\; \frac{4 \ln n}{\varepsilon^2/2 - \varepsilon^3/3},

then there exists a linear map f:RD→Rkf: \mathbb{R}^D \to \mathbb{R}^k (a random Gaussian matrix scaled by 1/k1/\sqrt{k} works with probability at least 1/n1/n by this proof, and in practice a single draw almost always works, as the cell below shows) such that for all pairs,

(1−ε) ∥xi−xj∥2  ≤  ∥f(xi)−f(xj)∥2  ≤  (1+ε) ∥xi−xj∥2.(1-\varepsilon)\,\lVert\mathbf{x}_i-\mathbf{x}_j\rVert^2 \;\le\; \lVert f(\mathbf{x}_i)-f(\mathbf{x}_j)\rVert^2 \;\le\; (1+\varepsilon)\,\lVert\mathbf{x}_i-\mathbf{x}_j\rVert^2 .

In plain words, nn points can be squeezed into about log⁡n/ε2\log n / \varepsilon^2 dimensions while every pairwise distance stays within a factor 1±ε1\pm\varepsilon. This is the form proved by Dasgupta and Gupta (2003). Notice what's missing, namely the original dimension DD. The target dimension depends only on the number of points, and only logarithmically.

Worked numbers make the bound concrete. For n=300n = 300 points and ε=0.3\varepsilon = 0.3, the bound asks for k≥4ln⁡300/(0.045−0.009)≈634k \ge 4 \ln 300 / (0.045 - 0.009) \approx 634. For a million points and ε=0.1\varepsilon = 0.1, it asks for 11,842 dimensions, and going to a billion points only raises that to about 17,800. The bound is conservative (you'll measure how conservative in the exercise), but its shape is the lesson. The logarithm is the deep reason why modest dimensions can serve enormous vocabularies, and in practice the dimensions needed are smaller still, because real data is far from the worst case and an embedding only has to keep the distances that matter for its task. It's also the theory behind random-projection tricks in sketching, hashing, and approximate nearest-neighbor search.

Python · cell 4
Research

How many dimensions does JL say you need?

Write jl_min_dim(n, eps) that returns the smallest integer kk satisfying the Dasgupta–Gupta bound above. Then use the max_distortion helper from the cell above (it's provided in this exercise's setup) to check empirically whether the bound is tight or loose for n=300n = 300, ε=0.3\varepsilon = 0.3. Print what you find. The tests only check jl_min_dim, and the investigation is for you. (Ask the AI to review your conclusion.)

Hidden tests, hints, and AI help, from $1.99 a month.

Random hyperplanes, and search that skips most of the data

Johnson–Lindenstrauss projects to fewer real numbers. Push the idea to its extreme and keep only the sign of each random projection, one bit per hyperplane. This is SimHash (Charikar, 2002), the simplest locality-sensitive hashing (LSH) scheme for cosine similarity, and it comes with an exact formula.

Draw a random direction r\mathbf{r} with independent Gaussian entries and record the bit h(x)=[r⊤x>0]h(\mathbf{x}) = [\mathbf{r}^\top\mathbf{x} > 0]. The hyperplane orthogonal to r\mathbf{r} splits space in two, and two vectors get different bits exactly when the hyperplane passes between them. Then

P[h(u)≠h(v)]=θπ,θ=arccos⁡cos⁡(u,v).\mathbb{P}\big[h(\mathbf{u}) \neq h(\mathbf{v})\big] = \frac{\theta}{\pi}, \qquad \theta = \arccos\cos(\mathbf{u}, \mathbf{v}).
∂ Why the separation probability is θ/π

Only the component of r\mathbf{r} in the plane spanned by u\mathbf{u} and v\mathbf{v} matters for the signs of r⊤u\mathbf{r}^\top\mathbf{u} and r⊤v\mathbf{r}^\top\mathbf{v}. Because r\mathbf{r} is rotation invariant, the direction of that 2-D component is uniform on the circle. The two signs differ exactly when that direction makes an angle of more than 90° with one vector and less than 90° with the other. On the circle, those directions form two arcs, each of angle θ\theta (one near each side of the wedge between the vectors). So the probability is 2θ/2π=θ/π2\theta/2\pi = \theta/\pi. The same lemma is the heart of the Goemans–Williamson rounding algorithm for MAX-CUT.

In plain words, each random hyperplane is a coin that comes up "different" with probability equal to the angle between the vectors, as a fraction of 180°. For cosine 0.9 (θ≈25.8°\theta \approx 25.8°) the coin says "different" 14.4% of the time. For cosine 0 (θ=90°\theta = 90°) it says "different" half the time.

Now build an index. Concatenate bb bits into a bucket key, so two vectors share a bucket only if all bb hyperplanes agree, with probability pbp^b where p=1−θ/πp = 1 - \theta/\pi. One table with large bb is precise but misses true neighbors, so use LL independent tables and take every point that shares a bucket with the query in any table as a candidate. The candidate probability is

Pcand(θ)=1−(1−p b)L,p=1−θπ.P_{\text{cand}}(\theta) = 1 - \big(1 - p^{\,b}\big)^{L}, \qquad p = 1 - \frac{\theta}{\pi}.

In plain words, bb makes buckets picky and LL gives each neighbor several chances, and together they turn a gentle function of the angle into a steep S-curve. With b=16b = 16 and L=20L = 20, a neighbor at cosine 0.9 becomes a candidate with probability 0.83, one at cosine 0.5 with probability 0.03, and an unrelated vector at cosine 0 with probability 0.0003. Then we rank only the candidates by exact cosine. Search cost drops from all nn points to a small candidate set, at the price of occasionally missing a neighbor.

How do random hyperplanes find similar vectors without comparing against everything?
Drag u and v around the circle. Each gray line is a random hyperplane, and it gives each vector one bit depending on its side. Lines that separate u and v are highlighted. The right panel shows how b bits per table and L tables turn the angle into a steep candidate probability.
Loading interactive figure…
Warm-up

The LSH candidate probability

Implement collision_prob(cos_sim, n_bits, n_tables) returning PcandP_{\text{cand}} from the formula above. It must work elementwise on NumPy arrays of cosines, and clip the cosine into [−1,1][-1, 1] first (rounding can produce 1.0000000002, and arccos of that is NaN).

Hidden tests, hints, and AI help, from $1.99 a month.
Interview

A random-hyperplane LSH index versus brute force

Build a small cosine-similarity index. Implement a class LSHIndex(E, n_bits, n_tables, seed=0) that draws n_tables independent sets of n_bits Gaussian hyperplanes, hashes every row of E into one bucket per table, and a method query(q, k) that returns (ids, n_candidates). The ids are the top k candidates ranked by exact cosine with q (most similar first), and n_candidates is the number of distinct rows that shared a bucket with q in at least one table.

The tests plant 50 tight clusters of 40 points in 32 dimensions and require that your index finds at least 90% of the true top 10 while scoring fewer than a quarter of the points. This is the design question behind every vector database, in miniature.

Hidden tests, hints, and AI help, from $1.99 a month.

Real systems mostly use graph indexes (HNSW) and product quantization rather than plain LSH, because those adapt to the data instead of using random hyperplanes. But LSH is the one whose behavior you can derive exactly, and the trade-off it exposes (more candidates means higher recall and more work) is the same knob every vector index turns. §7.4 builds the rest.

In PyTorch

🔥 In PyTorchfor a machine with PyTorch

Read more

Watch next

🎬 Dot products and duality | Chapter 9, Essence of linear algebra · 3Blue1BrownThe best visual explanation of what a dot product really is.
🎬 Transformers, the tech behind LLMs | Deep Learning Chapter 5 · 3Blue1Brown · starts at 12:27The word-embeddings segment shows directions and dot products in a real embedding space.