模型评估与调参-避免过拟合
模型训练完了,怎么知道它好不好?怎么让它更好?这是被很多初学者轻视、但实际上极为关键的一步。
模型评估与调参:避免过拟合
模型训练完了,怎么知道它好不好?怎么让它更好?这是被很多初学者轻视、但实际上极为关键的一步。
一、为什么模型评估是独立的学问
"准确率高就是好模型"——这个想法很危险
很多初学者觉得模型评估很简单:"跑一下,看准确率,越高越好"。这个想法在大多数情况下都会给你错误的答案。
模型评估之所以是一个独立的学问,有三个根本原因:
第一,准确率不是唯一指标,也不总是正确指标。
想象一个医疗诊断模型,判断患者是否有癌症。如果癌症患者只占1%,那一个"什么都判断为正常"的模型准确率是99%——看起来很好,但实际上毫无用处,因为它没有检测出任何癌症患者。不同的应用场景需要关注不同的指标。
第二,在训练集上的表现没有意义。
模型的目标是对它从没见过的新数据做出准确预测,而不是"背答案"。如果只在训练集上评估,你看到的只是模型记忆能力,不是泛化能力。
第三,超参数调优需要一个"独立的评判者"。
你不能用测试集来选择模型,因为一旦你根据测试集表现来调整模型,测试集就不再是"真实世界的模拟"了。
二、过拟合:死记硬背 vs 真正理解
用类比建立直觉
**过拟合(Overfitting)**就像一个学生死记硬背。他背下了所有练习题的答案,考试时只要题目和练习题一模一样,他能满分;但题目稍有变化,他就不会了。他学到的不是解题方法,而是具体答案。
**欠拟合(Underfitting)**则相反,就像一个完全不学习的学生,他的"答题策略"是"所有题目都答A"。他对练习题表现差,对测试题也表现差,因为他根本没有从数据中学到任何有用的规律。
恰当拟合是目标:学生理解了解题方法,对练习题和新题目都能举一反三,表现稳定。
识别过拟合/欠拟合
# 过拟合的症状:
# 训练集准确率: 99%
# 测试集准确率: 65% ← 差距巨大,过拟合了
# 欠拟合的症状:
# 训练集准确率: 65%
# 测试集准确率: 63% ← 差距很小,但两个都很低,欠拟合了
# 理想状态:
# 训练集准确率: 92%
# 测试集准确率: 89% ← 差距小,且两个都高
过拟合在代码里的示例:
# 错误做法:在训练集上评估
model.fit(X, y)
score = model.score(X, y) # 这是训练集准确率,没有意义!

过拟合/最优/欠拟合三个区域的损失曲线
三、训练集/验证集/测试集:三种角色
为什么需要三个集合
只有两个集合(训练集和测试集)有什么问题?问题在于:如果你根据测试集表现来调整模型,你就在"对着答案改作业"。测试集表现再好,也不代表模型真的泛化——你只是把模型调到对这批测试数据特别好。
验证集的存在,就是让你可以放心地用它来调参,因为你还有一个"最后的测试集"从未被触碰过,可以作为最终的无偏估计。
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次结果的方差能告诉你模型的稳定性(结果波动大说明模型不稳定)。
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}")
你应该看到类似这样的输出:
5折交叉验证准确率: [0.9667 0.9333 0.9667 0.9667 1.0000]
均值: 0.9667 ± 0.0219
5次的结果都在93%到100%之间,均值约96.7%,标准差约2.2%——说明模型比较稳定。

5折交叉验证示意图
K值怎么选? 常用5或10。K越大,评估越可靠,但计算成本越高。数据量少时用K=10;数据量大时K=5足够。
五、分类指标详解
准确率为什么不够
准确率看似公平,但它对"多数类"有天然的偏向。
在一个99%样本都是正常、1%是欺诈的数据集里,一个"什么都预测为正常"的傻模型准确率高达99%。这个模型没有任何用处,却有非常高的准确率。
真正有用的指标需要区分不同类型的错误:漏报(把欺诈判断为正常)和误报(把正常判断为欺诈),在不同场景下代价是完全不同的。
常见评估指标速查表
混淆矩阵的四个格子:
预测正例 预测负例
实际正例 | 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
- 类别不平衡:避免用准确率
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的直觉解释:随机从正类和负类各取一个样本,模型给正类样本打的分数高于负类的概率。
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=0.92,曲线越高越好
七、超参数调优
超参数 vs 参数:两个不同层次的"设置"
参数(Parameters):模型在训练过程中自动学习的量,比如线性回归的系数、神经网络的权重。训练过程会自动优化它们。
超参数(Hyperparameters):不是模型自己学出来的,而是人为设定的"配置",比如决策树的深度、随机森林的树的数量、学习率。这些决定了模型的"形状"和"能力上限",必须在训练前设定好。
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个随机组合往往能找到接近最优的参数。当参数空间很大时,随机搜索通常是更好的选择。
八、学习曲线:诊断过拟合/欠拟合
学习曲线展示随着训练数据增多,模型在训练集和验证集上表现的变化趋势。
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做完整的实战项目,把本章学过的所有概念串联起来。