07 - 模型评估与超参数调优¶
🎯 为什么需要科学的评估?¶
常见错误:
❌ 只在训练集上评估
❌ 用测试集调参
❌ 数据泄露
❌ 选择了错误的评估指标
正确做法:
✅ 独立的训练/验证/测试集
✅ 交叉验证
✅ 多个评估指标
✅ 理解业务需求
📊 评估指标详解¶
回归指标¶
1. MSE (均方误差)¶
特点: - 对大误差敏感 (因为平方) - 可微 (便于优化) - 单位: 目标变量的平方
应用: 当需要惩罚大误差时
from sklearn.metrics import mean_squared_error
import numpy as np
y_true = np.array([3, -0.5, 2, 7]) # np.array创建NumPy数组
y_pred = np.array([2.5, 0.0, 2, 8])
mse = mean_squared_error(y_true, y_pred)
print(f"MSE: {mse}")
2. RMSE (均方根误差)¶
特点: - 与目标变量同单位 - 易于解释
3. MAE (平均绝对误差)¶
特点: - 对异常值鲁棒 - 直观易懂 - 不可微 (在0点)
对比 MSE vs MAE:
数据: [1, 2, 3, 4, 100] (100是异常值)
MSE: 对100的误差平方,影响大
MAE: 线性惩罚,影响小
选择:
- 数据有异常值 → MAE
- 需要可微优化 → MSE
4. R² (决定系数)¶
含义: 模型解释的方差比例
范围: - \(R² = 1\): 完美预测 - \(R² = 0\): 等于预测均值 - \(R² < 0\): 比预测均值还差
from sklearn.metrics import r2_score
r2 = r2_score(y_true, y_pred)
print(f"R²: {r2}")
# 解释: 模型解释了{r2*100:.1f}%的方差
5. MAPE (平均绝对百分比误差)¶
特点: - 相对误差,可跨数据集比较 - 对y接近0时敏感
应用: 业务报告 (易于理解)
def mape(y_true, y_pred):
# 注意: y_true 中包含0时会导致除零错误,需过滤或使用 SMAPE
mask = y_true != 0
return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100
分类指标¶
1. 混淆矩阵 (Confusion Matrix)¶
预测
正 负
实际 正 TP FN
负 FP TN
TP (True Positive): 正确预测为正
TN (True Negative): 正确预测为负
FP (False Positive): 错误预测为正 (第一类错误)
FN (False Negative): 错误预测为负 (第二类错误)
示例: 癌症检测
from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt
y_true = [1, 0, 1, 1, 0, 1]
y_pred = [1, 0, 0, 1, 0, 1]
cm = confusion_matrix(y_true, y_pred)
# 可视化
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('预测')
plt.ylabel('实际')
plt.show()
2. 准确率 (Accuracy)¶
问题: 类别不平衡时会误导
3. 精确率 (Precision)¶
含义: 预测为正的样本中,真正为正的比例
问题: "预测的病人得病,真的得病了吗?"
应用: - 垃圾邮件分类 (不想误判正常邮件) - 推荐系统 (推荐的内容要准确)
4. 召回率 (Recall / Sensitivity / TPR)¶
含义: 真正为正的样本中,被预测为正的比例
问题: "所有得病的人,被检测出来了吗?"
应用: - 疾病诊断 (不能漏诊) - 异常检测 (不能漏掉异常)
5. F1-Score¶
特点: 精确率和召回率的调和平均
为什么用调和平均?
应用: 需要平衡精确率和召回率
from sklearn.metrics import precision_score, recall_score, f1_score, classification_report
precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)
print(f"Precision: {precision:.2f}")
print(f"Recall: {recall:.2f}")
print(f"F1: {f1:.2f}")
# 完整报告
print(classification_report(y_true, y_pred,
target_names=['健康', '患病']))
6. ROC曲线与AUC¶
TPR (True Positive Rate) = Recall $\(TPR = \frac{TP}{TP + FN}\)$
FPR (False Positive Rate): $\(FPR = \frac{FP}{FP + TN}\)$
ROC曲线: 不同阈值下的 TPR vs FPR
理想情况:
TPR = 1, FPR = 0 (完美分类器)
随机猜测:
TPR = FPR (对角线)
AUC (Area Under Curve):
- AUC = 1.0: 完美
- AUC = 0.5: 随机
- AUC < 0.5: 比随机还差
from sklearn.metrics import roc_curve, auc, roc_auc_score
# 需要概率输出
y_prob = [0.1, 0.4, 0.35, 0.8, 0.2, 0.9]
fpr, tpr, thresholds = roc_curve(y_true, y_prob)
roc_auc = auc(fpr, tpr)
# 或直接计算
auc_score = roc_auc_score(y_true, y_prob)
# 绘制ROC曲线
plt.figure()
plt.plot(fpr, tpr, label=f'ROC (AUC = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], 'k--') # 随机线
plt.xlabel('FPR')
plt.ylabel('TPR')
plt.title('ROC曲线')
plt.legend()
plt.show()
7. PR曲线¶
Precision-Recall 曲线: 不同阈值下的 Precision vs Recall
何时用 PR 而非 ROC? - 类别极度不平衡 - 更关注正类
from sklearn.metrics import precision_recall_curve, average_precision_score
precision, recall, thresholds = precision_recall_curve(y_true, y_prob)
ap = average_precision_score(y_true, y_prob)
plt.figure()
plt.plot(recall, precision, label=f'PR (AP = {ap:.2f})')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('PR曲线')
plt.legend()
plt.show()
多分类指标¶
宏平均 (Macro Average):
微平均 (Micro Average):
from sklearn.metrics import precision_score
# 多分类
y_true_multi = [0, 1, 2, 0, 1, 2]
y_pred_multi = [0, 2, 1, 0, 0, 2]
# 宏平均
precision_macro = precision_score(y_true_multi, y_pred_multi,
average='macro')
# 微平均
precision_micro = precision_score(y_true_multi, y_pred_multi,
average='micro')
🔪 数据划分策略¶
1. 训练集/验证集/测试集¶
训练集 (Train, 60-70%):
用于训练模型参数
验证集 (Validation, 15-20%):
用于调参和模型选择
测试集 (Test, 15-20%):
用于最终评估,只使用一次!
from sklearn.model_selection import train_test_split
# 第一次划分: 训练+验证 vs 测试
X_trainval, X_test, y_trainval, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 第二次划分: 训练 vs 验证
X_train, X_val, y_train, y_val = train_test_split(
X_trainval, y_trainval, test_size=0.25, random_state=42
)
# 最终比例: 60% 训练, 20% 验证, 20% 测试
2. K折交叉验证 (K-Fold CV)¶
数据分成K份:
┌─────┬─────┬─────┬─────┬─────┐
│ 1 │ 2 │ 3 │ 4 │ 5 │
└─────┴─────┴─────┴─────┴─────┘
第1轮: [测试] [训练] [训练] [训练] [训练]
第2轮: [训练] [测试] [训练] [训练] [训练]
...
第5轮: [训练] [训练] [训练] [训练] [测试]
最终性能 = 5轮的平均
优点: - 充分利用数据 - 减少方差 - 更稳健的评估
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(random_state=42)
# 5折交叉验证
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"各折准确率: {scores}")
print(f"平均准确率: {scores.mean():.4f}")
print(f"标准差: {scores.std():.4f}")
3. 分层K折 (Stratified K-Fold)¶
问题: 普通K折可能打乱类别分布
解决: 每折保持类别比例
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[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
# 训练和评估...
4. 时间序列交叉验证¶
不能用随机划分! (会破坏时间顺序)
方法: 前向链 (Forward Chaining)
训练1: [1-2-3] → 测试: [4]
训练2: [1-2-3-4] → 测试: [5]
训练3: [1-2-3-4-5] → 测试: [6]
...
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, val_idx in tscv.split(X):
# 注意: train_idx 在前, val_idx 在后
X_train, X_val = X[train_idx], X[val_idx]
# ...
🎛️ 超参数调优¶
什么是超参数?¶
模型参数 (Model Parameters):
- 算法学习得到
- 例如: 权重w, 偏置b
超参数 (Hyperparameters):
- 人工设置
- 例如: 学习率、树的深度、正则化系数
- 需要调优!
1. 网格搜索 (Grid Search)¶
穷举所有组合
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
# 定义参数网格
param_grid = {
'C': [0.1, 1, 10, 100],
'gamma': [1, 0.1, 0.01, 0.001],
'kernel': ['rbf', 'linear']
}
# 网格搜索
grid = GridSearchCV(
SVC(),
param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1, # 并行
verbose=1
)
grid.fit(X_train, y_train)
# 最佳参数和模型
print(f"最佳参数: {grid.best_params_}")
print(f"最佳得分: {grid.best_score_:.4f}")
best_model = grid.best_estimator_
问题: 组合爆炸!
2. 随机搜索 (Random Search)¶
随机采样参数组合
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
# 定义参数分布
param_distributions = {
'C': uniform(loc=0.1, scale=99.9), # 范围 [0.1, 100](uniform的参数是loc和scale,非上下界)
'gamma': uniform(loc=0.001, scale=0.999), # 范围 [0.001, 1]
'kernel': ['rbf', 'linear']
}
# 随机搜索
random_search = RandomizedSearchCV(
SVC(),
param_distributions,
n_iter=50, # 只尝试50种组合
cv=5,
random_state=42
)
random_search.fit(X_train, y_train)
优点: 比网格搜索更高效
理论: 只需要少量尝试就能找到好的参数组合
3. 贝叶斯优化¶
思路: 用概率模型建模"参数→性能"的映射
步骤: 1. 用少量随机搜索初始化 2. 拟合代理模型 (如高斯过程) 3. 用采集函数选择下一个参数 4. 重复2-3
# 推荐使用 Optuna(活跃维护,功能更强大)
# 注: scikit-optimize (skopt) 已停止维护,不建议在新项目中使用
import optuna
def objective(trial):
C = trial.suggest_float('C', 0.1, 100, log=True)
gamma = trial.suggest_float('gamma', 0.001, 1, log=True)
model = SVC(C=C, gamma=gamma)
score = cross_val_score(model, X_train, y_train, cv=5).mean()
return score
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=50)
print(f"最佳参数: {study.best_params}")
print(f"最佳分数: {study.best_value:.4f}")
其他库: - Hyperopt - Ray Tune
常用超参数¶
随机森林¶
param_grid = {
'n_estimators': [100, 200, 500], # 树的数量
'max_depth': [None, 10, 20, 30], # 最大深度
'min_samples_split': [2, 5, 10], # 分裂最小样本数
'min_samples_leaf': [1, 2, 4], # 叶节点最小样本数
'max_features': ['sqrt', 'log2', None] # 每次分裂考虑的特征数 ('auto'已弃用)
}
XGBoost¶
param_grid = {
'max_depth': [3, 5, 7], # 树深度
'learning_rate': [0.01, 0.1, 0.3], # 学习率
'n_estimators': [100, 200, 500], # 迭代次数
'subsample': [0.8, 1.0], # 样本采样比例
'colsample_bytree': [0.8, 1.0], # 特征采样比例
'reg_alpha': [0, 0.1, 1], # L1正则
'reg_lambda': [1, 10, 100] # L2正则
}
神经网络¶
param_grid = {
'hidden_layer_sizes': [(64,), (128, 64), (256, 128, 64)],
'activation': ['relu', 'tanh'],
'learning_rate_init': [0.001, 0.01, 0.1],
'batch_size': [32, 64, 128],
'alpha': [0.0001, 0.001, 0.01] # L2正则
}
🔍 偏差-方差权衡¶
数学推导:MSE 的偏差-方差分解¶
设真实函数为 \(f(x)\),观测值 \(y = f(x) + \epsilon\)(\(\epsilon\) 为噪声,\(\mathbb{E}[\epsilon]=0\),\(\text{Var}(\epsilon) = \sigma^2\)),模型预测为 \(\hat{f}(x)\)(在不同训练集上是随机变量)。
对于均方误差 \(\text{MSE} = \mathbb{E}[(y - \hat{f}(x))^2]\),展开如下:
由于 \(\epsilon\) 与 \(\hat{f}\) 独立且 \(\mathbb{E}[\epsilon]=0\),中间交叉项为零,第三项 \(= \sigma^2\)。
对第一项,加减 \(\mathbb{E}[\hat{f}(x)]\):
因此:
- Bias²:模型的平均预测与真实值的偏离程度(模型"能力不足")
- Variance:模型在不同训练集上预测的波动程度(模型"过于敏感")
- \(\sigma^2\):数据本身的噪声,任何模型都无法消除
理解偏差和方差¶
偏差 (Bias):
模型过于简单,无法拟合数据
→ 欠拟合
→ 训练集和测试集误差都大
方差 (Variance):
模型过于复杂,对数据变化敏感
→ 过拟合
→ 训练集误差小,测试集误差大
可视化:
诊断与解决¶
def diagnose_model(model, X_train, y_train, X_test, y_test):
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
print(f"训练集准确率: {train_score:.4f}")
print(f"测试集准确率: {test_score:.4f}")
print(f"差距: {train_score - test_score:.4f}")
if train_score < 0.7:
print("\n⚠️ 高偏差 (欠拟合)")
print("解决方案:")
print("- 增加模型复杂度")
print("- 减少正则化")
print("- 添加更多特征")
elif train_score - test_score > 0.1:
print("\n⚠️ 高方差 (过拟合)")
print("解决方案:")
print("- 更多训练数据")
print("- 增加正则化")
print("- 简化模型")
print("- 集成方法")
else:
print("\n✅ 模型良好!")
diagnose_model(model, X_train, y_train, X_test, y_test)
🛡️ 防止过拟合¶
1. 正则化¶
L1正则 (Lasso): $\(Loss + λ\sum_{j=1}^{d}|w_j|\)$
L2正则 (Ridge): $\(Loss + λ\sum_{j=1}^{d} w_j^2\)$
ElasticNet: L1 + L2
from sklearn.linear_model import Ridge, Lasso, ElasticNet
# L2正则
ridge = Ridge(alpha=1.0)
# L1正则
lasso = Lasso(alpha=1.0)
# 混合
elastic = ElasticNet(alpha=1.0, l1_ratio=0.5)
2. 早停 (Early Stopping)¶
在验证集性能不再提升时停止训练
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2
)
# 监控验证集性能
model = GradientBoostingClassifier(
n_iter_no_change=10, # 10轮没提升就停止
validation_fraction=0.2
)
model.fit(X_train, y_train)
3. Dropout (深度学习)¶
import torch.nn as nn
class Net(nn.Module): # 继承nn.Module定义神经网络层
def __init__(self): # __init__构造方法,创建对象时自动调用
super().__init__() # super()调用父类方法
self.dropout = nn.Dropout(0.5) # 50%概率丢弃
self.fc1 = nn.Linear(784, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.dropout(torch.relu(self.fc1(x)))
x = self.fc2(x)
return x
4. 数据增强¶
from torchvision import transforms
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.RandomAffine(0, shear=10),
transforms.ToTensor()
])
📈 学习曲线¶
可视化训练过程¶
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
train_sizes, train_scores, val_scores = learning_curve(
model, X, y,
train_sizes=np.linspace(0.1, 1.0, 10),
cv=5,
scoring='accuracy'
)
train_mean = train_scores.mean(axis=1)
val_mean = val_scores.mean(axis=1)
plt.figure()
plt.plot(train_sizes, train_mean, label='训练集')
plt.plot(train_sizes, val_mean, label='验证集')
plt.xlabel('训练样本数')
plt.ylabel('准确率')
plt.legend()
plt.title('学习曲线')
plt.show()
分析:
🎯 实战建议¶
评估清单¶
1. 数据划分:
□ 是否有独立的测试集?
□ 是否用验证集调参?
□ 时序数据是否保持顺序?
2. 评估指标:
□ 是否符合业务目标?
□ 是否考虑类别不平衡?
□ 是否查看多个指标?
3. 调参:
□ 是否理解每个超参数的作用?
□ 是否从粗到细搜索?
□ 是否记录实验结果?
4. 过拟合:
□ 训练/验证差距是否过大?
□ 是否应用了正则化?
□ 是否有足够的训练数据?
快速评估脚本¶
def quick_evaluate(model, X_train, y_train, X_test, y_test):
"""快速评估模型"""
from sklearn.metrics import accuracy_score, classification_report
import time
# 训练时间
start = time.time()
model.fit(X_train, y_train)
train_time = time.time() - start
# 预测时间
start = time.time()
y_pred = model.predict(X_test)
predict_time = time.time() - start
# 性能
train_acc = model.score(X_train, y_train)
test_acc = accuracy_score(y_test, y_pred)
print(f"训练时间: {train_time:.2f}s")
print(f"预测时间: {predict_time:.4f}s")
print(f"训练准确率: {train_acc:.4f}")
print(f"测试准确率: {test_acc:.4f}")
print(f"过拟合程度: {train_acc - test_acc:.4f}")
print("\n分类报告:")
print(classification_report(y_test, y_pred))
return {
'train_time': train_time,
'predict_time': predict_time,
'train_acc': train_acc,
'test_acc': test_acc,
'overfitting': train_acc - test_acc
}
# 使用
results = quick_evaluate(model, X_train, y_train, X_test, y_test)
下一步: 学习 08-特征工程.md,掌握数据预处理和特征构建!