模型评估与超参调优:交叉验证、Optuna 与模型选择策略

文章目录

    • [一、准确率 99% 的谎言](#一、准确率 99% 的谎言)
    • 二、分类评估指标:选对指标才能说真话
      • [2.1 指标适用场景全景](#2.1 指标适用场景全景)
      • [2.2 核心指标详解](#2.2 核心指标详解)
      • [2.3 回归评估指标](#2.3 回归评估指标)
    • [三、交叉验证策略:选错了 CV,评估就是幻觉](#三、交叉验证策略:选错了 CV,评估就是幻觉)
      • [3.1 四种交叉验证策略对比](#3.1 四种交叉验证策略对比)
      • [3.2 CV 策略选择错误示例](#3.2 CV 策略选择错误示例)
    • [四、超参搜索进化:从 Grid Search 到贝叶斯优化](#四、超参搜索进化:从 Grid Search 到贝叶斯优化)
      • [4.1 三种搜索策略对比](#4.1 三种搜索策略对比)
      • [4.2 Optuna 实战](#4.2 Optuna 实战)
    • 五、偏差-方差权衡:学习曲线诊断模型问题
      • [5.1 学习曲线的三种形态](#5.1 学习曲线的三种形态)
    • 六、集成方法与自动化模型选择
      • [6.1 Stacking vs Voting](#6.1 Stacking vs Voting)
      • [6.2 与 AutoML 的关系](#6.2 与 AutoML 的关系)
    • 七、实战:信用卡欺诈检测的完整评估链路
      • [7.1 完整代码](#7.1 完整代码)
      • [7.2 关键设计决策](#7.2 关键设计决策)
      • [7.3 效果对比](#7.3 效果对比)
    • 八、小结

一、准确率 99% 的谎言

在一个信用卡欺诈检测场景中,欺诈交易仅占全部交易的 0.1%。一个模型如果将所有交易都预测为"正常",准确率将高达 99.9%------但它一个欺诈案例都检测不出来。这就是"准确率陷阱":在不平衡数据上,高准确率往往与模型质量无关。

模型评估的核心问题不是"分数有多高",而是"这个分数在什么条件下成立"。交叉验证的策略选择错误、评估指标使用不当、超参搜索方式不科学,都会导致评估结果成为自欺欺人的幻觉。本文从评估指标、交叉验证策略、超参搜索方法和模型选择策略四个维度,建立一套严谨的模型评估体系,并以信用卡欺诈检测为实战场景,演示从数据不平衡到 Optuna 自动调参的完整链路。

二、分类评估指标:选对指标才能说真话

2.1 指标适用场景全景

#mermaid-svg-tYpjZ9vA6i8e5mYG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tYpjZ9vA6i8e5mYG .error-icon{fill:#552222;}#mermaid-svg-tYpjZ9vA6i8e5mYG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tYpjZ9vA6i8e5mYG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .marker.cross{stroke:#333333;}#mermaid-svg-tYpjZ9vA6i8e5mYG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tYpjZ9vA6i8e5mYG p{margin:0;}#mermaid-svg-tYpjZ9vA6i8e5mYG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .cluster-label text{fill:#333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .cluster-label span{color:#333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .cluster-label span p{background-color:transparent;}#mermaid-svg-tYpjZ9vA6i8e5mYG .label text,#mermaid-svg-tYpjZ9vA6i8e5mYG span{fill:#333;color:#333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .node rect,#mermaid-svg-tYpjZ9vA6i8e5mYG .node circle,#mermaid-svg-tYpjZ9vA6i8e5mYG .node ellipse,#mermaid-svg-tYpjZ9vA6i8e5mYG .node polygon,#mermaid-svg-tYpjZ9vA6i8e5mYG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tYpjZ9vA6i8e5mYG .rough-node .label text,#mermaid-svg-tYpjZ9vA6i8e5mYG .node .label text,#mermaid-svg-tYpjZ9vA6i8e5mYG .image-shape .label,#mermaid-svg-tYpjZ9vA6i8e5mYG .icon-shape .label{text-anchor:middle;}#mermaid-svg-tYpjZ9vA6i8e5mYG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tYpjZ9vA6i8e5mYG .rough-node .label,#mermaid-svg-tYpjZ9vA6i8e5mYG .node .label,#mermaid-svg-tYpjZ9vA6i8e5mYG .image-shape .label,#mermaid-svg-tYpjZ9vA6i8e5mYG .icon-shape .label{text-align:center;}#mermaid-svg-tYpjZ9vA6i8e5mYG .node.clickable{cursor:pointer;}#mermaid-svg-tYpjZ9vA6i8e5mYG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .arrowheadPath{fill:#333333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tYpjZ9vA6i8e5mYG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tYpjZ9vA6i8e5mYG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tYpjZ9vA6i8e5mYG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tYpjZ9vA6i8e5mYG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tYpjZ9vA6i8e5mYG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tYpjZ9vA6i8e5mYG .cluster text{fill:#333;}#mermaid-svg-tYpjZ9vA6i8e5mYG .cluster span{color:#333;}#mermaid-svg-tYpjZ9vA6i8e5mYG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tYpjZ9vA6i8e5mYG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tYpjZ9vA6i8e5mYG rect.text{fill:none;stroke-width:0;}#mermaid-svg-tYpjZ9vA6i8e5mYG .icon-shape,#mermaid-svg-tYpjZ9vA6i8e5mYG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tYpjZ9vA6i8e5mYG .icon-shape p,#mermaid-svg-tYpjZ9vA6i8e5mYG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tYpjZ9vA6i8e5mYG .icon-shape .label rect,#mermaid-svg-tYpjZ9vA6i8e5mYG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tYpjZ9vA6i8e5mYG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tYpjZ9vA6i8e5mYG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tYpjZ9vA6i8e5mYG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 平衡

正例 ~ 50%
轻度不平衡

正例 10%~30%
严重不平衡

正例 < 5%
减少误报

宁可漏检
减少漏检

宁可误报
两者兼顾
分类问题
类别是否平衡?
Accuracy / F1 / ROC-AUC
F1-Score /

ROC-AUC
PR-AUC /

Precision-Recall
可解释性强

适合业务汇报
F1 兼顾 Precision 和 Recall
PR-AUC 对少数类敏感

ROC-AUC 会高估性能
业务更看重什么?
高 Precision

严格阈值
高 Recall

宽松阈值
F1-Score

2.2 核心指标详解

混淆矩阵四象限

实际情况 \ 预测 预测为正 预测为负
实际为正 TP(真正例) FN(假负例)
实际为负 FP(假正例) TN(真负例)

Precision(精确率)

在所有预测为正例的样本中,实际为正例的比例。适用于"宁可漏检,不可误报"的场景,如垃圾邮件过滤------误将正常邮件标为垃圾的代价远高于漏掉一封垃圾邮件。

复制代码
Precision = TP / (TP + FP)

Recall(召回率)

在所有实际为正例的样本中,被正确预测为正例的比例。适用于"宁可误报,不可漏检"的场景,如疾病筛查------漏掉一个病人的代价远高于多检查几个健康人。

复制代码
Recall = TP / (TP + FN)

F1-Score

Precision 和 Recall 的调和平均,在两者之间取得平衡。当 Precision 和 Recall 存在冲突时,F1 是更全面的指标。

复制代码
F1 = 2 × Precision × Recall / (Precision + Recall)

ROC-AUC vs PR-AUC

ROC 曲线以假正例率(FPR)为横轴、真正例率(TPR)为横轴,AUC 表示随机抽取一个正例和一个负例,模型将正例排在负例前面的概率。ROC-AUC 在不平衡数据上表现稳定,但对少数类的变化不敏感。

PR 曲线以 Recall 为横轴、Precision 为纵轴,PR-AUC 直接衡量模型在正例上的查准-查全权衡。在类别严重不平衡(正例 < 5%)时,PR-AUC 比 ROC-AUC 更能反映模型的真实能力------因为 ROC 的 FPR 分母(TN + FP)中 TN 数量巨大,FP 的变化几乎不会改变 FPR。

python 复制代码
from sklearn.metrics import roc_auc_score, average_precision_score

roc_auc = roc_auc_score(y_true, y_proba)
pr_auc = average_precision_score(y_true, y_proba)

# 严重不平衡时,PR-AUC 才是可信指标
print(f"ROC-AUC: {roc_auc:.4f}")      # 可能高达 0.98
print(f"PR-AUC: {pr_auc:.4f}")        # 可能只有 0.45
指标 公式 适用场景 不平衡数据可靠性
Accuracy (TP+TN)/(TP+TN+FP+FN) 类别完全平衡
Precision TP/(TP+FP) 重视误报控制
Recall TP/(TP+FN) 重视漏检控制
F1 2PR/(P+R) 平衡 Precision/Recall
ROC-AUC ROC 曲线下面积 通用,跨模型比较 中高
PR-AUC PR 曲线下面积 严重不平衡(正例<5%)

2.3 回归评估指标

指标 公式 特性 适用场景
MAE mean(|y - ŷ|) 对异常值不敏感 异常值较多的数据
MSE mean((y - ŷ)²) 对大误差惩罚重 大误差代价高的场景
RMSE sqrt(MSE) 与目标变量同量纲 通用,最常用
1 - MSE/var(y) 被解释方差比例 模型拟合度评估
MAPE mean(|y - ŷ|/y) 百分比误差 目标值范围跨度大的场景

RMSE 对大误差敏感的原因在于平方运算:一个误差为 10 的样本对 MSE 的贡献等于 100 个误差为 1 的样本。因此,在房价预测等场景中,如果大误差(如豪宅预测偏差)的代价远高于小误差,RMSE 是比 MAE 更合适的选择。

三、交叉验证策略:选错了 CV,评估就是幻觉

交叉验证的核心目的是估计模型在未见数据上的泛化能力。但 CV 策略的选择直接影响这个估计的可信度------用错了 CV 策略,评估结果可能比单折划分更不可靠。

3.1 四种交叉验证策略对比

#mermaid-svg-AF5c6c6flGxypkw5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AF5c6c6flGxypkw5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AF5c6c6flGxypkw5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AF5c6c6flGxypkw5 .error-icon{fill:#552222;}#mermaid-svg-AF5c6c6flGxypkw5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AF5c6c6flGxypkw5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AF5c6c6flGxypkw5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AF5c6c6flGxypkw5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AF5c6c6flGxypkw5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AF5c6c6flGxypkw5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AF5c6c6flGxypkw5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AF5c6c6flGxypkw5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AF5c6c6flGxypkw5 .marker.cross{stroke:#333333;}#mermaid-svg-AF5c6c6flGxypkw5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AF5c6c6flGxypkw5 p{margin:0;}#mermaid-svg-AF5c6c6flGxypkw5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AF5c6c6flGxypkw5 .cluster-label text{fill:#333;}#mermaid-svg-AF5c6c6flGxypkw5 .cluster-label span{color:#333;}#mermaid-svg-AF5c6c6flGxypkw5 .cluster-label span p{background-color:transparent;}#mermaid-svg-AF5c6c6flGxypkw5 .label text,#mermaid-svg-AF5c6c6flGxypkw5 span{fill:#333;color:#333;}#mermaid-svg-AF5c6c6flGxypkw5 .node rect,#mermaid-svg-AF5c6c6flGxypkw5 .node circle,#mermaid-svg-AF5c6c6flGxypkw5 .node ellipse,#mermaid-svg-AF5c6c6flGxypkw5 .node polygon,#mermaid-svg-AF5c6c6flGxypkw5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AF5c6c6flGxypkw5 .rough-node .label text,#mermaid-svg-AF5c6c6flGxypkw5 .node .label text,#mermaid-svg-AF5c6c6flGxypkw5 .image-shape .label,#mermaid-svg-AF5c6c6flGxypkw5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-AF5c6c6flGxypkw5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AF5c6c6flGxypkw5 .rough-node .label,#mermaid-svg-AF5c6c6flGxypkw5 .node .label,#mermaid-svg-AF5c6c6flGxypkw5 .image-shape .label,#mermaid-svg-AF5c6c6flGxypkw5 .icon-shape .label{text-align:center;}#mermaid-svg-AF5c6c6flGxypkw5 .node.clickable{cursor:pointer;}#mermaid-svg-AF5c6c6flGxypkw5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AF5c6c6flGxypkw5 .arrowheadPath{fill:#333333;}#mermaid-svg-AF5c6c6flGxypkw5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AF5c6c6flGxypkw5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AF5c6c6flGxypkw5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AF5c6c6flGxypkw5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AF5c6c6flGxypkw5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AF5c6c6flGxypkw5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AF5c6c6flGxypkw5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AF5c6c6flGxypkw5 .cluster text{fill:#333;}#mermaid-svg-AF5c6c6flGxypkw5 .cluster span{color:#333;}#mermaid-svg-AF5c6c6flGxypkw5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AF5c6c6flGxypkw5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AF5c6c6flGxypkw5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-AF5c6c6flGxypkw5 .icon-shape,#mermaid-svg-AF5c6c6flGxypkw5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AF5c6c6flGxypkw5 .icon-shape p,#mermaid-svg-AF5c6c6flGxypkw5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AF5c6c6flGxypkw5 .icon-shape .label rect,#mermaid-svg-AF5c6c6flGxypkw5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AF5c6c6flGxypkw5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AF5c6c6flGxypkw5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AF5c6c6flGxypkw5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 普通 IID 数据
类别不平衡
存在分组结构

同组样本相关
时间序列
数据集
数据特点?
KFold

随机分 K 折
StratifiedKFold

每折保持类别比例
GroupKFold

同组不出现在多折
TimeSeriesSplit

训练集只包含过去数据
通用场景

默认选择
分类任务必用

尤其不平衡
医学影像

同一患者多切片

推荐系统同一用户多条记录
时间序列/金融预测

防止未来信息泄漏

KFold

将数据随机分为 K 份,轮流用 K-1 份训练、1 份验证。K=5 或 K=10 是最常用的配置。适用于数据独立同分布(IID)的场景。

python 复制代码
from sklearn.model_selection import KFold

kf = KFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in kf.split(X):
    X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]

StratifiedKFold

KFold 的改进版,确保每一折中各类别的比例与整体数据一致。在分类任务中,尤其是类别不平衡时,StratifiedKFold 是必须的选择。普通 KFold 可能将某一折的所有正例都分到了验证集,导致训练集没有正例可学。

python 复制代码
from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in skf.split(X, y):
    X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]

GroupKFold

当数据存在分组结构时使用。例如,医学影像数据中同一患者的多张切片高度相关,如果将同一患者的切片分散到训练集和验证集,模型会"记住"患者特征而非学到真正的病理模式。GroupKFold 确保同一组的所有样本要么全在训练集,要么全在验证集。

python 复制代码
from sklearn.model_selection import GroupKFold

gkf = GroupKFold(n_splits=5)
for train_idx, val_idx in gkf.split(X, y, groups=patient_ids):
    X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]

TimeSeriesSplit

时间序列数据的专用 CV 策略。训练集只包含验证集之前的时间点,严格模拟"用过去预测未来"的真实场景。普通 KFold 会随机打乱时间顺序,导致未来数据泄漏到训练集------在时间序列中这是致命错误。

python 复制代码
from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for train_idx, val_idx in tscv.split(X):
    # 训练集索引总是小于验证集索引
    X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]

3.2 CV 策略选择错误示例

数据类型 错误策略 后果
欺诈检测(0.1% 正例) KFold 某折可能无正例,训练失效
患者影像(同患者多切片) KFold 患者特征泄漏,高估性能
股票价格预测 KFold 未来价格泄漏到训练集
多城市房价 KFold 城市间分布差异被忽略

4.1 三种搜索策略对比

#mermaid-svg-6iK70sGXtZs5smRE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6iK70sGXtZs5smRE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6iK70sGXtZs5smRE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6iK70sGXtZs5smRE .error-icon{fill:#552222;}#mermaid-svg-6iK70sGXtZs5smRE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6iK70sGXtZs5smRE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6iK70sGXtZs5smRE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6iK70sGXtZs5smRE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6iK70sGXtZs5smRE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6iK70sGXtZs5smRE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6iK70sGXtZs5smRE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6iK70sGXtZs5smRE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6iK70sGXtZs5smRE .marker.cross{stroke:#333333;}#mermaid-svg-6iK70sGXtZs5smRE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6iK70sGXtZs5smRE p{margin:0;}#mermaid-svg-6iK70sGXtZs5smRE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6iK70sGXtZs5smRE .cluster-label text{fill:#333;}#mermaid-svg-6iK70sGXtZs5smRE .cluster-label span{color:#333;}#mermaid-svg-6iK70sGXtZs5smRE .cluster-label span p{background-color:transparent;}#mermaid-svg-6iK70sGXtZs5smRE .label text,#mermaid-svg-6iK70sGXtZs5smRE span{fill:#333;color:#333;}#mermaid-svg-6iK70sGXtZs5smRE .node rect,#mermaid-svg-6iK70sGXtZs5smRE .node circle,#mermaid-svg-6iK70sGXtZs5smRE .node ellipse,#mermaid-svg-6iK70sGXtZs5smRE .node polygon,#mermaid-svg-6iK70sGXtZs5smRE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6iK70sGXtZs5smRE .rough-node .label text,#mermaid-svg-6iK70sGXtZs5smRE .node .label text,#mermaid-svg-6iK70sGXtZs5smRE .image-shape .label,#mermaid-svg-6iK70sGXtZs5smRE .icon-shape .label{text-anchor:middle;}#mermaid-svg-6iK70sGXtZs5smRE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6iK70sGXtZs5smRE .rough-node .label,#mermaid-svg-6iK70sGXtZs5smRE .node .label,#mermaid-svg-6iK70sGXtZs5smRE .image-shape .label,#mermaid-svg-6iK70sGXtZs5smRE .icon-shape .label{text-align:center;}#mermaid-svg-6iK70sGXtZs5smRE .node.clickable{cursor:pointer;}#mermaid-svg-6iK70sGXtZs5smRE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6iK70sGXtZs5smRE .arrowheadPath{fill:#333333;}#mermaid-svg-6iK70sGXtZs5smRE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6iK70sGXtZs5smRE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6iK70sGXtZs5smRE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6iK70sGXtZs5smRE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6iK70sGXtZs5smRE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6iK70sGXtZs5smRE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6iK70sGXtZs5smRE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6iK70sGXtZs5smRE .cluster text{fill:#333;}#mermaid-svg-6iK70sGXtZs5smRE .cluster span{color:#333;}#mermaid-svg-6iK70sGXtZs5smRE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6iK70sGXtZs5smRE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6iK70sGXtZs5smRE rect.text{fill:none;stroke-width:0;}#mermaid-svg-6iK70sGXtZs5smRE .icon-shape,#mermaid-svg-6iK70sGXtZs5smRE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6iK70sGXtZs5smRE .icon-shape p,#mermaid-svg-6iK70sGXtZs5smRE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6iK70sGXtZs5smRE .icon-shape .label rect,#mermaid-svg-6iK70sGXtZs5smRE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6iK70sGXtZs5smRE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6iK70sGXtZs5smRE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6iK70sGXtZs5smRE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 超参搜索
Grid Search

全组合穷举
Random Search

随机采样
Bayesian Optimization

智能采样
优点:保证找到最优

缺点:指数爆炸
优点:更高效

缺点:可能遗漏好区域
优点:最快收敛

缺点:需多次迭代
100 个参数组合

5 折 CV = 500 次训练
100 次随机采样

5 折 CV = 500 次训练

但覆盖更广
100 次 trial

每次利用历史结果

指导下一步采样

Grid Search(网格搜索)

对每个超参定义一组候选值,穷举所有组合。假设 4 个超参,每个 5 个候选值,总组合数为 5⁴ = 625。配合 5 折 CV,需要训练 3125 次模型。Grid Search 在超参维度低时可行,但维度稍高就会陷入"组合爆炸"。

python 复制代码
from sklearn.model_selection import GridSearchCV

param_grid = {
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1],
    'n_estimators': [100, 200]
}
grid = GridSearchCV(
    XGBClassifier(random_state=42),
    param_grid,
    cv=StratifiedKFold(5),
    scoring='average_precision',  # PR-AUC
    n_jobs=-1
)
grid.fit(X, y)

Random Search(随机搜索)

在超参空间中随机采样固定次数(如 100 次),而非穷举所有组合。Bergstra & Bengio 的论文证明,在相同计算预算下,Random Search 通常能找到与 Grid Search 相当甚至更好的解------因为并非所有超参都同等重要,Random Search 给重要超参更多探索机会。

python 复制代码
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform, randint

param_distributions = {
    'max_depth': randint(3, 10),
    'learning_rate': uniform(0.001, 0.3),
    'n_estimators': randint(100, 1000)
}
random = RandomizedSearchCV(
    XGBClassifier(random_state=42),
    param_distributions,
    n_iter=100,
    cv=StratifiedKFold(5),
    scoring='average_precision',
    n_jobs=-1,
    random_state=42
)

Bayesian Optimization(贝叶斯优化)

利用已完成的 trial 结果构建超参空间的代理模型(通常是高斯过程或 Tree-structured Parzen Estimator),预测哪些区域可能产生更好的结果,从而智能地选择下一个采样点。相比 Random Search 的"盲目尝试",Bayesian Optimization 是"有依据地尝试"。

Optuna 是当前 Python 生态中最成熟的贝叶斯优化框架。

4.2 Optuna 实战

Optuna 的核心 API 简洁但功能强大。定义一个 objective 函数,在其中声明待搜索的超参,Optuna 会自动管理搜索过程。

python 复制代码
import optuna
from sklearn.model_selection import cross_val_score
from xgboost import XGBClassifier

# 早停回调------如果 PR-AUC 连续 10 轮无提升则终止当前 trial
early_stop = optuna.integration.XGBoostPruningCallback

def objective(trial):
    # 声明搜索空间
    params = {
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'learning_rate': trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True),
        'n_estimators': trial.suggest_int('n_estimators', 100, 2000),
        'subsample': trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),
        'gamma': trial.suggest_float('gamma', 1e-8, 1.0, log=True),
        'scale_pos_weight': trial.suggest_float('scale_pos_weight', 1, 100, log=True),
        'random_state': 42,
        'use_label_encoder': False,
        'eval_metric': 'logloss'
    }

    model = XGBClassifier(**params)

    # 使用 StratifiedKFold + PR-AUC
    scores = cross_val_score(
        model, X, y,
        cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42),
        scoring='average_precision',
        n_jobs=-1
    )

    return scores.mean()

# 创建 study,目标最大化 PR-AUC
study = optuna.create_study(
    direction='maximize',
    pruner=optuna.pruners.MedianPruner(n_startup_trials=10)
)

# 运行 100 次 trial
study.optimize(objective, n_trials=100, show_progress_bar=True)

# 最优结果
print(f"最优 PR-AUC: {study.best_value:.4f}")
print(f"最优参数: {study.best_params}")

Optuna 的进阶特性

对数尺度采样suggest_float(..., log=True)learning_rate 这类跨多个数量级的超参非常必要。线性采样会在 0.001~0.1 之间均匀取值,而对数采样会在每个数量级(0.0010.01、0.010.1)均匀取值,更好地覆盖低学习率区域。

早停剪枝(Pruning)MedianPruner 在 trial 运行过程中,如果当前 trial 的表现低于已完成 trial 的中位数,会提前终止该 trial,节省计算资源。

可视化分析

python 复制代码
import optuna.visualization as vis

# 优化历史------每个 trial 的 PR-AUC 随时间变化
fig1 = vis.plot_optimization_history(study)
fig1.show()

# 参数重要性------哪些超参对性能影响最大
fig2 = vis.plot_param_importances(study)
fig2.show()

# 并行坐标图------高维参数空间的可视化
fig3 = vis.plot_parallel_coordinate(study)
fig3.show()

# 等高线图------两个超参的交互效应
fig4 = vis.plot_contour(study, params=['max_depth', 'learning_rate'])
fig4.show()

五、偏差-方差权衡:学习曲线诊断模型问题

模型在训练集上表现很好但在验证集上很差,这是过拟合(高方差)。模型在训练集和验证集上表现都很差,这是欠拟合(高偏差)。学习曲线是诊断这两种问题的标准工具。

5.1 学习曲线的三种形态

#mermaid-svg-hkJs3wb24Q5xRfzB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hkJs3wb24Q5xRfzB .error-icon{fill:#552222;}#mermaid-svg-hkJs3wb24Q5xRfzB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hkJs3wb24Q5xRfzB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hkJs3wb24Q5xRfzB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hkJs3wb24Q5xRfzB .marker.cross{stroke:#333333;}#mermaid-svg-hkJs3wb24Q5xRfzB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hkJs3wb24Q5xRfzB p{margin:0;}#mermaid-svg-hkJs3wb24Q5xRfzB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hkJs3wb24Q5xRfzB .cluster-label text{fill:#333;}#mermaid-svg-hkJs3wb24Q5xRfzB .cluster-label span{color:#333;}#mermaid-svg-hkJs3wb24Q5xRfzB .cluster-label span p{background-color:transparent;}#mermaid-svg-hkJs3wb24Q5xRfzB .label text,#mermaid-svg-hkJs3wb24Q5xRfzB span{fill:#333;color:#333;}#mermaid-svg-hkJs3wb24Q5xRfzB .node rect,#mermaid-svg-hkJs3wb24Q5xRfzB .node circle,#mermaid-svg-hkJs3wb24Q5xRfzB .node ellipse,#mermaid-svg-hkJs3wb24Q5xRfzB .node polygon,#mermaid-svg-hkJs3wb24Q5xRfzB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hkJs3wb24Q5xRfzB .rough-node .label text,#mermaid-svg-hkJs3wb24Q5xRfzB .node .label text,#mermaid-svg-hkJs3wb24Q5xRfzB .image-shape .label,#mermaid-svg-hkJs3wb24Q5xRfzB .icon-shape .label{text-anchor:middle;}#mermaid-svg-hkJs3wb24Q5xRfzB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hkJs3wb24Q5xRfzB .rough-node .label,#mermaid-svg-hkJs3wb24Q5xRfzB .node .label,#mermaid-svg-hkJs3wb24Q5xRfzB .image-shape .label,#mermaid-svg-hkJs3wb24Q5xRfzB .icon-shape .label{text-align:center;}#mermaid-svg-hkJs3wb24Q5xRfzB .node.clickable{cursor:pointer;}#mermaid-svg-hkJs3wb24Q5xRfzB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hkJs3wb24Q5xRfzB .arrowheadPath{fill:#333333;}#mermaid-svg-hkJs3wb24Q5xRfzB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hkJs3wb24Q5xRfzB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hkJs3wb24Q5xRfzB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hkJs3wb24Q5xRfzB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hkJs3wb24Q5xRfzB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hkJs3wb24Q5xRfzB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hkJs3wb24Q5xRfzB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hkJs3wb24Q5xRfzB .cluster text{fill:#333;}#mermaid-svg-hkJs3wb24Q5xRfzB .cluster span{color:#333;}#mermaid-svg-hkJs3wb24Q5xRfzB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hkJs3wb24Q5xRfzB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hkJs3wb24Q5xRfzB rect.text{fill:none;stroke-width:0;}#mermaid-svg-hkJs3wb24Q5xRfzB .icon-shape,#mermaid-svg-hkJs3wb24Q5xRfzB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hkJs3wb24Q5xRfzB .icon-shape p,#mermaid-svg-hkJs3wb24Q5xRfzB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hkJs3wb24Q5xRfzB .icon-shape .label rect,#mermaid-svg-hkJs3wb24Q5xRfzB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hkJs3wb24Q5xRfzB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hkJs3wb24Q5xRfzB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hkJs3wb24Q5xRfzB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 平衡状态
训练分数略高于验证分数
差距小且收敛
继续增加数据

边际收益递减
高方差(过拟合)
训练分数 >> 验证分数
差距大
增加数据有帮助
解决方案:

正则化 / 减少特征 / 更多数据
高偏差(欠拟合)
训练分数 ~ 验证分数
两者都低
增加数据无帮助
解决方案:

增加特征 / 增加模型复杂度

python 复制代码
from sklearn.model_selection import learning_curve
import numpy as np
import matplotlib.pyplot as plt

train_sizes, train_scores, val_scores = learning_curve(
    estimator=model, X=X, y=y,
    cv=StratifiedKFold(5),
    scoring='average_precision',
    train_sizes=np.linspace(0.1, 1.0, 10),
    n_jobs=-1
)

train_mean = train_scores.mean(axis=1)
val_mean = val_scores.mean(axis=1)

plt.plot(train_sizes, train_mean, label='Training PR-AUC')
plt.plot(train_sizes, val_mean, label='Validation PR-AUC')
plt.fill_between(train_sizes, train_mean - train_scores.std(axis=1),
                 train_mean + train_scores.std(axis=1), alpha=0.1)
plt.fill_between(train_sizes, val_mean - val_scores.std(axis=1),
                 val_mean + val_scores.std(axis=1), alpha=0.1)
plt.xlabel('Training Set Size')
plt.ylabel('PR-AUC')
plt.legend()
plt.title('Learning Curve')
plt.show()

六、集成方法与自动化模型选择

6.1 Stacking vs Voting

VotingClassifier(投票集成)

多个基模型独立预测,按投票规则(硬投票:多数表决;软投票:概率加权平均)得出最终结果。实现简单,但基模型间差异越大,集成效果越好。

python 复制代码
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier

voting = VotingClassifier(
    estimators=[
        ('lr', LogisticRegression(max_iter=1000)),
        ('rf', RandomForestClassifier(n_estimators=200)),
        ('xgb', XGBClassifier(use_label_encoder=False, eval_metric='logloss'))
    ],
    voting='soft'  # 软投票通常优于硬投票
)

StackingClassifier(堆叠集成)

两层结构:第一层用多个基模型生成预测结果(元特征),第二层用另一个模型(元学习器,通常为逻辑回归)学习如何组合这些元特征。Stacking 比 Voting 更灵活,但需要注意第一层模型需用交叉验证生成元特征,防止数据泄漏。

python 复制代码
from sklearn.ensemble import StackingClassifier

stacking = StackingClassifier(
    estimators=[
        ('lr', LogisticRegression(max_iter=1000)),
        ('rf', RandomForestClassifier(n_estimators=200)),
        ('xgb', XGBClassifier(use_label_encoder=False, eval_metric='logloss'))
    ],
    final_estimator=LogisticRegression(max_iter=1000),
    cv=5,
    passthrough=False  # 是否将原始特征也传给元学习器
)

6.2 与 AutoML 的关系

Stacking 和 Voting 是 AutoML 框架(如 AutoGluon、auto-sklearn)的核心组件。AutoML 的本质是自动完成"模型选择 + 超参优化 + 集成"的循环。对于需要快速原型验证的场景,AutoGluon 可以在一行代码内完成上述全部流程:

python 复制代码
from autogluon.tabular import TabularPredictor

predictor = TabularPredictor(label='target', eval_metric='average_precision').fit(
    train_data=df,
    presets='best_quality',
    time_limit=3600
)

但 AutoML 的黑盒特性意味着难以调试和解释。在生产环境中,理解每个超参的作用、每个评估指标的含义,仍然是不可替代的工程能力。

七、实战:信用卡欺诈检测的完整评估链路

这是一个典型的极端不平衡分类问题:欺诈交易仅占 0.1%,评估指标、CV 策略和采样方法的选择都直接影响模型效果。

7.1 完整代码

python 复制代码
import pandas as pd
import numpy as np
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.metrics import classification_report, average_precision_score
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline
from xgboost import XGBClassifier
import optuna

# 加载数据(creditcard.csv 是公开数据集)
df = pd.read_csv('creditcard.csv')
X = df.drop('Class', axis=1)
y = df['Class']

# 特征标准化(除 Time 和 Amount 外,其他特征已 PCA 处理)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X[['Time', 'Amount']] = scaler.fit_transform(X[['Time', 'Amount']])

# Optuna 目标函数
def objective(trial):
    params = {
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'learning_rate': trial.suggest_float('learning_rate', 1e-3, 3e-1, log=True),
        'n_estimators': trial.suggest_int('n_estimators', 100, 2000),
        'subsample': trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'scale_pos_weight': trial.suggest_float('scale_pos_weight', 10, 1000, log=True),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),
        'random_state': 42,
        'use_label_encoder': False,
        'eval_metric': 'logloss'
    }

    model = XGBClassifier(**params)

    # StratifiedKFold 保证每折都包含欺诈样本
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

    # 注意:SMOTE 必须在每折内部应用,不能全局过采样后再 CV
    scores = []
    for train_idx, val_idx in skf.split(X, y):
        X_train_fold, X_val_fold = X.iloc[train_idx], X.iloc[val_idx]
        y_train_fold, y_val_fold = y.iloc[train_idx], y.iloc[val_idx]

        # 只在训练 fold 内做 SMOTE
        smote = SMOTE(random_state=42)
        X_resampled, y_resampled = smote.fit_resample(X_train_fold, y_train_fold)

        model.fit(X_resampled, y_resampled)
        y_proba = model.predict_proba(X_val_fold)[:, 1]
        scores.append(average_precision_score(y_val_fold, y_proba))

    return np.mean(scores)

# 运行优化
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=50, show_progress_bar=True)

print(f"最优 PR-AUC: {study.best_value:.4f}")
print(f"最优参数: {study.best_params}")

7.2 关键设计决策

决策点 选择 原因
评估指标 PR-AUC 类别极度不平衡(0.1%),ROC-AUC 会高估
CV 策略 StratifiedKFold 保证每折都有欺诈样本
采样方法 SMOTE(折内应用) 全局 SMOTE 会导致验证集数据泄漏
超参搜索 Optuna 贝叶斯优化 50 次 trial 远超 Grid Search 的覆盖能力
类别权重 scale_pos_weight XGBoost 原生支持,与 SMOTE 可叠加使用

7.3 效果对比

方案 PR-AUC 说明
基线(无处理,Accuracy 评估) 0.78 被 Accuracy 误导,实际 PR-AUC 很低
+ StratifiedKFold + PR-AUC 0.82 正确的评估方式暴露了真实性能
+ SMOTE 过采样 0.89 折内 SMOTE 缓解了类别不平衡
+ Optuna 调参 0.93 贝叶斯优化找到最优超参组合
+ scale_pos_weight 调优 0.95 类别权重与 SMOTE 的协同效应

八、小结

模型评估的可靠性建立在三个正确选择之上:正确的评估指标、正确的交叉验证策略、正确的超参搜索方法。

在不平衡数据上,Accuracy 是最大的谎言。PR-AUC 比 ROC-AUC 更能反映模型对少数类的识别能力。StratifiedKFold 是分类任务的默认选择,GroupKFold 用于分组数据,TimeSeriesSplit 用于时序数据------用错了 CV 策略,评估结果就是自欺欺人的幻觉。

超参搜索从 Grid Search 的全组合穷举,进化到 Random Search 的随机采样,再到 Bayesian Optimization 的智能采样。Optuna 的 log=True 对数尺度、MedianPruner 早停剪枝、以及可视化分析工具,将调参从"玄学"变成了有据可依的工程实践。

学习曲线是诊断模型问题的 X 光片:训练集和验证集分数都低 → 高偏差,需要增加特征或模型复杂度;训练集远高于验证集 → 高方差,需要正则化或更多数据。

此前专栏关于特征工程系统方法论、scikit-learn Pipeline 工程化以及 Pandas 数据处理的文章,为本文提供了从特征构造到模型评估的完整上游支撑。如果本文对模型评估与调优实践有所启发,欢迎点赞、收藏与关注。

相关推荐
RS&2 小时前
DAHITI水位数据产品批量下载(python)
python
27669582922 小时前
逆向视角解决:wsgsig dd03/dd05算法生成
python·滴滴出行·dd03·dd05·wsgsig·wsgsig算法·wsgsig逆向
小e说说2 小时前
海同科技可信吗?16年IT教育品牌深度实测解析
大数据·人工智能
apcipot_rain2 小时前
计科八股20260609——10分钟速通《线性代数》,知识点极简版
人工智能·线性代数·机器学习
步步为营DotNet2 小时前
Semantic Kernel 在.NET AI 开发中的深度探索与实践
人工智能·.net
AC赳赳老秦2 小时前
技术文章素材收集自动化:用 OpenClaw 自动爬取行业资讯、技术热点、优质文章
运维·开发语言·python·自动化·wpf·deepseek·openclaw
安全指北针2 小时前
AI检测 vs 传统SIEM:2026年安全运营效率实测对比
人工智能·安全
一次旅行2 小时前
【AI工具】Odysseus:GitHub 6万星自托管AI工作空间,隐私优先的本地化AI体验
人工智能·github
网络研究院2 小时前
利用人工智能破解中世纪密码
人工智能·研究·历史·语言·情报