↓ Skip to main content
  1. Posts/

从高斯消元到矩阵的 Rank:重新理解主元、自由变量与低秩

·4743 words·10 mins
Note: This article is available in Chinese only. 本文暂无英文版本。 View original

最近在看 LoRA(Low-Rank Adaptation)的时候,碰到了一个公式:

$$ \Delta W = BA $$

其中 \(B \in \mathbb{R}^{m \times r}\),\(A \in \mathbb{R}^{r \times n}\),中间那个维度 \(r\) 被称为 Rank。

我对 Rank 这个词并不陌生——以前在 ACM-ICPC 里写高斯消元的时候,算自由变量个数就用到了矩阵的秩。但当时的关注点全在怎么求解、怎么判断无解多解、怎么把实际问题转化成线性方程组。至于 Rank 本身代表什么数学结构、跟低秩分解有什么关系,说实话没有认真想过。

于是我试着把这些概念重新串一遍:Gaussian Elimination 做了什么,Pivot 到底是什么,Rank 为什么等于 Pivot 的数量,行秩和列秩为什么相等,自由变量的个数从哪来,低秩矩阵为什么可以用更少的参数表示。

这些概念之间的联系比我预期的要紧密。

从线性相关开始
#

先看两个二维向量:

$$ v_1 = (1, 2), \qquad v_2 = (2, 4) $$

\(v_2 = 2v_1\),方向完全相同,只是长度不同。如果我用这两个向量做线性组合 \(a v_1 + b v_2\),不管 \(a\) 和 \(b\) 怎么取,结果永远落在同一条直线上。第二个向量没有提供任何新的方向信息。

这就是 Linear Dependence(线性相关):一组向量中,某个向量可以写成其他向量的线性组合。

反过来,如果一组向量中任何一个都不能写成其他的线性组合,就是 Linear Independence(线性无关)。比如 \(u_1 = (1, 0)\) 和 \(u_2 = (0, 1)\),它们指向完全不同的方向,线性组合可以覆盖整个二维平面。

线性相关与线性无关

一组向量能够张成(Span)的空间,取决于其中有多少个线性无关的向量。把这些向量排成矩阵的行(或列),这个最大线性无关向量的个数,就是矩阵的 Rank。

来看一个具体的矩阵:

$$ A = \begin{bmatrix} 1 & 2 \\ 2 & 4 \end{bmatrix} $$

两行向量分别是 \((1, 2)\) 和 \((2, 4)\),第二行是第一行的 2 倍。只有一个独立方向,所以 \(\operatorname{rank}(A) = 1\)。

对比单位矩阵:

$$ I = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} $$

两行向量分别指向两个坐标轴方向,互相独立,\(\operatorname{rank}(I) = 2\)。

矩阵有多少行、多少列,并不等价于它有多少个独立方向。 一个 \(1000 \times 1000\) 的矩阵完全可能 Rank 只有 3——意味着它的 1000 个行向量中,只有 3 个是真正独立的,其余都是这 3 个的线性组合。

重新理解 Gaussian Elimination
#

这一节想回答的问题是:高斯消元到底在做什么?它跟 Rank 有什么关系?

取一个 \(3 \times 3\) 矩阵:

$$ A = \begin{bmatrix} 1 & 2 & 3 \\ 2 & 5 & 8 \\ 3 & 7 & 11 \end{bmatrix} $$

按标准的高斯消元步骤,用第一行去消掉下面两行的第一列:

$$ R_2 \leftarrow R_2 - 2R_1: \quad \begin{bmatrix} 1 & 2 & 3 \\ 0 & 1 & 2 \\ 3 & 7 & 11 \end{bmatrix} $$$$ R_3 \leftarrow R_3 - 3R_1: \quad \begin{bmatrix} 1 & 2 & 3 \\ 0 & 1 & 2 \\ 0 & 1 & 2 \end{bmatrix} $$

第三行和第二行变得完全一样了。继续消:

$$ R_3 \leftarrow R_3 - R_2: \quad U = \begin{bmatrix} 1 & 2 & 3 \\ 0 & 1 & 2 \\ 0 & 0 & 0 \end{bmatrix} $$

最终得到一个行阶梯形矩阵(Row Echelon Form)\(U\),第三行变成了全零。

这里有几个问题值得停下来想想。

为什么可以用一行减去另一行的倍数? 因为行变换等价于对方程组做等价变换——把一个方程减去另一个方程的倍数,不会改变方程组的解集。

为什么消元是可逆的? \(R_2 \leftarrow R_2 - 2R_1\) 这个操作可以通过 \(R_2 \leftarrow R_2 + 2R_1\) 恢复原样。整个消元过程等价于左乘一个可逆矩阵 \(E\),使得 \(EA = U\)。

第三行变成零意味着什么? 它说明原来的第三行 \((3, 7, 11)\) 是前两行的线性组合。实际上可以验证 \(r_3 = r_1 + r_2\):\((1,2,3) + (2,5,8) = (3,7,11)\)。消元的过程把这个冗余关系暴露了出来。

现在来看 Pivot。在行阶梯形 \(U\) 中,每个非零行的第一个非零元素叫做 Pivot(主元)。这里第一行的 Pivot 是第一列的 1,第二行的 Pivot 是第二列的 1。

Pivot 所在的列叫做 Pivot Column(主元列)。在 \(U\) 中,第 1 列和第 2 列是 Pivot Column,第 3 列不是。

高斯消元与 Pivot

Pivot 的数量就是非零行的数量,也就是矩阵的 Rank:

$$ \operatorname{rank}(A) = 2 $$

高斯消元做的事情,就是通过可逆的行变换,把矩阵中隐藏的线性相关关系一层层剥离出来。 最终剩下的非零行,就是这个矩阵真正独立的行向量,它们的数量就是 Rank。

行秩和列秩为什么相等
#

到这里为止,我一直在用行向量来讨论 Rank。但矩阵也有列向量,列向量的最大线性无关个数叫做 Column Rank(列秩),行向量的叫做 Row Rank(行秩)。

一个事实是,对于任意矩阵,行秩总是等于列秩。这一点我以前知道结论但从来没认真想过为什么。

先在具体矩阵上验证。原矩阵 \(A\) 的三个列向量分别是:

$$ c_1 = \begin{pmatrix} 1 \\ 2 \\ 3 \end{pmatrix}, \quad c_2 = \begin{pmatrix} 2 \\ 5 \\ 7 \end{pmatrix}, \quad c_3 = \begin{pmatrix} 3 \\ 8 \\ 11 \end{pmatrix} $$

可以验证 \(c_3 = -c_1 + 2c_2\):

$$ -\begin{pmatrix} 1 \\ 2 \\ 3 \end{pmatrix} + 2\begin{pmatrix} 2 \\ 5 \\ 7 \end{pmatrix} = \begin{pmatrix} 3 \\ 8 \\ 11 \end{pmatrix} $$

三个列向量中只有两个是独立的,所以 Column Rank = 2,恰好和 Row Rank = 2 一致。

需要注意的是,行向量和列向量的线性组合系数可以完全不同。行向量满足 \(r_3 = r_1 + r_2\)(系数是 1, 1),列向量满足 \(c_3 = -c_1 + 2c_2\)(系数是 -1, 2)。相等的是独立向量的个数,不是线性组合的系数。

为什么一般情况下总是相等?从高斯消元的角度可以看到一条清晰的推理路径。

设 \(E\) 是消元过程对应的可逆矩阵,\(U = EA\) 是行阶梯形。

行变换不改变行秩。 \(E\) 是可逆的,所以 \(U\) 的行空间和 \(A\) 的行空间相同——\(E\) 把原来的行做了可逆的线性组合,不会增加也不会减少独立行的数量。在 \(U\) 中,非零行的数量就是行秩,因为行阶梯形的非零行天然互相独立(每行的 Pivot 位置不同,前面行在该位置已经是零)。

行变换不改变列秩。 这一步稍微需要想一想。假设 \(A\) 的列向量满足某个线性相关关系 \(\alpha_1 c_1 + \alpha_2 c_2 + \dots + \alpha_n c_n = 0\),这等价于 \(A\alpha = 0\),其中 \(\alpha = (\alpha_1, \dots, \alpha_n)^T\)。左乘可逆矩阵 \(E\) 后:

$$ EA\alpha = E \cdot 0 = 0 $$

所以 \(U\) 的列向量也满足完全一样的线性相关关系。反过来,如果 \(U\alpha = 0\),那么 \(A\alpha = E^{-1}U\alpha = 0\)。可逆的行变换不会增加也不会减少列向量之间的线性相关关系,因此列秩也没有改变。

在行阶梯形 \(U\) 中:

  • 非零行的数量 = 行秩
  • Pivot Column 的数量 = 列秩(Pivot Column 对应的列向量互相独立,非 Pivot Column 的列向量可以由 Pivot Column 线性表出)
  • 每个非零行恰好贡献一个 Pivot

所以:

$$ \boxed{\text{Row Rank} = \text{Pivot 数量} = \text{Column Rank}} $$

这就是为什么可以直接说"矩阵的 Rank",不用区分行秩和列秩。

Rank 与自由变量
#

这一节连接回算法竞赛中最常打交道的场景:解线性方程组。

考虑齐次方程组 \(Ax = 0\),其中 \(A \in \mathbb{R}^{m \times n}\),\(\operatorname{rank}(A) = r\)。

对 \(A\) 做高斯消元化成行阶梯形 \(U\) 后,有 \(r\) 个 Pivot Column,对应 \(r\) 个 Pivot Variable(主元变量)。剩下的 \(n - r\) 个变量没有对应的 Pivot,它们可以自由取值,叫做 Free Variable(自由变量)。

用前面那个 \(3 \times 3\) 矩阵来看。\(U = \begin{bmatrix} 1 & 2 & 3 \\ 0 & 1 & 2 \\ 0 & 0 & 0 \end{bmatrix}\),Pivot 在第 1 列和第 2 列,所以 \(x_1\) 和 \(x_2\) 是主元变量,\(x_3\) 是自由变量。

从第二行回代:\(x_2 + 2x_3 = 0\),得 \(x_2 = -2x_3\)。

从第一行回代:\(x_1 + 2x_2 + 3x_3 = 0\),代入 \(x_2 = -2x_3\),得 \(x_1 = x_3\)。

令 \(x_3 = t\),解为:

$$ x = t \begin{pmatrix} 1 \\ -2 \\ 1 \end{pmatrix} $$

自由变量的数量:

$$ \boxed{n - r = 3 - 2 = 1} $$

解空间是一维的——由一个自由参数 \(t\) 张成。

这里有几个需要区分的地方:

  • 齐次方程组 \(Ax = 0\) 至少有零解 \(x = 0\),不可能无解。
  • 非齐次方程组 \(Ax = b\) 可能无解(当 \(b\) 不在 \(A\) 的列空间中)。
  • 对有解的非齐次方程组,自由变量数量仍然是 \(n - r\)。
  • Rank 越小,自由变量越多,解空间的自由度越高。

联系 ACM-ICPC 中的异或高斯消元
#

以前做的HDU 5833就是一个典型的例子。那道题的核心是在 GF(2)(二元有限域,运算规则是异或)上建立方程组,要求每个素因子的指数为偶数——等价于异或为 0 的齐次方程组。

高斯消元的逻辑在 GF(2) 上完全一样:消元、找 Pivot、数自由变量。如果有 \(k\) 个自由变量,每个自由变量有两种取值(0 或 1),解的总数就是 \(2^k\)。题目要求排除全零解(空集),答案就是 \(2^k - 1\)。

需要注意的是,\(2^k\) 这个解的计数方式是 GF(2) 的特性——每个自由变量只有 0 和 1 两个取值。在实数域上,自由变量可以取任意实数值,解有无穷多个(形成一个向量子空间),不能用 \(2^k\) 来计数。

从 Rank 到 Low-Rank Factorization
#

回到开头那个 Rank = 1 的矩阵:

$$ W = \begin{bmatrix} 1 & 2 \\ 2 & 4 \end{bmatrix} $$

因为第二行是第一行的 2 倍,只有一个独立方向。实际上可以把它写成两个向量的外积:

$$ W = \begin{bmatrix} 1 \\ 2 \end{bmatrix} \begin{bmatrix} 1 & 2 \end{bmatrix} $$

验证一下:右边是一个 \(2 \times 1\) 矩阵乘以一个 \(1 \times 2\) 矩阵,结果确实是 \(\begin{bmatrix} 1 & 2 \\ 2 & 4 \end{bmatrix}\)。

四个元素的矩阵被拆成了两个向量(共 4 个标量),看起来没省什么。但如果矩阵更大呢?

推广一下。如果 \(W \in \mathbb{R}^{m \times n}\),且 \(\operatorname{rank}(W) = r\),那么 \(W\) 可以精确分解为:

$$ W = BA $$

其中 \(B \in \mathbb{R}^{m \times r}\),\(A \in \mathbb{R}^{r \times n}\)。

为什么 \(\operatorname{rank}(BA) \leq r\)?乘积 \(BA\) 的每一列是 \(B\) 的 \(r\) 个列向量的线性组合——\(A\) 的每一列提供组合系数。无论怎么组合,结果都落在 \(B\) 的列向量张成的 \(r\) 维子空间里,所以 \(BA\) 的列秩不可能超过 \(r\)。

Low-Rank Factorization

矩阵的元素数量(\(m \times n\) 个标量),并不等价于它包含的独立线性信息量。 如果 Rank 远小于 \(\min(m, n)\),矩阵中的大部分信息是冗余的,可以用 \(m \times r + r \times n\) 个参数来精确表示。

当然,对于满秩矩阵,用较小的 \(r\) 做分解只能得到近似,不能精确重建。

矩阵分解的思想在推荐系统中很早就有应用。以前写过一篇LFM 隐因子模型笔记,核心就是把稀疏的用户-物品评分矩阵 \(R\) 近似分解为 \(P \times Q\),中间维度 \(K\) 对应隐因子的数量。同样的低秩分解思路,只是应用场景不同。

回到 LoRA
#

现在回到文章最开始的问题。

微调一个预训练模型,常规做法是直接更新权重矩阵:

$$ W' = W + \Delta W $$

其中 \(\Delta W\) 是训练过程中学到的增量,跟 \(W\) 同样大小。对于大语言模型中动辄 \(4096 \times 4096\) 的权重矩阵,\(\Delta W\) 有 \(4096^2 = 16{,}777{,}216\) 个参数需要训练。

LoRA 的核心假设是:微调过程中,权重的增量 \(\Delta W\) 不需要是满秩的——用一个低秩矩阵就够了:

$$ W' = W + BA $$

\(B \in \mathbb{R}^{4096 \times r}\),\(A \in \mathbb{R}^{r \times 4096}\)。预训练的 \(W\) 保持冻结不动,只训练 \(B\) 和 \(A\)。

取 \(r = 8\),可训练参数量:

$$ 4096 \times 8 + 8 \times 4096 = 65{,}536 $$

是原始矩阵参数量的 \(1/256\)。

这里有几个需要注意的地方:

  • 上面的对比只是单个目标矩阵的参数量,不是整个模型的显存节省比例。
  • \(r = 8\) 限制的是 \(\operatorname{rank}(\Delta W) \leq 8\),并不是说 \(\Delta W\) 的 Rank 一定等于 8——实际训练中 Rank 可能更低。
  • 原始的 \(W\) 本身可以是满秩矩阵,低秩约束只加在增量上。
  • LoRA 能工作的前提假设是:对于特定下游任务,权重的有效调整集中在少数几个方向上,不需要在所有方向上都有变化。这个假设在很多任务上经验性地成立,但不是对所有场景都一定成立。

LoRA 的初始化策略、缩放系数 \(\alpha / r\)、优化器状态、和全量微调的性能对比,这些工程细节留给独立的 LoRA 博客。这篇只关心数学上的一件事:为什么两个小矩阵可以代替一个大矩阵做更新。

写在最后
#

回头看,这篇文章想串起来的东西并不复杂:

Gaussian Elimination 通过消除线性相关的冗余,揭示矩阵有多少个独立约束——这个数量就是 Rank。 行变换是可逆的,不会增加也不会减少独立方向,所以行秩和列秩总是相等。

低秩矩阵的冗余意味着更紧凑的参数表示。 如果一个 \(m \times n\) 的矩阵只有 \(r\) 个独立方向,它的信息完全可以用 \(m \times r + r \times n\) 个参数来编码。

LoRA 把这个数学性质用到了神经网络微调上:约束权重更新的 Rank,用远少于原始矩阵的参数实现有效的适配。

Gaussian Elimination、Matrix Rank、Low-Rank Factorization 并不是三个独立的知识点。它们背后是同一件事——线性相关、独立方向、信息冗余——只是分别从算法执行、数学结构、参数表示的角度在观察。

当然,高斯消元本身并不能证明 LoRA 的低秩更新假设在实际任务中有效——那是一个经验性的发现。但至少在数学层面上,理解 Rank 的含义之后,\(\Delta W = BA\) 这个公式就不再神秘了。

参考资料
#

Related

从投机解码到 Rand5 模拟 Rand7:跨越十年的拒绝采样直觉

上一篇理清了 Prefill 和 Decode 的因果链之后,我开始翻 Decode 阶段的加速方案。结论已经很清楚了:在单请求、小 Batch 的典型场景下,Decode 阶段是 memory-bound 的——每生成一个 token 就要把整个模型权重从显存搬一遍,计算单元大部分时间在等数据(Batch 较大或模型结构不同时,瓶颈也可能转向 compute-bound,但单次请求的延迟优化场景下 memory-bound 是常态)。

PagedAttention:为什么 KV Cache 需要自己的内存管理系统?

前一篇写完之后,KV Cache 在我脑子里的形象已经变了——它不再是一次 forward 里的临时中间量,而是一个跨多轮存活、随 Decode 持续增长、每个请求各自独立的状态。 但"知道它是有状态的"和"知道怎么管它"是两件事。当我开始想具体的内存管理问题的时候,发现事情比想象的复杂。

从一次 Forward 到持续生成:LLM Inference 为什么不一样?

最近在从传统模型 Serving 转向 LLM Inference,一个让我停下来想了很久的问题是:为什么 LLM 的推理系统看起来和我之前熟悉的那套完全不一样? 我之前做推荐模型和 CV 模型的部署,排序模型的 Serving 架构虽然在工程上也有不少复杂性(远程 embedding 读取、稀疏参数缓存等),但核心计算模式不难概括:一个请求进来,跑一次 forward,返回结果。LLM Serving 里冒出来一大堆新概念——推理要分成 Prefill 和 Decode 两个阶段,中间要维护 KV Cache,请求调度要做 Continuous Batching,显存管理要用 PagedAttention——我花了些时间才把它们背后的因果链串起来。