给定 n 阶方阵 A 与系数向量 b∈Rn,求解满足
Ax=b
的未知向量 x 的问题被称为线性方程式问题。
本节讨论计算机中如何求解该类问题
线性代数中,如果 detA=0,则可以写出解
x=A−1b
但实际应用上,由于阶数 n 可能非常巨大,使得逆矩阵计算困难。
此时需要考虑基于 Gauss 消元法的求解
如果系数矩阵是一个上三角或者下三角矩阵,那么求解该方程将极其容易
将系数矩阵 A 改写为上三角矩阵 U 与下三角矩阵 L 的乘积,即 A=LU,称为 LU 分解
此时,方程可以改写为
L(Ux)=b
我们可以很轻松地求解出 Ux,进而求解出 x。
并且,我们可以要求下三角矩阵 L 的对角线元素全为 1,以此来保证分解的唯一性
接下来让我们看看如何实现 LU 分解
# 无 Pivoting 的 LU 分解
假设我们已经实现了 LU 分解,也就是 A=LU
对 L=(ℓij) 进行列向量分解,对 U=(uij) 进行行向量分解,得到
A=LU=(l1l2⋯ln)⎝⎜⎜⎜⎜⎛u1Tu2T⋮unT⎠⎟⎟⎟⎟⎞=k=1∑nlkukT
我们关注求和项,各自得到
lkukT=⎝⎜⎜⎜⎜⎜⎜⎜⎜⎜⎛00⋮ℓkk⋮ℓnk⎠⎟⎟⎟⎟⎟⎟⎟⎟⎟⎞(00⋯ukk⋯ukn)=⎝⎜⎜⎜⎜⎜⎜⎜⎜⎜⎛00⋮0⋮000⋮0⋮0⋯⋯⋱⋯⋱⋯00⋮ℓkkukk⋮ℓnkukk⋯⋯⋱⋯⋱⋯00⋮ℓkkukn⋮ℓnkukn⎠⎟⎟⎟⎟⎟⎟⎟⎟⎟⎞
观察不难得到,每一个第 k 个求和项实际上的贡献在于右下角开始的一个边长 ℓ(k) 的方阵
因此,通过这样的逐级分解,就可以得到 LU 分解
具体实现方法如下:
无 Pivoting 的 LU 分解算法
- 关注 A 的第一行第一列,这部分只能由单一的求和项给出,因此可以确定 ℓ1 与 u1T 的值。只需要将 u1T 指定为 A 的第一行,再相除得到 ℓ1
- 令 A1=A−ℓ1u1T
- 关注 A1 的第二行第二列,这部分只能由单一的求和项给出,因此可以确定 ℓ2 与 u2T 的值。只需要将 u2T 指定为 A1 的第二行,再相除得到 ℓ2
- 以此类推
以上,通过 n−1 次除法,n(n2−1)/3 次乘法,n(n2−1)/3 次减法,就可以得到 LU 分解。全体的计算量为 2n3/3,因此时间复杂度为 O(n3)
算法实现
| for(k=1; k<=n-1; k++){ |
| w = 1/a[k][k]; |
| for(i=k+1; i<=n; i++){ |
| a[i][k] = a[i][k] * w; |
| for(j=k+1; j<=n; j++){ |
| a[i][j] = a[i][j] - a[i][k] * a[k][j]; |
| } |
| } |
| } |
这样的 LU 分解有一个极其显著的优点:不占用内存
示例
对矩阵
A=⎝⎛321132343⎠⎞
进行 LU 分解
解
直接令第一行为 u1T
u1T=(3,1,3)
为了让 ℓ1 的第一个成分为 1,可解出
ℓ1=31⎝⎛321⎠⎞=⎝⎛12/31/3⎠⎞
做差
A1=A−ℓ1u1T=⎝⎛321132343⎠⎞−⎝⎛32112/31/3321⎠⎞=⎝⎛00007/35/3022⎠⎞
令第二行为 u2T
u2T=(0,7/3,2)
为了让 ℓ2 的第二个成分为 1,可解出
ℓ2=7/31⎝⎛07/35/3⎠⎞=⎝⎛015/7⎠⎞
做差
A2=A1−ℓ2u2T=⎝⎛00007/35/3022⎠⎞−⎝⎛00007/35/30210/7⎠⎞=⎝⎛000000004/7⎠⎞
因此,矩阵 A 的 LU 分解为
A=LU=⎝⎛12/31/3015/7001⎠⎞⎝⎛30017/30324/7⎠⎞
在完成 LU 分解后,就可以快速求解方程了
令 y=Ux,我们先求解 Ly=b
L 是一个下三角矩阵,通过每一次向矩阵的右侧移动消元,可以得到 y 的值。这个方法叫做 前代 (Forward Substitution)「前進消去」。
此时方程等价于
⎩⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎧y1ℓ21y1+y2ℓ31y1+ℓ32y2+y3⋮ℓn1y1+ℓn2y2+⋯+ℓn,n−1yn−1+yn=b1=b2=b3=bn
对第二个式子代入得到
y2=b2−ℓ21y1
对第三个式子代入得到
y3=b3−ℓ31y1−ℓ32y2
以此类推,我们就可以得到 y 的值了
算法实现
| for(i=1; i<=n; i++){ |
| s = 0; |
| for(j=1; j<=i-1; j++){ |
| s = s + a[i][j] * y[j]; |
| } |
| y[i] = b[i] - s; |
| } |
接下来求解 Ux=y,U 是一个上三角矩阵,通过每一次向矩阵的左侧移动消元,可以得到 x 的值。这个方法叫做 回代 (Backward Substitution)「後退代入」。
此时方程等价于
⎩⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎧u11x1+u12x2+⋯+u1nxnu22x2+⋯+u2nxn⋮unnxn=y1=y2=yn
我们需要反向代入 xn=yn/unn 到倒数第二个式子中,得到
xn−1=un−1,n−1yn−1−un−1,nxn
以此类推,我们就可以得到 x 的值了
算法实现
| for(i=n; i>=1; i--){ |
| s = 0; |
| for(j=i+1; j<=n; j++){ |
| s = s + a[i][j] * x[j]; |
| } |
| x[i] = (y[i] - s) / a[i][i]; |
| } |
# Pivoting 下的 LU 分解
LU 分解基于 Gauss 消元,而消元法中要求每一步所用的主元都不能为 0
如果出现了这种情况,就需要进行行交换,称为 Pivoting
回顾上述 LU 分解算法中,我们存在系数计算
w=akk1
这个计算显然只能在 akk=0 的情况下进行,但是实际应用上可能会出现 akk=0 的情况。细分上来说可以分为两种
- 在 akk=0 的情况下,同一列的其他元素有 aik,1≤i≤n 有非零元。这个时候我们可以考虑对 A 执行行交换来解决问题
- 如果整个列都是 0,那么不仅没办法做 LU 分解,方程本身都没有唯一解。我们也就不讨论了。
记行交换矩阵为 Pij,并简记所有行交换矩阵的乘积为 P,那么此时 LU 分解的形式为
PA=LU
虽然看似没有单纯对 A 执行分解,但是实际上行交换并不会对方程的解造成影响,所以在解方程的视角下是等价的
由于我们需要做除法,即使非零的除数,过小也会导致误差过大。所以原则上我们需要将 akk 替换为第 k 列中绝对值最大的元素来进行除法计算,这样就可以保证数值稳定性了
逐步交换行,并逐步执行 LU 分解的算法被称为 部分 Pivoting 的 LU 分解
等价地,我们也可以进行列的交换,记列交换矩阵为 Qij,并简记所有列交换矩阵的乘积为 Q,那么此时 LU 分解的形式为
AQ=LU
注意 Q−1=QT,因此
Ax=b⇔AQQTx=b⇔LUQTx=b
从左侧逐层求解即可
如果同时允许交换行和列,那么分解的形式为
PAQ=LU
这样的分解被称为 完全 Pivoting 的 LU 分解
# LDU 分解
单纯的 LU 分解虽然可以快速求解矩阵,但是并没有体现出 A 的诸多性质,尤其是正定值。
因此,通过对上三角矩阵 U 执行一次分解
U=DU′
其中 D 是一个对角矩阵,U′ 是一个上三角矩阵,并且 U′ 的对角线元素全为 1,我们就可以得到 (便于理解换回 U)
A=LDU
该分解被称为 LDU 分解
考虑无 Pivoting 下的分解 A=LU
如果 A 是一个对称矩阵,那么
AT=(LDU)T=UTDLT
结合 AT=A,我们可以得到
U=LT
此时,分解形如
A=LDLT
该分解被称为 LDLT 分解
更进一步,若 A 不仅对称,还是一个正定的,这意味着 D 的对角线元素全为正数,因此我们可以将 D 分解为
D=DD
其中 D 是一个将 D 的对角线元素开平方的对角矩阵,此时分解形如
A=LDDLT=(LD)(LD)T
令 C=LD,则分解形如
A=CCT
该分解被称为 Cholesky 分解
# 迭代法
上述的分解再回代的方法在数值计算中被称为直接方法,直接方法的优点是可以稳定地求解方程,缺点是当 n 非常大时,计算量过大,尤其是面对一下具有很多零元素的稀疏矩阵时,直接方法的效率会大大降低,因为分解反而会引入很多非零元素
因此,针对稀疏矩阵,我们可以考虑迭代方法来求解方程
迭代方法的基本思想是先给出一个初始解 x0,然后通过不断迭代来得到更好的解 x1,x2,…,直到满足某个收敛条件为止
考虑线性方程组
Ax=b
我们将通过一系列变形得到
x=Mx+c
其中 M 是一个矩阵,c 是一个向量
记 xk 为第 k 次迭代的解,那么我们通过代入
xk+1=Mxk+c
就可以反复更新 x 的值,并且判断误差
r:=Axk−b
是否足够小
线性方程组问题的迭代法一般有两种
- Gauss-Jacobi 迭代法,这个迭代方法需要占用内存
- Gauss-Seidel 迭代法,这个迭代方法不占用内存,但是由于后者的运算依赖于先前的结果,所以无法并行
我们先来看 Gauss-Jacobi 迭代法
对于矩阵 A,我们可以将其分解为一个一个对角矩阵 D,一个狭义下三角矩阵 E,一个狭义上三角矩阵 F 的和,即
A=D+E+F
其中成分分别为
D=⎝⎜⎜⎜⎜⎛a110⋮00a22⋮0⋯⋯⋱⋯00⋮ann⎠⎟⎟⎟⎟⎞,E=⎝⎜⎜⎜⎜⎛0a21⋮an100⋮an2⋯⋯⋱⋯00⋮0⎠⎟⎟⎟⎟⎞,F=⎝⎜⎜⎜⎜⎛00⋮0a120⋮0⋯⋯⋱⋯a1na2n⋮0⎠⎟⎟⎟⎟⎞
通过移项,原方程改写为
Dx=b−Ex−Fx
对角矩阵可以简单地求逆,因此
x=D−1(b−Ex−Fx)=−D−1(E+F)x+D−1b
接下来就只需要不断地更新 x 的值了
算法实现
| while(...){ |
| for(i=1; i<n; i++){ |
| double s = b[i]; |
| for(j=1; j<n; j++){ |
| if(j != i){ |
| s = s - a[i][j] * x[j]; |
| } |
| } |
| xnew[i] = s / a[i][i]; |
| } |
| for(i=1; i<n; i++){ |
| x[i] = xnew[i]; |
| } |
| } |
这里我们每一次都需要暂时储存一下更新后的 x 的值到 xnew 中,等到更新完成后再将 xnew 的值赋给 x,因此该方法需要占用内存
命题
令 n 阶方阵 A 为行对角占优的,也就是说
∀k:∣akk∣>j=k∑∣akj∣
则 Gauss-Jacobi 迭代法中涉及到的矩阵
M=−D−1(E+F)
的谱半径 ρ(M)<1,因此该迭代法是收敛的
Gauss-Seidel 迭代法考虑直接使用迭代后的值来更新 x 的值
对应到方程式中时,我们每次是先迭代出第一个成分 x1k+1 再利用它去迭代其他成分,所以下三角矩阵 E 对应着迭代后的值,也就是说
Dxk+1=b−Exk+1−Fxk
汇总整理得到
xk+1=−(D+E)−1Fxk+(D+E)−1b
从这里就可以看出该算法的一个劣势是需要计算 (D+E)−1,这并不如直接计算 D−1 来得简单,但是如下算法展示,他不需要占用内存
算法实现
| while(...){ |
| for(i=1; i<n; i++){ |
| double s = b[i]; |
| for(j=1; j<n; j++){ |
| if(j != i){ |
| s = s - a[i][j] * x[j]; |
| } |
| } |
| x[i] = s / a[i][i]; |
| } |
| } |
命题
令 n 阶方阵 A 为正定对称的,也就是说
A=AT,∀x=0:xTAx>0
则 Gauss-Seidel 迭代法中涉及到的矩阵
M=−(D+E)−1F
的谱半径 ρ(M)<1,因此该迭代法是收敛的
# 共轭梯度法
共轭梯度法 (Conjugate Gradient Method) 简称 CG 方法
是一种非常有趣的方法,它告诉我们,求解线性方程组问题也可以转为优化问题
设 A 为一个正定对称矩阵(这是优化问题有解的必要条件),定义二次型
φ(x)=21xTAx−bTx
我们想一想如何求解 φ 的最小值:梯度计算可得
∇φ(x)=Ax−b
因此,φ 的最小值点 x∗ 必须满足
Ax∗=b
这正是我们想要求解的线性方程组问题
而且误差 r=b−Ax=−∇φ(x) 正是 φ 的下降方向
最朴素的想法自然是直接沿着 r=−∇φ(x) 的方向进行梯度下降,设步长为 αk,则更新公式为
xk+1=xk+αkrk
一维精确线搜索要求:在固定的方向 rk 上,找到一个步长 α 使得目标函数最小。令
f(α)=φ(xk+αrk)
则条件为
dαdf(α)=0
利用多元函数链式法则求导
dαdφ(xk+αrk)=∇φ(xk+αrk)T⋅dαd(xk+αrk)=∇φ(xk+αrk)T⋅rk=(A(xk+αrk)−b)T⋅rk=(Axk−b)T⋅rk+α(rk)TArk=−(rk)T⋅rk+α(rk)TArk
解出
αk=rkTArkrkTrk
但是我们回顾这个过程;我们沿着一个方向 rk 在精确线搜索下,走到了函数的最小值点
此时该点的切线方向就是我们的搜索方向 rk,法线方向就是下一步搜索方向 rk+1,因此相邻搜索方向正交,即
⟨rk,rk+1⟩=rkTrk+1=0
如果我们的搜索范围是一个二维平面,来回两次之后我们就会回到与先前平行的方向。然而我们已经走过了在该方向上的最优点,这就会导致先前的结果被破坏,形成 Z 震荡
回到我们的起点,假设 Ax=b 的精确解为 x∗,定义每一次迭代的误差
ek=x∗−xk
将 xk=x∗−ek 和 b=Ax∗ 代入目标函数 φ(xk) 中展开:
φ(xk)=21(x∗−ek)TA(x∗−ek)−(x∗)TAT(x∗−ek)=21(x∗)TAx∗−(x∗)TAek+21(ek)TAek−(x∗)TAx∗+(x∗)TAek=21(ek)TAek−21(x∗)TAx∗
在该结果中,21(x∗)TAx∗ 是一个完全无关的常数项,这意味着数学意义上
Findxminφ(x)⟺Findemin21eTAe
对于方阵 A,定义 A - 范数
∥x∥A=xTAx
并可以自然诱导出内积
⟨x,y⟩A=41(∥x+y∥A2−∥x−y∥A2)=xTAy
那么我们可以说:真正应该关注的是 ∥e∥A 的大小
从当前点 xk 出发,沿着给定的搜索方向 pk 走一个确定的步长 αk,得到下一个点:
xk+1=xk+αkpk
残差 r 的定义得到
rk+1=b−Axk+1=b−A(xk+αkpk)=b−Axk−αkApk=rk−αkApk
可以看出,沿着 pk 走完一步后,系统中产生了一个全新的增量向量:−αkApk。
按照 Gram-Schmidt 正交化的思路,我们把这个 −αkApk 在过去所有残差方向上的投影,一个一个地减掉,剥离出纯净的新维度
rk+1=−αkApk−i=0∑k⟨ri,ri⟩⟨ri,−αkApk⟩ri
如果我们已经计算出了此时的负梯度,也就是残差方向 rk
如果不加任何处理,直接令 pk=rk 并顺着走下去,那就是最速下降法,上面分析已经说过了这会导致问题
我们对 {ri} 执行 A− 范数下的 Gram-Schmidt 正交化,得到新的搜索方向 pk,保证它与之前的残差方向 {ri} 都是 A− 正交的,即
pk=rk−i=0∑k−1⟨pi,pi⟩A⟨rk,pi⟩Api
该算法计算量非常复杂,内存占用也极高
共轭梯度法的高明之处就在于此
由于我们在由 p0,…,pk−1 张成的子空间中找到了极小值,所以当前的负梯度(残差 rk)必然垂直于这个子空间中的所有向量
(rk)Tri=0(∀i<k)
根据残差更新公式 rk+1=rk−αkApk 可以得到
Api=αi1(ri−ri+1)
代入到搜索方向的求和项的分子中
(rk)TApi=(rk)T[αi1(ri−ri+1)]=αi1[(rk)Tri−(rk)Tri+1]
考察求和号中的历史索引 0≤i<k
- 当 i<k−1 时,(rk)Tri=0,因此分子为 0
- 当 i=k−1 时,(rk)Trk−1=0,因此剩余
(rk)TApk−1=−αk−11(rk)Trk
这意味着巨大的求和项中,只有最后一个成分是非零的,因此搜索方向的更新公式可以简化为
pk=rk−βk−1pk−1
接下来推导系数 αk 和 βk 的计算公式,根据条件
(rk+1)Tpk=0
代入 rk+1=rk−αkApk:
(rk−αkApk)Tpk=0⟹αk=(pk)TApk(rk)Tpk
代入 pk=rk−βk−1pk−1:
(rk)Tpk=(rk)T(rk−βk−1pk−1)=(rk)Trk+=0−βk−1(rk)Tpk−1
因此
αk=(pk)TApk(rk)Trk
接下来看 βk,代入求和项中的系数得到
βk=(pk)TApk(rk+1)TApk
代入上述推导中剩余项 (rk)TApk−1 的结果
βk=(pk)TApkαk1(rk+1)Trk+1=(rk)Trk(rk+1)Trk+1
综上,共轭梯度法的算法如下
- 初始化解 x0,设定合适的极小值值 ε
- 计算初始残差 r0=b−Ax0
- 设定初始搜索方向 p0=r0
- 对 k=0,1,2,… 开始迭代
- 计算步长 αk=(pk)TApk(rk)Trk
- 更新解 xk+1=xk+αkpk
- 更新残差 rk+1=rk−αkApk
- 如果 ∥rk+1∥<ε,则认为已经收敛,停止迭代
- 为了更新搜索方向,计算系数 βk=(rk)Trk(rk+1)Trk+1
- 更新搜索方向 pk+1=rk+1−βkpk