Welcome to the seventh post in our series on Linear Algebra for Machine Learning, kicking off Part II: Core Theorems and Algorithms! After exploring norms and distances, we now dive into orthogonality and projections, fundamental concepts for error decomposition, principal component analysis (PCA), and embeddings in machine learning (ML). In this post, we’ll cover the mathematical foundations, their ML applications, and how to implement them in Python using NumPy and PyTorch. We’ll include visualizations, the Gram-Schmidt process, and Python exercises to solidify your understanding.
Two vectors are orthogonal if their dot product is zero:
Geometrically, orthogonal vectors are perpendicular (form a 90° angle). A set of vectors is orthonormal if they are pairwise orthogonal and each has a unit length (). For an orthonormal set :
The projection of a vector onto a vector is the vector along ’s direction that is closest to :
(Note: The subscript 2 in indicates the L2 norm, which is the Euclidean norm. See more on norms.)
If is a unit vector (), this simplifies to:
The vector is orthogonal to , enabling error decomposition.
The Gram-Schmidt process transforms a set of linearly independent vectors into an orthonormal set :
Start with .
For each :
Compute the orthogonal vector:
Normalize: .
This produces an orthonormal basis for the span of the original vectors.
Orthogonality and projections are central to ML:
These concepts enable efficient and interpretable ML models.
Let’s implement orthogonality checks, projections, and the Gram-Schmidt process using NumPy and PyTorch, with visualizations to illustrate their geometry.
Install the required libraries if needed:
pip install numpy torch matplotlib
Let’s verify orthogonality for two vectors:
import numpy as np
import matplotlib.pyplot as plt
# Define two vectors
u = np.array([1, 0])
v = np.array([0, 1])
# Compute dot product
dot_product = np.dot(u, v)
# Print results
print("Vector u:", u)
print("Vector v:", v)
print("Dot product u · v:", dot_product)
print("Orthogonal?", np.isclose(dot_product, 0, atol=1e-10))
# Visualize vectors
def plot_2d_vectors(vectors, labels, colors, title):
plt.figure(figsize=(6, 6))
origin = np.zeros(2)
for vec, label, color in zip(vectors, labels, colors):
plt.quiver(*origin, *vec, color=color, scale=1, scale_units='xy', angles='xy')
plt.text(vec[0], vec[1], label, color=color, fontsize=12)
plt.grid(True)
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.axhline(0, color='black', linewidth=0.5)
plt.axvline(0, color='black', linewidth=0.5)
plt.xlabel('X')
plt.ylabel('Y')
plt.title(title)
plt.show()
plot_2d_vectors(
[u, v],
['u', 'v'],
['blue', 'red'],
"Orthogonal Vectors"
)
Output:
Vector u: [1 0]
Vector v: [0 1]
Dot product u · v: 0
Orthogonal? True
This confirms and are orthogonal (dot product = 0) and visualizes their perpendicularity.
Let’s project a vector onto another:
# Define vectors
u = np.array([1, 2])
v = np.array([3, 1])
# Compute projection of u onto v
dot_uv = np.dot(u, v)
norm_v_squared = np.sum(v**2)
projection = (dot_uv / norm_v_squared) * v
# Print results
print("Vector u:", u)
print("Vector v:", v)
print("Projection of u onto v:", projection)
# Visualize
plot_2d_vectors(
[u, v, projection],
['u', 'v', 'proj_v(u)'],
['blue', 'red', 'green'],
"Projection of u onto v"
)
Output:
Vector u: [1 2]
Vector v: [3 1]
Projection of u onto v: [1.5 0.5]
This computes , projecting onto , and plots the result along ’s direction.
Let’s apply Gram-Schmidt to create an orthonormal basis:
# Define two linearly independent vectors
v1 = np.array([1, 1])
v2 = np.array([1, 0])
# Gram-Schmidt process
u1 = v1 / np.linalg.norm(v1) # Normalize v1
w2 = v2 - np.dot(v2, u1) * u1 # Orthogonalize v2
u2 = w2 / np.linalg.norm(w2) # Normalize w2
# Verify orthonormality
print("Orthonormal basis:")
print("u1:", u1)
print("u2:", u2)
print("u1 · u2:", np.dot(u1, u2))
print("Norm u1:", np.linalg.norm(u1))
print("Norm u2:", np.linalg.norm(u2))
# Visualize
plot_2d_vectors(
[v1, v2, u1, u2],
['v1', 'v2', 'u1', 'u2'],
['blue', 'red', 'green', 'purple'],
"Gram-Schmidt Orthonormal Basis"
)
Output:
Orthonormal basis:
u1: [0.70710678 0.70710678]
u2: [ 0.70710678 -0.70710678]
u1 · u2: 0.0
Norm u1: 1.0
Norm u2: 1.0
This applies Gram-Schmidt to , producing orthonormal vectors , and verifies orthogonality (dot product ≈ 0) and unit length.
Let’s compute a projection in PyTorch:
import torch
# Convert to PyTorch tensors
u_torch = torch.tensor([1.0, 2.0])
v_torch = torch.tensor([3.0, 1.0])
# Compute projection
dot_uv_torch = torch.dot(u_torch, v_torch)
norm_v_squared_torch = torch.sum(v_torch**2)
projection_torch = (dot_uv_torch / norm_v_squared_torch) * v_torch
# Print result
print("PyTorch projection of u onto v:", projection_torch.numpy())
Output:
PyTorch projection of u onto v: [1.5 0.5]
This confirms PyTorch’s projection matches NumPy’s.
Try these Python exercises to deepen your understanding. Solutions will be discussed in the next post!
In the next post, we’ll explore matrix inverses and systems of equations, crucial for solving linear models and understanding backpropagation. We’ll provide more Python code and exercises to keep building your ML expertise.
Happy learning, and see you in Part 8!