廣義最小殘量方法

在數學上，廣義最小殘量方法(一般簡稱GMRES)是一個求解線性方程組 數值解的迭代方法。這個方法利用在Krylov子空間中有著最小殘量的向量來逼近解。Arnoldi迭代方法被用來求解這個向量。

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

## GMRES方法

需要求解的線性方程組記為

$Ax = b \,$。

假設矩陣A是n$\times$n階的可逆的。進一步，假設b是標準化的，即

b

= 1 (在這篇文章中。

·

是Euclidean範數)。

這個問題的m階Krylov子空間是

$K_m = \operatorname{span} \, \{ b, Ab, A^2b, \ldots, A^{m-1}b \} \,$。

GMRES通過使得殘量Axm − b最小的向量xm ∈ Km來逼近Ax = b的精確解。

但是，向量b, Ab, …, Am−1b幾乎是線性相關的。因此，用Arnoldi迭代方法求得的這組Km的標準正交基

$$q_1, q_2, \ldots, q_m \,$$

來取代上面的那組基。所以，向量xm ∈ Km寫成xm = Qmym，其中ym ∈ Rm且Qm是由q1, …, qm組成的n$\times$m矩陣。

Arnoldi過程也產生一個 (m+1)$\times$m階上Hessenberg矩陣$\tilde{H}_m$滿足

$AQ_m = Q_{m+1} \tilde{H}_m \,$。

因為$Q_m$是正交的，我們有

$\| Ax_m - b \| = \| \tilde{H}_my_m - \beta e_1 \| \,$。

其中

$$e_1 = (1,0,0,\ldots,0) \,$$

是Rm+1的標準基的第一個向量，並且

$\beta = \|b-Ax_0\| \,$。

其中$x_0$是初始向量(通常是零向量)。因此，求使得殘量

$r_m = \tilde{H}_m y_m - \beta e_1$。

範數最小的$x_m$。這是一個m階線性最小二乘問題。

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

- 做一步Arnoldi迭代方法。
- 尋找使得

rm

最小的$y_m$。

- 計算$x_m = Q_m y_m$。
- 如果殘量不夠小，重複以上過程。

在每一步迭代中，必須計算一次矩陣向量積Aqm。對於一般的n階稠密矩陣，這要計算複雜度大約2n2次浮點運算。但是對於稀疏矩陣，這個計算複雜度能減少到O(n)。進一步，關於矩陣向量積，在m次迭代中能進行O(m n)次浮點運算。

## 收斂性

第m次迭代獲得在Krylov子空間Km下的最小殘量。因為每個子空間包含於下一個子空間中，所以殘量單調遞減。在第n次迭代後，其中n是矩陣A的階數，Krylov空間Kn是完整的Rn。因此，GMRES方法達到精確解。然而，問題在於：在極少的幾次迭代後(相對於n)，向量xm幾乎已經是精確解的一個很好的逼近。

但是，在一般情況下這是不會發生的。事實上，Greenbaum，Pták和Strakoš的理論說明了對於每一個單調減少的序列a1, …, an−1, an = 0，能夠找到一個矩陣A對於所有m滿足

rm

= am，其中rm是上面所定義的殘量。特別的，有可能找到一個矩陣，使得前n − 1次迭代的殘量一直保持為常數，而只在最後一次迭代時達到零。

在實驗中，GMRES方法經常表現得很好。在特殊的情況下這能夠被證明。如果A是正定的，則

$\|r_m\| \leq \left( 1-\frac{\lambda_{\mathrm{min}}(A^T + A)}{2 \lambda_{\mathrm{max}}(A^T + A)} \right)^{m/2} \|r_0\|$。

其中$\lambda_{\mathrm{min}}(M)$和$\lambda_{\mathrm{max}}(M)$分別為矩陣$M$的最小和最大特徵值。

如果A是對稱的並且是正定的，則

$\|r_m\| \leq \left( \frac{\kappa_2^2(A)-1}{\kappa_2^2(A)} \right)^{m/2} \|r_0\|$。

其中$\kappa_2(A)$記為A在Euclidean範數下的條件數。

一般情況下，其中A是非正定的，則

$\|r_m\| \le \inf_{p \in P_m} \|p_m(A)\| \le \kappa_2(V) \inf_{p \in P_m} \max_{\lambda \in \sigma(A)} |p(\lambda)| \,$。

其中Pm記為次數不超過m且p(0) = 1的多項式的集合，V是A的譜分解中的矩陣，而σ(A)是A的譜。粗略的說，當A的特徵值聚集在遠離原點的區域且A離正規不太遠時，收斂速度較快。

所有的不等式只界定殘量，而不是實際誤差(精確解和當前迭代xm之間的距離)。

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

同其他迭代方法一樣，為了加快收斂，GMRES經常結合預處理方法。

迭代的開銷以O(m2)增長，其中m是迭代次數。然而有時候，GMRES方法在k次迭代後重新開始，即xk又變回初始值。這樣的方法叫做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方法的其中一部分是求解向量$y_m$使得

$$\| \tilde{H}_m y_m - e_1 \| \,$$

最小。這個可以通過計算QR分解來實現：找到一個(m+1)$\times$(m+1)階正交矩陣Ωm和一個(m+1)$\times$m上三角矩陣$\tilde{R}_m$滿足

$\Omega_m \tilde{H}_m = \tilde{R}_m$。

三角矩陣的行數比列數多1，所以它的最後一行由零組成。因此，它能被分解為

$\tilde{R}_m = \begin{bmatrix} R_m \\ 0 \end{bmatrix}$。

其中$R_m$是一個m$\times$m階三角(方)矩陣。

QR分解能夠簡單的進行下去(update)，從一步迭代到下一步迭代。因為每次的Hessenberg矩陣只在一行零元素和一列元素上有所不同：

$\tilde{H}_{m+1} = \begin{bmatrix} \tilde{H}_m & h_m \\ 0 & h_{m+1,m} \end{bmatrix}$。

其中hm = (h1m, … hmm)T。這意味著，Hessenberg矩陣左乘上Ωm的擴大矩陣(通過並上零元素和單位元素)，所得到的是類似於三角矩陣的矩陣：

$$\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旋轉

$$G_m = \begin{bmatrix} I_{m-1} & 0 & 0 \\ 0 & c_m & s_m \\ 0 & -s_m & c_m \end{bmatrix}$$

其中

$c_m = \frac{\rho}{\sqrt{\rho^2+\sigma^2}} \quad\mbox{and}\quad s_m = \frac{\sigma}{\sqrt{\rho^2+\sigma^2}}$。

通過這個Givens旋轉，我們構造

$\Omega_{m+1} = G_m \begin{bmatrix} \Omega_m & 0 \\ 0 & 1 \end{bmatrix}$。

事實上。

$$\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分解，最小值問題就容易解決了。注意到

$\| \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 \|$。

記$\Omega_me_1$為

$$\tilde{g}_m = \begin{bmatrix} g_m \\ \gamma_m \end{bmatrix}$$

其中gm ∈ Rm和γm ∈ R，則

$\| \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為

$y_m = R_m^{-1} g_m$。

再一次，向量$g_m$能夠簡單的進行下去(update)。

## Example code

### Regular GMRES (python3)

```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()
```