Cholesky Decomposition¶
Decompose positive definite matrices as \(A = LL^T\).
Mental Model
Cholesky is a "square root" for positive definite matrices -- it splits \(A\) into a lower-triangular matrix \(L\) times its own transpose. It is roughly twice as fast as a general LU decomposition and serves as the workhorse for sampling multivariate normals, solving symmetric systems, and testing positive definiteness.
Connection to Solving Systems
When \(A\) is symmetric positive definite, Cholesky gives the fastest path to
solving \(Ax = b\): factor \(A = LL^T\) once, then solve two triangular systems
(\(Ly = b\), then \(L^T x = y\)) via scipy.linalg.cho_solve. This is roughly 2x
faster than the general np.linalg.solve (which uses LU) and exploits the
structure that solve cannot assume. Covariance matrices in statistics and
kernel matrices in ML are always SPD, making Cholesky the natural choice.
np.linalg.cholesky¶
1. Basic Usage¶
```python import numpy as np
def main(): # Symmetric positive definite matrix A = np.array([[4, 2, 1], [2, 5, 2], [1, 2, 6]])
L = np.linalg.cholesky(A)
print("A =")
print(A)
print()
print("L (lower triangular) =")
print(L.round(4))
if name == "main": main() ```
2. Mathematical Form¶
where \(L\) is lower triangular.
3. Verify Result¶
```python import numpy as np
def main(): A = np.array([[4, 2, 1], [2, 5, 2], [1, 2, 6]])
L = np.linalg.cholesky(A)
# Reconstruct
A_reconstructed = L @ L.T
print("Original A:")
print(A)
print()
print("L @ L^T:")
print(A_reconstructed.round(10))
print()
print(f"Match: {np.allclose(A, A_reconstructed)}")
if name == "main": main() ```
Requirements¶
1. Symmetric Matrix¶
```python import numpy as np
def main(): A = np.array([[4, 2], [2, 3]])
is_symmetric = np.allclose(A, A.T)
print(f"A is symmetric: {is_symmetric}")
L = np.linalg.cholesky(A)
print(f"Cholesky succeeded: L =\n{L}")
if name == "main": main() ```
2. Positive Definite¶
All eigenvalues must be positive.
```python import numpy as np
def main(): A = np.array([[4, 2], [2, 3]])
eigenvalues = np.linalg.eigvalsh(A)
is_positive_definite = np.all(eigenvalues > 0)
print(f"Eigenvalues: {eigenvalues}")
print(f"Positive definite: {is_positive_definite}")
if name == "main": main() ```
3. Error for Non-PD¶
```python import numpy as np
def main(): # Not positive definite (negative eigenvalue) A = np.array([[1, 2], [2, 1]])
print(f"Eigenvalues: {np.linalg.eigvalsh(A)}")
try:
L = np.linalg.cholesky(A)
except np.linalg.LinAlgError as e:
print(f"Error: {e}")
if name == "main": main() ```
Creating PD Matrices¶
1. From Random Matrix¶
```python import numpy as np
def main(): np.random.seed(42)
# Random matrix
B = np.random.randn(3, 3)
# A = B @ B^T is positive semi-definite
A = B @ B.T
# Add small diagonal for strict positive definite
A = A + 0.01 * np.eye(3)
print("A =")
print(A.round(3))
print()
print(f"Eigenvalues: {np.linalg.eigvalsh(A).round(4)}")
L = np.linalg.cholesky(A)
print("Cholesky succeeded!")
if name == "main": main() ```
2. Covariance Matrix¶
```python import numpy as np
def main(): np.random.seed(42)
# Sample data
X = np.random.randn(100, 3)
# Sample covariance (symmetric, typically PD)
cov = np.cov(X.T)
print("Covariance matrix:")
print(cov.round(3))
print()
L = np.linalg.cholesky(cov)
print("Cholesky factor:")
print(L.round(3))
if name == "main": main() ```
3. Regularization¶
```python import numpy as np
def main(): np.random.seed(42)
# Nearly singular covariance
X = np.random.randn(10, 5)
cov = np.cov(X.T)
# Add regularization
reg = 1e-6
cov_reg = cov + reg * np.eye(cov.shape[0])
L = np.linalg.cholesky(cov_reg)
print("Regularized Cholesky succeeded!")
if name == "main": main() ```
Applications¶
1. Solving Linear Systems¶
Faster than general solve for PD matrices.
```python import numpy as np from scipy import linalg
def main(): A = np.array([[4, 2, 1], [2, 5, 2], [1, 2, 6]]) b = np.array([1, 2, 3])
# Method 1: Standard solve
x1 = np.linalg.solve(A, b)
# Method 2: Cholesky-based solve
L = np.linalg.cholesky(A)
# Solve L @ y = b
y = linalg.solve_triangular(L, b, lower=True)
# Solve L^T @ x = y
x2 = linalg.solve_triangular(L.T, y, lower=False)
print(f"Standard solve: {x1}")
print(f"Cholesky solve: {x2}")
print(f"Match: {np.allclose(x1, x2)}")
if name == "main": main() ```
2. Multivariate Normal¶
Generate correlated random samples.
```python import numpy as np import matplotlib.pyplot as plt
def main(): np.random.seed(42)
# Mean and covariance
mean = np.array([0, 0])
cov = np.array([[1, 0.8],
[0.8, 1]])
# Cholesky factor
L = np.linalg.cholesky(cov)
# Generate samples: x = mean + L @ z
n_samples = 1000
z = np.random.randn(2, n_samples)
samples = mean.reshape(-1, 1) + L @ z
fig, ax = plt.subplots()
ax.scatter(samples[0], samples[1], alpha=0.3, s=10)
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.set_title('Correlated Normal Samples via Cholesky')
ax.set_aspect('equal')
plt.show()
if name == "main": main() ```
3. Log Determinant¶
Numerically stable computation.
```python import numpy as np
def main(): A = np.array([[4, 2, 1], [2, 5, 2], [1, 2, 6]])
L = np.linalg.cholesky(A)
# log|A| = 2 * sum(log(diag(L)))
log_det_cholesky = 2 * np.sum(np.log(np.diag(L)))
# Direct computation
sign, log_det_direct = np.linalg.slogdet(A)
print(f"Log det (Cholesky): {log_det_cholesky:.6f}")
print(f"Log det (slogdet): {log_det_direct:.6f}")
if name == "main": main() ```
scipy.linalg¶
1. Lower and Upper¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[4, 2], [2, 3]])
# Lower triangular (default)
L = linalg.cholesky(A, lower=True)
print("Lower Cholesky:")
print(L)
print()
# Upper triangular
U = linalg.cholesky(A, lower=False)
print("Upper Cholesky:")
print(U)
print()
# Verify: A = L @ L^T = U^T @ U
print(f"L @ L^T matches A: {np.allclose(L @ L.T, A)}")
print(f"U^T @ U matches A: {np.allclose(U.T @ U, A)}")
if name == "main": main() ```
2. Cholesky Solve¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[4, 2, 1], [2, 5, 2], [1, 2, 6]]) b = np.array([1, 2, 3])
# Direct Cholesky solve
L = linalg.cholesky(A, lower=True)
x = linalg.cho_solve((L, True), b)
print(f"Solution: {x}")
print(f"Verify A @ x = {A @ x}")
if name == "main": main() ```
3. Check for PD¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[4, 2], [2, 3]])
try:
L = linalg.cholesky(A, lower=True, check_finite=True)
print("Matrix is positive definite")
except linalg.LinAlgError:
print("Matrix is NOT positive definite")
if name == "main": main() ```
Performance¶
1. Efficiency¶
Cholesky is ~2x faster than LU decomposition for PD matrices.
```python import numpy as np import time
def main(): n = 1000
# Create PD matrix
B = np.random.randn(n, n)
A = B @ B.T + np.eye(n)
# Cholesky timing
start = time.perf_counter()
L = np.linalg.cholesky(A)
cholesky_time = time.perf_counter() - start
# General solve timing (uses LU)
b = np.random.randn(n)
start = time.perf_counter()
x = np.linalg.solve(A, b)
solve_time = time.perf_counter() - start
print(f"Cholesky decomposition: {cholesky_time:.4f} sec")
print(f"General solve: {solve_time:.4f} sec")
if name == "main": main() ```
2. Memory¶
Lower triangular storage uses half the memory.
3. Numerical Stability¶
Cholesky is numerically stable for PD matrices.
Exercises¶
Exercise 1.
Create a 3x3 symmetric positive definite matrix by computing A = B.T @ B + np.eye(3) where B = np.random.randn(3, 3). Compute the Cholesky decomposition L and verify that L @ L.T reconstructs A within floating-point tolerance.
Solution to Exercise 1
import numpy as np
np.random.seed(42)
B = np.random.randn(3, 3)
A = B.T @ B + np.eye(3)
L = np.linalg.cholesky(A)
reconstructed = L @ L.T
print(f"Match: {np.allclose(A, reconstructed)}")
Exercise 2.
Use Cholesky decomposition to solve the system A @ x = b where A is a symmetric positive definite matrix. First compute L = np.linalg.cholesky(A), then solve L @ y = b followed by L.T @ x = y using np.linalg.solve. Verify the result matches np.linalg.solve(A, b).
Solution to Exercise 2
import numpy as np
A = np.array([[4, 2, 1], [2, 5, 2], [1, 2, 6]], dtype=float)
b = np.array([1, 2, 3], dtype=float)
L = np.linalg.cholesky(A)
y = np.linalg.solve(L, b)
x_chol = np.linalg.solve(L.T, y)
x_direct = np.linalg.solve(A, b)
print(f"Cholesky: {x_chol}")
print(f"Direct: {x_direct}")
print(f"Match: {np.allclose(x_chol, x_direct)}")
Exercise 3.
Given a matrix that is symmetric but NOT positive definite (e.g., A = np.array([[1, 2], [2, 1]])), show that np.linalg.cholesky raises a LinAlgError. Catch the exception and print the error message. Then verify the matrix is not positive definite by checking that it has a negative eigenvalue.
Solution to Exercise 3
import numpy as np
A = np.array([[1, 2], [2, 1]], dtype=float)
try:
L = np.linalg.cholesky(A)
except np.linalg.LinAlgError as e:
print(f"Error: {e}")
eigvals = np.linalg.eigvalsh(A)
print(f"Eigenvalues: {eigvals}")
print(f"Has negative eigenvalue: {np.any(eigvals < 0)}")