大规模矩阵对角化方法:Lanczos

1. A的本征值即其Rayleigh商的极值

Rayleigh商定义:

对于nxn实对称阵A,任意非零n维矢量\(\vec{x}\neq 0\),定义Rayleigh商为
\(r_A( \vec{x} ) = \frac{ \vec{x}^\top A \vec{x} }{ \vec{x}^\top \vec{x} }.\)
显然,Rayleigh商对于\(\vec{x}\)的模并不依赖。

Rayleigh商的范围

假设A的本征值是\(\lambda_i, i=1,...,n\),从小到大排列:\(\lambda_1 \leq \lambda_2 ... \leq \lambda_n\)
本征矢是\(\vec{v}_i, i=1,...,n\),有\(A\vec{v}_i = \lambda_i \vec{v}_i\)
将非零矢量\(\vec{x}\)\(\vec{v}_i\)表达,有\(\vec{x} = \sum_i c_i \vec{v}_i\),则Rayleigh商为
\(r_A ( \vec{x} ) = \frac{ \sum_i c_i^2 \lambda_i^2 }{ \sum_i c_i^2 } \in [ \lambda_1, \lambda_n ]\)

Rayleigh商的极值

计算Rayleigh商对\(\vec{x}\)的梯度
\(\nabla r_A(\vec{x}) = \frac{2}{\vec{x}^\top \vec{x}}( A \vec{x} - r_A(\vec{x})\vec{x} ).\)
\(\nabla r_A(\vec{x}) = 0 \Leftrightarrow A \vec{x} = r_A(\vec{x})\vec{x}\)
所以,A的Rayleigh商\(r_A(\vec{x})\)在其本征矢\(\vec{v}\)处取得极值,极值即A的本征值\(\lambda_i\)

2. 子空间中的Rayleigh商

若有\(k\)个(\(k\leq n\))正交归一的n维矢量\(\{ \vec{q}_1, \vec{q}_1, \cdots \vec{q}_k \}\),构成子空间,记nxk的矩阵\(Q_k \equiv \begin{bmatrix} \vec{q}_1, \vec{q}_1, \cdots, \vec{q}_{k} \end{bmatrix}\)
这个子空间中任意非零矢量为\(\vec{x} = \sum^k_{i=1} y_i \vec{q}_i = Q_k \vec{y}\)\(\vec{y}\)是一个k维非零矢量,则A在这个子空间中的Rayleigh商为
\(r_A(Q_k \vec{y}) = \frac{ (Q_k \vec{y})^\top A (Q_k \vec{y}) }{ (Q_k \vec{y})^\top (Q_k \vec{y}) } = \frac{ \vec{y}^\top (Q^\top_k A Q_k) \vec{y} }{ \vec{y}^\top \vec{y} } = r_{Q^\top_k A Q_k}(\vec{y})\)
转化为\(Q^\top_k A Q_k\)的Rayleigh商。

3. Krylov子空间:快速逼近极端本征值

根据上面的分析,要想构造很小的子空间,使得该子空间内存在最大/最小本征值对应的本征矢\(\vec{v}_1, \vec{v}_n\)的近似矢量,则需要构造\(Q_k\),使得\(r_{Q^\top_k A Q_k}\)的最小值尽量小,最大值尽量大。
我们可以从选定的\(\vec{q}_1\)开始,逐渐增加\(\vec{q}_2, \vec{q}_3, ...\),逐渐逼近目标。
一个有用的性质是:\(\nabla r_A(\vec{q}_1) = \frac{2}{\vec{q}^\top_1 \vec{q}_1}( A \vec{q}_1 - r_A(\vec{q}_1)\vec{q}_1 )\),这个梯度是\(A\vec{q}_1, \vec{q}_1\)的线性叠加。
于是,美妙的想法产生了:何不如此构造子空间呢:\(\kappa(A, \vec{q}_1, k) = \{ \vec{q}_1, A\vec{q}_1, A^2\vec{q}_1, \cdots, A^{k-1}\vec{q}_1 \}\),这个子空间叫做Krylov子空间。这样构造的话,对于\(Q_k\),A的Rayleigh商在\(Q_k\)构造的子空间中任意一处的梯度矢量都在\(Q_{k+1}\)的子空间中。
所以,每次扩大子空间时:\(Q_1, Q_2, \cdots\),Rayleigh商的最大值都会尽可能地增大,Rayleigh商的最小值都会尽可能地减小。
在这个意义上,Lanczos方法可以看做是Rayleigh商\(r_A\)的一个梯度下降/上升过程。
这和共轭梯度法很相似,共轭梯度法相当于:每次在\(Q_k\)内找一个下降方向,并保证这些下降方向线性无关,所以最多只会有\(n\)个下降方向,因此\(n\)步就能到达极值。当然,在大规模对角化问题中,如果只需要极端本征值,\(n\)步是奢侈的,Lanczos中迭代次数远小于\(n\)

4. Lanczos方法

如上所述的策略很美妙,但还有一个小小的问题:我们如何判断子空间已经足够大了呢?换一句话说,我们如何判断子空间中的极端本征值已经足够大,或足够小?
\(k\)逐渐增大时,我们需要在\(\kappa(A, \vec{q}_1, k)\)中不断地对角化,得到各个子空间中的极端本征值,看它们是否收敛。如果随着\(k\)的增大,极端本征值的变化极其微小,我们则经验地认为已经收敛。
要在\(\kappa(A, \vec{q}_1, k)\)中对角化A,就需要正交归一化\(\vec{q}_1, A\vec{q}_1, \cdots\)
Lanczos方法即如下步骤,构造\(\vec{q}_1, \vec{q}_2, \cdots\),既保证每次增加矢量,得到的子空间都是Krylov子空间,又保证\(\vec{q}_1, \cdots, \vec{q}_2, \cdots\)是正交归一矢量。

正交化方案:

给定单位矢量\(\vec{q}_1\),定义\(\vec{r}_2 = A \vec{q}_1 - ( \vec{q}_1^\top A \vec{q}_1 ) \vec{q}_1, \vec{q}_2 = \vec{r}_2 / | \vec{r}_2 |\)。即从\(A\vec{q}_1\)中剔除\(\vec{q}_1\)的分量,然后归一化。
对于\(k\geq 2\)\(\vec{r}_{k+1} = A \vec{q}_k - (\vec{q}^\top_k A \vec{q}_k) \vec{q}_k - (\vec{q}^\top_{k-1} A \vec{q}_k) \vec{q}_{k-1}, \vec{q}_{k+1} = \vec{r}_{k+1} / | \vec{r}_{k+1} |\)。即从\(A\vec{q}_k\)中剔除\(\vec{q}_{k-1}, \vec{q}_k\)的分量,然后归一化。

正交化吗?归纳法证明

显然,如上构造的子空间\(\{\vec{q}_1, \vec{q}_2\} = \{\vec{q}_1, A\vec{q}_1 \}, \{\vec{q}_1, \vec{q}_2, \vec{q}_3 \} = \{ \vec{q}_1, A\vec{q}_1, A^2\vec{q}_1 \}\)。并且\(\vec{q}_1, \vec{q}_2, \vec{q}_3\)正交。

以此为起点,开始归纳法证明:如果\(i=1,2,\cdots,k\),都有\(\{\vec{q}_1, \cdots, \vec{q}_i\} = \{ \vec{q}_1, A\vec{q}_1, \cdots, A^{i-1}\vec{q}_1 \}\)并且\(\vec{q}_1, \cdots, \vec{q}_i\)两两垂直,那么,如上构造\(\vec{q}_{k+1}\)以后,也会有\(\{\vec{q}_1, \cdots, \vec{q}_i\} = \{ \vec{q}_1, A\vec{q}_1, \cdots, A^{i-1}\vec{q}_{k+1} \}\)并且\(\vec{q}_1, \cdots, \vec{q}_{k+1}\)两两垂直。

\(\{\vec{q}_1, \cdots, \vec{q}_k, \vec{q}_{k+1}\} = \{ \vec{q}_1, A\vec{q}_1, \cdots, A^{k}\vec{q}_1 \}\)

由于\(A\vec{q}_1, A\vec{q}_2, \cdots, A\vec{q}_{k-2}\)都是子空间\(\{ \vec{q}_1, \vec{q}_2, \cdots, \vec{q}_{k-1} \}\)中的矢量,所以都垂直于\(\vec{q}_k\),即
\(\vec{q}_k^\top A \vec{q}_{k-2} = \vec{q}^\top_k A \vec{q}_{k-3} = \cdots \vec{q}^\top_k A \vec{q}_1 = 0\)
构造\(\vec{r}_{k+1} = A \vec{q}_k - (\vec{q}^\top_k A \vec{q}_k) \vec{q}_k - (\vec{q}^\top_{k-1} A \vec{q}_k) \vec{q}_{k-1}, \vec{q}_{k+1} = \vec{r}_{k+1} / | \vec{r}_{k+1} |\)
则显然有\(\{\vec{q}_1, \cdots, \vec{q}_k, \vec{q}_{k+1}\} = \{ \vec{q}_1, A\vec{q}_1, \cdots, A^{k}\vec{q}_1 \}\),剩下的任务是证明\(\vec{q}_{k+1}\)\(\vec{q}_1, \cdots, \vec{q}_k\)垂直。

\(\vec{q}_{k+1}\)\(\vec{q}_k, \vec{q}_{k-1}\)都正交

由于\(\vec{q}_{k+1}\)\(A\vec{q}_k\)剔除\(\vec{q}_k, \vec{q}_{k-1}\)的成分构造的,自然与\(\vec{q}_k, \vec{q}_{k-1}\)正交。

\(\vec{q}_{k+1}\)\(\vec{q}_1, \vec{q}_2, \cdots \vec{q}_{k-2}\)都正交

因为\(\vec{q}_k\)与子空间\(\{ \vec{q}_1, \cdots \vec{q}_{k-1} \}\)正交,所以\(\vec{q}_k\)\(A\vec{q}_1, A\vec{q}_2, \cdots, A\vec{q}_{k-2}\)都正交,即
\(\vec{q}^\top_k A \vec{q}_1 = \vec{q}^\top_k A \vec{q}_2 = \cdots = \vec{q}^\top_k A \vec{q}_{k-2} = 0\)
利用A的对称性,上式即
\(\vec{q}^\top_1 A \vec{q}_k = \vec{q}^\top_2 A \vec{q}_k = \cdots = \vec{q}^\top_{k-2} A \vec{q}_{k} = 0\)

因此,\(\vec{q}_{k+1}\)\(\{ \vec{q}_1, \cdots, \vec{q}_k \}\)正交。
至此,命题得证。Lanczos方案确实可以得到正交归一化的Krylov子空间。

三对角化矩阵

由于上文中说明的\(\vec{q}^\top_k A \vec{q}_{k-2}=0, k \geq 2\),所以,\(T_k = Q^\top_k A Q_k\)是一个三对角矩阵。\(A\)\(Q_k\)子空间中的Rayleigh商即\(T_k = Q^\top_k A Q_k\)的Rayleigh商。随着\(k\)的增加,不断对角化\(T_k\),直到\(T_k\)的本征值收敛。

截断误差

因为截断误差的存在,执行上面的策略以后,也不能保证\(\vec{q}_1, \vec{q}_2, \cdots\)的正交性,所以每次计算出\(A\vec{q}_{k+1}\),都要依次与\(\vec{q}_i, i=1,\cdots,k-1\)正交化:
\(\vec{q}_{k+1} = \vec{q}_{k+1} - (\vec{q}^\top_i \vec{q}_{k+1}) \vec{q}_i\)
总结下来,Lanczos的做法即:构造\(A\vec{q}_k\),然后依次剔除其中的\(\vec{q}_k, \vec{q}_{k-1}, \cdots, \vec{q}_1\)的组分,注意这个剔除顺序是逆序的,逆序优于顺序,因为顺序可能出现大数减小数的情况,扣不掉截断误差。

5. Lanczos 代码

下面是我写的Lanczos代码,考虑了截断误差,不变子空间等问题。但是可读性比较差,因为没有时间所有也没有仔细优化。如果在实际问题中优化,主要是针对 矩阵x矢量 的部分优化。
/*
* Lanczos(...) Lanczos method to diagonalize symmetric matrix A[n][n], the output is eigenvalues in z[] and eigenvectors in eigvec[]
* int n dimmension of the real symmetric matrix to be diagonalized
* int nkeep number of states to be kept, Lanczos(...) stops iterating if the lowest nkeep states converge
* double precision precision control parameter, it is used in
* error criterion = precision * max|A_{ij}|
* error = sqrt( 1/nkeep * sum^{nkeep}{i=0} (z^{k} [i] - z^{k-1} [i] )^2 )
* invariant subspace criterion: | q
| < precision * max|A_{ij}|
*/
void Lanczos(int n, double **A, int nkeep, double *z, double *eigvec, double precision){

    if(nkeep >n) nkeep = n;

    for(int i=0;i<n;i++) z[i] = 0;

    double maxele=0;
    for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                    if( maxele < fabs( A[i][j] ) )
                            maxele = fabs( A[i][j] );
    int i, j, k, l, maxiter=nkeep * 500;
    int upperk = n>maxiter ? maxiter : n;
    double y, error = precision * maxele * 1E6, criterion = precision * maxele;
    double * eigket = new double [ upperk * upperk ];
    double *d = new double [upperk];
    double *e = new double [upperk];
    double *dd = new double [upperk];
    double *ee = new double [upperk];
    bool finish = false;
    vec omega, v; omega.init(n); v.init(n);

    vec * q = new vec [upperk];
    for(int i=0;i<upperk;i++) q[i].init(n);

    k=0;
    while( k < upperk && error> criterion ){
            // get new Lanczos pivot qk
            omega.SetZero();
            while( sqrt( omega.InnerProduct( omega ) ) < precision ){
                    omega.RandomNormal(); omega.ProjectOut(k, q);
                    if( sqrt( omega.InnerProduct( omega ) ) < precision )
                            continue;
                    omega.Normalize();
            }

            while( error > criterion && k < upperk ){
                    // check orthogonalization
                    v.copy(1,omega); v.ProjectOut(k,q);
                    //cout<<"After projection, ||qk||^2 = "<<v.InnerProduct(v)<<endl;
                    // When it comes here, omega is the fresh lanczos vector
                    q[k].copy(1, omega); k++;
                    // diagonalize tridiagonal matrix
                    v.Av(A, omega);
                    d[k-1] = v.InnerProduct( omega ); // tridiagonal element
                    if(k==1) e[k-1] = 0;
                    else e[k-1] = v.InnerProduct( q[k-2] );
                    for(int i=0;i<k;i++){
                            dd[i] = d[i]; ee[i] = e[i];
                    }
                    //cout<<"dd: "; for(int i=0;i<k;i++)cout<<dd[i]<<"\t";cout<<endl;
                    //cout<<"ee: "; for(int i=0;i<k;i++)cout<<ee[i]<<"\t";cout<<endl;
                    tqls(k, dd, ee, eigket);// O(k^3)
                    sort_eig(k, eigket, dd);
                    // calculate error
                    if( k > nkeep ){// error = sum of squared deviations
                            error = 0;
                            for(i=0;i<nkeep;i++)
                                    error += (dd[i] - z[i])*(dd[i] - z[i]);
                            error = sqrt(error/nkeep);
                    }

                    if( k % 20 ==0 ){
                            cout<<"Lanczos: after "<<k<<" iteractions, error = "<< error << (error > criterion ? " > ": " < ")<<criterion<<endl;
                            cout<<"\t lowest 10 lanczos eigen values:"<<endl;
                            for(int i=0;i<(k>10?10:k);i++)
                                    cout<<"\t"<<dd[i]<<endl;
                    }

                    if( error < criterion ){
                            finish = true;
                            break;
                    }
                    for(i=0;i<k;i++) z[i] = dd[i];
                    //cout<<"z:"; for(int i=0;i<k;i++)cout<<z[i]<<"\t"; cout<<endl;


                    // construct next Lanczos vector
                    v.ProjectOut(k, q); omega.copy(1, v);
                    if( sqrt(omega.InnerProduct(omega)) < precision * maxele )//invariant subspace
                            break;
                    omega.Normalize();
            }
            if(finish) break;
    }
    if(k==upperk && upperk < n ){
            cout<<"Lanczos: failed to converge after "<<upperk<<"iterations"<<endl;
            exit(1);
    }
    if(n==1){
            z[0] = A[0][0];
            eigket[0] = 1;
    }
    for(i=0;i<nkeep;i++){
            for(j=0;j<n;j++){
                    y=0;
                    for(l=0;l<k;l++){
                            y += eigket[i*k+l] * (q[l].v)[j];
                    }
                    eigvec[i*n+j] = y;
            }
    }
    delete [] d; delete [] e; delete [] dd; delete [] ee; delete [] eigket;
    omega.dele(); v.dele();
    for(int i=0;i<upperk;i++) q[i].dele(); delete [] q;
}

6 性能测试

    #include<iostream>
    using namespace std;
    #include<fstream>
    #include<cmath>
    #include<ctime>

    #include"class.h"
    #include"matrix.h"

    extern "C" void dsyev_(char *a,char *b,int *n,double *A,int *nn,double *e,double *work,int *lwork,int *info);

    int main(){

            int n=100;
            //cout<<"\t n="; cin>>n;

            clock_t tstart = clock();

            double **A = new double * [n];
            for(int i=0;i<n;i++) A[i] = new double [n];

            double *w = new double [n];
            double *v = new double [n];
            double *z = new double [n];
            double *eigvec = new double [n*n];
            clock_t tend = clock();
            cout<<" dynamic memory allocation takes "<< (double)(tend-tstart)/CLOCKS_PER_SEC <<" s"<<endl;

            ifstream fin("OveEven.txt");
            string line;
            getline(fin, line);
            tstart = clock();
            for(int i=0;i<n;i++){
                    for(int j=0;j<n;j++){
                            //fin >> A[i][j];
                            //if(i==j) A[i][j] = 1;
                            //else A[i][j] = 0;
                            //A[i][j] = i*i + j*j;
                            A[i][j] = (double)rand()/RAND_MAX;
                            A[j][i] = A[i][j];
                    }
            }
            tend = clock();
            cout<<" Value assignment of A[][] takes "<< (double)(tend-tstart)/CLOCKS_PER_SEC <<" s"<<endl;
            cout<<"A assigned"<<endl;
            for(int i=0;i<n;i++) v[i] = (double)rand()/RAND_MAX;
            cout<<"assignment of values finished"<<endl;
            double y;
            tstart = clock();
            for(int i=0;i<n;i++){
                    y = 0;
                    for(int j=0;j<n;j++)
                            y += A[i][j] * v[j];
                    w[i] = y;
            }
            tend = clock();
            cout<<" matrix multiplication takes "<< (double)(tend-tstart)/CLOCKS_PER_SEC <<" s"<<endl;

            Lanczos( n, A, 1, z, eigvec, 1E-3 );
            //SimpleLanczos(n, A, 1, z, eigvec, 1E-5 );

            double * a = new double [n*n];
            for(int i=0;i<n;i++){
                    for(int j=0;j<n;j++){
                            a[i*n+j] = A[i][j];
                    }
            }

            char alpha='v', beta='L';
            int lwork = 3 * n, info=0;
            double *e = new double [ n ];
            double *work = new double [ 3*n ];

            if( n <=1000 ){
                    dsyev_(&alpha, &beta, &n, a, &n, e, work, &lwork, &info);
                    cout<<" The lowest 20 dsyev eigenvalues"<<endl;
                    for(int i=0;i<(n>20?20:n);i++)
                            cout<<"\t"<<e[i]<<endl;

                    double error;
                    for(int i=0;i<(n>20?20:n);i++)
                            error += ( e[i] - z[i] ) * ( e[i] - z[i] );
                    cout<<"Lanczos RMSD = "<< sqrt(error/n)<<endl;
            }

            return 0;
    }

我使用 [0,1) 的随机数构成的实对称矩阵,进行测试。如果只求最低本征值,要求最后一次迭代前后结果只差小于 1E-3,则
n = 1E2 时,26 次迭代即收敛。
n = 1E3 时,35 次迭代即收敛。
n = 1E4 时,48 次迭代即收敛。
n = 1E5 时,内存溢出。
所以,对于大矩阵极端本征值,Lanczos还是很管用的。

posted on 2021-03-11 18:34  luyi07  阅读(912)  评论(0编辑  收藏  举报

导航