☰
Cholesky分解与平方根方法:对称正定矩阵线性方程组高效求解
2026/10/17 4:00:01 网站建设 项目流程

对称正定矩阵线性方程组,几乎是数值计算里最常打交道的一类问题。而平方根方法——也就是 Cholesky 分解——大概是解决这类问题时最优雅、最实用的一把刀。构建有限元刚度矩阵、求解最小二乘正规方程、对高斯分布做随机采样,这些场景背后都在用同一个思路:把一个对称正定矩阵拆成一个三角矩阵和它转置的乘积。把原方程降维成两次带代字的三角方程,计算量和稳定性都能兼顾。

这篇内容会从数学原理讲到代码实现,再把实际工程中容易踩的坑一并拉出来。无论你是刚接触矩阵分解的学生,还是需要在底层手写线性代数库的开发,参考这份笔记都可以省不少时间。

1. 平方根方法的核心思想与数学背景

1.1 从直接求解转向矩阵分解

如果只面对一个具体的线性方程组 (A\mathbf{x} = \mathbf{b}),最朴素的思路当然是高斯消元。但工程里更常见的做法是:多次用同一个系数矩阵 (A),配合不同的右端项 (\mathbf{b}) 求解。比如仿真计算中反复加载不同工况,或者统计模型里对同一协方差矩阵做多次采样。这时候直接消元就太浪费了,更好的策略是把矩阵 (A) 预先拆成两个结构简单的矩阵乘积:

[ A = L L^T ]

其中 (L) 是一个下三角矩阵。这个分解过程只处理一次,之后每次求解都只需要解两个三角方程组:

[ L \mathbf{y} = \mathbf{b}, \quad L^T \mathbf{x} = \mathbf{y} ]

三角方程组可以用前代、回代,直接把解算出来,不需要任何消元操作。这个“先分解、后回代”的模式,是所有现代线性代数库绕不开的根基。

1.2 对称正定:一个被低估的苛刻条件

既然平方根方法要求矩阵对称正定,就必须先想明白“对称正定”到底意味着什么。

对称,就是 (A_{ij} = A_{ji}),矩阵沿主对角线镜像相等。正定,对于任意非零向量 (\mathbf{v}),都有

[ \mathbf{v}^T A \mathbf{v} > 0 ]

用更直白的话说,正定矩阵像一个理想的弹性势能场,任何方向上的扰动都会产生一个正的二次型。正定同时保证了所有特征值都是正的,也保证了 Cholesky 分解过程中每个被除的主元严格大于零。

你可能会问:如果只是对称但不正定呢?那就不能直接用平方根方法。比如对称不定矩阵,它的主元可能在某一步变成负数,开根号在实数域直接卡死。实际工程里经常出现“理论上应当正定,但数值上偏了”的情况,这部分我在第 4 节会详细讲排查思路。

1.3 平方根方法的本质是什么

很多人第一次接触“平方根”三个字会以为是要对矩阵开根号。确实可以这样理解:因为矩阵函数的存在,一个正定矩阵可以有唯一的对称正定平方根,但 Cholesky 分解实际上得到的是下三角因子,它比矩阵平方根计算成本低得多。

从高斯消元的视角来看,对对称正定矩阵做 LU 分解时,主元恰好都是正数,而且上三角矩阵 (U) 恰好就是 (L^T) 乘一个对角阵。进一步把对角阵吸收进去,就得到 (A = L L^T)。

手工递推公式也很朴素。假设我们已经知道 (L) 的前若干列,考虑第 (k) 列元素 (L_{ik}),由矩阵乘法关系:

[ A_{ik} = \sum_{j=1}^{k} L_{ij} L_{kj} ]

拆出最后一项 (L_{ik} L_{kk}),就能反解出 (L_{ik})。对角元则用

[ A_{kk} = \sum_{j=1}^{k} L_{kj}^2 ]

开根号得出。整个过程遵循“从左到右、从上到下”的顺序,一遍扫描即可完成,所以平方根方法的时间复杂度是 (O(n^3/6)),只做实数运算,存储量天然减半。

2. 平方根方法的完整计算过程

2.1 一个 3 阶矩阵的手算演示

与其记公式,不如用具体例子走一遍。设

[ A = \begin{pmatrix} 4 & 6 & 2 \ 6 & 13 & 5 \ 2 & 5 & 6 \end{pmatrix} ]

要找满足 (A = LL^T) 的下三角矩阵

[ L = \begin{pmatrix} l_{11} & 0 & 0 \ l_{21} & l_{22} & 0 \ l_{31} & l_{32} & l_{33} \end{pmatrix} ]

第一步,由 (A_{11}=l_{11}^2),先算

[ l_{11} = \sqrt{4} = 2 ]

第二行第一列关系 (A_{21} = l_{21} l_{11}),于是

[ l_{21} = \frac{6}{2} = 3 ]

同样 (l_{31} = \frac{A_{31}}{l_{11}} = \frac{2}{2} = 1)。

接着处理第二列。矩阵元素 (A_{22} = l_{21}^2 + l_{22}^2),因此

[ l_{22} = \sqrt{13 - 9} = 2 ]

再算 (l_{32}),利用 (A_{32} = l_{31} l_{21} + l_{32} l_{22}),得

[ l_{32} = \frac{5 - 1 \times 3}{2} = 1 ]

最后一列:

[ l_{33} = \sqrt{A_{33} - (l_{31}^2 + l_{32}^2)} = \sqrt{6 - 2} = 2 ]

最终

[ L = \begin{pmatrix} 2 & 0 & 0 \ 3 & 2 & 0 \ 1 & 1 & 2 \end{pmatrix} ]

你可以用矩阵乘法验证一下,重构出来的 (LL^T) 和原矩阵完全相同。这个手算过程虽然简单,但已经暴露了平方根方法最重要的操作:每一步都在做“减去已求和、除以主元、开根号”。如果过程中某个被开根号的数变成负数,就说明原始矩阵在数值上已经不正定了。

2.2 通用算法公式与编程实现

把上面的逐列更新写程通用循环,核心伪代码如下:

for k = 1 to n A[k][k] = sqrt(A[k][k]) for i = k+1 to n A[i][k] = A[i][k] / A[k][k] end for i = k+1 to n for j = k+1 to i A[i][j] = A[i][j] - A[i][k] * A[j][k] end end end

注意这里用了原地刷新:矩阵下三角部分被覆盖为 (L),上三角部分虽然没有被修改,但后续不再需要。用 Python 手写一个不依赖scipy的 Cholesky 分解,可以这样写:

import numpy as np def cholesky(A): """ 对对称正定矩阵 A 执行 Cholesky 分解。 输入 A: 至少传入下三角部分,函数内部会读取下三角。 返回 L: 下三角矩阵,满足 A = L @ L.T """ A = np.asarray(A, dtype=float) n = A.shape[0] L = np.zeros((n, n)) for i in range(n): for j in range(i + 1): s = A[i, j] - np.dot(L[i, :j], L[j, :j]) if i == j: if s <= 0: raise ValueError(f"矩阵不正定,主元 {s} 小于等于零,平方根方法无法继续") L[i, j] = np.sqrt(s) else: L[i, j] = s / L[j, j] return L

求解时,用前代解 (L\mathbf{y} = \mathbf{b}),再用回代解 (L^T\mathbf{x} = \mathbf{y})。这里也可以直接用scipy.linalg.cholesky获得上三角形式,或者用scipy.linalg.cho_factor和cho_solve一把梭。对于大多数工程场景,我推荐直接用cho_factor + cho_solve,因为它能同时返回一个布尔标记,方便你判断分解是否成功。

2.3 改进平方根方法:避开开根的 LDL^T 分解

有些场景下矩阵的正定性和条件数都比较敏感,而且你可能不希望引入计算误差更大的平方根运算,这时候就可以使用改进平方根方法,也叫 LDL^T 分解:

[ A = L D L^T ]

其中 (L) 是单位下三角矩阵,主对角线都是 1,(D) 是对角矩阵。这个方法没有开根号,计算量也更小,特别适合做不定矩阵的分析。它同样能解对称正定方程组,而且数值行为在某些情况下比标准 Cholesky 更稳。很多商用求解器内部默认采用的就是改进平方根法。

3. 数值稳定性、计算量与应用场景分析

3.1 为什么平方根方法比普通高斯消元稳定

高斯消元如果遇到主元过小,常需要部分选主元、交换行,目的是避免放大舍入误差。但对称正定矩阵不需要人为主元策略,因为所有二阶主子式都是正的,主元不可能急剧变小。Cholesky 分解的另一个优势是它只产生乘除法运算,几乎不涉及减法抵消;在进行 (A - \sum L_{ij}L_{kj}^2) 时,因为 (A) 正定,被更新的子矩阵仍然保持正定,这就从结构上保证了舍入误差不会被混沌放大。

从误差分析来看,Cholesky 的误差界大约为

[ | \Delta L | \le c n \varepsilon |A| ]

这里的 (c) 是一个与矩阵规模有关的小常数,(\varepsilon) 是机器精度。相比普通高斯消元中的增长因子问题,平方根方法几乎不会出现灾难性增长。所以在有限元刚度矩阵这类天然对称正定的系统里,Cholesky 是默认选择。

3.2 计算量与存储优势

设矩阵规模为 (n)。LU 分解的计算量大约是

[ \frac{2}{3} n^3 ]

而 Cholesky 分解只需

[ \frac{1}{3} n^3 ]

几乎节省了一半。存储方面只用保存下三角部分的 (n(n+1)/2) 个元素,恰好是完整矩阵的一半。如果使用的是改进平方根法,连平方根运算都省掉,进一步减小了浮点运算次数。

方法分解计算量是否需要选主元存储
高斯消元 LU(2n^3/3)通常需要需存储上下三角
平方根 Cholesky(n^3/3)天然不需要只需下三角
改进平方根 LDL^T(n^3/3)可选只需下三角+对角阵

实际调用现代库时,计算量差距可能因为底层优化的 BLAS 而变得不那么直观,但在稀疏矩阵的填充量上,Cholesky 分解依然有明显的结构优势。

3.3 高频出现的应用场景

  • 有限元静态分析:刚度矩阵是对称正定的,载荷向量多次变化,先分解一次,后续每个工况只需两次三角回代。
  • 最小二乘正规方程:对数据矩阵 (X) 构造 (A = X^TX),当 (X) 列满秩时,(A) 对称正定,可以用平方根方法解正规方程。
  • 高斯过程回归:协方差矩阵的正定性几乎天然满足,Cholesky 被用于采样和对数边际似然计算。
  • 卡尔曼滤波:协方差预测更新中常需要解 (AP = b) 类型的方程,或者直接用 LL^T 做确定性采样。

每次在这些场景里看到原生的 LL^T,我都会觉得“选对算法”比“优化代码”更重要。一个 (n=10^4) 的稠密矩阵,高斯消元和 Cholesky 的耗时差距可能是几分钟和几十秒的区别。

4. 实际操作中的坑与排查技巧

4.1 当矩阵在数值上不正定:最常见的翻车现场

写代码最容易遇到的问题不是算法本身,而是“理论正定、数值不正定”。典型场景:用浮点计算生成了协方差矩阵,由于累积误差,其最小特征值本来是 (10^{-13}) 级别的正数,结果算出来成了 (-10^{-15})。开根号时看到一个负数,直接崩掉。

我的处理手段,按顺序检查:

  1. 先检查原始数据是否有 NaN 或 Inf,这是最隐蔽的坑。
  2. 对矩阵做对称化,如果用A = (A + A.T) / 2再分解,问题经常就消失了。
  3. 如果仍然失败,计算最小特征值,看是否为负。
  4. 工程上最实用的做法是加一个正则化项

[ A' = A + \lambda I ]

只要 (\lambda) 略大于最小负特征值的绝对值即可。这个移位操作在统计学里叫作 jittering,在高斯过程领域几乎是必备技巧。

注意:加正则项会改变原问题的解,所以只能把它当作数值修复手段,不能无脑使用。修复之后应该重新验证残差是否在可接受范围。

4.2 大规模稀疏矩阵的工程实践

稠密矩阵的 Cholesky 分解只需要一段干净循环。但如果是有限元离散带来的稀疏矩阵,直接做标准 Cholesky 会遭遇灾难性的 fill-in——零元在分解过程中变成非零,矩阵越来越“胖”。

解决思路有三类:

  • 先做矩阵重排,常用最小度排序(Minimum Degree)或嵌套剖分排序,目的是让填充量最小。
  • 使用稀疏 Cholesky 库,比如scipy.sparse.linalg.spilu,或者直接使用sksparse.cholmod,它会自动完成重排和分解。
  • 如果矩阵规模极大且需要高精度,可以考虑移交给经过性能调优的 MKL 或 SuiteSparse 求解器。

在个人开发中,我处理过一个约 50 万自由度的结构力学模型,直接稀疏分解在配置较好的机器上十几秒就能完成,但前提是先做良好的节点重排。如果没有重排,内存占用可能直接多一个数量级,甚至分解中途直接卡死。

4.3 条件数对求解精度的影响

正定矩阵条件数可能非常大。以希尔伯特矩阵为例,即使矩阵完全正定,直接 Cholesky 再进行回代,残差可能很小,但误差却很大。这是因为残差 (|\mathbf{r}|) 小不代表解的误差 (|\mathbf{e}|) 小,两者通过条件数联系起来:

[ |\mathbf{e}| \le \kappa(A) \frac{|\mathbf{r}|}{|A|} ]

所以别只盯着残差看。如果问题的条件数是 (10^{12}),那你就必须接受误差大约有 (10^{-4}) 量级。想要更高的精度,要么换扩展精度,要么得重新考虑建模方式,减少病态来源。

4.4 一个小而常见的问题:忘记同步修改右端项

如果矩阵是分块并行分解,或者你在分布式环境里使用 Cholesky,经常有人只把矩阵分解结果分发到各进程,却忘记把右端项 (\mathbf{b}) 也做对应的行重排。这类问题在工程代码里相当隐蔽,出现的报错往往是“解出来的结果完全不对”。强烈建议在求解流程中维护同一个排列向量 (p),分解矩阵和置换向量必须成对保存。

5. 从算法到代码:一个完整的最小二乘示例

5.1 问题定义

考虑一个线性回归问题:已知样本矩阵 (X),每一行是一条样本,列数为特征数;目标变量为 (\mathbf{y})。要求解权重 (\mathbf{w}),使得

[ | X\mathbf{w} - \mathbf{y} |_2^2 ]

最小。当 (X) 列满秩时,正规方程如下:

[ (X^TX)\mathbf{w} = X^T\mathbf{y} ]

因为 (X^TX) 是对称半正定的,列满秩保证正定,所以恰好可以用平方根方法。

5.2 构造正规方程并求解

下面用模拟数据演示完整流程:

import numpy as np from scipy.linalg import cho_factor, cho_solve # 生成模拟数据,n=200个样本,m=5个特征 rng = np.random.default_rng(42) X = rng.normal(size=(200, 5)) true_w = np.array([1.5, -2.0, 0.8, 3.0, -0.5]) y = X @ true_w + 0.05 * rng.normal(size=200) A = X.T @ X b = X.T @ y # Cholesky 分解并求解 factor, lower = cho_factor(A, lower=True) w = cho_solve((factor, lower), b) print("求解得到的权重:", w)

这段代码运行后,得到的权重应该非常接近true_w。如果样本量很大、特征维度也很大,直接用X.T @ X构造可能产生额外舍入误差,更稳妥的做法是直接调用 SVD 或 QR 分解。但在特征数远小于样本数的常规回归里,正规方程 + Cholesky 是性能不错的选择。

5.3 结果验证与误差评估

单独的误差值没有意义,至少应该给出残差范数、相对误差和矩阵条件数:

residual = np.linalg.norm(X @ w - y) relative_error = np.linalg.norm(w - true_w) / np.linalg.norm(true_w) cond_A = np.linalg.cond(A) print(f"残差: {residual:.6f}") print(f"相对误差: {relative_error:.2e}") print(f"正规方程条件数: {cond_A:.3e}")

条件数是 (10^4) 量级时,相对误差大概在 (10^{-11}) 左右。这种残差和误差都能看得到的过程,才能确认算法确实跑对了。

6. 一些实际开发中的经验与思考

6.1 如何选库,而不是重复造轮子

很多时候,直接用现成库才是最稳妥的选择。scipy.linalg.cho_factor已经足够快;如果是真实落地的工业级应用,可以调用经过高度优化的 LAPACK 例程dpotrf和dpotrs。自己写循环版 Cholesky 适合教学和验证原理,真要在生产环境里跑,尽量把核心计算交给底层 BLAS。

但要求是,无论用什么库,都要理解它返回了哪些东西。cho_factor返回的factor并不是标准下三角矩阵,而是缓存了分解结果的紧凑格式。不了解这一点,就可能把因子矩阵直接拿去另作他用而出错。

6.2 分解前请先做一次对称化检查

这是我最想强调的防呆操作。在读取外部数据生成矩阵后,先用类似np.allclose(A, A.T)验证对称性。很多数值库不会在分解前帮你校验,而一个不对称的矩阵硬套对称分解,结果没有任何意义。

6.3 后续还能怎么延伸

如果矩阵规模继续扩大,可以关注分块 Cholesky 或 out-of-core 分解,把大矩阵按块写进磁盘;如果想在 GPU 上跑,可以看 cuSOLVER 的 Cholesky 例程。还可以把平方根方法扩展到广义特征值问题,或者与预处理共轭梯度法结合,用来生成更优质的预条件子。

对我个人而言,平方根方法最迷人的地方在于:它把一个看似复杂的结构化问题,用极其自然的分解形式解决掉,而且几乎不需要任何破坏性操作。每次使用,我都会重新提醒自己:数值计算的核心不是“能算出答案”,而是“在什么条件下一定能算出可靠的答案”。理解矩阵结构、理解算法前提,比背下公式更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询