在数学上，**广义最小残量方法**(一般简称**GMRES**)是一个求解线性方程组 数值解的迭代方法。这个方法利用在Krylov子空间中有着最小残量的向量来逼近解。Arnoldi迭代方法被用来求解这个向量。

GMRES方法由Yousef Saad和Martin H. Schultz在1986年提出。

GMRES方法
-------

需要求解的线性方程组记为

{\displaystyle Ax=b\,}。
假设矩阵*A*是*n*\times*n*阶的可逆的。进一步，假设*b*是标准化的，即||*b*|| = 1 (在这篇文章中，||·||是Euclidean范数)。 

这个问题的*m*阶Krylov子空间是

{\displaystyle K\_{m}=\operatorname {span} \,\{b,Ab,A^{2}b,\ldots ,A^{m-1}b\}\,}。
GMRES通过使得残量*Ax**m* − *b*最小的向量*x**m* ∈ *K**m*来逼近*Ax* = *b*的精确解。

但是，向量*b*, *Ab*, …, *A**m*−1*b*几乎是线性相关的。因此，用Arnoldi迭代方法求得的这组*K**m*的标准正交基

{\displaystyle q\_{1},q\_{2},\ldots ,q\_{m}\,}
来取代上面的那组基。所以，向量*x**m* ∈ *K**m*写成*x**m* = *Q**m**y**m*，其中*y**m* ∈ **R***m*且*Q**m*是由*q*1, …, *q*m组成的*n*\times*m*矩阵。

Arnoldi过程也产生一个 (*m*+1)\times*m*阶上Hessenberg矩阵{\displaystyle {\tilde {H}}\_{m}}满足

{\displaystyle AQ\_{m}=Q\_{m+1}{\tilde {H}}\_{m}\,}。
因为{\displaystyle Q\_{m}}是正交的，我们有

{\displaystyle \|Ax\_{m}-b\|=\|{\tilde {H}}\_{m}y\_{m}-\beta e\_{1}\|\,}，
其中

{\displaystyle e\_{1}=(1,0,0,\ldots ,0)\,}
是**R***m*+1的标准基的第一个向量，并且 

{\displaystyle \beta =\|b-Ax\_{0}\|\,} ，
其中x\_{0}是初始向量(通常是零向量)。因此，求使得残量

{\displaystyle r\_{m}={\tilde {H}}\_{m}y\_{m}-\beta e\_{1}}。
的范数最小的x\_{m}。这是一个*m*阶线性最小二乘问题。

这就是GMRES方法。在迭代的每一步中：

1. 做一步Arnoldi迭代方法；
2. 寻找使得||*r**m*||最小的{\displaystyle y\_{m}} ；
3. 计算{\displaystyle x\_{m}=Q\_{m}y\_{m}} ；
4. 如果残量不够小，重复以上过程。

在每一步迭代中，必须计算一次矩阵向量积*Aq**m*。对于一般的*n*阶稠密矩阵，这要计算复杂度大约2*n*2次浮点运算。但是对于稀疏矩阵，这个计算复杂度能减少到O(*n*)。进一步，关于矩阵向量积，在*m*次迭代中能进行O(*m* *n*)次浮点运算。

收敛性
---

第*m*次迭代获得在Krylov子空间*K**m*下的最小残量。因为每个子空间包含于下一个子空间中，所以残量单调递减。在第*n*次迭代后，其中*n*是矩阵*A*的阶数，Krylov空间*K**n*是完整的**R***n*。因此，GMRES方法达到精确解。然而，问题在于：在极少的几次迭代后(相对于*n*)，向量*x**m*几乎已经是精确解的一个很好的逼近。

但是，在一般情况下这是不会发生的。事实上，Greenbaum，Pták和Strakoš的理论说明了对于每一个单调减少的序列*a*1, …, *a**n*−1, *a**n* = 0 ，能够找到一个矩阵*A*对于所有*m*满足||*r**m*|| = *a**m* ，其中*r**m*是上面所定义的残量。特别的，有可能找到一个矩阵，使得前*n* − 1次迭代的残量一直保持为常数，而只在最后一次迭代时达到零。

在实验中，GMRES方法经常表现得很好。在特殊的情况下这能够被证明。如果*A*是正定的，则

{\displaystyle \|r\_{m}\|\leq \left(1-{\frac {\lambda \_{\mathrm {min} }(A^{T}+A)}{2\lambda \_{\mathrm {max} }(A^{T}+A)}}\right)^{m/2}\|r\_{0}\|}，
其中{\displaystyle \lambda \_{\mathrm {min} }(M)}和{\displaystyle \lambda \_{\mathrm {max} }(M)}分别为矩阵M的最小和最大特征值。

如果*A*是对称的并且是正定的，则

{\displaystyle \|r\_{m}\|\leq \left({\frac {\kappa \_{2}^{2}(A)-1}{\kappa \_{2}^{2}(A)}}\right)^{m/2}\|r\_{0}\|}。
其中{\displaystyle \kappa \_{2}(A)}记为*A*在Euclidean范数下的条件数。

一般情况下，其中*A*是非正定的，则

{\displaystyle \|r\_{m}\|\leq \inf \_{p\in P\_{m}}\|p\_{m}(A)\|\leq \kappa \_{2}(V)\inf \_{p\in P\_{m}}\max \_{\lambda \in \sigma (A)}|p(\lambda )|\,}，
其中*P**m*记为次数不超过*m*且*p*(0) = 1的多项式的集合，*V*是*A*的谱分解中的矩阵，而σ(*A*)是*A*的谱。粗略的说，当*A*的特征值聚集在远离原点的区域且*A*离正规不太远时，收敛速度较快。

所有的不等式只界定残量，而不是实际误差(精确解和当前迭代*x**m*之间的距离)。

GMRES方法的拓展( Restarted GMRES )
-----------------------------

同其他迭代方法一样，为了加快收敛，GMRES经常结合预处理方法。

迭代的开销以O(*m*2)增长，其中*m*是迭代次数。然而有时候，GMRES方法在*k*次迭代后重新开始，即*x**k*又变回初始值。这样的方法叫做GMRES(*k*)。

与其他解法的比较
--------

对于对称矩阵，Arnoldi迭代方法变成Lanczos迭代方法。对应的Krylov子空间方法叫做Paige和Saunders的最小残量方法(MinRes)。不像非对称的情况，MinRes方法由三项循环关系(three-term recurrence relation)给出，并且同GMRES一样，使残量的范数最小。而对于一般矩阵，Krylov子空间方法不能由短的循环关系(short recurrence relation)给出。

另一类方法由非对称Lanczos迭代方法给出，特别的是BiCG方法。这个利用了three-term recurrence relation，但他们没有达到最小的残量，因此对于这些方法残量不会单调递减。收敛性是不能保证的。

第三类方法由CGS和BiCGSTAB给出。这些也由three-term recurrence relation给出(因此，非最优)。而且可能过早的终止迭代了而没有达到收敛的目的。这些方法的想法是合适的选择迭代序列所产生的多项式。

对于所有矩阵，这三类方法都不是最好的；总有例使得一类方法好于另一类。因而，各种解法应该进行实际的试验，来决定对于给定的问题哪一种是最优的。

求解最小二乘问题
--------

GMRES方法的其中一部分是求解向量{\displaystyle y\_{m}}使得

{\displaystyle \|{\tilde {H}}\_{m}y\_{m}-e\_{1}\|\,}
最小。这个可以通过计算QR分解来实现：找到一个(*m*+1)\times(*m*+1)阶正交矩阵Ω*m*和一个(*m*+1)\times*m*上三角矩阵{\displaystyle {\tilde {R}}\_{m}}满足

{\displaystyle \Omega \_{m}{\tilde {H}}\_{m}={\tilde {R}}\_{m}}。
三角矩阵的行数比列数多1，所以它的最后一行由零组成。因此，它能被分解为

{\displaystyle {\tilde {R}}\_{m}={\begin{bmatrix}R\_{m}\\0\end{bmatrix}}}，
其中R\_m是一个*m*\times*m*阶三角(方)矩阵。

QR分解能够简单的进行下去(update)，从一步迭代到下一步迭代。因为每次的Hessenberg矩阵只在一行零元素和一列元素上有所不同：

{\displaystyle {\tilde {H}}\_{m+1}={\begin{bmatrix}{\tilde {H}}\_{m}&h\_{m}\\0&h\_{m+1,m}\end{bmatrix}}} ，
其中*h**m* = (*h*1*m*, … *h**mm*)T。这意味着，Hessenberg矩阵左乘上Ω*m*的扩大矩阵(通过并上零元素和单位元素)，所得到的是类似于三角矩阵的矩阵：

{\displaystyle {\begin{bmatrix}\Omega \_{m}&0\\0&1\end{bmatrix}}{\tilde {H}}\_{m+1}={\begin{bmatrix}R\_{m}&r\_{k}\\0&\rho \\0&\sigma \end{bmatrix}}}
这个矩阵可以三角化，如果σ为零。为了修正这个矩阵，需要进行Givens旋转

{\displaystyle G\_{m}={\begin{bmatrix}I\_{m-1}&0&0\\0&c\_{m}&s\_{m}\\0&-s\_{m}&c\_{m}\end{bmatrix}}}
其中

{\displaystyle c\_{m}={\frac {\rho }{\sqrt {\rho ^{2}+\sigma ^{2}}}}\quad {\mbox{and}}\quad s\_{m}={\frac {\sigma }{\sqrt {\rho ^{2}+\sigma ^{2}}}}}。
通过这个Givens旋转，我们构造

{\displaystyle \Omega \_{m+1}=G\_{m}{\begin{bmatrix}\Omega \_{m}&0\\0&1\end{bmatrix}}}。
事实上，

{\displaystyle \Omega \_{m+1}{\tilde {H}}\_{m+1}={\begin{bmatrix}R\_{m}&r\_{m}\\0&r\_{mm}\\0&0\end{bmatrix}}\quad {\text{其 中}}\quad r\_{mm}={\sqrt {\rho ^{2}+\sigma ^{2}}}}
是一个三角矩阵。 

给出了QR分解，最小值问题就容易解决了。注意到

{\displaystyle \|{\tilde {H}}\_{m}y\_{m}-e\_{1}\|=\|\Omega \_{m}({\tilde {H}}\_{m}y\_{m}-e\_{1})\|=\|{\tilde {R}}\_{m}y\_{m}-\Omega \_{m}e\_{1}\|}。
记{\displaystyle \Omega \_{m}e\_{1}}为

{\displaystyle {\tilde {g}}\_{m}={\begin{bmatrix}g\_{m}\\\gamma \_{m}\end{bmatrix}}}
其中*g**m* ∈ **R***m*和γ*m* ∈ **R**，则

{\displaystyle \|{\tilde {H}}\_{m}y\_{m}-e\_{1}\|=\|{\tilde {R}}\_{m}y\_{m}-\Omega \_{m}e\_{1}\|=\left\|{\begin{bmatrix}R\_{m}\\0\end{bmatrix}}y-{\begin{bmatrix}g\_{m}\\\gamma \_{m}\end{bmatrix}}\right\|}。
使得这个表达式最小的向量*y*为

{\displaystyle y\_{m}=R\_{m}^{-1}g\_{m}}。
再一次，向量g\_{m}能够简单的进行下去(update)。

Example code
------------

### Regular GMRES (python3)

```
# from "https://github.com/J-N-ch/GMRES\_py\_restart/blob/master/GMRES\_API/GMRES.py"
import numpy as np
import math

class GMRES\_API(object):
    def \_\_init\_\_( self,
                  A\_coefficient\_matrix: np.array([], dtype = float ),
                  b\_boundary\_condition\_vector: np.array([], dtype = float ),
                  maximum\_number\_of\_basis\_used: int,
                  threshold = 1.0e-16 ):

        self.A = A\_coefficient\_matrix
        self.b = b\_boundary\_condition\_vector
        self.maximum\_number\_of\_basis\_used = maximum\_number\_of\_basis\_used
        self.threshold = threshold

    def initial\_guess\_input( self, x\_input\_vector\_initial\_guess: np.array([], dtype = float ) ):

        self.x = x\_input\_vector\_initial\_guess

        try:
            assert len( self.x ) == len( self.b )

        except Exception:

            print(" The input guess vector's size must equal to the system's size !\n")
            print(" The matrix system's size == ", len( self.b ))
            print(" Your input vector's size == ", len( self.x ))
            self.x = np.zeros( len( self.b ) ) 
            print(" Use default input guess vector = ", self.x, "instead of the incorrect vector you given !\n")

    def run( self ):

        n = len( self.A )
        m = self.maximum\_number\_of\_basis\_used

        r = self.b - np.dot(self.A , self.x)
        r\_norm = np.linalg.norm( r )

        b\_norm = np.linalg.norm( self.b )

        self.error = np.linalg.norm( r ) / b\_norm
        self.e = [self.error]
        
        # initialize the 1D vectors 
        sn = np.zeros( m )
        cs = np.zeros( m )
        e1 = np.zeros( m + 1 )
        e1[0] = 1.0

        beta = r\_norm \* e1 
        # beta is the beta vector instead of the beta scalar

        H = np.zeros(( m+1, m+1 ))
        Q = np.zeros((   n, m+1 ))
        Q[:,0] = r / r\_norm

        for k in range(m):

            ( H[0:k+2, k], Q[:, k+1] )    = \_\_class\_\_.arnoldi( self.A, Q, k)
            ( H[0:k+2, k], cs[k], sn[k] ) = \_\_class\_\_.apply\_givens\_rotation( H[0:k+2, k], cs, sn, k)
            
            # update the residual vector
            beta[ k+1 ] = -sn[k] \* beta[k]
            beta[ k   ] =  cs[k] \* beta[k]

            # calculate and save the errors
            self.error = abs(beta[k+1]) / b\_norm
            self.e = np.append(self.e, self.error)

            if( self.error <= self.threshold):
                break

        # calculate the result
        #y = np.matmul( np.linalg.inv( H[0:k+1, 0:k+1]), beta[0:k+1] )
        #TODO Due to H[0:k+1, 0:k+1] being a upper tri-matrix, we can exploit this fact. 
        y = \_\_class\_\_.\_\_back\_substitution( H[0:k+1, 0:k+1], beta[0:k+1] )

        self.x = self.x + np.matmul( Q[:,0:k+1], y )

        self.final\_residual\_norm = np.linalg.norm( self.b - np.matmul( self.A, self.x ) )

        return self.x

 '''''''''''''''''''''''''''''''''''
 ' Arnoldi Function '
 '''''''''''''''''''''''''''''''''''
    @staticmethod
    def arnoldi( A, Q, k ):
        h = np.zeros( k+2 )
        q = np.dot( A, Q[:,k] )
        for i in range ( k+1 ):
            h[i] = np.dot( q, Q[:,i])
            q = q - h[i] \* Q[:, i]
        h[ k+1 ] = np.linalg.norm(q)
        q = q / h[ k+1 ]
        return h, q 

 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''
 ' Applying Givens Rotation to H col '
 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    @staticmethod
    def apply\_givens\_rotation( h, cs, sn, k ):
        for i in range( k-1 ):
            temp   =  cs[i] \* h[i] + sn[i] \* h[i+1]
            h[i+1] = -sn[i] \* h[i] + cs[i] \* h[i+1]
            h[i]   = temp

        # update the next sin cos values for rotation
        cs\_k, sn\_k, h[k] = \_\_class\_\_.givens\_rotation( h[k-1], h[k] )
        
        # eliminate H[ k+1, i ]
        h[k + 1] = 0.0

        return h, cs\_k, sn\_k

    ##----Calculate the Given rotation matrix----##
    # From "http://www.netlib.org/lapack/lawnspdf/lawn150.pdf"
    # The algorithm used by "Edward Anderson"
    @staticmethod
    def givens\_rotation( v1, v2 ):
        if( v2 == 0.0 ):
            cs = np.sign(v1)
            sn = 0.0
            r = abs(v1)
        elif( v1 == 0.0 ):
            cs = 0.0
            sn = np.sign(v2)
            r = abs(v2)
        elif( abs(v1) > abs(v2) ):
            t = v2 / v1 
            u = np.sign(v1) \* math.hypot( 1.0, t )  
            cs = 1.0 / u
            sn = t \* cs
            r = v1 \* u
        else:
            t = v1 / v2 
            u = np.sign(v2) \* math.hypot( 1.0, t )  
            sn = 1.0 / u
            cs = t \* sn
            r = v2 \* u
        return cs, sn, r

    # From https://stackoverflow.com/questions/47551069/back-substitution-in-python
    @staticmethod
    def \_\_back\_substitution( A: np.ndarray, b: np.ndarray) -> np.ndarray:
        n = b.size
        if A[n-1, n-1] == 0.0:
            raise ValueError

        x = np.zeros\_like(b)
        x[n-1] = b[n-1] / A[n-1, n-1]
        for i in range( n-2, -1, -1 ):
            bb = 0
            for j in range ( i+1, n ):
                bb += A[i, j] \* x[j]
            x[i] = (b[i] - bb) / A[i, i]
        return x

    def final\_residual\_info\_show( self ):
        print( "x =", self.x, "residual\_norm = ", self.final\_residual\_norm ) 
    
def main():

    A\_mat = np.array( [[1.00, 1.00, 1.00],
                       [1.00, 2.00, 1.00],
                       [0.00, 0.00, 3.00]] )

    b\_mat = np.array( [3.0, 2.0, 1.0] )

    GMRES\_test\_itr2 = GMRES\_API( A\_mat, b\_mat, 2, 0.01)

    x\_mat = np.array( [1.0, 1.0, 1.0] )
    print("x =", x\_mat)

    # GMRES with restart, 2 iterations in each restart ( GMRES(2) )
    max\_restart\_counts = 100
    for restart\_counter in range(max\_restart\_counts):
        GMRES\_test\_itr2.initial\_guess\_input( x\_mat )

        x\_mat = GMRES\_test\_itr2.run()
        print(restart\_counter+1," : x =", x\_mat)

    xx = np.matmul( np.linalg.inv(A\_mat), b\_mat )
    print("ANS : xx =", xx) 

if \_\_name\_\_ == '\_\_main\_\_':
    main()

```

注记
--

1. ↑  Saad和Schultz
2. ↑  Trefethen & Bau, Thm 35.2
3. ↑  Stoer and Bulirsch, §8.7.2