TAI BUI
Back to curriculum
BuildPython

Linear Systems

Solving Ax = b is the oldest problem in mathematics that still runs your neural network.

TIME: ~120 minutesPREREQUISITES: Phase 1, Lessons 01 (Linear Algebra Intuition), 02 (Vectors & Matrices), 03 (Matrix Transformations)

Learning Objectives

  • Solve Ax = b using Gaussian elimination with partial pivoting and back substitution
  • Factor matrices with LU, QR, and Cholesky decompositions and explain when each is appropriate
  • Derive the normal equations for least squares and connect them to linear and ridge regression
  • Diagnose ill-conditioned systems using the condition number and apply regularization to stabilize them

The Problem

Train a linear regression and you're solving a linear system. Compute a least-squares fit and it's the same system. A neural network layer computing y = Wx + b is evaluating one side of it. Add regularization and you're modifying that system. Fit a Gaussian process, a model that predicts with a full distribution over possible functions instead of a single number, and you're factoring a matrix. Invert a covariance matrix for Mahalanobis distance and you're solving Ax = b all over again.

That equation, Ax = b, sits underneath almost everything else in this curriculum. A is a matrix of known coefficients, b is a vector of known outputs, and x is the vector of unknowns you're solving for. In linear regression specifically, A is your data matrix, b is your target vector, and x is the weight vector: the entire model reduces to finding the x that makes Ax land as close to b as possible.

This lesson builds every major method for finding that x, from scratch. You'll see why some methods are fast but numerically dangerous, why some only work on square systems while others handle the overdetermined case real data always hands you, and why the condition number of your matrix decides whether the answer you get back means anything at all.

Warm-up0/3
1. You have 5 different linear equations but only 2 unknown values to solve for. Based on ordinary algebra intuition, would you expect a single pair of values to satisfy all 5 equations exactly?Warm-up
Five labelled equation boxes above two unknown-value boxes, with a question mark asking whether one pair of values satisfies all five equations at once.
2. If you divide a fixed number by a value that is very close to zero, what generally happens to the result?Warm-up
A box reading 1 divided by 0.0001 equals question mark, with a note asking what happens when the divisor is very close to zero.
3. A matrix multiplies a vector. What does the result look like?Warm-up
A 2 by 2 grid of numbers multiplying a 2-entry list, with a question mark for the result.

The Concept

What Ax = b means geometrically

A system of linear equations has a geometric interpretation. Each equation defines a hyperplane. The solution is the point (or set of points) where all hyperplanes intersect.

2x + y = 5          Two lines in 2D.
x - y  = 1          They intersect at x=2, y=1.

Mermaid diagram

Three things can happen:

Mermaid diagram

In matrix form, "one solution" means A is invertible. "No solution" means the system is inconsistent. "Infinite solutions" means A has a null space. Most ML problems fall in the "no exact solution" category because you have more equations (data points) than unknowns (parameters). That is where least squares comes in.

Column picture vs row picture

There are two ways to read Ax = b.

Row picture. Each row of A defines one equation. Each equation is a hyperplane. The solution is where they all intersect.

Column picture. Each column of A is a vector. The question becomes: what linear combination of the columns of A produces b?

A = | 2  1 |    b = | 5 |
    | 1 -1 |        | 1 |

Row picture: solve 2x + y = 5 and x - y = 1 simultaneously.

Column picture: find x1, x2 such that:
  x1 * [2, 1] + x2 * [1, -1] = [5, 1]
  2 * [2, 1] + 1 * [1, -1] = [4+1, 2-1] = [5, 1]   check.

The column picture is more fundamental. If b lies in the column space of A, the system has a solution. If b does not, you find the closest point in the column space. That closest point is the least-squares solution.

Gaussian elimination

Gaussian elimination transforms Ax = b into an upper triangular system Ux = c that you solve by back substitution. It is the most direct method.

The algorithm:

1. For each column k (the pivot column):
   a. Find the largest entry in column k at or below row k (partial pivoting).
   b. Swap that row with row k.
   c. For each row i below k:
      - Compute multiplier m = A[i][k] / A[k][k]
      - Subtract m times row k from row i.
2. Back substitute: solve from the last equation upward.

Example:

Original:
| 2  1  1 | 8 |       R2 = R2 - (2)R1     | 2  1   1 |  8 |
| 4  3  3 |20 |  -->  R3 = R3 - (1)R1 --> | 0  1   1 |  4 |
| 2  3  1 |12 |                            | 0  2   0 |  4 |

                       R3 = R3 - (2)R2     | 2  1   1 |  8 |
                                       --> | 0  1   1 |  4 |
                                           | 0  0  -2 | -4 |

Back substitute:
  -2 * x3 = -4    -->  x3 = 2
  x2 + 2  = 4     -->  x2 = 2
  2*x1 + 2 + 2 = 8 --> x1 = 2

Gaussian elimination costs O(n^3) operations. For a 1000x1000 system, that is about a billion floating-point operations. Fast, but you can do better if you need to solve multiple systems with the same A.

Partial pivoting: why it matters

Without pivoting, Gaussian elimination can fail or produce garbage. If a pivot element is zero, you divide by zero. If it is small, you amplify rounding errors.

Bad pivot:                       With partial pivoting:
| 0.001  1 | 1.001 |            Swap rows first:
| 1      1 | 2     |            | 1      1 | 2     |
                                 | 0.001  1 | 1.001 |
m = 1/0.001 = 1000              m = 0.001/1 = 0.001
R2 = R2 - 1000*R1               R2 = R2 - 0.001*R1
| 0.001  1     | 1.001   |      | 1      1     | 2     |
| 0     -999   | -999.0  |      | 0      0.999 | 0.999 |

x2 = 1.000 (correct)            x2 = 1.000 (correct)
x1 = (1.001 - 1)/0.001          x1 = (2 - 1)/1 = 1.000 (correct)
   = 0.001/0.001 = 1.000        Stable because the multiplier is small.

Floating-point arithmetic has limited precision, and the unpivoted version can burn through significant digits fast. Partial pivoting always picks the largest available pivot, which keeps that error amplification in check.

LU decomposition

LU decomposition factors A into a lower triangular matrix L and an upper triangular matrix U: A = LU. The L matrix stores the multipliers from Gaussian elimination. The U matrix is the result of elimination.

A = L @ U

| 2  1  1 |   | 1  0  0 |   | 2  1   1 |
| 4  3  3 | = | 2  1  0 | @ | 0  1   1 |
| 2  3  1 |   | 1  2  1 |   | 0  0  -2 |

Why factor instead of just eliminating? Because once you have L and U, solving Ax = b for any new b costs only O(n^2):

Ax = b
LUx = b
Let y = Ux:
  Ly = b    (forward substitution, O(n^2))
  Ux = y    (back substitution, O(n^2))

The O(n^3) cost is paid once during factorization. Every subsequent solve is O(n^2). If you need to solve 1000 systems with the same A but different b vectors, LU saves a factor of 1000/3 in total work.

With partial pivoting, you get PA = LU where P is a permutation matrix recording the row swaps.

QR decomposition

QR decomposition factors A into an orthogonal matrix Q and an upper triangular matrix R: A = QR.

An orthogonal matrix has the property Q^T Q = I. Its columns are orthonormal vectors. Multiplying by Q preserves lengths and angles.

A = Q @ R

Q has orthonormal columns: Q^T Q = I
R is upper triangular

To solve Ax = b:
  QRx = b
  Rx = Q^T b    (just multiply by Q^T, no inversion needed)
  Back substitute to get x.

QR is numerically more stable than LU for solving least-squares problems. The Gram-Schmidt process builds Q column by column:

Given columns a1, a2, ... of A:

q1 = a1 / ||a1||

q2 = a2 - (a2 . q1) * q1        (subtract projection onto q1)
q2 = q2 / ||q2||                (normalize)

q3 = a3 - (a3 . q1) * q1 - (a3 . q2) * q2
q3 = q3 / ||q3||

R[i][j] = qi . aj    for i <= j

Each step removes the component along all previous q vectors, leaving only the new orthogonal direction.

Cholesky decomposition

When A is symmetric (A = A^T) and positive definite (all eigenvalues positive), you can factor it as A = L L^T where L is lower triangular. This is the Cholesky decomposition.

A = L @ L^T

| 4  2 |   | 2  0 |   | 2  1 |
| 2  5 | = | 1  2 | @ | 0  2 |

L[i][i] = sqrt(A[i][i] - sum(L[i][k]^2 for k < i))
L[i][j] = (A[i][j] - sum(L[i][k]*L[j][k] for k < j)) / L[j][j]    for i > j

Cholesky is twice as fast as LU and requires half the storage. It only works for symmetric positive definite matrices, but those show up constantly:

  • Covariance matrices are symmetric positive semi-definite (positive definite with regularization).
  • The kernel matrix in Gaussian processes is symmetric positive definite.
  • The Hessian of a convex function at a minimum is symmetric positive definite.
  • A^T A is always symmetric positive semi-definite.

In Gaussian processes, you factor the kernel matrix K with Cholesky, then solve K alpha = y to get the predictive mean. The Cholesky factor also gives you the log-determinant for the marginal likelihood: log det(K) = 2 * sum(log(diag(L))).

Least squares: when Ax = b has no exact solution

If A is m x n with m > n (more equations than unknowns), the system is overdetermined. There is no exact solution. Instead, you minimize the squared error:

minimize ||Ax - b||^2

This is the sum of squared residuals:
  sum((A[i,:] @ x - b[i])^2 for i in range(m))

The minimizer satisfies the normal equations:

A^T A x = A^T b

Derivation: expand ||Ax - b||^2 = (Ax - b)^T (Ax - b) = x^T A^T A x - 2 x^T A^T b + b^T b. Take the gradient with respect to x, set it to zero: 2 A^T A x - 2 A^T b = 0.

Original system (overdetermined, 4 equations, 2 unknowns):
| 1  1 |         | 3 |
| 1  2 | x     = | 5 |       No exact x satisfies all 4 equations.
| 1  3 |         | 6 |
| 1  4 |         | 8 |

Normal equations:
A^T A = | 4  10 |    A^T b = | 22 |
        | 10 30 |            | 63 |

Solve: x = [1.5, 1.6]

This is linear regression. x[0] is the intercept, x[1] is the slope.

Normal equations = linear regression

The connection is exact. In linear regression, your data matrix X has one row per sample and one column per feature. Your target vector y has one entry per sample. The weight vector w satisfies:

X^T X w = X^T y
w = (X^T X)^(-1) X^T y

This is the closed-form solution to linear regression. Every call to sklearn.linear_model.LinearRegression.fit() computes this (or an equivalent via QR or SVD).

Add a regularization term lambda * I to the matrix and you get ridge regression:

(X^T X + lambda * I) w = X^T y
w = (X^T X + lambda * I)^(-1) X^T y

The regularization makes the matrix better conditioned (easier to invert accurately) and prevents overfitting by shrinking the weights toward zero. The matrix X^T X + lambda * I is always symmetric positive definite when lambda > 0, so you can use Cholesky to solve it.

Pseudoinverse (Moore-Penrose)

The pseudoinverse A+ generalizes matrix inversion to non-square and singular matrices. For any matrix A:

x = A+ b

where A+ = V Sigma+ U^T    (computed via SVD)

Sigma+ is formed by taking the reciprocal of each nonzero singular value and transposing the result. If A = U Sigma V^T, then A+ = V Sigma+ U^T.

A = U Sigma V^T        (SVD)

Sigma = | 5  0 |       Sigma+ = | 1/5  0  0 |
        | 0  2 |                | 0  1/2  0 |
        | 0  0 |

A+ = V Sigma+ U^T

The pseudoinverse gives the minimum-norm least-squares solution. If the system has:

  • One solution: A+ b gives it.
  • No solution: A+ b gives the least-squares solution.
  • Infinite solutions: A+ b gives the one with the smallest ||x||.

NumPy's np.linalg.lstsq and np.linalg.pinv both use the SVD internally.

Condition number

The condition number measures how sensitive the solution is to small changes in the input. For a matrix A, the condition number is:

kappa(A) = ||A|| * ||A^(-1)|| = sigma_max / sigma_min

where sigma_max and sigma_min are the largest and smallest singular values.

Well-conditioned (kappa ~ 1):        Ill-conditioned (kappa ~ 10^15):
Small change in b -->                Small change in b -->
small change in x                    huge change in x

| 2  0 |   kappa = 2/1 = 2          | 1   1          |   kappa ~ 10^15
| 0  1 |   safe to solve            | 1   1+10^(-15) |   solution is garbage

Rules of thumb:

  • kappa < 100: safe, solution is accurate.
  • kappa ~ 10^k: you lose about k digits of precision from your floating-point arithmetic.
  • kappa ~ 10^16 (for float64): the solution is meaningless. The matrix is effectively singular.

In ML, ill-conditioning happens when features are nearly collinear. Regularization (adding lambda * I) improves the condition number from sigma_max / sigma_min to (sigma_max + lambda) / (sigma_min + lambda).

Iterative methods: conjugate gradient

For very large sparse systems (millions of unknowns), direct methods like LU or Cholesky get too expensive to run. Iterative methods take a different approach: start with a guess and improve it a little on every pass.

Conjugate gradient (CG) solves Ax = b when A is symmetric positive definite. It finds the exact solution in at most n iterations (in exact arithmetic), but typically converges much faster if the eigenvalues of A are clustered.

Algorithm sketch:
  x0 = initial guess (often zero)
  r0 = b - A x0           (residual)
  p0 = r0                 (search direction)

  For k = 0, 1, 2, ...:
    alpha = (rk . rk) / (pk . A pk)
    x_{k+1} = xk + alpha * pk
    r_{k+1} = rk - alpha * A pk
    beta = (r_{k+1} . r_{k+1}) / (rk . rk)
    p_{k+1} = r_{k+1} + beta * pk
    if ||r_{k+1}|| < tolerance: stop

CG is used in:

  • Large-scale optimization (Newton-CG method)
  • Solving PDE discretizations
  • Kernel methods where the kernel matrix is too large to factor
  • Preconditioning for other iterative solvers

The convergence rate depends on the condition number. Better conditioned systems converge faster, which is another reason regularization helps.

The full picture: which method when

MethodRequirementsCostUse case
Gaussian eliminationSquare, nonsingular AO(n^3)One-off solve of a square system
LU decompositionSquare, nonsingular AO(n^3) factor + O(n^2) solveMultiple solves with the same A
QR decompositionAny A (m >= n)O(mn^2)Least squares, numerically stable
CholeskySymmetric positive definite AO(n^3/3)Covariance matrices, Gaussian processes, ridge regression
Normal equationsOverdetermined (m > n)O(mn^2 + n^3)Linear regression (small n)
SVD / pseudoinverseAny AO(mn^2)Rank-deficient systems, minimum-norm solutions
Conjugate gradientSymmetric positive definite, sparse AO(n * k * nnz)Large sparse systems, k = iterations

Connection to ML

Every method in this lesson appears in production ML:

Linear regression. The closed-form solution solves the normal equations X^T X w = X^T y. This is done via Cholesky (if n is small) or QR (if numerical stability matters) or SVD (if the matrix might be rank-deficient).

Ridge regression. Adds lambda * I to X^T X. The regularized system (X^T X + lambda * I) w = X^T y is always solvable via Cholesky because X^T X + lambda * I is symmetric positive definite for lambda > 0.

Gaussian processes. The predictive mean requires solving K alpha = y where K is the kernel matrix. Cholesky factorization of K is the standard approach. The log marginal likelihood uses log det(K) = 2 sum(log(diag(L))).

Neural network initialization. Orthogonal initialization uses QR decomposition to create weight matrices whose columns are orthonormal. This prevents signal collapse in deep networks.

Preconditioning. Large-scale optimizers use incomplete Cholesky or incomplete LU as preconditioners for conjugate gradient solvers.

Feature engineering. The condition number of X^T X tells you if your features are collinear. If kappa is large, drop features or add regularization.

Check: The Concept0/37
1. What is a linear system in the sense of Ax = b?Define
The matrix [[2, 1], [1, -1]] times an unknown column [x1, x2] equals [5, 1], labelled as the equation A x equals b.
2. What question does the column picture of Ax = b ask, as opposed to the row picture?Define
The same matrix's two columns, [2, 1] and [1, -1], with the question x1 times column 1 plus x2 times column 2 equals the target vector [5, 1].
3. What is an overdetermined system?Define
A flow of a matrix shaped 4 rows by 2 columns, an unknown vector of 2 entries, and an output vector of 4 entries, asking whether an exact fit usually exists.
4. What is Gaussian elimination?Define
The lesson's own 3 by 3 matrix [[2,1,1],[4,3,3],[2,3,1]] next to its right-hand side [8,20,12], with a question mark asking what row operation turns this into a triangular system.
5. What is partial pivoting?Define
Two rows in the same column: row k holding 0.002 and the row below holding 5, with a question mark asking which row should sit in the pivot position before eliminating.
6. What is back substitution?Define
A 3 by 3 triangular pattern of x's above the diagonal and 0's below it, with a question mark asking which equation has only one unknown and can be solved first.
7. What is forward substitution?Define
A 3 by 3 triangular pattern of x's on and below the diagonal and 0's above it, with a question mark asking which equation has only one unknown and can be solved first here.
8. What is LU decomposition?Define
The lesson's own matrix A written as a lower triangular matrix times an upper triangular matrix, asking what this split is called.
9. What is QR decomposition?Define
A 3 by 2 matrix A with a question mark, asking what must be true of Q's columns in the factoring A equals Q times R.
10. What is Cholesky decomposition?Define
The symmetric matrix [[4, 2], [2, 5]] with a question mark, asking what kind of matrix A must be for A equals L times L transpose to work.
11. What is least squares?Define
Four data points plotted that do not sit on a common straight line, with a note asking what x minimizes the total squared miss.
12. What are the normal equations?Define
The equation A transpose A x equals A transpose b, with A transpose A = [[4, 10], [10, 30]] and A transpose b = [22, 63], asking what this system is called.
13. What is the pseudoinverse A+?Define
Sigma from an SVD shown as a 3 by 2 matrix with 5 and 2 on the diagonal, with a question mark asking what goes into Sigma-plus for the pseudoinverse.
14. What does the condition number of a matrix measure?Define
The formula kappa equals sigma max over sigma min, above two boxes, system A and system B, each given the same small nudge to b, asking which system's x moves more.
15. What is ridge regression?Define
The regularized normal equations with lambda = 1.0 added to the diagonal of X transpose X, with a question mark asking what happens to the fitted weights as lambda grows.
16. What is the conjugate gradient method?Define
A symmetric positive definite system with 6 unknowns, starting from a zero guess that improves a little each pass, asking how many passes it takes to reach the exact answer.
17. Why does Gaussian elimination without pivoting go wrong on some systems?Explain
Two rows with the pivot at 0.001 left in place and 1 below it, asking what multiplier eliminating with this pivot needs.
18. What is the main advantage of LU decomposition over Gaussian elimination when you need to solve Ax = b for many different b vectors?Explain
A flow from factoring A once to solving for b1, b2 and more, with a question mark asking what factoring once changes about the cost of each solve.
19. Why is QR decomposition generally more numerically stable than LU for solving least-squares problems?Explain
A flow from A x equals b to Q R x equals b to R x equals a question mark, asking what replaces the question mark after multiplying both sides by Q transpose.
20. Why is Cholesky decomposition preferred over LU for solving (X^T X + lambda I) w = X^T y in ridge regression?Explain
Boxes asking whether X transpose X plus lambda I is symmetric and positive definite, with a question mark asking which factorization can exploit both properties.
21. What do you get by expanding ||Ax - b||^2, taking the gradient with respect to x, and setting it to zero?Explain
The expanded squared error formula, with a note to take its gradient with respect to x and set it to zero, asking what equation falls out.
22. What does the pseudoinverse give you when Ax = b has infinitely many solutions?Explain
A line labelled x plus 2y equals 5, with the origin marked, asking which of the infinitely many points on the line the pseudoinverse picks.
23. How does adding lambda I to a matrix change its condition number?Explain
Illustrative singular values 8 and 0.02, with lambda 1 added to both, asking whether the ratio of the largest to smallest grows, shrinks, or stays the same.
24. When is conjugate gradient preferred over direct methods like LU or Cholesky?Explain
An illustrative growth chart with a steeply rising direct-factorization curve and a much flatter iterative-method curve, asking which costs less as the system grows.
25. During Gaussian elimination, the diagonal entry at the current pivot position is 0.0001, but a row below it has a much larger value in that column. What does partial pivoting say to do?Apply
A pivot entry of 0.0001 with a row below it holding 6.2, asking what partial pivoting says to do before eliminating.
26. After elimination, a system reads 2x1 + x2 + x3 = 8, then x2 + x3 = 4, then -2x3 = -4. What is x1?Apply
The triangular system 2x1 plus x2 plus x3 equals 8, x2 plus x3 equals 4, and negative 2 x3 equals negative 4, asking what x1 is.
27. Gaussian elimination is applied to rows [0.001, 1 | 1.001] and [1, 1 | 2]. After partial pivoting swaps the rows, what multiplier m is used to eliminate the entry below the pivot?Apply
Row A holding 0.001 and 1, row B holding 1 and 1, with a note that pivoting swaps row B into the pivot position, asking what multiplier clears the entry below it.
28. A matrix has condition number kappa = 10^8. You are using float64 (about 15 digits of precision). How many digits of the solution can you trust?Apply
A condition number of 10 to the 8th, with float64 giving about 15 digits total, asking how many digits of the solution can be trusted.
29. A teammate calls gaussian_elimination(A, b) directly on a 4x2 data matrix and gets an error or nonsense. What is the right fix?Analyze
A flow from a 4 by 2 matrix A into gaussian_elimination directly, ending in error or nonsense, asking what the right fix is.
30. A pipeline needs Ax = b solved for 500 new right-hand sides with the same A, and profiling shows the cubic cost dominates. Which change targets the cause?Analyze
A scenario card: solving Ax = b for 500 different b vectors with the same A, profiling shows the cubic cost dominates, asking what the fix is.
31. Cholesky on X^T X raises 'not positive definite' after someone adds a second copy of one feature column. Why?Analyze
A flow from X with two identical columns through X transpose X into cholesky, asking why cholesky raises a not-positive-definite error.
32. Two of your regression features are nearly duplicates, and adding a tiny amount of noise to the training data makes the fitted weights swing wildly. What fix does the lesson point to?Analyze
A scenario card: two nearly-duplicate feature columns where a tiny bit of noise swings the fitted weights wildly, asking what the fix is.
33. A Gaussian process kernel matrix has condition number near 10^15. Cholesky returns a factor without any error, and the predictions look plausible. How should you read this?Analyze
A scenario card: a Gaussian process kernel matrix with condition number near 10 to the 15th, Cholesky factors it without error, predictions look plausible, asking how to read this.
34. After adding many nearly collinear features, conjugate gradient on a large SPD system needs far more iterations. What explains this?Analyze
A scenario card: adding many nearly collinear features to the same conjugate gradient solver, asking what happens to the iterations needed.
35. You must compute many predictive means, each by solving K alpha = y for a new y, where K is a fixed symmetric positive definite kernel matrix of moderate size. Which design fits best?Design
A scenario card: a fixed symmetric positive definite kernel matrix K of moderate size, with a new y arriving each time, asking what the best design is.
36. You are fitting a regression whose design matrix might be rank-deficient, and you need a stable answer with minimum-norm weights. Which method fits?Design
A scenario card: a regression design matrix with two identical columns, needing a stable answer with minimum-norm weights, asking which method fits.
37. A kernel method needs to solve a symmetric positive definite system with millions of unknowns, and the matrix is sparse. Which approach should you choose?Design
A scenario card: a kernel method's linear system with millions of unknowns, sparse and symmetric positive definite, asking what approach to choose.

Build It

Step 1: Gaussian elimination with partial pivoting

python
import numpy as np
 
def gaussian_elimination(A, b):
    n = len(b)
    Ab = np.hstack([A.astype(float), b.reshape(-1, 1).astype(float)])
 
    for k in range(n):
        max_row = k + np.argmax(np.abs(Ab[k:, k]))
        Ab[[k, max_row]] = Ab[[max_row, k]]
 
        if abs(Ab[k, k]) < 1e-12:
            raise ValueError(f"Matrix is singular or nearly singular at pivot {k}")
 
        for i in range(k + 1, n):
            m = Ab[i, k] / Ab[k, k]
            Ab[i, k:] -= m * Ab[k, k:]
 
    x = np.zeros(n)
    for i in range(n - 1, -1, -1):
        x[i] = (Ab[i, -1] - Ab[i, i+1:n] @ x[i+1:n]) / Ab[i, i]
 
    return x

Step 2: LU decomposition

python
def lu_decompose(A):
    n = A.shape[0]
    L = np.eye(n)
    U = A.astype(float).copy()
    P = np.eye(n)
 
    for k in range(n):
        max_row = k + np.argmax(np.abs(U[k:, k]))
        if max_row != k:
            U[[k, max_row]] = U[[max_row, k]]
            P[[k, max_row]] = P[[max_row, k]]
            if k > 0:
                L[[k, max_row], :k] = L[[max_row, k], :k]
 
        for i in range(k + 1, n):
            L[i, k] = U[i, k] / U[k, k]
            U[i, k:] -= L[i, k] * U[k, k:]
 
    return P, L, U
 
def lu_solve(P, L, U, b):
    n = len(b)
    Pb = P @ b.astype(float)
 
    y = np.zeros(n)
    for i in range(n):
        y[i] = Pb[i] - L[i, :i] @ y[:i]
 
    x = np.zeros(n)
    for i in range(n - 1, -1, -1):
        x[i] = (y[i] - U[i, i+1:] @ x[i+1:]) / U[i, i]
 
    return x

Step 3: Cholesky decomposition

python
def cholesky(A):
    n = A.shape[0]
    L = np.zeros_like(A, dtype=float)
 
    for i in range(n):
        for j in range(i + 1):
            s = A[i, j] - L[i, :j] @ L[j, :j]
            if i == j:
                if s <= 0:
                    raise ValueError("Matrix is not positive definite")
                L[i, j] = np.sqrt(s)
            else:
                L[i, j] = s / L[j, j]
 
    return L

Step 4: Least squares via normal equations

python
def least_squares_normal(A, b):
    AtA = A.T @ A
    Atb = A.T @ b
    return gaussian_elimination(AtA, Atb)
 
def ridge_regression(A, b, lam):
    n = A.shape[1]
    AtA = A.T @ A + lam * np.eye(n)
    Atb = A.T @ b
    L = cholesky(AtA)
    y = np.zeros(n)
    for i in range(n):
        y[i] = (Atb[i] - L[i, :i] @ y[:i]) / L[i, i]
    x = np.zeros(n)
    for i in range(n - 1, -1, -1):
        x[i] = (y[i] - L.T[i, i+1:] @ x[i+1:]) / L.T[i, i]
    return x

Step 5: Condition number

python
def condition_number(A):
    U, S, Vt = np.linalg.svd(A)
    return S[0] / S[-1]
Check: Build It0/5
1. gaussian_elimination is called with A = [[1, 2], [2, 4]] and b = [3, 6]. What happens?Apply
The call gaussian_elimination with A = [[1, 2], [2, 4]] and b = [3, 6], asking what happens when it runs.
2. cholesky is called on A = [[1, 2], [2, 1]], which is symmetric. What happens?Apply
The call cholesky with A = [[1, 2], [2, 1]], a symmetric matrix, asking what happens when it runs.
3. In lu_solve, what is the first thing done to b before the forward substitution loop?Apply
The start of the lesson's own lu_solve function with its first real step replaced by question marks, before the forward and back substitution lines.
4. In ridge_regression, the first loop computes y[i] = (Atb[i] - L[i, :i] @ y[:i]) / L[i, i]. What system does it solve?Apply
The first loop of the lesson's own ridge_regression, computing y[i] from Atb, L and already-known y values, asking what system this loop solves.
5. condition_number returns S[0] / S[-1] from the SVD. For A = [[2, 0], [0, 1]], what does it return?Apply
The call condition_number on A = [[2, 0], [0, 1]], returning S[0] divided by S[-1] from the SVD, with a question mark for the result.

Use It

Put the pieces together: fit linear regression and ridge regression on real data, then check both against NumPy and scikit-learn.

python
np.random.seed(42)
X_raw = np.random.randn(100, 3)
w_true = np.array([2.0, -1.0, 0.5])
y = X_raw @ w_true + np.random.randn(100) * 0.1
 
X = np.column_stack([np.ones(100), X_raw])
 
w_ols = least_squares_normal(X, y)
print(f"OLS weights (ours):    {w_ols}")
 
w_np = np.linalg.lstsq(X, y, rcond=None)[0]
print(f"OLS weights (numpy):   {w_np}")
print(f"Max difference: {np.max(np.abs(w_ols - w_np)):.2e}")
 
w_ridge = ridge_regression(X, y, lam=1.0)
print(f"Ridge weights (ours):  {w_ridge}")
 
from sklearn.linear_model import Ridge
ridge_sk = Ridge(alpha=1.0, fit_intercept=False)
ridge_sk.fit(X, y)
print(f"Ridge weights (sklearn): {ridge_sk.coef_}")

Ship It

This lesson produces:

  • code/linear_systems.py containing from-scratch implementations of Gaussian elimination, LU decomposition, Cholesky decomposition, least squares, and ridge regression
  • A working demonstration that normal equations and sklearn's LinearRegression produce the same weights
Author's Note

One habit worth building early: you will almost never call np.linalg.inv in real code, and if you catch yourself reaching for it, that's usually a sign you picked the wrong tool. inv computes a full inverse you don't need, which costs more and is less stable than just solving the system in front of you. np.linalg.solve picks LU under the hood. scipy.linalg.cho_solve picks Cholesky once you already know the matrix is symmetric positive definite. Neither one materializes an inverse.

The condition number section is the one to actually internalize, not just pass the quiz on. I've watched a Gaussian process blow up in production because its kernel matrix, built from nearly duplicate training points, had a condition number north of 10^14. The Cholesky factorization didn't crash. It just quietly returned garbage that looked like a valid answer. Adding a small jitter term to the diagonal, the same lambda * I trick as ridge regression, made the problem disappear. Nobody warns you about that failure mode until it's already sitting in front of a customer.

Exercises

  1. Solve the system [[1,2,3],[4,5,6],[7,8,10]] x = [6, 15, 27] using your Gaussian elimination, your LU solver, and np.linalg.solve. Verify all three give the same answer within floating-point tolerance.

  2. Generate a 50x5 random matrix X and target y = X @ w_true + noise. Solve for w using normal equations, QR (via np.linalg.qr), SVD (via np.linalg.svd), and np.linalg.lstsq. Compare all four solutions. Measure the condition number of X^T X and explain how it affects which method you trust.

  3. Create a nearly singular matrix by making two columns almost identical (e.g., column 2 = column 1 + 1e-10 * noise). Compute its condition number. Solve Ax = b with and without regularization (add 0.01 * I). Compare the solutions and residuals. Explain why regularization helps.

  4. Implement the conjugate gradient algorithm for a 100x100 random symmetric positive definite matrix. Count how many iterations it takes to converge to tolerance 1e-8. Compare with the theoretical maximum of n iterations.

  5. Time your Cholesky solver vs your LU solver vs np.linalg.solve on symmetric positive definite matrices of size 10, 50, 200, 500. Plot the results. Verify Cholesky is roughly 2x faster than LU.

Key Terms

TermWhat people sayWhat it actually means
Linear system"Solve for x"A set of linear equations Ax = b. Finding x means finding the input that produces output b under transformation A.
Gaussian elimination"Row reduce"Systematically zero out entries below the diagonal using row operations, producing an upper triangular system solvable by back substitution. O(n^3).
Partial pivoting"Swap rows for stability"Before eliminating in column k, swap the row with the largest absolute value in that column to the pivot position. Prevents division by small numbers.
LU decomposition"Factor into triangles"Write A = LU where L is lower triangular (stores multipliers) and U is upper triangular (the eliminated matrix). Amortizes the O(n^3) cost over multiple solves.
QR decomposition"Orthogonal factorization"Write A = QR where Q has orthonormal columns and R is upper triangular. More stable than LU for least squares.
Cholesky decomposition"Square root of a matrix"For symmetric positive definite A, write A = LL^T. Half the cost of LU. Used for covariance matrices, kernel matrices, and ridge regression.
Least squares"Best fit when exact is impossible"Minimize the sum of squared residuals
Normal equations"The calculus shortcut"A^T A x = A^T b. Setting the gradient of
Pseudoinverse"Inversion for non-square matrices"A+ = V Sigma+ U^T via SVD. Gives the minimum-norm least-squares solution for any matrix, square or rectangular, singular or not.
Condition number"How trustworthy is this answer"kappa = sigma_max / sigma_min. Measures sensitivity to input perturbations. Lose about log10(kappa) digits of precision.
Ridge regression"Regularized least squares"Solve (X^T X + lambda I) w = X^T y. Adding lambda I improves conditioning and shrinks weights toward zero. Prevents overfitting.
Conjugate gradient"Iterative Ax=b for big matrices"An iterative solver for symmetric positive definite systems. Converges in at most n steps. Practical for large sparse systems where factorization is too expensive.
Overdetermined system"More data than parameters"m > n in an m-by-n system. No exact solution exists. Least squares finds the best approximation. This is every regression problem.
Back substitution"Solve from the bottom up"Given an upper triangular system, solve the last equation first, then substitute backward. O(n^2).
Forward substitution"Solve from the top down"Given a lower triangular system, solve the first equation first, then substitute forward. O(n^2). Used in the L step of LU solves.

Further Reading

Code

Ships With