共役勾配法

n元連立1次方程式

ls_cg.eq1.gif

の係数行列Aが正値対称行列であるならば, その解ls_cg.eq2.gifを求めることは, 以下の方程式の最小値探索と同等である.

ls_cg.eq3.gif

ここで,(*,*)はベクトルの内積を表す. ls_cg.eq4.gifを直接解く代わりに, 方程式ls_cg.eq5.gifの最小値探索により解を求める方法を 共役勾配法(conjugate gradient method:CG法)と呼ぶ.

ls_cg.eq5.gifの最小値探索が元のn元連立1次方程式を解くことと同等になることの証明を以下に示す.

まず,ls_cg.eq5.gifを成分で表すと,

ls_cg.eq6.gif

となる.最小値を持つ点では関数の偏微分値(傾き)が0となる. ls_cg.eq7.gifで偏微分すると,右辺第1項はi=kとj=kの要素のみが残るので,

ls_cg.eq8.gif

Aは対称行列であるので,ls_cg.eq9.gifである.よって,

ls_cg.eq10.gif

この式はls_cg.eq4.gifを成分で表示したものである. よって,ls_cg.eq4.gifの解はls_cg.eq5.gifを最小にするls_cg.eq2.gifと同じになる.

共役勾配法の計算手順

共役勾配法の計算手順を以下に示す.

  1. 初期近似解ls_cg.eq11.gifを適当に設定
  2. 初期近似解に対する残差を計算,修正方向ベクトルを70%&ref(ls_cg.eq13.gif,nolink);ls_cg.eq12.gifで初期化
    ls_cg.eq14.gif
  3. 以下の計算を収束するまで繰り返す(k=0,1,2,...)
    1. ls_cg.eq15.gifを計算
    2. 修正係数ls_cg.eq16.gifを計算
      ls_cg.eq17.gif
    3. k+1ステップの近似値を算出
      ls_cg.eq18.gif
    4. k+1の近似値に対する残差を計算
      ls_cg.eq19.gif
    5. ls_cg.eq20.gifならば収束したとして計算を終了
    6. 方向ベクトルls_cg.eq21.gifの修正係数ls_cg.eq22.gifを計算
      ls_cg.eq23.gif
    7. k+1ステップの方向ベクトルls_cg.eq24.gifを計算
      ls_cg.eq25.gif

共役勾配法の実装

共役勾配法のC++による実装例を以下に示す.

/*!
 * 共役勾配法によりA・x=bを解く
 * @param[in] A n×n正値対称行列
 * @param[in] b 右辺ベクトル
 * @param[out] x 結果ベクトル
 * @param[in] n 行列の大きさ
 * @param[inout] max_iter 最大反復数(反復終了後,実際の反復数を返す)
 * @param[inout] eps 許容誤差(反復終了後,実際の誤差を返す) 
 * @return 1:成功,0:失敗
 */
int CGSolver(const vector< vector<double> > &A, const vector<double> &b, vector<double> &x, int n, int &max_iter, double &eps)
{
    if(n <= 0) return 0;

    vector<double> r(n), p(n), y(n);
    x.assign(n, 0.0);

    // 第0近似解に対する残差の計算
    for(int i = 0; i < n; ++i){
        double ax = 0.0;
        for(int j = 0; j < n; ++j){
            ax += A[i][j]*x[j];
        }
        r[i] = b[i]-ax;
        p[i] = r[i];
    }

    double rr0 = dot(r, r, n), rr1;
    double alpha, beta;

    double e = 0.0;
    int k;
    for(k = 0; k < max_iter; ++k){
        // y = AP の計算
        for(int i = 0; i < n; ++i){
            y[i] = dot(A[i], p, n);
        }

        // alpha = r*r/(P*AP)の計算
        alpha = rr0/dot(p, y, n);

        // 解x、残差rの更新
        for(int i = 0; i < n; ++i){
            x[i] += alpha*p[i];
            r[i] -= alpha*y[i];
        }

        // (r*r)_(k+1)の計算
        rr1 = dot(r, r, n);

        // 収束判定 (||r||<=eps)
        e = sqrt(rr1);
        if(e < eps){
            k++;
            break;
        }

        // βの計算とPの更新
        beta = rr1/rr0;
        for(int i = 0; i < n; ++i){
            p[i] = r[i]+beta*p[i];
        }

        // (r*r)_(k+1)を次のステップのために確保しておく
        rr0 = rr1;
    }

    max_iter = k+1;
    eps = e;

    return 1;
}

ここで,std::vectorの内積を求めるdot関数を定義して用いている.

条件数

ここでは前処理付き共役勾配法について説明する前に, 共役勾配法の収束性と係数行列の条件数について述べる.

共役勾配法は正定値対称行列にのみ適用できる. なぜ対称行列出なければならないのかというのは方程式ls_cond.eq1.gifの最小化に関しての記述のところで説明した.一方でなぜ正定値(positive definite)でなければならないのかを考える. 方程式

ls_cond.eq2.gif

は2次式であり,個々の式はls_cond.eq3.gifの形で表すことができる. この2次式が最小値を持つ条件を考える. まず,2次式ls_cond.eq3.gifを,

ls_cond.eq4.gif

と変形する(要は解の公式を導出する過程の式). このときls_cond.eq5.gifにかかる係数ls_cond.eq6.gifls_cond.eq7.gifならば2次式のグラフが上に開いた形になり,最小値を持つ. 逆にls_cond.eq8.gifだと2次式は最大値を持ち,ls_cond.eq9.gifで最小値も最大値も持たない. 大事なのは係数ls_cond.eq6.gifの符号である.

一方,ls_cond.eq10.gifの場合はどうだろうか. まず,Aが正定値であるということは,ls_cond.eq11.gifの任意の実ベクトルls_cond.eq12.gifについて,

ls_cond.eq13.gif

であるということである.ここで,ls_cond.eq14.gifを行列Aの2次形式と呼ぶ.

行列が正定値であること = その行列の固有値がすべて正

であるので,行列の固有値がすべて正ならば最小値を持つことを証明する.

n元連立1次方程式ls_cond.eq15.gifの係数行列Aの モード行列をR,固有値を対角成分にした対角行列をDとする. モード行列とは行列の固有ベクトルを並べた行列で,ls_cond.eq16.gifの直交行列である. 係数行列はls_cond.eq17.gifで表されるので,連立1次方程式の式は,

ls_cond.eq18.gif

となる.ここで,ls_cond.eq19.gifとおくと,

ls_cond.eq20.gif

よって,

ls_cond.eq21.gif

である.方程式ls_cond.eq22.gifls_cond.eq23.gifを使って表すと,

ls_cond.eq24.gif

ここで,

ls_cond.eq25.gif

と変形できるので,

ls_cond.eq26.gif

となる.さらに,ls_cond.eq27.gifとおくと,

ls_cond.eq28.gif

Dは対角行列なのでls_cond.eq29.gifである.よって,

ls_cond.eq30.gif

右辺第1項は2次形式である.そして,Dは対角行列で,その対角成分,つまり,固有値がls_cond.eq31.gifの係数aになる. 上で示したように,2次式が最小値を持つ条件はls_cond.eq7.gifであるので, すべての固有値が正ならばls_cond.eq1.gifは最小値を持つ. つまり,言い換えるとAが正定値ならばls_cond.eq1.gifは最小値を持つ.

これでAが正定値でなければならないことは証明できた. 次に,ls_cond.eq32.gifに関する式が実際にどのようになっているかを考える. 2次元(ls_cond.eq33.gif)のとき,

ls_cond.eq34.gif

の形になる.この式はls_cond.eq35.gifを軸とする楕円(ls_cond.eq36.gif)であり, 固有値(ls_cond.eq37.gif)の比は,軸の長さls_cond.eq38.gifの平方根の比と等しい. つまり,固有値の最大値と最小値の差が大きいと平べったい楕円になってしまう. ここで,行列Aの条件数(condition number)というものを考える. 条件数は,

ls_cond.eq39.gif

と書け,2つのノルムの積となる. 正定値実対称行列の場合,最大固有値を最小固有値で割った比が条件数になる(すべての固有値は正であることに注意). 条件数の最小値は1であり,条件数が1に近いほど誤差が小さく,アルゴリズムの収束性がよくなるため, 連立1次方程式の精度や収束性を評価する上でとても重要な指標となる.

前処理付き共役勾配法

前処理付き共役勾配法

係数行列Aの条件数が大きい場合,つまり,ls_pcg.eq1.gifが平べったい楕円体になっていると, 誤差が大きくなり,収束が遅くなる. 収束を早めたい場合は,条件数を小さくするように前処理としてAを変形すればよい. つまり,

ls_pcg.eq2.gif

ここで,ls_pcg.eq3.gifls_pcg.eq4.gifの正則行列である. このときにls_pcg.eq5.gifの条件数が1に近いと高速な収束することが期待できる. このように前処理を施すことで収束を早める方法が前処理付きクリロフ部分空間法(Preconditioned Krylov subspace method)である. 共役勾配法に適用した場合は,前処理付き共役勾配法(Preconditioned Conjugate Gradiate method : PCG法)と呼ぶ. ちなみに,固有値の最大値と最小値の比が収束性に影響するのは共役勾配法だけでなく, 他のクリロフ部分空間法でも同様であるので,上記の前処理をそれらに適用することもできる. ここではよく用いられる不完全コレスキー分解付き共役勾配法(Incomplete Cholesky Conjugate Gradient method : ICCG法)について述べる.

不完全コレスキー分解付き共役勾配法

不完全コレスキー分解により係数行列は以下のように分解される.

ls_pcg.eq6.gif

対角行列ls_pcg.eq7.gifに対して,ls_pcg.eq8.gifを定義する. Dは対角行列であるのでls_pcg.eq9.gifである. Aをこれを用いて表すと,

ls_pcg.eq10.gif

ここで,

ls_pcg.eq11.gif

と置いた. Uを使ってls_pcg.eq12.gifの式を書き換える.

ls_pcg.eq13.gif

ls_pcg.eq14.gifよりls_pcg.eq15.gifなので,

ls_pcg.eq16.gif

両辺にls_pcg.eq17.gifを掛ける.

ls_pcg.eq18.gif

この式から,

ls_pcg.eq19.gif

と計算するのが不完全コレスキー分解付共役勾配法(Incomplete Cholesky Conjugate Gradient method : ICCG法)である (ちなみに,コレスキー分解できればよいので修正コレスキー分解でも同じ). 前節の式に当てはめた場合,ls_pcg.eq20.gifである.

変形した係数行列ls_pcg.eq21.gifについて考える. 係数行列を変形すると,

ls_pcg.eq22.gif

ls_pcg.eq23.gifより,ls_pcg.eq24.gifなので,

ls_pcg.eq25.gif

と単位行列となる.単位行列の最小・最大固有値はともに1なので, もし正確に分解されているならば,係数行列の条件数は1になっている. 修正コレスキー分解ならばこの処理で常に条件数1が得られるが,計算速度の問題から近似的な分解ではあるが, 不完全コレスキー分解がよく用いられる.

次に共役勾配法の各手順を変換して,ICCG法に対応させる. ICCG法で得られる係数行列,変数,右辺項などを以下のように表す.

ls_pcg.eq26.gif

共役勾配法の各手順について変換した式を求める.

  • 残差ベクトルls_pcg.eq27.gifについて
    ls_pcg.eq28.gif
    よって,
    ls_pcg.eq29.gif
  • 方向ベクトルls_pcg.eq30.gifについて
    ls_pcg.eq31.gif
    とおくと,ls_pcg.eq32.gifより,
    ls_pcg.eq33.gif
    よって,
    ls_pcg.eq34.gif
  • 修正係数ls_pcg.eq35.gifについて
    ls_pcg.eq36.gif
    ls_pcg.eq37.gifなので,
    ls_pcg.eq38.gif
    よって,
    ls_pcg.eq39.gif
  • 修正係数ls_pcg.eq40.gifについて
    ls_pcg.eq41.gif
    ls_pcg.eq35.gifと同様に変換すると以下が得られる.
    ls_pcg.eq42.gif
  • ls_pcg.eq43.gifについて
    ls_pcg.eq44.gif
    よって,
    ls_pcg.eq45.gif
  • 残差ベクトルの更新値ls_pcg.eq46.gifについて
    ls_pcg.eq47.gif
    よって,
    ls_pcg.eq48.gif
  • 方向ベクトルの更新値ls_pcg.eq49.gifについて
    ls_pcg.eq50.gif
    よって,
    ls_pcg.eq51.gif

不完全コレスキー分解付き共役勾配法の計算手順

不完全コレスキー分解付き共役勾配法の計算手順を以下に示す(ls_pcg.eq52.gifとしている).

  1. 初期近似解ls_pcg.eq53.gifを適当に設定
  2. 不完全コレスキー分解によりL,Dを計算
  3. 初期近似解に対する残差を計算,修正方向ベクトルを70%&ref(ls_pcg.eq30.gif,nolink);ls_pcg.eq54.gifで初期化
    ls_pcg.eq55.gif
  4. 以下の計算を収束するまで繰り返す(k=0,1,2,...)
  5. ls_pcg.eq56.gifを計算
    1. 修正係数ls_pcg.eq35.gifを計算
      ls_pcg.eq57.gif
    2. k+1ステップの近似値を算出
      ls_pcg.eq45.gif
    3. k+1の近似値に対する残差を計算
      ls_pcg.eq58.gif
    4. ls_pcg.eq59.gifならば収束したとして計算を終了
    5. ls_pcg.eq60.gifを残差に掛ける
      ls_pcg.eq61.gif
    6. 方向ベクトルls_pcg.eq62.gifの修正係数ls_pcg.eq40.gifを計算
      ls_pcg.eq63.gif
    7. k+1ステップの方向ベクトルls_pcg.eq49.gifを計算
      ls_pcg.eq64.gif

ここで,ls_pcg.eq65.gifls_pcg.eq66.gifの計算は, ls_pcg.eq60.gifを直接計算するのではなく,

ls_pcg.eq67.gif

を前進代入,後退代入で解く.

不完全コレスキー分解付き共役勾配法の実装

不完全コレスキー分解付きの共役勾配法のC++による実装例を以下に示す.

/*!
 * 不完全コレスキー分解による前処理付共役勾配法によりA・x=bを解く
 * @param[in] A n×n正値対称行列
 * @param[in] b 右辺ベクトル
 * @param[out] x 結果ベクトル
 * @param[in] n 行列の大きさ
 * @param[inout] max_iter 最大反復数(反復終了後,実際の反復数を返す)
 * @param[inout] eps 許容誤差(反復終了後,実際の誤差を返す) 
 * @return 1:成功,0:失敗
 */
int ICCGSolver(const vector< vector<double> > &A, const vector<double> &b, vector<double> &x, int n, int &max_iter, double &eps)
{
    if(n <= 0) return 0;

    vector<double> r(n), p(n), y(n), r2(n);
    x.assign(n, 0.0);

    vector<double> d(n);
    vector< vector<double> > L(n, vector<double>(n, 0.0));
    IncompleteCholeskyDecomp2(A, L, d, n);

    // 第0近似解に対する残差の計算
    for(int i = 0; i < n; ++i){
        double ax = 0.0;
        for(int j = 0; j < n; ++j){
            ax += A[i][j]*x[j];
        }
        r[i] = b[i]-ax;
    }

    // p_0 = (LDL^T)^-1 r_0 の計算
    ICRes(L, d, r, p, n);

    float rr0 = dot(r, p, n), rr1;
    float alpha, beta;


    double e = 0.0;
    int k;
    for(k = 0; k < max_iter; ++k){
        // y = AP の計算
        for(int i = 0; i < n; ++i){
            y[i] = dot(A[i], p, n);
        }

        // alpha = r*r/(P*AP)の計算
        alpha = rr0/dot(p, y, n);

        // 解x、残差rの更新
        for(int i = 0; i < n; ++i){
            x[i] += alpha*p[i];
            r[i] -= alpha*y[i];
        }

        // (r*r)_(k+1)の計算
        ICRes(L, d, r, r2, n);
        rr1 = dot(r, r2, n);

        // 収束判定 (||r||<=eps)
        e = sqrt(rr1);
        if(e < eps){
            k++;
            break;
        }

        // βの計算とPの更新
        beta = rr1/rr0;
        for(int i = 0; i < n; ++i){
            p[i] = r2[i]+beta*p[i];
        }

        // (r*r)_(k+1)を次のステップのために確保しておく
        rr0 = rr1;
    }

    max_iter = k;
    eps = e;

    return 1;
}

ここで,dot関数はstd::vectorの内積を求める関数, IncompleteCholeskyDecomp2関数は三角分解のところで説明した不完全コレスキー分解のコードである.

また,ICRes関数は前進代入,後退代入でls_pcg.eq68.gifを計算する関数である.

/*!
 * (LDL^T)^-1 r の計算
 * @param[in] L,d IC分解で得られた下三角行列と対角行列(対角成分のみのベクトル)
 * @param[in] r 残差ベクトル
 * @param[in] u (LDL^T)^-1 rを計算した結果
 */
inline void ICRes(const vector< vector<double> > &L, const vector<double> &d, const vector<double> &r, vector<double> &u, int n)
{
    vector<double> y(n);
    for(int i = 0; i < n; ++i){
        double rly = r[i];
        for(int j = 0; j < i; ++j){
            rly -= L[i][j]*y[j];
        }
        y[i] = rly/L[i][i];
    }

    for(int i = n-1; i >= 0; --i){
        double lu = 0.0;
        for(int j = i+1; j < n; ++j){
            lu += L[j][i]*u[j];
        }
        u[i] = y[i]-d[i]*lu;
    }
}

参考文献

  • 佐藤次男, 中村理一郎, “よくわかる数値計算 アルゴリズムと誤差解析の実際”, 日刊工業新聞社, 2001.
  • 川上一郎, “数値計算の基礎”, http://www7.ocn.ne.jp/~kawa1/

トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2024-03-08 (金) 18:06:11