The lesson on particular and general solutions used the null space as a tool: it was the homogeneous part which, added to a particular solution, generated all the others. This one takes it as the object of study. How a basis is obtained by hand, how a computer decides its dimension in floating point, and what it means for a model's matrix to have a non-trivial null space.
The homogeneous system
The null space is the solution set of . That system is always consistent: the zero vector satisfies it for any . The relevant question is whether it admits any other solution.
The three conditions are equivalent and have already appeared separately in earlier lessons. A non-trivial null space means some non-zero combination of the columns vanishes, that is, that at least one column carries no information the others do not already contain.
The minus-one trick
There is a procedure for reading a basis of the null space directly off the reduced row echelon form, without solving anything. It is known as the minus-one trick.
Given the reduced form of , build an matrix: each row whose index corresponds to a pivot column receives the corresponding row of the reduced form, and each row whose index corresponds to a free column receives a on the diagonal and zeros elsewhere. That done, the columns containing those entries form a basis of the null space.
A, with the second row twice the first
Step 1 / 5
The reason it works is direct. Call the square matrix built and its -th column for a free column . The rows of coming from the reduced form are combinations of the rows of , so annihilating those rows against is equivalent to annihilating . And the product of pivot row with is precisely : the entry of the reduced form minus itself, contributed by the .
The columns so obtained are independent by construction, since each carries a in a position where the others have a .
For the matrix in the figure, whose second row doubles the first, the resulting basis is , and both vectors vanish when multiplied by .
Exact basis against numerical basis
scipy.linalg.null_space returns an orthonormal basis in floating point; sympy returns an exact rational basis.
from scipy.linalg import null_space
null_space(A)
# array([[-0.894, -0.186],
# [ 0.447, -0.373],
# [ 0. , 0.909]])
import sympy as sp
sp.Matrix(A).nullspace()
# [Matrix([[-2], [1], [0]]), Matrix([[1], [0], [1]])]
The vectors do not coincide, and there is no reason they should: a subspace admits infinitely many bases. What does coincide is the subspace they span and its dimension. The rational basis from sympy reproduces, up to sign, the one from the minus-one trick; the one from scipy is what the singular value decomposition produces, orthonormal and therefore convenient for projecting.
The choice follows the criterion of the previous lesson: sympy to reason about exact structure in small matrices, scipy for numerical computation.
Rank is a decision, not a fact
In exact arithmetic the rank is determined. In floating point it is not, and the dimension of the null space a program returns depends on that difference.
np.linalg.matrix_rank computes the singular values and counts how many exceed a tolerance:
The figure perturbs a single entry of a rank-deficient matrix and shows the smallest singular value against that tolerance:
- σ₁
- 5.4772
- σ₂
- 4.08e-7
- tolerance
- 3.65e-15
- matrix_rank
- 2
σ₂ against the tolerance
dim ker(A) = 1
ε perturbs a single entry of a rank-1 matrix.
Rank in floating point is a decision about a threshold, not a property read off the matrix.
The jump occurs between and : below it, the matrix is declared rank 1 and the null space has dimension 2; above it, rank 2 and dimension 1. The matrix changes continuously; the program's answer does not.
An instructive detail appears at the extreme. At the smaller singular value is exactly zero, not merely small: the spacing between representable numbers near is about , so 2 + 1e-16 evaluates to 2 and the perturbation never comes into existence.
When the rank matters, the tolerance should be set explicitly rather than left at the default:
np.linalg.matrix_rank(A, tol=1e-10) # threshold matched to the noise in the data
The reasonable criterion is to place the tolerance above the measurement noise of the data and below the magnitude of the directions that are to count as significant.
Application: collinearity
In a linear model, each row of the design matrix is an observation and each column a feature. If one column is a linear combination of others, has a non-trivial null space, and a concrete consequence follows.
X = np.array([[1., 2., 3.],
[2., 1., 3.],
[3., 5., 8.],
[0., 4., 4.]]) # column 3 = column 1 + column 2
n = np.array([1., 1., -1.]) # belongs to the null space
X @ n # array([0., 0., 0., 0.])
For any weight vector and any scalar :
The predictions are identical. Infinitely many weight vectors produce exactly the same fit, and therefore the same value of the loss. The coefficients are not identified: their individual magnitudes carry no interpretation, because they can be shifted arbitrarily along the null space without anything observable changing.
This is the phenomenon known as collinearity, and it explains why the coefficients of a regression with redundant features come out unstable across retrainings. L2 regularisation resolves it by adding to the loss: the term stops being constant along the null space and selects the minimum-norm representative, which is unique.
The diagnosis is direct:
np.linalg.matrix_rank(X) # 2, with 3 columns → one redundant direction
null_space(X) # which combination of features is superfluous
The second call gives more than the first: it reports not merely that redundancy exists, but which specific combination is superfluous, which is the information needed to decide what to drop.
Exercise. Apply the minus-one trick by hand to , which is already in reduced form, and check the result against null_space. Verify that both bases span the same subspace by solving the system expressing each vector of one in terms of the other.