最近使う機会があったので、特異値分解について紹介します。
まず、$A$をランク$r$の$m$行$n$列の実行列とします。
(複素行列も考慮したい場合は、この後出てくる転置行列を随伴行列に読み替え、直行行列をユニタリ行列で読み替えてください。)
このとき、$U$は$m\times m$の直行行列、$V$は$n\times n$の直行行列、Sは非対角成分が0の$m \times n$行列を用いて、
$$A = USV^{\top}$$
と分解することを特異値分解と言います。
$S$の対角成分のうち$0$でないもの(特異値)の個数が$A$のランク$r$になります。
さて、この特異値分解をPythonで実行する方法は複数あり、numpy, SciPy, scikit-learnにそれぞれ実装があります。
参考:
numpy.linalg.svd
scipy.linalg.svd
sklearn.decomposition.TruncatedSVD
これらの使い分けですが、機械学習のパイプラインに組み込んだり、可視化が目的の時など次元削減のために利用するのであればscikit-learnがおすすめです。
それ以外の場合は、SciPyが良いでしょう。numpyより設定できるオプションが多く、また、いくつか補助的な便利関数が用意されています。
とりあえず乱数で生成した行列を、SciPyで特異値分解してみます。
import numpy as np
import scipy
m = 5
n = 3
np.random.seed(110) # 乱数固定
A = np.random.randint(-10, 10, size=(m, n))
print(A)
"""
[[-10 -7 5]
[ 5 -10 6]
[ 6 -4 -8]
[ -2 -1 -5]
[-10 7 -9]]
"""
U, S_diags, V_t = scipy.linalg.svd(A)
# U
print(U)
"""
U:
[[ 0.10801327 0.81765612 -0.427367 -0.18878079 -0.31857632]
[ 0.62638958 0.06527027 -0.28451679 0.07142207 0.71925307]
[ 0.04632544 -0.55033827 -0.69658511 -0.39810108 -0.226421 ]
[-0.16944935 -0.04718317 -0.43857367 0.87461141 -0.1084836 ]
[-0.75173806 0.14859275 -0.24254891 -0.18929111 0.56404698]]
"""
# Sの対角成分:
print(S_diags)
# [19.70238709 15.08068891 9.76671719]
# Vの転置行列:"
print(V_t)
"""
[[ 0.51699558 -0.6241887 0.58575083]
[-0.83177902 -0.20473932 0.51597042]
[ 0.20213668 0.75396968 0.62503639]]
"""
変数名で表現してみましたが、 $U$以外の二つは少し癖のある形で返ってきます。
$S$は対角成分しか戻ってきませんし、$V$は転置された状態で戻されます。
再度掛け算して結果を角にする時などは注意が必要です。
さて、$S$は対角成分だけ返ってきましたが、これを$m\times n$行列に変換するには、専用の関数が用意されています。
scipy.linalg.diagsvd
S = scipy.linalg.diagsvd(S_diags, m, n)
print(S)
"""
[[19.70238709 0. 0. ]
[ 0. 15.08068891 0. ]
[ 0. 0. 9.76671719]
[ 0. 0. 0. ]
[ 0. 0. 0. ]]
"""
あとは、 $USV^{\top}$計算して、元の$A$になることをみておきましょう。
print(U@S@V_t)
"""
[[-10. -7. 5.]
[ 5. -10. 6.]
[ 6. -4. -8.]
[ -2. -1. -5.]
[-10. 7. -9.]]
"""
元の値が復元できましたね。