嵌套交叉验证
内层选择超参数,外层估计整个选择流程的泛化,减少最佳 CV 分数的选择乐观。
学习前需要掌握
背景与问题
在多个候选中选择最高验证分数,会同时选择到一部分偶然波动。把获胜候选的同一验证均值当作最终性能,往往会乐观,尤其在搜索空间大或数据小时。
嵌套交叉验证把“选方案”与“评方案”分到两层,让外层评价整套搜索流程在未参与选择的数据上的表现。
概念定义
每个外层训练集内部运行完整 GridSearchCV,再在外层验证折评价已选择配置。
直观理解
外层考察“学习加选参数”整套流程,内层才负责练习阶段挑方案。
核心原理
外层先留出一折,其余数据交给内层搜索。内层只能在外层训练部分做预处理、调参、阈值选择或其他开发;选出方案后在该外层训练部分 refit,再评价外层留出折。
不同外层折可能选出不同超参数,这并非错误,而是训练样本变化影响选择的证据。汇总的外层分数估计的是这个选择流程,而不是某个固定参数在全部数据训练后的直接测试分数。
外层验证不能用来扩大搜索空间、挑选报告指标或指导早停,否则外层也参与开发。群组与时间约束还要在内外两层保持,不能外层分组、内层又把同一患者随机拆开。
计算量包含每个候选的内层拟合与每个外层的最终 refit。完成评价后,实际部署通常还要在完整开发数据上按既定搜索流程选参并拟合;嵌套评估不会自动产生唯一最终模型。
数学表达
核心表达
计算成本是外折×内折×候选数;外层验证不能用于内层早停或选择。
变量含义
变量定义与本页概念定义一致,代码中的同名变量保持相同语义。
计算步骤
- 1定义内层 CV
- 2定义外层 CV
- 3创建 search estimator
- 4外层 cross_validate
代码对应位置
示例 1把 GridSearchCV 作为外层被评估流程:固定小型嵌套搜索,外层结果与内层候选统计分别输出,不冒充真实业务测试。
案例:3 外折、3 内折与 2 个候选
每个外折训练部分内做 2 × 3 = 6 次候选拟合,再将最佳流程 refit 1 次,三个外折共 21 次拟合。外层留出样本只用于评分,不参与这 7 次训练中的任何一步。
下方在 Iris 上运行这个小搜索,并保留每个外层的最佳估计器。输出仅演示嵌套流程;如果读完外层结果又修改 C 的范围,就应承认这批外层结果已经参与开发,而不能继续作为最终独立证据。
计算与实现步骤
- 定义内层 CV
- 定义外层 CV
- 创建 search estimator
- 外层 cross_validate
- 报告外层分数
- 再训练最终方案
代码实现
示例 1
把 GridSearchCV 作为外层被评估流程
example_01.py用途:固定小型嵌套搜索,外层结果与内层候选统计分别输出,不冒充真实业务测试。
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_validate
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_iris(return_X_y=True)
pipeline = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
inner = StratifiedKFold(3, shuffle=True, random_state=42)
outer = StratifiedKFold(3, shuffle=True, random_state=100)
search = GridSearchCV(pipeline, {"logisticregression__C": [0.1, 1.0]},
cv=inner, scoring="accuracy", refit=True)
result = cross_validate(search, X, y, cv=outer, scoring="accuracy", return_estimator=True)
assert len(result["estimator"]) == 3
print("outer_accuracy:", result["test_score"].round(3).tolist())
print("inner_candidates:", [len(model.cv_results_["params"]) for model in result["estimator"]])
print("total_fits:", 3 * (3 * 2 + 1))代码解析
解析始终位于完整代码下方,并按实际代码段逐项对应。
输入数据与任务
固定小型嵌套搜索,外层结果与内层候选统计分别输出,不冒充真实业务测试。
X- 完整特征矩阵,每行一个样本、每列一个特征。
y- 目标标签或连续目标值。
model- 组合预处理和估计器的模型对象。
Step 1 · 1–5 行
导入当前步骤需要的数值计算、预处理、模型或评价工具。依赖集中写在代码开头,便于复现。
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_validate
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScalerStep 2 · 7–17 行
输出中间参数、形状或最终指标,用于核对代码是否符合预期。
X, y = load_iris(return_X_y=True)
pipeline = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
inner = StratifiedKFold(3, shuffle=True, random_state=42)
outer = StratifiedKFold(3, shuffle=True, random_state=100)
search = GridSearchCV(pipeline, {"logisticregression__C": [0.1, 1.0]},
cv=inner, scoring="accuracy", refit=True)
result = cross_validate(search, X, y, cv=outer, scoring="accuracy", return_estimator=True)
assert len(result["estimator"]) == 3
print("outer_accuracy:", result["test_score"].round(3).tolist())
print("inner_candidates:", [len(model.cv_results_["params"]) for model in result["estimator"]])
print("total_fits:", 3 * (3 * 2 + 1))代码与数学原理
减去训练均值并除以训练标准差,对应代码中的 mean/std 或 StandardScaler。
预期输出或运行结果
本地 scikit-learn 1.8.0 的核对输出: outer_accuracy: [0.96, 0.92, 0.98] inner_candidates: [2, 2, 2] total_fits: 21
常见错误 · 4 条
- 不要在测试集上调用 fit 或 fit_transform;测试集只能复用训练参数。
- 把内层 best_score_ 当成外层结果,或让外层样本参与预处理与早停。
- 外层遵守群组或时间边界,内层却改用不匹配的随机划分。
- 报告只计算候选次数,漏掉每个外层 refit,并误认为嵌套运行自动得到最终部署模型。
实际应用
- 小样本严格比较
- 多参数选择
常见错误
- 把内层 best_score_ 当成外层结果,或让外层样本参与预处理与早停。
- 外层遵守群组或时间边界,内层却改用不匹配的随机划分。
- 报告只计算候选次数,漏掉每个外层 refit,并误认为嵌套运行自动得到最终部署模型。
推荐学习资料
参考库不会生成虚假资源或无效外部链接。