我经常会在线性代数教材以及论坛讨论中看到不建议使用逆矩阵A−1来求解线性方程Ax=b,尽管我一直遵循这样的原则(实践中逆矩阵确实不够稳定),但仍然不明白不使用逆矩阵的理由。本文总结了我在网上看到的一些关于逆矩阵的讨论,希望能解释为什么要少用逆矩阵来求解线性方程。
数学原理
解线性方程组(逆矩阵)
考虑求解线性方程组中的x∈Rn:
Ax=b
其中A∈Rn×n,b∈Rn。
显然一种简单直观的求解方法是计算A的逆矩阵A−1:
x=A−1b
求解逆矩阵通常需要2n3次浮点数操作(floating-point operations,flops),此外计算A−1b需要2n2 flops,总共2n3+2n2 flops。
不过实践中不推荐逆矩阵求解,因为可能在准确性上产生问题。求解线性方程更常用的是分解方法,比如接下来的LU分解。
解线性方程组(LU分解)
LU分解(lower–upper decomposition)是一种常用的矩阵分解方法,基本思路是利用高斯消元法将A转化为上三角矩阵U,主要过程如下所示:
×××××××××A→×00××××××L1A→×00××0×××L2L1A
其中L1,L2是一系列下三角矩阵,用来表示第n行加上第n-1行乘以某个数的消元操作,所以矩阵A可以消元成为一个上三角矩阵U:
Ln−1Ln−2⋯L1A=U
矩阵A的LU分解为:
A=(L1−1⋯Ln−2−1Ln−1−1)U=LU
下三角矩阵的逆依然是下三角矩阵,并且一系列下三角矩阵的乘积也是一个三角矩阵,因此L是下三角矩阵。此外,虽然涉及到求Li的逆,但是这一步骤实施起来是非常简单的,Li主对角线元素不变、下三角元素全部乘以-1即可得到Li−1。因为Li每次都只对第i列上的主元做消元操作,所以下三角部分只会在第i列存在不为0的元素。以3×3矩阵为例,L1可以写成如下的形式:
1ab010001
很自然的可以证明L1−1为:
1−a−b010001
上述步骤的LU分解需要的浮点数操作近似为32n3 flops。
接下来我们看看如何利用LU分解解线性方程组。约定A\b代表Ax=b的解x(这也是MATLAB求解线性方程组的代码),按照以下步骤求解出x:
LUL\bU\y=A=y=x
所以先求解Ly=b得到中间变量y,再求解Ux=y得到x,看上去比直接求解Ax=b复杂了不少,为什么要这么做呢?
原因在于L和U作为三角矩阵能显著简化线性方程组求解,比如Ly=b,我们有:
1⋮li,1⋮ln,1⋱⋯⋮⋯1⋮⋯⋱⋯1y1⋮yi⋮yn=b1⋮bi⋮bn
可以用递归方法求解y:
y1yi=b1=bi−li,1y1−⋯−li,i−1yi−1
这种思路叫forward substitution,即从上往下求解y。之后可以用相似的方式从下往上求解x,叫backward substitution。forward substitution的成本为(每一个元素各需要i−1次乘法和减法)n2−n flops,同理可得backward substitution的计算成本为n2+n flops,所以用LU分解求解线性方程组的总计算成本为32n3+2n2 flops。
解线性方程组(QR分解)
QR分解(QR decomposition)也常用于求解线性方程组。QR分解将A∈Rn×n分解为正交矩阵Q∈Rn×n和上三角矩阵R∈Rn×n:
A=QR
对于正交矩阵Q,它的逆矩阵Q−1实际就是QT本身,所以对于线性方程组可以简化为:
AxQRxRx=b=b=QTb
因为R是上三角矩阵,对于x的求解可以采用LU分解中介绍的backward substitution方式求解,从而避免了求逆的操作。
QR分解可以采用Gram-Schmidt正交化过程计算得到,这一过程与正交的几何表示密切相关,因此易于理解,不过这种正交化过程容易数值不稳定,因此很少直接在实践中使用。实践中更常用的是使用Givens rotations方法来实现QR分解。
Givens rotations的步骤与LU分解相似,对于矩阵A,采用一系列旋转矩阵Gij将A逐步转化为上三角矩阵:
×××××××××A→××0××××××G31A→×00××××××G21G31A→×00××0×××G32G21G31A
对于需要置0的第aij个元素(i>j),Gij构造为:
Gij=1⋮0⋮0⋮0⋯⋱⋯⋯⋯0⋮cjj⋮sij⋮0⋯⋯⋱⋯⋯0⋮−sji⋮cii⋮0⋯⋯⋯⋱⋯0⋮0⋮0⋮1
其中cjj=cii=cos(θ),sij=sji=sin(θ)。显而易见这是一个对ij平面上的向量进行旋转的矩阵,对于任意向量,若想将其在第i个轴上的分量置0,则需要将其按θ角旋转至对应的坐标轴上,旋转变化不会改变除第i、j行外的其他元素,因此通过反复运用旋转变换,可以将A的下三角区域置0,有:
Gnn−1⋯Gn−11Gn1A=R
又因为旋转矩阵本身就是正交矩阵,所以其逆就是矩阵转置本身,所以有:
A=(Gnn−1⋯Gn−11Gn1)TR=QR
Givens rotations实现的QR分解的复杂度大约是37n3 flops(我也不确定),优势在于可以较为容易的实现并行化操作。
复杂度分析
从上面的分析中可以看出,LU分解大概需要32n3 flops,逆矩阵大概需要2n3 flops,是LU分解的3倍,因此线性方程组求解采用逆矩阵方法会更慢一些。尽管逆矩阵求解存在更快的算法,但是通常来说LU分解还是更有效率,所以一般而言还是应当直接求解线性方程组而不是计算逆矩阵。
此外,逆矩阵计算往往需要更大的内存。如果A是稀疏矩阵(大多数元素是0),则可以使用更加有效率的稀疏结构存储。但是A−1通常是稠密的,因此存储逆矩阵需要更多的内存。
准确性分析
逆矩阵的另一个缺点在于求解的误差更大,特别在A是病态矩阵的情况下尤为明显。
数值分析领域常用前向误差(forward error)和反向误差(backward error)来分析误差。比如对于函数y=f(x),用y^来近似输出y,那么前向误差被定义为∣y^−y∣(这似乎就是我们常说的误差),而反向误差则衡量问题的敏感性,即为了产生近似解,输入数据同真实数据的偏移情况。y^的反向误差是满足y^=f(x+Δx)的最小Δx。
前向误差和反向误差联合起来可以分析一个问题是良态(well-conditioned)还是病态(ill-conditioned)的。对于良态问题,输入较小的改变会导致输出产生较小的改变;而病态问题,输入较小的改变会导致输出较大的改变。条件数(condition number)就是用来判断良态和病态的工具,条件数越大,输入很小的差异就会产生较大的输出误差,更倾向于病态问题。矩阵的条件数κ(A)定义为:
κ(A)=∥A∥∥A−1∥
如果采用矩阵的2范数,上式可以进一步简化为最大特征值λ1与最小特征值λn之比:
κ(A)=λnλ1
stackexchange和这篇blog详细分析了前向误差∥x−x^∥和反向误差∥b−Ax^∥(注意这里x是我们求解问题的输出),有如下结论:
- 对于良态矩阵A,LU分解的前向误差和逆矩阵的前向误差是很接近的
- 对于病态矩阵A,逆矩阵的反向误差会远大于LU分解的反向误差
- 即使对于良态矩阵A,逆矩阵的反向误差也比LU分解的反向误差更大
此外,前向误差和反向误差有如下不等式关系:
∥x−x^∥≤κ(A)∥b−Ax^∥
综合来看的话,利用LU分解、QR分解等方法直接求解线性方程组比逆矩阵求解要更加准确。
源码分析
TODO: compare python and matlab code