K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

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

中級

02.2.10

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

最終更新 2026-03-06 読了時間 5 分
まとめ
サンプル数 200 に対して特徴量が 500 個、そのうち有用なのは 20 個だけという高次元疎データで、6 つの分類器 × 4 つの前処理 = 24 本のパイプラインを 5 分割交差検証の F1-macro で比べます。SelectKBest(k=50) や L1 正則化の LogisticRegression のように有用な列を絞り込む処理が精度を大きく改善し、前処理なしの RBF カーネル SVM は 475 個のノイズ列に距離を乱されて過学習します。GaussianNB は特徴量を独立に扱うため、前処理の有無にかかわらず安定した精度を保ちます。読み終えると、「特徴量がサンプル数を超えたらモデルより先に次元を減らす」という判断を、ヒートマップの数字で説明できるようになります。

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


1. データの特徴 #

n=200 サンプル、p=500 特徴量の 3 クラス分類で、有用な特徴量は 20 個とその線形結合 5 個だけ、残り 475 個はクラスと無関係なノイズです。特徴量がサンプル数の 2.5 倍あるので、どのサンプルどうしの距離もノイズ 475 列の影響で似た値になり、k-NN や RBF カーネルのような距離ベースの手法は「近い点」を正しく見つけられません(次元の呪い)。

2. 合成データの生成 #

次のコードは make_classification で 200 × 500 のデータを作り、特徴量の分散のヒストグラム、F 値上位 2 特徴量の散布図、F 値上位 50 特徴量の棒グラフの 3 枚で「有用な列がどれだけ少ないか」を見ます。

 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. 実験と結果 #

次のコードは 24 本のパイプラインを同じ 5 分割で交差検証し、F1-macro の平均を 4 行 × 6 列のヒートマップに並べます。すべてのパイプラインの先頭に StandardScaler を置き、前処理は交差検証の各分割の中で学習データだけから当てはめます。

 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) で 500 列を 50 列に絞ると、ほぼすべての分類器で F1-macro が改善します。L1 正則化の LogisticRegression は前処理なしでも比較的健闘しますが、これは内部でスパースな特徴選択を行い、475 本のノイズ列の係数を 0 にしているためです。RBF カーネルの SVM は前処理なしだと F1 が大幅に低下します。500 次元空間ではすべてのサンプル間距離がほぼ等しくなり、RBF カーネルの距離計算が機能しないからです。GaussianNB は前処理によらず安定しており、特徴量を独立に扱う仮定が、クラスと無関係なノイズ列の影響を打ち消す方向に働いています。


5. 誤差パターン分析 #

ヒートマップの最良と最悪に近い 3 つの組み合わせを、同じ 70/30 分割で学習・評価し、混同行列で「どのクラスをどのクラスと間違えたか」を比べます。

 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 カーネルを使うことです。500 次元の空間ではすべてのサンプル間距離がほぼ等しくなり、カーネルが効きません。線形カーネルを使うか、SelectKBest や PCA で次元削減を先に行います。二つ目は、全特徴量でランダムフォレストを使うことです。動きはしますが、475 個のノイズ列がスプリット候補に入るため精度が落ちます。max_features を小さく設定するか、事前に特徴選択をしてください。

まとめ #

サンプル 200 件・特徴量 500 個のデータでは、分類器の選択より「500 列を 50 列に絞る前処理」のほうが F1-macro を大きく動かしました。SelectKBest(k=50) と PCA(n_components=50) はほぼ全モデルを改善し、L1 正則化の LogisticRegression は前処理なしでも自前の特徴選択で持ちこたえます。RBF カーネルの SVM は距離が均一化する高次元で最も崩れ、GaussianNB は独立仮定のおかげで最も安定しました。特徴選択は必ず Pipeline に入れて交差検証の中で行い、評価データのラベルを使ったリークを防いでください。