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

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

言語 JA

交差検証

入門

03.1.1

交差検証

最終更新 2020-01-29 読了時間 3 分
まとめ
交差検証は、データを \(k\) 個のフォールド(ブロック)に分け、1 つを検証用・残りを学習用にして成績を測る操作を、検証用を入れかえながら \(k\) 回くり返して平均を取る評価法です。1 回だけの train_test_split では「どの 20% が検証に回ったか」で ROC-AUC が 0.5 にも 0.8 にもなり得ますが、5 分割の平均なら全データが 1 回ずつ検証に使われるので偶然に左右されにくくなります。このページでは 300 件の不均衡データでホールドアウトと cross_validate(cv=5) を比べ、前者が 0.528、後者が 0.844 という差が出る理由を追います。読み終えると、scoring に複数指標を渡して一度に集計し、クラス不均衡なら StratifiedKFold、時系列なら TimeSeriesSplit に切り替える判断ができるようになります。

標本データを分割し、その一部でモデルを学習、残りでテストを行い、解析自体の妥当性を検証する手法 — 交差検証 (Wikipedia)


直感:答えを見て解いたテストでは実力が分からない #

過去問 100 問を丸暗記して同じ 100 問で 100 点を取っても実力とは言えないので、80 問で勉強し残り 20 問を初見のテストにする、というのがホールドアウト(train_test_split)です。ただし、どの 20 問が残るかで点数は変わります。交差検証は 100 問を 5 つの山に分け、山 1 をテストにして残りで勉強、次は山 2 をテストに、と 5 回くり返して平均を取るので、たまたま簡単な山がテストに回った偶然がならされます。典型例は k-fold(k 分割)と Stratified k-fold(各フォールドでラベル比率を揃える)の 2 種です。scikit-learn の cross_validate は分類では既定で層化分割を使います。


何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:データを増やして 2 本の線の差を見る。2 本の差が残るなら過学習、2 本とも誤差が高いままなら学習不足(図の縦軸は RMSE で、低いほど良い)。訓練スコアと検証スコアの 2 本を描くために、各点で交差検証が使われている。

Python でホールドアウトと 5 分割を比べる #

Python 3.13 環境を前提とし、必要なライブラリを導入します。

1
2
python --version        # 例: Python 3.13.0
pip install scikit-learn matplotlib

次のコードは、make_classification で陰性 2 : 陽性 8 の不均衡な 300 件のデータを作り、RandomForestClassifier(max_depth=4) を単一のホールドアウト(8:2 分割)と cross_validate の 5 分割で評価して、ROC-AUC を並べます。

 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
59
60
61
62
63
64
from __future__ import annotations

import numpy as np
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import cross_validate, train_test_split

RANDOM_STATE = 42

def make_dataset() -> tuple[np.ndarray, np.ndarray]:
    """不均衡な二値分類データセットを生成する。"""
    features, labels = make_classification(
        n_samples=300,
        n_classes=2,
        weights=[0.2, 0.8],
        n_informative=4,
        n_features=6,
        n_clusters_per_class=2,
        shuffle=True,
        random_state=RANDOM_STATE,
    )
    return features, labels

def holdout_score() -> float:
    """単一のホールドアウト分割で ROC-AUC を計算する。"""
    features, labels = make_dataset()
    x_train, x_valid, y_train, y_valid = train_test_split(
        features,
        labels,
        test_size=0.2,
        stratify=labels,
        random_state=RANDOM_STATE,
    )
    model = RandomForestClassifier(max_depth=4, random_state=RANDOM_STATE)
    model.fit(x_train, y_train)
    predictions = model.predict(x_valid)
    return roc_auc_score(y_valid, predictions)

def cross_validation_scores() -> dict[str, float]:
    """5 分割交差検証で ROC-AUC と Accuracy の平均を計算する。"""
    features, labels = make_dataset()
    model = RandomForestClassifier(max_depth=4, random_state=RANDOM_STATE)
    scores = cross_validate(
        model,
        features,
        labels,
        cv=5,
        scoring=("roc_auc", "accuracy"),
        return_train_score=False,
        n_jobs=None,
    )
    return {
        "roc_auc": float(np.mean(scores["test_roc_auc"])),
        "accuracy": float(np.mean(scores["test_accuracy"])),
    }

if __name__ == "__main__":
    holdout = holdout_score()
    print(f"Hold-out ROC-AUC: {holdout:.3f}")

    cv_result = cross_validation_scores()
    print(f"5-fold ROC-AUC: {cv_result['roc_auc']:.3f}")
    print(f"5-fold Accuracy: {cv_result['accuracy']:.3f}")

出力例:

Hold-out ROC-AUC: 0.528
5-fold ROC-AUC: 0.844
5-fold Accuracy: 0.858

実行結果の読み方 #

単一ホールドアウトでは ROC-AUC が 0.528 とほぼランダム(0.5)に見えますが、交差検証では 0.844 と安定した性能が確認できます。差の原因は 2 つで、1 つは 60 件の検証データが偶然難しい組になったこと、もう 1 つはホールドアウト側が predict の 0/1 で AUC を計算していることです。同じモデルでも「1 回の分け方」と「評価に渡す値」で評価が 0.3 も動くので、モデル比較では交差検証の平均と標準偏差を使います。


交差検証の設計で決めること #

フォールド数は一般的に 5 または 10 で、データが少ない場合は 1 件ずつ検証に回す LeaveOneOut も候補ですが、300 件なら 300 回学習することになり計算コストが高いです。クラス不均衡がある場合は StratifiedKFold などで各フォールドのラベル比率を揃えます。揃えないと、陽性が 1 件も入らないフォールドで ROC-AUC が計算できなくなります。scoring にタプルを渡すと複数指標を一度に計算できるので、ROC-AUC と Accuracy のように「順位の良さ」と「閾値 0.5 での正解率」を組み合わせるとバランスが見えやすいです。GridSearchCV や RandomizedSearchCV は内部で交差検証を行うので、その外側にもう 1 段の交差検証を置く ネステッド交差検証 にすると、ハイパーパラメータ探索による過学習を防げます。


使いどころと落とし穴 #

データに時系列の依存がある場合は、未来のデータで学習して過去を当てるリークが起きるので、cv=5 ではなく TimeSeriesSplit を使います。ビジネスで重要な指標(例: Recall や Log Loss)を scoring に含め、意思決定者と「何を平均しているか」の認識を合わせます。交差検証は k 回学習するため、1 回の学習に 10 分かかるモデルなら 5 分割で 50 分になり、n_jobs=-1 で並列化するか k を減らす判断が必要です。再現可能にするため、random_state と cv の設定値をノートブックに残し、モデル更新時にも同じ分割で比較できるようにしておきます。


まとめ #

交差検証は \(k\) 個のフォールドを順番に検証用に回して \(k\) 個のスコアを平均する手法で、300 件の例ではホールドアウトの ROC-AUC 0.528 に対して 5 分割の平均は 0.844 と、1 回の分け方による偶然を取り除いた値が得られます。scikit-learn では cross_validate(model, X, y, cv=5, scoring=("roc_auc", "accuracy")) で複数指標を同じ分割でまとめて取得でき、np.std を添えればばらつきも報告できます。クラス不均衡では StratifiedKFold、時系列では TimeSeriesSplit、ハイパーパラメータ探索を含めるならネステッド交差検証に切り替え、k 回分の計算コストを見積もって n_jobs で並列化します。分割を固定する random_state を残しておけば、モデル更新時にも同じ条件で比較できます。

分割数と交差検証 #

分割数 k を 2 から 10 まで変えると、各回の訓練データが 50% から 90% に増え、検証データが 50% から 10% に減る様子を確認できます。