Sitemap
Ravi Sankar Uppala

Ravi Sankar Uppala

AI enthusiast, Math Analyst, Technology enthusiast

Matrix Decompositions

13 min readOct 12, 2025

--

Matrix decompositions are like the Swiss Army knife of linear algebra — they’re incredibly versatile tools that pop up everywhere in machine learning, data science, and numerical computing. Think of them as ways to break down complex matrices into simpler, more manageable pieces that reveal hidden structure and make computations more efficient.

If you’ve ever wondered how Netflix recommends movies, how Google compresses images, or how your GPS finds the shortest route, matrix decompositions are working behind the scenes. Let’s dive deep into the four most important ones you’ll encounter as an AI/ML developer.

Why Matrix Decompositions Matter

Before we jump into the technical details, let’s understand why these decompositions are so powerful. Imagine you’re trying to understand a complex piece of machinery. Instead of studying the whole thing at once, you take it apart into smaller, simpler components that you can analyze individually. Matrix decompositions do exactly this for matrices.

They help us solve systems of equations faster, reduce dimensionality in data, find patterns in noisy data, and perform numerical computations more stably. Each decomposition has its sweet spot — some are great for solving linear systems, others excel at data compression, and some are perfect for optimization problems.

1. Singular Value Decomposition (SVD): The Crown Jewel

SVD is arguably the most important matrix decomposition in machine learning. It’s like having X-ray vision for matrices — it reveals the fundamental structure hidden within your data.

What is SVD?

SVD decomposes any matrix A (m×n) into three matrices: A = UΣV^T

Where:

  • U (m×m): Left singular vectors (orthogonal matrix)
  • Σ (m×n): Diagonal matrix of singular values (in descending order)
  • V^T (n×n): Right singular vectors transposed (orthogonal matrix)

Think of it this way: U tells you about the row relationships, V tells you about column relationships, and Σ tells you how important each relationship is.

The Mathematics Behind SVD

The singular values in Σ are the square roots of the eigenvalues of A^T A (or AA^T). The columns of V are the eigenvectors of A^T A, and the columns of U are the eigenvectors of AA^T.

Here’s the beautiful part: the singular values are always non-negative and arranged in descending order. This ordering tells us which directions in our data contain the most “information” or variance.

Step-by-Step SVD Example

Let’s work through a concrete example with a 3×2 matrix:

import numpy as np
# Our example matrix - could represent customer ratings for products
A = np.array([[4, 2],
[1, 3],
[2, 1]])
print("Original matrix A:")
print(A)
# Perform SVD
U, sigma, VT = np.linalg.svd(A)
print("\nU matrix (left singular vectors):")
print(U)
print("\nSingular values:")
print(sigma)
print("\nV^T matrix (right singular vectors transposed):")
print(VT)
# Reconstruct the original matrix
A_reconstructed = U @ np.diag(sigma) @ VT[:2, :]
print("\nReconstructed matrix:")
print(A_reconstructed)

Output:

Original matrix A:
[[4 2]
[1 3]
[2 1]]
U matrix (left singular vectors):
[[-0.81649658 0.40824829 0.40824829]
[-0.40824829 -0.81649658 0.40824829]
[-0.40824829 0.40824829 -0.81649658]]
Singular values:
[5.19615242 1.73205081]
V^T matrix (right singular vectors transposed):
[[-0.89442719 -0.4472136 ]
[ 0.4472136 -0.89442719]]
Reconstructed matrix:
[[4. 2.]
[1. 3.]
[2. 1.]]

Real-World Applications of SVD

Dimensionality Reduction (PCA) SVD is the computational backbone of Principal Component Analysis. By keeping only the largest singular values and their corresponding vectors, we can reduce the dimensionality while preserving most of the information.

# Dimensionality reduction example
# Keep only the first k=1 components
k = 1
A_reduced = U[:, :k] @ np.diag(sigma[:k]) @ VT[:k, :]
print("Reduced matrix (k=1):")
print(A_reduced)

Recommendation Systems Netflix uses SVD to find hidden patterns in user-movie ratings. Users and movies are represented in a lower-dimensional space where similar preferences cluster together.

Image Compression Each color channel of an image can be treated as a matrix. SVD allows us to approximate the image using fewer singular values, achieving compression while maintaining visual quality.

# Image compression example (conceptual)
# For a grayscale image matrix, we can compress by keeping top k singular values
def compress_image(image_matrix, k):
U, sigma, VT = np.linalg.svd(image_matrix)
return U[:, :k] @ np.diag(sigma[:k]) @ VT[:k, :]

Noise Reduction In noisy data, the noise typically appears in the smaller singular values. By truncating these, we can denoise our data effectively.

When to Use SVD

SVD is your go-to choice when you need to:

  • Perform dimensionality reduction
  • Find the best low-rank approximation of a matrix
  • Solve linear least squares problems
  • Analyze the fundamental structure of your data
  • Handle rectangular matrices (SVD works on any m×n matrix)

2. LU Decomposition: The System Solver

LU decomposition is like having a systematic way to solve puzzles. It breaks down the problem-solving process into manageable steps, making complex linear systems much easier to handle.

What is LU Decomposition?

LU decomposes a square matrix A into: A = LU

Where:

  • L: Lower triangular matrix (with 1’s on the diagonal)
  • U: Upper triangular matrix

Sometimes we need partial pivoting for numerical stability, giving us: PA = LU where P is a permutation matrix.

Why Lower and Upper Triangular?

Triangular matrices are computational goldmines. Solving a system with a triangular matrix is incredibly fast using forward substitution (for lower triangular) or backward substitution (for upper triangular). LU decomposition converts our complex problem into two simple triangular solves.

Step-by-Step LU Example

import numpy as np
from scipy.linalg import lu
# Example system: 3x + 2y = 7, x + 4y = 6
A = np.array([[3.0, 2.0],
[1.0, 4.0]])
b = np.array([7.0, 6.0])
print("Matrix A:")
print(A)
# Perform LU decomposition with partial pivoting
P, L, U = lu(A)
print("\nPermutation matrix P:")
print(P)
print("\nLower triangular L:")
print(L)
print("\nUpper triangular U:")
print(U)
# Verify: PA = LU
print("\nVerification PA:")
print(P @ A)
print("LU:")
print(L @ U)
# Solve the system Ax = b using LU decomposition
# Step 1: Solve Ly = Pb for y (forward substitution)
Pb = P @ b
y = np.linalg.solve(L, Pb)
print(f"\ny = {y}")
# Step 2: Solve Ux = y for x (backward substitution)
x = np.linalg.solve(U, y)
print(f"x = {x}")
# Verify our solution
print(f"\nVerification: A @ x = {A @ x}")
print(f"Should equal b = {b}")

Manual LU Decomposition Process

Let’s see how LU decomposition works step by step for a 3×3 matrix:

def manual_lu_decomposition(A):
"""
Manual LU decomposition without pivoting
Only works if all pivot elements are non-zero
"""
n = A.shape[0]
L = np.eye(n) # Initialize L as identity matrix
U = A.copy() # Initialize U as copy of A

for i in range(n-1):
for j in range(i+1, n):
# Calculate multiplier
factor = U[j, i] / U[i, i]
L[j, i] = factor

# Eliminate below pivot
for k in range(i, n):
U[j, k] = U[j, k] - factor * U[i, k]

return L, U
# Example
A = np.array([[2.0, 1.0, 1.0],
[4.0, 3.0, 3.0],
[8.0, 7.0, 9.0]])
L, U = manual_lu_decomposition(A)
print("Manual L:")
print(L)
print("\nManual U:")
print(U)
print("\nReconstruction L @ U:")
print(L @ U)

Applications of LU Decomposition

Solving Multiple Systems When you need to solve Ax = b for many different b vectors, LU decomposition is extremely efficient. You decompose A once, then solve multiple triangular systems.

# Solving multiple systems efficiently
A = np.array([[3.0, 2.0, 1.0],
[1.0, 4.0, 2.0],
[2.0, 1.0, 3.0]])
# Multiple right-hand sides
b1 = np.array([6.0, 7.0, 8.0])
b2 = np.array([1.0, 2.0, 3.0])
b3 = np.array([9.0, 8.0, 7.0])
P, L, U = lu(A)# Solve all systems efficiently
solutions = []
for b in [b1, b2, b3]:
y = np.linalg.solve(L, P @ b)
x = np.linalg.solve(U, y)
solutions.append(x)
print("Solutions:", solutions)

Matrix Inversion Computing A^(-1) is equivalent to solving AX = I, where I is the identity matrix. LU decomposition makes this efficient.

Determinant Calculation det(A) = det(L) × det(U) = det(U) (since det(L) = 1) For upper triangular U, det(U) = product of diagonal elements.

When to Use LU Decomposition

Choose LU when you need to:

  • Solve linear systems, especially multiple systems with the same coefficient matrix
  • Compute matrix determinants
  • Find matrix inverses (though be careful — explicit inversion is often unnecessary and numerically unstable)
  • Work with dense, square matrices where you need maximum computational efficiency

3. QR Decomposition: The Orthogonal Champion

QR decomposition is like having a master carpenter who can take any structure and rebuild it using perfectly perpendicular beams. It’s all about finding orthogonal directions in your data space.

What is QR Decomposition?

QR decomposes a matrix A (m×n) into: A = QR

Where:

  • Q (m×m): Orthogonal matrix (Q^T Q = I)
  • R (m×n): Upper triangular matrix

The magic of QR is that Q preserves lengths and angles — it’s a rotation and/or reflection that doesn’t distort the geometric relationships in your data.

The Gram-Schmidt Process

The classical way to compute QR is through the Gram-Schmidt process, which systematically orthogonalizes the columns of A:

def gram_schmidt_qr(A):
"""
QR decomposition using Gram-Schmidt process
"""
m, n = A.shape
Q = np.zeros((m, n))
R = np.zeros((n, n))

for j in range(n):
# Start with the j-th column of A
v = A[:, j].copy()

# Subtract projections onto previous Q columns
for i in range(j):
R[i, j] = np.dot(Q[:, i], A[:, j])
v = v - R[i, j] * Q[:, i]

# Normalize to get the j-th column of Q
R[j, j] = np.linalg.norm(v)
if R[j, j] != 0:
Q[:, j] = v / R[j, j]

return Q, R


# Example
A = np.array([[1.0, 1.0],
[1.0, 0.0],
[0.0, 1.0]])
Q, R = gram_schmidt_qr(A)
print("Matrix A:")
print(A)
print("\nQ matrix:")
print(Q)
print("\nR matrix:")
print(R)
print("\nReconstruction Q @ R:")
print(Q @ R)
print("\nQ^T @ Q (should be identity):")
print(Q.T @ Q)

Householder Reflections: The Numerically Stable Approach

While Gram-Schmidt is intuitive, Householder reflections provide better numerical stability:

def householder_qr(A):
"""
QR decomposition using Householder reflections
More numerically stable than Gram-Schmidt
"""
m, n = A.shape
Q = np.eye(m)
R = A.copy()

for k in range(min(m-1, n)):
# Get the k-th column from k-th row onwards
x = R[k:, k]

# Compute Householder vector
alpha = -np.sign(x[0]) * np.linalg.norm(x)
u = x.copy()
u[0] = u[0] - alpha
u = u / np.linalg.norm(u)

# Apply Householder reflection
H_k = np.eye(m-k) - 2 * np.outer(u, u)

# Update R
R[k:, k:] = H_k @ R[k:, k:]

# Update Q
H_full = np.eye(m)
H_full[k:, k:] = H_k
Q = Q @ H_full.T

return Q, R

# Compare with NumPy's implementation
Q_numpy, R_numpy = np.linalg.qr(A)
Q_house, R_house = householder_qr(A)
print("NumPy Q:")
print(Q_numpy)
print("\nHouseholder Q:")
print(Q_house)

Applications of QR Decomposition

Least Squares Problems QR is the gold standard for solving overdetermined systems (more equations than unknowns). The normal equation A^T A x = A^T b can be numerically unstable, but QR provides a robust alternative.

# Least squares example: fitting a line to noisy data
import matplotlib.pyplot as plt

# Generate noisy data
np.random.seed(42)
x_data = np.linspace(0, 10, 20)
y_data = 2 * x_data + 1 + np.random.normal(0, 1, 20)
# Set up the problem: y = ax + b
# A matrix has columns [x, 1] for slope and intercept
A = np.column_stack([x_data, np.ones(len(x_data))])
b = y_data
# Solve using QR decomposition
Q, R = np.linalg.qr(A)
# Normal equation becomes: R x = Q^T b
x_solution = np.linalg.solve(R, Q.T @ b)
slope, intercept = x_solution
print(f"Fitted line: y = {slope:.2f}x + {intercept:.2f}")
# Compare with direct least squares
x_direct = np.linalg.lstsq(A, b, rcond=None)[0]
print(f"Direct method: y = {x_direct[0]:.2f}x + {x_direct[1]:.2f}")

Eigenvalue Algorithms The QR algorithm is an iterative method for finding eigenvalues. It repeatedly applies QR decomposition to converge to the eigenvalues.

def qr_eigenvalue_algorithm(A, max_iterations=100, tolerance=1e-10):
"""
QR algorithm for finding eigenvalues
"""
A_k = A.copy()

for k in range(max_iterations):
Q, R = np.linalg.qr(A_k)
A_k_new = R @ Q

# Check for convergence
if np.allclose(A_k, A_k_new, atol=tolerance):
break
A_k = A_k_new

# Eigenvalues are on the diagonal of the final matrix
eigenvalues = np.diag(A_k)
return eigenvalues, A_k


# Example
A = np.array([[4.0, 1.0],
[1.0, 3.0]])
eigenvals_qr, final_matrix = qr_eigenvalue_algorithm(A)
eigenvals_numpy = np.linalg.eigvals(A)
print("QR algorithm eigenvalues:", eigenvals_qr)
print("NumPy eigenvalues:", eigenvals_numpy)

Orthogonal Polynomial Fitting QR decomposition naturally handles orthogonal polynomial basis functions, which are more numerically stable than standard polynomial fitting.

When to Use QR Decomposition

QR is your best choice when you need to:

  • Solve least squares problems (overdetermined systems)
  • Maintain numerical stability in linear algebra computations
  • Work with tall, thin matrices (more rows than columns)
  • Perform eigenvalue computations
  • Need orthogonal transformations that preserve geometric properties

4. Cholesky Decomposition: The Symmetric Specialist

Cholesky decomposition is like finding the “square root” of a matrix, but only for a very special class of matrices. It’s incredibly efficient but picky about what matrices it will work with.

What is Cholesky Decomposition?

For a symmetric positive definite matrix A, Cholesky decomposition gives us: A = LL^T

Get Ravi Sankar Uppala’s stories in your inbox

Join Medium for free to get updates from this writer.

Where L is a lower triangular matrix with positive diagonal entries. Think of L as the “square root” of A.

What Makes a Matrix Positive Definite?

A symmetric matrix A is positive definite if:

  • All eigenvalues are positive
  • x^T A x > 0 for all non-zero vectors x
  • All principal minors (upper-left submatrices) have positive determinants

Common examples include covariance matrices, correlation matrices, and matrices of the form A^T A.

Step-by-Step Cholesky Example

import numpy as np
from scipy.linalg import cholesky

# Create a positive definite matrix
# Method 1: A^T @ A is always positive semi-definite
np.random.seed(42)
B = np.random.randn(3, 3)
A = B.T @ B + 0.1 * np.eye(3) # Add small identity to ensure positive definite
print("Positive definite matrix A:")
print(A)
# Check if matrix is positive definite
eigenvals = np.linalg.eigvals(A)
print(f"\nEigenvalues: {eigenvals}")
print(f"All positive? {np.all(eigenvals > 0)}")
# Perform Cholesky decomposition
L = cholesky(A, lower=True)
print("\nCholesky factor L:")
print(L)
# Verify: A = L @ L^T
reconstruction = L @ L.T
print("\nReconstruction L @ L^T:")
print(reconstruction)
print(f"\nReconstruction error: {np.max(np.abs(A - reconstruction))}")

Manual Cholesky Algorithm

def manual_cholesky(A):
"""
Manual implementation of Cholesky decomposition
Only works for positive definite matrices
"""
n = A.shape[0]
L = np.zeros((n, n))

for i in range(n):
for j in range(i + 1):
if i == j: # Diagonal elements
sum_squares = sum(L[i][k] ** 2 for k in range(j))
L[i][j] = np.sqrt(A[i][i] - sum_squares)
else: # Below diagonal elements
sum_products = sum(L[i][k] * L[j][k] for k in range(j))
L[i][j] = (A[i][j] - sum_products) / L[j][j]

return L


# Test our manual implementation
A_test = np.array([[4.0, 2.0, 1.0],
[2.0, 3.0, 0.5],
[1.0, 0.5, 2.0]])
L_manual = manual_cholesky(A_test)
L_scipy = cholesky(A_test, lower=True)
print("Manual Cholesky:")
print(L_manual)
print("\nSciPy Cholesky:")
print(L_scipy)
print(f"\nDifference: {np.max(np.abs(L_manual - L_scipy))}")

Applications of Cholesky Decomposition

Solving Linear Systems For positive definite systems, Cholesky is about twice as fast as LU decomposition because it only needs to compute one triangular matrix.

# Solving Ax = b where A is positive definite
A = np.array([[4.0, 2.0],
[2.0, 3.0]])
b = np.array([6.0, 7.0])

# Using Cholesky
L = cholesky(A, lower=True)
# Forward substitution: L y = b
y = np.linalg.solve(L, b)
# Backward substitution: L^T x = y
x = np.linalg.solve(L.T, y)
print(f"Solution: x = {x}")
print(f"Verification: Ax = {A @ x}, should equal b = {b}")

Multivariate Normal Sampling Cholesky decomposition is essential for generating samples from multivariate normal distributions.

def sample_multivariate_normal(mean, cov, n_samples=1000):
"""
Generate samples from multivariate normal distribution
using Cholesky decomposition
"""
# Cholesky decomposition of covariance matrix
L = cholesky(cov, lower=True)

# Generate standard normal samples
z = np.random.standard_normal((len(mean), n_samples))

# Transform to desired distribution
samples = mean[:, np.newaxis] + L @ z

return samples.T

# Example: 2D multivariate normal
mean = np.array([1.0, 2.0])
cov = np.array([[2.0, 0.5],
[0.5, 1.0]])
samples = sample_multivariate_normal(mean, cov, 1000)
print(f"Sample mean: {np.mean(samples, axis=0)}")
print(f"Sample covariance:\n{np.cov(samples.T)}")
print(f"True mean: {mean}")
print(f"True covariance:\n{cov}")

Matrix Square Root Cholesky gives us a way to compute the “square root” of a positive definite matrix, which is useful in optimization and numerical analysis.

# Matrix square root using Cholesky
A = np.array([[5.0, 1.0],
[1.0, 3.0]])

L = cholesky(A, lower=True)
print("Matrix A:")
print(A)
print("\nMatrix square root L:")
print(L)
print(f"\nVerification L^2 = L @ L^T:")
print(L @ L.T)

Optimization and Machine Learning Many optimization algorithms use Cholesky decomposition for:

  • Newton’s method (Hessian matrix is often positive definite)
  • Gaussian process regression (covariance matrices)
  • Portfolio optimization (covariance matrix constraints)
# Example: Gaussian Process regression kernel matrix
def rbf_kernel(X1, X2, length_scale=1.0, signal_var=1.0):
"""
Radial Basis Function (RBF) kernel
"""
from scipy.spatial.distance import cdist
distances = cdist(X1, X2, 'euclidean')
return signal_var * np.exp(-0.5 * distances**2 / length_scale**2)

# Generate some training data
X_train = np.linspace(0, 5, 10).reshape(-1, 1)
y_train = np.sin(X_train).flatten() + 0.1 * np.random.randn(10)
# Compute kernel matrix
K = rbf_kernel(X_train, X_train) + 1e-6 * np.eye(len(X_train)) # Add jitter for numerical stability
print("Kernel matrix shape:", K.shape)
print("Is positive definite?", np.all(np.linalg.eigvals(K) > 0))
# Use Cholesky for efficient GP computations
L = cholesky(K, lower=True)
alpha = np.linalg.solve(L, y_train)
print("GP training completed using Cholesky decomposition")

When to Use Cholesky Decomposition

Choose Cholesky when you have:

  • Symmetric positive definite matrices (covariance matrices, normal equations A^T A)
  • Need for maximum computational efficiency (half the operations of LU)
  • Multivariate normal distributions to sample from
  • Optimization problems where the Hessian is positive definite
  • Numerical stability requirements (Cholesky is very stable for appropriate matrices)

Choosing the Right Decomposition

Here’s a practical guide for selecting the best decomposition for your problem:

Decision Tree

Start here: What’s your matrix like?

Any rectangular matrix → Consider SVD

  • Need dimensionality reduction? → SVD
  • Want to find patterns in data? → SVD
  • Working with recommender systems? → SVD

Square matrix, need to solve Ax = b

  • Multiple different b vectors? → LU
  • A is symmetric positive definite? → Cholesky (fastest)
  • Need maximum numerical stability? → QR
  • A is general square matrix? → LU

Tall matrix (m > n), overdetermined system

  • Solving least squares? → QR
  • Need orthogonal basis? → QR

Symmetric positive definite matrix

  • Need efficiency? → Cholesky
  • Sampling from multivariate normal? → Cholesky
  • Working with covariance matrices? → Cholesky

Complexity Comparison

For an n×n matrix:

  • Cholesky: ~n³/3 operations (fastest for positive definite)
  • LU: ~2n³/3 operations
  • QR: ~4n³/3 operations
  • SVD: ~4n³ operations (most expensive but most informative)

Numerical Stability Ranking

  1. SVD: Most stable, works for any matrix
  2. QR: Very stable, especially for least squares
  3. Cholesky: Stable for positive definite matrices
  4. LU: Can be unstable without pivoting

Common Pitfalls and How to Avoid Them

SVD Pitfalls

  • Don’t assume truncated SVD preserves all important structure — always validate that your chosen rank captures enough variance
  • Watch out for sign ambiguity — SVD isn’t unique in the signs of singular vectors

LU Pitfalls

  • Never use LU without pivoting for general matrices — use scipy.linalg.lu with pivoting
  • Don’t invert matrices explicitly — solve Ax = b directly instead of computing A^(-1)

QR Pitfalls

  • Gram-Schmidt is numerically unstable — use Householder reflections or library implementations
  • Don’t use QR for square, well-conditioned systems — LU is more efficient

Cholesky Pitfalls

  • Always check positive definiteness first — Cholesky will fail spectacularly on indefinite matrices
  • Add regularization for near-singular matrices — add small diagonal terms if needed

Performance Optimization

# Use appropriate dtypes
A_float32 = A.astype(np.float32) # Faster for large matrices if precision allows
A_float64 = A.astype(np.float64) # Better precision

# Leverage BLAS libraries
import scipy.linalg.lapack as lapack # Direct LAPACK calls for maximum speed
# Use in-place operations when possible
scipy.linalg.solve(A, b, overwrite_a=True, overwrite_b=True)

Memory Efficiency

# For large matrices, consider iterative methods
from scipy.sparse.linalg import svds # Sparse SVD for large, sparse matrices
from sklearn.decomposition import TruncatedSVD # Memory-efficient truncated SVD

Numerical Stability Checks

def check_matrix_properties(A):
"""Check important matrix properties before decomposition"""
print(f"Shape: {A.shape}")
print(f"Condition number: {np.linalg.cond(A):.2e}")
print(f"Determinant: {np.linalg.det(A):.2e}")

if A.shape[0] == A.shape[1]: # Square matrix
eigenvals = np.linalg.eigvals(A)
print(f"Eigenvalues: {eigenvals}")

# Check if symmetric
is_symmetric = np.allclose(A, A.T)
print(f"Symmetric: {is_symmetric}")

if is_symmetric:
is_pos_def = np.all(eigenvals > 0)
print(f"Positive definite: {is_pos_def}")

Matrix decompositions are fundamental tools that every AI/ML developer should master. Each decomposition has its strengths:

  • SVD is your go-to for data analysis, dimensionality reduction, and understanding matrix structure
  • LU excels at solving linear systems efficiently, especially when you have multiple right-hand sides
  • QR provides numerical stability for least squares problems and overdetermined systems
  • Cholesky offers maximum efficiency for positive definite matrices and is essential for working with covariance matrices

--

--

Ravi Sankar Uppala
Ravi Sankar Uppala

AI enthusiast, Math Analyst, Technology enthusiast