【科学技術計算講座ミニ】QR法で行列の固有値を求める②

前回、行列の固有値をQR法で求める方法について解説しました。

行列の固有値を求めるQR法について解説します。ヘッセンベルグ行列、ウィルキンソンの移動法、デフレーションによるQR法の改良についても解説します。

今回はPythonでQR法のプログラムを作成していきます。

QR法のプログラム

早速ですが全体のプログラムを示しておきます。

import numpy as np

# sign function
def sign(x):
  if x >= 0:
    return 1
  else:
    return -1

# Hessenberg matrix
def hessenberg(a):
  n = a.shape[0]
  r = a.copy()
  u = np.zeros(n)
  for k in range(n-2):
    # |x|
    absx = 0
    for i in range(k+1, n):
      absx += r[i][k]**2
    absx = np.sqrt(absx)
    if absx == 0:
      continue

    # u=x-y
    u[k+1] = r[k+1][k] + sign(r[k+1][k]) * absx
    absu = u[k+1]**2
    for i in range(k+2, n):
      u[i] = r[i][k]
      absu += u[i]**2

    # H=I-2uu'/|u|^2
    h = np.eye(n)
    for i in range(k+1, n):
      for j in range(k+1, n):
        h[i][j] -= 2 * u[i] * u[j]  / absu

    # R=H'RH
    r = np.dot(h, r)
    r = np.dot(r, h)  

  return r    

# QR decomposition for Hessenberg matrix
def qr_decomposition(a):
  n = a.shape[0]
  r = a.copy()
  q = np.eye(n)
  u = np.zeros(n)
  for k in range(n-1):
    # |x|
    absx = 0
    for i in range(k, k+2):
      absx += r[i][k]**2
    absx = np.sqrt(absx)
    if absx == 0:
      continue

    # u=x-y
    u[k] = r[k][k] + sign(r[k][k]) * absx
    u[k+1] = r[k+1][k]
    absu = u[k]**2 + u[k+1]**2

    # H=I-2uu'/|u|^2
    h = np.eye(n)
    for i in range(k, k+2):
      for j in range(k, k+2):
        h[i][j] -= 2 * u[i] * u[j]  / absu

    r = np.dot(h, r)
    q = np.dot(q, h)  

  return q, r    

# Wilkinson shift
def wilkinson_shift(a):
  n = a.shape[0]
  a11 = a[n-2][n-2]
  a12 = a[n-2][n-1]
  a21 = a[n-1][n-2]
  a22 = a[n-1][n-1]
  c1 = a11 + a22
  c2 = np.sqrt((a11 - a22)**2 + 4 * a12 * a21)
  mu1 = 0.5 * (c1 + c2)
  mu2 = 0.5 * (c1 - c2)
  dmu1 = np.abs(a22 - mu1)
  dmu2 = np.abs(a22 - mu2)
  if dmu1 <= dmu2:
    return mu1
  else:
    return mu2  

# input parameter
iter_max = 1000
eps = 1.0e-12

a0 = np.array([[6., -3., 5.], [-1., 4., -5.], [-3., 3., -4.]]) 
n = a0.shape[0]
eigen_value = np.zeros(n)
h = hessenberg(a0)

print("A =\n", a0, "\n")
print("Hessenberg =\n", h, "\n")

#QR method
for k in range(n, 1, -1):
  # deflation
  a = np.zeros((k, k))
  for i in range(k):
    for j in range(k):
      a[i][j] = h[i][j]

  #QR method with Wilkinson shift
  for iter in range(iter_max):
    mu = wilkinson_shift(a)
    for i in range(k):
      a[i][i] -= mu

    q, r = qr_decomposition(a)
    a = np.dot(r, q)

    for i in range(k):
      a[i][i] += mu

    if np.abs(a[k-1][k-2]) < eps:
      eigen_value[k-1] = a[k-1][k-1]
      if k == 2:
        eigen_value[0] = a[0][0]
      h = a.copy()
      break   

print("Eigenvalue =\n", eigen_value, "\n")

プログラム全体のフローは次のようになっています。

QR法のフロー

以下プログラムの内容を説明していきますが、QR分解に関する部分は以下のページで詳しく解説しているので要点だけ示します。

行列のQR分解について解説します。ハウスホルダー変換を用いたQR分解の仕組を解説します。またPythonによるQR分解のプログラムも示します。

ヘッセンベルグ行列

ヘッセンベルグ行列を計算する関数 hessenbergを定義しています。

# Hessenberg matrix
def hessenberg(a):

基本的にはQR分解のコードと同じです。ただし、対角要素の一つ下まで値が入るため行が一つずれていることに注意してください。最後にハウスホルダー行列を右と左から掛け合わせています。

QR分解

QR分解を行う関数 qr_decompositionを定義しています。

# QR decomposition for Hessenberg matrix
def qr_decomposition(a):

以前のQR分解のコードと違うのは、扱う行列がヘッセンベルグ行列であるため、行に対しては対角要素の一つ下の要素までしか演算していないところです。

Wilkinsonの移動法

原点移動法のWilkinsonシフトの関数wilkinson_shiftを定義します。

# Wilkinson shift
def wilkinson_shift(a):
  n = a.shape[0]
  a11 = a[n-2][n-2]
  a12 = a[n-2][n-1]
  a21 = a[n-1][n-2]
  a22 = a[n-1][n-1]
  c1 = a11 + a22
  c2 = np.sqrt((a11 - a22)**2 + 4 * a12 * a21)
  mu1 = 0.5 * (c1 + c2)
  mu2 = 0.5 * (c1 - c2)
  dmu1 = np.abs(a22 - mu1)
  dmu2 = np.abs(a22 - mu2)
  if dmu1 <= dmu2:
    return mu1
  else:
    return mu2 

与えられた行列aの右下角の2×2の行列で固有値mu1mu2を求めます。2次の行列なので2次方程式の解の公式から求めています。このうち右下角の要素a22に近い方の固有値を返しています。

パラメータの入力

計算に必要なパラメータを入力します。

# input parameter
iter_max = 1000
eps = 1.0e-12

a0 = np.array([[6., -3., 5.], [-1., 4., -5.], [-3., 3., -4.]]) 
n = a0.shape[0]
eigen_value = np.zeros(n)
h = hessenberg(a0)

print("A =\n", a0, "\n")
print("Hessenberg =\n", h, "\n")

iter_maxはQR法の繰り返し計算の最大繰り返し数。epsはQR法で要素がゼロになったかどうかの収束判定値。a0は固有値を求める行列。nは行列の次数。eigen_valueは算出された固有値を格納する配列です。

ha0を変換したヘッセンベルグ行列です。a0hprintで出力しています。

QR法

ここからがQR法のメイン部分です。

#QR method
for k in range(n, 1, -1):
  # deflation
  a = np.zeros((k, k))
  for i in range(k):
    for j in range(k):
      a[i][j] = h[i][j]

最終的に、固有値はN個見つかります。1回のステップで、行列の一番右下角に求める固有値が計算されます。固有値が1つ見つかるとデフレーションで行列の次数を減らします。最終的にN-1回目のループで2×2の行列となり、2つの対角成分がそれぞれ固有値なので、全て見つかったことになります。

最初のfor文はN-1回のループで、インデックスkはNから2までループされます。

デフレーションで次数kの行列aを作っています。

  #QR method with Wilkinson shift
  for iter in range(iter_max):
    mu = wilkinson_shift(a)
    for i in range(k):
      a[i][i] -= mu

QR法の繰り返し部分です。先ほど定義したiter_maxまで最大でループされますが、この後出てくる収束判定により途中でループを抜けることになります。

まずmuにWilkinsonシフトで求めた固有値を入れます。そして、対角成分から引きます。

    q, r = qr_decomposition(a)
    a = np.dot(r, q)

    for i in range(k):
      a[i][i] += mu

qrにQR分解をしたQとRの行列を入れます。そして、それを逆にかけてaとします。最後に対角成分にmuを加えます。これで原点移動付きのQR法の計算になります。

    if np.abs(a[k-1][k-2]) < eps:
      eigen_value[k-1] = a[k-1][k-1]
      if k == 2:
        eigen_value[0] = a[0][0]
      h = a.copy()
      break   

最後に収束判定をします。a[k-1][k-2]epsより小さくゼロになったと判定されれば、QR法が収束したものとみなします。a[k-1][k-2]は行列の一番右下角の一つ左の要素です。今はヘッセンベルグ行列を考えているので、この要素がゼロになれば、最後の行の対角要素以外が全てゼロになったとみなすことができます。

収束すれば、eigen_valueに対角要素a[k-1][k-1](これが求める固有値)を格納します。k=2のときは2×2行列になったので、a[0][0]も格納してやります。

そして、ahにコピーしてbreakでループを抜けます。

プログラムの実行結果

プログラムを実行すると、次のように出力されます。

A =
 [[ 6. -3.  5.]
 [-1.  4. -5.]
 [-3.  3. -4.]]

Hessenberg =
 [[ 6.00000000e+00 -3.79473319e+00  4.42718872e+00]
 [ 3.16227766e+00 -3.80000000e+00  5.60000000e+00]
 [-5.55111512e-16 -2.40000000e+00  3.80000000e+00]]

Eigenvalue =
 [3. 2. 1.]

※このプログラムは固有値が複素数になる問題には対応していません。

まとめ

QR法による行列の固有値計算のプログラムをPythonで作成しました。今回は固有値のみを求めましたが、固有ベクトルは別途求める必要があります。固有ベクトルの求め方はまた別の機会にお話しします。

当サイトの以下のページでは行列の固有値を実際に計算することができますので試してみてください。

行列の固有値と固有ベクトルを計算するオンラインツールです。行列を入力して、[計算実行]ボタンを押すと計算結果が表示されます。実数、複素数どちらにも対応しています。固有値はQR法(原点シフト、デフレーション付き)、固有ベクトルは逆反復法で計算しています。


全体の目次

スポンサーリンク
科学技術計算のご相談は「キャットテックラボ」へ

科学技術計算やCAEに関するご相談、計算用プログラムの開発などお困りのことは「株式会社キャットテックラボ」へお問い合わせください。

お問い合わせはこちら

フォローする