课程0基础Agent开发课 / 传统机器学习 / 模型评估与调参-避免过拟合
— 14 min read

模型评估与调参-避免过拟合

模型训练完了,怎么知道它好不好?怎么让它更好?这是被很多初学者轻视、但实际上极为关键的一步。

模型评估与调参:避免过拟合

模型训练完了,怎么知道它好不好?怎么让它更好?这是被很多初学者轻视、但实际上极为关键的一步。


一、为什么模型评估是独立的学问

"准确率高就是好模型"——这个想法很危险

很多初学者觉得模型评估很简单:"跑一下,看准确率,越高越好"。这个想法在大多数情况下都会给你错误的答案。

模型评估之所以是一个独立的学问,有三个根本原因:

第一,准确率不是唯一指标,也不总是正确指标。

想象一个医疗诊断模型,判断患者是否有癌症。如果癌症患者只占1%,那一个"什么都判断为正常"的模型准确率是99%——看起来很好,但实际上毫无用处,因为它没有检测出任何癌症患者。不同的应用场景需要关注不同的指标。

第二,在训练集上的表现没有意义。

模型的目标是对它从没见过的新数据做出准确预测,而不是"背答案"。如果只在训练集上评估,你看到的只是模型记忆能力,不是泛化能力。

第三,超参数调优需要一个"独立的评判者"。

你不能用测试集来选择模型,因为一旦你根据测试集表现来调整模型,测试集就不再是"真实世界的模拟"了。


二、过拟合:死记硬背 vs 真正理解

用类比建立直觉

**过拟合(Overfitting)**就像一个学生死记硬背。他背下了所有练习题的答案,考试时只要题目和练习题一模一样,他能满分;但题目稍有变化,他就不会了。他学到的不是解题方法,而是具体答案。

**欠拟合(Underfitting)**则相反,就像一个完全不学习的学生,他的"答题策略"是"所有题目都答A"。他对练习题表现差,对测试题也表现差,因为他根本没有从数据中学到任何有用的规律。

恰当拟合是目标:学生理解了解题方法,对练习题和新题目都能举一反三,表现稳定。

识别过拟合/欠拟合

python
# 过拟合的症状:
# 训练集准确率: 99%
# 测试集准确率: 65%  ← 差距巨大,过拟合了

# 欠拟合的症状:
# 训练集准确率: 65%
# 测试集准确率: 63%  ← 差距很小,但两个都很低,欠拟合了

# 理想状态:
# 训练集准确率: 92%
# 测试集准确率: 89%  ← 差距小,且两个都高

过拟合在代码里的示例

python
# 错误做法:在训练集上评估
model.fit(X, y)
score = model.score(X, y)  # 这是训练集准确率,没有意义!

过拟合与欠拟合曲线
过拟合/最优/欠拟合三个区域的损失曲线


三、训练集/验证集/测试集:三种角色

为什么需要三个集合

只有两个集合(训练集和测试集)有什么问题?问题在于:如果你根据测试集表现来调整模型,你就在"对着答案改作业"。测试集表现再好,也不代表模型真的泛化——你只是把模型调到对这批测试数据特别好。

验证集的存在,就是让你可以放心地用它来调参,因为你还有一个"最后的测试集"从未被触碰过,可以作为最终的无偏估计。

python
from sklearn.model_selection import train_test_split

# 第一次分割:留出测试集(永远不碰,最后评估用)
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 第二次分割:训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(
    X_temp, y_temp, test_size=0.25, random_state=42
    # 0.25 × 0.8 = 0.2,即总数据的20%用于验证
)

print(f"训练集: {len(X_train)} 样本")
print(f"验证集: {len(X_val)} 样本")
print(f"测试集: {len(X_test)} 样本")

各集合的用途

  • 训练集:模型学习,调整参数
  • 验证集:选模型、调超参数(可以反复使用)
  • 测试集:最终评估,只用一次(模拟真实世界的新数据)

四、交叉验证:更可靠的评估

为什么需要交叉验证:单次划分的随机性

假设你只有500条数据。按照80/20划分,训练集400条、测试集100条。问题是:这100条测试数据是随机选的,测试结果会有很大的随机性——换100条测试数据,结果可能差很多。你无法知道你得到的准确率是真实水平还是碰巧运气好。

**交叉验证(Cross-Validation)**解决了这个问题:不是固定地用某20%做测试,而是把数据分成K份,轮流让每一份做一次测试集,其余的做训练集。最终得到K个结果,取平均值作为模型的性能估计。

交叉验证的直觉:让每一条数据都有机会当"测试样本",评估更全面;K次结果的方差能告诉你模型的稳定性(结果波动大说明模型不稳定)。

python
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

rf = RandomForestClassifier(n_estimators=100, random_state=42)

# 5折交叉验证:把数据分成5份,轮流用每份做测试
cv_scores = cross_val_score(rf, X, y, cv=5, scoring='accuracy')
print(f"5折交叉验证准确率: {cv_scores}")
print(f"均值: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")

你应该看到类似这样的输出

code
5折交叉验证准确率: [0.9667 0.9333 0.9667 0.9667 1.0000]
均值: 0.9667 ± 0.0219

5次的结果都在93%到100%之间,均值约96.7%,标准差约2.2%——说明模型比较稳定。

K折交叉验证
5折交叉验证示意图

K值怎么选? 常用5或10。K越大,评估越可靠,但计算成本越高。数据量少时用K=10;数据量大时K=5足够。


五、分类指标详解

准确率为什么不够

准确率看似公平,但它对"多数类"有天然的偏向。

在一个99%样本都是正常、1%是欺诈的数据集里,一个"什么都预测为正常"的傻模型准确率高达99%。这个模型没有任何用处,却有非常高的准确率。

真正有用的指标需要区分不同类型的错误:漏报(把欺诈判断为正常)和误报(把正常判断为欺诈),在不同场景下代价是完全不同的。

常见评估指标速查表

混淆矩阵的四个格子

code
              预测正例  预测负例
实际正例  |    TP    |    FN   |
实际负例  |    FP    |    TN   |

TP(True Positive):真正例,预测为正且实际为正
FP(False Positive):假正例,预测为正但实际为负(误报)
FN(False Negative):假负例,预测为负但实际为正(漏报)
TN(True Negative):真负例,预测为负且实际为负

四个核心指标

指标 公式 直觉 适用场景
准确率(Accuracy) (TP+TN)/(全部) 整体对了多少 类别均衡时
精确率(Precision) TP/(TP+FP) 我认为有问题的里,真正有问题的比例 宁可漏报不能误报(垃圾邮件)
召回率(Recall) TP/(TP+FN) 所有真正有问题的里,我发现了多少 宁可误报不能漏报(癌症筛查)
F1分数 2×精确率×召回率/(精确率+召回率) 精确率和召回率的综合 两者都重要时

什么时候用什么指标

  • 欺诈检测、癌症筛查:重视召回率(不能漏掉)
  • 垃圾邮件过滤:重视精确率(不能误杀正常邮件)
  • 搜索引擎:两者都重要,用F1
  • 类别不平衡:避免用准确率
python
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score,
    f1_score, classification_report
)
import numpy as np

# 示例:不平衡数据集(99%正常,1%欺诈)
y_true = np.array([0]*990 + [1]*10)  # 真实标签
y_pred_lazy = np.zeros(1000)          # 全预测为0(偷懒模型)
y_pred_smart = np.array([0]*985 + [1]*15)  # 有一定识别能力

print("=== 偷懒模型(全预测正常)===")
print(f"准确率: {accuracy_score(y_true, y_pred_lazy):.4f}")  # 99%!但没用
print(f"召回率: {recall_score(y_true, y_pred_lazy):.4f}")    # 0,没发现任何欺诈

print("\n=== 正常模型 ===")
print(f"准确率: {accuracy_score(y_true, y_pred_smart):.4f}")
print(f"精确率: {precision_score(y_true, y_pred_smart):.4f}")
print(f"召回率: {recall_score(y_true, y_pred_smart):.4f}")
print(f"F1分数: {f1_score(y_true, y_pred_smart):.4f}")

你应该看到偷懒模型准确率99%但召回率为0,而正常模型召回率明显更高——这说明准确率在不平衡数据集上完全无法衡量模型的真实价值。


六、ROC曲线和AUC

ROC曲线(Receiver Operating Characteristic Curve)展示的是:当你改变决策阈值时,模型的真正率(召回率)和假正率(误报率)如何变化。

**AUC(Area Under Curve,曲线下面积)**是ROC曲线下方的面积,取值0到1。

  • AUC = 0.5:随机猜测,和瞎猜一样
  • AUC > 0.9:非常好的模型
  • AUC = 1.0:完美分类器

AUC的直觉解释:随机从正类和负类各取一个样本,模型给正类样本打的分数高于负类的概率。

python
from sklearn.metrics import roc_curve, auc
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer

data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

# 获取预测概率(不是直接预测类别,而是预测每个类别的概率)
y_prob = rf.predict_proba(X_test)[:, 1]

# 计算ROC曲线
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
roc_auc = auc(fpr, tpr)

print(f"AUC: {roc_auc:.4f}")
# 越接近1越好,0.95以上是优秀

你应该看到 AUC约0.98以上,说明随机森林在这个数据集上区分良性/恶性的能力很强。

混淆矩阵热图
混淆矩阵:TP/FP/FN/TN及精确率召回率

ROC曲线与AUC
ROC曲线:AUC=0.92,曲线越高越好


七、超参数调优

超参数 vs 参数:两个不同层次的"设置"

参数(Parameters):模型在训练过程中自动学习的量,比如线性回归的系数、神经网络的权重。训练过程会自动优化它们。

超参数(Hyperparameters):不是模型自己学出来的,而是人为设定的"配置",比如决策树的深度、随机森林的树的数量、学习率。这些决定了模型的"形状"和"能力上限",必须在训练前设定好。

python
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier

# 方法1:网格搜索(穷举所有参数组合)
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, None],
    'min_samples_split': [2, 5, 10]
}
# 一共 3×3×3 = 27 种组合,每种做5折交叉验证 = 135次训练

grid_search = GridSearchCV(
    RandomForestClassifier(random_state=42),
    param_grid,
    cv=5,
    scoring='f1',
    n_jobs=-1,
    verbose=1
)
grid_search.fit(X_train, y_train)

print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳F1: {grid_search.best_score_:.4f}")

你应该看到训练过程打印出最佳参数组合和对应的F1分数。

网格搜索 vs 随机搜索的选择

网格搜索穷举所有组合,但时间复杂度随参数数量指数增长(3个参数各3个取值就是27次训练)。随机搜索看起来"不可靠",但研究表明:大多数参数中只有少数几个真正重要,搜索20~50个随机组合往往能找到接近最优的参数。当参数空间很大时,随机搜索通常是更好的选择。


八、学习曲线:诊断过拟合/欠拟合

学习曲线展示随着训练数据增多,模型在训练集和验证集上表现的变化趋势。

python
from sklearn.model_selection import learning_curve
import numpy as np
import matplotlib.pyplot as plt

def plot_learning_curve(model, X, y, title):
    train_sizes, train_scores, val_scores = learning_curve(
        model, X, y, cv=5,
        train_sizes=np.linspace(0.1, 1.0, 10),
        scoring='accuracy'
    )

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

    plt.figure(figsize=(10, 6))
    plt.plot(train_sizes, train_mean, label='训练集')
    plt.plot(train_sizes, val_mean, label='验证集')
    plt.xlabel('训练样本数')
    plt.ylabel('准确率')
    plt.title(title)
    plt.legend()
    plt.show()

# 如何判断:
# 训练集和验证集分数都低 → 欠拟合(需要更复杂的模型或更多特征)
# 训练集高、验证集低 → 过拟合(需要正则化或更多数据)
# 两者都高且接近 → 理想状态

小结

问题 诊断方法 解决方案
过拟合 训练集高、测试集低 减少模型复杂度、增加数据、正则化
欠拟合 训练集和测试集都低 增加模型复杂度、更多特征
类别不平衡 准确率高但召回率低 class_weight、过采样/欠采样
评估指标选错 准确率高但业务不好 根据业务目标选精确率/召回率/F1

模型评估的核心思想:永远用独立的数据评估模型,选择与任务目标相符的指标,用多次验证而不是单次结果来做决策

下一篇,用sklearn做完整的实战项目,把本章学过的所有概念串联起来。

本页目录