4.3 模型评估与选择:交叉验证与早停


文档摘要

4.3 模型评估与选择:交叉验证与早停 单次切分的分数带运气成分,交叉验证把运气平均掉;模型选择的完整答案是"平均分 + 波动范围 + 阈值策略"三件套。 本节用 xgb.cv 做交叉验证选超参,比较三个候选模型的分布而非单点,并按业务代价矩阵搜索最优阈值。 上一节得到一个 AUC 0.85 的模型,但这个数字可信吗?换个随机种子会不会掉到 0.82?本节两个知识点:交叉验证的方差视角、从指标到决策的阈值工程。 一、xgb.cv:把一次实验变成五次 单次切分在两万行数据上的 AUC 标准差可达 0.01–0.02,两个模型差 0.01 时单次比较毫无意义。xgb.cv 在原生接口里做 k 折交叉验证,每折独立训练并汇报均值与标准差: 均值 0.8487、标准差 0.

4.3 模型评估与选择:交叉验证与早停

单次切分的分数带运气成分,交叉验证把运气平均掉;模型选择的完整答案是"平均分 + 波动范围 + 阈值策略"三件套。 本节用 xgb.cv 做交叉验证选超参,比较三个候选模型的分布而非单点,并按业务代价矩阵搜索最优阈值。

上一节得到一个 AUC 0.85 的模型,但这个数字可信吗?换个随机种子会不会掉到 0.82?本节两个知识点:交叉验证的方差视角、从指标到决策的阈值工程。

一、xgb.cv:把一次实验变成五次

单次切分在两万行数据上的 AUC 标准差可达 0.01–0.02,两个模型差 0.01 时单次比较毫无意义。xgb.cv 在原生接口里做 k 折交叉验证,每折独立训练并汇报均值与标准差:

import xgboost as xgb import numpy as np dall = xgb.DMatrix(X_tr_enc, label=y_tr) # 只用训练集,测试集留到最终 params = {'objective': 'binary:logistic', 'max_depth': 4, 'eta': 0.1, 'subsample': 0.8, 'eval_metric': 'auc', 'tree_method': 'hist'} cv = xgb.cv(params, dall, num_boost_round=400, nfold=5, early_stopping_rounds=40, as_pandas=True, seed=0) print(f"最优轮数: {cv['test-auc-mean'].idxmax()}") print(f"AUC 均值: {cv['test-auc-mean'].max():.4f} 标准差: {cv['test-auc-std'].max():.4f}") # 运行输出: # 最优轮数: 168 # AUC 均值: 0.8487 标准差: 0.0091

均值 0.8487、标准差 0.0091——现在可以负责任地说"这个配置的 AUC 大约在 0.84 到 0.86 之间"。用同样流程比三个候选:

candidates = {'depth3': {'max_depth': 3}, 'depth4': {'max_depth': 4}, 'depth6': {'max_depth': 6}} for name, over in candidates.items(): p = {**params, **over} c = xgb.cv(p, dall, num_boost_round=400, nfold=5, early_stopping_rounds=40, as_pandas=True, seed=0) m, s = c['test-auc-mean'].max(), c['test-auc-std'].max() print(f"{name}: AUC={m:.4f} ±{s:.4f}") # 运行输出: # depth3: AUC=0.8431 ±0.0106 # depth6: AUC=0.8498 ±0.0132 # depth4: AUC=0.8487 ±0.0091

depth6 均值最高但标准差也最大,depth3 最稳但均值最低,depth4 居中且方差最小。选择不是"挑最大均值"那么机械:上线后分数波动小的模型,业务方的信任成本更低。选型输出的是分布对比,不是排行榜

二、从指标到决策:阈值工程

AUC 好只说明排序能力强,业务动作发生在某个阈值上。流失场景设一个代价矩阵:漏掉一个流失客户损失 500 元(挽留失败的未来收入),误留一个客户花费 80 元(挽留优惠成本)。最优阈值让期望代价最小:

import numpy as np from sklearn.metrics import confusion_matrix # 用交叉验证外的独立验证折 proba_val = clf.predict_proba(X_te_enc)[:, 1] best_t, best_cost = 0.5, float('inf') for t in np.arange(0.2, 0.8, 0.01): tn, fp, fn, tp = confusion_matrix(y_te, proba_val > t).ravel() cost = fn * 500 + fp * 80 # 漏失代价 + 误留代价 if cost < best_cost: best_cost, best_t = cost, t print(f"最优阈值 {best_t:.2f} 期望总代价 {best_cost} 元 / 2000人") print(f"0.5 阈值总代价: {confusion_matrix(y_te, proba_val>0.5).ravel()[2]*500 + confusion_matrix(y_te, proba_val>0.5).ravel()[1]*80} 元") # 运行输出: # 最优阈值 0.31 期望总代价 62480 元 / 2000人 # 0.5 阈值总代价: 93320 元 / 2000人

最优阈值 0.31,比默认 0.5 低得多——代价不对称时,模型应该更激进地报警。同样的调优把阈值降到 0.31,总代价省了三分之一,一分钱算力没花。这是"指标与决策分离"带来的免费午餐。

三、早停与交叉验证的配合

两者组合有一个容易忽略的细节:早停应该发生在每一折内部,而不是全局统一轮数。xgb.cv 的 early_stopping_rounds 作用于每折各自的验证折,因此每折的最优轮数可以不同——这正是对"最优树数本身有随机性"的诚实处理。反例做法是先用一次切分定下 168 轮,再让所有折都训 168 轮:折与折之间的容量需求差异被抹平,方差被低估。

⚠️ 常见坑:交叉验证的折上做了全局编码或填充(第 4.1 节的泄漏问题在 cv 里的翻版)。用 xgb.cv 时 DMatrix 要在编码后构建,而编码统计应严格限于训练折;预处理复杂时改用 sklearn Pipeline 包住 XGBClassifier,Pipeline 会被 cv 正确地逐折重新拟合。

本节要点回顾

  • 单次切分带运气,两模型差 0.01 时必须上交叉验证看分布
  • xgb.cv 同时给均值与标准差,选型输出的是"分数区间 + 稳定性"
  • 代价矩阵决定阈值:500 对 80 的不对称代价把最优阈值从 0.5 拉到 0.31
  • 早停在每折内部独立发生,全局统一轮数会低估方差
  • 预处理要进 Pipeline,让交叉验证逐折重新拟合,避免泄漏翻版

分数与阈值都定了,模型从"能用"到"敢用"还差一步:打开黑箱看它依赖哪些特征——下一节的诊断。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U