在调优梯度提升模型的超参数时,仅依靠单一的训练-验证集划分来评估性能可能会产生误导。选定的超参数可能对该特定的验证集过拟合,导致在未见过的数据上泛化能力下降。交叉验证 (CV) 通过在数据的多个不同子集上评估模型,为给定超参数集提供了更可靠的性能估计。将 CV 有效地整合到您的调优流程中,对于构建在实际中表现良好的模型是必要的。交叉验证在超参数调优中的作用回顾一下,网格搜索、随机搜索或贝叶斯优化等调优方法通过提出不同的超参数配置并评估其性能来工作。我们不使用单个验证集进行此评估,而是使用交叉验证。在调优过程中测试的每个超参数组合:训练数据被临时划分为 $K$ 个折叠(子集)。模型被训练 $K$ 次。每次训练都在 $K-1$ 个折叠上进行,并在剩余的一个折叠(保留折叠)上进行验证。性能指标(例如,AUC、RMSE、LogLoss)在每次 $K$ 运行的保留折叠上计算。$K$ 个性能分数被平均(或有时使用其他统计量如标准差进行汇总),以提供该特定超参数配置的单一、更稳定的性能估计。这个平均分数随后被调优算法(例如,贝叶斯优化)使用,以决定下一步尝试哪些超参数,或者在网格搜索的情况下,从已测试的组合中找出最佳组合。标准K折交叉验证最常见的CV策略是K折交叉验证。训练数据被打乱并分成 $K$ 个大小相等的折叠。通常,$K$ 选择为5或10。更高的 $K$ 值在每次迭代中会使用更多数据进行训练,但会增加计算时间。digraph KFold { rankdir=LR; node [shape=rect, style=filled, color="#e9ecef", fontcolor=black, fontname="Arial"]; edge [arrowhead=none, color="#868e96"]; subgraph cluster_kfold { label="K折交叉验证 (K=5 示例)"; style=dashed; bgcolor="#f8f9fa"; Data [label="训练数据"]; F1 [label="折叠 1", fillcolor="#a5d8ff"]; F2 [label="折叠 2", fillcolor="#a5d8ff"]; F3 [label="折叠 3", fillcolor="#a5d8ff"]; F4 [label="折叠 4", fillcolor="#a5d8ff"]; F5 [label="折叠 5", fillcolor="#a5d8ff"]; Train1 [label="训练 (折叠 2-5)", fillcolor="#b2f2bb"]; Val1 [label="验证 (折叠 1)", fillcolor="#ffc9c9"]; Train2 [label="训练 (折叠 1, 3-5)", fillcolor="#b2f2bb"]; Val2 [label="验证 (折叠 2)", fillcolor="#ffc9c9"]; Train5 [label="训练 (折叠 1-4)", fillcolor="#b2f2bb"]; Val5 [label="验证 (折叠 5)", fillcolor="#ffc9c9"]; AvgScore [label="平均分数\n(F1-F5上的指标)", shape=ellipse, fillcolor="#ffe066"]; Data -> F1; Data -> F2; Data -> F3; Data -> F4; Data -> F5; F1 -> Val1; F2 -> Train1; F3 -> Train1; F4 -> Train1; F5 -> Train1; F1 -> Train2; F2 -> Val2; F3 -> Train2; F4 -> Train2; F5 -> Train2; F1 -> Train5; F2 -> Train5; F3 -> Train5; F4 -> Train5; F5 -> Val5; Val1 -> AvgScore; Val2 -> AvgScore; Val5 -> AvgScore; } } 超参数调优循环中的基本K折交叉验证过程。用于分类的分层K折交叉验证在分类问题中,特别是处理不平衡数据集时,标准K折交叉验证可能会随机创建折叠,导致类别分布与整体分布存在显著差异。这可能导致不可靠的性能估计。分层K折交叉验证通过确保每个折叠中每个类别的样本百分比与完整数据集中观察到的百分比一致来解决这个问题。它是分类任务的推荐默认选项。大多数库(如Scikit-learn)都提供特定的实现(StratifiedKFold)。用于相关数据的分组K折交叉验证有时,数据点不是独立的。例如,您可能有来自同一患者的多次测量、同一地点的图像或同一用户会话的日志。如果使用标准K折交叉验证,来自同一组的数据可能会在给定的划分中同时出现在训练集和验证集中。这种数据泄露可能导致过于乐观的性能估计,因为模型学会了识别特定组而不是一般模式。分组K折交叉验证确保属于同一组的所有样本在每次划分中完全分配到训练集或验证集中的一个。您需要一个用于分组的标识符(例如,patient_id、user_id)。这可以防止模型“偷看”它将被测试的同一组数据,从而更实际地评估泛化性能。时间序列交叉验证"时间序列数据带来了一个独特的问题:时间顺序很重要。像标准K折交叉验证那样随机打乱数据,会破坏时间依赖性并导致“未来数据偏差”——即利用未来数据来预测过去,这在实际情况中是不可能的。"时间序列交叉验证的策略保持时间顺序:滚动预测起点(或TimeSeriesSplit): 训练集随时间向前扩展或滑动。在折叠1上训练,在折叠2上验证在折叠1-2上训练,在折叠3上验证在折叠1-2-3上训练,在折叠4上验证...依此类推。 " 这模拟了随着新数据可用而定期重新训练模型的场景。Scikit-learn 的 TimeSeriesSplit 实现了这一点。"滑动窗口: 类似于滚动预测,但训练窗口大小固定,随时间向前滑动。在折叠1-2上训练,在折叠3上验证在折叠2-3上训练,在折叠4上验证在折叠3-4上训练,在折叠5上验证{"layout": {"title": "时间序列交叉验证 (滚动预测)", "xaxis": {"title": "时间 / 数据索引"}, "yaxis": {"title": "CV划分迭代", "tickvals": [1, 2, 3, 4], "ticktext": ["划分 1", "划分 2", "划分 3", "划分 4"]}, "height": 300, "width": 600, "shapes": [{"type": "rect", "xref": "x", "yref": "y", "x0": 0, "y0": 0.6, "x1": 20, "y1": 1.4, "fillcolor": "#b2f2bb", "opacity": 0.5, "layer": "below", "line": {"width": 0}}, {"type": "rect", "xref": "x", "yref": "y", "x0": 20, "y0": 0.6, "x1": 30, "y1": 1.4, "fillcolor": "#ffc9c9", "opacity": 0.5, "layer": "below", "line": {"width": 0}}, {"type": "rect", "xref": "x", "yref": "y", "x0": 0, "y0": 1.6, "x1": 30, "y1": 2.4, "fillcolor": "#b2f2bb", "opacity": 0.5, "layer": "below", "line": {"width": 0}}, {"type": "rect", "xref": "x", "yref": "y", "x0": 30, "y0": 1.6, "x1": 40, "y1": 2.4, "fillcolor": "#ffc9c9", "opacity": 0.5, "layer": "below", "line": {"width": 0}}, {"type": "rect", "xref": "x", "yref": "y", "x0": 0, "y0": 2.6, "x1": 40, "y1": 3.4, "fillcolor": "#b2f2bb", "opacity": 0.5, "layer": "below", "line": {"width": 0}}, {"type": "rect", "xref": "x", "yref": "y", "x0": 40, "y0": 2.6, "x1": 50, "y1": 3.4, "fillcolor": "#ffc9c9", "opacity": 0.5, "layer": "below", "line": {"width": 0}}, {"type": "rect", "xref": "x", "yref": "y", "x0": 0, "y0": 3.6, "x1": 50, "y1": 4.4, "fillcolor": "#b2f2bb", "opacity": 0.5, "layer": "below", "line": {"width": 0}}, {"type": "rect", "xref": "x", "yref": "y", "x0": 50, "y0": 3.6, "x1": 60, "y1": 4.4, "fillcolor": "#ffc9c9", "opacity": 0.5, "layer": "below", "line": {"width": 0}}], "annotations": [{"x": 10, "y": 1, "text": "训练", "showarrow": false, "font": {"color": "#495057"}}, {"x": 25, "y": 1, "text": "验证", "showarrow": false, "font": {"color": "#495057"}}, {"x": 15, "y": 2, "text": "训练", "showarrow": false, "font": {"color": "#495057"}}, {"x": 35, "y": 2, "text": "验证", "showarrow": false, "font": {"color": "#495057"}}, {"x": 20, "y": 3, "text": "训练", "showarrow": false, "font": {"color": "#495057"}}, {"x": 45, "y": 3, "text": "验证", "showarrow": false, "font": {"color": "#495057"}}, {"x": 25, "y": 4, "text": "训练", "showarrow": false, "font": {"color": "#495057"}}, {"x": 55, "y": 4, "text": "验证", "showarrow": false, "font": {"color": "#495057"}}]}, "data": []}使用滚动预测起点表示时间序列交叉验证。绿色块表示训练数据,红色块表示每个划分迭代的验证数据。选择合适的时间序列划分取决于您是否预期模式会随时间变化(滑动窗口可能更好),或者旧数据是否仍然具有相关性(滚动预测)。将CV与调优框架结合现代的超参数优化库被设计为与交叉验证配合工作。Scikit-learn: GridSearchCV 和 RandomizedSearchCV 等类都有一个 cv 参数,您可以在其中指定折叠数量(例如,cv=5)或传递一个特定的CV划分器对象(例如,cv=StratifiedKFold(n_splits=5) 或 cv=TimeSeriesSplit(n_splits=5))。该框架会自动对它评估的每个超参数集执行CV循环。Optuna/Hyperopt: 在定义这些库优化的目标函数时,您通常会在其中包含一个交叉验证循环。该函数接受一组超参数(在Optuna中称为 trial),执行K折交叉验证(或另一种策略),计算各折叠的平均分数,并返回此分数。然后,优化库使用此返回的分数来指导其搜索。与提前停止的配合梯度提升模型通常受益于提前停止,以找到最佳的提升轮数(n_estimators)并防止过拟合。将提前停止与交叉验证结合需要谨慎。在CV循环中评估单个超参数集的一种常见方法是:对于 $K$ 个折叠中的每一个:将 $K-1$ 个训练折叠进一步划分为子训练集和提前停止验证集。在子训练集上训练模型,使用提前停止集来确定该折叠的最佳轮数。记录在使用为该折叠找到的最佳轮数训练的模型在主要保留验证折叠上的性能分数。同时,记录所使用的轮数。平均 $K$ 个折叠的性能分数。这就是正在评估的超参数集的分数。可选地,平均或找出 $K$ 个折叠中找到的最佳轮数的中位数。当训练最终模型时(在使用上述CV过程找到最佳超参数后),您在整个训练数据集上进行训练。这个最终模型的提升轮数可以设置为在CV期间找到的平均/中位数轮数,或者如果可用,可以使用单独的最终验证集来确定。一些库(如XGBoost和LightGBM)允许在主要的 .fit() 调用期间传递评估集,从而在此最终训练阶段启用提前停止。计算成本和最终模型训练交叉验证将调优过程的计算成本乘以 $K$ 倍。使用5折CV评估100个超参数组合意味着训练500个模型。这是获得可靠性能估计的必要成本。为了管理这一点:如果计算成本过高,使用较少的折叠($K=3$ 或 $K=5$)。优先选择随机搜索或贝叶斯优化等高效搜索策略,而不是穷举网格搜索。考虑进行初步的宽泛搜索,使用较少的折叠或较少的数据,然后对超参数空间中有希望的区域进行更精细的搜索。重要的是,请记住调优过程中的交叉验证仅用于评估超参数集。一旦找到最佳超参数,您将使用这些最优参数在整个训练数据集上最后一次训练您的最终模型。从交叉验证过程中获得的性能估计,可作为您对这个最终模型在新、未见过的数据上表现的预期。