Machine Learning: 次元削減 — PCAとt-SNEによる高次元データ可視化ガイド
最終更新:2026-08-26
高次元空間では、すべての点が互いに遠く離れています。次元削減は単なる圧縮ではなく、データの真の構造を明らかにするための手段です。
1. 学習内容
- 次元の呪い:高次元空間における距離の崩壊とデータのスパース性
- PCA(主成分分析):共分散行列、固有値分解、寄与率
- t-SNE多様体可視化:perplexityパラメータ、KLダイバージェンス、高次元から2D/3Dへのマッピング
- UMAPの概要:t-SNEより高速で、グローバルな構造を保持する代替手法
- ECサイトのユーザー行動ケーススタディ:50次元の行動特徴量 → 自然なユーザーセグメントを明らかにする2D可視化
2. 現役データサイエンティストの体験談
(1) 課題:50次元のユーザー行動データは見えない・理解できない
Bobは50個のユーザー行動特徴量(セッション時間、クリック数、カテゴリごとのエンゲージメント率など)を収集しましたが、ユーザーがどのように分布しているかを直感的に把握する手段がありませんでした。テーブルは読み切れず、散布図は2次元が限界で、クラスタリング結果の検証も困難でした。高次元データは人間の直感にとってブラックボックスです。
(2) 解決策:PCA + t-SNE
PCAは分散を最大限に保持しながらデータを2次元に圧縮し、t-SNEは局所的な構造を2D平面にマッピングします。これにより、高次元データを実際に「見える」ものにできます。
PYTHON
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
X_scaled = StandardScaler().fit_transform(X_50d)
# PCA: fast, preserves global variance
X_pca = PCA(n_components=2).fit_transform(X_scaled)
# t-SNE: slow, preserves local structure
X_tsne = TSNE(n_components=2, perplexity=30).fit_transform(X_scaled)
(3) 成果:2Dプロットで3つの自然なユーザーグループが一目で判明
次元削減後、Bobは3つの明確なユーザーグループ(高価値ユーザー、アクティブユーザー、休眠ユーザー)を一目で確認できました。これはクラスタリング結果ともよく一致しており、ビジネスチームは数秒でデータ分布を理解できました。
3. 次元の呪い
(1) 高次元空間における直感に反する現象
- 距離の崩壊:高次元空間では、最近傍点までの距離と最遠点までの距離が同じ値に収束します
- データのスパース性:体積は次元とともに指数関数的に増大するため、固定数のサンプルはますますスパースになります
- 過学習のリスク:特徴量の数がサンプル数を大きく上回ると、モデルは「ノイズを記憶」しがちです
▶ サンプル:次元の呪い実験
PYTHON
import numpy as np
rng = np.random.default_rng(42)
for dim in [2, 10, 50, 100, 500]:
n = 1000
X = rng.uniform(0, 1, (n, dim))
# Compute pairwise distances
from sklearn.metrics import pairwise_distances
dists = pairwise_distances(X)
np.fill_diagonal(dists, np.inf)
min_dist = dists.min()
max_dist = dists.max()
ratio = (max_dist - min_dist) / max_dist
print(f"Dim={dim:4d}: min={min_dist:.3f}, max={max_dist:.3f}, "
f"relative_range={ratio:.4f}")
出力:
TEXT
📖 参照専用
# Runs successfully
| 次元数 | 最小距離 | 最大距離 | 相対範囲 | 意味 |
|---|---|---|---|---|
| 2 | 0.02 | 1.41 | 0.99 | 距離は意味を持つ |
| 10 | 0.78 | 1.87 | 0.58 | 距離が崩壊し始める |
| 50 | 2.42 | 3.28 | 0.26 | 距離はほぼ無意味 |
| 100 | 3.54 | 4.05 | 0.13 | すべての距離が収束 |
| 500 | 8.11 | 8.47 | 0.04 | 完全に崩壊 |
4. PCA(主成分分析)
(1) PCAの仕組み
PCAは固有値分解を用いて分散が最大となる方向(主成分)を見つけ、データを低次元空間に射影します。
graph TB
INPUT[Standardized Data] --> COV[Compute Covariance Matrix]
COV --> EIG[Eigenvalue Decomposition]
EIG --> SELECT[Select Top K Eigenvectors]
SELECT --> PROJECT[Project Data to K Dimensions]
PROJECT --> OUTPUT[Low-Dimensional Representation]
▶ サンプル:PCAによる次元削減と寄与率
PYTHON
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
import numpy as np
X, y = load_iris(return_X_y=True)
# Standardize first (PCA is sensitive to scale)
X_scaled = StandardScaler().fit_transform(X)
# PCA with all components to see explained variance
pca_full = PCA()
pca_full.fit(X_scaled)
print("Explained variance ratio:")
for i, (evr, cum) in enumerate(zip(pca_full.explained_variance_ratio_,
pca_full.explained_variance_ratio_.cumsum())):
print(f" PC{i+1}: {evr:.3f} (cumulative: {cum:.3f})")
# Reduce to 2D
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
print(f"\n2D PCA preserves {pca.explained_variance_ratio_.sum():.1%} variance")
# Visualize
fig, ax = plt.subplots(figsize=(8, 6))
for target in np.unique(y):
mask = y == target
ax.scatter(X_pca[mask, 0], X_pca[mask, 1], label=load_iris().target_names[target], s=40)
ax.set_xlabel(f"PC1 ({pca.explained_variance_ratio_[0]:.1%})")
ax.set_ylabel(f"PC2 ({pca.explained_variance_ratio_[1]:.1%})")
ax.set_title("PCA of Iris Dataset")
ax.legend()
plt.tight_layout()
plt.savefig("pca_iris.png", dpi=150)
出力:
TEXT
📖 参照専用
Explained variance ratio:
(2) 保持する成分数の選び方
▶ サンプル:累積寄与率プロット
PYTHON
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
n, p = 500, 50
X = rng.standard_normal((n, p))
# Make first 5 dimensions have more signal
X[:, :5] *= 3
X_scaled = StandardScaler().fit_transform(X)
pca = PCA().fit(X_scaled)
fig, ax = plt.subplots(figsize=(10, 5))
cumvar = pca.explained_variance_ratio_.cumsum()
ax.plot(range(1, len(cumvar)+1), cumvar, "b-o", markersize=3)
ax.axhline(0.95, color="red", linestyle="--", label="95% variance")
ax.axhline(0.90, color="orange", linestyle="--", label="90% variance")
ax.set_xlabel("Number of Components")
ax.set_ylabel("Cumulative Explained Variance")
ax.set_title("PCA Explained Variance")
ax.legend()
n_95 = (cumvar < 0.95).sum() + 1
print(f"Components for 95% variance: {n_95}")
plt.tight_layout()
plt.savefig("pca_variance.png", dpi=150)
出力:
TEXT
📖 参照専用
# Runs successfully
| しきい値 | 意味 | 典型的な結果 |
|---|---|---|
| 90% | 主要なシグナルを保持 | 5〜15成分(50次元の場合) |
| 95% | より多くの詳細を保持 | 10〜25成分(50次元の場合) |
| 99% | ほぼ無損失 | 20〜40成分(50次元の場合) |
5. t-SNE多様体可視化
(1) t-SNEの仕組み
t-SNEは、高次元空間と低次元空間におけるペアワイズ類似度のKLダイバージェンスを最小化することで、局所的な近傍関係を保持します。
▶ サンプル:t-SNE可視化
PYTHON
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
import numpy as np
X, y = load_digits(return_X_y=True)
X_scaled = StandardScaler().fit_transform(X)
# t-SNE with different perplexity
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
for ax, perplexity in zip(axes, [5, 30, 50]):
tsne = TSNE(n_components=2, perplexity=perplexity, random_state=42, init="pca")
X_tsne = tsne.fit_transform(X_scaled)
scatter = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap="tab10", s=5, alpha=0.7)
ax.set_title(f"t-SNE (perplexity={perplexity})")
ax.set_xlabel("t-SNE 1")
ax.set_ylabel("t-SNE 2")
plt.tight_layout()
plt.savefig("tsne_perplexity.png", dpi=150)
出力:
TEXT
📖 参照専用
# Runs successfully
(2) t-SNEの注意点
| 考慮事項 | 備考 |
|---|---|
| perplexity | 通常は5〜50。小さい値 → 近傍に注目、大きい値 → グローバルな構造に注目 |
| 乱数シード | シードが異なると結果も変わるため、random_stateを固定する |
| 下流タスクへの利用不可 | t-SNEは距離を保持しないため、モデルの特徴量入力には不向き |
| 計算が遅い | 大規模データセット(1万件以上)では、まずPCAで削減してからt-SNEを実行する |
| クラスタサイズに意味がない | t-SNEはクラスタの相対的なサイズを誇張または縮小することがある |
▶ サンプル:PCA前処理によるt-SNEの高速化
PYTHON
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import numpy as np
rng = np.random.default_rng(42)
X = rng.standard_normal((5000, 50))
# Direct t-SNE (slow)
# tsne = TSNE(n_components=2, random_state=42)
# X_tsne = tsne.fit_transform(X) # Very slow on 5000x50!
# Best practice: PCA first, then t-SNE
X_scaled = StandardScaler().fit_transform(X)
X_pca_30 = PCA(n_components=30).fit_transform(X_scaled) # Fast PCA to 30D
X_tsne = TSNE(n_components=2, perplexity=30, random_state=42,
init="pca", learning_rate="auto").fit_transform(X_pca_30)
print(f"PCA→t-SNE shape: {X_tsne.shape}")
出力:
TEXT
📖 参照専用
# Runs successfully
6. 実践:ECサイトユーザー行動の次元削減
▶ サンプル:50次元のユーザー特徴量 → 2D可視化
PYTHON
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
n = 3000
# Generate 50-dim user behavior data with 3 natural groups
group1 = rng.normal(loc=2, scale=1, size=(800, 50)) # High-value users
group2 = rng.normal(loc=0, scale=1.5, size=(1200, 50)) # Regular users
group3 = rng.normal(loc=-2, scale=0.8, size=(1000, 50)) # Dormant users
X = np.vstack([group1, group2, group3])
# Add noise dimensions
X[:, 20:] += rng.normal(0, 3, (n, 30))
# Standardize
X_scaled = StandardScaler().fit_transform(X)
# PCA to 2D
X_pca = PCA(n_components=2).fit_transform(X_scaled)
# t-SNE to 2D (with PCA preprocessing)
X_pca30 = PCA(n_components=30).fit_transform(X_scaled)
X_tsne = TSNE(n_components=2, perplexity=30, random_state=42,
init="pca", learning_rate="auto").fit_transform(X_pca30)
# K-Means labels for reference
labels = KMeans(n_clusters=3, random_state=42, n_init=10).fit_predict(X_scaled)
# Visualize
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
for ax, X_2d, title in [(axes[0], X_pca, "PCA"), (axes[1], X_tsne, "t-SNE")]:
scatter = ax.scatter(X_2d[:, 0], X_2d[:, 1], c=labels, cmap="Set1", s=8, alpha=0.6)
ax.set_title(title)
ax.set_xlabel(f"{title} 1")
ax.set_ylabel(f"{title} 2")
plt.tight_layout()
plt.savefig("user_behavior_dim_reduction.png", dpi=150)
出力:
TEXT
📖 参照専用
# Runs successfully
(1) PCA vs t-SNE vs UMAP
| 観点 | PCA | t-SNE | UMAP |
|---|---|---|---|
| 保持するもの | グローバルな分散 | 局所的な近傍 | グローバル + 局所的 |
| 速度 | 高速(1秒未満) | 低速(数分) | 中程度(数秒) |
| 再現性 | 決定的 | シード依存 | シード依存 |
| 解釈性 | 高い(寄与率) | 低い | 中程度 |
| 下流タスクでの利用 | ✅ 特徴量として使用可能 | ❌ 可視化専用 | ⚠️ 検証が必要 |
| 新規データのマッピング | ✅ transform | ❌ 再実行が必要 | ✅ transform |
❓ よくある質問
Q PCAで削減したデータをモデルの学習に使えますか?
A はい、使えます。PCAの主成分はモデルの入力として使用でき、特に高次元・少サンプルのシナリオで有用です。ただし、PCAは訓練データに対してfitし、テストデータにはtransformのみを適用してください。
Q なぜt-SNEはモデルの特徴量として使えないのですか?
A t-SNEはグローバルな距離関係を保持せず、新規データを変換することもできません(毎回ゼロから再計算する必要があります)。可視化専用であり、モデルの入力には適していません。
Q perplexityパラメータはどう選べばよいですか?
A 一般的な範囲は5〜50です。小規模データセット(約1千件)では5〜20、大規模データ(1万件以上)では30〜50を使用します。いくつかの値を試して、クラスタが最も明確に現れるものを選んでください。
Q PCAには標準化が必要ですか?
A 必須です。PCAは分散を最大化するため、特徴量のスケールが異なる場合(例:年齢と収入)、分散の大きい特徴量が主成分を支配してしまいます。PCAの前には必ずStandardScalerを実行してください。
Q UMAPはt-SNEより何が優れているのですか?
A 3つの利点があります。1)10〜100倍高速であること、2)グローバルな構造をよりよく保持すること、3)新規データの変換に対応していることです。ただし、
pip install umap-learnで別途インストールが必要です。Q 次元削減でどの程度の情報が失われますか?
A 保持する成分数によります。分散の95%を保持すれば、失われる情報は約5%にとどまり、失われる部分はノイズである可能性もあります。
cumsum(explained_variance_ratio_)で確認して判断してください。📖 まとめ
- 次元の呪い:距離が崩壊し、データはスパースになり、過学習のリスクが高まるため、次元削減は不可欠です
- PCA:分散が最大の方向を保持します。高速な次元削減やモデル入力に適していますが、事前に標準化が必要です
- t-SNE:局所的な近傍関係を保持します。可視化に最適ですが、モデルの特徴量としては使用できず、計算も遅いです
- UMAP:t-SNEの代替手法で、より高速であり、グローバルな構造を保持し、新規データのマッピングにも対応しています
- 典型的なパイプライン:StandardScaler → PCA(高速化 + ノイズ除去) → t-SNE/UMAP(可視化)
- PCAの成分数の選択:累積寄与率を確認し、90〜95%が一般的な閾値です
📝 練習問題
- 基礎(難易度 ⭐):IrisデータセットにPCAを適用して2次元に削減し、散布図を描いて寄与率を注記してください。ヒント:
PCA(n_components=2)+ scatter。 - 応用(難易度 ⭐⭐):
load_digitsデータ(64次元)に対し、まずPCAで30次元に削減してからt-SNEで2次元に削減してください。直接t-SNEを実行した場合とPCA + t-SNEの場合の実行時間と可視化品質を比較してください。ヒント:time.time()で計測。 - 発展(難易度 ⭐⭐⭐):3つのユーザーグループを含む50次元データを生成し、PCA、t-SNE、UMAPそれぞれで2次元に削減して、K-Meansクラスタリング(ARI/NMI)で評価し、どの手法が元のグループ構造を最もよく保持しているかを判定してください。ヒント:
sklearn.metrics.adjusted_rand_score。