18.06 第 10 讲:四个基本子空间
- 118.06 第 1 讲:方程组的行图与列图
- 218.06 第 2 讲:用矩阵做消元
- 318.06 第 3 讲:矩阵乘法的四种看法与逆矩阵
- 418.06 第 4 讲:分解成 A = LU
- 518.06 第 5 讲:转置、置换与向量空间
- 618.06 第 6 讲:列空间与零空间
- 718.06 第 7 讲:解 Ax = 0,主变量与特解
- 818.06 第 8 讲:解 Ax = b,行最简形 R
- 918.06 第 9 讲:线性无关、基与维数
- 1018.06 第 10 讲:四个基本子空间本文
- 1118.06 第 11 讲:矩阵空间、秩 1 矩阵与小世界图
- 1218.06 第 12 讲:图、网络与关联矩阵
- 1318.06 第 13 讲:第一次测验复习
- 1418.06 第 14 讲:正交向量与正交子空间
- 1518.06 第 15 讲:投影到子空间
- 1618.06 第 16 讲:投影矩阵与最小二乘
- 1718.06 第 17 讲:正交矩阵与 Gram-Schmidt
- 1818.06 第 18 讲:行列式的性质
- 1918.06 第 19 讲:行列式公式与代数余子式
- 2018.06 第 20 讲:Cramer 法则、逆矩阵与体积
- 2118.06 第 21 讲:特征值与特征向量
- 2218.06 第 22 讲:对角化与矩阵的幂
- 2318.06 第 23 讲:微分方程与 exp(At)
- 2418.06 第 24 讲:Markov 矩阵与 Fourier 级数
- 2518.06 第 24b 讲:第二次测验复习
- 2618.06 第 25 讲:对称矩阵与正定性
- 2718.06 第 26 讲:复矩阵与快速 Fourier 变换
- 2818.06 第 27 讲:正定矩阵与极小值
- 2918.06 第 28 讲:相似矩阵与 Jordan 标准形
- 3018.06 第 29 讲:奇异值分解
- 3118.06 第 30 讲:线性变换及其矩阵
- 3218.06 第 31 讲:基变换与图像压缩
- 3318.06 第 32 讲:第三次测验复习
- 3418.06 第 33 讲:左逆、右逆与伪逆
- 3518.06 第 34 讲:期末复习
一个 、秩为 的矩阵带着四个子空间:列空间、行空间、零空间、左零空间,维数依次是 、、、。Strang 在这一讲里把它们的基和维数全部交代清楚,我用他黑板上的 3 × 4 矩阵从消元和 SVD 两条路各求了一遍,得到的维数都是 2、2、2、1。然后我随手造了 5 个形状和秩各不相同的矩阵, 与 一共 10 次检查,10 次都成立。
视频:Lecture 10: The four fundamental subspaces(OCW 页面与字幕) 教材:《Introduction to Linear Algebra》第 6 版 §3.5 Dimensions of the Four Subspaces 前置:第 9 讲:线性无关、基与维数 一句话: 里的行空间( 维)与零空间( 维)加起来是 维, 里的列空间( 维)与左零空间( 维)加起来是 维。
- 开头几分钟他在纠正上一讲的一个错误,本篇第一节用代码把它重算了一遍,想看他怎么自嘲可以看,想赶进度可以倍速。
- 他从 消元出 ,再从 的最后一行读出左零空间的那一段要认真看,这是本讲唯一不能从 直接读出来的东西。
- 他讲维数的那一段只给了结论,行空间的维数是 要靠后面用 的前 行来论证,听的时候留意这一步。
- 最后几分钟他开始讲“矩阵本身也是向量”,只是引子,具体的维数他说留到下一讲。我在最后一节先数了一遍。
先改一个错:三列相关,因为有两行相同
上一讲末尾,他为了说明“基不唯一”,取了三个向量 、、,以为第三个会稍微伸出前两个张成的平面。下课后有学生告诉他:不对,这三个向量线性相关。她并没有去解 的系数,她只是把这三个向量排成矩阵的三列,然后看了一眼:
前两行一模一样,所以 不可逆,三列不可能无关。为什么看行能知道列的事? 因为行空间和列空间的维数相同,都等于秩。两行相同,行空间最多 2 维,所以秩最多是 2,三列里最多有两列是独立的。这就是他说的“行告诉了我列的事情”,也是这一讲真正想说的话的预告。
我把这件事算一遍,包括那个他当时懒得去找的组合:
import numpy as np
def main() -> None: np.set_printoptions(precision=4, suppress=True)
# 第 9 讲的错误:三列 (1,1,2)、(2,2,5)、(3,3,8) A = np.array([[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [2.0, 5.0, 8.0]]) print("A 的秩:", np.linalg.matrix_rank(A), ",det =", np.linalg.det(A)) print("A 的转置的秩:", np.linalg.matrix_rank(A.T))
# 学生没有去找组合,而是看到了两行相同;组合其实是 -1 份列一加 2 份列二 coef, *_ = np.linalg.lstsq(A[:, :2], A[:, 2], rcond=None) print("第三列 = 列一和列二的组合,系数:", coef) print("验证:", np.allclose(A[:, :2] @ coef, A[:, 2]))
# 对照:(3, 3, 7) 本来就是列一加列二,(3, 3, 8) 以为能伸出平面 for last in (7.0, 8.0): B = A.copy() B[:, 2] = [3.0, 3.0, last] print(f"第三列 (3, 3, {last:.0f}):秩 =", np.linalg.matrix_rank(B))
if __name__ == "__main__": main()A 的秩: 2 ,det = 0.0A 的转置的秩: 2第三列 = 列一和列二的组合,系数: [-1. 2.]验证: True第三列 (3, 3, 7):秩 = 2第三列 (3, 3, 8):秩 = 2 和 的秩都是 2。第三列等于 份第一列加 份第二列:,系数 是最小二乘返回的,验证为 True。最后两行对比他的两个版本: 和 的秩都是 2,因为只要前两行相同,第三列无论怎么改都不会让秩升到 3。他当时的直觉是对的( 确实是前两列之和),错在以为改个 8 就能伸出平面,却没看那两行。
四个子空间,两个大空间
的矩阵 对应四个子空间:
| 子空间 | 记号 | 是什么 | 住在 | 维数 |
|---|---|---|---|---|
| 列空间 | 列的所有组合 | |||
| 零空间 | 的所有解 | |||
| 行空间 | 行的所有组合 | |||
| 左零空间 | 的所有解 |
“行空间”为什么写成 ? 行是横着躺的,他偏好所有向量都站着,于是把 转置,行就变成了列:行空间就是 的列空间,数学上什么也没发生,只是让躺着的向量站起来。
归属的规则很好记:看向量有几个分量。 里的 有 个分量,所以零空间在 ; 的每一行有 个分量,行空间也在 ; 的每一列有 个分量,列空间在 ; 里的 有 个分量,左零空间也在 。(他说第四个子空间一直受着“二等公民”的待遇,不应该被挤到一边。我读到这里替它觉得有点委屈。)
他先给了维数的“短答案”:
- 列空间:基是 的主列,维数是秩 。
- 零空间:基是特解,每个自由变量一个,所以维数是 。
- 行空间:维数也是 。
- 左零空间: 是 的矩阵,有 列,同一条规则“主变量加自由变量等于列数”,所以维数是 。
我觉得最好记的方式是把它看成预算: 个变量的预算里, 个被方程占用(主变量),剩下的 个自由支配(自由变量),总数不变。 有 个变量,同样的预算表再用一遍。
一次消元,四个基
“短答案”只给了数字,接下来他要拿基来证明它们。他用的矩阵是上一讲末尾的 例子:
他做的消元是:第二行减第一行,第三行减第一行,第二行乘 ,再从第一行里减去两份第二行,得到
主列是第 1、2 列,。前三个子空间的基就在这里:
- 列空间: 的主列,也就是 的第 1、2 列。注意是 的,不是 的。
- 行空间: 的前 行, 和 。
- 零空间:特解,第三、第四列是自由列,各给一个特解。
第四个,左零空间,在 里看不出来。他想到的办法是回忆 Gauss-Jordan:在 右边接一个单位矩阵,消元一路做下去, 变成 。 是一个 矩阵,记着从 到 的全部行操作,所以 。方阵可逆时 , 就是 ;现在 是长方形, 不再是逆,但它仍然是那个把 变成 的矩阵。
下面的脚本自己写了一个 rref(只在前 列里找主元,不然接在后面的单位矩阵会被一起“约化”,得到的就不是 Strang 的 了),然后一次性读出四个基,并逐个验证:
import numpy as np
def rref( M: np.ndarray, ncols: int, tol: float = 1e-10) -> tuple[np.ndarray, list[int]]: """对 M 做 Gauss-Jordan 消元,只在前 ncols 列里找主元。""" R = M.astype(float).copy() m = R.shape[0] pivots: list[int] = [] row = 0 for col in range(ncols): if row == m: break found = np.nonzero(np.abs(R[row:, col]) > tol)[0] if found.size == 0: continue p = row + found[0] R[[row, p]] = R[[p, row]] R[row] /= R[row, col] for i in range(m): if i != row: R[i] -= R[i, col] * R[row] pivots.append(col) row += 1 return R, pivots
def main() -> None: np.set_printoptions(precision=4, suppress=True) A = np.array( [[1.0, 2.0, 3.0, 1.0], [1.0, 1.0, 2.0, 1.0], [1.0, 2.0, 3.0, 1.0]] ) m, n = A.shape
# [A I] 消元成 [R E],E 记录了从 A 到 R 的全部行操作 RE, pivots = rref(np.hstack([A, np.eye(m)]), n) R, E = RE[:, :n], RE[:, n:] r = len(pivots) print("R =\n", R + 0.0) print("E =\n", E + 0.0) print( "主列:", pivots, ",r =", r, ",E @ A == R:", np.allclose(E @ A, R) )
# 列空间:A 的主列(是 A 的,不是 R 的) col_basis = A[:, pivots] # 行空间:R 的前 r 行 row_basis = R[:r] # 零空间:每个自由变量一个特解 free = [j for j in range(n) if j not in pivots] N = np.zeros((n, len(free))) for k, j in enumerate(free): N[j, k] = 1.0 N[pivots, k] = -R[:r, j] # 左零空间:E 的后 m - r 行 left_basis = E[r:].T
print("列空间的基(列):\n", col_basis) print("行空间的基(行):\n", row_basis + 0.0) print("零空间的基(列):\n", N) print("左零空间的基(列):\n", left_basis)
print("A @ N == 0:", np.allclose(A @ N, 0)) print("A.T @ y == 0:", np.allclose(A.T @ left_basis, 0)) dims = [col_basis.shape[1], row_basis.shape[0], N.shape[1]] dims.append(left_basis.shape[1]) print("四个维数:", dims, ",期望:", [r, r, n - r, m - r])
# 行操作不改变行空间,但改变了列空间 same_rows = np.linalg.matrix_rank(np.vstack([A, R])) print("A 与 R 的行叠在一起的秩(等于 r 就是同一个行空间):", same_rows) probe = np.column_stack([R[:, pivots], A[:, 0]]) print( "A 的第一列放进 R 的列空间后的秩(大于 r 就是跑出去了):", np.linalg.matrix_rank(probe), )
if __name__ == "__main__": main()R = [[1. 0. 1. 1.] [0. 1. 1. 0.] [0. 0. 0. 0.]]E = [[-1. 2. 0.] [ 1. -1. 0.] [-1. 0. 1.]]主列: [0, 1] ,r = 2 ,E @ A == R: True列空间的基(列): [[1. 2.] [1. 1.] [1. 2.]]行空间的基(行): [[1. 0. 1. 1.] [0. 1. 1. 0.]]零空间的基(列): [[-1. -1.] [-1. 0.] [ 1. 0.] [ 0. 1.]]左零空间的基(列): [[-1.] [ 0.] [ 1.]]A @ N == 0: TrueA.T @ y == 0: True四个维数: [2, 2, 2, 1] ,期望: [2, 2, 2, 1]A 与 R 的行叠在一起的秩(等于 r 就是同一个行空间): 2A 的第一列放进 R 的列空间后的秩(大于 r 就是跑出去了): 3 与他在黑板上得到的完全一样, 也是他写的 。E @ A == R 为真,所以 确实记录了所有行操作。
左零空间在哪里? 它是 的最后 行:。含义是“ 份第一行加 份第三行等于零行”,第一行和第三行相同,当然如此。脚本里 A.T @ left_basis 全是零,验证了 。
为什么叫“左”零空间? 把 两边转置,得到 : 是一个行向量,从左边乘在 上。零空间是“列的哪些组合给出零列”,左零空间是“行的哪些组合给出零行”。他的约定是不写成行向量,仍然写 ,让向量都站着。
输出的最后四个维数是 ,和 吻合。(这个例子有点不够区分,因为 ,下面的随机矩阵会补上这个漏洞。)
行操作保住的是行空间,不是列空间
他还特意问了一个问题: 和 的行空间、列空间各自一样吗?
- 行空间一样。 每一步行操作都是“把一行减去另一行的倍数”,结果仍然是原来那些行的组合,所以每一行始终留在行空间里;反过来,每一步可逆,倒着做回去, 的行也都是 的行的组合。两组行张成同一个空间。所以 的前 行是 的行空间的基:它们属于这个空间,彼此独立,个数恰好是 。这也就解释了为什么“行空间的维数是 ”:主元有 个,非零行就有 个。
- 列空间不一样。 是 的第一列,但 的列空间只有前两个分量非零的向量,它够不着 。
脚本最后两行用秩验证了这两句话:把 和 的行叠在一起,秩仍是 2,没有长出新方向,说明行空间相同;把 的第一列放进 的列空间,秩从 2 升到 3,说明它跑出去了。所以“列空间的基用 的主列,行空间的基用 的行”这个分工不能搞混:前者必须回到原矩阵,后者必须用化简后的。
用 SVD 再算一遍
消元是手算时代的做法,数值上有更稳的一条路。奇异值分解 (第 29 讲才会正式讲,这里只拿来用)把四个子空间的标准正交基一次给出来: 的前 列是列空间,后 列是左零空间; 的前 列是行空间,后 列是零空间。 就是大于容差的奇异值个数。
我拿它检验两件事:SVD 的基和消元给出的基是不是张成同一个空间(把两组基并排,秩不增加就说明是),以及维数公式在不同形状、不同秩的矩阵上是不是都成立。后者我用 default_rng(1806) 造了 5 个矩阵,每个都是 乘 的随机矩阵,所以秩是已知的 :
import numpy as np
def four_subspaces(A: np.ndarray, tol: float = 1e-10): """用 SVD 给出四个子空间的标准正交基,各以列的形式返回。""" U, s, Vt = np.linalg.svd(A) r = int(np.sum(s > tol)) V = Vt.T return U[:, :r], V[:, :r], V[:, r:], U[:, r:]
def main() -> None: np.set_printoptions(precision=4, suppress=True) A = np.array( [[1.0, 2.0, 3.0, 1.0], [1.0, 1.0, 2.0, 1.0], [1.0, 2.0, 3.0, 1.0]] ) col, row, null, left = four_subspaces(A) print( "SVD 的四个维数:", col.shape[1], row.shape[1], null.shape[1], left.shape[1], )
# 和消元法给出的基张成同一个空间吗?把两组基并排,秩不涨就是同一个 col_rref = A[:, [0, 1]] row_rref = np.array([[1.0, 0.0, 1.0, 1.0], [0.0, 1.0, 1.0, 0.0]]) null_rref = np.array([[-1.0, -1.0, 1.0, 0.0], [-1.0, 0.0, 0.0, 1.0]]).T left_rref = np.array([[-1.0, 0.0, 1.0]]).T pairs = [ ("列空间", col, col_rref), ("行空间", row, row_rref.T), ("零空间", null, null_rref), ("左零空间", left, left_rref), ] for name, svd_basis, rref_basis in pairs: stacked = np.hstack([svd_basis, rref_basis]) print( name, "并排后的秩:", np.linalg.matrix_rank(stacked), ",SVD 的基是标准正交的:", np.allclose(svd_basis.T @ svd_basis, np.eye(svd_basis.shape[1])), )
# 随机造不同形状、不同秩的矩阵,验证 r、n - r、r、m - r rng = np.random.default_rng(1806) print(" m n r | 列 行 零 左 | 行+零=n 列+左=m") for m, n, r in [(3, 6, 2), (5, 3, 3), (4, 4, 4), (6, 5, 1), (7, 9, 4)]: B = rng.standard_normal((m, r)) @ rng.standard_normal((r, n)) c, w, z, lf = four_subspaces(B) dims = (c.shape[1], w.shape[1], z.shape[1], lf.shape[1]) print( f"{m:3d} {n:3d} {r:3d} | {dims[0]:2d} {dims[1]:2d} " f"{dims[2]:2d} {dims[3]:2d} | " f"{dims[1] + dims[2] == n} {dims[0] + dims[3] == m}" )
if __name__ == "__main__": main()SVD 的四个维数: 2 2 2 1列空间 并排后的秩: 2 ,SVD 的基是标准正交的: True行空间 并排后的秩: 2 ,SVD 的基是标准正交的: True零空间 并排后的秩: 2 ,SVD 的基是标准正交的: True左零空间 并排后的秩: 1 ,SVD 的基是标准正交的: True m n r | 列 行 零 左 | 行+零=n 列+左=m 3 6 2 | 2 2 4 1 | True True 5 3 3 | 3 3 0 2 | True True 4 4 4 | 4 4 0 0 | True True 6 5 1 | 1 1 4 5 | True True 7 9 4 | 4 4 5 3 | True True前半段,四个子空间的 SVD 基都和消元的基张成同一个空间:列空间并排后的秩 2,行空间 2,零空间 2,左零空间 1,都恰好等于各自的维数。此外 SVD 给的基还是标准正交的,True 就是这个意思。(消元给的基没有这个性质,这是第 14 讲以后才有用的东西,先记下。)
后半段是维数的验证, 各不相同。、秩 2 的矩阵,零空间有 4 维;、秩 1 的矩阵,左零空间有 5 维; 的满秩方阵,零空间和左零空间都是 0 维。五行里“行 + 零 = ”与“列 + 左 = ”全是 True。我觉得有意思的是 这一行:矩阵看着不小,列空间却只有 1 维,一条直线,剩下 5 个维度全归了左零空间。
矩阵也是向量
最后几分钟,他把“向量”的意思推广了一步:所有 矩阵组成一个向量空间 。能做的运算只有两个,矩阵相加、矩阵乘以数,用不上矩阵乘法。零矩阵是零向量,那八条向量空间公理自然满足。 里有子空间:上三角矩阵、对称矩阵,它们的交集是对角矩阵。
他当场给了对角子空间的基:三个矩阵,比如 、、(他自己说这个 7 选得不怎么样),所以维数是 3。上三角和对称的维数他留到了下一讲。我先数了一遍,办法是把 矩阵拉直成 9 维向量,用秩来数张成的维数:
import numpy as np
def unit(i: int, j: int) -> np.ndarray: """只有 (i, j) 位置为 1 的 3 x 3 矩阵。""" E = np.zeros((3, 3)) E[i, j] = 1.0 return E
def dim_span(mats: list[np.ndarray]) -> int: """把矩阵拉直成 9 维向量,张成的空间的维数。""" return int(np.linalg.matrix_rank(np.array([M.ravel() for M in mats])))
def main() -> None: # 3 x 3 矩阵本身就是向量,整个空间 M 是 9 维的 pairs = [(i, j) for i in range(3) for j in range(3)] everything = [unit(i, j) for i, j in pairs] print("M 的维数:", dim_span(everything))
# Strang 给的三个对角矩阵(其中有他随手写的 7) diagonal = [np.diag(d) for d in ([3.0, 0, 0], [0, 2.0, 0], [0, 0, 7.0])] print("对角子空间 D 的维数:", dim_span(diagonal)) target = np.diag([5.0, -4.0, 1.0]) flat = np.array([D.ravel() for D in diagonal]).T coef, *_ = np.linalg.lstsq(flat, target.ravel(), rcond=None) combo = sum(c * D for c, D in zip(coef, diagonal)) print("对角矩阵的组合系数:", np.round(coef, 4)) print("还原出 diag(5, -4, 1):", np.allclose(combo, target))
# 他留到下一讲的问题:上三角与对称各是几维?交集呢? upper = [unit(i, j) for i, j in pairs if i <= j] sym = [unit(i, j) + unit(j, i) for i, j in pairs if i <= j] both = dim_span(upper + sym) print("上三角:", dim_span(upper), ",对称:", dim_span(sym)) print("二者之和:", both, ",交集的维数:", 6 + 6 - both)
if __name__ == "__main__": main()M 的维数: 9对角子空间 D 的维数: 3对角矩阵的组合系数: [ 1.6667 -2. 0.1429]还原出 diag(5, -4, 1): True上三角: 6 ,对称: 6二者之和: 9 ,交集的维数: 3整个 是 9 维,对角子空间是 3 维。上三角和对称各 6 维,两个子空间合起来张满整个 9 维的 ,所以交集的维数是 ,恰好等于对角子空间的维数,和他说的“交集是对角矩阵”一致。这里用到了子空间维数公式 ,我是从两个子空间的基并排后看秩得到它的(这个公式是我自己加的,Strang 这一讲没有写),在这个例子里它自洽。
在控制里
状态方程 里“系统能不能被控制”,可以直接翻译成这一讲的语言。从零状态出发, 步之内能到达的所有状态,是能控性矩阵 的列的所有组合,也就是 的列空间。所以“能控”就是“列空间是整个 ”,也就是 的秩等于 ,这和第 1 讲“列的组合能不能填满空间”是同一个问题,只是列换成了 、、……。
秩不够的时候, 维的“够不着的方向”不在列空间里,而是由左零空间描述:存在 使得 ,意思是状态沿 的分量 ,无论输入怎么给都推不动。四个子空间里,列空间告诉你够得着哪里,左零空间告诉你够不着哪里,两者的维数加起来是 。
这一节只讲概念,没有跑代码。
四个子空间到这里讲完了,但它们之间还有一层关系没碰:行空间和零空间都在 里,维数加起来是 ,它们到底是怎么摆放的?答案是互相垂直,这是第 14 讲的内容。在那之前,下一讲先把矩阵当成向量继续往下数维数,再看秩为 1 的矩阵长什么样,见 18.06 第 11 讲:矩阵空间、秩 1 矩阵与小世界图。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!











