2.4 模型评估 (Model Evaluation) 本节摘要:模型评估回答一个朴素却关键的问题——这个模型放到没见过的新数据上,到底靠不靠谱。训练集上漂亮不等于真本事,模型可能只是把答案背了下来,一上考场就露馅。本节把评估拆成两块:分类任务看混淆矩阵、准确率、精确率、召回率、F1 和 ROC 曲线,回归任务看均方误差、平均绝对误差、均方根误差与决定系数。最后讲交叉验证为什么比单次划分更可信,以及怎么按业务场景挑指标。
本节摘要:模型评估回答一个朴素却关键的问题——这个模型放到没见过的新数据上,到底靠不靠谱。训练集上漂亮不等于真本事,模型可能只是把答案背了下来,一上考场就露馅。本节把评估拆成两块:分类任务看混淆矩阵、准确率、精确率、召回率、F1 和 ROC 曲线,回归任务看均方误差、平均绝对误差、均方根误差与决定系数。最后讲交叉验证为什么比单次划分更可信,以及怎么按业务场景挑指标。
阅读完本节,你应当能够:
先说个反直觉的场景。你训练了一个垃圾邮件过滤器,测试下来准确率 99%,听起来已经能上线了。可这堆邮件里本来就有 99% 是正常邮件,1% 是垃圾邮件。哪怕模型什么都不做、把每封都判成"正常",它也能拿到 99% 的准确率——而真正的垃圾邮件一封都没拦下来。
这就是模型评估要解决的核心问题:用错了尺子,再高的分数也没意义。 训练集上的表现只能说明模型把见过的数据记住了多少,不代表它在陌生数据上还能站稳。我们要的,是估计模型那项看不见的能力——泛化能力。
所以评估这件事,一半是"量什么",一半是"怎么量"。"量什么"指选指标,分类和回归各有一套;"怎么量"指怎么划分数据、怎么算平均,交叉验证就是为此准备的。下面先讲分类的尺子。
分类模型每做一个预测,无非落在四种结局里。把真实标签当行、预测标签当列,填进一张表,就是混淆矩阵:
| 真实 \ 预测 | 预测为正 | 预测为负 |
|---|---|---|
| 实际为正 | 真正例(TP) | 假反例(FN) |
| 实际为负 | 假正例(FP) | 真反例(TN) |
四个格子的名字听上去绕,其实就一个判断口诀:第一个字说"猜对没",第二个字说"猜成了啥"。猜对了是"真",猜错了是"假";猜成"正"是正例,猜成"负"是反例。真正例和真反例在对角线上,代表猜对的;假正例和假反例在反对角线上,代表猜错的。
用医院的场景最好记。把"有病"当正例:真正例是"确诊了,确实有病",假正例是"误诊了,其实没病",假反例是"漏诊了,其实有病"。误诊让人白担心,漏诊可能耽误人命,这两种错误的代价完全不同——这就是为什么不能只看一个准确率。
从混淆矩阵出发,能算出四个常用指标:
准确率最迷惑人的地方,是它把四个格子统统摊平,只看"猜对的比例",却对"谁被猜错了"视而不见。一旦数据里两类悬殊,这个盲区就被无限放大。前面那 99% 的垃圾邮件例子不是极端个例,而是现实常态:欺诈交易、设备故障、罕见病,正例往往只占千分之几。此时一个只会回答"正常"的空模型,准确率也能轻松到 99.9%,却一个真正例都没抓到。
这也解释了为什么不能只看准确率,而要请出精确率、召回率、F1。精确率盯着"我报的阳性里有几个是真",召回率盯着"真的阳性我捞回了几个",两者都只关心少数类这一侧的表现,不会被多数类刷分。F1 再把两者压成一个数,方便横向比较。选哪个,取决于业务更怕误报还是漏报——但无论如何,类别一失衡,准确率就该退居二线,只作参考。
动手验证一下,用鸢尾花数据:
from sklearn.metrics import confusion_matrix, classification_report # y_test 是真实标签,y_pred 是模型预测 print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred))
classification_report 会把每个类别的精确率、召回率、F1 和支持度(该类别样本数)一次列全,比单独调函数省事。多分类时记得指定 average 参数:macro 是各类别平均、不看样本量,weighted 按样本量加权,micro 则把所有格子的数合并后再算。
还有一对只看概率、不看阈值边界的指标:ROC 曲线和AUC。分类器内部通常先算出一个概率,再拿阈值切成正负。挪动阈值,误报率和召回率会此消彼长。把不同阈值下的(假正例率,真正例率)连成线,就是 ROC 曲线;曲线下的面积就是 AUC。AUC 越接近 1,说明模型越能把正例排在负例前面——它衡量的是"排序能力",跟阈值怎么定无关,因此在类别不平衡时尤其稳。
回归预测的是连续数值,评估就是看"预测值和真实值差多远"。差得远不远,有好几种量法,区别在于怎么惩罚大误差、以及单位好不好解释。
| 指标 | 惩罚方式 | 量纲 | 对异常值 | 典型用途 |
|---|---|---|---|---|
| 均方误差 | 平方,重罚大误差 | 目标的平方 | 敏感 | 模型训练的目标函数 |
| 平均绝对误差 | 绝对值,不放大 | 与目标一致 | 稳健 | 误差大小要直观沟通时 |
| 均方根误差 | 平方后开根 | 与目标一致 | 敏感 | 报告"平均偏差多少单位" |
| 决定系数 | 相对比例 | 无量纲 | 受影响 | 横向比较多个模型 |
一个实用的经验:报给业务方时优先说 MAE 或 RMSE,因为单位好懂;内部比模型时用 R² 更直观。用代码算一遍很快:
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score import numpy as np mse = mean_squared_error(y_test, y_pred) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mse) r2 = r2_score(y_test, y_pred)
指标选定了,还有一个更隐蔽的问题——把数据切成一份训练、一份测试,这刀切在哪,分数会跟着漂。 数据小的时候尤其明显,换个随机种子,准确率可能从 0.90 跳到 0.84。你根本分不清是模型真的变了,还是只是运气好切到了好验证的那一刀。
K 折交叉验证就是对付这个漂移的。把数据均匀切成 K 份,轮流拿其中一份当验证集、其余 K-1 份当训练集,训 K 次、评 K 次,最后取平均。每个样本都当过验证样本,分数不再由某一刀的运气决定。
from sklearn.model_selection import cross_val_score, StratifiedKFold from sklearn.linear_model import LogisticRegression skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(LogisticRegression(), X, y, cv=skf, scoring='f1') print(scores.mean())
几个策略要分清:
| 策略 | 做法 | 适用场景 | 代价 |
|---|---|---|---|
| 留出法 | 一次切分 | 数据足够大、快速摸底 | 分数受切分运气影响 |
| K 折 | 均匀切 K 份轮换 | 常规首选 | 要训 K 次 |
| 分层 K 折 | 保持各类别比例 | 类别不平衡的分类 | 同上,略慢 |
| 留一法 | 每次留一个样本 | 数据极小 | 计算量巨大 |
对分类任务,几乎总是该用分层 K 折——它能保证每个折里类别比例和整体一致,避免某一折里恰好缺了少数类。cross_val_score 只给分数,想要训练时间、训练集分数等更全的信息,换成 cross_validate,它返回一个字典。
⚠️ 常见坑:默认用准确率当评分。类别不平衡时准确率会骗人,还会引导网格搜索选出一个"全预测多数类"的废物模型。调参前先确认
scoring用的是对的那个指标。
⚠️ 常见坑:评估时把预处理也当成模型的一部分丢进交叉验证外部去做,导致验证集的信息泄漏进训练。这正是下一节模型选择、以及后面管道要重点防的事。
💡 关键直觉:精确率和召回率是一对跷跷板,没有哪个天生更好,看业务——垃圾邮件过滤宁漏不误(高精确率),癌症筛查宁误不漏(高召回率)。先想清楚"误报"和"漏报"哪个更贵,指标自然就定了。
挑指标的决策其实很简单:先看是分类还是回归;分类再看类别是否均衡、业务更怕误报还是漏报;回归再看是给业务沟通(用同量纲的 MAE 或 RMSE)还是内部比较(用 R²)。
还有个落地细节值得单独说:交叉验证和网格搜索里的 scoring 用的是字符串别名,分类有 accuracy、f1、roc_auc,回归有 neg_mean_squared_error、r2。回归误差指标前面那个 neg 是"取负"的意思——因为搜索器永远挑分数最大的组合,误差越小越好,于是误差被取负后,越接近 0 的反而越大。读到负的误差分数别慌,取个绝对值才是真实误差。

下一节我们把这套尺子放进模型选择里,用它来挑算法、调参数、诊断过拟合与欠拟合。