一、评估指标
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∑kyiklogpik\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:
- 找 x\mathbf{x}x 的 KKK 个少数类近邻。
- 随机选一个近邻 x′\mathbf{x}'x′。
- 随机生成 λ∈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 "漏掉欺诈损失")。
- 阈值使总业务损失 最小:mintNFP(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)hyperoptoptuna(最常用,含剪枝、可视化)bayesian-optimizationRay 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)
- 粗网格:宽范围粗略定位。
- 细网格:在粗搜索最优附近加密。
- 贝叶斯 / 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 与网格搜索的协同
- 用 Validation Curve 找"最优超参区间"(U 形底部)。
- 在区间内网格/贝叶斯搜索细化。
- 用 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 应警惕)。