import numpy as onp
def angle(v, w):
return onp.arccos(v.dot(w) / (onp.linalg.norm(v) * onp.linalg.norm(w)))
angle(onp.array([0, 1, 2]), onp.array([2, 3, 4]))Dive into Deep Learning · §23.1
The geometry under the algebra
angles, projections, hyperplanes, and how matrices move space.
Motivation
A vector is a list of numbers, but reading it as a picture is where the intuition lives.
These pictures are the foundation for the eigendecomposition and the SVD in the sections that follow.
01
Vectors and their geometry
points, directions, dot products, projection
Vectors
The same array names a point (its coordinates) or a direction (an arrow that may start anywhere). Deep learning mostly uses the direction view.
Reading a vector as a direction makes addition visual: follow one arrow, then the next, tip to tail.
Vectors
Why is the dot product tied to the angle? The two vectors span a plane, so measure \|\mathbf{v}-\mathbf{w}\|^2 two ways and equate.
Algebra gives \|\mathbf{v}\|^2 - 2\,\mathbf{v}\cdot\mathbf{w} + \|\mathbf{w}\|^2; the law of cosines gives \|\mathbf{v}\|^2 + \|\mathbf{w}\|^2 - 2\,\|\mathbf{v}\|\,\|\mathbf{w}\|\cos\theta.
Cancel the common terms: \;\mathbf{v}\cdot\mathbf{w} = \|\mathbf{v}\|\,\|\mathbf{w}\|\cos\theta, so \theta=\arccos\!\big(\mathbf{v}\cdot\mathbf{w}/\|\mathbf{v}\|\,\|\mathbf{w}\|\big).
0.4189900840328574
Vectors
\arccos only accepts inputs in [-1, 1], so we need a guarantee.
Cauchy–Schwarz. \;|\mathbf{v}\cdot\mathbf{w}| \le \|\mathbf{v}\|\,\|\mathbf{w}\|, with equality iff \mathbf{v} and \mathbf{w} are collinear.
The proof needs only one fact: a squared length is never negative, so q(t) = \|\mathbf{v} - t\mathbf{w}\|^2 \ge 0 is a parabola that cannot dip below zero, forcing its discriminant \le 0. Dividing through shows the cosine never leaves [-1, 1], so \theta is always a genuine angle.
Vectors
How much of \mathbf{v} points along \mathbf{w}? Drop \mathbf{v} onto the line through \mathbf{w}:
\operatorname{proj}_{\mathbf{w}}\mathbf{v} = \frac{\mathbf{v}\cdot\mathbf{w}}{\mathbf{w}\cdot\mathbf{w}}\,\mathbf{w}.
The residual \mathbf{r} leaves at a right angle, so Cauchy–Schwarz is just “a leg is no longer than the hypotenuse.” Two vectors are orthogonal when \mathbf{v}\cdot\mathbf{w} = 0.
This is least squares in one dimension (the best fit leaves an orthogonal residual); the SVD scales the same idea to any matrix.
Vectors
The same formula, one matrix heavier: stack an orthonormal basis of a subspace S as the columns of \mathbf{Q} (so \mathbf{Q}^\top\mathbf{Q} = \mathbf{I}), and \mathbf{P} = \mathbf{Q}\mathbf{Q}^\top drops any vector onto S.
\mathbf{P}\mathbf{x} is the unique closest point of S; the residual \mathbf{x} - \mathbf{P}\mathbf{x} is orthogonal to all of S; and \mathbf{P}^2 = \mathbf{P}: projecting twice changes nothing.
qr manufactures the orthonormal basis. Both claims check to roundoff on a random 3-dimensional subspace of \mathbb{R}^5, and residual-orthogonality is least squares: the optimal fit’s error is orthogonal to every feature.
import numpy as onp
# A random 5x3 matrix whose columns span a 3-dim subspace of R^5
onp.random.seed(0)
A = onp.random.randn(5, 3)
Q, _ = onp.linalg.qr(A) # orthonormal basis of the column space
P = Q.dot(Q.T) # projection matrix onto that subspace
x = onp.random.randn(5)
r = x - P.dot(x) # residual
(onp.linalg.norm(P.dot(P) - P), # P^2 = P (idempotent)
onp.linalg.norm(Q.T.dot(r))) # residual is orthogonal to the subspace(4.153077993666847e-16, 4.211628519682908e-16)
Vectors
Two subspaces ride on every matrix: the column space is what it can produce, the null space is what it sends to zero.
02
Similarity in high dimensions
cosine similarity and near-orthogonality
High dimensions
Comparing direction, not magnitude, is often what we want: an image and a dimmed copy point the same way, so they should score as identical.
\cos\theta = \frac{\mathbf{v}\cdot\mathbf{w}}{\|\mathbf{v}\|\,\|\mathbf{w}\|} \;\in\; [-1, 1].
This is the signal behind embedding retrieval, the scaled dot products inside attention, and the alignment objective of contrastive learning.
High dimensions
Drop two unrelated unit vectors into \mathbb{R}^d: what cosine do we expect?
For a random unit vector, \;\mathbb{E}[\cos\theta] = 0 and \operatorname{Var}(\cos\theta) = \tfrac{1}{d}.
Why: rotate so \mathbf{u}=\mathbf{e}_1, so \cos\theta=v_1. Symmetry \mathbf{v}\mapsto-\mathbf{v} kills the mean; \sum_i v_i^2=1 with all coordinates alike gives \mathbb{E}[v_1^2]=\tfrac{1}{d}.
So the cosine concentrates at 0 with width 1/\sqrt{d}: a cosine well above 0 is unlikely to be an accident, which is why cosine similarity works.
High dimensions
Attention compares one query against thousands of keys by dot product, and near-orthogonality is its operating environment: the many unrelated keys score near zero, so the few that share structure with the query stand out against a quiet background.
The mysterious \sqrt{d} falls out of our variance result. With entries of typical size 1, \|\mathbf{q}\| \approx \|\mathbf{k}\| \approx \sqrt{d} while \operatorname{sd}(\cos\theta) = 1/\sqrt{d}, so
\operatorname{sd}(\mathbf{q}\cdot\mathbf{k}) \approx \sqrt{d}\cdot\sqrt{d}\cdot\tfrac{1}{\sqrt{d}} = \sqrt{d}.
\mathbf{Q}\mathbf{K}^\top/\sqrt{d} is standardization: dividing the raw scores by their standard deviation keeps them O(1), so the softmax stays in its responsive range instead of saturating.
03
Hyperplanes and decision boundaries
the primitive every classifier shares
Hyperplanes
The set \{\mathbf{x} : \mathbf{w}\cdot\mathbf{x} = b\} is a plane with normal \mathbf{w}, sitting at distance b/\|\mathbf{w}\| from the origin. Sliding b translates it without rotating.
For any point, the signed distance (\mathbf{w}\cdot\mathbf{x} - b)/\|\mathbf{w}\| is positive on one side and negative on the other. That number is exactly a linear classifier’s margin.
Hyperplanes
Take Fashion-MNIST t-shirts and trousers. Average each class: the means are blurry but recognizable. The line between them, \mathbf{w} = \overline{\mathbf{x}}_1 - \overline{\mathbf{x}}_0, is our normal.
Hyperplanes
Classify each 784-dimensional image by the side it falls on, with the threshold at the midpoint of the two means’ projections:
import numpy as onp
def as_numpy(x):
if hasattr(x, 'asnumpy'): # MXNet
return x.asnumpy()
if hasattr(x, 'detach'): # PyTorch
return x.detach().cpu().numpy()
if hasattr(x, 'numpy'): # TensorFlow
return x.numpy()
return onp.asarray(x) # JAX / NumPy
X_test_np, y_test_np = as_numpy(X_test), as_numpy(y_test)
ave_0_np, ave_1_np = as_numpy(ave_0), as_numpy(ave_1)
# Normal = difference of class means; threshold = midpoint of their projections
w = (ave_1_np - ave_0_np).ravel()
b = onp.dot(w, (ave_0_np + ave_1_np).ravel()) / 2
predictions = X_test_np.reshape(len(X_test_np), -1).dot(w) > b
# Accuracy
onp.mean(predictions.astype(y_test_np.dtype) == y_test_np, dtype=onp.float64)0.922
Over 2{,}000 test images, this hand-built rule is right about 92% of the time, and nothing was learned.
Hyperplanes
Reduce every image to one number, \mathbf{w}\cdot\mathbf{x}, its position along the normal. The two classes form two humps; the dashed threshold cuts between them.
A learned classifier just tilts this boundary to trim the overlap; a deep net learns features that pull the humps apart.
04
Matrices as linear maps
skew, rotate, scale, and the determinant
Linear maps
A matrix is fixed by where it sends the basis vectors, namely its columns. Every other vector follows as a weighted sum, so the entire grid is carried along: lines stay lines, the origin stays put, cells stay evenly spaced.
Matrices cannot bend space, only skew, rotate, and scale it. Multiplying two matrices composes their maps.
Linear maps
\mathbf{B} = \bigl[\begin{smallmatrix}2&-1\\4&-2\end{smallmatrix}\bigr] has dependent columns, \mathbf{b}_1 + 2\,\mathbf{b}_2 = \mathbf{0}. The whole plane lands on one line (the column space), and the direction (1,2)^\top is crushed to the origin (the null space). Once inputs collide, no inverse can tell them apart.
Rank–nullity. \operatorname{rank}\mathbf{A} + \dim\ker\mathbf{A} = n: of the n directions coming in, \dim\ker\mathbf{A} are destroyed and \operatorname{rank}\mathbf{A} survive. For \mathbf{B}: 1 + 1 = 2.
Linear maps
A square matrix is orthogonal when \mathbf{Q}^\top\mathbf{Q} = \mathbf{I}. Such maps preserve every dot product, hence all lengths and angles:
(\mathbf{Q}\mathbf{x})\cdot(\mathbf{Q}\mathbf{y}) = \mathbf{x}^\top\mathbf{Q}^\top\mathbf{Q}\,\mathbf{y} = \mathbf{x}\cdot\mathbf{y}.
They are the pure rotations and reflections: the distortion-free building blocks of both the spectral theorem and the SVD.
Linear maps
The unit square maps to a parallelogram; its signed area is \det\mathbf{A}. The sign records orientation, and a zero determinant means space was crushed to a lower dimension.
For our grid matrix, \det = 1\cdot3 - 2\cdot(-1) = 5:
4.999999999999999
Composed maps multiply their area scalings, \det(\mathbf{A}\mathbf{B})=\det\mathbf{A}\,\det\mathbf{B}. Two payoffs: \det\mathbf{Q}=\pm1 for orthogonal \mathbf{Q}, and (next section) \det\mathbf{A}=\prod_i\lambda_i, the per-axis stretches multiplied.
Linear maps
Three properties pin the determinant down in every dimension, as a function of the columns: multilinear (linear in each column), alternating (zero when two columns are equal), normalized (\det\mathbf{I} = 1). Exactly one such function exists.
Consequences: swapping two columns flips the sign, and a triangular determinant is the product of the diagonal:
import numpy as onp
A = onp.array([[2, 1, 0], [1, 3, 1], [0, 1, 2]])
A_swap = onp.array([[1, 2, 0], [3, 1, 1], [1, 0, 2]]) # first two columns swapped
T = onp.array([[2, 5, 1], [0, 3, 4], [0, 0, 1]]) # upper triangular
(onp.linalg.det(A), # 8, matching the cofactor expansion
onp.linalg.det(A_swap), # -8: a column swap flips the sign
onp.linalg.det(T)) # 6 = 2 * 3 * 1, the diagonal product(8.000000000000002, -8.000000000000002, 6.0)
\det(\mathbf{A}\mathbf{B}) = \det\mathbf{A}\,\det\mathbf{B} = \det(\mathbf{B}\mathbf{A}): determinants commute even when matrices do not.
Linear maps
For a square matrix: \det\mathbf{A} = 0 \iff the columns are linearly dependent \iff \mathbf{A} is not invertible.
Our collapsing \mathbf{B} scores 2\cdot(-2) - (-1)\cdot 4 = 0, exactly as it must.
When invertible, \mathbf{A}^{-1} undoes the map; the 2\times2 formula checks out, multiplying back to the identity:
array([[1., 0.],
[0., 1.]])
Prefer linalg.solve(A, b) over inv(A) @ b: stabler, and it never forms the inverse.
05
Einstein summation
one rule for every product
Einstein notation
Dot products, matrix–vector, matrix–matrix, and traces are one pattern, (\mathbf{A}\mathbf{B})_{ik} = a_{ij}\,b_{jk}: multiply entries, sum the repeated index. einsum makes that index string the whole definition (the matrix–vector call recovers our worked \mathbf{A}\mathbf{v} = [0, -5]^\top):
Wrap-up
These pictures carry straight into the eigendecomposition and the SVD, and all the way up to attention and embeddings.