Skip to main content

Why Your Embeddings Fail: The Geometry of Cosine Similarity

Why Your Embeddings Fail: The Geometry of Cosine Similarity

Topic Overview

What is cosine similarity? At its core, it is a metric that tells you how aligned two vectors are, completely ignoring their size.

Why does it exist? In modern AI, we map complex data—like paragraphs of text, images, or audio—into continuous high-dimensional vectors called embeddings. When we compare these embeddings, we usually want to know if they represent the same concept, not whether they have the same length.

What problem does it solve? Consider comparing a 10-word summary of a concept to a 1,000-word textbook chapter on that same concept. If we use standard distance metrics, the chapter looks “further away” from the summary simply because it contains more words, resulting in a larger vector magnitude. Cosine similarity solves this by stripping away magnitude and looking strictly at the angle between the vectors. An angle of 0 degrees means perfect alignment (similarity of 1).

Why should engineers care? If you build a RAG (Retrieval-Augmented Generation) system, a recommendation engine, or train a contrastive learning model like CLIP, you are fundamentally relying on cosine similarity to retrieve the right documents, match users to items, or align images with text. If you misunderstand this metric, your retrieval will fail, and your models will collapse.


Prerequisites

1. Dot Product * Why it is needed: The dot product is the raw mathematical engine of cosine similarity. It measures the projection of one vector onto another. * Where it appears: It is the exact numerator in the cosine similarity formula. It captures both the magnitude and the directional alignment of the vectors.

2. L2 Norm (Euclidean Length) * Why it is needed: To isolate direction from magnitude, we must divide out the size of the vectors. The L2 norm gives us that exact size. * Where it appears: It forms the denominator of the cosine similarity formula. By dividing the dot product by the product of the two L2 norms, we force the vectors to behave as if they had a length of 1 (unit vectors).


Historical Motivation

In the 1950s and 60s, as computers began handling text, researchers developed the “Vector Space Model” for information retrieval. The problem? Early systems used Boolean logic (keyword A AND keyword B). This was rigid; it failed to handle synonyms.

Researchers started representing documents as vectors of term frequencies. A document about “dogs” might have a vector [10, 0, 0] (the word “dog” appeared 10 times). But a massive 500-page book about dogs would have a vector like [5000, 0, 0].

If you used Euclidean distance to find similar documents, the short summary and the long book would look incredibly far apart. The raw distance was dominated by document length, not topical relevance. They needed a mathematical way to say, “These two documents point in the exact same semantic direction, ignore how many words they contain.” The solution was borrowing the geometric definition of the cosine of an angle from trigonometry and applying it to these term-frequency vectors.


Intuitive Explanation

Imagine you and a friend are standing in a field, and you are both pointing laser pointers at a tall building in the distance.

You are standing very close to the building. Your arm is bent, and the laser beam is short. Your friend is a mile away. Their arm is stretched out straight, and the laser beam is very long.

If we use Euclidean distance (straight-line distance), we measure the distance between the tips of your laser beams. Because your friend is far away, the tips are very far apart. A computer using Euclidean distance would conclude you are pointing at completely different things.

But you aren’t. You are pointing at the exact same building.

Cosine similarity ignores the length of the laser beams. Instead, it measures the angle between your arms. Because you are both pointing at the same building, the angle between your arms is 0 degrees. The cosine of 0 degrees is 1. Cosine similarity declares you to be perfectly aligned.

Conversely, if you point at the building and your friend points at a tree to the left, there is now a wide angle between your arms—say, 90 degrees. The cosine of 90 degrees is 0. Cosine similarity correctly declares you to be completely unrelated.


Visual Understanding

To visualize this, imagine a 2D graph. 1. Draw an origin point \((0,0)\) at the center. 2. Draw Vector A pointing up and to the right, at a 30-degree angle from the horizontal. Make it short (length 2). 3. Draw Vector B pointing up and to the right, at a 35-degree angle from the horizontal. Make it very long (length 10). 4. Draw Vector C pointing straight up, at a 90-degree angle. Make it length 5.

Now, draw a unit circle (a circle with a radius of 1) around the origin. Project the tip of all three vectors onto this circle by drawing a straight line from the origin through the tip, stopping exactly at the circle’s edge.

Notice what happened: Vector A and Vector B now sit right next to each other on the circle’s edge. Their magnitudes (2 and 10) have been completely erased. The only thing that remains is their angle. Because they are only 5 degrees apart, they are highly similar. Vector C is on the opposite side of the circle (90 degrees away from both). The arc-length between A and C is large. Cosine similarity is simply the cosine of the angle formed by those lines on the unit circle.


Mathematical Foundations

Let us derive exactly how we get from the geometry of angles to the formula used in PyTorch.

We start with the Law of Cosines from trigonometry, which relates the lengths of the sides of a triangle to the cosine of one of its angles. If our vectors are \(\mathbf{u}\) and \(\mathbf{v}\), and the vector connecting their tips is \(\mathbf{u} - \mathbf{v}\), the Law of Cosines states:

\[\|\mathbf{u} - \mathbf{v}\|^2 = \|\mathbf{u}\|^2 + \|\mathbf{v}\|^2 - 2\|\mathbf{u}\|\|\mathbf{v}\|\cos(\theta)\]

Here, \(\theta\) is the angle between \(\mathbf{u}\) and \(\mathbf{v}\).

Now, we use the algebraic definition of the L2 norm squared. The squared length of a vector is equal to its dot product with itself: \(\|\mathbf{x}\|^2 = \mathbf{x} \cdot \mathbf{x}\).

Let’s substitute this into the left side of our equation: \[(\mathbf{u} - \mathbf{v}) \cdot (\mathbf{u} - \mathbf{v}) = \|\mathbf{u}\|^2 + \|\mathbf{v}\|^2 - 2\|\mathbf{u}\|\|\mathbf{v}\|\cos(\theta)\]

Expand the left side using the distributive property of the dot product: \[(\mathbf{u} \cdot \mathbf{u}) - (\mathbf{u} \cdot \mathbf{v}) - (\mathbf{v} \cdot \mathbf{u}) + (\mathbf{v} \cdot \mathbf{v})\]

Because the dot product is commutative (\(\mathbf{u} \cdot \mathbf{v} = \mathbf{v} \cdot \mathbf{u}\)), this simplifies to: \[\|\mathbf{u}\|^2 - 2(\mathbf{u} \cdot \mathbf{v}) + \|\mathbf{v}\|^2\]

Now, plug this back into our equation: \[\|\mathbf{u}\|^2 - 2(\mathbf{u} \cdot \mathbf{v}) + \|\mathbf{v}\|^2 = \|\mathbf{u}\|^2 + \|\mathbf{v}\|^2 - 2\|\mathbf{u}\|\|\mathbf{v}\|\cos(\theta)\]

Notice that \(\|\mathbf{u}\|^2\) and \(\|\mathbf{v}\|^2\) appear on both sides. Subtract them from both sides: \[- 2(\mathbf{u} \cdot \mathbf{v}) = - 2\|\mathbf{u}\|\|\mathbf{v}\|\cos(\theta)\]

Divide by \(-2\): \[\mathbf{u} \cdot \mathbf{v} = \|\mathbf{u}\|\|\mathbf{v}\|\cos(\theta)\]

Finally, isolate the cosine term to get our standard formula: \[\cos(\theta) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\|\|\mathbf{v}\|}\]

Symbol breakdown: * \(\mathbf{u} \cdot \mathbf{v}\): The dot product. If vectors point the same direction, this is a large positive number. If opposite, a large negative number. * \(\|\mathbf{u}\|\): The L2 norm (length) of vector \(\mathbf{u}\). Always positive. * \(\|\mathbf{v}\|\): The L2 norm of vector \(\mathbf{v}\). Always positive. * \(\cos(\theta)\): The output, strictly bounded between \([-1, 1]\) due to the Cauchy-Schwarz inequality. 1 means identical direction, 0 means orthogonal (unrelated), -1 means opposite direction.


Worked Numerical Example

Let’s calculate the cosine similarity between two 3-dimensional vectors. Let \(\mathbf{u} = [1, 2, 3]\) Let \(\mathbf{v} = [4, 5, 6]\)

Step 1: Calculate the dot product (\(\mathbf{u} \cdot \mathbf{v}\)) \[\mathbf{u} \cdot \mathbf{v} = (1 \times 4) + (2 \times 5) + (3 \times 6)\] \[\mathbf{u} \cdot \mathbf{v} = 4 + 10 + 18 = 32\]

Step 2: Calculate the L2 norm of \(\mathbf{u}\) (\(\|\mathbf{u}\|\)) \[\|\mathbf{u}\| = \sqrt{1^2 + 2^2 + 3^2} = \sqrt{1 + 4 + 9} = \sqrt{14} \approx 3.7417\]

Step 3: Calculate the L2 norm of \(\mathbf{v}\) (\(\|\mathbf{v}\|\)) \[\|\mathbf{v}\| = \sqrt{4^2 + 5^2 + 6^2} = \sqrt{16 + 25 + 36} = \sqrt{77} \approx 8.7750\]

Step 4: Calculate the denominator (\(\|\mathbf{u}\|\|\mathbf{v}\|\)) \[\|\mathbf{u}\|\|\mathbf{v}\| = 3.7417 \times 8.7750 \approx 32.8316\]

Step 5: Compute Cosine Similarity \[\cos(\theta) = \frac{32}{32.8316} \approx 0.9746\]

Interpretation: Because 0.9746 is very close to 1, these vectors point in almost the exact same direction in 3D space, even though \(\mathbf{v}\) is much longer than \(\mathbf{u}\).


Computational Interpretation

What is the computer actually doing when you ask it to compute cosine similarity for a batch of data?

Data Structures: We are dealing with dense arrays of floating-point numbers (typically float32 or bfloat16 on modern GPUs).

Complexity: For a single pair of vectors of dimension \(d\): * Dot product requires \(d\) multiplications and \(d-1\) additions. \(\mathcal{O}(d)\). * Norm calculation requires \(d\) squarings, \(d-1\) additions, and 1 square root per vector. \(\mathcal{O}(d)\). * Total for one pair: \(\mathcal{O}(d)\).

The Bottleneck (All-Pairs Similarity): In ML, we rarely compare one pair. We compare a batch of \(N\) queries against a batch of \(M\) documents. This creates an \(N \times M\) similarity matrix. * Computational complexity: \(\mathcal{O}(N \cdot M \cdot d)\). * Memory cost: The output matrix requires \(N \times M \times 4\) bytes (for float32). * Engineering Reality: If you are doing RAG and have a vector database with \(M = 10,000,000\) documents, and a batch of \(N = 100\) queries, the output matrix is \(100 \times 10,000,000\), which is 4 Gigabytes. You cannot compute this all-pairs matrix naively. This exact computational bottleneck is why we use Approximate Nearest Neighbor (ANN) algorithms like HNSW (used in FAISS) to search the space without computing every cosine similarity.


Implementation Perspective

Let’s look at how this is implemented, moving from a naive approach to the highly optimized GPU approach.

1. NumPy Implementation (Educational)

import numpy as np

def cosine_similarity_numpy(u, v):
    # u shape: (d,), v shape: (d,)
    dot_product = np.dot(u, v)
    norm_u = np.linalg.norm(u)
    norm_v = np.linalg.norm(v)
    return dot_product / (norm_u * norm_v)

Why this is bad for ML: The division operation at the end is element-wise and slow on GPUs. Furthermore, if norm_u or norm_v is zero, we get a NaN (divide by zero).

2. PyTorch Naive Approach

import torch.nn.functional as F

# u shape: [Batch, Dim], v shape: [Batch, Dim]
sim = F.cosine_similarity(u, v, dim=-1)
# Output shape: [Batch]

Explanation: F.cosine_similarity computes the exact formula under the hood, with an internal eps (like \(10^{-8}\)) added to the denominator to prevent division by zero. It returns a 1D tensor of similarities corresponding to the batch dimension.

3. PyTorch Optimized Approach (What you MUST use in production)

import torch
import torch.nn.functional as F

# u shape: [Batch_Query, Dim] (e.g., 512, 768)
# v shape: [Batch_Doc, Dim]   (e.g., 1024, 768)

# Step 1: L2 Normalize
# p=2 means L2 norm. dim=-1 means normalize across the 768 dimension.
u_norm = F.normalize(u, p=2, dim=-1) 
v_norm = F.normalize(v, p=2, dim=-1)

# Step 2: Matrix Multiplication
# sim_matrix shape: [512, 1024]
sim_matrix = u_norm @ v_norm.T 

Why this is strictly better: 1. Fused Kernels: F.normalize is highly optimized. 2. No Division: Once vectors are on the unit circle (length = 1), the denominator of the cosine formula becomes \(1 \times 1 = 1\). Therefore, the dot product of normalized vectors is exactly the cosine similarity. We replace slow division with a fast matrix multiplication (@). 3. BLAS Optimization: GPU Tensor Cores are explicitly designed to execute @ (GEMM operations) at staggering speeds. 4. Broadcasting: u_norm is [512, 768]. v_norm.T is [768, 1024]. Standard matrix multiplication rules broadcast this perfectly into a [512, 1024] similarity matrix, giving you the similarity of every query to every document in one fast operation.


Where It Appears In AI

AI Component Exact Usage
Embedding Layer The outputs of embedding layers are often L2 normalized during inference specifically so downstream dot products equal cosine similarity.
Contrastive Learning (CLIP) The InfoNCE loss requires computing the cosine similarity between every image embedding and every text embedding in a batch to form a similarity matrix.
Dense Retrieval (RAG) Vector databases (Pinecone, Milvus, FAISS) store L2-normalized embeddings. When a query arrives, it is normalized, and the database performs a maximization of dot product (which is mathematically equivalent to cosine similarity).
Face Recognition Models like ArcFace output an embedding that is explicitly trained by pushing cosine similarities apart based on an angular margin.
Sentence Transformers SBERT models output embeddings where semantic textual similarity (STS) benchmarks are evaluated exclusively using cosine similarity.
Transformer Attention (See Deep Dive) Attention uses scaled dot-product, not cosine similarity. This is a vital distinction.

Deep Dive Into Real Models

CLIP (Contrastive Language-Image Pre-training) CLIP is the quintessential cosine similarity architecture. * You pass an image through a Vision Transformer (ViT). Output shape: [Batch, Dim] (e.g., [256, 768]). * You pass text through a Text Transformer. Output shape: [256, 768]. * Crucial Step: CLIP applies F.normalize(..., p=2, dim=-1) to both outputs. * It then computes logits = image_features @ text_features.T. * Because they are normalized, this matrix contains the exact cosine similarities. A value of 0.25 in cell \((i, j)\) means the angle between image \(i\) and text \(j\) has a cosine of 0.25. CLIP then applies a learned temperature parameter \(\tau\) to scale these logits before Cross-Entropy loss.

Why Self-Attention is NOT Cosine Similarity A common beginner mistake is thinking Attention weights are cosine similarities. They are not. * Attention formula: \(\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\) * Notice what is missing: There is no L2 normalization of \(Q\) and \(K\). * Why does it omit it? In attention, the magnitude of the Query and Key vectors contains meaningful information. A token that is very “confident” or “important” might produce a Query vector with a high L2 norm. If we normalized it (turning it into cosine similarity), we would destroy that magnitude information. The \(\sqrt{d_k}\) scaling is there to prevent dot products from growing too large, but it preserves relative magnitudes.


Failure Modes

1. The Zero-Vector Division Bug * Cause: If an input vector is all zeros (e.g., a text prompt full of [PAD] tokens gets pooled into a zero vector), its L2 norm is 0. * Symptoms: NaN (Not a Number) values infect your loss function or similarity matrix, destroying the model’s weights in the next gradient step. * Solution: Always add an \(\epsilon\) (epsilon) to the denominator, or use PyTorch’s F.normalize which handles this internally.

2. High-Dimensional Concentration (The “Curse of Dimensionality”) * Cause: In extremely high dimensions (e.g., \(d=4096\)), random vectors tend to become almost perfectly orthogonal to each other. The cosine similarity between any two random vectors approaches 0. * Symptoms: Your contrastive loss struggles to separate positive pairs from negative pairs because all negative pairs already have a similarity near 0. The gradients become incredibly weak. * Solution: Models like CLIP solve this by introducing a learned temperature parameter \(\tau\) that sharply scales the similarities before the softmax, artificially expanding the gap between 0.01 and -0.01.

3. Using Cosine Similarity when Magnitude Matters * Cause: Assuming cosine similarity is always the best metric for vector comparison. * Symptoms: In anomaly detection, an anomalous event might have the exact same direction as a normal event, but a massively amplified magnitude (e.g., sensor readings spike to 100x normal levels, but the ratios between sensors stay the same). Cosine similarity will report a similarity of 1.0, completely missing the anomaly. * Solution: Use Euclidean distance or un-normalized dot products when scale/amplitude is semantically meaningful.


Engineering Insights

Tradeoffs: Normalizing inside the model vs. outside If you are building an embedding model for retrieval, do you L2-normalize at the final layer of the neural network, or do you leave the vectors unnormalized and let the vector database handle it? * Insight: You should normalize inside the model architecture (or right before saving to the DB). If you pass unnormalized vectors to a distributed database, every node must compute the norm before searching, wasting compute.

GPU Memory Implications When computing the all-pairs similarity matrix u @ v.T for Contrastive Learning, the matrix size scales as \(N^2\). If you are training on 8 GPUs and your batch size is 32,768 (common in modern CLIP training), the similarity matrix is \(32768 \times 32768\). * In float32, this is 4 Gigabytes per GPU just for the logits matrix. * Engineering solution: Modern frameworks use techniques like “Gradient Checkpointing” on the similarity matrix, or compute the cross-entropy loss in chunks (e.g., split the matrix into 8 sub-matrices) to prevent Out-Of-Memory (OOM) errors on the GPU.


Interview Questions

Beginner: Question: What is the range of cosine similarity, and what do the extreme values mean? Answer: The range is \([-1, 1]\). A value of \(1\) means the vectors point in the exact same direction (angle of 0 degrees). A value of \(0\) means they are orthogonal (90 degrees), implying no relationship. A value of \(-1\) means they point in exact opposite directions (180 degrees).

Intermediate: Question: Why do we use cosine similarity instead of Euclidean distance for comparing text embeddings? Answer: Text embeddings are created by summing or averaging token vectors. Longer texts will naturally have larger magnitudes, even if they discuss the exact same topic as a short text. Euclidean distance is highly sensitive to vector magnitude, so it would incorrectly classify a long essay and a short summary as dissimilar. Cosine similarity divides out magnitude, isolating the semantic direction.

Advanced: Question: How would you efficiently compute the top-10 most similar documents to a query from a database of 100 million documents? Answer: You cannot compute the full cosine similarity matrix (\(100M \times 100M\) is impossible). Instead, you pre-L2-normalize all 100M document vectors at index time. At query time, you L2-normalize the query vector. Because they are normalized, cosine similarity equals the dot product. You then use an Approximate Nearest Neighbor (ANN) index like HNSW (Hierarchical Navigable Small World), which traverses a graph structure to find the vectors with the highest dot products in \(\mathcal{O}(\log N)\) time rather than \(\mathcal{O}(N)\).

Research-Level: Question: In the paper “ArcFace”, they add an angular margin to the cosine similarity target. Why is adding a margin in cosine space more effective than adding a margin in Euclidean space for face recognition? Answer: In Euclidean space, a linear margin corresponds to a fixed distance. However, the distribution of deep features on the hypersphere is not uniform; features become highly concentrated as dimensionality grows. An angular margin in cosine space strictly translates to a geodesic distance margin on the hypersphere’s surface. This enforces a stricter separation on the manifold where the features actually lie, forcing the network to learn more discriminative, tightly clustered identities that are inherently more robust to intra-class variance (like lighting changes).


Research Perspective

Current Limitations: Cosine similarity assumes a spherical geometry. However, real-world semantic relationships are often hierarchical (e.g., a “dog” is an “animal”, which is a “living thing”). On a sphere, “dog” and “animal” might just have a high cosine similarity, failing to capture the strict nested hierarchy.

Modern Improvements: 1. Hyperbolic Similarity: Researchers are increasingly mapping embeddings into hyperbolic space (Poincaré balls) where distance naturally represents hierarchy, replacing cosine similarity with hyperbolic distance. 2. Learned Metrics: Instead of fixed cosine similarity, models like CLIP learn a temperature parameter \(\tau\) that dynamically scales the cosine similarities, effectively learning how “sharp” the angle differences should be. 3. Decoupled Contrastive Learning: Recent research (like DCL) has shown that forcing negative pairs to have a cosine similarity of exactly 0 (by stopping the gradient on the negative terms) actually improves representation learning, proving that the strict mathematical bounds of cosine similarity can sometimes be too restrictive for gradient flows.


Connections

Prerequisites: * Dot Product (Provides the numerator) * L2 Norm (Provides the denominator)

Enables: * Angular Loss Functions (ArcFace, CosFace) * Vector Databases / ANN Search

Used by: * RAG Architectures (Query-Document matching) * CLIP / SigLIP (Vision-Language alignment) * SimCLR / SimCSE (Self-supervised learning)

Extended by: * Scaled Dot-Product Attention (Adds magnitude and \(\sqrt{d_k}\) scaling) * Hyperbolic Geometry (Replaces spherical angle with tree-like distance)

Related Concepts: * Euclidean Distance * Mahalanobis Distance * InfoNCE Loss


Final Mental Model

Imagine a dark room with a central spotlight. Every piece of data (a sentence, an image, a song) is a person holding a flashlight, standing at the exact center of the room, shining their light at a specific point on the spherical wall.

  • Magnitude (Length) is the wattage of the flashlight. A 1000W flashlight and a 10W flashlight pointing at the exact same spot are doing the exact same thing semantically. We don’t care about wattage.
  • Cosine Similarity is simply measuring the distance, in degrees, between the two spots on the wall.

If the spots overlap, similarity is 1. If they are on opposite sides of the room, similarity is -1. In Deep Learning, we L2-normalize the vectors (turning all flashlights into exactly 10W bulbs) and use fast matrix multiplication to measure the angles between millions of flashlights simultaneously.

Comments

Popular posts from this blog

Why Every AI Model is Actually Bayesian

Why Every AI Model is Actually Bayesian Topic Overview Standard probability asks: “What is the chance of this event happening in the entire universe?” Conditional probability asks: “Given that I already know this to be true, what is the chance of that happening?” Bayes’ Theorem is the engineering tool that flips this condition around. It answers the most critical question in machine learning: “Given the data I just observed, what is the probability that my hypothesis is true?” Why does it exist? Because in the real world, we usually know \(P(\text{Data}|\text{Hypothesis})\) —how likely a certain observation is under a specific model—but what we actually want is \(P(\text{Hypothesis}|\text{Data})\) —how likely our model is to be correct given the observation. Bayes’ Theorem is the mathematical bridge between the data we generate and the truths we want to infer. What problem does it solve? It provides a rigorous, mathematically sound mechani...

Why Everything in AI is a Vector ?

Why Everything in AI is a Vector ? Topic Overview In the previous lesson, you learned about the scalar: a single number representing magnitude. But reality is rarely one-dimensional. A single pixel has three color channels (red, green, blue). A word’s meaning has dozens of grammatical and semantic features. A model’s parameters number in the millions. To represent entities that possess multiple attributes simultaneously, we need a mathematical object that holds multiple scalars in a specific order. That object is a vector. A vector \(\mathbf{v} \in \mathbb{R}^n\) is an ordered list of \(n\) scalars. It represents a point, or an arrow from the origin, in \(n\) -dimensional space. Why does it exist? Because we need a mathematical language for things that have more than one degree of freedom. A scalar tells you “how hot.” A vector tells you “where you are” and “which way you’re going.” What problem does it solve? It provides the fundamental d...

Why Your PyTorch Code Keeps Breaking (It’s the Scalars)

Why Your PyTorch Code Keeps Breaking (It’s the Scalars) Topic Overview You are about to learn about the most fundamental object in all of mathematics and machine learning: the scalar. A scalar is a single number. That is the entire definition. But do not confuse simplicity with insignificance. Every neural network, no matter how many billions of parameters it contains, is ultimately optimized by reading and writing scalars. The loss value is a scalar. The learning rate is a scalar. The temperature parameter in softmax is a scalar. The scale factor in attention is a scalar. Every gradient clipping decision is based on a scalar. The final prediction of a classification model is a scalar probability. Why does the scalar exist as a named concept? Because not all numbers are created equal. Some numbers exist as components of larger structures—a coordinate in a vector, an entry in a matrix, a pixel in an image. A scalar is a number that stands alone...