Index Ai Academy
Phase 2 · Math, when you need it · Linear algebra

Strange geometry in high dimensions

Near-perpendicular random directions, bunched-up distances, and the curse of dimensionality.

About 25 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Embeddings have hundreds or thousands of numbers each. Your intuition for space comes from three dimensions, and in 1,536 it misleads you badly. Up there, two random directions are almost always nearly perpendicular, the distances between random points bunch together, and nearly all of a ball's volume sits just under its surface. These facts explain why cosine similarity works for embeddings, why nearest-neighbor search gets harder as data gets wider, and why reducing dimensions helps. This lesson lets you see each one by running small experiments.

What you'll be able to do

  • Show by experiment that random directions in many dimensions are nearly perpendicular
  • Explain why distances between random points bunch together as dimensions grow
  • Say why embeddings are compared by cosine similarity rather than raw distance
  • Describe the curse of dimensionality and what reducing dimensions buys you

What it covers

  1. Random directions are nearly perpendicular
  2. Most of the volume is near the surface
  3. Distances bunch together
  4. Measure the bunching
  5. Why embeddings use cosine similarity
  6. The curse of dimensionality
  7. How thin does the data get?
  8. High dimensions in AI
Quick check
Before any explaining, a guess. Pick two random directions in 1,000 dimensions. What is the cosine of the angle between them, typically?
  1. Exactly 0.5, every time
  2. Close to 1: nearly the same direction
  3. Close to 0: nearly perpendicular
  4. Close to −1: nearly opposite
Show the answer

Close to 0: nearly perpendicular

In high dimensions, random directions are almost always nearly perpendicular, with cosines bunched tightly around 0. It's one of several facts about high dimensions that surprise everyone the first time.

Random directions are nearly perpendicular

The cosine similarity of two vectors, from the dot product lesson, is their dot product divided by both lengths. It's 1 for the same direction, 0 for perpendicular and −1 for opposite. Here's an experiment: draw random vectors and measure the cosine between pairs, first in 2 dimensions, then in 1,000.

import random
random.seed(0)

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (sum(x * x for x in a) ** 0.5 * sum(y * y for y in b) ** 0.5)

for d in [2, 1000]:
    cos = [cosine([random.gauss(0, 1) for _ in range(d)], [random.gauss(0, 1) for _ in range(d)]) for _ in range(5)]
    print(d, [round(c, 2) for c in cos])
It runs right here in your browser, with nothing to install.

In 2 dimensions the cosines land anywhere from −1 to 1. In 1,000 they all sit within a few hundredths of 0. A rough rule: the typical cosine between random directions is about 1 / √d, so about 0.03 in 1,000 dimensions. (Drawing every coordinate from a normal distribution, with random.gauss, makes every direction equally likely, which is what "random direction" should mean.)

Quick check
Two vectors point along different axes. What does this print?
a = [1, 0, 0]
b = [0, 1, 0]
dot = sum(x * y for x, y in zip(a, b))
print(dot / ((sum(x * x for x in a) ** 0.5) * (sum(y * y for y in b) ** 0.5)))
  1. -1.0
  2. 0.0
  3. 1.0
  4. 0.5
Show the answer

0.0

The dot product is 1×0 + 0×1 + 0×0 = 0, so the cosine is 0: the vectors are perpendicular. Random high-dimensional vectors come close to this without being exactly on the axes.

Most of the volume is near the surface

Picture an orange: in 3 dimensions, the peel is a small part of the fruit. Now count how much of a ball's volume lies in its outer shell. A ball's volume grows like its radius to the power of the dimension, so the inner part, at 95% of the radius, holds 0.95ᵈ of the volume. In 3 dimensions that's 86%, so the outer 5% shell holds only 14%. In 100 dimensions, the inner part holds 0.95¹⁰⁰, under 1%, and the thin shell holds nearly everything.

The same holds for a cube: in many dimensions, almost all of it is in the corners and near the faces, and almost none is near the middle. It sounds like a curiosity, but it has a practical side: in high dimensions, random points almost all sit about the same distance from the center, and, as the next section shows, about the same distance from each other.

Try it now

The share of a ball's volume in its outer shell: 1 − (1 − thickness)ᵈ. Push the dimensions up and watch the shell swallow the ball.

Quick check
In 3 dimensions, what share of a ball's volume lies within 95% of its radius? What does this print?
print(round(0.95 ** 3, 3))
  1. 0.143
  2. 0.857
  3. 2.85
  4. 0.95
Show the answer

0.857

Volume grows like the radius cubed in 3 dimensions, so the inner 95% of the radius holds 0.95³ ≈ 0.857 of the volume. The outer shell holds the other 14%.

Distances bunch together

Take a query point and a crowd of random points, and measure the distance to each. In 2 dimensions, some points are much closer than others. In 500 dimensions, the nearest and the farthest are almost the same distance away. A useful measure is the relative contrast: (farthest − nearest) ÷ nearest.

import random
random.seed(1)

def dist(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

for d in [2, 500]:
    query = [random.random() for _ in range(d)]
    ds = [dist(query, [random.random() for _ in range(d)]) for _ in range(100)]
    print(d, round((max(ds) - min(ds)) / min(ds), 2))

Real data is less extreme than these random points, because real features are correlated and the data lies close to a much smaller surface inside the space. That's exactly why embeddings and PCA work at all. Still, when everything is roughly equally far away, "the nearest neighbor" stops meaning much. This is a big part of the curse of dimensionality, and it's why search over raw high-dimensional data needs care.

Quick check
When every random point is about equally far from a query, what happens to "the nearest neighbor"?
  1. It moves to the center of the space
  2. It's barely closer than any other point
  3. It's always the first point in the list
  4. It becomes the query point itself
Show the answer

It's barely closer than any other point

If the nearest point is only slightly closer than the farthest, being "nearest" says little about real similarity. That's why search over raw high-dimensional data needs better measures or fewer dimensions.

Drill

Measure the bunching

Write contrast(d): with random.seed(2), make one random query point and 200 random points in d dimensions (each coordinate random.random()), and return (farthest − nearest) ÷ nearest. Then set low_d = contrast(2) and high_d = contrast(300).

import random

def dist(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

def contrast(d):
    random.seed(2)
    # a query point, 200 random points, and (farthest - nearest) / nearest
    return 0

low_d = contrast(2)
high_d = contrast(300)
print(round(low_d, 2), round(high_d, 2))
In the lesson, your code is checked as you write it.
Quick check
Your drill found a large contrast in 2 dimensions and a small one in 300. What would you expect in 30 dimensions?
  1. Exactly zero, with no spread
  2. Larger than in 2 dimensions
  3. Somewhere in between the two
  4. Smaller than in 300 dimensions
Show the answer

Somewhere in between the two

The contrast shrinks steadily as dimensions are added, so 30 dimensions lands between the two. Try contrast(30) to see it.

Why embeddings use cosine similarity

An embedding's direction carries its meaning; its length often reflects things like how long or common a text is. Comparing directions with cosine similarity ignores those lengths. And because random directions in high dimensions sit near cosine 0, any noticeably positive cosine stands out: in 1,536 dimensions, a cosine of 0.3 or 0.4 is far above what chance produces, even though it would look small on a 2-D diagram.

That's also why the absolute numbers differ between embedding models. Each model spreads its texts differently, so "similar" might mean 0.8 in one model and 0.4 in another. Judge cosines by comparing them with each other, or with a few known pairs, rather than against a fixed cutoff. Many vector databases also scale every embedding to length 1 before storing it; then the plain dot product equals the cosine, which is cheaper to compute.

Quick check
In a 1,536-dimension embedding space, two support tickets have a cosine similarity of 0.42. As in the dot product lesson, what should you make of it?
  1. Nothing: anything under 0.9 means unrelated
  2. They're nearly opposite in meaning
  3. Cosines can't be used in that many dimensions
  4. It's notable: random directions there sit near 0
Show the answer

It's notable: random directions there sit near 0

Random pairs in that many dimensions have cosines within a few hundredths of 0, so 0.42 is far above chance. Compare it with other pairs from the same model to judge how similar the tickets are.

The curse of dimensionality

Each new feature multiplies the amount of space data has to fill. Cut every feature into 10 bins: 1 feature makes 10 cells, 2 make 100, and 6 make a million. A million data points would put 100,000 in each cell of a 1-feature grid but about 1 in each cell of a 6-feature grid. In high dimensions, data is always sparse: most of the space is empty, and every point is far from every other.

That's the curse of dimensionality: methods that rely on nearby examples, like k-nearest neighbors, need far more data as features grow; models can find patterns in the emptiness that aren't real; and pictures are impossible. Dimensionality reduction, like PCA, helps by keeping the few directions where the data actually varies. Collecting more data helps less than you'd hope: each extra feature would need about ten times as much to keep the grid as full.

Drill

How thin does the data get?

You have a million data points, and you cut every feature into 10 bins. Set per_cell to a list with the average number of points per cell for 1, 2, 3 and 6 features (a million divided by 10 to the power of the number of features).

points = 1_000_000
features = [1, 2, 3, 6]

per_cell = []

print(per_cell)
In the lesson, your code is checked as you write it.

High dimensions in AI

Embedding models produce vectors with hundreds or thousands of numbers, and vector databases find neighbors among millions of them with approximate methods, because exact search through a sparse, high-dimensional space is slow. A surprising result, the Johnson-Lindenstrauss lemma, says you can project high-dimensional points onto far fewer random directions and still keep their distances nearly intact, which is why random projections and compressed embeddings work at all. And inside a transformer, attention scores are divided by the square root of the dimension, because dot products of high-dimensional vectors grow with the dimension and would otherwise be too large.

Quick check
Two vectors point the same way, one longer than the other. What does this print?
a = [2, 0]
b = [5, 0]
dot = sum(x * y for x, y in zip(a, b))
print(dot / ((sum(x * x for x in a) ** 0.5) * (sum(y * y for y in b) ** 0.5)))
  1. 0.4
  2. 2.5
  3. 1.0
  4. 10.0
Show the answer

1.0

Cosine similarity ignores length: 10 / (2 × 5) = 1.0, because the vectors point the same way. That's why it suits embeddings, whose directions carry the meaning.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 6-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Two random directions in 1,000 dimensions typically have a cosine of about what?

  1. Close to 0
  2. Close to 1
  3. Close to −1
  4. About 0.5

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson