特異値分解(Singular Value Decomposition, SVD)は、任意の行列を3つの行列の積に分解する手法で、線形代数における最も重要な分解の1つです。機械学習では、主成分分析(PCA)、推薦システム、自然言語処理(潜在意味解析)など、幅広い場面で利用されています。
固有値分解が正方行列にしか適用できないのに対し、SVDは任意のサイズの行列に適用可能であるという利点があります。
本記事の内容
- 特異値分解の定義と幾何学的な意味
- 特異値と固有値の関係
- 低ランク近似(truncated SVD)
- PCAとの関係
- Pythonでの実装と画像圧縮への応用
前提知識
この記事を読む前に、以下の概念を押さえておくと理解が深まります。
- 部分空間とは?機械学習に必要な線形代数の基礎を解説
- 固有値・固有ベクトルの基礎
特異値分解の定義
任意の $m \times n$ 行列 $\bm{A}$ は、以下のように分解できます。
$$ \bm{A} = \bm{U}\bm{\Sigma}\bm{V}^T $$
ここで、
- $\bm{U} \in \mathbb{R}^{m \times m}$: 左特異ベクトル行列(直交行列、$\bm{U}^T\bm{U} = \bm{I}$)
- $\bm{\Sigma} \in \mathbb{R}^{m \times n}$: 特異値を対角に並べた行列($\sigma_1 \geq \sigma_2 \geq \dots \geq 0$)
- $\bm{V} \in \mathbb{R}^{n \times n}$: 右特異ベクトル行列(直交行列、$\bm{V}^T\bm{V} = \bm{I}$)
特異値 $\sigma_i$ は非負の実数で、降順に並びます。
特異値と固有値の関係
SVDの各要素は、$\bm{A}^T\bm{A}$ と $\bm{A}\bm{A}^T$ の固有値分解と関係しています。
$$ \bm{A}^T\bm{A} = \bm{V}\bm{\Sigma}^T\bm{\Sigma}\bm{V}^T $$
$$ \bm{A}\bm{A}^T = \bm{U}\bm{\Sigma}\bm{\Sigma}^T\bm{U}^T $$
すなわち、
- $\bm{V}$ の列は $\bm{A}^T\bm{A}$ の固有ベクトル
- $\bm{U}$ の列は $\bm{A}\bm{A}^T$ の固有ベクトル
- 特異値の二乗 $\sigma_i^2$ はこれらの固有値
この関係から、特異値分解は固有値分解の一般化であると見なせます。
低ランク近似
SVDの最も重要な応用の1つが 低ランク近似(truncated SVD) です。上位 $k$ 個の特異値のみを保持した近似を行います。
$$ \bm{A}_k = \sum_{i=1}^{k} \sigma_i \bm{u}_i \bm{v}_i^T = \bm{U}_k \bm{\Sigma}_k \bm{V}_k^T $$
Eckart-Youngの定理 により、この $\bm{A}_k$ はフロベニウスノルムの意味で、ランク $k$ の行列の中で $\bm{A}$ に最も近い行列です。
$$ \bm{A}_k = \arg\min_{\text{rank}(\bm{B}) = k} \|\bm{A} – \bm{B}\|_F $$
近似の相対誤差は以下で評価できます。
$$ \frac{\|\bm{A} – \bm{A}_k\|_F^2}{\|\bm{A}\|_F^2} = \frac{\sum_{i=k+1}^{r} \sigma_i^2}{\sum_{i=1}^{r} \sigma_i^2} $$
ここで $r$ は $\bm{A}$ のランクです。
PCAとの関係
データ行列 $\bm{X} \in \mathbb{R}^{n \times d}$(各行がデータ点、中心化済み)に対してSVDを行うと、
$$ \bm{X} = \bm{U}\bm{\Sigma}\bm{V}^T $$
データの共分散行列は、
$$ \frac{1}{n-1}\bm{X}^T\bm{X} = \bm{V}\frac{\bm{\Sigma}^2}{n-1}\bm{V}^T $$
$\bm{V}$ の列が主成分方向、$\sigma_i^2 / (n-1)$ が各主成分の分散(固有値)に対応します。すなわち、PCAはデータ行列のSVDとして実行できます。
Pythonでの実装
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# --- SVDの基本 ---
# サンプル行列
A = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12]
], dtype=float)
U, S, Vt = np.linalg.svd(A, full_matrices=False)
print("=== SVD分解結果 ===")
print(f"A ({A.shape})")
print(f"U ({U.shape}):\n{U.round(3)}")
print(f"S: {S.round(3)}")
print(f"Vt ({Vt.shape}):\n{Vt.round(3)}")
print(f"再構成誤差: {np.linalg.norm(A - U @ np.diag(S) @ Vt):.10f}")
# --- 低ランク近似による画像圧縮 ---
# グレースケール画像を合成
x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X_grid, Y_grid = np.meshgrid(x, y)
image = np.sin(X_grid) * np.cos(Y_grid) + \
0.5 * np.exp(-(X_grid**2 + Y_grid**2) / 2) + \
np.random.randn(200, 200) * 0.05
# SVD
U, S, Vt = np.linalg.svd(image, full_matrices=False)
# 異なるランクで近似
ranks = [1, 5, 20, 50]
fig, axes = plt.subplots(1, len(ranks) + 1, figsize=(18, 3.5))
# 元の画像
axes[0].imshow(image, cmap='viridis')
axes[0].set_title(f'Original (rank={np.linalg.matrix_rank(image)})')
axes[0].axis('off')
for i, k in enumerate(ranks):
# ランクkの近似
approx = U[:, :k] @ np.diag(S[:k]) @ Vt[:k, :]
error = np.linalg.norm(image - approx) / np.linalg.norm(image)
compression = k * (200 + 200 + 1) / (200 * 200)
axes[i + 1].imshow(approx, cmap='viridis')
axes[i + 1].set_title(f'Rank {k}\n(error={error:.3f}, comp={compression:.2%})')
axes[i + 1].axis('off')
plt.suptitle('Low-Rank Approximation via SVD', fontsize=14)
plt.tight_layout()
plt.show()
# --- 特異値の分布 ---
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 特異値の大きさ
ax1 = axes[0]
ax1.bar(range(len(S)), S, color='steelblue', alpha=0.7)
ax1.set_xlabel('Index')
ax1.set_ylabel('Singular Value')
ax1.set_title('Singular Values')
ax1.grid(True, alpha=0.3)
# 累積エネルギー比率
ax2 = axes[1]
energy = np.cumsum(S**2) / np.sum(S**2)
ax2.plot(range(len(S)), energy, 'b-o', markersize=2)
ax2.axhline(y=0.95, color='r', linestyle='--', label='95% energy')
ax2.axhline(y=0.99, color='g', linestyle='--', label='99% energy')
k_95 = np.argmax(energy >= 0.95) + 1
ax2.axvline(x=k_95, color='r', linestyle=':', alpha=0.5)
ax2.set_xlabel('Number of Singular Values')
ax2.set_ylabel('Cumulative Energy Ratio')
ax2.set_title('Cumulative Energy')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"95%のエネルギーを保持するために必要な特異値数: {k_95}")
低ランク近似では、少数の特異値でも元の行列をかなりの精度で再現できることが確認できます。特異値が急速に減衰する場合、少ないランクで十分な近似が得られます。
まとめ
本記事では、特異値分解(SVD)の理論と応用について解説しました。
- SVDは任意の行列を $\bm{U}\bm{\Sigma}\bm{V}^T$ に分解する手法で、固有値分解の一般化
- 低ランク近似はフロベニウスノルムの意味で最適であり、データ圧縮や次元削減に有効
- PCAはデータ行列のSVDとして実行でき、右特異ベクトルが主成分方向に対応
- 特異値の減衰の速さがデータの低ランク性を示す指標となる
次のステップとして、以下の記事も参考にしてください。