资讯动态

矩阵的秩:从线性代数定义到SVD数值秩与最小二乘实践

发布时间:2026/10/29 21:13:34 来源:尧图企业网站定制
很多人第一次对矩阵的秩产生怀疑是这样一个场景手算一个 4×4 矩阵行阶梯化之后非零行有 3 行于是写了个结论秩等于 3转头把同样的矩阵丢进代码numpy.linalg.matrix_rank返回 3看着对上了心里踏实了。再过两天换一组数据手算是 4代码返回 3两边开始打架于是开始怀疑人生——到底是自己算错了还是库函数有 bug。我见过太多次这种分歧绝大多数情况下两边都没错问题出在秩这个词在严格数学和浮点计算里根本是两个不同的概念一个要求精确的线性相关性判定一个只能在有限精度下给出一个够用的估计。这篇就围绕矩阵的秩展开把它从定义、几何含义、手算流程、代码实现、阈值选择一直到方程组判定、最小二乘、低秩逼近这些实际落点完整地捋一遍。如果你是在校学生赶线代作业或者是工程岗上要判断一个线性系统是否可控、一个回归设计矩阵是否秩亏再或者是写代码时被matrix_rank的返回值整懵过下面的内容应该都对得上。1. 秩度量的不是非零行数而是独立方向的个数1.1 从方程组里的冗余方程说起要理解秩最省力的入口不是定义而是冗余。假设你解一个三元一次方程组里面有四个方程。直觉上四个方程约束三个未知数应该比三个方程更强但实际上多出来的那个方程很可能只是前三个的线性组合比如它是第一个方程乘以 2 减去第二个方程。这种方程不提供任何新信息删掉它解集完全不变。秩干的事情就是数一数这一堆方程里真正独立、互不重复的到底有几条。把这个想法形式化就得到矩阵秩的第一个定义矩阵的行向量组中极大线性无关组所含向量的个数。所谓极大线性无关组是指从里面挑出一组线性无关的向量再多挑任何一个进去都会变成线性相关。这个数量是唯一的不会因为你挑了哪一组而改变这一点很关键——它保证了秩是一个定义良好的量而不是某种人为约定。行向量是这么数那么列向量呢这就引出矩阵理论里一个不太直观、但极其重要的结论。1.2 行秩等于列秩这件事远没有看上去那么显然行向量组有它的秩叫行秩列向量组有它的秩叫列秩。一个 m×n 的矩阵行向量住在 n 维空间里列向量住在 m 维空间里完全是两个不同的空间凭什么它们的独立方向个数必须相等这件事在数学上要单独证明而且证明过程本身并不平凡常见做法是先用初等行变换把矩阵化到行阶梯形证明行变换不改变行秩同时也不改变列向量之间的线性关系从而把两个秩绑定在一起。从使用的角度你不需要每次都在脑子里跑一遍证明但必须记住这个结论因为它是所有算法的基础。一个直接推论秩不可能超过 m 和 n 中的较小者即 rank(A) ≤ min(m, n)。当 rank(A) min(m, n) 时我们称矩阵是满秩的否则就叫秩亏。方阵满秩等价于可逆等价于行列式非零等价于齐次方程组 Ax 0 只有零解——这一串等价关系在后面的实操里会反复用到。另一个常用定义来自子式矩阵的秩等于它所有非零子式的最高阶数。也就是说rank(A) r 意味着存在一个 r 阶子式不为零而所有 r1 阶及以上的子式全为零。这个定义在理论上很好用因为它把一个线性相关性的定性问题转化成了行列式是否为零的定量判据但在计算上它是个灾难原因后面会讲到。1.3 几何视角秩就是线性映射把空间压扁之后剩下的维数我最喜欢用的一个解释是几何解释。把 m×n 的矩阵 A 看作一个从 n 维空间到 m 维空间的线性映射输入任意向量 x输出 Ax。这个映射会把整个 n 维空间拍到 m 维空间里的某个子空间上这个像空间的维数恰好等于 rank(A)。举个例子一个 3×3 的矩阵如果它的秩是 2那么它把整个三维空间拍成了一个平面。三维空间里所有的点在映射之后都被压到同一个平面上垂直于这个平面的方向被压没了那些被压成零的输入向量构成的集合就是零空间也叫核。零空间的维数和秩之间有一个漂亮的关系dim(零空间) rank(A) n这就是秩-零化度定理。它直接解释了为什么解线性方程组 Ax b 时自由变量的个数是 n − rank(A)每一个自由变量对应零空间里的一个维度你可以沿着这些方向自由移动而不改变等式左边解的自由度就是这么来的。这个视角还解释了数据科学里一个极常见的操作。如果你手里有一个 1000×50 的数据矩阵每一行是一个样本那么它的秩最多是 50。如果实际算出来只有 12说明这 50 个特征里只有 12 个是真正独立的信息源其余 38 个都是这 12 个的线性组合换句话说数据被低维结构锁住了。降维算法的出发点就在这里。2. 手算路线消元、子式以及什么时候该换招2.1 初等行变换求秩流程、主元选择与计数规则手工求秩的主力方法是初等行变换因为初等行变换不改变行空间自然也不改变行秩。具体流程是固定的第一步从第一列开始找一个非零元素作为主元。为了减少分数运算优先挑绝对值小、形式简单的元素比如 1、-1实在没有就挑个能整除同列其它元素的数。第二步用主元所在的行去消掉它下方所有行的该列元素。注意这里是消下方不是把整列都消干净这一步做完矩阵就朝上三角方向走了。第三步主元行固定不动把目光移到右下角的子矩阵重复第一步和第二步。第四步一直做到没有可用的列最后数一数非零行的行数就是秩。这里有几个手算时才体会得到的经验。其一不要急着把元素化成 1很多时候保留整数更好算。比如某一行是 (2, 4, 6)你除以 2 得到 (1, 2, 3)后面确实好算但如果同一列其它行也有 2 的倍数直接做整数加减往往更快。其二主元所在列消元完成后不要再回头去动它因为后续的行变换可能会破坏已经消好的零这是新手最常见的手误来源。其三遇到整行变成零要及时打住那说明这一行已经被前面的行线性表出不需要再参与后续计算。举个具体的例子矩阵[1 2 3] [2 4 6] [1 1 1]第一行主元 1第二行减第一行的 2 倍得 (0,0,0)第三行减第一行得 (0,-1,-2)。此时矩阵变成[1 2 3] [0 0 0] [0 -1 -2]交换第二、三行得到阶梯形非零行两行秩为 2。你会发现第二行是零原因很直接它本来就是第一行的 2 倍。这就是冗余在计算中的直接体现。2.2 子式法的真实成本为什么它只适合极小矩阵子式法的逻辑是从最高阶子式开始往下找第一个不为零的 k 阶子式其阶数 k 就是秩。这个定义很漂亮也很好记但它只适合 2×2、3×3 这种规模。算一笔账就明白了。一个 10×10 的矩阵要判断它是不是满秩按子式法得先算 10 阶行列式计算量按展开式是 10! 级别的如果行列式为零还要检查 C(10,9)×C(10,9) 100 个 9 阶子式再不行检查 C(10,8)×C(10,8) 2025 个 8 阶子式。这个组合爆炸增长得极快10×10 还能勉强算20×20 就已经完全不现实了。所以子式法的正确用法是小矩阵的快速判定而不是通用求秩算法。尤其是当你只需要回答这个方阵满不满秩这一个问题的时候算个行列式用消元法算不是用展开式是最快的。三阶以内直接套对角线法则或者展开比写消元过程更省事。反过来当你需要知道确切秩值、矩阵又超过三四阶时果断放弃子式法走消元。这是我在教学和实际使用里反复验证过的一条分界线。2.3 消元过程中的验算习惯三个能救命的检查点手工算秩出错率不低尤其是带分数的时候。我养成三个验算习惯基本上能把错误率压到很低。第一个检查点是维度。结果秩必须满足 rank ≤ min(m, n)如果算出来比行数还大那一定是哪里数错了。这个检查几乎不花时间但拦下的错误比想象的多。第二个检查点是行数和列数的奇偶性直觉。如果矩阵有 m 行 n 列且 m n那么最多只能有 n 行非零行消元结束后至少要有 m − n 行是零行。如果你的结果里零行数量少于这个数说明消元没做彻底或者中间算错了。第三个检查点是回代验证。挑一个你认为是零行的原矩阵行向量看它是不是真的是其它行向量的线性组合。这个验证在 3×4 以内的矩阵上非常快能直接暴露算术错误。超大型矩阵当然做不了但一旦结果和你预期比如你从业务背景判断它应该满秩不符这个回代就值得做一遍。3. 代码路线精确算、浮点算、阈值算三条路走的是不同的秩3.1 精确域上的秩有理数运算与有限域运算如果你处理的矩阵元素全部是整数或分数而且你需要的是数学意义上绝对精确的秩那么正确的做法是在有理数域上做消元全程用分数表示不做任何除法近似。Python 里可以用fractions.Fraction手工实现也可以用符号计算库的rank()方法后者内部就是做精确消元。这类实现的特点是不存在阈值这个概念判定标准就是严格等于零。from fractions import Fraction def rank_exact(A): A: 二维列表元素为 int 或 Fraction返回精确秩 M [[Fraction(x) for x in row] for row in A] rows, cols len(M), len(M[0]) r 0 for c in range(cols): # 找一个非零主元 pivot None for i in range(r, rows): if M[i][c] ! 0: pivot i break if pivot is None: continue M[r], M[pivot] M[pivot], M[r] # 归一化主元行 pv M[r][c] M[r] [x / pv for x in M[r]] # 消去上方和下方 for i in range(rows): if i ! r and M[i][c] ! 0: factor M[i][c] M[i] [a - factor * b for a, b in zip(M[i], M[r])] r 1 if r rows: break return r这段代码对整数矩阵完全可靠缺点是分数会越滚越大元素多的时候分子分母可能变成大整数速度会掉得很厉害。所以它适合中小规模、且必须精确的场合比如验证一个定理、检查一个组合结构、处理竞赛题或作业题。还有一种更工程师的做法在有限域上算秩。如果你只关心矩阵在模某个素数 p 下是否满秩那就把所有元素对 p 取模在有限域里做高斯消元全程整数运算速度快、绝对精确。这是很多组合算法和编码理论里的标准做法。3.2 浮点数上的秩SVD 与数值秩的定义工程实际中矩阵元素来自测量、来自浮点运算本身就不精确。这时候是否线性相关这个问题变得模糊两个向量可能只是几乎相关对应到矩阵上就是某个奇异值非常小但不为零。这时候秩的定义必须打补丁变成数值秩大于某个阈值的奇异值个数。标准做法是对矩阵做奇异值分解 A UΣVᵀ得到一组非负的奇异值 σ₁ ≥ σ₂ ≥ … ≥ σ_min然后数一数有多少个超过阈值。为什么用 SVD 而不是直接消元因为 SVD 在数值上是稳定的它不会像高斯消元那样放大舍入误差而且奇异值的大小本身就携带了接近相关的程度信息你可以从谱的断崖上直观看出该取哪个秩。import numpy as np A np.array([[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [1.0, 1.0, 1.0]]) u, s, vt np.linalg.svd(A) print(s) # 奇异值 tol s.max() * max(A.shape) * np.finfo(float).eps rank int((s tol).sum()) print(rank)这里的三行代码里藏着这篇文章最需要说清楚的一件事tol怎么定。上面这个阈值来自 LAPACK 的默认约定形式是 σ_max × max(m, n) × ε其中 ε 是机器精度双精度约 2.22e-16。它的含义是把相对于最大奇异值、在机器精度量级上的误差视为零。这是numpy.linalg.matrix_rank的默认行为所以它返回的秩和你手工用sum(s 0)数出来的很可能不一样。3.3 为什么matrix_rank会给出奇怪的结果我遇到最多的疑问是这一类矩阵明明是构造出来的秩 2代码返回 3。原因通常是构造过程中用到了浮点运算比如元素是通过sin、exp或者矩阵乘法算出来的理论上应该精确为零的奇异值变成了 1e-17 这个量级。用sum(s 0)去数它就被算进去了于是秩变成 3。这时候有两个处理方向。如果你希望它被识别为秩 2就用默认阈值或者显式传一个阈值如果你希望严格按浮点值判断就用sum(s 0)但要清楚这意味着什么。更稳的做法是显式指定阈值把判定标准写进代码而不是依赖默认行为rank np.linalg.matrix_rank(A, tol1e-10)这样做的另一个好处是可复现。同一份代码在不同平台、不同 BLAS 实现下默认阈值可能有细微差别显式指定之后行为就固定了。还有一个容易踩的点matrix_rank对整型输入会先转成浮点。一个元素全是整数的矩阵理论上秩可能因为浮点表示而误判虽然双精度能精确表示 2^53 以内的整数一般没问题但如果你的整数特别大就要小心。这种场合下换成精确算法更稳妥。4. 数值实现里最容易被忽略的三件事阈值、缩放、条件数4.1 阈值的选取不是玄学但也确实没有唯一答案阈值取多大取决于你对多小算零的业务定义。默认的 σ_max × max(m,n) × ε 是一个保守的下界它保证在机器精度范围内不会把真实的零误判成非零。但在实际问题里数据本身带有测量噪声噪声量级远大于机器精度这时候用机器精度当阈值就太严格了会把噪声放大出来的伪方向当成真实维度。我的经验是先看一眼奇异值的谱。如果谱上有一个明显的断崖比如前 10 个是 10¹、10⁰ 量级后面 5 个是 10⁻¹⁶ 量级那阈值取在断崖中间任何位置都行结果都是 10。如果谱是平滑衰减的没有明显断崖那说明这个矩阵的有效秩本身就是个连续变化的量强行取一个整数没有太大意义你需要结合下游任务来定比如重构误差降到多少可以接受。有一个常见的错误做法是固定阈值比如 1e-6然后不看数据直接用。这在量纲不一致的矩阵上会直接翻车原因见下一节。4.2 量纲不统一会直接把秩算错设想一个矩阵第一列是长度单位米数值在 1 到 10 之间第二列是质量单位千克数值在 0.001 到 0.01 之间。这两列在数值上差了三个数量级做 SVD 的时候第二列的贡献会被天然压低。如果你的阈值是绝对阈值很可能把第二列整个判成零于是算出来的秩比实际小。解决办法有两个。一是先把各列归一化标准化到同一尺度再算秩二是用相对阈值相对于最大奇异值这样量纲的影响会被部分抵消。前者更彻底后者更省事。我通常建议在数据进入任何线性代数运算之前就做标准化这不止影响秩的计算也影响条件数、影响最小二乘的数值稳定性。4.3 条件数大的矩阵秩是多少这个问题本身就模糊条件数 κ σ_max / σ_min衡量的是矩阵对输入的敏感程度。条件数越大矩阵越接近奇异或者说越接近降秩。一个条件数 10¹² 的方阵理论上满秩但实际上它的最小奇异值只有最大奇异值的万亿分之一在双精度下有效数字掉了一大半。这种矩阵上问秩到底是多少答案取决于你的阈值而阈值的合理取值又取决于噪声水平而噪声水平往往未知。我的处理方式是不纠结于一个整数而是把奇异值谱打印出来结合业务判断哪些方向是可信的。如果下游任务是最小二乘那就直接用 SVD 求伪逆让算法自己处理小奇异值如果是判断可逆性那条件数超过 1/ε 量级时直接判定数值上不可逆更符合实际。这里有个实用的检查算一下 rank(A) 和 rank(A δE)E 是随机扰动δ 是噪声量级。如果扰动之后秩变了说明你原来的秩判定处于临界状态不可靠。5. 判断方程组解的结构秩最经典的用法5.1 三种情况一张表说清楚线性方程组 Ax b 的解的结构完全由系数矩阵 A 和增广矩阵 [A|b] 的秩决定。这张表值得背下来条件解的情况说明rank(A) rank([A|b]) n唯一解n 为未知数个数矩阵列满秩rank(A) rank([A|b]) n无穷多解自由变量个数 n − rank(A)rank(A) rank([A|b])无解增广矩阵多出一个独立方向注意这里的 n 是未知数个数也就是 A 的列数。如果 A 是 m×n 且 m ≠ n上面的判断依然成立只是要小心 n 的定义别搞混成行数。这个错误我见过不止一次尤其是在用代码自动判断的时候。代码里怎么判最稳的做法是分别算两个秩再比较。浮点环境下最好用同一个阈值否则两个秩可能因为阈值差异出现不一致import numpy as np def solve_type(A, b, tol1e-10): A np.asarray(A, dtypefloat) b np.asarray(b, dtypefloat) Ab np.column_stack([A, b]) rA np.linalg.matrix_rank(A, toltol) rAb np.linalg.matrix_rank(Ab, toltol) n A.shape[1] if rA rAb: return 无解 if rA n: return 唯一解 return f无穷多解自由变量 {n - rA} 个5.2 齐次方程组秩直接告诉你解空间的维数Ax 0 永远有零解所以问题不是有没有解而是除了零解还有没有别的。答案很干脆当年 rank(A) n列满秩时只有零解否则解空间维数是 n − rank(A)。这个结论在判断一个线性变换是不是单射时直接用得上列满秩等价于零空间只有零向量等价于映射是单射。对于方阵列满秩等价于可逆等价于行列式非零。实际工程里这个判据常用来检查一个参数估计问题是否唯一可解。如果你有 5 个待估参数观测方程有 8 个但系数矩阵的秩只有 4那说明有一个参数在模型里是不可识别的——别的参数的某种组合可以吸收它的影响你无论用多少数据都估不出它的绝对值。这时候去看零空间基向量哪个分量非零哪个参数就是共线的参与者这是排查模型设计问题最有效的手段之一。5.3 用秩亏诊断建模问题而不是绕开它遇到秩亏绝大多数人的第一反应是加个正则项或者换成伪逆把数值问题绕过去。这当然能让代码跑通但可能掩盖了真实的建模缺陷。我的建议是先查秩亏的原因再决定要不要绕。常见的三种原因对应三种不同的处理。第一种是特征重复两个特征其实是一回事那应该删掉一个。第二种是特征存在硬约束比如所有样本的某个特征都等于另外两个特征之和例如占比数据加起来恒为 100%这种要主动去掉一个或者改用对数比变换。第三种是样本量不足或者采样不充分导致某些方向完全没有数据支撑这时候加正则或者增加采样都对但方向不一样。用零空间基向量来定位问题非常直观对 A 做 SVD取 V 里对应小奇异值的列看哪些分量明显非零。这一步做完问题往往就明摆着了。6. 低秩、最小二乘与控制判据秩在工程里的几个真实落点6.1 最小二乘遇到秩亏为什么pinv能给出一个合理答案标准最小二乘要解 AᵀAx Aᵀb这等价于解正规方程而正规方程有唯一解的前提是 A 列满秩。一旦秩亏AᵀA 就不可逆正规方程有无穷多解。此时最常用的做法是求最小范数解也就是在所有最小二乘解里挑欧氏范数最小的那个。SVD 天然给出这个答案x VΣ⁺Uᵀb其中 Σ⁺ 是把非零奇异值取倒数、零奇异值保持为零之后转置得到的。代码里就是np.linalg.pinv(A) b或者np.linalg.lstsq(A, b, rcondNone)。这两者在数值上都是稳定的不需要你先判断秩。但这里有个容易误解的点伪逆给出的最小范数解只是无穷多解中的一个它不代表正确答案。如果你对参数有额外的物理约束比如非负、比如拓扑关系伪逆给的结果很可能没有意义。我见过有人拿伪逆的结果当物理解释然后得出荒谬结论的。这种情况应该上约束优化而不是线性代数一条道走到黑。6.2 低秩逼近秩决定了你能不能压缩Eckart–Young 定理说的是给定矩阵 A 和任意秩不超过 k 的矩阵 B‖A − B‖ 的最小值由 A 的前 k 个奇异值对应的截断 SVD 达到。换句话说前 k 个奇异向量就是最好的秩 k 近似。这件事的直接应用是数据压缩和降维。一张 1000×1000 的图片如果需要 100 个奇异值才能把误差压到可接受范围那么存储量就从 10⁶ 降到 100×(100010001) ≈ 2×10⁵压缩比接近 5 倍。这个比例不算夸张但说明了一个道理矩阵的有效秩远小于名义维度时压缩才有空间如果奇异值衰减很慢有效秩接近满秩那这个方法就没什么油水。实践中判断要不要用低秩方法就看奇异值谱的衰减速度。我一般会画个对数坐标的谱图看 90% 的能量集中在多少个奇异值里。如果这个数远小于 min(m, n)低秩方法值得一试如果接近满秩趁早换思路。6.3 控制系统里的秩判据可控性与可观性控制理论里有两个经典的秩判据形式上都是某个矩阵满秩。可控性判据说一个 n 维线性系统完全可控当且仅当可控性矩阵 [B, AB, A²B, …, Aⁿ⁻¹B] 的秩等于 n。可观性判据类似把 B 换成 Cᵀ看 [Cᵀ, AᵀCᵀ, …, (Aᵀ)ⁿ⁻¹Cᵀ] 的秩是否等于 n。这两个判据在实际使用中有一个众所周知的坑它是数值上非常脆弱的判据。可控性矩阵里 A 的幂次会让元素量级迅速分化条件数变得极差一个理论上不可控的系统浮点计算出来可能显示满秩反过来也可能误判。工程上更可靠的做法是用 PBH 判据检查 A − λI 和 B 拼起来的矩阵对每个特征值 λ 是否行满秩或者直接做可控性分解、算 Hankel 奇异值。这个例子说明的是秩作为一个理论判据很干净但落到数值实现上必须考虑条件数。判断满不满秩这件事本质上是判断最小奇异值是否大于零而它在临界点附近永远不可靠。6.4 图与网络邻接矩阵的秩透露的结构信息一个无向图的邻接矩阵是对称的它的秩等于非零特征值的个数也等于邻接矩阵的像空间维数。对于二分图邻接矩阵可以写成 [[0, B], [Bᵀ, 0]] 的形式它的秩等于 2·rank(B)是偶数。所以邻接矩阵的秩为奇数可以直接判定这个图不是二分图。在更一般的网络分析里拉普拉斯矩阵 L D − A 的零空间维数等于图的连通分量个数这是个非常实用的结论算 L 的零特征值个数用秩来算就是 n − rank(L)直接得到连通分量数不需要跑图搜索。对于大规模稀疏图这个计算可以用稀疏特征值求解器高效完成。这类应用的共同特点是秩不是最终答案而是通向结构信息的一座桥。你要的往往不是秩是几而是秩比满秩少了多少这个差值才有物理含义。7. 复盘几个典型翻车现场第一个场景是拿行列式判满秩。一个 50×50 的矩阵算行列式得到 1e-12判定接近奇异。但行列式是所有特征值的乘积50 个 0.5 乘起来就很小了跟奇异与否完全没关系。正确的判据是看条件数或者最小奇异值不是看行列式。这个坑太经典了但凡涉及矩阵是否可逆的判断都应该先想一遍这个反例。第二个场景是浮点消元时用 0判断主元。理论上应该为零的元素浮点算出来是 1e-17于是被当成非零主元继续消元整个结果崩掉。正确做法是设一个相对容差用abs(x) tol * scale来判断其中 scale 取该列或该行的最大绝对值。第三个场景是对稀疏大矩阵直接用稠密 SVD。一个百万维的稀疏矩阵稠密化之后内存直接爆掉更不用说 SVD 的复杂度。这类问题的正确工具是稀疏 SVD 或者随机化算法只求前 k 个奇异值对不要试图求完整的谱。第四个场景是矩阵元素量纲差异极大导致阈值失效。前面 4.2 节已经说过解决方案是预处理归一化。第五个场景是把数值秩当成理论秩写进报告。这在学术场景里是要出问题的。如果你的结论依赖于这个矩阵的秩恰好是 k那必须说明计算方法和阈值并且做一次扰动敏感性分析否则这个结论站不住。我对这件事的体会是秩这个概念的美感在于它的定义极其干净——独立方向的个数一个整数不含糊。但只要你把脚踩到浮点数上这个整数就变成了一个带参数的估计值参数选得对不对直接决定结论对不对。所以真正要练的不是会不会消元而是知道什么时候该用精确算法、什么时候该用 SVD、阈值该定在哪儿、以及结论在多大扰动下还成立。这几个判断比任何一条定义都更值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑