跳转至

07 - 模型评估与超参数调优

模型评估与超参数调优图

🎯 为什么需要科学的评估?

Text Only
常见错误:
❌ 只在训练集上评估
❌ 用测试集调参
❌ 数据泄露
❌ 选择了错误的评估指标

正确做法:
✅ 独立的训练/验证/测试集
✅ 交叉验证
✅ 多个评估指标
✅ 理解业务需求

📊 评估指标详解

回归指标

1. MSE (均方误差)

\[MSE = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2\]

特点: - 对大误差敏感 (因为平方) - 可微 (便于优化) - 单位: 目标变量的平方

应用: 当需要惩罚大误差时

Python
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 (均方根误差)

\[RMSE = \sqrt{\frac{1}{n}\sum(y_i - \hat{y}_i)^2}\]

特点: - 与目标变量同单位 - 易于解释

Python
rmse = np.sqrt(mse)
print(f"RMSE: {rmse}")

3. MAE (平均绝对误差)

\[MAE = \frac{1}{n}\sum|y_i - \hat{y}_i|\]

特点: - 对异常值鲁棒 - 直观易懂 - 不可微 (在0点)

对比 MSE vs MAE:

Text Only
数据: [1, 2, 3, 4, 100]  (100是异常值)

MSE: 对100的误差平方,影响大
MAE: 线性惩罚,影响小

选择:
- 数据有异常值 → MAE
- 需要可微优化 → MSE


4. R² (决定系数)

\[R² = 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2}\]

含义: 模型解释的方差比例

范围: - \(R² = 1\): 完美预测 - \(R² = 0\): 等于预测均值 - \(R² < 0\): 比预测均值还差

Python
from sklearn.metrics import r2_score

r2 = r2_score(y_true, y_pred)
print(f"R²: {r2}")

# 解释: 模型解释了{r2*100:.1f}%的方差

5. MAPE (平均绝对百分比误差)

\[MAPE = \frac{100\%}{n}\sum\left|\frac{y_i - \hat{y}_i}{y_i}\right|\]

特点: - 相对误差,可跨数据集比较 - 对y接近0时敏感

应用: 业务报告 (易于理解)

Python
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)

Text Only
                    预测
                正     负
实际    正   TP      FN
        负   FP      TN

TP (True Positive):  正确预测为正
TN (True Negative):  正确预测为负
FP (False Positive): 错误预测为正 (第一类错误)
FN (False Negative): 错误预测为负 (第二类错误)

示例: 癌症检测

Text Only
TP: 正确识别癌症患者
TN: 正确识别健康人
FP: 误诊为癌症 (虚警)
FN: 漏诊癌症 (致命!)

Python
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)

\[Accuracy = \frac{TP + TN}{TP + TN + FP + FN}\]

问题: 类别不平衡时会误导

Text Only
例子: 99人健康, 1人患病

模型: 总是预测健康
准确率: 99/100 = 99%
但完全无用! (没识别出患者)

3. 精确率 (Precision)

\[Precision = \frac{TP}{TP + FP}\]

含义: 预测为正的样本中,真正为正的比例

问题: "预测的病人得病,真的得病了吗?"

应用: - 垃圾邮件分类 (不想误判正常邮件) - 推荐系统 (推荐的内容要准确)


4. 召回率 (Recall / Sensitivity / TPR)

\[Recall = \frac{TP}{TP + FN}\]

含义: 真正为正的样本中,被预测为正的比例

问题: "所有得病的人,被检测出来了吗?"

应用: - 疾病诊断 (不能漏诊) - 异常检测 (不能漏掉异常)


5. F1-Score

\[F1 = 2 \cdot \frac{Precision \cdot Recall}{Precision + Recall}\]

特点: 精确率和召回率的调和平均

为什么用调和平均?

Text Only
算术平均: (1 + 0) / 2 = 0.5
F1公式:  2 × (1 × 0) / (1 + 0) = 0

调和平均对极端值更敏感:
必须两个都好, F1才高

应用: 需要平衡精确率和召回率

Python
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

Text Only
理想情况:
TPR = 1, FPR = 0 (完美分类器)

随机猜测:
TPR = FPR (对角线)

AUC (Area Under Curve):
- AUC = 1.0: 完美
- AUC = 0.5: 随机
- AUC < 0.5: 比随机还差
Python
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? - 类别极度不平衡 - 更关注正类

Python
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):

Text Only
对每个类别单独计算指标,再平均
- 每个类别权重相同
- 适合关注小类别

微平均 (Micro Average):

Text Only
全局计算混淆矩阵,再计算指标
- 样本数多的类别影响大
- 适合关注整体性能

Python
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. 训练集/验证集/测试集

Text Only
训练集 (Train, 60-70%):
用于训练模型参数

验证集 (Validation, 15-20%):
用于调参和模型选择

测试集 (Test, 15-20%):
用于最终评估,只使用一次!
Python
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)

Text Only
数据分成K份:
┌─────┬─────┬─────┬─────┬─────┐
│  1  │  2  │  3  │  4  │  5  │
└─────┴─────┴─────┴─────┴─────┘

第1轮: [测试] [训练] [训练] [训练] [训练]
第2轮: [训练] [测试] [训练] [训练] [训练]
...
第5轮: [训练] [训练] [训练] [训练] [测试]

最终性能 = 5轮的平均

优点: - 充分利用数据 - 减少方差 - 更稳健的评估

Python
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折可能打乱类别分布

解决: 每折保持类别比例

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[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]
    # 训练和评估...

4. 时间序列交叉验证

不能用随机划分! (会破坏时间顺序)

Text Only
方法: 前向链 (Forward Chaining)

训练1: [1-2-3]  →  测试: [4]
训练2: [1-2-3-4] →  测试: [5]
训练3: [1-2-3-4-5] → 测试: [6]
...
Python
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]
    # ...

🎛️ 超参数调优

什么是超参数?

Text Only
模型参数 (Model Parameters):
- 算法学习得到
- 例如: 权重w, 偏置b

超参数 (Hyperparameters):
- 人工设置
- 例如: 学习率、树的深度、正则化系数
- 需要调优!

穷举所有组合

Python
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_

问题: 组合爆炸!

Text Only
4 × 4 × 2 = 32种组合
如果10个参数,每个10个值 = 10^10种 (不可能!)

随机采样参数组合

Python
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

Python
# 推荐使用 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


常用超参数

随机森林

Python
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

Python
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正则
}

神经网络

Python
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]\),展开如下:

\[\text{MSE} = \mathbb{E}[(y - \hat{f}(x))^2]\]
\[= \mathbb{E}[(f(x) + \epsilon - \hat{f}(x))^2]\]
\[= \mathbb{E}[(f(x) - \hat{f}(x))^2] + 2\mathbb{E}[(f(x) - \hat{f}(x))\epsilon] + \mathbb{E}[\epsilon^2]\]

由于 \(\epsilon\)\(\hat{f}\) 独立且 \(\mathbb{E}[\epsilon]=0\),中间交叉项为零,第三项 \(= \sigma^2\)

对第一项,加减 \(\mathbb{E}[\hat{f}(x)]\)

\[\mathbb{E}[(f(x) - \hat{f}(x))^2] = \mathbb{E}\left[\left((f(x) - \mathbb{E}[\hat{f}]) + (\mathbb{E}[\hat{f}] - \hat{f})\right)^2\right]\]
\[= (f(x) - \mathbb{E}[\hat{f}])^2 + \mathbb{E}[(\hat{f} - \mathbb{E}[\hat{f}])^2] + 2(f(x) - \mathbb{E}[\hat{f}])\underbrace{\mathbb{E}[\hat{f} - \mathbb{E}[\hat{f}]]}_{=0}\]

因此:

\[\boxed{\text{MSE} = \underbrace{(f(x) - \mathbb{E}[\hat{f}(x)])^2}_{\text{Bias}^2} + \underbrace{\mathbb{E}[(\hat{f}(x) - \mathbb{E}[\hat{f}(x)])^2]}_{\text{Variance}} + \underbrace{\sigma^2}_{\text{不可约噪声}}}\]
  • Bias²:模型的平均预测与真实值的偏离程度(模型"能力不足")
  • Variance:模型在不同训练集上预测的波动程度(模型"过于敏感")
  • \(\sigma^2\):数据本身的噪声,任何模型都无法消除

理解偏差和方差

Text Only
偏差 (Bias):
模型过于简单,无法拟合数据
→ 欠拟合
→ 训练集和测试集误差都大

方差 (Variance):
模型过于复杂,对数据变化敏感
→ 过拟合
→ 训练集误差小,测试集误差大

可视化:

Text Only
高偏差高方差:
    🎯🎯🎯
  🎯🎯🎯🎯🎯
🎯🎯🎯🎯🎯🎯🎯

高偏差低方差 (欠拟合):
    ●●●
    ●●●
    ●●●

低偏差低方差 (理想):
    🔥🔥🔥
    🔥🔥🔥
    🔥🔥🔥


诊断与解决

Python
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

Python
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)

在验证集性能不再提升时停止训练

Python
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 (深度学习)

Python
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. 数据增强

Python
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()
])

📈 学习曲线

可视化训练过程

Python
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()

分析:

Text Only
训练和验证都低 → 高偏差
训练高,验证低 → 高方差
都高且接近 → 理想


🎯 实战建议

评估清单

Text Only
1. 数据划分:
   □ 是否有独立的测试集?
   □ 是否用验证集调参?
   □ 时序数据是否保持顺序?

2. 评估指标:
   □ 是否符合业务目标?
   □ 是否考虑类别不平衡?
   □ 是否查看多个指标?

3. 调参:
   □ 是否理解每个超参数的作用?
   □ 是否从粗到细搜索?
   □ 是否记录实验结果?

4. 过拟合:
   □ 训练/验证差距是否过大?
   □ 是否应用了正则化?
   □ 是否有足够的训练数据?

快速评估脚本

Python
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,掌握数据预处理和特征构建!