【ベンチマーク】高次元疎データの分類

中級

2.2.20

【ベンチマーク】高次元疎データの分類

最終更新 2026-03-06 読了時間 3 分
まとめ
  • サンプル数200に対して特徴量500(うち有用20)の高次元疎データで6つの分類器 × 4つの前処理を比較する。
  • L1正則化やSelectKBestによる特徴選択が精度を大きく改善する。
  • RBF-SVMは次元削減なしだと過学習し、ナイーブベイズは高次元でも安定する。

テキスト分類・遺伝子発現データ・アンケートの自由記述など、特徴量がサンプル数を超えるデータセットは実務で珍しくない。こうした高次元疎データでは、モデルの選択以上に前処理(特徴選択・次元削減)が精度を左右する。


1. データの特徴 #

  • n=200 サンプル、p=500 特徴量、3クラス分類
  • 有用な特徴量は20個だけ。残り480個はノイズ
  • 次元の呪い:特徴空間が広すぎて、距離ベースの手法は機能しにくい

2. 合成データの生成 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification

X, y = make_classification(
    n_samples=200, n_features=500, n_informative=20,
    n_redundant=5, n_classes=3, n_clusters_per_class=1,
    random_state=42, class_sep=1.0,
)

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

# 特徴量の分散分布
variances = X.var(axis=0)
axes[0].hist(variances, bins=40, color="#2563eb", edgecolor="white")
axes[0].set_title("特徴量の分散分布")
axes[0].set_xlabel("分散")
axes[0].set_ylabel("特徴量数")
axes[0].grid(alpha=0.2)

# 上位2特徴量で散布図
from sklearn.feature_selection import f_classif
f_scores, _ = f_classif(X, y)
top2 = np.argsort(f_scores)[-2:]
colors_map = {0: "#2563eb", 1: "#10b981", 2: "#f97316"}
for c in np.unique(y):
    mask = y == c
    axes[1].scatter(X[mask, top2[0]], X[mask, top2[1]], c=colors_map[c],
                    label=f"クラス{c}", alpha=0.6, s=20)
axes[1].set_title("F値上位2特徴量による散布図")
axes[1].legend(fontsize=8)
axes[1].grid(alpha=0.2)

# F値の上位50
top50_idx = np.argsort(f_scores)[-50:]
axes[2].barh(range(50), f_scores[top50_idx], color="#2563eb", height=0.8)
axes[2].set_title("F値 上位50特徴量")
axes[2].set_xlabel("F値")
axes[2].set_ylabel("特徴量インデックス")
axes[2].set_yticks([])
axes[2].grid(alpha=0.2, axis="x")

fig.suptitle("高次元疎データの構造(n=200, p=500, 有用=20)", fontsize=13)
fig.tight_layout()
plt.show()

高次元疎データの構造

3. 比較するパイプライン #

前処理分類器
なしLogistic(L1), Logistic(L2), SVM(linear), SVM(RBF), NaiveBayes, RandomForest
VarianceThreshold (中央値以上)同上
SelectKBest (k=50)同上
PCA (n=50)同上

合計: 4前処理 × 6分類器 = 24パイプライン


4. 実験と結果 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.feature_selection import VarianceThreshold, SelectKBest, f_classif
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
import seaborn as sns

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

preprocessors = {
    "なし": None,
    "VarThresh": VarianceThreshold(threshold=np.median(X.var(axis=0))),
    "KBest50": SelectKBest(f_classif, k=50),
    "PCA50": PCA(n_components=50),
}
classifiers = {
    "Logistic(L1)": LogisticRegression(penalty="l1", solver="saga", max_iter=5000, C=1.0, random_state=42),
    "Logistic(L2)": LogisticRegression(penalty="l2", max_iter=5000, C=1.0, random_state=42),
    "SVM(linear)": SVC(kernel="linear", C=1.0, random_state=42),
    "SVM(RBF)": SVC(kernel="rbf", C=1.0, random_state=42),
    "NaiveBayes": GaussianNB(),
    "RandomForest": RandomForestClassifier(n_estimators=100, random_state=42),
}

results = []
for prep_name, prep in preprocessors.items():
    for clf_name, clf in classifiers.items():
        steps = [("scaler", StandardScaler())]
        if prep is not None:
            steps.append(("prep", prep))
        steps.append(("clf", clf))
        pipe = Pipeline(steps)
        scores = cross_val_score(pipe, X, y, cv=cv, scoring="f1_macro")
        results.append({
            "前処理": prep_name, "分類器": clf_name,
            "F1-macro": scores.mean(), "std": scores.std(),
        })

df = pd.DataFrame(results)
pivot = df.pivot_table(index="前処理", columns="分類器", values="F1-macro")
prep_order = ["なし", "VarThresh", "KBest50", "PCA50"]
clf_order = ["Logistic(L1)", "Logistic(L2)", "SVM(linear)", "SVM(RBF)", "NaiveBayes", "RandomForest"]
pivot = pivot.reindex(index=[p for p in prep_order if p in pivot.index],
                      columns=[c for c in clf_order if c in pivot.columns])

fig, ax = plt.subplots(figsize=(12, 4))
sns.heatmap(pivot, annot=True, fmt=".3f", cmap="RdYlGn",
            linewidths=0.5, ax=ax, vmin=0.3, vmax=1.0,
            cbar_kws={"label": "F1-macro(高いほど良い)"})
ax.set_title("高次元疎データ: 前処理 × 分類器 F1-macro ヒートマップ")
ax.set_xlabel("")
ax.set_ylabel("")
fig.tight_layout()
plt.show()

F1-macro ヒートマップ

読み方のポイント #

  • SelectKBest(k=50)やPCA(n=50)で次元を絞ると、ほぼすべての分類器で精度が改善する。
  • L1正則化のLogisticは前処理なしでも比較的健闘する。内部的にスパースな特徴選択を行っているため。
  • RBF-SVMは前処理なしだとF1が大幅に低下する。高次元空間ではRBFカーネルの距離計算が機能しにくい。
  • ナイーブベイズは前処理によらず安定。独立性仮定がノイズ特徴量を無視する効果を生む。

5. 誤差パターン分析 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

# ベスト vs ワーストを固定データ分割で比較
from sklearn.model_selection import train_test_split
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)

configs = [
    ("KBest50 + Logistic(L1)", [("scaler", StandardScaler()), ("prep", SelectKBest(f_classif, k=50)),
                                  ("clf", LogisticRegression(penalty="l1", solver="saga", max_iter=5000, random_state=42))]),
    ("なし + SVM(RBF)", [("scaler", StandardScaler()),
                          ("clf", SVC(kernel="rbf", random_state=42))]),
    ("PCA50 + RandomForest", [("scaler", StandardScaler()), ("prep", PCA(n_components=50)),
                                ("clf", RandomForestClassifier(n_estimators=100, random_state=42))]),
]

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
for i, (name, steps) in enumerate(configs):
    pipe = Pipeline(steps)
    pipe.fit(X_tr, y_tr)
    y_pred = pipe.predict(X_te)
    cm = confusion_matrix(y_te, y_pred)
    ConfusionMatrixDisplay(cm, display_labels=[0, 1, 2]).plot(ax=axes[i], cmap="Blues", colorbar=False)
    axes[i].set_title(name, fontsize=10)

fig.suptitle("混同行列の比較(テストデータ)", fontsize=13)
fig.tight_layout()
plt.show()

混同行列の比較

前処理なしのRBF-SVMはオフダイアゴナル(誤分類)が多く、特にクラス間の混同が激しい。KBest+L1 Logisticは対角成分が支配的で、3クラスとも安定して分類できている。


6. よくある失敗パターン #

  • 前処理なしでRBFカーネルを使う: 高次元空間ではすべてのサンプル間距離がほぼ等しくなり、カーネルが効かない。線形カーネルか次元削減を先に行う。
  • 全特徴量でランダムフォレストを使う: 動きはするが、ノイズ特徴量がスプリット候補に入るため精度が落ちる。max_featuresを小さく設定するか、事前に特徴選択する。