M.Hiroi's Home Page

Julia Language Programming

お気楽 Julia プログラミング超入門

[ Home | Light | Julia ]

線形代数編

●逆反復法

固有値 \(\lambda_i\) の近似値 \(\lambda'\) が分かっている場合、それよりも高い精度の固有値と固有ベクトルを逆累乗法を使って求めることができます。これを「逆反復法」といいます。原理を簡単に説明しましょう。行列 \(A\) の固有値が \(\lambda_1, \ldots, \lambda_i, \ldots \lambda_n\) とすると、行列 \(A' = A - \lambda'I\) の固有値は \(\lambda_1 - \lambda', \ldots, \lambda_i - \lambda', \ldots, \lambda_n - \lambda'\) となります。\(\lambda'\) は \(\lambda_i\) に近い値なので、\(|\lambda_i - \lambda|\) は最小値であることが期待できます。つまり、行列 \(A'\) に逆累乗法を適用すれば、\(\lambda_i - \lambda'\) を求めることができるわけです。この値を k とすれば、\(\lambda_i = \lambda' + k\) となります。

なお、同様な原理で累乗法を高速化する手法があります。\(B = A - pI\) としたとき、\(|\frac{\lambda_2}{\lambda_1}| \gt |\frac{\lambda_2 - p}{\lambda_1 - p}|\) になるならば、行列 \(B\) に累乗法を適用したほうが収束が速くなるはずです。これを「原点移動 (シフト法)」といいます。

ただし、参考文献によっては、逆反復法のことをシフト法と呼ぶことがあるようです。まあ、どちらの方法も対角成分の値をシフトするので、シフト法というと混乱するような気がします。本稿では前者を逆反復法、後者を原点移動と呼ぶことにします。

プログラムは簡単です。次のリストを見てください。

リスト : 逆反復法と原点移動

# 逆反復法
function power_inv_shift(a::Matrix{Float64}, d::Float64; max_iter = 512, eps = 1e-8, tiny = 1e-10)
    k, xs = power_inv(a - d * diagm(0 => ones(size(a, 1))), max_iter=max_iter, eps=eps, tiny=tiny)
    k + d, xs
end

# 原点移動
function power_shift(a::Matrix{Float64}, d::Float64; max_iter = 512, eps = 1e-8, tiny = 1e-10)
    k, xs = power(a - d * diagm(0 => ones(size(a, 1))), max_iter=max_iter, eps=eps, tiny=tiny)
    k + d, xs
end

どの関数も引数 a が行列で、d がシフトする値です。Julia の場合、a - dI は a - d * diagm(0 => ones(size(a, 1))) で求めることができます。あとは、適切な関数 (power_inv, power) を呼び出して、求めた固有値に d を加算するだけです。

それでは実際に試してみましょう。最初は逆反復法です。ご参考までに収束の様子を表示しています。

julia> a = [1. 4. 5.; 4. 2. 6.; 5. 6. 3.]
3×3 Matrix{Float64}:
 1.0  4.0  5.0
 4.0  2.0  6.0
 5.0  6.0  3.0

julia> eigen(a)
Eigen{Float64,Float64,Matrix{Float64},Vector{Float64}}
eigenvalues:
3-element Vector{Float64}:
 -3.668683097953268
 -2.5072879670936397
 12.175971065046879
eigenvectors:
3×3 Matrix{Float64}:
 -0.312986   0.809585  0.4966
 -0.57735   -0.57735   0.57735
  0.754126  -0.10601   0.648117

julia> power_inv_shift(a, 12.1)
1 3.195039716555222 [0.488765, 0.580469, 0.651279]
2 13.16183309095262 [0.49664, 0.577333, 0.648101]
3 13.16290613028577 [0.4966, 0.57735, 0.648117]
4 13.162906158845166 [0.4966, 0.57735, 0.648117]
(12.175971065046905, [0.4966, 0.57735, 0.648117])

julia> power_inv_shift(a, -3.6)
1 -0.8108108108108013 [-0.175618, -0.658568, 0.731742]
2 -14.160434261633576 [0.321257, 0.571438, -0.755151]
3 -14.55800683665054 [-0.312466, -0.577721, 0.754058]
4 -14.559616430436828 [0.313018, 0.577327, -0.754131]
5 -14.55962279027976 [-0.312984, -0.577352, 0.754126]
6 -14.559622815406422 [0.312986, 0.57735, -0.754126]
(-3.668683097953267, [-0.312986, -0.57735, 0.754126])

julia> power_inv_shift(a, -2.5)
1 -89.99999999999244 [-0.810186, 0.576133, 0.108025]
2 -137.21166936789768 [0.80959, -0.577342, -0.106021]
3 -137.21247460733682 [-0.809585, 0.57735, 0.10601]
4 -137.21247463815925 [0.809585, -0.57735, -0.10601]
(-2.5072879670936414, [-0.809585, 0.57735, 0.10601])

julia> b = ones(4, 4)
4×4 Matrix{Float64}:
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0

julia> b += diagm(0 => [4,5,6,7])
4×4 Matrix{Float64}:
 5.0  1.0  1.0  1.0
 1.0  6.0  1.0  1.0
 1.0  1.0  7.0  1.0
 1.0  1.0  1.0  8.0

julia> eigen(b)
Eigen{Float64,Float64,Matrix{Float64},Vector{Float64}}
eigenvalues:
4-element Vector{Float64}:
 4.296089645312119
 5.392275290272983
 6.5077487053636425
 9.80388635905124
eigenvectors:
4×4 Matrix{Float64}:
  0.905684   -0.225903   0.135941  -0.332002
 -0.380963   -0.801782   0.226102  -0.401113
 -0.157381    0.517536   0.671404  -0.506561
 -0.0991762   0.19563   -0.692542  -0.687225

julia> power_inv_shift(b, 5.3)
1 -0.22425084342131507 [-0.0858362, 0.903939, -0.387402, -0.159519]
2 9.39906651478699 [0.257113, 0.789462, -0.519894, -0.200881]
3 10.823449646815815 [0.223123, 0.80303, -0.516856, -0.195497]
4 10.837021987942329 [0.226165, 0.801677, -0.517567, -0.195672]
5 10.837136434204796 [0.225879, 0.801792, -0.51753, -0.195628]
6 10.8371373992314 [0.225905, 0.801781, -0.517536, -0.19563]
(5.392275290273574, [0.225903, 0.801782, -0.517535, -0.19563])

julia> power_inv_shift(b, 6.6)
1 -0.5642023346303511 [-0.366356, -0.189494, -0.505318, 0.757977]
2 -9.963758148788951 [0.148462, 0.230594, 0.66566, -0.694033]
3 -10.837782504602306 [-0.136476, -0.226509, -0.670924, 0.692769]
4 -10.839948840139247 [0.135969, 0.226141, 0.671373, -0.692554]
5 -10.839956238031057 [-0.135942, -0.226105, -0.671402, 0.692543]
6 -10.839956272963281 [0.135941, 0.226102, 0.671404, -0.692542]
(6.507748705363639, [-0.135941, -0.226102, -0.671404, 0.692542])

逆反復法の場合、固有値の近似が良ければ収束も速くなるようで、高い精度の固有値とその固有ベクトルを高速に求めることができます。

次はシフト法です。

julia> a
3×3 Matrix{Float64}:
 1.0  4.0  5.0
 4.0  2.0  6.0
 5.0  6.0  3.0

julia> power(a)
1, 0.0, [0.0, 0.0, 1.0]
2, 3.0, [0.597614, 0.717137, 0.358569]
3, 10.428571428571429, [0.455378, 0.517475, 0.724465]
4, 12.000428449014567, [0.50746, 0.5946, 0.623646]
5, 12.159906346157214, [0.493337, 0.571957, 0.65535]
6, 12.174512735519642, [0.497551, 0.578981, 0.645929]
7, 12.17583872124317, [0.496317, 0.576855, 0.648774]
8, 12.175959052754244, [0.496684, 0.5775, 0.647919]
9, 12.17596997462294, [0.496575, 0.577305, 0.648176]
10, 12.175970966057655, [0.496607, 0.577364, 0.648099]
11, 12.175971056060387, [0.496598, 0.577346, 0.648122]
(12.175971064231074, [0.4966, 0.577352, 0.648115])

julia> power_shift(a, 3.)
1, 0.0, [1.0, 0.0, 0.0]
2, -2.0, [-0.298142, 0.596285, 0.745356]
3, 1.1555555555555561, [0.892008, 0.356803, 0.277514]
4, 4.491159135559923, [0.124621, 0.589555, 0.798056]

    ・・・省略・・・

30, 9.175970956226864, [0.49658, 0.577316, 0.648162]
31, 9.175971007573628, [0.496614, 0.577375, 0.648084]
32, 9.175971034691985, [0.49659, 0.577332, 0.648141]
33, 9.175971049014588, [0.496607, 0.577364, 0.648099]
(12.175971056579188, [0.496594, 0.577341, 0.648129])

julia> power_shift(a, -3.)
1, 0.0, [0.0, 0.0, 1.0]
2, 6.0, [0.507673, 0.609208, 0.609208]
3, 15.134020618556699, [0.495752, 0.576144, 0.649837]
4, 15.175889771554242, [0.496626, 0.577411, 0.648042]
5, 15.17597090760561, [0.496598, 0.577348, 0.64812]
6, 15.175971064741647, [0.4966, 0.57735, 0.648117]
(12.17597106504631, [0.4966, 0.57735, 0.648117])

julia> b
4×4 Matrix{Float64}:
 5.0  1.0  1.0  1.0
 1.0  6.0  1.0  1.0
 1.0  1.0  7.0  1.0
 1.0  1.0  1.0  8.0

julia> power(b)
1, 0.0, [0.0, 0.0, 0.0, 1.0]
2, 8.0, [0.122169, 0.122169, 0.122169, 0.977356]
3, 8.716417910447761, [0.206842, 0.220632, 0.234421, 0.923896]
4, 9.234074919186156, [0.258927, 0.288519, 0.32107, 0.864076]

    ・・・省略・・・

23, 9.803886246828288, [0.331985, 0.401085, 0.506479, 0.68731]
24, 9.803886309604092, [0.331991, 0.401094, 0.506507, 0.687282]
25, 9.803886337263988, [0.331995, 0.401101, 0.506525, 0.687263]
26, 9.803886349451385, [0.331997, 0.401105, 0.506537, 0.68725]
(9.803886354821369, [0.331999, 0.401108, 0.506545, 0.687242])

julia> power_shift(b, 4.)
1, 0.0, [0.0, 0.0, 0.0, 1.0]
2, 4.0, [0.229416, 0.229416, 0.229416, 0.917663]
3, 5.263157894736842, [0.297133, 0.339581, 0.382029, 0.806505]
4, 5.690090090090091, [0.318991, 0.378339, 0.452523, 0.741841]
5, 5.782180397336416, [0.326812, 0.392174, 0.483169, 0.711296]
6, 5.799820963675336, [0.32985, 0.397455, 0.496432, 0.6977]
7, 5.803127118525351, [0.331091, 0.399578, 0.502176, 0.691762]
8, 5.803744616019188, [0.331613, 0.40046, 0.504664, 0.689187]
9, 5.803859897236706, [0.331835, 0.400833, 0.505741, 0.688073]
10, 5.803881418854967, [0.33193, 0.400993, 0.506206, 0.687592]
11, 5.803885436750952, [0.331971, 0.401061, 0.506408, 0.687384]
12, 5.803886186863651, [0.331989, 0.401091, 0.506495, 0.687294]
13, 5.803886326904883, [0.331996, 0.401103, 0.506533, 0.687255]
14, 5.80388635304972, [0.331999, 0.401109, 0.506549, 0.687238]
(9.8038863579308, [0.332001, 0.401111, 0.506556, 0.687231])

julia> power_shift(b, -4.)
1, 0.0, [0.0, 0.0, 0.0, 1.0]
2, 12.0, [0.0824786, 0.0824786, 0.0824786, 0.989743]
3, 12.489795918367347, [0.150552, 0.157098, 0.163644, 0.962224]
4, 12.910835939843178, [0.202964, 0.219081, 0.236204, 0.924671]

    ・・・省略・・・

34, 13.803886271700657, [0.331985, 0.401084, 0.506479, 0.687311]
35, 13.8038863084367, [0.331989, 0.401091, 0.506498, 0.687291]
36, 13.803886329722985, [0.331992, 0.401097, 0.506513, 0.687275]
37, 13.80388634205714, [0.331994, 0.401101, 0.506525, 0.687263]
(9.803886349204083, [0.331996, 0.401104, 0.506533, 0.687254])

原点移動の場合、シフトする値によって収束回数が増えたり減ったりします。何かしらの選択基準があればよいのですが、よくわかりませんでした。シフト値の最適値は行列によって変わるでしょうから、明確な選択基準がなければ (M.Hiroi が知らないだけかもしれませんが)、ちょっと使いにくい方法のような気がします。興味のある方はいろいろ試してみてください。

●ヤコビ法

実対称行列の固有値と固有ベクトルを求める簡単な方法に「ヤコビ法 (Jacobi method)」があります。なお、連立一次方程式を反復法で解く方法にもヤコビ法があります。拙作のページ ヤコビ法 (反復法) で説明しているので、興味のある方はお読みくださいませ。

ヤコビ法のポイントは、直交行列 \(X_1\) を使って相似変換 \(A_1 = {X_1}^{\mathrm{T}} A X_1\) を行うところです。このとき、\(A\) の非対角成分のひとつが 0 になるように直交行列 \(X_1\) を定めます。つまり、相似変換 \(A_n = {X_n}^{\mathrm{T}} A_{n-1} X_n\) を繰り返して \(A\) を対角化するわけです。対角化が完了したとき (非対角成分がすべて 0 になったとき)、\(A_n\) の対角成分が固有値になり、\(X_1 X_2 \cdots X_n\) が固有ベクトルになります。これを式で表すと次のようになります。

\( {X_n}^{\mathrm{T}} \cdots {X_2}^{\mathrm{T}} {X_1}^{\mathrm{T}} A X_1 X_2 \cdots X_n = \varLambda \)

直交行列 \(X\) には「ギブンス回転」を使います。ギブスン回転は N 次元の行列 \(A\) の (i, j) 平面の回転を表します。

X[i, i] = cos(x)
X[j, j] = cos(x)
X[i, j] = sin(x)
X[j, i] = -sin(x)
X の対角成分 は 1, 残りの非対角成分は 0
                 i           j
  +---------------------------------------------
  | 1, 0, ...
  | 0, 1, ...
  |       ...
i |            cos(x), ..., sin(x), 
  |
  |                    ...
  |
j |           -sin(x), ..., cos(x),
  |                                   ...
  |                                   ..., 1, 0
  |                                   ..., 0, 1
  +---------------------------------------------

二次元行列の場合、ギブンス回転は回転行列と同じになります。ここでは二次元行列で説明します。非対角成分を 0 にする角度 r は次のように求めることができます。

行列 A = [[x, z],   X = [[ cos(r), sin(r)],
          [z, y]]        [-sin(r), cos(r)]]

XTAX = [[cos(r), -sin(r)],   [[x, z],    [[cos(r), sin(r)],
        [sin(r),  cos(r)]  *  [z, y]]  *  [-sin(r), cos(r)]]

非対角成分 => z * (cos2(r) - sin2(r)) + (x - y) * sin(r)cos(r) = 0

三角関数の公式

tan(x) = sin(x) / cons(x)
cos2(x) - sin2(x) = cos(2x)
sin(x)cos(x) = sin(2x) / 2

を使って

z * cos(2r) = (y - x) * sin(2r) / 2
2 * z / (y - x) = sin(2r) / cons(2r) = tan(2r)

r = atan(2 * z / (y - x)) / 2,  (x != y)
r = π/ 4,                      (x == y)

実際の数値計算では、角度ではなくて sir(r) と cos(r) の値を求めたほうが誤差が少なくなるようです。sin(r) と cos(r) の求め方は 参考 URL 4 がわかりやすくまとまっていると思います。優れたドキュメントとプログラムを公開されている fussy さんに感謝いたします。今回はプログラムを簡単にするため、atan() で角度を求めることにしましょう。

それでは、これで本当に 0 になるのか実際に試してみましょう。

julia> givens(r) = [cos(r) sin(r); -sin(r) cos(r)]
givens (generic function with 1 method)

julia> a = [2. 1.; 1. 3.]
2×2 Matrix{Float64}:
 2.0  1.0
 1.0  3.0

julia> x = givens(0.5 * atan(2 / (3 - 2)))
2×2 Matrix{Float64}:
  0.850651  0.525731
 -0.525731  0.850651

julia> x' * a * x
2×2 Matrix{Float64}:
 1.38197      -1.11022e-16
 2.22045e-16   3.61803

julia> eigen(a)
Eigen{Float64,Float64,Matrix{Float64},Vector{Float64}}
eigenvalues:
2-element Vector{Float64}:
 1.381966011250105
 3.618033988749895
eigenvectors:
2×2 Matrix{Float64}:
 -0.850651  0.525731
  0.525731  0.850651

julia> b = [2. 1.; 1. 2.]
2×2 Matrix{Float64}:
 2.0  1.0
 1.0  2.0

julia> pi
π = 3.1415926535897...

julia> y = givens(pi / 4.)
2×2 Matrix{Float64}:
  0.707107  0.707107
 -0.707107  0.707107

julia> y' * b * y
2×2 Matrix{Float64}:
 1.0  2.22045e-16
 0.0  3.0

julia> eigen(b)
Eigen{Float64,Float64,Matrix{Float64},Vector{Float64}}
eigenvalues:
2-element Vector{Float64}:
 1.0
 3.0
eigenvectors:
2×2 Matrix{Float64}:
 -0.707107  0.707107
  0.707107  0.707107

julia> c = rand(2, 2)
2×2 Matrix{Float64}:
 0.897777  0.648882
 0.136918  0.703306

julia> c[1, 2] = c[2, 1]
0.13691784459147915

julia> c
2×2 Matrix{Float64}:
 0.897777  0.136918
 0.136918  0.703306

julia>  z = givens(0.5 * atan(2 * c[1, 2] / (c[2, 2] - c[1, 1])))
2×2 Matrix{Float64}:
 0.888543  -0.458794
 0.458794   0.888543

julia> z' * c * z
2×2 Matrix{Float64}:
 0.968474     5.55112e-17
 5.55112e-17  0.632609

julia> eigen(c)
Eigen{Float64,Float64,Matrix{Float64},Vector{Float64}}
eigenvalues:
2-element Vector{Float64}:
 0.6326094347289221
 0.968473829063695
eigenvectors:
2×2 Matrix{Float64}:
  0.458794  -0.888543
 -0.888543  -0.458794

確かにギブンス回転を使って非対角成分を 0 にすることができました。

●ヤコビ法のプログラム

それではプログラムを作りましょう。ヤコビ法は Julia を使うと簡単にプログラムすることができます。次のリストを見てください。

リスト : ヤコビ法

# ヤコビ法
function jacobi(a::Matrix{Float64}, max_iter = 512, e = 1e-14)
    n = size(a, 1)
    vs = ones(n)
    ks = diagm(0 => vs)          # 固有ベクトル
    xs = Matrix{Float64}(undef, n, n)
    for i = 1 : max_iter
        # 絶対値最大の非対角成分を探す
        m, j = findmax(abs.(triu(a, 1)))
        x, y = Tuple(j)
        # println("$i, $x, $y, $(a[x, y]), $(a[y, x])")
        if m < e
            return diag(a), ks
        end
        # 角度を求める
        d = a[y, y] - a[x, x]
        if d == 0.0
            r = pi / 4.0
        else
            r = 0.5 * atan(2.0 * a[x, y] / d)
        end
        # 直交行列の生成
        fill!(xs, 0.0)
        xs[diagind(xs)] = vs
        xs[x, x] = cos(r)
        xs[y, y] = cos(r)
        xs[x, y] = sin(r)
        xs[y, x] = - sin(r)
        #
        a = xs' * a * xs
        ks = ks * xs
    end
end

引数 a は実対称行列で、max_iter が繰り返しの最大値、e が 0 と判断する値です。変数 ks に固有ベクトルを格納します。ks は単位行列に初期化します。非対角成分の選択方法ですが、今回は絶対値最大の成分から順に消していくことにします。a は対称行列なので、対角成分を除いた上三角行列の中から最大値を findmax() で求めています。返り値は見つけた最大値とその位置です。

引数が行列の場合、findmax(), findmin() が返す添字のデータ型は CartesianIndex(x, y) になるので、Tuple() でタプルに変換して変数 x, y にセットしています。簡単な例を示しましょう。

julia> a = rand(5, 5)
5×5 Matrix{Float64}:
 0.513379  0.525335  0.847615   0.0579795  0.558406
 0.504764  0.74323   0.0838081  0.490538   0.235075
 0.922555  0.323586  0.201859   0.597376   0.316467
 0.139468  0.964848  0.261251   0.941041   0.0879425
 0.944212  0.819577  0.485142   0.679319   0.116133

julia> m, i = findmax(a)
(0.9648481555934574, CartesianIndex(4, 2))

julia> x, y = Tuple(i)
(4, 2)

julia> a[i]
0.9648481555934574

julia> a[x, y]
0.9648481555934574

julia> m, i = findmin(a)
(0.057979472597648574, CartesianIndex(1, 4))

julia> x, y = Tuple(i)
(1, 4)

julia> a[i]
0.057979472597648574

julia> a[x, y]
0.057979472597648574

モジュール LinearAlgebra の関数 triu() は引数の行列を上三角行列に、tril() は下三角行列に変換します。二番目の引数に整数値を指定すると、取り出す三角行列の範囲を変更することができます。簡単な例を示しましょう。

julia> a = ones(5,5)
5×5 Matrix{Float64}:
 1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0

julia> triu(a)
5×5 Matrix{Float64}:
 1.0  1.0  1.0  1.0  1.0
 0.0  1.0  1.0  1.0  1.0
 0.0  0.0  1.0  1.0  1.0
 0.0  0.0  0.0  1.0  1.0
 0.0  0.0  0.0  0.0  1.0

julia> tril(a)
5×5 Matrix{Float64}:
 1.0  0.0  0.0  0.0  0.0
 1.0  1.0  0.0  0.0  0.0
 1.0  1.0  1.0  0.0  0.0
 1.0  1.0  1.0  1.0  0.0
 1.0  1.0  1.0  1.0  1.0

julia> triu(a, 1)
5×5 Matrix{Float64}:
 0.0  1.0  1.0  1.0  1.0
 0.0  0.0  1.0  1.0  1.0
 0.0  0.0  0.0  1.0  1.0
 0.0  0.0  0.0  0.0  1.0
 0.0  0.0  0.0  0.0  0.0

julia> triu(a, -1)
5×5 Matrix{Float64}:
 1.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0
 0.0  1.0  1.0  1.0  1.0
 0.0  0.0  1.0  1.0  1.0
 0.0  0.0  0.0  1.0  1.0

最大値 a[x, y] の絶対値が e よりも小さくなったならば、非対角成分がすべて 0 になったと判断して、固有値 (a の対角成分) と固有ベクトル ks を返します。そうでなければ、直交行列 xs を生成して a を相似変換して、ks を ks * xs の値に更新します。

関数 diagind(A) は行列 A の対角成分の座標を返します。簡単な例を示しましょう。

julia> a = rand(5, 5)
5×5 Matrix{Float64}:
 0.239522  0.663719  0.396802   0.924408  0.00656942
 0.315202  0.387456  0.908763   0.280866  0.87524
 0.980997  0.755987  0.492619   0.469537  0.191903
 0.377995  0.506459  0.0658157  0.705611  0.123246
 0.926698  0.77      0.784128   0.481811  0.418447

julia> diagind(a)
1:6:25

julia> a[diagind(a)]
5-element Vector{Float64}:
 0.23952150080966295
 0.3874563894608829
 0.49261909906925605
 0.7056106623795324
 0.4184469857223021

julia> a[diagind(a)] = ones(5)
5-element Vector{Float64}:
 1.0
 1.0
 1.0
 1.0
 1.0

julia> a
5×5 Matrix{Float64}:
 1.0       0.663719  0.396802   0.924408  0.00656942
 0.315202  1.0       0.908763   0.280866  0.87524
 0.980997  0.755987  1.0        0.469537  0.191903
 0.377995  0.506459  0.0658157  1.0       0.123246
 0.926698  0.77      0.784128   0.481811  1.0

●実行例

それでは実際に試してみましょう。収束の様子を見るため、0 にする対角成分の添字と値を表示しています。

julia> jacobi([2. 1.; 1. 3.])
1, 1, 2, 1.0, 1.0
2, 1, 2, -1.1102230246251565e-16, 2.220446049250313e-16
([1.38197, 3.61803], [0.850651 0.525731; -0.525731 0.850651])

julia> jacobi([2. 1.; 1. 2.])
1, 1, 2, 1.0, 1.0
2, 1, 2, 2.220446049250313e-16, 0.0
([1.0, 3.0], [0.707107 0.707107; -0.707107 0.707107])

julia> jacobi([1. 4. 5.; 4. 2. 6.; 5. 6. 3.])
1, 2, 3, 6.0, 6.0
2, 1, 3, 6.387849389602061, 6.387849389602061
3, 1, 2, -0.3837300281922351, -0.38373002819223523
4, 2, 3, -0.20518562849673092, -0.20518562849673116
5, 1, 3, 0.07764848736577329, 0.07764848736577315
6, 1, 2, 0.0010057157079803223, 0.0010057157079802084
7, 2, 3, 5.318604906830139e-6, 5.318604906608818e-6
8, 1, 3, 4.605674995833599e-9, 4.605674853394528e-9
9, 1, 2, -1.5459956007250589e-15, -1.6600536215271706e-15
([-2.50729, -3.66868, 12.176], [0.809585 0.312986 0.4966; -0.57735 0.57735 0.57735; -0.10601 -0.754126 0.648117])

julia> d = ones(4, 4)
4×4 Matrix{Float64}:
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0

julia> d += diagm(0 => [4,5,6,7])
4×4 Matrix{Float64}:
 5.0  1.0  1.0  1.0
 1.0  6.0  1.0  1.0
 1.0  1.0  7.0  1.0
 1.0  1.0  1.0  8.0

julia> jacobi(d)
1, 1, 2, 1.0, 1.0
2, 2, 3, 1.3763819204711736, 1.3763819204711736
3, 3, 4, 1.6580314962279525, 1.6580314962279525
4, 1, 3, 0.4011483560399159, 0.4011483560399158
5, 2, 4, 0.27753210704204684, 0.2775321070420468
6, 1, 2, -0.2418160453957441, -0.24181604539574428
7, 2, 3, 0.2300801682095411, 0.23008016820954125
8, 3, 4, 0.06517159208983365, 0.06517159208983363
9, 1, 3, 0.05297008305598126, 0.05297008305598116
10, 1, 4, 0.0076388221073113265, 0.00763882210731137
11, 2, 4, -0.005417016203362336, -0.005417016203362362
12, 1, 2, -0.002742939677036741, -0.002742939677037079
13, 2, 3, -0.00013318242271641932, -0.00013318242271624547
14, 3, 4, 7.411922640029331e-5, 7.411922640029834e-5
15, 1, 4, 1.332060065787488e-5, 1.3320600657920408e-5
16, 1, 3, -5.867295797949511e-7, -5.867295799118133e-7
17, 2, 4, -3.1094169513768315e-8, -3.109416954510149e-8
18, 1, 2, -1.7534582710833612e-11, -1.7534921680978462e-11
19, 3, 4, -3.533813375839731e-12, -3.5338083403353147e-12
20, 2, 3, -6.992031425874347e-13, -6.990293571343703e-13
21, 1, 4, 1.0692825552099565e-19, 4.5636996101360793e-17
([4.29609, 5.39228, 9.80389, 6.50775], 
 [0.905684 0.225903 0.332002 -0.135941;
 -0.380963 0.801782 0.401113 -0.226102;
 -0.157381 -0.517536 0.506561 -0.671404;
 -0.0991762 -0.19563 0.687225 0.692542])

ヤコビ法の場合、相似変換のとき以前 0 にした非対角成分が 0 でなくなることがあります。それでも、最初よりは 0 に近い値になるので、相似変換を繰り返し行っていけば、非対角成分は 0 に近づいていきます。最終的にはすべての非対角成分を 0 にすることができますが、それなりの回数が必要になります。特に、行列 (次元数 N) が大きくなると、収束するまでに必要な回数は大幅に増えてしまいます。このため、ヤコビ法が実用になるのは N が数十程度までといわれています。

●参考文献・URL

  1. 奥村晴彦,『C言語による最新アルゴリズム事典』, 技術評論社, 1991
  2. 行列の固有値問題 (PDF), (桂田祐史さん)
  3. 固有値解析 (PDF), (中島研吾さん)
  4. 固有値問題 (1) 対称行列の固有値, (fussy さん)
  5. ヤコビ法- Wikipedia
  6. ギブンス回転 - Wikipedia

●プログラムリスト

#
# eig.jl : 固有値と固有ベクトル
#
#          Copyright (C) 2018-2021 Makoto Hiroi
#
using LinearAlgebra

#
# 累乗法
#
function power(a::Matrix{Float64}; max_iter = 512, eps = 1e-8, tiny = 1e-10)
    x = zeros(size(a, 1))
    _, i = findmax(abs.(diag(a)))
    x[i] = 1.0
    k = 0.0
    for i = 1 : max_iter
        println("$i, $k, $x")
        x1 = a * x
        n = norm(x1)
        if n < tiny
            error("power($a)\nベクトルの大きさが $n になりました")
        end
        k1 = dot(x1, x) / dot(x, x)
        if abs(k1 - k) < eps
            return k1, x1 / n
        end
        k = k1
        x = x1 / n
    end
end

function power_all(a::Matrix{Float64}; max_iter = 512, eps = 1e-8, tiny = 1e-10)
    n = size(a, 1)
    xs = zeros(n)       # 固有値
    yss = zeros(n, n)   # 固有ベクトル
    for i = 1 : n
        x, ys = power(a, max_iter=max_iter, eps=eps, tiny=tiny)
        xs[i] = x
        yss[:, i] = ys
        zs = reshape(ys, n, 1)
        a -= x * zs * zs'
    end
    xs, yss
end

#
# LU 分解による連立一次方程式の解法
#

# ピボット選択
function select_pivot(xs::Matrix{Float64}, idx::Vector{Int}, i::Int)
    _, k = findmax(abs.(xs[i:end, i]))
    k += i - 1
    if k != i
        temp = xs[i, :]
        xs[i, :] = xs[k, :]
        xs[k, :] = temp
        idx[i], idx[k] = idx[k], idx[i]
    end
end

function mylu_pv(xs::Matrix{Float64})
    n = size(xs, 1)
    zs = copy(xs)
    idx = collect(1 : n)
    for i = 1 : n
        select_pivot(zs, idx, i)
        for j = i + 1 : n
            temp = zs[j, i] / zs[i, i]
            for k = i + 1 : n
                zs[j, k] -= temp * zs[i, k]
            end
            zs[j, i] = temp
        end
    end
    zs, idx
end

function mylu_solver(xs::Matrix{Float64}, ys::Vector{Float64})
    n = size(xs, 1)
    zs = copy(ys)
    # 前進代入
    for i = 2 : n, j = 1 : i - 1
        zs[i] -= xs[i, j] * zs[j]
    end
    # 後退代入
    for i = n : -1 : 1
       for j = i + 1 : n
           zs[i] -= xs[i, j] * zs[j]
       end
       zs[i] /= xs[i, i]
    end
    zs
end

#
# 逆累乗法
#
function power_inv(a::Matrix{Float64}; max_iter = 512, eps = 1e-8, tiny = 1e-10)
    a1, idx = mylu_pv(a)
    x = zeros(size(a, 1))
    x[1] = 1.0
    k = 0.0
    for i = 1 : max_iter
        x1 = mylu_solver(a1, x[idx])
        n = norm(x1)
        if n < tiny
            error("power_inv($a)\nベクトルの大きさが $n になりました")
        end
        k1 = dot(x1, x) / dot(x, x)
        if abs(k1 - k) < eps
            return 1 / k1, x1 / n
        end
        k = k1
        x = x1 / n
        # println("$i $k $x")
    end
end

# 逆反復法
function power_inv_shift(a::Matrix{Float64}, d::Float64; max_iter = 512, eps = 1e-8, tiny = 1e-10)
    k, xs = power_inv(a - d * diagm(0 => ones(size(a, 1))), max_iter=max_iter, eps=eps, tiny=tiny)
    k + d, xs
end

# 原点移動
function power_shift(a::Matrix{Float64}, d::Float64; max_iter = 512, eps = 1e-8, tiny = 1e-10)
    k, xs = power(a - d * diagm(0 => ones(size(a, 1))), max_iter=max_iter, eps=eps, tiny=tiny)
    k + d, xs
end

#
# ヤコビ法
#
function jacobi(a::Matrix{Float64}, max_iter = 512, e = 1e-14)
    n = size(a, 1)
    vs = ones(n)
    ks = diagm(0 => vs)          # 固有ベクトル
    xs = Matrix{Float64}(undef, n, n)
    for i = 1 : max_iter
        # 絶対値最大の非対角成分を探す
        m, j = findmax(abs.(triu(a, 1)))
        x, y = Tuple(j)
        # println("$i, $x, $y, $(a[x, y]), $(a[y, x])")
        if m < e
            return diag(a), ks
        end
        # 角度を求める
        d = a[y, y] - a[x, x]
        if d == 0.0
            r = pi / 4.0
        else
            r = 0.5 * atan(2.0 * a[x, y] / d)
        end
        # 直交行列の生成
        fill!(xs, 0.0)
        xs[diagind(xs)] = vs
        xs[x, x] = cos(r)
        xs[y, y] = cos(r)
        xs[x, y] = sin(r)
        xs[y, x] = - sin(r)
        #
        a = xs' * a * xs
        ks = ks * xs
    end
end

2021/12/05 改訂: Julia のバージョンを 1.6 に変更

Copyright (C) 2018-2021 Makoto Hiroi
All rights reserved.

[ Home | Light | Julia ]