Digital Reactor
機械学習

次元削減手法の比較検証:定量的・定性的評価によるPCA、t-SNE、UMAPの特徴分析

次元削減手法の比較検証:定量的・定性的評価によるPCA、t-SNE、UMAPの特徴分析

はじめに

数十次元、数百次元のデータをそのまま眺めても、その構造はまず見えてきません。二次元や三次元に落とすと、クラスターの分かれ方や外れ値の位置がようやく目に入ります。この次元の落とし方の定番がPCA(主成分分析)、t-SNE、UMAPの3つですが、同じデータでもどれを選ぶかで見える絵は大きく変わります。PCAでは重なっていたクラスターがt-SNEでははっきり分かれる、といったことが普通に起きます。そこで3手法を同じデータに適用し、図の見え方と定量指標の両面から、どの場面でどれを選ぶかを整理します。

対象読者:

  • 機械学習エンジニアやデータサイエンティスト
  • 次元削減の手法選択に悩んでいる方
  • 高次元データの可視化や分析に関心のある方

記事のポイント:

  • PCA、t-SNE、UMAPが何を保とうとする手法なのかが分かる
  • 各手法の長所と短所を踏まえ、目的とデータサイズで使い分けられる
  • 定量的な評価指標(トラストワーシネス、再構成誤差)と図の見え方の両面で比較できる

各手法は何を保とうとしているか

PCA(主成分分析)

PCAは最も基本的で、広く使われている次元削減手法です。データの分散が最大になる方向(主成分)を順に見つけ、そこへデータを射影します。線形変換なので、変換後の空間から元の空間へ戻す逆変換ができ、計算も速く、大規模なデータセットにもそのまま適用できます。主成分は次の最適化問題を解いて求めます。

maxwwTXTXwsubject tow=1\max_{w} w^T X^T X w \quad \text{subject to} \quad \|w\| = 1

ここで、XX はデータ行列、ww は主成分ベクトルを表します。

t-SNE

t-SNEは非線形の次元削減手法で、データの局所的な構造の保持に優れています。高次元空間と低次元空間のそれぞれでデータ点間の類似度を確率分布として表し、2つの分布が近づくように低次元側の配置を最適化します。クラスターのような局所構造を見るのに向く一方、計算コストが高く、大規模なデータセットに使うには工夫が要ります。最適化では次のKLダイバージェンスを最小化します。

KL(PQ)=ijpijlogpijqijKL(P||Q) = \sum_{i}\sum_{j} p_{ij} \log\frac{p_{ij}}{q_{ij}}

ここで、PP は高次元空間での確率分布、QQ は低次元空間での確率分布、pijp_{ij}qijq_{ij} はそれぞれデータ点 iijj 間の類似度を表します。

UMAP

UMAPは比較的新しい非線形の次元削減手法で、t-SNEの弱点を補う設計になっています。リーマン幾何学とトポロジーの概念を使い、高次元と低次元の間でデータ点どうしの関係が保たれるように配置を最適化します。t-SNEより高速で、全体の構造もある程度残ります。学習済みの低次元空間へ新しいデータ点を射影する機能(transform)を持つ点も、運用に組み込むうえでは大きな違いです。

評価指標

図の見た目だけで優劣を語ると主観に寄るため、定量的な指標を2つ併用します。

トラストワーシネス(Trustworthiness)

トラストワーシネス(trustworthiness)は、局所的な構造の保持度を測る指標です。元の高次元空間での近傍関係が低次元空間でもどの程度保たれているかを0から1の値で表し、1に近いほど近傍関係が崩れていません。scikit-learnで計算できます。

from sklearn.manifold import trustworthiness
trust_score = trustworthiness(X_original, X_reduced)

再構成誤差(Reconstruction Error)

再構成誤差(reconstruction error)はPCAに特有の指標で、情報の保持度を表します。低次元空間に射影したデータを元の高次元空間に戻したとき、元のデータとどの程度の差(二乗平均平方根誤差、RMSE)が生じるかを測り、値が小さいほど元の情報が保たれています。次のPythonコードで計算できます。

def compute_reconstruction_error(X, X_transformed, pca):
    X_reconstructed = pca.inverse_transform(X_transformed)
    return np.mean(np.square(X - X_reconstructed))

実験:3つのデータセットでの比較

実験設定

比較には3つのデータセットを使いました。非線形多様体の代表例であるSwiss Roll(3次元の人工データ)、滑らかな曲面のS-curve(同じく3次元の人工データ)、そして実データとして4次元の特徴量を持つアヤメ(Iris)データセットです。人工データの2つは、線形手法では平面に開けない構造を意図的に含んでいます。

import numpy as np
from sklearn.datasets import make_swiss_roll, make_s_curve, load_iris
from sklearn.preprocessing import StandardScaler

# データセットの生成と前処理
X_swiss, color_swiss = make_swiss_roll(n_samples=1000, random_state=42)
X_s_curve, color_s_curve = make_s_curve(n_samples=1000, random_state=42)
iris = load_iris()
X_iris = StandardScaler().fit_transform(iris.data)

実験結果

定量的評価の比較

評価指標データセットPCAt-SNEUMAP
トラストワーシネスSwiss Roll0.9821.0000.999
S-curve0.9781.0000.999
Iris0.9740.9910.982
計算時間 (秒)Swiss Roll0.012.450.32
S-curve0.012.380.30
Iris0.011.120.15
再構成誤差Swiss Roll12.390--
S-curve0.113--
Iris0.042--

トラストワーシネスはどの手法も高い水準ですが、非線形構造を持つSwiss RollとS-curveではt-SNEとUMAPがほぼ1.0に達し、PCAとの差が出ます。計算時間は桁が違い、PCAが0.01秒で終わるのに対し、UMAPは0.3秒前後、t-SNEは2秒台です。1,000点でこの差なので、数万点の規模になるとt-SNEの所要時間は無視できません。

定性的特徴の比較

特徴PCAt-SNEUMAP
非線形性の扱い線形のみ優れている優れている
局所構造の保持中程度非常に優れている優れている
グローバル構造の保持良好弱い中程度
スケーラビリティ優れている低い良好
新規データの射影可能不可能可能
解釈のしやすさ容易難しいやや難しい

目的とデータサイズで選ぶ

実験結果を踏まえると、手法の選択は「何を知りたいか」と「データが何点あるか」の2つでほぼ決まります。

用途別の推奨手法

用途推奨手法理由
次元の意味理解PCA主成分の解釈が容易で、データの変動の方向性が明確
クラスタリング可視化t-SNE/UMAP局所構造の保持に優れ、クラスター間の分離が明確
大規模データ処理UMAP計算効率が良く、比較的高品質な結果が得られる
リアルタイム処理PCA計算が非常に高速で、新規データの射影も容易
教師なし異常検知UMAP局所構造を保持しつつ、計算効率も比較的良好で、外れ値の検出に適している

データサイズによる選択指針

データサイズ推奨手法注意点
小規模(<1000)全て使用可まずPCAで試し、必要に応じてt-SNEやUMAPを使用
中規模(1000-10000)PCA/UMAPt-SNEは計算時間に注意
大規模(>10000)UMAPPCAも可能だが、非線形性の表現が限定的になる可能性がある。

まとめ

3手法の性格は実験にそのまま表れました。PCAは0.01秒で終わり逆変換もできますが、Swiss Rollのような非線形構造は平面に開けません。t-SNEはトラストワーシネスが最も高い一方、計算に時間がかかり、新しいデータを射影できません。UMAPはその中間で、速度・局所構造・新規データ対応のバランスが取れています。

実務での進め方としては、まず高速なPCAで全体像と分散の構造をつかみ、クラスターを細かく見たい段階でUMAPに移り、t-SNEは小規模データの仕上げの可視化に絞るのが無理のない順序です。次に高次元データを可視化する機会があれば、1つの手法で済ませず、同じデータに複数の手法をかけて図を並べてみてください。図がどう食い違うかを見れば、そのデータのどの構造が線形でどの構造が非線形なのかを読み取れます。

コード

実験に使ったスクリプトの全体です。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_swiss_roll, make_s_curve, load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE, trustworthiness
from sklearn.metrics.pairwise import euclidean_distances
import umap
import japanize_matplotlib
from time import time

def compute_reconstruction_error(X, X_transformed, pca):
    """PCAの再構成誤差を計算"""
    if not isinstance(pca, PCA):
        return None
    X_reconstructed = pca.inverse_transform(X_transformed)
    return np.mean(np.square(X - X_reconstructed))

def compute_metrics(X, X_transformed, method_name, pca=None):
    """各種評価指標を計算"""
    metrics = {}

    # トラストワーシネス(局所構造の保持度)
    metrics['trustworthiness'] = trustworthiness(X, X_transformed)

    # 再構成誤差(PCAのみ)
    if isinstance(pca, PCA):
        metrics['reconstruction_error'] = compute_reconstruction_error(X, X_transformed, pca)

    return metrics

def generate_datasets():
    # スイスロールデータセット
    X_swiss, color_swiss = make_swiss_roll(n_samples=1000, random_state=42)

    # S字カーブデータセット
    X_s_curve, color_s_curve = make_s_curve(n_samples=1000, random_state=42)

    # IRISデータセット
    iris = load_iris()
    X_iris = StandardScaler().fit_transform(iris.data)
    color_iris = iris.target

    return (X_swiss, color_swiss, "Swiss_Roll"), (X_s_curve, color_s_curve, "S_Curve"), (X_iris, color_iris, "Iris")

def apply_dimension_reduction(X, methods):
    results = {}
    times = {}
    metrics = {}

    for name, method in methods.items():
        start_time = time()
        results[name] = method.fit_transform(X)
        times[name] = time() - start_time

        # 評価指標の計算
        pca = method if isinstance(method, PCA) else None
        metrics[name] = compute_metrics(X, results[name], name, pca)

    return results, times, metrics

def plot_results(results, colors, dataset_name, times, metrics):
    fig = plt.figure(figsize=(15, 5))

    for idx, (name, embedding) in enumerate(results.items()):
        ax = fig.add_subplot(1, 3, idx + 1)
        scatter = ax.scatter(embedding[:, 0], embedding[:, 1], c=colors, cmap='viridis')

        # タイトルに評価指標を追加
        title = f'{name}\nTime: {times[name]:.2f}s\n'
        title += f'Trust: {metrics[name]["trustworthiness"]:.3f}'
        if "reconstruction_error" in metrics[name]:
            title += f'\nRecon Error: {metrics[name]["reconstruction_error"]:.3f}'
        ax.set_title(title)

        if dataset_name == "Iris":
            plt.colorbar(scatter, ticks=[0, 1, 2], label='Species')
        else:
            plt.colorbar(scatter)

    plt.suptitle(f'{dataset_name}の次元削減結果比較')
    plt.tight_layout()
    plt.savefig(f'{dataset_name}_comparison.png')
    plt.close()

def main():
    # 次元削減手法の定義
    methods = {
        'PCA': PCA(n_components=2),
        't-SNE': TSNE(n_components=2, random_state=42),
        'UMAP': umap.UMAP(random_state=42)
    }

    # データセットの生成
    datasets = generate_datasets()

    # 各データセットに対して次元削減を適用
    for X, colors, name in datasets:
        results, times, metrics = apply_dimension_reduction(X, methods)
        plot_results(results, colors, name, times, metrics)

        # 評価指標の詳細を出力
        print(f"\n{name}データセットの評価指標:")
        for method_name, method_metrics in metrics.items():
            print(f"\n{method_name}:")
            for metric_name, value in method_metrics.items():
                print(f"  {metric_name}: {value:.3f}")

if __name__ == '__main__':
    main()

関連記事

← 技術ブログ一覧へ