第九章 机器学习 -- 模型评估与调优

一、评估指标

1.1 分类指标

评估指标是"模型好坏的量化"。分类任务与回归任务的指标族完全不同------选错指标会让模型看似很好、实际无用。

基础:混淆矩阵

二分类的预测与真实标签组合成 2×22 \times 22×2 矩阵:

实际正 实际负
预测正 TP FP
预测负 FN TN
  • TP(True Positive):实际正、预测正
  • FP(False Positive):实际负、预测正(误报)
  • FN(False Negative):实际正、预测负(漏报)
  • TN(True Negative):实际负、预测负

准确率(Accuracy)

Accuracy=TP+TNTP+TN+FP+FN\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}Accuracy=TP+TN+FP+FNTP+TN

  • 优点:直观、容易解释。
  • 致命缺点:类别严重不平衡时失效。如 99% 负样本,预测全为负的模型也能 99% 准确,但实际无法识别任何正类。
  • 适用:类别大致平衡。

精确率(Precision)与召回率(Recall)

  • 精确率(查准率) :预测为正的样本中实际为正的比例------"我说是正的话里,多少真为正":
    Precision=TPTP+FP\text{Precision} = \frac{TP}{TP + FP}Precision=TP+FPTP
  • 召回率(查全率 / 灵敏度) :实际为正的样本中预测为正的比例------"实际正的样本里,我识别出多少":
    Recall=TPTP+FN\text{Recall} = \frac{TP}{TP + FN}Recall=TP+FNTP
  • F1 Score :精确率与召回率的调和平均:
    F1=2⋅Precision⋅RecallPrecision+RecallF_1 = \frac{2 \cdot \text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}F1=Precision+Recall2⋅Precision⋅Recall

精确率与召回率常此消彼长(提高阈值 → 精确率 ↑、召回率 ↓;降低阈值 → 反之)。

ROC 曲线与 AUC

ROC(Receiver Operating Characteristic)以 FPR 为横轴、TPR(Recall) 为纵轴,描绘阈值从 0 到 1 时的性能轨迹:

  • FPR=FPFP+TN\text{FPR} = \frac{FP}{FP + TN}FPR=FP+TNFP(实际负样本被误报为正的比例)
  • TPR=Recall\text{TPR} = \text{Recall}TPR=Recall

AUC(Area Under Curve) 是 ROC 曲线下面积:

  • AUC = 1.0:完美模型
  • AUC = 0.5:与随机猜测无异
  • AUC < 0.5:模型反向(取反即改善)
  • 含义:随机正样本的预测分数高于随机负样本的概率

AUC vs Accuracy

  • AUC 对阈值不敏感(只关心排序)。
  • AUC 在类别不平衡时仍稳健(关注排序而非绝对预测)。
  • Accuracy 在不平衡时失效。

PR 曲线与 AP(Average Precision)

PR 曲线以 Recall 为横轴、Precision 为纵轴。在严重不平衡 或关注少数类时,PR 曲线比 ROC 更敏感。

AP(Average Precision) 是 PR 曲线下面积的近似(按 Recall 加权平均 Precision):

AP=∑n(Rn−Rn−1)Pn\text{AP} = \sum_n (R_n - R_{n-1}) P_nAP=n∑(Rn−Rn−1)Pn

sklearn.metrics.average_precision_score。

多分类指标

  • Macro-F1:各类 F1 算术平均,等权重。
  • Micro-F1:所有类的 TP/FP/FN 加总后算 F1,等于准确率。
  • Weighted-F1:按各类样本数加权的 F1。
  • Cohen's Kappa :考虑随机一致性的修正准确率,−1-1−1 到 111。
python 复制代码
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
                              f1_score, classification_report)
print(classification_report(y_test, y_pred, digits=4))

Top-K 准确率

多分类中常用:

  • Top-1:预测概率最高的类 = 真实类。
  • Top-5:预测概率前 5 类中含真实类。

ImageNet 等大规模任务上 Top-5 是主流。

Log Loss / Cross-Entropy

对概率输出的评分:

LogLoss=−1N∑i∑kyiklog⁡pik\text{LogLoss} = -\frac{1}{N} \sum_i \sum_k y_{ik} \log p_{ik}LogLoss=−N1i∑k∑yiklogpik

对预测概率的置信度 敏感(过度自信会被罚),比准确率更"严苛"。log_loss。

Brier Score

预测概率的均方误差:

Brier=1N∑i∑k(pik−yik)2\text{Brier} = \frac{1}{N} \sum_i \sum_k (p_{ik} - y_{ik})^2Brier=N1i∑k∑(pik−yik)2

越小越好;与 log loss 互补,对校准(calibration)敏感。

校准(Calibration)

模型输出 P(y=1∣x)P(y=1 | x)P(y=1∣x) 是否对应真实频率?

  • 可靠性图(reliability diagram):分桶后看预测概率 vs 实际频率。
  • 校准方法:Platt scaling、温度缩放(temperature scaling)、isotonic regression。
  • sklearn.calibration.CalibratedClassifierCV。

指标选择流程

业务场景 推荐指标
类别平衡 准确率、F1
严重不平衡(少数类关键) PR-AUC、F1、Recall@K
排序/排名 ROC-AUC、NDCG、MAP
概率预测 LogLoss、Brier Score
多分类 Macro-F1、Weighted-F1
阈值敏感 调阈值看 Precision-Recall 曲线

scikit-learn 速查

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

roc_auc_score(y_test, y_proba)              # 二分类
average_precision_score(y_test, y_proba)    # AP(PR-AUC 近似)
f1_score(y_test, y_pred)                    # F1

易错点

  • 用准确率评估不平衡数据:看似 99% 实际毫无意义;改用 F1 / PR-AUC。
  • AUC 高但实际业务差:AUC 只看排序,不看阈值;若业务对绝对概率敏感,需校准。
  • 多分类指标算错 :用 average="macro" 严格等权;用 average="weighted" 按样本数加权。
  • log loss 用于无概率输出 :SVM 原输出是函数距离,要先 probability=True 或 Platt scaling。

1.2 回归指标

MSE(Mean Squared Error)

MSE=1N∑i(yi−y^i)2\text{MSE} = \frac{1}{N} \sum_i (y_i - \hat{y}_i)^2MSE=N1i∑(yi−y^i)2

  • 优点:凸、可微、最小化 MSE 等价于高斯似然。
  • 缺点:对异常值极度敏感(平方放大)。

RMSE(Root Mean Squared Error)

RMSE=MSE\text{RMSE} = \sqrt{\text{MSE}}RMSE=MSE

与 yyy 同量纲,更易解释。

MAE(Mean Absolute Error)

MAE=1N∑i∣yi−y^i∣\text{MAE} = \frac{1}{N} \sum_i |y_i - \hat{y}_i|MAE=N1i∑∣yi−y^i∣

  • 优点:对异常值鲁棒(线性增长)。
  • 缺点:在 0 处不可微;最小化 MAE 等价于 Laplace 似然(中位数预测)。

R2R^2R2(决定系数)

R2=1−∑(yi−y^i)2∑(yi−yˉ)2=1−SSRSSTR^2 = 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \bar{y})^2} = 1 - \frac{\text{SSR}}{\text{SST}}R2=1−∑(yi−yˉ)2∑(yi−y^i)2=1−SSTSSR

  • R2=1R^2 = 1R2=1:完美拟合。
  • R2=0R^2 = 0R2=0:与均值基线(yˉ\bar yyˉ)相当。
  • R2<0R^2 < 0R2<0:比均值基线还差(训练时罕见、多见于有正则化时;测试集上外推失效时也会出现)。
  • 调整 R2R^2R2 :Rˉ2=1−(1−R2)(N−1)/(N−p−1)\bar R^2 = 1 - (1 - R^2)(N-1)/(N-p-1)Rˉ2=1−(1−R2)(N−1)/(N−p−1),ppp 为特征数(不含截距列),惩罚加特征。

解释方差(Explained Variance)

EV=1−Var(y−y^)Var(y)\text{EV} = 1 - \frac{\text{Var}(y - \hat{y})}{\text{Var}(y)}EV=1−Var(y)Var(y−y^)

与 R2R^2R2 类似但分子是方差而非平方和;对系统误差不敏感。

MAPE(Mean Absolute Percentage Error)

MAPE=100%N∑i∣yi−y^iyi∣\text{MAPE} = \frac{100\%}{N} \sum_i \left| \frac{y_i - \hat{y}_i}{y_i} \right|MAPE=N100%i∑ yiyi−y^i

  • 优点:相对误差,与量级无关。
  • 缺点:yi=0y_i = 0yi=0 时未定义;对低 yyy 的误差过度敏感。
  • 业务上常用,但严格场景慎用。

Huber Loss

MSE 与 MAE 的折衷:

Lδ(y,y^)={12(y−y^)2∣y−y^∣≤δδ∣y−y^∣−12δ2otherwiseL_\delta(y, \hat y) = \begin{cases} \frac{1}{2}(y - \hat y)^2 & |y - \hat y| \le \delta \\ \delta |y - \hat y| - \frac{1}{2} \delta^2 & \text{otherwise} \end{cases}Lδ(y,y^)={21(y−y^)2δ∣y−y^∣−21δ2∣y−y^∣≤δotherwise

  • 残差小时用 MSE(二次、可微)。
  • 残差大时用 MAE(线性、对异常值鲁棒)。
  • 训练目标函数 vs 评估指标可不同;sklearn 的回归器用训练损失,评估用 neg_mean_absolute_error 等。

Quantile Loss(分位数损失)

Lτ(y,y^)=(τ−1{y<y^})⋅(y−y^)L_\tau(y, \hat y) = (\tau - \mathbf{1}\{y < \hat y\}) \cdot (y - \hat y)Lτ(y,y^)=(τ−1{y<y^})⋅(y−y^)

预测 τ\tauτ-分位数(如 0.5 = 中位数)。QuantileRegressor。

指标的业务选择

场景 推荐
通用回归 RMSE(与 yyy 同量纲)、R2R^2R2
含异常值 MAE、Huber
业务要求相对误差 MAPE、MSPE
预测分布(如 P50/P90) Quantile Loss
销量预测(业务上 0 是合理下限) Tweedie 损失 + RMSE

scikit-learn 速查

python 复制代码
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# 注意:mean_squared_error 的 squared=False 自 1.4 弃用、1.6 移除,改用:
from sklearn.metrics import root_mean_squared_error      # sklearn 1.4+
rmse = root_mean_squared_error(y_test, y_pred)
mae  = mean_absolute_error(y_test, y_pred)
r2   = r2_score(y_test, y_pred)

注意:scoring 参数的方向

scikit-learn 的 cross_val_score(..., scoring="neg_mean_squared_error") 返回负值 (约定"越大越好")。GridSearchCV 也用负值;最终结果取负号才是真实 MSE。

易错点

  • MSE vs RMSE 选错:度量报告要选与业务同量纲的;MSE 用于训练(光滑可微),RMSE 用于报告。
  • R2R^2R2 跨数据集不可比 :不同 yyy 方差的数据集上 R2R^2R2 不可比。
  • MAPE 在 y≈0y \approx 0y≈0 处爆炸:业务数据若有零值应改 MAE。
  • scoring 符号反了 :neg_* 越大越好;MSE、MAE 越小越好;直接用会反过来。

二、类别不平衡处理

2.1 重采样方法

类别不平衡的普遍性

真实数据常是极度不平衡的:欺诈(0.1%)、罕见病(0.01%)、广告点击(1%)、机器故障(0.5%)。在不平衡数据上训练的模型常全部预测为多数类------准确率高但毫无业务价值。

重采样两大类

  • 过采样(Oversampling) :增加少数类样本。
    • 随机过采样:复制少数类样本。
    • SMOTE(Synthetic Minority Oversampling Technique, Chawla et al. 2002) :在少数类样本间插值生成新样本。
    • ADASYN:SMOTE 的变种,对难样本生成更多合成样本。
  • 欠采样(Undersampling) :减少多数类样本。
    • 随机欠采样:随机丢弃多数类。
    • NearMiss:保留与少数类"近"的多数类样本。
    • Tomek Links:移除 Tomek 链路上的多数类样本。
    • ENN(Edited Nearest Neighbours):剔除被邻居错分的样本。

SMOTE 的算法

对每个少数类样本 x\mathbf{x}x:

  1. 找 x\mathbf{x}x 的 KKK 个少数类近邻。
  2. 随机选一个近邻 x′\mathbf{x}'x′。
  3. 随机生成 λ∈0,1\lambda \in 0, 1λ∈0,1,合成新样本 xnew=x+λ(x′−x)\mathbf{x}_{\text{new}} = \mathbf{x} + \lambda (\mathbf{x}' - \mathbf{x})xnew=x+λ(x′−x)。

特点:合成样本在少数类决策边界附近,缓解过拟合(与随机复制相比)。

SMOTE 的变体

  • Borderline-SMOTE:只在边界附近的少数类样本上插值。
  • Safe-Level-SMOTE:避免生成越过边界的样本。
  • SMOTE + Tomek / SMOTE + ENN:先 SMOTE 再清理边界样本。

何时选哪种方法

  • 过采样适合小数据(增加样本数让模型更稳),但可能过拟合。
  • 欠采样适合大数据(多数类有大量冗余),但可能丢信息。
  • SMOTE 在多数场景下比随机过采样好。
  • 实践上常过采样 + 欠采样结合。

类权重(Cost-sensitive)

不改变数据,直接在损失函数中给少数类更高权重:

python 复制代码
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(class_weight="balanced")
  • class_weight="balanced":权重 ∝1/nc\propto 1/n_c∝1/nc。
  • 自定义:class_weight={0: 1, 1: 10},正类权重 10 倍于负类。

阈值调整

训练后调分类阈值:

  • 训练阶段不变(用默认 0.5)。
  • 预测阶段选使 F1F_1F1 / 业务指标最优的阈值。
python 复制代码
from sklearn.metrics import precision_recall_curve
import numpy as np

prec, rec, thr = precision_recall_curve(y_test, y_proba)
# 注意:prec/rec 比 thr 长 1(最后一个点是强制召回 0 的端点),
# 直接 thr[f1.argmax()] 可能越界,需 clip:
f1 = 2 * prec * rec / (prec + rec + 1e-12)
idx = int(f1.argmax())
best_thr = thr[min(idx, len(thr) - 1)]

集成 + 重采样

  • EasyEnsemble:多次欠采样训练多个弱学习器,集成。
  • BalancedBaggingClassifier(imbalanced-learn):每棵树训练前对多数类欠采样。
  • RUSBoost / SMOTEBoost:在 Boosting 框架中嵌入重采样。

imbalanced-learn 库

bash 复制代码
pip install imbalanced-learn
python 复制代码
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler
from imblearn.pipeline import Pipeline

pipe = Pipeline([
    ("over", SMOTE(random_state=0)),
    ("clf", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train)

易错点

  • 重采样后再划分训练/测试集:应先划分再重采样(只在训练集上做)。否则测试集信息泄漏。
  • SMOTE 在高维稀疏数据上效果差:合成样本失去稀疏性;考虑 SMOTE + 编码组合。
  • 过采样后分布信息被破坏:合成样本不代表真实分布;对校准指标(Brier)可能不利。
  • 类别严重不平衡时用准确率评估:必须用 F1 / PR-AUC 等少数类敏感指标。

2.2 代价敏感与阈值调整

代价敏感学习

不同类型错误的代价不同。例如医疗诊断:

  • FN 漏报(实际有病但预测无病)→ 延误治疗,代价极高。
  • FP 误报(实际无病但预测有病)→ 多做检查,代价相对低。

代价敏感学习把这种差异纳入训练或决策。

实现方式

  • 类权重:训练时给少数类 / 高代价类更高权重。
  • 代价矩阵 :自定义 Cy,y\^Cy, \\hat yCy,y\^ 矩阵,损失 = ∑C⋅1\sum C \cdot \mathbf{1}∑C⋅1。
  • 阈值调整:在标准模型上选不同阈值使总代价最低。

贝叶斯最优决策

设 CFP,CFNC_{FP}, C_{FN}CFP,CFN 分别是 FP、FN 的代价,最优阈值:

t∗=CFPCFP+CFNt^* = \frac{C_{FP}}{C_{FP} + C_{FN}}t∗=CFP+CFNCFP

  • CFP=CFNC_{FP} = C_{FN}CFP=CFN:t∗=0.5t^* = 0.5t∗=0.5(默认)。
  • CFN>CFPC_{FN} > C_{FP}CFN>CFP(更怕漏报):t∗t^*t∗ 降低(更倾向预测正类)。
  • CFP>CFNC_{FP} > C_{FN}CFP>CFN(更怕误报):t∗t^*t∗ 提高。

业务驱动的阈值调优

不用理论阈值,而用业务指标找最优:

  • 已知每次 FP / FN 的成本(广告召回的"用户投诉成本" vs "漏掉欺诈损失")。
  • 阈值使总业务损失 最小:min⁡tNFP(t)⋅CFP+NFN(t)⋅CFN\min_t N_{FP}(t) \cdot C_{FP} + N_{FN}(t) \cdot C_{FN}mintNFP(t)⋅CFP+NFN(t)⋅CFN。

精确率-召回率权衡

阈值从 0 到 1 描绘 PR 曲线:

  • 高阈值:预测稀少 → Precision ↑、Recall ↓
  • 低阈值:预测频繁 → Precision ↓、Recall ↑

业务选点:

  • 垃圾邮件:宁可漏几个 spam 也不要误标正常邮件(高阈值)。
  • 罕见病筛查:宁可误报(再复查)也不要漏报(低阈值)。

scikit-learn 阈值调优示例

python 复制代码
from sklearn.metrics import precision_recall_curve
import numpy as np

# 假设业务上每漏报一次的损失 = 100,每误报一次的损失 = 1
C_FN, C_FP = 100, 1
prec, rec, thr = precision_recall_curve(y_test, y_proba)

# 不同阈值下的业务总成本
# 在每个阈值下,预测为正 (y_proba >= thr),计算 TP/FP/FN 数量
costs = []
for t in thr:
    pred = (y_proba >= t).astype(int)
    fp = ((pred == 1) & (y_test == 0)).sum()
    fn = ((pred == 0) & (y_test == 1)).sum()
    costs.append(fp * C_FP + fn * C_FN)
best_t = thr[np.argmin(costs)]

多分类代价矩阵

KKK 类问题用 K×KK \times KK×K 代价矩阵 CCC:

L=∑i∑k,k′Ck,k′P(y^i=k′∣yi=k)L = \sum_i \sum_{k, k'} Ck, k' P(\hat y_i = k' | y_i = k)L=i∑k,k′∑Ck,k′P(y^i=k′∣yi=k)

业务中常对角线(正确)为 0、误分类为不同代价。class_weight 是 CCC 的对角矩阵简化版。

PR 曲线 vs 阈值扫描

PR 曲线给出 Precision-Recall 的所有可能权衡。在严重不平衡数据上,PR 比 ROC 信息更丰富(ROC 在大量真负时变化小,PR 区分度高)。

LIFT 与 Gain 曲线

营销与风控常用:

  • LIFT :在阈值 ttt 下,预测为正的前 X%X\%X% 样本里,正类的富集倍数 = 实际命中率 / 全局基线命中率。
  • Gain 曲线:累计 Recall vs 累计样本比例。

scikitplot 库有 LIFT 曲线;或手画。

校准(Calibration)与代价敏感

阈值调整 只改变决策、不改变概率估计;类权重 则会改变训练出的模型本身------概率估计随之改变,且常变得更不校准。若要配合"期望业务损失"等决策使用概率,应先固定模型再校准,或在类权重训练后用 CalibratedClassifierCV 重新校准。

scikit-learn 校准示例

python 复制代码
from sklearn.calibration import CalibratedClassifierCV

# 在已有模型上校准
calibrated = CalibratedClassifierCV(estimator=clf, method="sigmoid", cv=5)  # sklearn 1.2+ 用 estimator;1.2 之前为 base_estimator
calibrated.fit(X_train, y_train)
proba_calibrated = calibrated.predict_proba(X_test)

两种校准方法:

  • Platt scaling(sigmoid):在 SVM/树输出上拟合 sigmoid。
  • Isotonic regression:非参数保序回归,灵活但需更多数据。

阈值选择的标准工具

工具 适用
precision_recall_curve 业务要"高精度"或"高召回"
roc_curve 看排序质量
f1_score (最大化) 平衡精度与召回
业务损失加权 已知 FP/FN 代价
期望值最大化 概率输出 + 行动决策

易错点

  • 混淆代价时仍用 0.5 阈值:业务上 FN 比 FP 严重 10 倍时,阈值应小于 0.5。
  • 类权重与阈值双重调整:选其一;否则易过度补偿。
  • 校准后不重做阈值选择:校准改变概率绝对值,最优阈值会变。
  • 用 predict 选阈值而非 predict_proba :predict 已用 0.5 截断,看不到全部权衡。

三、模型选择

3.1 交叉验证

为什么需要交叉验证

  • 单一 train/test 划分对数据分割敏感(不同划分下评分波动大)。
  • 大数据下也能用,但小数据上尤其关键。

K 折交叉验证(K-Fold CV)

text 复制代码
把 D 随机划分为 K 个等大的子集 D_1, ..., D_K
for k = 1, ..., K:
    训练 D \ D_k,验证 D_k
输出: 平均验证得分
  • K 常用 5 或 10。
  • 留一交叉验证(LOOCV)是 K=N 的特例。

分层 K 折(Stratified K-Fold)

每折中各类别比例与全数据集一致。对类别不平衡至关重要------否则某折可能完全没有少数类。

StratifiedKFold 自身就是分层分折器;cross_val_score / GridSearchCV 在 cv=int 且估计器为分类器时也会默认走 StratifiedKFold。回归任务没有分层版本 (StratifiedKFold / RepeatedStratifiedKFold 仅用于分类);回归的常见替代做法是把连续 yyy 分箱后按箱做分层,或直接用 KFold。

时间序列的 CV

时间序列不能随机划分(破坏时序)。用前向链接(forward chaining):

text 复制代码
训练 [1..t1], 验证 [t1+1..t2]
训练 [1..t2], 验证 [t2+1..t3]
...

sklearn.model_selection.TimeSeriesSplit。

Group K-Fold

数据有"组"结构(同一用户多条记录)时,需保证同一组的样本在同一折内,避免信息泄漏。

GroupKFold(n_splits=K)。

留一法(LOOCV)

  • 优点:数据利用率高(N−1N-1N−1 训练)。
  • 缺点:训练 NNN 次,NNN 大时极慢;评分方差大。
  • 适用:极小数据(N<100N < 100N<100)。

重复 K 折(Repeated K-Fold)

做多次 K 折平均,减少单次划分的随机性:

python 复制代码
from sklearn.model_selection import RepeatedKFold, cross_val_score
scores = cross_val_score(clf, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=3), scoring="accuracy")

cross_val_score 的使用

python 复制代码
from sklearn.model_selection import cross_val_score
scores = cross_val_score(clf, X, y, cv=5, scoring="roc_auc")
print(f"Mean: {scores.mean():.4f} ± {scores.std():.4f}")

数据泄漏与 Pipeline

CV 评估中,预处理必须放在 Pipeline 内,否则每折的预处理信息会泄漏到验证集:

python 复制代码
# 错误:预处理 fit 在全量数据上
X_scaled = StandardScaler().fit_transform(X)
scores = cross_val_score(clf, X_scaled, y, cv=5)  # 泄漏!

# 正确:预处理在 Pipeline 中按折重 fit
from sklearn.pipeline import Pipeline
pipe = Pipeline([("scale", StandardScaler()), ("clf", clf)])
scores = cross_val_score(pipe, X, y, cv=5)

数据泄漏的常见来源

泄漏源 后果 防范
预处理 fit 在全量数据上 评估虚高 Pipeline 化
目标编码用全量样本均值 评估虚高 CV 内计算
特征选择用全量数据 评估虚高 CV 内做选择
时序数据随机划分 评估虚高 TimeSeriesSplit
同组样本分到不同折 评估虚高 Group K-Fold
重复样本(同一用户多次记录) 评估虚高 去重后划分

Holdout 验证 vs CV

维度 Holdout K-Fold CV
训练数据 固定比例(如 80%) 1−1/K1 - 1/K1−1/K(每折)
评估稳定性 较低 较高
计算成本 低 高(K 倍)
数据量小时 易浪费 充分利用
适用 大数据 中小数据

Bootstrap 与 .632 估计

Bootstrap(Efron 1979)通过有放回抽样估计统计量的分布。

  • .632 估计 (Efron 1983):acc0.632=0.368⋅acctrain+0.632⋅accOOB\text{acc}{0.632} = 0.368 \cdot \text{acc}{\text{train}} + 0.632 \cdot \text{acc}_{\text{OOB}}acc0.632=0.368⋅acctrain+0.632⋅accOOB,结合训练误差与 OOB 误差,缓解 CV 对过拟合模型的低估。
  • .632+ 估计 (Efron & Tibshirani 1997):引入相对过拟合率 RRR 与自适应权重 w=0.632/(1−0.368R)w = 0.632 / (1 - 0.368 R)w=0.632/(1−0.368R),对严重过拟合模型更稳健。

工程上不常用,CV 已足够。

嵌套 CV(Nested CV)

当需要同时做模型选择 和评估时:

  • 外层 CV:评估最终模型性能。
  • 内层 CV:对每个外层 fold 做超参搜索(GridSearchCV)。

普通 GridSearchCV 的 best_score_ 会偏高 (同一份数据既选超参又评估,超参选择过程"过拟合"于该数据,selection bias)。嵌套 CV 把"超参选择"和"最终评估"分到不同层的数据切分中,给出无偏的最终评估。

python 复制代码
from sklearn.model_selection import GridSearchCV, cross_val_score
inner = GridSearchCV(pipe, param_grid, cv=5, scoring="roc_auc")
outer_scores = cross_val_score(inner, X, y, cv=5, scoring="roc_auc")

模型比较的统计显著性检验

两个模型的 CV 分数"一个高一点"不等于显著更好------差异可能来自折间随机性。经典检验:

  • McNemar 检验 (分类,单一测试集):只比较两模型预测不一致 的部分。构造 2×2 表(A 对 B 错 bbb 个、A 错 B 对 ccc 个),统计量
    χ2=(∣b−c∣−1)2b+c\chi^2 = \frac{(|b - c| - 1)^2}{b + c}χ2=b+c(∣b−c∣−1)2
    服从自由度 1 的卡方分布;p < 0.05 才可宣称 A 显著优于 B。
  • 5×2 交叉验证 t 检验(Dietterich 1998):重复 5 次 2 折 CV,用每次的分数差构造 t 统计量。它修正了普通 K 折 t 检验"折间不独立"的问题,是算法级比较的推荐方案。

工程含义:集成、调参带来的提升常在 0.1%--1% 量级------先做显著性检验,再决定是否为复杂模型买单(与第一章奥卡姆剃刀呼应)。

易错点

  • 预处理未 Pipeline 化:最常见的 CV 错误,所有评估虚高。
  • 类别不平衡数据用普通 K-Fold :可能某折无少数类;用 StratifiedKFold。
  • 时序数据随机 K-Fold :评估虚高(用未来预测过去),用 TimeSeriesSplit。
  • GridSearchCV 单一分数当最终评估:实际有选择偏置;用嵌套 CV 或 holdout 评估最终模型。
  • 不打印 CV 分数的标准差:只报告均值会掩盖不稳定。

3.2 超参数搜索

超参与参数的区别

  • 参数 :模型从数据学到的(线性回归的 w\mathbf{w}w、神经网络的权重)。
  • 超参 :人为设定、不在训练中学的(学习率、树的深度、CCC、KKK)。

网格搜索(Grid Search)

遍历所有超参组合,CV 选最优。

python 复制代码
from sklearn.model_selection import GridSearchCV

param_grid = {
    "max_depth": [3, 5, 7, 10],
    "min_samples_leaf": [1, 5, 20],
    "n_estimators": [100, 300, 500],
}
grid = GridSearchCV(clf, param_grid, cv=5, scoring="roc_auc", n_jobs=-1, verbose=1)
grid.fit(X_train, y_train)
print("best params:", grid.best_params_)
print("best CV score:", grid.best_score_)

随机搜索(Randomized Search)

Bergstra & Bengio 2012 证明:随机搜索在高维超参空间比网格搜索更高效(同等预算下覆盖更大范围)。

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

param_dist = {
    "max_depth": [3, 5, 7, 10, 15, 20, None],
    "min_samples_leaf": [1, 3, 5, 10, 20, 50],
    "n_estimators": [100, 200, 300, 500, 1000],
    "learning_rate": loguniform(0.005, 0.3),
}
rs = RandomizedSearchCV(clf, param_dist, n_iter=100, cv=5, n_jobs=-1, random_state=0)

贝叶斯优化(Bayesian Optimization)

基于历史评估结果建立超参-性能的代理模型(高斯过程 / TPE),选"期望提升最大"的下一组超参。

  • 比网格/随机搜索更高效------特别是评估代价高的场景(深度学习)。
  • 库:
    • scikit-optimize(skopt)
    • hyperopt
    • optuna(最常用,含剪枝、可视化)
    • bayesian-optimization
    • Ray Tune(大规模分布式)

Optuna 示例

python 复制代码
import optuna
import xgboost as xgb

def objective(trial):
    params = {
        "max_depth": trial.suggest_int("max_depth", 3, 20),
        "learning_rate": trial.suggest_float("learning_rate", 1e-3, 0.3, log=True),
        "n_estimators": trial.suggest_int("n_estimators", 100, 1000),
        "subsample": trial.suggest_float("subsample", 0.5, 1.0),
    }
    scores = cross_val_score(xgb.XGBClassifier(**params), X, y, cv=5, scoring="roc_auc")
    return scores.mean()

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=200, show_progress_bar=True)
print("best params:", study.best_params)

搜索策略对比

策略 适用 效率 实现
网格搜索 2-3 个关键超参 低 GridSearchCV
随机搜索 4+ 超参、宽范围 中 RandomizedSearchCV
贝叶斯优化 评估代价高、连续超参 高 Optuna / Hyperopt / skopt
Hyperband / BOHB 大规模并行 高 Ray Tune / Optuna
元学习 冷启动 最高 AutoML 系统(Auto-sklearn、H2O)

多目标优化

optuna.create_study(directions=["maximize", "minimize"]):Pareto 前沿同时优化 AUC 与训练时间。

分阶段搜索(Coarse-to-Fine)

  1. 粗网格:宽范围粗略定位。
  2. 细网格:在粗搜索最优附近加密。
  3. 贝叶斯 / Optuna 微调。

过拟合超参选择的风险

多次比较会引入"多重检验"问题------大量超参组合中总会找到偶然"看起来好"的组合。

  • 用 holdout 评估最终选定的超参,避免"用同一份数据既选又评"。
  • 嵌套 CV 是无偏方案。

Param 重要性

optuna 提供 optuna.importance.get_param_importances(study):哪些超参对性能影响最大。优先调重要超参。

Halving 搜索(HalvingGridSearchCV)

sklearn 0.24+ 的 HalvingGridSearchCV(实验性,需 from sklearn.experimental import enable_halving_search_cv):先用少资源(少样本 / 少迭代)筛掉明显差的超参组合,对剩下的用更多资源。计算高效。

scikit-learn 与搜索的整合

  • GridSearchCV、RandomizedSearchCV 用法一致。
  • cv=5(或更大)保证稳定。
  • n_jobs=-1 用全部 CPU。
  • refit=True(默认):CV 选最优组合后用全量数据 refit。

易错点

  • 网格搜索步长过大:最优值落在两个步长之间,被错失。先粗后细。
  • 未在 Pipeline 中调 :超参搜索与预处理一起时,必须用 pipe__clf__param 命名。
  • refit=True 后用全量数据重新训练:CV 用的是子集,但 refit 用了全量------再评估时不能用训练/验证,要 holdout 评估。
  • 优化目标选错:业务要 F1 但 optimize accuracy,得到 F1 一般的"高准确率"模型。

3.3 学习曲线诊断

学习曲线(Learning Curve)

绘制训练集大小 vs 训练/验证分数的曲线。揭示模型是欠拟合、过拟合,还是数据不足。

典型模式

  • 欠拟合(高偏差) :训练分数和验证分数都低,且接近。
    • 行动:增加模型复杂度、减少正则化、加特征。
  • 过拟合(高方差) :训练分数高、验证分数低,差距大。
    • 行动:增加数据、加正则化、降复杂度、特征选择。
  • 数据不足 :增加更多数据后,验证分数持续上升。
    • 行动:获取更多数据。
  • 正常:随数据增加,训练分数略降、验证分数上升,两者收敛到较高水平。

sklearn 绘制

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

train_sizes, train_scores, val_scores = learning_curve(
    estimator=clf, X=X, y=y, cv=5,
    train_sizes=np.linspace(0.1, 1.0, 10),
    scoring="roc_auc", n_jobs=-1,
)
train_mean = train_scores.mean(axis=1)
val_mean = val_scores.mean(axis=1)
# 画 train_mean / val_mean vs train_sizes

验证曲线(Validation Curve)

绘制单个超参 vs 训练/验证分数。

python 复制代码
from sklearn.model_selection import validation_curve
param_range = [1, 5, 10, 50, 100, 300]
train_scores, val_scores = validation_curve(
    estimator=clf, X=X, y=y, param_name="n_estimators",
    param_range=param_range, cv=5, scoring="roc_auc",
)

偏差-方差分解

期望预测误差 = 偏差² + 方差 + 噪声(详见第二章 §2.2):

  • 高偏差:模型平均预测偏离真实值。
  • 高方差:训练集变化导致预测波动。
  • 噪声:问题本身的不可约误差。

学习曲线是诊断偏差-方差的工具。

过拟合与欠拟合的特征

维度 欠拟合 过拟合
训练误差 高 低
验证误差 高 较高(与训练误差有差距)
训练-验证差距 小 大
模型复杂度 不足 过高
行动 加特征/加复杂度/减小正则 加数据/加正则/减复杂度/早停

正则化强度 vs 性能

正则化参数 λ\lambdaλ:

  • λ\lambdaλ 太小:训练误差低、验证误差高 → 过拟合。
  • λ\lambdaλ 太大:训练误差高、验证误差高 → 欠拟合。
  • 最佳 λ\lambdaλ:验证误差最低点。

学习曲线的局限

  • 只反映"当前超参下"的表现。需配合超参搜索用。
  • 单次曲线有随机性;多次平均(cv=5 已有 5 次)才稳。

Validation Curve 上的过/欠拟合指示

text 复制代码
训练误差  ──___
                   ___ (随 λ 上升, 训练误差单调上升)
验证误差  /‾‾___
                ___ (U 形曲线:左过拟合、右欠拟合)

Validation Curve 与网格搜索的协同

  1. 用 Validation Curve 找"最优超参区间"(U 形底部)。
  2. 在区间内网格/贝叶斯搜索细化。
  3. 用 Learning Curve 验证"是否数据不足 / 过/欠拟合"。

sklearn 的诊断工具汇总

工具 用途
learning_curve 训练-验证分数 vs 样本数
validation_curve 训练-验证分数 vs 单个超参
cross_val_score 整体 CV 分数
learning_curve + 参数网格 数据量与超参的联合诊断

特征工程与数据规模

  • 数据少 + 特征多:用正则化强的简单模型(线性、Lasso)。
  • 数据多 + 特征少:用复杂模型(深度学习)。
  • 经验法则:N≥10×dN \ge 10 \times dN≥10×d 时复杂模型可工作(粗略)。

早停(Early Stopping)

监控验证集 loss,连续 kkk 步不下降则停训:

  • 神经网络:每个 epoch 监控验证 loss。
  • GBDT:每个 boosting 轮次监控验证 loss(XGBoost、LightGBM 内置)。

早停是最简单有效的正则化之一。

易错点

  • 学习曲线只看一次:必须多次(cv 平均)才有意义。
  • 欠拟合时拼命加数据:欠拟合与数据量无关,加数据无益。改加复杂度。
  • 过拟合时拼命加特征:加特征会进一步过拟合。应减特征或加正则。
  • validation_curve 看单峰:U 形可能有多峰(局部最优),需用粗-细两阶段。
  • 不打印标准差:只报告均值可能掩盖不稳定(cv=5 时分数 std > 0.05 应警惕)。
相关推荐
李航19832 小时前
DeepSeek AI建模(二)让AI建第一个模型
人工智能·python·计算机视觉·ai·ai编程
二宝哥2 小时前
03.Spring AI Embedding 实战:从向量化原理到项目落地
人工智能·spring boot·向量·spring ai
涛声依旧-底层原理研究所2 小时前
Agent 沙箱实现原理:拆解 DeepSeek Harness 的三层沙箱架构与三平台后端
人工智能·架构
推送者2 小时前
大将怪兽桌宠教程:从一张正脸照到本地透明桌宠的工程化路线
人工智能·大将怪兽桌宠教程
shennv8882 小时前
2026年国内如何开通ChatGPT Plus和Pro?订阅方式、Codex额度及常见问题整理
人工智能·chatgpt
u1301302 小时前
GitHub 热榜项目:日榜(2026-10-25)
人工智能·github
机器学习之心2 小时前
电池SOH估计和RUL预测,五个时间特征 + 基于GPR高斯过程回归的锂电池健康状态估计和剩余寿命预测
人工智能·数据挖掘·回归·锂电池健康状态估计·剩余寿命预测
代码方舟2 小时前
零信任架构实战:基于天远企业二要素验证构建自动化小微快贷法人资质核验网关
运维·人工智能·架构·自动化
tellmewhoisi2 小时前
深度学习13:反向传播4:输出层Softmax-with-Loss
人工智能·深度学习