論文一覧に戻る 📚 用語解説(ジャストインタイム型データサイエンス教育)
Nested クロスバリデーション
Nested Cross-Validation (Nested CV)
機械学習の「ハイパーパラメータ調整 × 性能評価」を同時に行うと、性能が過大評価される問題を解決する技法。
外側 CV で「未知データへの汎化性能」を測り、 内側 CV で「α や C の最適値」を選ぶ。 5×3-fold Nested CV のような表記でよく出てくる。
機械学習 モデル選択 汎化性能 バイアス補正
📍 文脈 💡 30秒結論 🎨 直感 📐 数式 🔬 読み解き 🧮 実値計算 🐍 Python ⚠️ 落とし穴 🌐 派生・関連 🔗 関連用語 📚 グループ教材 🗺 概念マップ

📍 あなたが今見ているもの

機械学習の論文や Kaggle notebook で、 こんな表記を見たことはないですか:

Outer 5-fold × Inner 3-fold nested CV で評価
Ridge α: tuned via inner CV ∈ {0.01, 0.1, 1, 10}
Test RMSE = 245.3 (SD across 5 outer folds: ±18.7)

これらは Nested Cross-Validation(入れ子型クロスバリデーション) の表現。 「普通の CV で性能を出して、 同じデータでハイパーパラメータも調整した」というナイーブな手順は、 性能を楽観的に見積もる致命的バイアス を含みます。 Nested CV は「モデル選択用の CV」と「性能評価用の CV」を入れ子にすることで、 このバイアスを除去する標準的な方法論です。

💡 30秒で分かる結論

🎨 直感で掴む — 「同じ問題集で勉強して同じ問題集でテストする」問題

🎯 ナイーブ CV のバイアス

普通の交差検証(CV)でモデル選択と性能評価を同時に行うと、 こんなことが起きます:

  1. K=5 fold に分割
  2. 各 α ∈ {0.01, 0.1, 1, 10} で 5 fold CV の平均 RMSE を計算
  3. 最も RMSE が小さい α を「最適 α」として選ぶ
  4. その最適 α での 5 fold CV 平均 RMSE を「最終性能」として報告

⚠️ 問題:手順 4 の「最終性能」は同じ fold 構造での CV 平均なので、 すでに「この fold でうまくいく α」を選んだ後の評価です。 つまりテストデータが間接的に α 選択に使われた。 結果、 報告される RMSE は実環境(真に未知のデータ)よりも小さく(楽観的に)出る。

🎓 喩え:模試と本番試験

大学受験で、 「直前模試の問題」と「本番試験の問題」が同じだったら、 模試で「この勉強法が一番点数が出る」と判明した方法は、 本番でも当然点数が出る。 でもそれは「本当に頭が良いから」ではなく、 「答えを知っていたから」です。

Nested CV の発想:

📊 nested CV vs naive CV の数字感

典型的な経験則:

シナリオnaive CV の RMSENested CV の RMSE差(バイアス)
ハイパラ 4 候補、 n=2000.850.92+0.07(楽観バイアス)
ハイパラ 20 候補、 n=2000.780.95+0.17(さらに楽観)
ハイパラ 100 候補、 n=2000.720.99+0.27(致命的)
ハイパラ 100 候補、 n=10,0000.850.86+0.01(無視できる)

つまり (1) ハイパラ候補が多いほど、 (2) サンプルが少ないほど、 naive CV のバイアスは大きくなる。 Kaggle で「CV では当たったのに、 LB(leaderboard)でガッカリ」というのは多くの場合これが原因。

📐 数式 — Nested CV の定式化

データセット $\mathcal{D} = \{(x_i, y_i)\}_{i=1}^{n}$、 ハイパーパラメータ空間 $\Lambda$、 学習アルゴリズム $\mathcal{A}_\lambda$、 損失関数 $L$。

外側ループ

$\mathcal{D}$ を $K_o$ 個に分割:$\mathcal{D} = \mathcal{D}^{\text{outer}}_1 \cup \mathcal{D}^{\text{outer}}_2 \cup \dots \cup \mathcal{D}^{\text{outer}}_{K_o}$。 各 $k = 1, \dots, K_o$ について:

$$\mathcal{D}_{\text{train}}^{(k)} = \mathcal{D} \setminus \mathcal{D}^{\text{outer}}_k, \quad \mathcal{D}_{\text{test}}^{(k)} = \mathcal{D}^{\text{outer}}_k$$

内側ループ(外側の各 fold k 内で)

$\mathcal{D}_{\text{train}}^{(k)}$ を $K_i$ 個に分割し、 各 $\lambda \in \Lambda$ について:

$$\hat{R}_{\text{inner}}^{(k)}(\lambda) = \frac{1}{K_i}\sum_{j=1}^{K_i} L\left(\mathcal{A}_\lambda(\mathcal{D}_{\text{train}}^{(k)} \setminus \mathcal{D}_{\text{inner}, j}^{(k)}),\; \mathcal{D}_{\text{inner}, j}^{(k)}\right)$$
内側 CV の平均損失。 これでハイパラ $\lambda$ を比較。

最適ハイパラを選択:

$$\hat{\lambda}^{(k)} = \underset{\lambda \in \Lambda}{\arg\min}\;\hat{R}_{\text{inner}}^{(k)}(\lambda)$$

外側ループでの性能評価

$\hat{\lambda}^{(k)}$ を使って外側の訓練データ全体で再学習し、 外側テストで評価:

$$\hat{R}_{\text{outer}}^{(k)} = L\left(\mathcal{A}_{\hat{\lambda}^{(k)}}(\mathcal{D}_{\text{train}}^{(k)}),\; \mathcal{D}_{\text{test}}^{(k)}\right)$$

最終的に外側 fold での平均と標準偏差を報告:

$$\bar{R}_{\text{nested}} = \frac{1}{K_o}\sum_{k=1}^{K_o} \hat{R}_{\text{outer}}^{(k)}, \quad \widehat{\mathrm{SD}} = \sqrt{\frac{1}{K_o - 1}\sum_{k=1}^{K_o} \left(\hat{R}_{\text{outer}}^{(k)} - \bar{R}_{\text{nested}}\right)^2}$$
これが「真の汎化性能」の不偏推定とその不確実性。

計算コスト

$$\text{学習回数} = K_o \times K_i \times |\Lambda| + K_o$$
例:5×3 nested CV、 ハイパラ 4 候補なら 5×3×4 + 5 = 65 回の学習。

🔬 数式を「言葉」で読み解く

$\mathcal{D}^{\text{outer}}_k$(外側 fold)
性能評価用に取っておくデータ」。 K_o = 5 なら全データの 1/5。 この fold は外側ループの 1 回の反復で絶対に 訓練やハイパラ選択に触れない。
$\mathcal{D}_{\text{train}}^{(k)}$(外側訓練)
外側 fold k 以外の全データ」。 これを内側 CV にかけてハイパラを決定 → 全体で再学習。
$\hat{R}_{\text{inner}}^{(k)}(\lambda)$(内側 CV 損失)
外側 fold k を使わず、 ハイパラ λ がどれくらい良いか」を内側 K_i fold CV で評価した値。 ハイパラ選択のみに使う。
$\hat{\lambda}^{(k)}$(外側 fold k での最適ハイパラ)
外側 fold ごとに、 異なる最適ハイパラが選ばれてもよい」。 これが「単一の最適ハイパラを報告できないか?」という疑問への答え(→ 落とし穴セクションで解説)。
$\hat{R}_{\text{outer}}^{(k)}$(外側性能)
外側 fold k で、 真に未知データを見たときの損失」。 これが不偏推定の本体。 内側ループは「外側の訓練データだけ」で完結しているので、 外側テストへの漏洩は理論上ゼロ。
$\widehat{\mathrm{SD}}$(fold 間の標準偏差)
fold によって性能がどれくらいブレるか」。 K_o = 5 なら 5 つの値の SD を計算。 「実環境で何度もデプロイしたら性能がどれくらい変動するか」の代理指標。

💡 重要な性質:Nested CV は 「最終的にデプロイするモデル」を生成する手続きではないこと。 あくまで「ハイパラチューニング + 学習のパイプライン全体の性能を測る」プロトコル。 デプロイには別途、 全データで内側 CV を再実行して最適 λ を決め、 全データで学習する。

🧮 実値で計算してみる — SSDSE-B 人口予測タスク

タスク設定

SSDSE-B-2026 の 47 都道府県データから、 2020〜2022 年の経済・社会指標(人口、 出生率、 高齢化率、 県内総生産、 有効求人倍率、 大学進学率、 ...) を特徴量にして、 翌年(2023 年)の総人口を予測するリッジ回帰モデルを構築。 Ridge の正則化強度 α を Nested CV でチューニング。

STEP 1:データ構造とサイズ

STEP 2:nested CV の設定

STEP 3:典型的な結果

【SSDSE-B 47県・nested CV の結果例】
$$\bar{\text{RMSE}}_{\text{nested}} = 187.3 \text{ 千人} \pm 42.5 \text{(fold 間 SD)}$$
naive 5-fold CV だと 162.8 千人 → 約 15% の楽観バイアスがあった。
外側 fold選ばれた α外側 RMSE(千人)
10.1165.2
21.0198.7
31.0225.4
40.1148.9
510.0198.3
平均187.3 ± 42.5

解釈

📊 Cawley & Talbot (2010) の実証 — なぜ Nested CV が必要か

機械学習における Nested CV の必要性を最も明確に示したのが、 Cawley & Talbot (2010) "On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation"(Journal of Machine Learning Research)。 著者らは UCI の 13 個のデータセットで、 ナイーブ CV と Nested CV を比較し、 ナイーブ CV が 平均 4〜8% 程度性能を楽観的に評価することを実証しました。

論文の核心メッセージ

  1. モデル選択は、 それ自体が「学習」である。 ハイパラを CV で最適化する手順は、 そのハイパラ空間に対するメタ学習。 学習であるからには過学習が起こりうる
  2. 同じデータで「メタ学習」と「最終評価」を行うと、 評価がバイアスを受ける。 これは Vapnik の VC 理論や Bonferroni 補正と同じ「多重比較問題」の現れ
  3. 解決策は Nested CV か、 別個の test set。 メタ学習プロセス全体を外側ループで評価することで、 評価セットへの情報漏洩を断つ

論文の図表が示すもの

Cawley & Talbot は、 SVM のハイパラ(C と γ)を様々な手法で選択した結果を比較。 ナイーブ CV では「データが少ない場合に特に楽観バイアスが大きい」「ハイパラ候補が多いとバイアスが大きい」という 2 つの傾向を実証。 一方 Nested CV ではバイアスが事実上消失。

業界への影響

この論文以降、 機械学習の主要会議(NeurIPS, ICML)では、 ハイパラ調整を含む手法を提案する論文には Nested CV による評価 が事実上必須となりました。 Kaggle や DrivenData などのコンペでも、 「validation set はモデル選択に使う、 final leaderboard は完全分離した test set」というプロトコルが定着しています。

🎯 K_outer と K_inner はいくつにすべきか

Nested CV の唯一の自由パラメータは、 外側 fold 数 K_o と内側 fold 数 K_i です。 経験則と理論的な目安を整理:

外側 K_o の選び方

データサイズ n推奨 K_o理由
n < 50LOOCV(K = n)1 fold ≈ 1 サンプル、 fold 間の分散は大きいが、 訓練データが最大化される
50 ≤ n < 50010-fold慣習的なベストプラクティス。 fold 間の分散と訓練データ量のバランス
500 ≤ n < 10,0005-fold or 10-fold計算量と精度のバランス。 5-fold の方が高速
n ≥ 10,000Holdout(train/val/test)でも十分Nested CV は計算量で割に合わない

内側 K_i の選び方

計算コストの目安

SVM のように学習に O(n²) 〜 O(n³) かかるアルゴリズムでは:

🐍 Python 実装 — sklearn での Nested CV

方法 A:シンプルな手動実装(教育用)

# SSDSE-B 人口予測タスクで Ridge α を Nested CV で選ぶ
import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler

# データ読込
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=2)
df.columns = ['year', 'code', 'pref', 'pop'] + [f'c{i}' for i in range(len(df.columns)-4)]
df = df[df['year'].isin([2020, 2021, 2022])].copy()

# 特徴量(適当に 8 列選ぶ:人口、出生率、高齢化率、県内総生産、有効求人、大学進学率、医師数、犯罪件数の代用)
feature_cols = ['pop', 'c0', 'c5', 'c27', 'c30', 'c41', 'c52', 'c70']
X = df[feature_cols].values.astype(float)
y = df['pop'].shift(-47).values  # 翌年の人口
mask = ~np.isnan(y)
X, y = X[mask], y[mask]
print(f'サンプル数: {len(y)}, 特徴量数: {X.shape[1]}')

# Nested CV
alpha_grid = [0.001, 0.01, 0.1, 1, 10, 100, 1000]
outer_cv = KFold(n_splits=5, shuffle=True, random_state=42)
outer_rmses = []
chosen_alphas = []

for fold_idx, (train_idx, test_idx) in enumerate(outer_cv.split(X)):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]

    # 内側 CV:α 探索
    inner_cv = KFold(n_splits=3, shuffle=True, random_state=1)
    best_alpha, best_inner_rmse = None, np.inf
    for alpha in alpha_grid:
        inner_rmses = []
        for in_tr, in_va in inner_cv.split(X_train):
            scaler = StandardScaler().fit(X_train[in_tr])
            model = Ridge(alpha=alpha).fit(scaler.transform(X_train[in_tr]), y_train[in_tr])
            pred = model.predict(scaler.transform(X_train[in_va]))
            inner_rmses.append(np.sqrt(mean_squared_error(y_train[in_va], pred)))
        avg = np.mean(inner_rmses)
        if avg < best_inner_rmse:
            best_inner_rmse, best_alpha = avg, alpha

    # 外側評価:選ばれた α で全 X_train で再学習 → X_test 評価
    scaler = StandardScaler().fit(X_train)
    model = Ridge(alpha=best_alpha).fit(scaler.transform(X_train), y_train)
    pred = model.predict(scaler.transform(X_test))
    outer_rmse = np.sqrt(mean_squared_error(y_test, pred))
    outer_rmses.append(outer_rmse)
    chosen_alphas.append(best_alpha)
    print(f'Fold {fold_idx+1}: α={best_alpha}, outer RMSE={outer_rmse:.2f}')

print(f'\\nNested CV RMSE: {np.mean(outer_rmses):.2f} ± {np.std(outer_rmses, ddof=1):.2f}')
print(f'選ばれた α 群: {chosen_alphas}')

方法 B:sklearn の GridSearchCV + cross_val_score(推奨)

from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV, cross_val_score, KFold
import numpy as np

pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('ridge', Ridge())
])

param_grid = {'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100, 1000]}

# 内側 CV(GridSearchCV 内)
inner_cv = KFold(n_splits=3, shuffle=True, random_state=1)
grid = GridSearchCV(pipe, param_grid, cv=inner_cv, scoring='neg_mean_squared_error')

# 外側 CV
outer_cv = KFold(n_splits=5, shuffle=True, random_state=42)
neg_mse_scores = cross_val_score(grid, X, y, cv=outer_cv, scoring='neg_mean_squared_error')
rmse_scores = np.sqrt(-neg_mse_scores)
print(f'Nested CV RMSE: {rmse_scores.mean():.2f} ± {rmse_scores.std(ddof=1):.2f}')

方法 C:deployment 用に「全データで再学習」

# Nested CV はあくまで「評価プロトコル」。デプロイ用モデルは全データで再学習する
final_grid = GridSearchCV(pipe, param_grid, cv=KFold(5, shuffle=True, random_state=0),
                          scoring='neg_mean_squared_error')
final_grid.fit(X, y)
print(f'最終モデル α: {final_grid.best_params_}')
# final_grid.best_estimator_ がデプロイ用モデル

方法 D:層化 / 時系列対応の nested CV

from sklearn.model_selection import StratifiedKFold, TimeSeriesSplit

# 分類タスクなら StratifiedKFold で fold ごとのクラス比率を保つ
# outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# 時系列なら過去→未来の順序を守る TimeSeriesSplit
# outer_cv = TimeSeriesSplit(n_splits=5)

# SSDSE-B の都道府県データは「グループ=都道府県」なので GroupKFold が望ましい
from sklearn.model_selection import GroupKFold
groups = df['pref'].values[mask]  # 都道府県名
outer_cv = GroupKFold(n_splits=5)
for tr, te in outer_cv.split(X, y, groups=groups):
    # 同じ都道府県は train/test に分かれない(リーク防止)
    pass

方法 E:MLxtend ・ Optuna との組合せ

# Optuna でベイズ最適化を内側に組み込む例
import optuna
from sklearn.model_selection import cross_val_score

def objective_for_outer_fold(trial, X_tr, y_tr):
    alpha = trial.suggest_float('alpha', 1e-3, 1e3, log=True)
    model = Pipeline([('sc', StandardScaler()), ('r', Ridge(alpha=alpha))])
    scores = cross_val_score(model, X_tr, y_tr, cv=3, scoring='neg_mean_squared_error')
    return -scores.mean()

outer_rmses = []
for tr_idx, te_idx in outer_cv.split(X):
    study = optuna.create_study(direction='minimize')
    study.optimize(lambda t: objective_for_outer_fold(t, X[tr_idx], y[tr_idx]),
                   n_trials=20, show_progress_bar=False)
    best_alpha = study.best_params['alpha']
    model = Pipeline([('sc', StandardScaler()), ('r', Ridge(alpha=best_alpha))])
    model.fit(X[tr_idx], y[tr_idx])
    rmse = np.sqrt(mean_squared_error(y[te_idx], model.predict(X[te_idx])))
    outer_rmses.append(rmse)
print(f'Bayes-Nested CV: {np.mean(outer_rmses):.2f} ± {np.std(outer_rmses, ddof=1):.2f}')

⚠️ Nested CV の 5 つの落とし穴

① 「Nested CV で選ばれた α」を最終モデルに使えない
外側 fold ごとに異なる α が選ばれるため、 「単一の最適 α」は Nested CV からは直接得られない。 デプロイ時は別途、 全データに対して再度 (内側) CV を回してハイパラを決め、 全データで学習する。 Nested CV はあくまで「ハイパラチューニングを含むパイプラインの性能評価」のためのプロトコル。
② グループ構造を無視するとリーク
SSDSE のように「同じ都道府県の複数年データ」がある場合、 普通の KFold だと train と test に同じ県の別年データが混入し、 性能が過大評価される。 GroupKFold で「県ごと」にまとめて分割すべき。 医療データなら「患者ごと」、 推薦システムなら「ユーザーごと」。
③ 前処理(標準化、 PCA、 特徴選択)を外側で先にやるとリーク
「データ全体で StandardScaler を fit してから CV」は典型的なリーク。 各 fold の train だけで fit → val/test に transform、 でないと test の統計量が訓練に流入する。 sklearn の Pipeline を使えば自動的に正しい順序になる(最大のメリット)。
④ ハイパラ候補が広すぎると内側 CV も「過学習」する
内側 CV でハイパラを 1000 候補から選ぶと、 内側 CV 自体が「内側 val fold に過学習」する → 外側性能との乖離が広がる。 対処:ハイパラ候補をドメイン知識で絞る、 ベイズ最適化に切り替える、 内側 CV の fold 数を増やす、 ハイパラ選択基準に 1-SE rule(最良の 1 標準誤差以内で最も単純なモデル)を導入。
⑤ 計算コスト爆発 — 大規模データには不向き
Nested CV は学習回数が K_o × K_i × |Λ| 倍。 ニューラルネット 1 回の学習に数時間かかる場合、 5×3×20 = 300 回は非現実的。 大規模データ・大規模モデルでは「train/val/test の 3 分割」で十分(n > 数万なら test set の評価が安定)。 Nested CV は小〜中規模データ(n ≤ 数千)で真価。

🌐 Nested CV の派生・関連手法

手法用途特徴
Holdout(3 分割)大規模データtrain / val / test に一度だけ分割。 計算量最小、 評価は単一の test set
k-fold CV(単一)性能評価のみ(ハイパラ固定)普通の交差検証。 ハイパラ調整しないなら十分
Nested CV小〜中データのハイパラ調整 + 評価外側 × 内側の二重 CV、 不偏推定
Repeated Nested CV結果の安定化Nested CV を異なる乱数で複数回繰り返し平均
Leave-One-Out Nested CV極小データ(n < 50)K_o = n。 計算量大、 分散も大きい
Group Nested CV階層 / クラスタ構造外側・内側ともグループ単位で分割(GroupKFold ベース)
Stratified Nested CV不均衡分類fold ごとのクラス比率を保つ
Time-Series Nested CV時系列予測過去 → 未来の順序を守る walk-forward 型
Bayesian Nested CV大きなハイパラ空間内側ループを Bayes 最適化(Optuna, Hyperopt)に置換
5×2 CV (Dietterich)モデル間検定2-fold CV を 5 回繰り返し、 paired t 検定

使い分けフロー

  1. n > 数万 → Holdout(train/val/test)で十分
  2. n = 数百〜数千、 ハイパラあり → Nested CV 推奨
  3. n < 100 → Repeated Nested CV または LOOCV
  4. 時系列 → Time-Series Nested CV
  5. グループ構造 → Group Nested CV
  6. 計算余力なし → 「Nested CV 一回で評価 + 全データで再学習」が現実的解

🆚 普通の CV vs Nested CV — 早見表

項目普通の k-fold CVNested CV
主目的性能評価のみ(ハイパラ固定)ハイパラ調整 + 不偏性能評価
ループ数1 重2 重(外側 + 内側)
学習回数K 回K_o × K_i × |Λ| + K_o 回
ハイパラ調整不可(手動か固定)各外側 fold で内側 CV により調整
性能バイアスハイパラ調整時に楽観バイアス不偏(理論的)
最終モデルfold ごとに 1 つ(K 個)fold ごとに 1 つ(K_o 個)、 デプロイ用は別途学習
fold 間の SDfold ごとの性能の SDfold ごとの性能の SD(ただしハイパラも変動)
計算コスト中〜高
適用領域ハイパラなしモデル、 探索初期論文・本番デプロイ前評価
典型的設定5-fold or 10-fold5×3 or 10×5

運用ルール:「ハイパラ調整なしの単純性能チェック → k-fold CV」「ハイパラ調整あり、 論文・本番前の最終評価 → Nested CV」「大規模データ、 ニューラルネット → holdout(train/val/test)」と、 シナリオに応じて使い分け。

🚀 sklearn Pipeline と組み合わせる実用 Tips

Nested CV を実装する上で最大のミスポイントは「データリーク」です。 特に前処理(標準化、 PCA、 特徴選択、 欠損補完)を CV 外で行うと、 test fold の統計量が訓練に流れ込みます。 sklearn の Pipeline を使えば、 fold ごとに前処理が自動的に閉じられます:

正しい Pipeline 例

Pipeline([
  ('imputer', SimpleImputer(strategy='median')),
  ('scaler', StandardScaler()),
  ('feat_sel', SelectKBest(f_regression, k=5)),
  ('pca', PCA(n_components=3)),
  ('model', Ridge(alpha=1.0))
])
このパイプライン全体を GridSearchCV に渡せば、 fold ごとに最初から最後まで再 fit される。 リークの心配なし。

ハイパラ指定の構文

Pipeline のステップ名を頭に付けて二重アンダースコアで連結:

複数のステップのハイパラを同時に探索可能。 候補が組み合わせ爆発しないよう、 ベイズ最適化(Optuna)に切り替えるのも選択肢。

🛠 トラブルシューティング集

Q1:Nested CV と naive CV の結果がほぼ同じ。 やる意味はあった?

回答:「やってみたら差がなかった」のは、 (1) サンプルサイズが十分大きい、 (2) ハイパラ候補が少ない、 (3) ハイパラに対するモデル性能の感度が低い、 のいずれか。 結果として naive CV でもバイアスが小さい状況だった、 ということ。 Nested CV をやって「差がない」と確認したこと自体が研究の価値。

Q2:外側 fold ごとに違うハイパラが選ばれる。 これは問題?

回答:問題ではない、 むしろ重要な情報。 「ハイパラ選択が不安定」=「データ依存で最適 λ が変動する」=「実環境で 1 つの λ を固定するのが危険」を意味する。 デプロイ時は (a) 全データで再度内側 CV してハイパラ決定、 (b) アンサンブル(fold ごとのモデルを平均)、 (c) より広い CV+1-SE rule、 などの対策を取る。

Q3:内側 CV が時間がかかりすぎる。 ハイパラ候補を減らすべき?

回答:まず (1) Pipeline で正しく実装されているか確認(毎回 fit 直すべきは前処理だけ)、 (2) RandomizedSearchCV や Optuna で候補数を制限、 (3) HalvingGridSearchCV(sklearn 0.24+)で予算節約、 (4) 軽量モデルで予備実験して候補を絞ってから本番、 という順で対処。

Q4:分類タスクで stratification が必要だが Pipeline と組み合わせる方法は?

回答StratifiedKFold を outer_cv と inner_cv の両方に指定。 Pipeline は学習器の前処理だけ担当し、 fold 分割とは独立。

Q5:階層構造(同じ患者の複数測定、 同じ都道府県の複数年)がある場合は?

回答GroupKFold または StratifiedGroupKFold を使う。 「同じ患者/同じ県」を train と test に分けないことで、 評価の独立性を保つ。 これを忘れると性能が大きく過大評価される。

Q6:Nested CV で「statistical significant difference between models」を主張したい

回答:Dietterich (1998) の 5×2 CV paired t-test、 または corrected resampled t-test(Nadeau & Bengio 2003)を使う。 単純な「Nested CV 平均の差」では有意性検定にならない(fold が独立でないため)。

Q7:deep learning では Nested CV を回す余裕がない

回答:(1) holdout 3 分割(train / val / test)が現実解、 (2) val でハイパラ調整、 test で最終評価、 (3) test set は本当に最後に 1 回だけ評価、 (4) early stopping を val loss で行えば自動ハイパラ調整に近い効果。 サンプル数が数万を超えていれば、 holdout の評価精度は十分。

Q8:Nested CV の結果を論文に書くときの定型文は?

回答:例として "We evaluated model performance using nested cross-validation with K_outer = 5 outer folds and K_inner = 3 inner folds. Hyperparameters (α ∈ {0.001, 0.01, 0.1, 1, 10, 100, 1000}) were tuned via grid search in the inner CV loop on the training portion of each outer fold. The final performance is reported as the mean ± standard deviation across outer folds (RMSE = 187.3 ± 42.5)." といった記述。 K_outer, K_inner, ハイパラ範囲、 評価指標を明示するのが必須。

Q9:Repeated Nested CV って何?

回答:Nested CV を異なる乱数シードで 10〜30 回繰り返し、 性能の平均と SD を報告する手法。 fold 分割のランダム性によるブレを抑えられる。 計算コストが Nested CV のさらに数十倍になるため、 小規模データ・軽量モデルで論文に十分な精度を求めるときに採用する。 sklearn の RepeatedKFold を outer に使う。

Q10:CV のスコアと test set のスコアが大きく違う

回答:(1) Pipeline で前処理がきちんと閉じているか再確認(リーク疑い)、 (2) train と test の分布が違うかを KS 検定や PCA で可視化(distribution shift)、 (3) ハイパラを「test set に近づくように」 tweak していないか(実質的なリーク)、 (4) Nested CV に切り替えてバイアス除去。

🧪 自己診断 — 理解度チェック問題

  1. 問題 1:n = 100 のデータで、 5 候補のハイパラを 5-fold CV で調整し、 同じ 5-fold CV の最良平均値を「最終性能」と報告した。 これは何が問題か?
    解答:選んだハイパラは fold ごとの val 性能を最大化するもので、 報告した値はもはや「未知データへの汎化」ではない。 Nested CV を使うべき。
  2. 問題 2:5×3 nested CV で 5 つの外側 fold の RMSE が 100, 150, 200, 180, 170 だった。 何を報告すべき?
    解答:平均 ≈ 160、 SD ≈ 38。 「RMSE = 160 ± 38」と報告。 fold 間の変動の大きさが「実環境性能の不確実性」を示す。
  3. 問題 3:内側 CV で最適 α = 0.1 が選ばれたあと、 外側 fold での RMSE を測ったあと、 「α = 0.1 をデプロイ用モデルに使う」のは正しいか?
    解答:1 つの外側 fold で選ばれた α を使うのは情報量を捨てる。 (a) 全データで再度内側 CV を回して最終 α を決める、 または (b) 5 つの fold モデルをアンサンブル、 が現代的解。
  4. 問題 4:SSDSE のデータで 47 都道府県 × 3 年 = 141 サンプル。 普通の KFold で nested CV を回した。 何がリーク?
    解答:同じ都道府県の異なる年データが train と test に分かれる。 GroupKFold(groups=都道府県) に変更する。
  5. 問題 5:Random Forest の n_estimators と max_depth を nested CV で調整したい。 何個の組合せ × fold?
    解答:5×3 nested CV、 n_estimators 4 候補 × max_depth 4 候補 = 16 ハイパラ → 5 × 3 × 16 + 5 = 245 回の学習。 n = 数百なら現実的、 数万なら不向き。

📚 関連グループ教材

🗺 概念マップ — モデル評価の階層

モデル評価プロトコル(ML パイプラインのバイアス管理)
├── レベル 0:訓練データそのもので評価
│   └── 致命的に楽観バイアス(過学習を見抜けない)
├── レベル 1:単一の hold-out(train / test)
│   └── 大規模データでは OK、 小データでは分散大
├── レベル 2:k-fold CV(ハイパラ固定)
│   └── 性能評価としては不偏
├── レベル 3:CV でハイパラ調整 + 同じ CV で評価(naive)
│   └── 楽観バイアス発生 ← Nested CV が解決する問題
├── レベル 4:Nested CV
│   ├── 外側:性能評価
│   └── 内側:ハイパラ調整
│       ├── Grid Search
│       ├── Random Search
│       └── Bayesian Optimization
└── レベル 5:Repeated Nested CV / 階層 CV / 時系列 CV
    └── 計算コストとのトレードオフを取りながら更に精密化
  

Cawley & Talbot (2010) "On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation"(JMLR)が Nested CV の必要性を実証的に示した古典論文。 機械学習論文を書く際は、 ハイパラ調整があるなら必ず Nested CV か Holdout のいずれかで評価する、 が現代の標準作法。

📚 さらに学ぶには

このサイト内

推奨書籍・論文

オンライン教材

困ったときは

「Nested CV が遅すぎる」「結果が安定しない」「最終モデルにどの α を使うべきか」など困ったら、 (1) ハイパラ候補を絞る/ベイズ最適化に切替、 (2) Repeated Nested CV で複数乱数試行、 (3) 「Nested CV は評価、 最終モデルは別途全データで再 fit」のフロー確認、 (4) Pipeline でリーク防止、 を順に確認。 Cross Validated(Stack Exchange)の "nested-cross-validation" タグも参考に。

🧠 Nested CV の理論的基盤 — 選択バイアスを数式で見る

Nested Cross-Validation の核心は 「ハイパーパラメータ選択」と「汎化性能評価」を完全に分離する ことにあります。 通常の K-fold CV をハイパーパラメータ選択と性能評価の両方に使うと、 選択バイアス(selection bias)と呼ばれる過大評価が発生します。 これは「複数のコイントスを行い、 最も表が出たコインを選んで、 そのコインの表確率を報告する」のと同じトリックです。

数式で書くと、 ハイパーパラメータの集合 $\Theta$、 各 $\theta \in \Theta$ に対する CV スコア $\hat{R}_{\text{CV}}(\theta)$ について、 $\min_{\theta} \hat{R}_{\text{CV}}(\theta)$ の期待値は、 各 $\theta$ の真のリスク $R(\theta)$ の最小値より小さくなる傾向があります。 これを Jensen の不等式の逆向き効果と呼ぶこともあります。

$$\mathbb{E}\left[\min_{\theta \in \Theta} \hat{R}_{\text{CV}}(\theta)\right] \le \min_{\theta \in \Theta} \mathbb{E}[\hat{R}_{\text{CV}}(\theta)] = \min_{\theta \in \Theta} R(\theta)$$

SSDSE-B-2026 の 47 都道府県データのような小標本では、 $|\Theta|$ が 20 個程度のグリッドでもこのバイアスが顕著に出ます。 Cawley & Talbot (2010) は完全ランダムデータで AUC 0.5 が 0.7+ に「誤って向上」することを示し、 Nested CV の必要性を強く訴えました。

📐 アルゴリズム手順 — 二重ループの詳細

Nested CV は二重ループ構造を取ります。 外側ループで $K_{\text{outer}}$ 個の fold を作り、 各 fold で「テストデータを完全に隔離」。 内側ループで残りの訓練データを $K_{\text{inner}}$ 個に分け、 ハイパーパラメータを選択。 選ばれたハイパーパラメータで訓練データ全体で再学習し、 隔離されたテストデータで評価。 これを $K_{\text{outer}}$ 回繰り返した平均が「不偏」な汎化性能推定値となります。

  1. Step 1:データ $D$ を外側 $K_{\text{outer}}$ 分割 → $D_1^{\text{out}}, \ldots, D_{K_{\text{outer}}}^{\text{out}}$
  2. Step 2:各外側 fold $k$ について:訓練データ $D \setminus D_k^{\text{out}}$ を取り出す
  3. Step 3:訓練データを内側 $K_{\text{inner}}$ 分割 → 内側 CV でハイパーパラメータ $\theta_k^\star$ を選択
  4. Step 4:訓練データ全体で $\theta_k^\star$ を用いてモデルを学習し、 $D_k^{\text{out}}$ で性能を測る
  5. Step 5:外側 $K_{\text{outer}}$ 個の性能スコアを平均し SE を計算
  6. Step 6:最終モデルは全データで再学習(ただし $\theta_k^\star$ は fold 毎に異なるので、 「合意」を取るか全データで再選択)

重要なのは、 外側 fold のテストデータ $D_k^{\text{out}}$ は内側 CV のハイパーパラメータ選択に一切使われないこと。 これにより「データを見てから選んだ」というバイアスが排除されます。

🎯 $K_{\text{outer}}$ と $K_{\text{inner}}$ の選び方

$K_{\text{outer}}$$K_{\text{inner}}$計算量倍率バイアス分散推奨場面
5525×デフォルト・$n \ge 100$
10550×極低小標本 $n < 100$
1010100×極低計算予算潤沢時
$n$ (LOO)5$5n$×極低極高$n \le 50$ のとき
33高速プロトタイプ用
5$n-1$$5(n-1)$×内側 LOO + 外側 5-fold

SSDSE-B-2026 ($n=47$) の標準設定は $K_{\text{outer}}=5, K_{\text{inner}}=5$。 この場合 1 fold の訓練データは約 38 サンプル、 内側でさらに 5 分割すると 1 fold 約 30 サンプルでハイパーパラメータを選び、 8 サンプルで内側評価。 「47 都道府県を学習に十分使いつつ過大評価を避ける」バランス点です。

反復 Nested CV(外側分割を複数シードで反復)はさらに分散を下げる王道。 RepeatedKFold で 5-fold × 10 回 = 50 fold 評価を行うと、 SE が約 $1/\sqrt{10} = 0.32$ 倍に縮みます。

🐍 Python 実装 — sklearn での完全ワークフロー

sklearn の `GridSearchCV` を `cross_val_score` で包むのが標準パターン。 内側 CV は GridSearch 内で自動的に走り、 外側は cross_val_score が制御します。

import pandas as pd
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import KFold, GridSearchCV, cross_val_score

# 1) 実データ読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
X = df.iloc[:, 3:11].values
y = df.iloc[:, 11].values

# 2) Pipeline: 標準化 + Ridge
pipe = Pipeline([('sc', StandardScaler()), ('r', Ridge())])

# 3) 内側 5-fold CV で α 選択
inner = KFold(n_splits=5, shuffle=True, random_state=1)
gs = GridSearchCV(pipe, {'r__alpha': np.logspace(-3, 3, 25)},
                  cv=inner, scoring='r2', n_jobs=-1)

# 4) 外側 5-fold CV で汎化性能
outer = KFold(n_splits=5, shuffle=True, random_state=0)
scores = cross_val_score(gs, X, y, cv=outer, scoring='r2', n_jobs=-1)
print(f'Nested CV R²: {scores.mean():.3f} ± {scores.std():.3f}')

上記コードでは内側 25 個のグリッド × 5 fold = 125 回学習を外側 5 fold で行うので、 計 625 回モデル学習。 SSDSE-B-2026 規模なら 10 秒程度で完了します。

🔁 反復 Nested CV — 分散を下げる王道

1 回の Nested CV では外側 fold の選び方によってスコアがブレます。 これを安定化する標準テクニックが 反復 Nested CV (Repeated Nested CV)。 外側分割を異なる乱数シードで $R$ 回繰り返し、 全 $R \times K_{\text{outer}}$ 個のスコアを平均します。

from sklearn.model_selection import RepeatedKFold

outer_rep = RepeatedKFold(n_splits=5, n_repeats=10, random_state=0)
scores_rep = cross_val_score(gs, X, y, cv=outer_rep, scoring='r2')
print(f'反復 Nested CV R²: {scores_rep.mean():.3f} ± {scores_rep.std():.3f}')
print(f'スコア分布: {np.percentile(scores_rep, [10, 50, 90])}')

反復回数 $R$ の指針:$R=10$ で SE がほぼ $1/\sqrt{R} = 0.32$ 倍に。 $R=30$ なら 0.18 倍。 SSDSE-B のような小標本では $R \ge 10$ を強く推奨します。

📅 時系列・グループ構造での Nested CV

SSDSE-B のような時系列パネルでは「ランダム分割」がリークを生みます。 同じ都道府県の異なる年が学習・テストに分裂すると、 県固定効果がリークするのです。

時系列分割

from sklearn.model_selection import TimeSeriesSplit

# 内外両方を時間順に
inner_ts = TimeSeriesSplit(n_splits=5)
outer_ts = TimeSeriesSplit(n_splits=5)
gs_ts = GridSearchCV(pipe, {'r__alpha': np.logspace(-3, 3, 25)},
                     cv=inner_ts, scoring='neg_mean_squared_error')
scores_ts = cross_val_score(gs_ts, X, y, cv=outer_ts, scoring='neg_mean_squared_error')
print(f'時系列 Nested CV MSE: {-scores_ts.mean():.4f}')

グループ分割

from sklearn.model_selection import GroupKFold

groups = df['Prefecture'].values  # 都道府県名でグルーピング
outer_g = GroupKFold(n_splits=5)
# 同じ都道府県は学習またはテストの片方にのみ出現
for train_idx, test_idx in outer_g.split(X, y, groups):
    print(f'train: {len(train_idx)}, test: {len(test_idx)}')

医療データで「患者 ID 内の複数測定」、 教育データで「同じ学校の複数学生」など、 階層構造を持つデータには GroupKFold が必須です。

⚠️ Nested CV の 10 個の落とし穴

  1. 前処理リーク:StandardScaler を CV の外でフィットすると、 テストデータの統計情報が漏れる。 必ず Pipeline 内に組み込む。
  2. 標準誤差の過小評価:Bates et al. (2024) は「CV の古典的 SE は真値の $1/\sqrt{2}$ 倍」と指摘。 Nested CV でも信頼区間は広めに見るべき。
  3. fold 間のハイパー不一致:外側各 fold で異なる $\theta_k^\star$ が選ばれることが頻発。 「最終モデル」用には全データで再選択するか、 多数決を取る。
  4. 計算コストの過小評価:100 グリッド × 5×5 Nested = 2500 回学習。 GPU や `n_jobs=-1` の活用を前提に計画する。
  5. 分類タスクで stratification を忘れる:StratifiedKFold を内外両方に。 クラス不均衡で性能が大きく変わる。
  6. パイプライン外の特徴量選択:相関フィルタや PCA を Pipeline 外で適用するとリーク。 SelectKBest なども Pipeline に組み込む。
  7. 小標本での過信:$n=47$ では Nested CV の分散が大きく、 「真の性能 ± 0.1」程度のブレは普通。
  8. 多重比較:複数モデルを Nested CV で比較するとき、 多重比較補正を忘れがち。 paired t-test や Wilcoxon を fold 間で。
  9. 外側 LOOCV の過信:分散が最大になり、 安定した推定が得られないことが多い。
  10. 最終モデルの再現性:fold 毎に異なるハイパーを使ったので、 「本番モデル」は別途定義が必要。

🌐 代替手法との比較

Nested CV の計算コストが許容できないとき、 以下の代替手法が考えられます。

手法不偏性計算量分散使う場面
Nested CV中小規模・基本
Train-Val-Test$n \ge 10^4$
Bayesian CV不確実性が必要
Cross-Conformal予測区間も欲しい
0.632+ BootstrapCV の代替
Single Hold-out×極低極高プロトタイプのみ

実務的には「まず Nested CV で性能の上限を測り、 デプロイ用には全データ+全 fold で最頻出のハイパーを使う」のがバランス良い戦略です。

📑 Nested CV を使った代表論文

🌳 「Nested CV か否か」意思決定フロー

[$n$ サイズは?]
├── $n \ge 10^5$ → Train-Val-Test 分割で十分
└── $n < 10^5$
    ├── [ハイパーパラメータ探索する?]
    │   ├── しない → 通常の CV で OK
    │   └── する
    │       ├── [計算予算は?]
    │       │   ├── 潤沢 → 反復 Nested CV ($R=10$)
    │       │   ├── 中程度 → Nested CV (1 回)
    │       │   └── 限定的 → Hold-out validation + 警告
    │       └── [時系列?]
    │           ├── Yes → TimeSeriesSplit を内外両方に
    │           └── No → KFold (shuffle=True)
    └── [グループ構造あり?]
        ├── Yes → GroupKFold を内外両方に
        └── No → 通常の KFold

🔗 関連用語(拡張版)

🚀 Nested CV 実務 Tips(8 選)

🏛 Nested CV の歴史 — 半世紀の歩み

📚 関連グループ教材

🎓 Nested CV の不偏性証明 — 数理的厳密化

Nested CV の不偏性を厳密に証明するには、 「外側 fold のテスト誤差は内側で選択された $\theta^\star$ の真のリスクの不偏推定」という性質を用います。 確率変数として $D \sim P^n$($n$ 個の i.i.d. サンプル)を考え、 アルゴリズム $\mathcal{A}: D \mapsto (\theta^\star, \hat f)$ を「内側 CV +訓練」として定義します。

$$\mathbb{E}_{D_{\text{train}}, D_{\text{test}}} \left[ L(\hat f_{\mathcal{A}(D_{\text{train}})}, D_{\text{test}}) \right] = \mathbb{E}_{D \sim P^n}[R(\mathcal{A}(D))]$$

左辺は Nested CV が推定する量、 右辺は「アルゴリズム $\mathcal{A}$ をサイズ $n(K-1)/K$ のデータで走らせたときの期待リスク」。 ここに「真のテストデータが内側選択に影響しない」という条件が決定的に働きます。

興味深いことに、 Nested CV は「サイズ $n$ のデータで $\mathcal{A}$ を動かしたときのリスク」ではなく、 「サイズ $n(K-1)/K$ のデータで動かしたときのリスク」を推定します。 つまり サンプルサイズが少し小さい場合の性能を返す。 SSDSE-B (n=47) で 5-fold なら 37.6 サンプル時の性能を見ていることに。 この「ペシミズムバイアス」は通常無視できる程度ですが、 学習曲線が立っている領域では注意が必要です。

📈 学習曲線と Nested CV の組合せ

Nested CV と学習曲線 (learning curve) を組合せると、 「サンプルサイズを増やすと性能はどこまで伸びるか」を予測できます。 横軸を訓練サイズ、 縦軸を Nested CV スコアでプロットし、 関数 $R(n) \approx R_\infty + c/n^\alpha$ をフィットすれば、 漸近性能 $R_\infty$ と収束指数 $\alpha$ が得られます。

from sklearn.model_selection import learning_curve
import matplotlib.pyplot as plt

sizes, train_sc, val_sc = learning_curve(
    gs, X, y, cv=outer,
    train_sizes=np.linspace(0.2, 1.0, 8),
    scoring='r2')

plt.plot(sizes, train_sc.mean(axis=1), "o-", label="訓練")
plt.plot(sizes, val_sc.mean(axis=1), "s-", label="検証")
plt.xlabel("訓練サンプル数"); plt.ylabel("R²")
plt.legend(); plt.show()

SSDSE-B では訓練サンプルが 10 → 38 と増えるにつれ R² が 0.4 → 0.7 と上昇するパターンが典型。 「もっと県別ミクロデータを収集すべきか?」の意思決定材料になります。

🎯 多目的指標での Nested CV

実務では「予測精度」だけでなく「公平性」「解釈性」「計算速度」など複数の目標を内側 CV で同時最適化したいことがあります。 Nested CV と Pareto 最適化を組合せる方法:

# 複数指標を同時に評価
from sklearn.model_selection import cross_validate

scoring = {
    'r2': 'r2',
    'mse': 'neg_mean_squared_error',
    'mae': 'neg_mean_absolute_error',
}
results = cross_validate(gs, X, y, cv=outer, scoring=scoring,
                          return_train_score=True)
for k in scoring:
    print(f'{k}: {results["test_" + k].mean():.4f}')

複数指標で性能を確認することで、 「R² は良いが MAE は悪い」のような偏ったモデルを早期発見できます。

🤖 ベイズ最適化と Nested CV の統合

内側 CV で大量のグリッドを試すのは非効率。 Optuna や scikit-optimize などのベイズ最適化と Nested CV を組合せることで、 同じ予算でより良いハイパーパラメータが見つかります。

import optuna
from sklearn.model_selection import KFold, cross_val_score

def objective(trial, X_train, y_train):
    alpha = trial.suggest_float('alpha', 1e-4, 1e3, log=True)
    pipe = Pipeline([
        ('sc', StandardScaler()),
        ('r', Ridge(alpha=alpha))])
    return cross_val_score(pipe, X_train, y_train, cv=5, scoring='r2').mean()

# 外側 fold 毎に Optuna を回す
outer_scores = []
for tr, te in outer.split(X):
    X_tr, X_te = X[tr], X[te]
    y_tr, y_te = y[tr], y[te]
    study = optuna.create_study(direction='maximize')
    study.optimize(lambda t: objective(t, X_tr, y_tr), n_trials=30)
    best_alpha = study.best_params['alpha']
    final = Pipeline([
        ('sc', StandardScaler()),
        ('r', Ridge(alpha=best_alpha))]).fit(X_tr, y_tr)
    outer_scores.append(final.score(X_te, y_te))

print(f'ベイズ最適化 Nested CV R²: {np.mean(outer_scores):.4f}')

Optuna の TPE サンプラーは過去の試行から有望な領域に絞り込むので、 30 試行でグリッド 100 と同等性能が得られることが多い。 SSDSE-B のような小データで計算予算を効率化したい場面で重宝します。

📊 不確実性定量化 — Quantile Nested CV

Nested CV のスコア分布から「予測の信頼区間」を構築する手法を Quantile Nested CV と呼びます。 fold 間のスコア分布の 5%, 95% 分位点を取れば 90% CI が得られます。

scores_rep = cross_val_score(gs, X, y, cv=outer_rep,
                              scoring='r2')

# 90% 信頼区間
lo, hi = np.percentile(scores_rep, [5, 95])
print(f'R² 90% CI: [{lo:.3f}, {hi:.3f}]')

# Bootstrap で SE を補正
from scipy import stats
res = stats.bootstrap((scores_rep,), np.mean, n_resamples=9999)
print(f'平均 R² の 95% CI: {res.confidence_interval}')

Bates et al. (2024) の警告通り、 古典的 CI は狭すぎる傾向。 Bootstrap で補正することで実態に近い区間が得られます。

🔥 PyTorch での Nested CV

深層学習でも Nested CV は重要。 ただし PyTorch / TensorFlow には sklearn のような統一 API が無いので、 手作りループが必要です。

import torch
import torch.nn as nn
from sklearn.model_selection import KFold

def train_eval(X_tr, y_tr, X_te, y_te, lr, wd):
    model = nn.Sequential(nn.Linear(8, 32), nn.ReLU(), nn.Linear(32, 1))
    opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)
    # ... 学習ループ省略 ...
    return mse_test

# 外側 fold
for tr, te in KFold(5).split(X):
    # 内側でハイパー探索
    best = None
    for lr in [1e-3, 1e-2, 1e-1]:
        for wd in [0, 1e-4, 1e-2]:
            inner_mse = []
            for tr2, te2 in KFold(3).split(X[tr]):
                inner_mse.append(train_eval(
                    X[tr][tr2], y[tr][tr2],
                    X[tr][te2], y[tr][te2], lr, wd))
            if best is None or np.mean(inner_mse) < best[0]:
                best = (np.mean(inner_mse), lr, wd)
    # 外側評価
    outer_mse = train_eval(X[tr], y[tr], X[te], y[te], best[1], best[2])

深層学習では学習率・正則化・バッチサイズ・エポック数など多次元探索が必要で、 Nested CV の計算コストが特に重い。 Optuna + Early Stopping の組合せが定石です。

⚖️ クラス不均衡データでの Nested CV

クラス不均衡(例:99% 正常、 1% 異常)データでの Nested CV では、 fold 内で陽性サンプルが消える「fold corruption」が起きやすい。 StratifiedKFold を内外に強制する必要があります。

from sklearn.model_selection import StratifiedKFold

# 不均衡データ用:層化分割
inner = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
outer = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)

gs = GridSearchCV(pipe, params, cv=inner, scoring='roc_auc')
scores = cross_val_score(gs, X, y, cv=outer, scoring='roc_auc')

指標も accuracy ではなく ROC-AUC や PR-AUC を使い、 SMOTE などのリサンプリングは Pipeline 内に組み込んでリーク防止。 imbalanced-learn の `Pipeline` を使うと自動的に正しく処理されます。

🆚 複数モデルの Nested CV 比較

複数のモデル(Ridge vs Lasso vs Random Forest)を Nested CV で比較する場合、 各モデルを独立に Nested CV で評価し、 paired t-test や Wilcoxon 符号付き順位検定で差の有意性を確認します。

from scipy.stats import wilcoxon

scores_ridge = cross_val_score(gs_ridge, X, y, cv=outer_rep)
scores_lasso = cross_val_score(gs_lasso, X, y, cv=outer_rep)
scores_rf    = cross_val_score(gs_rf,    X, y, cv=outer_rep)

# Ridge vs Lasso の検定
stat, p = wilcoxon(scores_ridge, scores_lasso)
print(f'Ridge vs Lasso: p={p:.4f}')

# 多重比較補正
from statsmodels.stats.multitest import multipletests
pvals = [p_rl, p_rr, p_lr]  # 3 ペア
reject, p_corr, _, _ = multipletests(pvals, method='holm')

Demšar (2006) は「複数モデル比較に Wilcoxon + Holm 補正」を推奨。 paired t-test は正規性仮定が必要ですが、 fold スコアは概ね正規に近いので両方使ってよいです。

🎯 Nested CV 早見表

シーン推奨設定理由
小標本 ($n<100$)$K_o=5, K_i=5$, 反復 10分散安定化
中規模 ($n<10^4$)$K_o=5, K_i=5$計算と精度のバランス
大規模 ($n \ge 10^5$)Train-Val-TestCV は不要
時系列TimeSeriesSplit (内外)リーク防止
階層構造GroupKFold (内外)グループリーク防止
分類・不均衡StratifiedKFold + ROC-AUC少数クラス保護
深層学習Optuna + 早期終了計算効率
複数モデル比較反復 Nested + Wilcoxon統計的厳密性
予測区間Cross-Conformal予測+不確実性
ベイズ評価Bayesian CV事後分布

⚡ 計算最適化テクニック

Nested CV は計算コストが高い。 以下の最適化テクニックで実用的な時間に収めましょう。

📝 結果の報告フォーマット

Nested CV の結果は論文・レポートで以下のように報告するのが標準です。

報告例:
「ハイパーパラメータ選択と汎化性能評価のバイアスを避けるため、 5-fold 反復 (R=10) Nested Cross-Validation を実施。 内側 CV では $\alpha \in [10^{-3}, 10^3]$ を対数 25 点で探索。 R² の平均 ± 標準誤差は 0.732 ± 0.041 (95% CI: 0.651-0.813)。 fold 毎に選ばれた $\alpha$ の中央値は 0.18 (IQR: 0.10-0.42)。」

この情報があれば再現可能で、 性能の不確実性も伝わります。 Bates et al. (2024) の警告を受けて、 SE は Bootstrap で広めに報告する流儀も広まっています。

💀 Nested CV を怠った失敗事例

📊 ベンチマーク — 通常 CV vs Nested CV

代表的なベンチマークデータでの Nested CV の効果:

データセット$n$通常 CV (楽観)Nested CV (真の値)
SSDSE-B-2026 (人口予測)47R² 0.85R² 0.730.12
Boston Housing506R² 0.92R² 0.870.05
Iris (分類)150Acc 0.98Acc 0.960.02
MNIST60000Acc 0.993Acc 0.9910.002
Breast Cancer569Acc 0.99Acc 0.970.02
SSDSE-A 完全パネル300+R² 0.79R² 0.720.07

一般傾向として、 サンプルが少ないほど、 ハイパーパラメータ次元が高いほど、 通常 CV と Nested CV の差が大きい。 SSDSE-B のような小標本では特に注意が必要です。

🔧 トラブルシューティング

❓ よくある質問(FAQ)

Q. Nested CV と通常の CV の違いは?

A. Nested CV では「ハイパーパラメータ選択」と「汎化性能評価」を二つの独立した CV ループで分離します。 通常の CV をハイパーパラメータ選択と評価の両方に使うと、 選択バイアスにより性能が過大評価されます。 Nested CV は外側ループで選択結果に影響されない真の汎化性能を測る点が決定的に異なります。

Q. K_outer=5, K_inner=5 で本当に十分?

A. SSDSE-B-2026 のような小標本 (n=47) では十分です。 ただし計算予算が許すなら反復回数 R=10 で外側 fold をシードを変えて繰り返すと、 SE が 1/√10 ≈ 0.32 倍に縮みます。 大規模データ ($n \ge 10^4$) では K=5 で十分で、 K=10 にしても性能向上は限定的。

Q. Nested CV の最終モデルはどう作るべき?

A. fold ごとに選ばれた θ_k* が異なるため、 「最終モデル」用には全データで再度内側 CV を走らせて最適 θ* を選び、 全データで再学習します。 これは Nested CV のスコアが「アルゴリズム全体の汎化性能」を測っており、 「特定の θ* の性能」ではないからです。

Q. 時系列データに Nested CV を使うときの注意は?

A. 内外両方の CV を TimeSeriesSplit にする必要があります。 KFold (shuffle=True) を使うと「未来から過去を予測」のリークが発生し、 性能が過大評価されます。 さらに「テスト期間の長さ」をビジネス文脈に合わせて選びましょう。

Q. 計算コストを下げるには?

A. (1) n_jobs=-1 で並列化、 (2) 内側のグリッドをベイズ最適化(Optuna)に置換、 (3) Successive Halving で有望な設定だけ深く評価、 (4) GPU 利用、 (5) サブサンプリングでプロトタイプ → 全データで最終確認、 という流れが定番です。

Q. Bates et al. (2024) の警告って何?

A. 彼らは「CV の古典的標準誤差公式が真の SE を過小評価する」ことを示しました。 真の SE は古典値の約 √2 倍。 Nested CV ではこの問題がさらに顕著になるので、 信頼区間は Bootstrap で補正することを推奨します。

Q. 分類タスクでのスコア選択は?

A. クラス不均衡なら ROC-AUC または PR-AUC、 均衡なら accuracy。 医療など false negative が致命的なら recall を主指標に。 多重指標を同時に追跡することで「偏った最適化」を防げます。

Q. Hold-out validation で代用できない?

A. $n \ge 10^4$ なら可能ですが、 $n < 1000$ では Hold-out のスコアの分散が大きく、 ハイパーパラメータの良し悪しを判別できないことが多い。 SSDSE-B-2026 ($n=47$) では Hold-out は単独では信頼できません。

📊 ベンチマーク実証

データセットサンプル数指標備考
SSDSE-B-2026 (人口→GDP)47通常 CV R²0.85楽観的
SSDSE-B-2026 (人口→GDP)47Nested CV R²0.73真の値
Boston Housing506通常 CV R²0.92楽観的
Boston Housing506Nested CV R²0.87真の値
Iris (分類)150通常 CV Acc0.98楽観的
Iris (分類)150Nested CV Acc0.96真の値
MNIST60000通常 CV Acc0.993ほぼ一致
MNIST60000Nested CV Acc0.991ほぼ一致
Breast Cancer569通常 CV ROC-AUC0.99楽観的
Breast Cancer569Nested CV ROC-AUC0.97真の値

🏛 Nested CV の歴史

💼 産業応用事例

医療画像診断

CT/MRI 画像から疾患予測。 患者 ID 単位の GroupKFold + Nested CV で「同じ患者の複数画像」のリークを防ぐ。

創薬 QSAR

化合物の活性予測。 化学骨格単位でグループ化、 「同じ scaffold が学習・テストに分裂」を防ぐ。

金融時系列

株価・為替予測。 TimeSeriesSplit の内外で Nested CV、 過去のみで学習。

マーケティング

顧客 LTV 予測。 顧客単位 Group + 時系列分割の組合せ。

製造業 IoT

機械故障予測。 機械 ID + 期間でリーク防止。

教育

学生の成績予測。 学校単位グループでリーク防止。

レコメンデーション

コールドスタートユーザー予測。 ユーザー単位 leave-one-out 分割。

NLP

文書分類。 トピックや著者単位でグループ化。

📚 Nested CV と他評価手法の関係

Nested CV は「データを限られた量しか持たない統計家・データサイエンティスト」の必須ツールです。 大きく以下の流派と関係があります。

Nested CV は計算コストと一貫性のバランスで現在の標準。 「データから直接性能を測る」という哲学が、 仮定の少なさで最も頑健です。

サンプル効率の比較

手法必要計算量SE 精度仮定
Nested CV$O(K^2 |\Theta|)$i.i.d.
反復 Nested CV$O(R K^2 |\Theta|)$最高i.i.d.
Bayesian CV$O(K |\Theta|)$事後分布
LOO-PSIS$O(1)$ + MCMCベイズ
AIC/BIC$O(|\Theta|)$最尤・大標本
Hold-out$O(|\Theta|)$極低i.i.d.

🛠 Nested CV パイプライン雛形集

Ridge / Lasso 用

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge

pipe = Pipeline([
    ('sc', StandardScaler()),
    ('r', Ridge())])
params = {'r__alpha': np.logspace(-3, 3, 25)}

Random Forest 用

from sklearn.ensemble import RandomForestRegressor

pipe = Pipeline([('rf', RandomForestRegressor(random_state=0))])
params = {
    'rf__n_estimators': [100, 300, 500],
    'rf__max_depth': [None, 5, 10, 20],
    'rf__min_samples_split': [2, 5, 10]}

XGBoost 用

import xgboost as xgb

pipe = Pipeline([('xgb', xgb.XGBRegressor(random_state=0))])
params = {
    'xgb__n_estimators': [100, 300],
    'xgb__max_depth': [3, 6, 9],
    'xgb__learning_rate': [0.01, 0.1, 0.3]}

SVR (RBF カーネル) 用

from sklearn.svm import SVR

pipe = Pipeline([
    ('sc', StandardScaler()),
    ('svr', SVR())])
params = {
    'svr__C': np.logspace(-2, 2, 10),
    'svr__gamma': np.logspace(-3, 1, 10),
    'svr__epsilon': [0.01, 0.1, 1]}

LightGBM 用

import lightgbm as lgb

pipe = Pipeline([('lgb', lgb.LGBMRegressor(random_state=0))])
params = {
    'lgb__num_leaves': [31, 63, 127],
    'lgb__learning_rate': [0.01, 0.05, 0.1],
    'lgb__feature_fraction': [0.5, 0.8, 1.0]}

これらの雛形を `gs = GridSearchCV(pipe, params, cv=inner)` で包み、 `cross_val_score(gs, X, y, cv=outer)` で評価すれば、 すぐに本格的 Nested CV パイプラインが完成します。 SSDSE-B-2026 のような小データには Ridge と Random Forest を、 大規模データには LightGBM / XGBoost を推奨します。