Linear algebra basics

NumPy · Practical · 15 min

Regression, graphics, physics and ML all reduce to a few linear-algebra calls; knowing the right one avoids slow and unstable code.

* on arrays is element-wise. For matrix multiplication use the `@` operator (np.matmul): A @ B for matrices, A @ v for matrix-vector. A 1-D vector is treated as a row or column as needed, and the result drops that dimension again.

np.linalg holds the standard toolkit, backed by optimised LAPACK/BLAS libraries:

  • np.linalg.solve(A, b) solves A x = b. Prefer it to np.linalg.inv(A) @ b; solving is faster and more numerically accurate than forming an inverse.
  • np.linalg.lstsq(A, b) for least squares when A is not square (e.g. fitting a line).
  • np.linalg.norm(x) for vector length or matrix norms, with axis= for many vectors.
  • np.linalg.det, inv, matrix_rank, eig (general), eigh (symmetric, faster and real), svd, qr, cholesky.

Most of these functions are batched: given a stack of matrices with shape (n, k, k), @ and solve operate on each matrix at once, broadcasting over the leading axes. That avoids Python loops over many small matrices.

NumPy 2 brought helpers from the array API standard: a.mT transposes only the last two axes (unlike .T, which reverses all of them), and np.vecdot(x, y) computes dot products along the last axis of stacks of vectors.

np.dot predates @ and has odd rules for N-D inputs; use @ for matrices and np.dot or np.vecdot for vectors. Avoid the np.matrix class; it's deprecated in spirit and plain arrays do everything it did.

Singular systems raise np.linalg.LinAlgError. Nearly singular ones don't raise but give huge, unreliable results; np.linalg.cond(A) warns you.

Example

import numpy as np

A = np.array([[3.0, 1.0],
              [1.0, 2.0]])
b = np.array([9.0, 8.0])

x = np.linalg.solve(A, b)
print(x)                         # [2. 3.]
print(np.allclose(A @ x, b))     # True

# least-squares line fit y = m*t + c
t = np.array([0.0, 1.0, 2.0, 3.0])
y = np.array([1.1, 2.9, 5.2, 6.8])
X = np.column_stack([t, np.ones_like(t)])
(m, c), *_ = np.linalg.lstsq(X, y)
print(round(m, 2), round(c, 2))  # 1.94 1.09

vals, vecs = np.linalg.eigh(A)   # symmetric matrix
print(vals.round(3))             # [1.382 3.618]

stack = np.stack([A, 2 * A, 3 * A])      # shape (3, 2, 2)
print((stack @ b).shape)                 # (3, 2) batched
print(np.linalg.norm([3.0, 4.0]))        # 5.0

Remember

  • @ is matrix multiplication; * is element-wise
  • Use solve (or lstsq) rather than computing an inverse
  • np.linalg functions broadcast over stacks of matrices
  • eigh for symmetric matrices; check cond for near-singular systems

Watch out for

  • Writing A * B for a matrix product and getting an element-wise result with no error when shapes happen to match.

Check yourself

  1. You must solve A x = b for a 500x500 A. Which is the best choice?
  2. `S` has shape (10, 3, 4). What is the shape of `S.mT`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: numpy.org/doc/stable/reference/routines.linalg.html