跳到正文
格致开物MATHWIKI

共轭梯度法

共轭梯度法(conjugate gradient method,CG)是求解实对称正定线性系统的 Krylov 子空间迭代法。它沿相互 A 共轭的方向逐次修正,使二次能量在不断扩大的子空间内最小。每轮主要需要一次矩阵向量积,适合大型稀疏系统;非对称或不定矩阵不满足本方法的基本假设。

两步解出一个二阶系统

取 A=(4113),b=(1,2)T,x0=0. 初始残差 r0=b−Ax0=(1,2)T,首方向 p0=r0。因为 Ap0=(6,7)T,步长为 α0=(r0Tr0)/(p0TAp0)=5/20=1/4。故 x1=(1/4,1/2)T,r1=(−1/2,1/4)T. 新残差与旧残差的内积为零。取 β0=(r1Tr1)/(r0Tr0)=1/16,新方向为 p1=r1+β0p0=(−7/16,3/8)T。计算 Ap1=(−11/8,11/16)T,所以 p1TAp1=55/64、α1=4/11。第二步得到 x2=x1+α1p1=(1/11,7/11)T. 代回两行方程分别为一和二,残差为零。这里只用了两步,来自精确算术下二维空间的有限终止性质,不意味着所有大问题都只要少量步数。

二次能量与共轭方向

对称正定性使 ϕ(x)=12xTAx−bTx 有唯一极小点 x∗=A−1b,因为 ϕ(x)−ϕ(x∗)=12(x−x∗)TA(x−x∗). 因此最小化能量等价于最小化 A 范数误差。方向 pi,pj 若满足 piTApj=0,称为 A 共轭,它们通常不是普通欧氏正交。

沿方向 pk 令 xk+1=xk+αpk,对 ϕ 求导得到 −pkTrk+αpkTApk=0。在 CG 的正交关系下 pkTrk=rkTrk,所以 αk=rkTrkpkTApk,rk+1=rk−αkApk,pk+1=rk+1+βkpk,βk=rk+1Trk+1rkTrk. SciPy 的接口明确要求矩阵为 Hermitian 正定,并以残差容限决定是否收敛。[1]

二次能量的椭圆等值线上标出 CG 从原点经过首步再到精确解的路径
两个方向在 A 内积下共轭。首步点已经沿第一方向最小化能量,第二方向不会破坏此前的最小化条件。

正交递推与有限终止的证明

可用归纳法证明:到第 k 步,方向 p0,…,pk 两两 A 共轭,残差 rk 与此前方向正交。新残差满足 pkTrk+1=pkTrk−αkpkTApk=0. 对更早的 pj,新残差与其内积等于 pjTrk−αkpjTApk=0。由于此前残差由此前方向线性组合而成,新残差也与此前残差正交。

要让新方向与当前方向共轭,应取 βk=−rk+1TApk/(pkTApk)。利用 Apk=(rk−rk+1)/αk 和残差正交,分子为 −‖rk+1‖2/αk,代入便得到上述平方残差比。对 j<k,Apj=(rj−rj+1)/αj,新残差与两项均正交,所以新方向也与所有早期方向共轭。归纳闭合。

每个非零共轭方向线性独立:若线性组合为零,与其中一个方向作 A 内积,正定性迫使它的系数为零。因此在 n 维精确算术下最多产生 n 个方向。此时残差与整个空间正交,只能为零。相同正交关系还说明 xk 在 x0+𝒦k(A,r0) 上最小化能量,其中 𝒦k=span⁡{r0,Ar0,…,Ak−1r0}。

浮点运算会破坏精确正交和有限终止。条件数大时,实际步数与残差核验尤其重要。经典误差界 ‖ek‖A≤2[(κ−1)/(κ+1)]k‖e0‖A 使用 κ=λmax/λmin;特征值聚簇时实际表现还可能更好。预条件子正是通过改变有效几何来改善这些性质。[2]

可运行的稠密演示

代码输入实对称正定矩阵、右端、容限和最大步数,返回近似及迭代数。它检查对称性和迭代中出现的正曲率,但不以这些有限检查替代完整正定性证明。演示使用稠密乘法,每步 O(n2) 时间,工作向量为 O(n);稀疏存储时乘法可降为 O(nnz⁡(A))。不修改输入。

Python 3
import math


def cg(A, b, tol=1e-12, maxiter=None):
    A = [list(map(float, row)) for row in A]
    b = list(map(float, b)); n = len(b)
    if not n or len(A) != n or any(len(row) != n for row in A):
        raise ValueError("square matrix matching nonempty b required")
    if not all(math.isfinite(v) for row in A for v in row) or not all(math.isfinite(v) for v in b):
        raise ValueError("finite data required")
    if any(A[i][j] != A[j][i] for i in range(n) for j in range(i)):
        raise ValueError("symmetric matrix required")
    if not math.isfinite(tol) or tol <= 0:
        raise ValueError("positive finite tolerance required")
    maxiter = n if maxiter is None else maxiter
    if not isinstance(maxiter, int) or maxiter < 0:
        raise ValueError("nonnegative integer maxiter required")
    dot = lambda u, v: sum(a*c for a, c in zip(u, v))
    mv = lambda p: [dot(row, p) for row in A]
    x = [0.0]*n; r = b[:]; p = r[:]; rr = dot(r, r)
    if not math.isfinite(rr):
        raise OverflowError("initial residual square outside floating-point range")
    target = tol*max(1.0, math.sqrt(rr))
    if not math.isfinite(target):
        raise OverflowError("residual threshold outside floating-point range")
    for k in range(maxiter+1):
        actual = [v-w for v, w in zip(b, mv(x))]
        if math.hypot(*actual) <= target:
            return x, k
        if k == maxiter:
            break
        Ap = mv(p); denominator = dot(p, Ap)
        if not math.isfinite(denominator) or denominator <= 0 or not math.isfinite(rr):
            raise ArithmeticError("breakdown or nonpositive curvature")
        alpha = rr/denominator
        x = [v+alpha*d for v, d in zip(x, p)]
        r = [v-alpha*d for v, d in zip(r, Ap)]
        rr_new = dot(r, r)
        if not math.isfinite(rr_new) or not all(math.isfinite(v) for v in x):
            raise OverflowError("nonfinite iteration")
        beta = rr_new/rr
        p = [v+beta*d for v, d in zip(r, p)]; rr = rr_new
    raise RuntimeError("true residual did not reach tolerance")


print(cg([[4, 1], [1, 3]], [1, 2]))  # ([1/11, 7/11], 2), up to rounding

有解变式取 A=diag⁡(2,5)、b=(2,0)T。初始残差已经是一个特征向量,首步 α=1/2 就到达 x=(1,0)T。若取不定矩阵 diag⁡(1,−1) 和 b=(1,1)T,第一步分母为零,直接暴露正定假设的作用;这时应选与不定系统匹配的方法。

参考资料

  1. ↑ SciPy developers. scipy.sparse.linalg.cg。
  2. ↑ Richard Barrett et al. Templates for the Solution of Linear Systems,Conjugate Gradient。