Math for AI course · Lesson 44 of 180
Angles and Cosine Similarity
Compute angles and cosine similarity from vector coordinates, normalize embeddings responsibly, and handle zero vectors in retrieval code.
Two documents can be represented by vectors and . Their raw dot product is , so a dot-product search sees the second vector as much larger. Their directions are identical, though: after dividing by both vector lengths, their cosine similarity is . The scoring rule changes what “similar” means.
By the end of this lesson, you will be able to compute an angle and cosine similarity from coordinates, explain why positive rescaling leaves cosine unchanged, and handle a zero vector without assigning it a direction that it does not have.
Learning outcome: Compute an angle and cosine similarity and handle zero-vector limits.
This lesson combines two earlier ideas: Lesson 039: Norms measure vector size gives us Euclidean lengths, and Lesson 041: Inner products measure alignment gives us dot products. Lesson 043: Distance depends on the chosen geometry then showed why a distance keeps magnitude in the comparison. Section 3.4 of the official Mathematics for Machine Learning book develops the angle relationship. The examples, code, visual, and assessments here are original.
An angle separates direction from length
For two nonzero vectors , write for the angle between them. The usual dot product and Euclidean norm are
The first expression combines matching coordinates into one scalar. The second measures each vector’s Euclidean length. The third divides the alignment score by the two lengths, leaving a directional comparison. The Cauchy–Schwarz inequality guarantees that the final ratio lies between and for nonzero vectors, so the angle is recovered unambiguously in the interval :
This normalized value is cosine similarity. A value near means the vectors point in nearly the same direction; means a right angle under the usual dot-product geometry; and means opposite directions. Those interpretations assume both vectors are nonzero and that we are using the standard Euclidean inner product.
A worked angle from coordinates
Take and . These are two-dimensional vectors, so each has two coordinates and no physical unit is implied. We can calculate every part before applying an inverse cosine.
The intermediate values matter. A dot product of alone does not tell us the angle, because another pair of vectors could have the same dot product but different lengths. Dividing by both norms makes the result comparable across vector magnitudes.
Positive scaling leaves cosine unchanged
Suppose a nonzero vector is stretched by a positive number . Its direction does not change, and the algebra shows why the cosine score stays fixed:
The same cancellation works when is positively scaled. This is the useful feature for directional embedding comparisons: a document vector with the same orientation but a larger norm receives the same cosine score. It is also a modeling choice. If magnitude carries signal about confidence, frequency, or another property of the representation, removing it can discard information.
Explore the angle, score, and lengths together
The explorer starts with two nonzero vectors. Move any coordinate with a mouse, touch input, or arrow keys. The plot shows the same vectors from the origin, while the table repeats the dot product, both norms, cosine similarity, and recovered angle in text. Set either vector to to see the zero-vector boundary.
x = (1, 0.5); y = (0.5, 1.5); dot product = 1.25; norm of x = 1.118; norm of y = 1.581; cosine similarity = 0.707; angle = 0.785 radians (45 degrees). Interpretation: the score reflects partial directional alignment.
| Quantity | Value |
|---|---|
| Dot product x · y | 1.25 |
| Norm of x | 1.118 |
| Norm of y | 1.581 |
| Cosine similarity | 0.707 |
| Angle | 0.785 radians / 45 degrees |
The vector lines use distinct dash patterns and labels, not color alone. The numeric table remains meaningful when the plot is unavailable or motion is reduced.
The plotted angle is an aid to reasoning, not a separate definition. When the controls change, the numerical table recomputes the same formula shown above. A pair that becomes longer without changing direction keeps the cosine value, while rotating either vector changes both the angle and the score.
Use the score carefully in embedding retrieval
An embedding is a vector representation of an item such as a document, query, image, or user. If the intended match is orientation, cosine similarity is a natural score: it compares normalized vectors rather than their raw lengths. The score still depends on the embedding model, preprocessing, corpus, and task; it does not turn “similar” into a universal property of the underlying documents.
The current scikit-learn pairwise-metrics guide describes cosine similarity as an L2-normalized dot product and notes its use for document vectors. Its cosine_similarity API treats rows as samples and returns a matrix of pairwise scores. That shape is easy to lose when moving between one vector and a batch, so make the row convention explicit:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
query = np.array([[3.0, 1.0]]) # shape (1 sample, 2 features)
documents = np.array([
[3.0, 1.0],
[1.0, 2.0],
[30.0, 10.0],
]) # shape (3 samples, 2 features)
scores = cosine_similarity(query, documents)
print(scores.round(4))
# [[1. 0.7071 1. ]]
For a transparent NumPy implementation, the one-dimensional dot product and vector norms are enough. NumPy’s dot documentation defines the 1-D case as the inner product of two vectors, while numpy.linalg.norm accepts an axis argument when an array contains multiple vectors. The guard is part of the implementation rather than a cosmetic check:
import numpy as np
def cosine_similarity(x, y):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
x_norm = np.linalg.norm(x)
y_norm = np.linalg.norm(y)
if x_norm == 0 or y_norm == 0:
raise ValueError('cosine similarity needs two nonzero vectors')
raw = np.dot(x, y) / (x_norm * y_norm)
return np.clip(raw, -1.0, 1.0)
print(cosine_similarity(np.array([3.0, 1.0]), np.array([1.0, 2.0])))
# 0.7071067811865475
The clip keeps a tiny floating-point overshoot from sending arccos outside its domain. It should not be used to hide a larger numerical or data-shape error. For a batch with shape (number_of_samples, number_of_features), compute a norm across axis=1 and keep the resulting one score per row; the current NumPy documentation demonstrates this axis convention for vector norms.
Boundaries that change the interpretation
A zero vector has no angle
If , then and the cosine formula divides by zero. The dot product is still defined, but it does not mean that the zero vector points at a right angle to . A zero vector has no direction, so the angle and cosine similarity are undefined mathematically.
Libraries may choose a policy for this input. The current cosine_similarity example includes a zero row and displays a score of 0.0; that is an API convention to check for the version and data path you use, not a newly defined direction. A production retrieval pipeline should decide whether to reject, filter, or separately route zero embeddings and test that policy explicitly.
Opposite vectors are not “unrelated”
For nonzero , the pair has
The score is maximally negative because the vectors point in opposite directions. A score near means orthogonality under this geometry; it does not automatically mean that two items are semantically unrelated. The interpretation belongs to the representation and task.
Cosine is not the same as distance or a raw dot product
Cosine similarity ignores positive scale. Euclidean distance does not. For and , cosine similarity is , but
The pair is directionally identical and still far apart as points. Conversely, a raw dot product mixes direction with both lengths. Choose cosine when directional agreement is the intended signal; choose a norm-induced distance or a raw score when magnitude should remain part of the decision. The scikit-learn cosine_distances API defines its value as minus cosine similarity, so it preserves the same directional choice rather than recovering Euclidean separation.
The next lesson, Lesson 045: Orthogonality means no linear overlap, focuses on the special case where the inner product is zero and explains what “no linear overlap” means. The Math for AI course hub keeps the dependency sequence visible as each lesson becomes available.
Check your understanding
Question 1
For x = (3, 1) and y = (1, 2), compute the dot product, both L2 norms, cosine similarity, and angle.
Show the step-by-step solution
The dot product is . The norms are and . Therefore . Applying inverse cosine gives radians, or . Each step uses the two-coordinate vectors and the nonzero-norm assumption.
Question 2
Why do x = (1, 2) and y = (10, 20) have different dot products with their own lengths but the same cosine similarity?
Show the step-by-step solution
The vectors satisfy , so . Their norms are and . The normalized score is . The dot product grows with the scale of , while the same scale also appears in its norm and cancels in cosine.
Question 3
Let x = (1, 0) and y = (0, 2). What are their cosine similarity and angle, and what geometric relationship does that show?
Show the step-by-step solution
The dot product is . The norms are and , so the cosine similarity is . Since , the angle is . The vectors are therefore orthogonal in the standard dot-product geometry, even though neither vector is zero.
Question 4
For x = (1, 0) and y = (−4, 0), compute cosine similarity and angle. Why is the score not zero?
Show the step-by-step solution
The dot product is , and the norms are and . Thus . Inverse cosine gives . The vectors lie on the same line but point in opposite directions, so they are maximally negatively aligned rather than perpendicular.
Question 5
What should a cosine-similarity implementation do for x = (0, 0) and y = (2, 3), and why is returning 0 not a mathematical angle?
Show the step-by-step solution
The norm of is , so the denominator is zero. The formula is undefined and has no direction. An implementation can reject the pair, filter the zero vector, or adopt a documented API convention such as returning ; that convention must be tested and must not be described as a right angle.
Question 6
If x = (1, 2), y = (2, 1), and x is replaced by −3x, how does cosine similarity change? Show the sign calculation.
Show the step-by-step solution
First, and , so the original cosine is . With , the numerator is multiplied by , while the norm of is . Therefore the new score is : negative scaling reverses the direction.
Question 7
Why does the NumPy implementation clip the normalized dot product before calling arccos, and what should a large overshoot indicate?
Show the step-by-step solution
Mathematically, Cauchy–Schwarz puts cosine in , but floating-point
arithmetic can produce a value such as after a long or
nearly parallel calculation. Since is only real-valued on that
interval, np.clip(raw, -1.0, 1.0) removes a tiny round-off overshoot. A
large overshoot suggests a data, shape, or numerical-conditioning problem;
clipping should not be used to conceal it.
Question 8
A query is q = (3, 1) and three document vectors are a = (3, 1), b = (1, 2), and c = (30, 10). Compare raw dot-product ranking with cosine-similarity ranking.
Show the step-by-step solution
The raw dot products are , , and , so raw-dot ranking is , then , then . Vector , so . For , . Cosine ranking therefore ties and at the top, followed by . The difference exposes the modeling decision: dot products reward magnitude as well as direction, while cosine removes positive scale.
Sources
- Mathematics for Machine Learning book PDF, Section 3.4
- NumPy documentation: numpy.dot
- NumPy documentation: numpy.linalg.norm
- NumPy documentation: numpy.arccos
- scikit-learn documentation: Pairwise metrics, affinities and kernels
- scikit-learn documentation: cosine_similarity
- scikit-learn documentation: cosine_distances