Pythonで1から行列の固有値全て求めてみた~べき乗法~

0. はじめに


Pythonで固有値を求めたい.
人生でそう思う時はよくありますよね.
学校で主成分分析をすることになったのですが, そのためにPythonで固有値を求める必要がありました.

例えば$${\bm{A} = \left( \begin{array}{cc} 1 & 2 \\ 2 & 4 \end{array} \right)}$$
の行列式を求めたい.
そんな時使えるコードがこちらです.

import numpy as np
import numpy.linalg as LA

A = np.array([[1,2],[2,4])

LA.eig(A)

これで求まりました!

いやこんなの認めない.
こんなパッケージに頼るわけにいかない.
そんな人のための記事です.

ここから, 以下の順序で話していきます.
また今回考える行列は正定値対称行列だとします.
(動機が主成分分析だったためです. 拡張は頑張るとできると思います.) 

  1.  冪乗法とは?

  2. n個の固有値

  3. 実際のコード

  4. より良いアルゴリズム

最初に断っておきますが, 実用性は皆無です.
固有値求めたいなら, 上で書いたようなパッケージを使いましょう.

1. べき乗法とは

べき乗法とはその名の通り, 行列のべき乗(2乗,3乗,…,)を考えていくという手法です. 

今, 固有値を求めたい正定値対称行列$${\bm{A}  \in \mathbb{R}^{n\times n}}$$
とします.

この時, 固有値を$${\lambda_1,…,\lambda_n}$$
($${\lambda_1 > \cdots  > \lambda_n}$$ とします),
固有ベクトルを$${\bm{v}_1,…,\bm{v}_n}$$(これらは全て直交する単位行列とします)とすると, 

$${A = \lambda_1 \bm{v}_1 \bm{v}_1^\top + \cdots +  \lambda_n \bm{v}_n \bm{v}_n^\top}$$
と書けます.(固有値分解については以下を参照してください)https://qiita.com/KodaiS/items/b72ed968c480ee0f6aef


ここで, あるベクトル$${\bm{x} \in \mathbb{R}^n}$$が$${c_1,…,c_n\in \mathbb{R}}$$ として,

$$
\bm{x} = c_1\bm{v}_1 + \cdots c_n\bm{v}_n
$$

で表されるとします.

この時,

$$
\bm{Ax} = \lambda_1\ c_1 \bm{x}_1 + \cdots + \lambda_n c_n \bm{x}_n
$$

となります.

よって,

$$
\bm{A}^k\bm{x} = \lambda_1^k\ c_1 \bm{x}_1 + \cdots + \lambda_n^k c_n \bm{x}_n
$$

となります.

ここで,$${\bm{y}^k = \frac{\bm{A}^k\bm{x}}{\|\bm{A}^k\bm{x}\|} }$$とします.

すると, $${c_1\neq 0}$$ならば,

$$
\bm{y}^k =\lambda_1^k c_1' \left\{  \bm{v}_1 +\left( \frac{\lambda_2}{\lambda_1} \right)^k \frac{c_2'}{c_1'}\bm{v_2}  \cdots + \left( \frac{\lambda_n}{\lambda_1} \right)^k \frac{c_2'}{c_1'}\bm{v_n} \right\}
$$

が成り立ちます.

$${\left\| \frac{\lambda_i}{\lambda_1} \right\| < 1\ (i>1)}$$なので,

$$
\lim_{k\rightarrow \infty} \left( \frac{\lambda_n}{\lambda_1} \right)^k = 0
$$

が成り立ちます. よって, $k$を大きくしていくと, $${y^k = \bm{v}_1}$$に成ります. $${(\bm{y}^k ,\bm{Ay}^k)}$$を求めれば, $${\lambda_1}$$も求まります 

2. n個の固有値

前章では, 一番大きな固有値を求めました.
今, n個の固有値を求めたいとします.
ここでは, 方法の一つとして, 線型空間を限定します. 
すなはち, $${\bm{v}_k}$$を求める時には, 常に, 考えるベクトルが, $${\bm{v}_1,…,\bm{v}_{k-1}}$$と直交するようにしておきます.

今回は$${\bm{x}}$$が$${\bm{v}_1,…,\bm{v}_{k-1}}$$と直交するためには

$$
\bm{x} = \bm{x} - \sum_{i=1}^{k-1} (\bm{x},\bm{v}_i)\bm{v}_i
$$


とすれば良いです. (本当は一回この操作をすれば, 良いのですが, 数値誤差などもあるので, 毎回, この作業をします.)

3. 実際のコード

# k個の固有値を求める関数

import numpy as np

def power_method(A,k):

    if A.shape[0] != A.shape[1]:
        print("Matrix is not a hermitian matrix")
        return 0

    n = A.shape[0]

    if A.shape[0] < k:
        print("Matrix is smaller than the number")
        return 0

    eps = 1e-8

    lam_multiple = []
    v_multiple = []

    for i in range(k):
        #ベクトルの初期化
        v = np.zeros(n)
        v[0] = 1
    
        #直交化
        for s in range(i):
            v -= np.dot(v,v_multiple[s])*v_multiple[s]

        T = 100
        lam = 0 
        for j in range(T):
            #更新
            v_next = np.dot(A,v)
            lam_next = np.dot(v_next,v_next)/np.dot(v_next,v)
       #収束
            if np.abs(lam_next - lam) < eps: 
                lam_multiple.append(lam)
                v_multiple.append(v)
                break
            else:
                #直交化
                for s in range(i): 
                    v_next -= np.dot(v_next,v_multiple[s])*v_multiple[s]
                #正規化
                v = v_next/np.sqrt(np.dot(v_next,v_next))
                lam = lam_next
            if j == T -1:
                print("error")
                return 0

    return lam_multiple,v_multiple 

これを用いて実際に確認してみましょう.

np.random.seed(1)

A = np.random.rand(10,10)
A = np.dot(A.T,A)

lam_multiple,v_multiple = power_method(A,10)
for i in range(10):
    print(lam_multiple[i],np.sort(LA.eig(A)[0])[9-i])

とすると,

25.140071139463146 25.140071147995904
1.901219696015664 1.9012197232353796
1.625216786296971 1.6252167697648858
1.256183924521971 1.2561839575241638
1.1399416975318308 1.1399416539026153
0.6726468744662356 0.6726468715225401
0.30344342638193034 0.30344343181034394
0.1990857245214645 0.19908571381457515
0.024128617199257688 0.024128629377396238
0.013025928571743344 0.013025916022419945

が得られました!

4 . より良いアルゴリズム

今回のアルゴリズムは$${c_1 = 0}$$の場合を無視しています.
おそらくそんなことは滅多に起きないのですが,
乱数などを何度か振って対応すれば良いのかなぁと思っています.

他にも ハウスホルダ変換やヤコビ法など色々あるようです. 暇な時に実装したいと思います.

この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?