🔖 キーワード索引
日本語名・英語名・別名・略語・混同しやすい語をまとめる。 同義の表記は論文や Kaggle notebook で頻繁に登場するので、 検索時の手がかりとして全て押さえておく。
日本語名 :入れ子型クロスバリデーション、 ネステッド CV、 二重 CV、 入れ子検証
英語名 :Nested Cross-Validation、 Nested CV、 Double Cross-Validation、 Outer-Inner CV
別名・略語 :NCV、 nCV、 outer/inner CV、 hyperparameter-tuned CV、 unbiased CV estimator
混同しやすい語 :通常の K-fold CV(ハイパラ調整を含まず性能評価のみ)、 hold-out 法(1 回分割)、 Grid Search CV(内側 CV のみで外側評価なし)、 bootstrap 法(復元抽出ベース)
主要文脈 :scikit-learn の cross_val_score(GridSearchCV(...))、 Kaggle 公開ノート、 Cawley & Talbot 2010 JMLR、 Varma & Simon 2006 BMC Bioinformatics
アンカーリンクで本ページ内をジャンプ:
📍 文脈 /
💡 30秒結論 /
🎨 直感 /
📐 数式 /
🔬 読み解き /
🧮 実値計算 /
🐍 Python /
⚠️ 落とし穴 /
🌐 派生 /
🔗 関連 /
📚 教材 /
🗺 概念マップ
💡 30秒で分かる結論
🍰 まずはやさしく
二重の枠組みで評価する方法です。
正しい性能を測るために使います。
部活の練習と本番を分ける感覚です。
この手法の結論を短くまとめます。
定義 :外側 CV ループ(性能評価)と内側 CV ループ(ハイパラ調整)を入れ子にする、 機械学習の評価プロトコル
解く問題 :「同じ CV を性能評価とハイパラ調整に二重使いすると、 テスト fold が間接的に訓練に漏洩 し、 汎化性能が過大評価される」というバイアス
外側ループ :データを K_outer 個に分割。 各 fold で性能を測る
内側ループ :外側の訓練部分を K_inner 個に分割し、 ハイパラ探索(GridSearch、 RandomSearch、 Bayes など)
典型的設定 :K_outer = 5 または 10、 K_inner = 3 または 5
計算量 :K_outer × K_inner × (ハイパラ候補数)回の学習。 普通の CV の数十倍だが、 結果の信頼性が桁違い
代替案 :データが大きいときは「train / val / test の 3 分割(ホールドアウト)」で十分。 Nested CV は小〜中規模データ で真価を発揮
📍 あなたが今見ているもの
🍰 まずはやさしく
専門的な書き方のルールです。
分析の結果を正しく伝えるために使います。
スマホのアプリの評価画面のようなものです。
実際の表記例と意味を解説します。
機械学習の論文や 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 」を入れ子にすることで、 このバイアスを除去する標準的な方法論です。
🎨 直感で掴む — 「同じ問題集で勉強して同じ問題集でテストする」問題
🍰 まずはやさしく
テストのカンニングを防ぐ仕組みです。
本当の実力を知るために使います。
同じ問題集で勉強して試験を受ける例です。
なぜこの方法が必要か直感的に説明します。
🎯 ナイーブ CV のバイアス
普通の交差検証(CV)でモデル選択と性能評価を同時に行うと、 こんなことが起きます:
K=5 fold に分割
各 α ∈ {0.01, 0.1, 1, 10} で 5 fold CV の平均 RMSE を計算
最も RMSE が小さい α を「最適 α」として選ぶ
その最適 α での 5 fold CV 平均 RMSE を「最終性能」として報告
⚠️ 問題 :手順 4 の「最終性能」は同じ fold 構造での CV 平均なので、 すでに「この fold でうまくいく α」を選んだ後の評価です。 つまりテストデータが間接的に α 選択に使われた 。 結果、 報告される RMSE は実環境(真に未知のデータ)よりも小さく(楽観的に)出る。
🎓 喩え:模試と本番試験
大学受験で、 「直前模試の問題」と「本番試験の問題」が同じだったら、 模試で「この勉強法が一番点数が出る」と判明した方法は、 本番でも当然点数が出る。 でもそれは「本当に頭が良いから」ではなく、 「答えを知っていたから」 です。
Nested CV の発想:
外側 fold :本番試験(真の未知データの代理)
内側 fold :模試(外側の訓練データだけで勉強法 = ハイパラを決める)
外側 fold で測った性能は、 内側で勉強法を決めたことを知らずに 受けた本番試験の点数 = 真の汎化性能の不偏推定
📊 nested CV vs naive CV の数字感
典型的な経験則:
シナリオ naive CV の RMSE Nested CV の RMSE 差(バイアス)
ハイパラ 4 候補、 n=200 0.85 0.92 +0.07(楽観バイアス)
ハイパラ 20 候補、 n=200 0.78 0.95 +0.17(さらに楽観)
ハイパラ 100 候補、 n=200 0.72 0.99 +0.27(致命的)
ハイパラ 100 候補、 n=10,000 0.85 0.86 +0.01(無視できる)
つまり (1) ハイパラ候補が多いほど、 (2) サンプルが少ないほど、 naive CV のバイアスは大きく なる。 Kaggle で「CV では当たったのに、 LB(leaderboard)でガッカリ」というのは多くの場合これが原因。
🎨 概念図で押さえる
Nested CV は 「外側 CV で性能評価、 内側 CV でハイパラ選択」 を分離する二重ループ構造。 以下 3 つの図で「二重ループ」「選択バイアス回避」「単純 CV との誤差差」を視覚化する。
図 1: 二重ループの構造
外側 CV: 5-fold (性能評価用)
Outer Fold 1
Outer Fold 2
Outer Fold 3
Outer Fold 4
Outer 5
内側 CV: 5-fold (ハイパラ選択)
In1
In2
In3
In4
→ best_C, best_gamma 決定
外側ループは合計 5 回
内側ループは外側ごと 5 回
合計 5 × 5 = 25 回 fit
→ 外側でモデル評価、 内側でハイパラ調整 。 計算コストは単純 CV の k 倍だが、 選択バイアスを排除できる。
図 2: 選択バイアスの発生メカニズム
❌ 単純 GridSearchCV
① 同じ CV データでハイパラ探索
② best_score_ を報告
→ そのスコアは「最大化された値」
→ 楽観的バイアス (0.05-0.1 過大)
例: best_score_ = 0.064
真の汎化性能 ≈ -0.05
差 = 0.116 が選択バイアス
✅ Nested CV
① 内側でハイパラ選択
② 外側で性能評価 (独立)
→ 「未使用データ」での評価
→ ほぼ偏りなし
例: nested_score = -0.052
真の汎化性能 ≈ -0.05
差 ≈ 0 (誤差範囲内)
→ 単純 CV は 「同じデータでハイパラ選んで同じデータで評価」 するため楽観的。 Nested CV は内側と外側を分離するため公正な評価ができる。
図 3: ハイパラ空間と内側 CV の役割
log C
accuracy
内側 CV の最良点
C=1.0
候補
候補
内側 CV の役割
「外側に何も見せず」
最良 C を 1 つ返すだけ
→ 内側 CV はハイパラを 1 つ返すブラックボックス 。 外側はそれを受け取って独立データで評価するだけなので、 ハイパラ探索の楽観的バイアスから守られる。
🎨 補足図で押さえる(サンプルサイズ・正則化・残差の視点)
図 4: Nested CV の核心は「内側 CV でハイパラ選択、 外側 CV で性能評価」の二段構造。 標本サイズと検証分割の関係を示す。
図 5: 単一の CV でハイパラを選んで同じデータで評価すると楽観的バイアスが入る。 Nested CV はその過学習を防ぐ。
図 6: 外側 CV の各 fold で計算された残差を集計することで汎化誤差の不偏推定が得られる。
📐 数式 — 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$ について:
内側ループ(外側の各 fold k 内で)
$\mathcal{D}_{\text{train}}^{(k)}$ を $K_i$ 個に分割し、 各 $\lambda \in \Lambda$ について:
最適ハイパラを選択:
外側ループでの性能評価
$\hat{\lambda}^{(k)}$ を使って外側の訓練データ全体で再学習し、 外側テストで評価:
最終的に外側 fold での平均と標準偏差を報告:
計算コスト
📐 アルゴリズム手順 — 二重ループの詳細
Nested CV は二重ループ構造を取ります。 外側ループで $K_{\text{outer}}$ 個の fold を作り、 各 fold で「テストデータを完全に隔離」。 内側ループで残りの訓練データを $K_{\text{inner}}$ 個に分け、 ハイパーパラメータを選択。 選ばれたハイパーパラメータで訓練データ全体で再学習し、 隔離されたテストデータで評価。 これを $K_{\text{outer}}$ 回繰り返した平均が「不偏」な汎化性能推定値となります。
Step 1 :データ $D$ を外側 $K_{\text{outer}}$ 分割 → $D_1^{\text{out}}, \ldots, D_{K_{\text{outer}}}^{\text{out}}$Step 2 :各外側 fold $k$ について:訓練データ $D \setminus D_k^{\text{out}}$ を取り出すStep 3 :訓練データを内側 $K_{\text{inner}}$ 分割 → 内側 CV でハイパーパラメータ $\theta_k^\star$ を選択Step 4 :訓練データ全体で $\theta_k^\star$ を用いてモデルを学習し、 $D_k^{\text{out}}$ で性能を測るStep 5 :外側 $K_{\text{outer}}$ 個の性能スコアを平均し SE を計算Step 6 :最終モデルは全データで再学習(ただし $\theta_k^\star$ は fold 毎に異なるので、 「合意」を取るか全データで再選択)重要なのは、 外側 fold のテストデータ $D_k^{\text{out}}$ は内側 CV のハイパーパラメータ選択に一切使われない こと。 これにより「データを見てから選んだ」というバイアスが排除されます。
🎯 $K_{\text{outer}}$ と $K_{\text{inner}}$ の選び方
$K_{\text{outer}}$ $K_{\text{inner}}$ 計算量倍率 バイアス 分散 推奨場面 5 5 25× 低 中 デフォルト・$n \ge 100$ 10 5 50× 極低 高 小標本 $n < 100$ 10 10 100× 極低 低 計算予算潤沢時 $n$ (LOO) 5 $5n$× 極低 極高 $n \le 50$ のとき 3 3 9× 中 低 高速プロトタイプ用 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$ 倍に縮みます。
🔬 数式を「言葉」で読み解く
💡 重要な性質 :Nested CV は 「最終的にデプロイするモデル」を生成する手続きではない こと。 あくまで「ハイパラチューニング + 学習のパイプライン全体の性能 を測る」プロトコル。 デプロイには別途、 全データで内側 CV を再実行して最適 λ を決め、 全データで学習する。
🔬 数式を言葉で読み解く(補講: ネスト CV の実装ハンズオン)
ネスト交差検証は「ハイパーパラメータ選択」と「汎化性能推定」を分離するための二重ループ構造である。 ここでは SSDSE-B-2026 を用いて、 内側ループでチューニング、 外側ループで純粋な汎化誤差を測定する流れを 4 つの Python ブロックで具体化する。
▶ ブロック 1: SSDSE-B-2026 の読込と特徴量の準備
このコードでやること : SSDSE-B-2026(2023 年・47 都道府県)を読み込み、 実在コード列(A1101=総人口/A1303=65歳以上人口/A4101=出生数/B4101=年平均気温)を特徴量、 L3221=消費支出(二人以上世帯)を目的変数とするデータセットを準備する。 ネスト CV を試す土台になる。
📥 入力データ (抜粋):
SSDSE-B-2026, Code, Prefecture, A1101, A1303, A4101, B4101, L3221, ...(2023 年 47 行)
R01000, 北海道, A1101=5092000, A1303=1681000, A4101=24430, B4101=11.0, L3221=296888
R13000, 東京都, A1101=14086000, A1303=3205000, A4101=86348, B4101=17.6, L3221=341320
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14 import pandas as pd
from sklearn.preprocessing import StandardScaler
# SSDSE-B-2026 を読込(cp932・2 行目の日本語ヘッダを skiprows=[1] で除外)
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ] . copy () # 最新年(47 都道府県)
feat = [ 'A1101' , 'A1303' , 'A4101' , 'B4101' ] # 総人口/65歳以上人口/出生数/年平均気温
y = df [ 'L3221' ] . values # 目的変数: 消費支出(二人以上世帯)
X = df [ feat ] . fillna ( df [ feat ] . median ()) . values
X = StandardScaler () . fit_transform ( X )
print ( f 'X shape: { X . shape } , y shape: { y . shape } ' )
print ( f '目的変数 y (L3221 消費支出) range: { y . min () } - { y . max () } ' )
📤 実行例:
X shape: (47, 4), y shape: (47,)
目的変数 y (L3221 消費支出) range: 223423 - 344092
💬 47 県 × 4 特徴量の小規模データ。 サンプル数が少ないため通常の Hold-out では汎化評価が不安定で、 ネスト CV の出番である。
▶ ブロック 2: 外側ループ + 内側 GridSearchCV (Ridge)
このコードでやること : 外側 5-fold、 内側 3-fold でネスト CV を実行。 内側で Ridge の α をチューニングし、 外側で各 fold の RMSE を集計する。
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14 import numpy as np
from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold , GridSearchCV , cross_val_score
inner = KFold ( n_splits = 3 , shuffle = True , random_state = 0 )
outer = KFold ( n_splits = 5 , shuffle = True , random_state = 0 )
param_grid = { 'alpha' : [ 0.01 , 0.1 , 1 , 10 , 100 ]}
gs = GridSearchCV ( Ridge (), param_grid , cv = inner ,
scoring = 'neg_root_mean_squared_error' )
scores = cross_val_score ( gs , X , y , cv = outer ,
scoring = 'neg_root_mean_squared_error' )
print ( f '外側 RMSE 各 fold: { ( - scores ) . round ( 1 ) } ' )
print ( f '外側 RMSE 平均 ± SD: { - scores . mean () : .1f } ± { scores . std () : .1f } ' )
📤 実行例:
外側 RMSE 各 fold: [18784.1 24580.5 20973.6 32305.4 20055.4]
外側 RMSE 平均 ± SD: 23339.8 ± 4879.8
💬 消費支出(円)の 5 fold 平均 RMSE 約 23,300 円。 各 fold で α が個別に最適化されているため、 単純な GridSearchCV より楽観的でない正直な汎化指標。
▶ ブロック 3: 通常の CV と比較し「楽観バイアス」を可視化
このコードでやること : 同じデータで非ネスト CV (GridSearchCV のみ) を回し、 ネスト CV の結果と比較。 楽観バイアスがどの程度生じるかを定量化する。
📋 コピー from sklearn.model_selection import GridSearchCV
gs_flat = GridSearchCV ( Ridge (), param_grid , cv = outer ,
scoring = 'neg_root_mean_squared_error' )
gs_flat . fit ( X , y )
flat_rmse = - gs_flat . best_score_
nested_rmse = - scores . mean ()
print ( f '非ネスト CV RMSE (best score): { flat_rmse : .1f } ' )
print ( f 'ネスト CV RMSE 平均: { nested_rmse : .1f } ' )
print ( f '楽観バイアス (差分): { nested_rmse - flat_rmse : .1f } ' )
📤 実行例:
非ネスト CV RMSE (best score): 22431.2
ネスト CV RMSE 平均: 23339.8
楽観バイアス (差分): 908.7
💬 非ネスト CV はデータをチューニングと評価の双方に使うため、 RMSE が約 900 円分楽観的に(小さく)出る。 ネスト CV は計算コストが 5×3=15 倍になるが、 「本当の汎化誤差」を正直に出してくれる。
⚠️ 環境依存の注記(実測・再現条件) :上の RMSE 数値は Python 3・scikit-learn 1.9・NumPy 2.3 、 seed 固定(内側 random_state=0/外側 random_state=0)、 ブロック 1 で特徴量を標準化した X を用いた当環境の実測値です(外側 fold RMSE = [18784.1, 24580.5, 20973.6, 32305.4, 20055.4]、 平均 23339.8 ± 4879.8、 非ネスト 22431.2、 楽観バイアス +908.7、 fold 別 best α = [10, 10, 100, 100, 0.01])。 重要 :この楽観バイアスは標準化を行った場合 に現れます。 標準化せず生の特徴量(総人口など桁の大きい列)のまま同じ α グリッドを回すと、 内側 CV は常に最大の α=100 を選び(正則化が実質効かないため)、 fold 別 α は [100, 100, 100, 100, 100] で固定、 非ネスト=ネスト=22634.5 円となりバイアスはほぼ 0 に潰れます。 「弱い探索(α 5 候補)× 小標本(n=47)× α 選択が飽和する条件」では楽観バイアスは小さく、 逆に標準化して α 選択が実際に fold ごとに動く条件でこそ楽観バイアスが顕在化 する、 というのが当環境の実測から読み取れる教訓です。
▶ ブロック 4: fold ごとの選択 α を取り出して安定性を診る
このコードでやること : 外側 fold ごとに内側で選ばれた α を確認。 fold をまたいで α が大きくぶれるなら「不安定なチューニング」のサインで、 特徴量選定の再考が必要。
📋 コピー from sklearn.model_selection import KFold
selected_alphas = []
for tr , te in outer . split ( X ):
gs2 = GridSearchCV ( Ridge (), param_grid , cv = inner ,
scoring = 'neg_root_mean_squared_error' )
gs2 . fit ( X [ tr ], y [ tr ])
selected_alphas . append ( gs2 . best_params_ [ 'alpha' ])
print ( f '外側 fold ごとの best α: { selected_alphas } ' )
print ( 'α の分布が一致 → チューニング安定、 ばらつく → 不安定' )
📤 実行例:
外側 fold ごとの best α: [10, 10, 100, 100, 0.01]
α の分布が一致 → チューニング安定、 ばらつく → 不安定
💬 5 fold 中 fold ごとに α が 10・10・100・100・0.01 と 4 桁も揺れており、 サンプル数 47 に対してハイパラ選択が不安定なサイン。 これは「naive CV の 1 個の best α」だけを見ていては決して気づけない情報で、 特徴量の見直しや反復 Nested CV を検討すべき合図。 交差検証 や ハイパーパラメータ調整 と組み合わせる際に確認したい指標。
🧮 実値で計算してみる — SSDSE-B 消費支出予測タスク
タスク設定
SSDSE-B-2026 の 47 都道府県データ(2023 年)から、 実在コード列 (A1101=総人口、 A1303=65歳以上人口、 A4101=出生数、 B4101=年平均気温) を特徴量にして、 L3221=消費支出(二人以上世帯)を予測 するリッジ回帰モデルを構築。 Ridge の正則化強度 α を Nested CV でチューニング。
STEP 1:データ構造とサイズ
対象:47 都道府県(2023 年、 cp932・skiprows=[1] で読込) = 47 サンプル
特徴量:4 列(A1101 / A1303 / A4101 / B4101)
ターゲット:L3221 消費支出(連続値、 円)
ハイパラ候補:α ∈ {0.01, 0.1, 1, 10, 100} の 5 候補
STEP 2:nested CV の設定
外側 K_o = 5 → 各 fold ≈ 9 サンプル
内側 K_i = 3
総学習回数 = 5 × 3 × 5 + 5 = 80 回(リッジ回帰なので一瞬)
STEP 3:計算結果(実データ)
外側 fold 選ばれた α 外側 RMSE(円)
1 10 18784.1
2 10 24580.5
3 100 20973.6
4 100 32305.4
5 0.01 20055.4
平均 — 23339.8 ± 4879.8
解釈 :
「最適 α」は fold によって違う(0.01, 10, 100 で揺れる)→ ハイパラ選択自体が不安定であることが明示される
外側 fold 間で RMSE が 18,784〜32,305 と大きく変動 → 「実環境では性能が大きくブレる可能性」を警告
これらは naive CV だと見えない真実
📊 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% 程度 性能を楽観的に評価することを実証しました。
論文の核心メッセージ
モデル選択は、 それ自体が「学習」である 。 ハイパラを CV で最適化する手順は、 そのハイパラ空間に対するメタ学習。 学習であるからには過学習が起こりうる
同じデータで「メタ学習」と「最終評価」を行うと、 評価がバイアスを受ける 。 これは Vapnik の VC 理論や Bonferroni 補正と同じ「多重比較問題」の現れ
解決策は 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 < 50 LOOCV(K = n) 1 fold ≈ 1 サンプル、 fold 間の分散は大きいが、 訓練データが最大化される
50 ≤ n < 500 10-fold 慣習的なベストプラクティス。 fold 間の分散と訓練データ量のバランス
500 ≤ n < 10,000 5-fold or 10-fold 計算量と精度のバランス。 5-fold の方が高速
n ≥ 10,000 Holdout(train/val/test)でも十分 Nested CV は計算量で割に合わない
内側 K_i の選び方
慣習的に K_i = 3 または 5
K_i を増やすほど、 内側でのハイパラ選択が安定するが、 計算量は K_i 倍に
K_i = 3 が「コスト効率」のスイートスポット
ハイパラ空間が大きい(Bayes 最適化など)場合は K_i = 3、 グリッドサーチで小さい場合は K_i = 5
計算コストの目安
SVM のように学習に O(n²) 〜 O(n³) かかるアルゴリズムでは:
5×3 nested CV + 5 ハイパラ → 75 回の学習 → n=1,000 で数分
10×5 nested CV + 10 ハイパラ → 500 回の学習 → n=1,000 で数十分
ニューラルネット(1 回数時間)では nested CV は非現実的 → holdout で代替
🧮 数式に値を入れて手で計算する: ネスト CV のフィット数
合成データで外 K=5、 内 K'=3、 グリッド 10 候補の総フィット数を計算する。
Step 1: 構造
外ループ: 5 fold (各 train+val)
内ループ: 3 fold × 10 グリッド = 30 フィット
外ループ最終モデル: 1 フィット (best param で full train)
Step 2: 合計
内ループ合計 = 5 × 30 = 150
外ループ最終 = 5
全フィット数 = 150 + 5 = 155
🐍 Python で再現
📋 コピー outer = 5
inner = 3
grid = 10
inner_total = outer * inner * grid
outer_final = outer
total = inner_total + outer_final
print ( f "内ループ: { inner_total } " )
print ( f "外ループ最終: { outer_final } " )
print ( f "合計: { total } " )
📤 実行結果
内ループ: 150
外ループ最終: 5
合計: 155
💬 手計算 (Step 2) 155 と Python 出力が完全一致。
🐍 Python 実装 — sklearn での Nested CV
方法 A:シンプルな手動実装(教育用)
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) B4101(年平均気温) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 1,681,000 24,430 11.0 296,888
東京都 14,086,000 3,205,000 86,348 17.6 341,320
沖縄県 1,468,000 350,000 12,549 23.8 251,222
…(全 47 行)
📋 コピー 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 # 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
# データ読込(cp932・日本語ヘッダ行を skiprows=[1] で除外)
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ] . copy () # 最新年(47 都道府県)
# 特徴量: 実在コード列(総人口/65歳以上人口/出生数/年平均気温)
feature_cols = [ 'A1101' , 'A1303' , 'A4101' , 'B4101' ]
X = df [ feature_cols ] . fillna ( df [ feature_cols ] . median ()) . values . astype ( float )
y = df [ 'L3221' ] . values . astype ( float ) # 目的変数: 消費支出(二人以上世帯)
print ( f 'サンプル数: { len ( y ) } , 特徴量数: { X . shape [ 1 ] } ' )
# Nested CV
alpha_grid = [ 0.01 , 0.1 , 1 , 10 , 100 ]
outer_cv = KFold ( n_splits = 5 , shuffle = True , random_state = 0 )
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 = 0 )
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 ' \n Nested CV RMSE: { np . mean ( outer_rmses ) : .2f } ± { np . std ( outer_rmses , ddof = 1 ) : .2f } ' )
print ( f '選ばれた α 群: { chosen_alphas } ' )
📤 実行例(実測)
サンプル数: 47, 特徴量数: 4
Fold 1: α=10, outer RMSE=18721.11
Fold 2: α=10, outer RMSE=24578.56
Fold 3: α=100, outer RMSE=21023.19
Fold 4: α=100, outer RMSE=32081.96
Fold 5: α=0.01, outer RMSE=19905.63
Nested CV RMSE: 23262.09 ± 5394.96
選ばれた α 群: [10, 10, 100, 100, 0.01]
方法 B:sklearn の GridSearchCV + cross_val_score(推奨)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 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 } ' )
📤 実行例(実測)
Nested CV RMSE: 21917.41 ± 7686.36
方法 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_ がデプロイ用モデル
📤 実行例(実測)
最終モデル α: {'ridge__alpha': 0.1}
方法 D:層化 / 時系列対応の nested CV
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 1,681,000 24,430 296,888
東京都 14,086,000 3,205,000 86,348 341,320
沖縄県 1,468,000 350,000 12,549 251,222
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 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
# ── この抜粋で使うデータを用意します ──
import pandas as pd
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ] . copy () # 最新年(47 都道府県)
X = df [[ 'A1101' , 'A1303' , 'A4101' ]] . to_numpy ( dtype = float )
y = df [ 'L3221' ] . to_numpy ( dtype = float )
# 列名は 'pref' ではなく 'Prefecture'
groups = df [ 'Prefecture' ] . values # 都道府県名
outer_cv = GroupKFold ( n_splits = 5 )
for tr , te in outer_cv . split ( X , y , groups = groups ):
# 同じ都道府県は train/test に分かれない(リーク防止)
pass
方法 E:MLxtend ・ Optuna との組合せ
📋 コピー 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 # Optuna でベイズ最適化を内側に組み込む例
# X, y, groups, outer_cv は「方法 D」で用意したものをそのまま使う
import numpy as np
import optuna
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import cross_val_score
optuna . logging . set_verbosity ( optuna . logging . WARNING )
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 , chosen = [], []
# GroupKFold は groups を渡さないと分割できない(渡さないと ValueError)。
# 方法 D で作った「都道府県」の groups をそのまま外側の分割に使う。
for tr_idx , te_idx in outer_cv . split ( X , y , groups = groups ):
study = optuna . create_study ( direction = 'minimize' ,
sampler = optuna . samplers . TPESampler ( seed = 42 ))
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' ]
chosen . append ( round ( best_alpha , 3 ))
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 ( '外側 fold ごとの best alpha:' , chosen )
print ( f 'Bayes-Nested CV: { np . mean ( outer_rmses ) : .2f } ± { np . std ( outer_rmses , ddof = 1 ) : .2f } ' )
📤 実行結果 :
外側 fold ごとの best alpha: [0.009, 0.017, 8.29, 0.024, 0.017]
Bayes-Nested CV: 22872.82 ± 4287.21
💬 結果の読み方 : 内側をグリッド探索からベイズ最適化 (TPE, 20 試行) に替えても、 外側 RMSE は 22872.82 ± 4287.21 で、 グリッド版の 23339.8 ± 4879.8 とほぼ同じ。 入れ子交差検証が測っているのは「探索アルゴリズムを含む手続き全体」の汎化性能 なので、 探索が賢くなっても外側の推定値が勝手に良くなるわけではない、 という点が重要。 ここで外側 RMSE だけが下がったなら、 それは性能向上ではなく外側テストへの漏れを疑うべき合図になる。 なお α は fold ごとに 0.009〜8.29 とばらついており、 このデータでは正則化の強さが あまり効いていない (対数スケールで動かしてもスコアが平坦)ことを示している。
🐍 Python 実装 — sklearn での完全ワークフロー
sklearn の `GridSearchCV` を `cross_val_score` で包むのが標準パターン。 内側 CV は GridSearch 内で自動的に走り、 外側は cross_val_score が制御します。
🎯 解説: SSDSE-B-2026 の都道府県データで Ridge の alpha を Nested CV で選び、 不偏な汎化性能を推定する標準ワークフロー。 内側 5-fold で 25 個のグリッドを探索し、 外側 5-fold で評価する。
📥 入力例: data/raw/SSDSE-B-2026.csv(cp932・skiprows=[1]・2023 年 47 行)
X = df[['A1101','A1303','A4101','B4101']] # 総人口/65歳以上/出生数/気温
y = df['L3221'] # 目的変数: 消費支出(二人以上世帯)
📋 コピー 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 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) 実データ読み込み(cp932・日本語ヘッダ行を skiprows=[1] で除外)
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ] . copy () # 最新年(47 都道府県)
cols = [ 'A1101' , 'A1303' , 'A4101' , 'B4101' ] # 総人口/65歳以上/出生数/気温
X = df [ cols ] . fillna ( df [ cols ] . median ()) . values
y = df [ 'L3221' ] . values . astype ( float ) # 目的変数: 消費支出(二人以上世帯)
# 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 } ' )
📤 出力例: Nested CV R²: -0.052 ± 0.202
外側 5 fold の平均と標準偏差で、 真の汎化性能を不偏推定。
💬 ポイント: 単純 GridSearchCV の best_score_ (0.064) より Nested CV (-0.052) のほうが小さい。 この差約 0.116 が「選択バイアス」の正体。 消費支出は 4 指標だけでは説明力がほぼ無く(R²≈0)、 naive CV はそれを 0.064 と楽観視してしまうが、 Nested CV は「真の説明力はゼロ」という現実を暴く。
上記コードでは内側 25 個のグリッド × 5 fold = 125 回学習を外側 5 fold で行うので、 計 625 回モデル学習。 SSDSE-B-2026 規模なら 10 秒程度で完了します。
⚠️ 環境依存の注記(実測・再現条件) :上の R² 数値は Python 3・scikit-learn 1.9・NumPy 2.3 、 seed 固定(内側 random_state=1/外側 random_state=0)、 特徴量を Pipeline 内で標準化した条件での 当環境の実測値 です。 非ネスト(単純 GridSearchCV)の best_score_ = 0.064 、 ネスト R² = -0.052 ± 0.202 、 選択バイアス(非ネスト − ネスト)= 約 0.116 。 n=47 の小標本かつ R²≈0 の弱い信号のため fold 分割・sklearn バージョン・標準化の有無で小数第 2〜3 位は変動します。 「非ネスト CV が楽観的(値を大きめに見せる)」という符号と方向 は当環境でも再現しています。
🔁 反復 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 ]) } ' )
📤 実行例(実測)
反復 Nested CV R²: -0.225 ± 0.497
スコア分布: [-0.48367967 -0.08621448 0.08707914]
反復回数 $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 } ' )
📤 実行例(実測)
時系列 Nested CV MSE: 627753348.2700
グループ分割 📋 コピー 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 ) } ' )
📤 実行例(実測)
train: 37, test: 10
train: 37, test: 10
train: 38, test: 9
train: 38, test: 9
train: 38, test: 9
医療データで「患者 ID 内の複数測定」、 教育データで「同じ学校の複数学生」など、 階層構造を持つデータには GroupKFold が必須です。
⚠️ 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 の 10 個の落とし穴
前処理リーク :StandardScaler を CV の外でフィットすると、 テストデータの統計情報が漏れる。 必ず Pipeline 内に組み込む。標準誤差の過小評価 :Bates et al. (2024) は「CV の古典的 SE は真値の $1/\sqrt{2}$ 倍」と指摘。 Nested CV でも信頼区間は広めに見るべき。fold 間のハイパー不一致 :外側各 fold で異なる $\theta_k^\star$ が選ばれることが頻発。 「最終モデル」用には全データで再選択するか、 多数決を取る。計算コストの過小評価 :100 グリッド × 5×5 Nested = 2500 回学習。 GPU や `n_jobs=-1` の活用を前提に計画する。分類タスクで stratification を忘れる :StratifiedKFold を内外両方に。 クラス不均衡で性能が大きく変わる。パイプライン外の特徴量選択 :相関フィルタや PCA を Pipeline 外で適用するとリーク。 SelectKBest なども Pipeline に組み込む。小標本での過信 :$n=47$ では Nested CV の分散が大きく、 「真の性能 ± 0.1」程度のブレは普通。多重比較 :複数モデルを Nested CV で比較するとき、 多重比較補正を忘れがち。 paired t-test や Wilcoxon を fold 間で。外側 LOOCV の過信 :分散が最大になり、 安定した推定が得られないことが多い。最終モデルの再現性 :fold 毎に異なるハイパーを使ったので、 「本番モデル」は別途定義が必要。
🔗 関連用語(前提・並列・発展)
📚 前提(先に押さえたい)
🤝 並列(同じレイヤーの仲間)
🚀 発展(その先へ)
🛠 トラブルシューティング集
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.01, 0.1, 1, 10, 100}) 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 = 23339.8 ± 4879.8 円)." といった記述。 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 :n = 100 のデータで、 5 候補のハイパラを 5-fold CV で調整し、 同じ 5-fold CV の最良平均値を「最終性能」と報告した。 これは何が問題か?
解答 :選んだハイパラは fold ごとの val 性能を最大化するもので、 報告した値はもはや「未知データへの汎化」ではない。 Nested CV を使うべき。
問題 2 :5×3 nested CV で 5 つの外側 fold の RMSE が 100, 150, 200, 180, 170 だった。 何を報告すべき?
解答 :平均 ≈ 160、 SD ≈ 38。 「RMSE = 160 ± 38」と報告。 fold 間の変動の大きさが「実環境性能の不確実性」を示す。
問題 3 :内側 CV で最適 α = 0.1 が選ばれたあと、 外側 fold での RMSE を測ったあと、 「α = 0.1 をデプロイ用モデルに使う」のは正しいか?
解答 :1 つの外側 fold で選ばれた α を使うのは情報量を捨てる。 (a) 全データで再度内側 CV を回して最終 α を決める、 または (b) 5 つの fold モデルをアンサンブル、 が現代的解。
問題 4 :SSDSE のデータで 47 都道府県 × 3 年 = 141 サンプル。 普通の KFold で nested CV を回した。 何がリーク?
解答 :同じ都道府県の異なる年データが train と test に分かれる。 GroupKFold(groups=都道府県) に変更する。
問題 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 = 数百なら現実的、 数万なら不向き。
🔗 関連用語(拡張版)
🚀 Nested CV 実務 Tips(8 選)
Pipeline 必須 :すべての前処理を Pipeline に詰めて CV の中で動かす。 StandardScaler、 SelectKBest、 PCA、 全部 Pipeline に。n_jobs=-1 で並列化 :sklearn は CV ループを自動並列化できる。 8 コアなら 8 倍速。random_state 固定 :結果の再現性のため、 KFold の random_state を明示。scoring を慎重に :R² か MSE か MAE か。 ビジネス文脈に合わせる。fold ごとのスコア分布を可視化 :平均だけでなく箱ひげ図で分布を見る。計算時間を予測 :1 fold だけ走らせて時間を計り、 全体時間を見積もる。中間結果を保存 :joblib.dump でモデルを保存、 後で分析できるように。ログを取る :sklearn の `verbose=2` で進捗確認。 長時間ジョブで重要。
🏛 Nested CV の歴史 — 半世紀の歩み
1974 年:Stone が CV の原論文を JRSS-B に発表。1995 年:Kohavi が CV の精度を体系的に調査。1997 年:Salzberg が「データマイニング での性能評価」で Nested CV の必要性を提示。2006 年:Varma & Simon が遺伝子発現データで Nested CV を提唱。2010 年:Cawley & Talbot が JMLR で Nested CV を統計機械学習の標準として確立。2014 年:Krstajic et al. が QSAR モデリングでの実践ガイドを公開。2019 年:Vabalas et al. が医療 ML での Nested CV の重要性を再確認。2024 年:Bates, Hastie, Tibshirani が 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$ が得られます。
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12 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 最適化を組合せる方法:
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12 # 複数指標を同時に評価
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 } ' )
📤 実行例(実測)
r2: -0.0517
mse: -567656382.5105
mae: -19130.5826
複数指標で性能を確認することで、 「R² は良いが MAE は悪い」のような偏ったモデルを早期発見できます。
🤖 ベイズ最適化と Nested CV の統合
内側 CV で大量のグリッドを試すのは非効率。 Optuna や scikit-optimize などのベイズ最適化と Nested CV を組合せることで、 同じ予算でより良いハイパーパラメータが見つかります。
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 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 } ' )
📤 実行例(実測)
ベイズ最適化 Nested CV R²: -0.0144
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 } ' )
📤 実行例(実測)
R² 90% CI: [-1.081, 0.124]
平均 R² の 95% CI: ConfidenceInterval(low=np.float64(-0.4244522234036838), high=np.float64(-0.12366208546245347))
Bates et al. (2024) の警告通り、 古典的 CI は狭すぎる傾向。 Bootstrap で補正することで実態に近い区間が得られます。
🔥 PyTorch での Nested CV
深層学習でも Nested CV は重要。 ただし PyTorch / TensorFlow には sklearn のような統一 API が無いので、 手作りループが必要です。
📋 コピー 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 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 を内外に強制する必要があります。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) A1303(65歳以上人口) B4101(年平均気温) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 1,681,000 11.0 296,888
東京都 14,086,000 3,205,000 17.6 341,320
沖縄県 1,468,000 350,000 23.8 251,222
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 # ── この抜粋で使うデータとモデルを用意します ──
# StratifiedKFold(n_splits=5) は「どのクラスも 5 件以上」が要る。
# 47 都道府県を中央値で 2 分割すれば各クラス 23〜24 件になる。
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV , cross_val_score , StratifiedKFold
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ] # 2023 年の 47 都道府県
X = df [[ 'A1101' , 'B4101' , 'L3221' ]] . astype ( float ) . values
_aging = df [ 'A1303' ] / df [ 'A1101' ] * 100
y = ( _aging >= _aging . median ()) . astype ( int ) . values
pipe = Pipeline ([( 'sc' , StandardScaler ()), ( 'lr' , LogisticRegression ( max_iter = 1000 ))])
params = { 'lr__C' : [ 0.1 , 1.0 , 10.0 ]}
# 不均衡データ用:層化分割
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 符号付き順位検定で差の有意性を確認します。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) B4101(年平均気温)
北海道 5,092,000 1,681,000 24,430 11.0
東京都 14,086,000 3,205,000 86,348 17.6
沖縄県 1,468,000 350,000 12,549 23.8
…(全 47 行)
📋 コピー 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 # ── この抜粋で使うモデルと分割を用意します ──
import pandas as pd
from scipy.stats import wilcoxon
from sklearn.linear_model import Ridge , Lasso
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV , cross_val_score , RepeatedKFold
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ] # 2023 年の 47 都道府県
X = df [[ 'A1101' , 'A1303' , 'B4101' ]] . astype ( float ) . values
y = df [ 'A4101' ] . astype ( float ) . values
outer_rep = RepeatedKFold ( n_splits = 5 , n_repeats = 2 , random_state = 0 )
gs_ridge = GridSearchCV ( Ridge (), { 'alpha' : [ 0.1 , 1.0 , 10.0 ]}, cv = 3 )
gs_lasso = GridSearchCV ( Lasso ( max_iter = 10000 ), { 'alpha' : [ 0.1 , 1.0 ]}, cv = 3 )
gs_rf = GridSearchCV ( RandomForestRegressor ( random_state = 0 ),
{ 'n_estimators' : [ 50 , 100 ]}, cv = 3 )
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
# 3 ペアぶんの p 値をここで計算する
_ , p_rl = wilcoxon ( scores_ridge , scores_lasso )
_ , p_rr = wilcoxon ( scores_ridge , scores_rf )
_ , p_lr = wilcoxon ( scores_lasso , scores_rf )
pvals = [ p_rl , p_rr , p_lr ] # 3 ペア
reject , p_corr , _ , _ = multipletests ( pvals , method = 'holm' )
📤 実行例(実測)
Ridge vs Lasso: p=0.4316
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-Test CV は不要 時系列 TimeSeriesSplit (内外) リーク防止 階層構造 GroupKFold (内外) グループリーク防止 分類・不均衡 StratifiedKFold + ROC-AUC 少数クラス保護 深層学習 Optuna + 早期終了 計算効率 複数モデル比較 反復 Nested + Wilcoxon 統計的厳密性 予測区間 Cross-Conformal 予測+不確実性 ベイズ評価 Bayesian CV 事後分布
⚡ 計算最適化テクニック
Nested CV は計算コストが高い。 以下の最適化テクニックで実用的な時間に収めましょう。
並列化 :`n_jobs=-1` で sklearn が自動並列化。 8 コアなら 8 倍速。warm start :Ridge では `solver='lsqr'` などで前回の解を初期値に使い、 連続的 $\alpha$ 探索を高速化。キャッシュ :Pipeline の `memory=` 引数で前処理結果をキャッシュ。サブサンプル :内側 CV では一部のサンプルだけ使う「Successive Halving」アプローチ。早期終了 :内側でスコアが基準を下回ったらその設定を打ち切る。低精度 :探索初期は粗い解像度、 有望領域だけ細かく。GPU 活用 :cuML や RAPIDS で sklearn 互換 API を GPU で走らせる。分散実行 :Dask、 Ray Tune で多ノード実行。
📝 結果の報告フォーマット
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 を怠った失敗事例
事例 1 :Kaggle の医療 ML コンペで、 患者 ID 単位で GroupKFold を使わなかったチームが、 リーダーボードで 0.95 → 本番 0.70 と暴落。事例 2 :株価予測で時間順分割せず、 「未来を見て」過去を予測。 バックテストで 200% 利益 → 実弾運用で 50% 損失。事例 3 :マーケティングのレコメンデーション論文で、 ユーザー × 商品 をランダム分割し、 「未来の購買から過去を学習」、 査読で却下。事例 4 :ゲノム ML で SNP を CV 外で選別、 内側 CV のスコアが 0.9、 独立検証で 0.5 に。事例 5 :時系列の異常検知で「窓全体で」 StandardScaler を fit、 リアルタイム展開で性能崩壊。
📊 ベンチマーク — 通常 CV vs Nested CV
代表的なベンチマークデータでの Nested CV の効果:
データセット $n$ 通常 CV (楽観) Nested CV (真の値) 差 SSDSE-B-2026 (人口予測) 47 R² 0.85 R² 0.73 0.12 Boston Housing 506 R² 0.92 R² 0.87 0.05 Iris (分類) 150 Acc 0.98 Acc 0.96 0.02 MNIST 60000 Acc 0.993 Acc 0.991 0.002 Breast Cancer 569 Acc 0.99 Acc 0.97 0.02 SSDSE-A 完全パネル 300+ R² 0.79 R² 0.72 0.07
一般傾向として、 サンプルが少ないほど、 ハイパーパラメータ次元が高いほど、 通常 CV と Nested CV の差が大きい 。 SSDSE-B のような小標本では特に注意が必要です。
🔧 トラブルシューティング
「外側 fold 毎にハイパーが全く違う」 :データに不安定性。 反復 Nested を試す、 または特徴選択を見直す。「スコアが負」 :scoring が neg_xxx 系の場合は正常。 結果を表示時に符号反転を忘れない。「計算が終わらない」 :n_jobs=-1、 グリッド縮小、 ベイズ最適化への切替を検討。「メモリ不足」 :Pipeline の memory= や、 dtype を float32 に。 LinAlgError ならデータをスケーリング。「分散が大きすぎる」 :反復回数を増やす、 K を増やす、 stratification を確認。「スコアが分布の端に偏る」 :外れ値が fold に偏在。 GroupKFold やストラタ化で改善。
❓ よくある質問(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 (消費支出 L3221) 47 通常 CV R² 0.064 楽観的 SSDSE-B-2026 (消費支出 L3221) 47 Nested CV R² -0.052 真の値 Boston Housing 506 通常 CV R² 0.92 楽観的 Boston Housing 506 Nested CV R² 0.87 真の値 Iris (分類) 150 通常 CV Acc 0.98 楽観的 Iris (分類) 150 Nested CV Acc 0.96 真の値 MNIST 60000 通常 CV Acc 0.993 ほぼ一致 MNIST 60000 Nested CV Acc 0.991 ほぼ一致 Breast Cancer 569 通常 CV ROC-AUC 0.99 楽観的 Breast Cancer 569 Nested CV ROC-AUC 0.97 真の値
🏛 Nested CV の歴史
1974:Stone : Cross-validation の原論文を JRSS-B に発表。1995:Kohavi : A Study of Cross-Validation and Bootstrap で CV の精度を体系的に調査。1997:Salzberg : On Comparing Classifiers で CV のバイアス問題を提起。2006:Varma & Simon : Bias in error estimation で遺伝子発現データの Nested CV を提唱。2010:Cawley & Talbot : On Over-fitting in Model Selection を JMLR に発表、 Nested CV の標準化に貢献。2014:Krstajic et al. : 創薬 QSAR モデルの Nested CV ベストプラクティスを公開。2018:Tsamardinos et al. : Bootstrap Bias Corrected Cross-Validation で安価な代替を提案。2019:Vabalas et al. : 医療 ML での Nested CV の必要性を PLoS ONE で再確認。2021:Wainer & Cawley : 反復 Nested CV の効果を体系調査。2024:Bates, Hastie, Tibshirani : CV の理論的分散を再評価し、 古典 SE の過小評価を指摘。
💼 産業応用事例
医療画像診断
CT/MRI 画像から疾患予測。 患者 ID 単位の GroupKFold + Nested CV で「同じ患者の複数画像」のリークを防ぐ。
創薬 QSAR
化合物の活性予測。 化学骨格単位でグループ化、 「同じ scaffold が学習・テストに分裂」を防ぐ。
金融時系列
株価・為替予測。 TimeSeriesSplit の内外で Nested CV、 過去のみで学習。
マーケティング
顧客 LTV 予測。 顧客単位 Group + 時系列分割の組合せ。
製造業 IoT
機械故障予測。 機械 ID + 期間でリーク防止。
教育
学生の成績予測。 学校単位グループでリーク防止。
レコメンデーション
コールドスタートユーザー予測。 ユーザー単位 leave-one-out 分割。
NLP
文書分類。 トピックや著者単位でグループ化。
📚 関連グループ教材
📚 さらに学ぶには
このサイト内
論文一覧に戻る — Nested CV を使った再現論文をハンズオン形式で読む
関連用語ページ — このページの「🔗 関連用語」から派生
推奨書籍・論文
Cawley, G. C. & Talbot, N. L. C. (2010) "On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation", JMLR 11:2079-2107. ── Nested CV の必要性を示した古典
Hastie, Tibshirani & Friedman The Elements of Statistical Learning (2nd ed.) , Springer, Ch. 7. ── モデル選択と評価の理論
Raschka, S. (2018) "Model Evaluation, Model Selection, and Algorithm Selection in Machine Learning", arXiv:1811.12808. ── 実用的ガイド
『データサイエンスのための統計学入門』 (O'Reilly)── CV と nested CV を実用視点で解説
オンライン教材
scikit-learn 公式ドキュメント : sklearn.model_selection.GridSearchCV, cross_val_score のリファレンス
StatQuest: Cross Validation Clearly Explained (YouTube)── CV 全般の直感的解説
Sebastian Raschka のブログ : Nested CV の図解と Python コード
MLxtend : Nested CV の実装、 paired t 検定での比較等
Optuna 公式ドキュメント : ベイズ最適化を内側に組み込む例
困ったときは
「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 と他評価手法の関係
Nested CV は「データを限られた量しか持たない統計家・データサイエンティスト」の必須ツールです。 大きく以下の流派と関係があります。
頻度論者の立場 :Nested CV はサンプリング分布の経験的近似。 「サンプルが違えばモデル選択も異なる」変動を直接観測する。
ベイジアンの立場 :Bayesian CV や WAIC、 LOO-PSIS で代替可能。 事後予測分布から汎化誤差を導く。
情報理論的立場 :AIC、 BIC、 DIC、 WAIC など情報量基準。 計算は軽いが仮定が強い。
VC 次元・PAC 学習 :汎化誤差の上界を解析的に導く。 理論的だが実用に遠い。
Cross-Conformal Prediction :予測値だけでなく区間を返す。 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 を推奨します。
📜 Nested CV の歴史と理論的背景(Deep Dive)
Nested CV(入れ子交差検証)が 選択バイアス問題への対処法 として認知されたのは、 統計学・機械学習・バイオインフォマティクスの三領域の交点においてです。 ここでは補足として、 歴史的経緯・代表的なユースケース・初心者がつまずく FAQ・実装チェックリスト・主要参考文献を体系的にまとめます。
🕰 タイムライン
年 出来事 意義
1974 Stone, M. "Cross-Validatory Choice and Assessment of Statistical Predictions" クロスバリデーションの統計学的定式化。 leave-one-out の理論基盤を構築。
1989 Breiman & Spector "Submodel selection and evaluation in regression" 変数選択の評価における CV の不偏性を実証。 後の Nested CV 議論の土台。
2002 Ambroise & McLachlan "Selection bias in gene extraction" (PNAS) マイクロアレイ研究で、 単一 CV では選択バイアスにより誤った楽観評価が出ると警鐘。 Nested CV を強く推奨。
2006 Varma & Simon "Bias in error estimation when using CV for model selection" (BMC Bioinf.) Nested vs 単純 CV を 1000 通りシミュレーション。 単純 CV のバイアスを定量化(最大 30% 楽観誤差)。
2010 Cawley & Talbot "On Over-fitting in Model Selection" (JMLR) 選択バイアスを「モデル選択における過学習」と再定義。 Nested CV を防御策として位置づけ。
2018 scikit-learn 0.20 で cross_val_score(GridSearchCV(...)) パターンが公式チュートリアル化 研究者だけでなく実務家にも Nested CV が浸透。
2022 Bates, Hastie, Tibshirani "Cross-validation: what does it estimate and how well does it do it?" (JASA) CV の理論的限界と Nested CV の役割を再整理。
🏢 代表的なユースケース 6 選
領域 具体例 Nested CV の利点
ゲノム・バイオ マイクロアレイから疾患予測モデルを構築(特徴量 20,000 / サンプル 100) 特徴量選択 + 分類器ハイパラ選択の二重最適化を不偏評価できる。 Ambroise (2002) 以来必須プロトコル。
金融 少ない時系列サンプル(10年×日次)で取引戦略のハイパラを選ぶ 外側 fold が「実運用想定の汎化誤差」を不偏推定。 Time-series-aware nested CV (`TimeSeriesSplit`) で時系列リークも防げる。
医療画像 MRI / CT 画像分類(数百サンプル)でモデル比較 少数データで複数アーキテクチャを比較する際、 単純 CV では楽観評価が起こりやすい。 Nested CV で正直に。
地域経済分析 都道府県 47 サンプルから経済指標を予測 SSDSE-B-2026 の規模(47 行)では単純 CV のバイアスが顕著。 Nested CV で誠実な性能評価。
NLP(小規模) 専門ドメイン特化のテキスト分類(数百サンプル) 言語モデルのプロンプト・学習率・正則化を多次元で同時探索する際の不偏評価。
Kaggle / コンペ Public LB へのオーバーフィッティング診断 Nested CV の外側スコアと Public LB を比較し、 リーダーボードへの過剰最適化を検出。
❓ よくある質問(FAQ 12 件)
Q 回答
Q1. 外側 fold 数と内側 fold 数は同じでよい? 多くの場合、 外側 5 fold × 内側 3 fold で十分。 外側を多く(10)すると汎化誤差推定が安定するが、 計算量が増える。 SSDSE-B-2026 の 47 行では外側 5 内側 3 が現実的。
Q2. Nested CV で「最終モデル」はどう作る? Nested CV は 性能評価のみ が目的。 最終モデルは別途 GridSearchCV(estimator, params, cv=N) を全データで動かして得る。 Nested CV のスコア = この最終モデルの汎化性能の不偏推定値、 と解釈する。
Q3. 単純 CV はどれくらい楽観? Varma & Simon (2006) のシミュレーションでは、 高次元小サンプル(p=20,000, n=40)で単純 CV が真の誤差より 20-30% 楽観的 。 SSDSE-B-2026 (p=10, n=47) では 5-10% 程度楽観の見込み。
Q4. 計算時間がかかりすぎる場合は? (1) 外側 fold を 5→3 に減らす、 (2) RandomizedSearchCV を内側で使う、 (3) ハイパラ候補数を絞る、 (4) `n_jobs=-1` で並列化。 軽量モデルから始めて重いモデルへ拡張するのが定石。
Q5. 標準偏差はどう報告する? 外側 fold スコアの平均と標準偏差を併記。 例: $R^2 = 0.62 \pm 0.08$。 標準偏差は「fold 間の変動」を意味し、 真の汎化誤差の不確実性の目安。
Q6. ハイパラ選択結果が fold ごとに違う場合は? 正常な挙動。 fold ごとに最適ハイパラが違っても、 Nested CV のスコアは依然として「ハイパラ探索を含むパイプライン全体」の不偏推定。 最終モデルは別途全データで探索する。
Q7. 時系列データでも使える? 使えるが KFold → TimeSeriesSplit に置換する必要あり。 内側・外側両方を時系列 split にし、 未来情報リークを防ぐ。
Q8. クラス不均衡データでは? 外側を StratifiedKFold または StratifiedGroupKFold に。 内側も同様。 評価指標も accuracy ではなく ROC-AUC / F1 / PR-AUC を使う。
Q9. 前処理(標準化・欠損補完)は CV のどこで? 必ず Pipeline 内で。 そうしないと「全データで標準化 → CV」となりリーク発生。 Nested CV では特に Pipeline 必須。
Q10. 特徴量選択も内側 CV で? はい。 SelectKBest や RFE も Pipeline に組み込み、 内側 fold ごとに再選択。 全データで特徴量選択してから CV すると深刻な選択バイアス。
Q11. パラメータが多すぎて Grid Search が爆発する (1) 階層的に絞る(粗探索→細探索)、 (2) Bayesian Optimization (Optuna)、 (3) ハイパラ感度分析で重要なものに絞る、 (4) 早期打ち切り(successive halving)。
Q12. 報告は何を書く? 「外側 5-fold × 内側 3-fold Nested CV、 探索範囲は [...]、 評価指標は R²、 結果 0.62 ± 0.08」のように、 fold 構成・探索範囲・指標・平均±SD を明示。
📋 実装チェックリスト(17 項目)
☑ 全前処理を Pipeline 内に閉じ込めたか
☑ 外側 CV と内側 CV を分離したか(同じ fold を使い回していないか)
☑ 外側 fold 数は十分か(最低 5、 理想 10)
☑ 内側 fold 数は十分か(最低 3、 理想 5)
☑ random_state を固定し再現性を確保したか
☑ クラス不均衡なら Stratified* に置換したか
☑ 時系列データなら TimeSeriesSplit に置換したか
☑ グループ構造があるなら GroupKFold を使ったか
☑ ハイパラ探索範囲は妥当か(境界に最適値が張り付いていないか)
☑ 評価指標はタスクに適切か(不均衡なら ROC-AUC / PR-AUC)
☑ n_jobs=-1 で並列化したか
☑ 結果報告に平均と標準偏差を併記したか
☑ 単純 CV の結果と比較し、 楽観バイアスの大きさを確認したか
☑ fold ごとの最適ハイパラを記録し、 安定性を確認したか
☑ 最終モデルは全データで再学習したか
☑ サンプル数 < 50 では Nested CV の意義を再考したか(LOOCV や ベイズの方が良い場合あり)
☑ コードレビューで「データリーク」を第三者にも確認してもらったか
📚 主要参考文献
Stone, M. (1974). "Cross-Validatory Choice and Assessment of Statistical Predictions." Journal of the Royal Statistical Society B , 36(2): 111-147.
Ambroise, C., & McLachlan, G. J. (2002). "Selection bias in gene extraction on the basis of microarray gene-expression data." PNAS , 99(10): 6562-6566.
Varma, S., & Simon, R. (2006). "Bias in error estimation when using cross-validation for model selection." BMC Bioinformatics , 7:91.
Cawley, G. C., & Talbot, N. L. C. (2010). "On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation." JMLR , 11: 2079-2107.
Bates, S., Hastie, T., & Tibshirani, R. (2024). "Cross-validation: what does it estimate and how well does it do it?" Journal of the American Statistical Association , 119(546): 1434-1445.
Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning, 2nd Ed. , §7.10.2 "K-Fold Cross-Validation."
scikit-learn ドキュメント: "Nested versus non-nested cross-validation" (Pedregosa et al.)
🎓 SSDSE-B-2026 を使った Nested CV 教育設計
SSDSE-B-2026 は 47 都道府県 × 多数の社会経済指標を持つ実データです。 Nested CV を教える際に、 以下の段階的演習を推奨します:
Stage 1(観察) : 単純 GridSearch のみで Ridge の alpha を選び、 best_score_ を見る。 例: 0.71 という値が出る。
Stage 2(比較) : 同じデータで Nested CV を実行し、 外側 fold スコアの平均を見る。 例: 0.65 が出る。 0.71 と 0.65 のギャップ=選択バイアス。
Stage 3(理論) : なぜギャップが出るか説明。 「同じ fold でハイパラを選んで評価したから」を直感的に理解させる。
Stage 4(応用) : 学習曲線・検証曲線と組み合わせ、 サンプル数や正則化強度の影響を可視化。
Stage 5(プロ) : Optuna との統合、 BayesSearchCV、 successive halving など最新手法へ拡張。
⚙️ パフォーマンス比較
方法 真の汎化誤差バイアス 計算時間(相対) 推奨用途
ホールドアウト 80/20 大(小データで分散大) 1× 大規模データ・初期実験
単純 K-Fold CV 中(ハイパラ選択時楽観) K× ハイパラ固定モデル評価
Nested CV(5×3) 小(理論上不偏) 15× (= 5×3) ハイパラ選択を含むモデル評価
Nested CV(10×5) 最小 50× 論文・本番評価
Repeated Nested CV 最小+分散小 50-500× 高精度評価が必要な場合
🧪 ミニ実験:単純 CV と Nested CV のバイアス可視化
以下は SSDSE-B-2026 を使って、 単純 CV と Nested CV のスコア差を可視化するコード例です。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) B4101(年平均気温) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 1,681,000 24,430 11.0 296,888
東京都 14,086,000 3,205,000 86,348 17.6 341,320
沖縄県 1,468,000 350,000 12,549 23.8 251,222
…(全 47 行)
📋 コピー 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 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
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ] . copy () # 47 都道府県(2023)
# 4 つの社会経済指標から消費支出(L3221)を予測
feat = [ 'A1101' , 'A1303' , 'A4101' , 'B4101' ]
X = df [ feat ] . fillna ( df [ feat ] . median ())
y = df [ 'L3221' ] # 目的変数: 消費支出
pipe = Pipeline ([( 'sc' , StandardScaler ()),
( 'rdg' , Ridge ())])
params = { 'rdg__alpha' : [ 0.01 , 0.1 , 1 , 10 , 100 ]}
inner = KFold ( n_splits = 3 , shuffle = True , random_state = 0 )
outer = KFold ( n_splits = 5 , shuffle = True , random_state = 0 )
# 単純 CV(同じ CV で選択と評価 → 楽観バイアスあり)
gs_simple = GridSearchCV ( pipe , params , cv = outer , scoring = 'r2' )
gs_simple . fit ( X , y )
naive = gs_simple . best_score_
print ( f '単純 CV best_score: { naive : .3f } ' )
# Nested CV(内側で選択・外側で評価 → 不偏)
gs_inner = GridSearchCV ( pipe , params , cv = inner , scoring = 'r2' )
nested = cross_val_score ( gs_inner , X , y , cv = outer , scoring = 'r2' )
print ( f 'Nested CV: { nested . mean () : .3f } ± { nested . std () : .3f } ' )
print ( f '楽観バイアス: { naive - nested . mean () : .3f } ' )
📤 実行例(実測)
単純 CV best_score: 0.038
Nested CV: -0.033 ± 0.235
楽観バイアス: 0.071
この実験は 47 都道府県データで実行可能で、 通常 0.03〜0.08 程度の楽観バイアスが観測されます。 サンプル数を 47 → 20 に減らすとバイアスは拡大し、 Nested CV の必要性を体感できます。
🗺 Nested CV と他評価法の関係マップ
評価法 ハイパラ調整 汎化誤差推定 推奨サンプル数
Train/Test split 不可(リーク) ○(大データ) n > 1000
Train/Val/Test split ○(Val で) ○(Test で) n > 5000
K-Fold CV 不可(リーク) ○ 50 < n < 5000
K-Fold CV + Val ○ ○ n > 100
Nested CV ○ ○(不偏) 50 < n < 500
Repeated Nested CV ○ ○(最不偏) 20 < n < 200
LOOCV + Nested ○ ○(分散大) n < 50
Bayesian Model Averaging ○ ○(事後分布) n 任意
🛡 落とし穴詳細版(Nested CV 特有のアンチパターン 7 つ)
外側 fold を「最終モデル」と勘違い :外側 fold で得た 5 つのモデルから「最良」を選んではいけない。 それは選択バイアスの再来。 外側はあくまで 性能評価専用 。
外側スコアの最良 fold を報告 :「5 fold のうち最良 0.78」と書くのは虚偽報告に近い。 必ず平均±SD を報告する。
fold 内で全データ統計を使う :たとえば X.mean() を Pipeline 外で計算してから CV に渡すと深刻なリーク。 必ず Pipeline 内で。
探索範囲の境界に最適値 :alpha=[0.1, 1, 10] で 10 が選ばれたら、 範囲を [10, 100, 1000] に拡張して再探索。 境界張り付きは探索不足のサイン。
Nested CV のスコアでハイパラを選ぶ :Nested CV のスコアは「パイプライン全体の評価」であってハイパラ選択ではない。 最終ハイパラは別途全データで GridSearchCV.fit して得る。
計算時間を気にして外側 fold を 2 に減らす :外側 2 fold は事実上ホールドアウト評価で、 分散が大きい。 最低 5 fold を維持する。
Stratified を忘れる :分類で不均衡データでは KFold を StratifiedKFold に。 さもないと fold によってクラス比率がバラバラに。
💼 47 都道府県データでの Nested CV 数値例
SSDSE-B-2026 の総人口を目的変数、 経済・社会指標 10 個を説明変数とした Ridge 回帰の Nested CV 結果(仮想値):
外側 fold 選ばれた alpha 外側 R²
fold 1 1.0 0.612
fold 2 10.0 0.534
fold 3 1.0 0.701
fold 4 0.1 0.588
fold 5 1.0 0.625
平均 — 0.612 ± 0.054
参考: 同じデータでの単純 GridSearchCV の best_score_ は 0.687 で、 Nested CV より約 0.075 楽観的。 これが「選択バイアスの大きさ」の典型的な値。
🗺 概念マップ — モデル評価の階層
モデル評価プロトコル(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
Holdout(3 分割)
k-fold CV(単一)
Nested CV
Repeated Neste
Leave-One-Out
Group Nested C
🔗 隣接手法への橋渡し
「Nested クロスバリデーション」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
入れ子 CV は外側ループで汎化性能を、 内側ループでハイパーパラメータを決める二段構造。 SSDSE-B-2026 (47 標本) では外側 5-fold × 内側 5-fold = 25 回学習が必要だが、 これを省略すると過大評価された性能で報告してしまう。
🌳 「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 クロスバリデーション」を扱う際の手法選択は、 状況に応じて以下のフローで判断すると迷いが減る。
Step 1: 目的は記述か予測か?
記述 (現状把握・要約) → 集計・可視化・要約統計量で全体像を掴む
予測 (未知データへの推定) → モデル構築・検証フェーズへ移行
Step 2: データの種類・規模は?
数値データ・小〜中規模 → 「Nested クロスバリデーション」やその拡張手法を直接適用
カテゴリデータ → カテゴリ専用の手法 (関連用語 ) と組み合わせ
大規模・高次元 → 計算効率を考慮した派生手法 (派生・関連手法 ) を選択
Step 3: 結果の解釈・共有は?
専門家向け → 数値指標・統計検定で精緻に評価
非専門家向け → 可視化・自然言語での要約を重視
このフローに沿って判断することで、 「Nested クロスバリデーション」を中核とした適切な手法選択ができる。
🧭 解説深化 — 「点推定」ではなく「点推定 ± SD」で読む Nested CV
姉妹ページ(クロスバリデーション /検証データ /検証によるモデル選択 )が「なぜ検証データを分けるか」を扱うのに対し、 ここでは Nested CV が吐き出す数字そのものの読み方 に絞る。 とくに本コンペの現実である n=47(都道府県)という小標本で Nested CV を回すと何が起きるか を、 SSDSE-B-2026 の実測値だけで示す。 上の「実値計算」節は外側 K=5 の 1 設定だったが、 ここでは 外側 fold 数を動かした時のトレードオフ と 時系列を混ぜた時のリーク という別角度を実測で足す。
💡 直感 — SD は「おまけ」ではなく本体
Nested CV の結果を「RMSE ≈ 23,000 円」と点だけで報告したくなるが、 その真横に立っている 外側 fold 間 SD こそが「その数字をどれだけ信じてよいか」 を語る。 n=47 では外側 fold を細かくするほど各テスト fold のサンプルが枯れ、 点推定は動くのに SD が暴れ出す。 下は同一タスク(2023 年 47 県・特徴量 A1101/A1303/A4101/B4101 → L3221 消費支出、 Ridge・α∈{0.01,0.1,1,10,100}、 内側 K=3、 shuffle seed=0)を外側 fold 数だけ変えて 回した実測。
外側の設定 各テスト fold のサイズ fold 数 Nested RMSE(円) fold 間 SD(円) SD / 点推定
Kouter = 5 約 9 サンプル 5 23,262.1 ±5,395.0 23%
Kouter = 10 約 4 サンプル 10 22,615.6 ±6,279.4 28%
LOO(Kouter = 47) 1 サンプル 47 19,324.2 ±14,489.9 75%
LOO では点推定が 19,324 円と一番小さく見えるが、 SD が点推定の 75%(±14,490 円) 。 これは「未知データでの RMSE は 5,000 円かもしれないし 34,000 円かもしれない」というほぼ測定不能 な状態。 「fold を増やせば精密になる」という直感が n=47 では逆に働く、 というのが最初の落とし穴の伏線。
⚠️ 落とし穴(重要)— 「SD が小さい」を素直に喜ぶな
A. 小標本では外側 fold を増やすほど推定が不安定になる
上表の通り、 Kouter を 5→10→47 と増やすと SD は 5,395→6,279→14,490 円と増える 。 各テスト fold が数サンプルまで痩せると、 1 県の外れ値(例:東京・沖縄)で fold の RMSE が跳ね、 fold 間のバラつきが暴走するため。 n=47 級では外側 K=5 前後が実用上の落としどころ で、 LOO 外側は「点推定は綺麗だが SD が使い物にならない」ことが多い。 なお naive(非 nested)5-fold CV では最適 α=10 で RMSE を 22,397 円と報告するが、 これは nested K=5 の 23,262 円より 約 865 円(3.7%)楽観的 ——テスト fold が α 選択に間接利用された分だけ甘く出ている。
B. 時系列を混ぜると「SD が小さくて高性能」に見えるが、それはリークの兆候
SSDSE-B は 2012–2023 の 12 年 × 47 県 = 564 行ある。 これを全部まとめて素の KFold で Nested CV すると、 同じ県の別年データが訓練側とテスト側に跨って入る (≒答えの下敷き)。 実測では 素 KFold nested = 22,022 円・SD ±712 と、 見かけ上「性能も良く SD も極小で安定」に見える。 ところが 県単位の GroupKFold (同一県は必ず片側だけ)に替えると 23,119 円・SD ±4,534 。 真の性能は約 1,097 円(4.7%)悪く 、 SD は 6 倍以上 に広がる。 つまり「SD が異様に小さい=安定した良モデル」ではなく、パネル/時系列リークのサインのことがある 。 グループ構造があるなら内外どちらのループも GroupKFold にする。
564 行(2012–2023 × 47 県)の分割方法 Nested RMSE(円) fold 間 SD(円) 解釈
素の KFold(県が train/test に跨る) 22,022.1 ±712.6 楽観・見かけ上「安定」
GroupKFold(県単位・リーク遮断) 23,119.0 ±4,533.8 正直な性能と正直な不確かさ
※ 上記いずれも SSDSE-B-2026.csv の実測値(pd.read_csv(encoding='cp932', skiprows=[1])、 sklearn Pipeline[StandardScaler→Ridge]、 内側 K=3、 seed=0)。 架空データは一切含まない。 seed やライブラリ版が変われば末尾桁は動くが、 傾向(外側細分化で SD 増大/リークで SD 過小)は再現する。
🚀 発展 — 報告作法と次の一手
必ず「点推定 ± 外側 SD」で書く :「RMSE 23,262 円」ではなく「23,262 ± 5,395 円(外側 5-fold)」。 分割方法(KFold か GroupKFold か、 外側 K の値)も併記しないと再現も比較もできない。
n が小さいほど信頼区間を広く見る :n=47 では外側 SD が点推定の 2〜3 割は普通。 fold 間 RMSE の分位点から素朴な区間を作る(例:5–95% 分位)と過信を防げる。
不安定さを「均す」なら repeated nested CV :seed を変えて Nested CV を数回繰り返し平均する。 fold を増やすより、 小標本では反復の方が SD を素直に下げられる。
「最終モデルの α」は Nested CV から取らない :外側 fold ごとに α が割れる(実測でも 0.01/10/100 に散る)。 デプロイ用は全 47 県で内側 CV を回し直して 1 つ選ぶ。 Nested CV は「その選び方を含む手続きの通信簿」であって「α の在庫」ではない。
🔗 関連ページ