在机器学习项目整个流程里,模型评估经常被当成最后一步来对待:训练完,看眼准确率,高就收工,低就继续调,调完再看。很多用Scikit-learn的初学者都是这样过来的,我也不例外。真正踩过几次坑之后才明白,评估不是一个“跑一下出个分数”的动作,而是一个贯穿数据准备、特征工程、模型选择和超参调优全过程的判断框架。你选了哪种指标、怎么切数据,都会直接影响对模型的判断。
哪怕你完整学完吴恩达的机器学习课,或者在网上看过大量入门教程,一旦开始亲手调一个真实的Scikit-learn项目,很快就会发现:模型评估才是真正拉开水平的地方。这篇内容我打算以Scikit-learn为主线,把机器学习模型评估里的核心环节——数据划分、交叉验证、分类/回归指标、模型选择与超参调优、常见问题排查——完整梳理一遍。适合刚开始接触机器学习、想弄清“模型到底好不好”的新手,也适合已经在用Scikit-learn、但发现测试集和交叉验证结果对不上,或者被准确率误导过的同学。看完你至少能建立起一套可复用的评估流程,而不是每次靠猜测。
1. 模型评估的本质:先搞懂你衡量的到底是什么
1.1 评估的本质是泛化能力,不是记忆能力
机器学习模型的任务是从训练数据里学到规律,再把这些规律用在新数据上。这个过程很像学生备考:平时作业做得再好,最终要看的是考场上那些没见过的题能不能答对。训练集就是练习册,测试集就是考场试卷。如果一个人练习册拿满分,一到考场就歇菜,说明他不是学会了知识,而是把题目和答案背下来了。机器学习里的过拟合,就是这种“背诵”行为——模型把训练样本里那些不该记的细节和噪声也记住了,测试集表现自然变差。
所以模型评估的本质,是评估模型在未见数据上的表现,也就是泛化能力。Scikit-learn提供的各类评估函数,本质上都在帮你量化“未见数据表现”这件事。你要把这个底层目的放在心里,否则很容易在评估指标的细节里迷失方向。我见过有人花大量时间比较0.95和0.96的准确率差异,却连测试集和验证集的区别都还没搞清楚,这是典型的把评估当成了“交作业”,而不是“诊断”。
1.2 偏差与方差:评估结果背后的两种“病因”
评估不只是给一个分数,还要帮你看懂模型得的是什么病。这里绕不开偏差和方差这两个概念。可以用打靶来理解:高偏差就是你每次都打偏,落点集中在靶心外侧,对应欠拟合;高方差就是你每次打得忽左忽右、分散得很,对应过拟合。实际训练中,高偏差的典型表现是训练集分数和验证集分数都不高;高方差的典型表现是训练集分数很高、验证集分数明显偏低。这两类情况的应对方式完全不同。
举个例子,在Scikit-learn里训练一个决策树,把max_depth从1慢慢加到几十。训练集准确率一路上升,从不足80%到接近100%,而测试集分数往往是先上升、到某个深度后反而下降。这条曲线的形状其实就是偏差-方差曲线。你不需要每次都把它画出来,但必须理解:看到训练集分数高、测试集分数低,说明方差偏大,优先考虑正则化、降低模型复杂度或者增加数据;两边都低,说明偏差偏大,优先考虑换更强的模型、加特征或者调整特征工程。评估的价值,就是帮你做这类诊断决策。
2. 数据划分:评估的第一道关卡
2.1 train_test_split的参数,你真的用对了吗
绝大多数评估流程都从划分数据开始。Scikit-learn最基础的划分工具是train_test_split,但它的几个参数对评估结果影响很大。
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )test_size=0.2表示拿20%的样本做测试集,剩下的80%做训练集,这是比较常见的比例。数据量很大时,test_size稍微小一点也能接受,因为测试集只要足够代表整体分布就行;数据量很小时,建议用交叉验证代替单次划分。random_state=42是一个随机种子,设了它之后,每次执行代码得到的划分结果都完全一样,实验可以复现。我自己的习惯是固定一个种子,但会在不同种子上多跑几次,观察评估结果是否稳定,不能因为一个种子的结果好就认定模型一定好,那是典型的“幸存者偏差”。
stratify=y是给分类任务用的分层参数。它会让训练集和测试集里的类别比例尽量和原始数据保持一致。如果数据里有90%负类和10%正类,不设置stratify,随机划分很可能导致测试集里只有一两个正类甚至没有正类,模型评估就会失真。对于回归问题,目标变量是连续值,不能用stratify,需要靠其他方式保证划分合理性。
2.2 分层采样为什么是分类任务的默认选择
分层采样是分类任务里特别容易被忽略但特别重要的机制。比如二分类数据,正负比是9:1,样本总量只有1000个。普通随机划分测试集为20%时,理论上测试集中应该有约20个正类,但实际随机性很大,有时正类只剩几个甚至一个都没有。如果测试集里没有足够的正类,模型对正类的召回率就无从谈起了。
StratifiedKFold和train_test_split里的stratify参数都做了同样的事:按类别比例分层抽样。Scikit-learn在分类模型里使用交叉验证时,如果cv参数直接传整数,内部会默认采用StratifiedKFold而不是普通KFold,这算是一个“隐藏默认值”,不少老手也是后面才注意到的。你应该善用这个机制,但也要意识到:它只能保证比例一致,并不等于测试集完全代表真实分布,样本量小的时候仍然需要谨慎。
2.3 时间序列和分组数据:划分方式不能照搬
train_test_split默认会shuffle数据,也就是打乱顺序后再划分。这对大多数表格任务是合理的,但对时间序列数据是个坑。假如你有一批按时间排序的销售数据,要预测未来一天的销量,如果打乱划分,相当于用未来的数据去预测过去,评估结果会严重偏乐观,上线后才会发现模型根本扛不住真实时序。
Scikit-learn提供了TimeSeriesSplit专门处理这类问题。它按时间顺序划分训练集和验证集,训练集永远是较早的数据,验证集永远是较晚的数据。在实际项目里,我还会额外留出一段最晚期的数据作为最终测试集,因为时序模型真正的考验是“未来”。
还有一种情况是分组数据。比如医疗数据里同一个病人有多条记录,或者用户行为数据里同一个用户有多条日志。如果直接用train_test_split随机划分,同一个人的部分数据可能出现在训练集,另一部分出现在测试集,模型等于提前“见过这个人”,评估结果其实是作弊。这时候要使用GroupKFold或GroupShuffleSplit,按组为单位划分,保证同一个人的数据要么全在训练集,要么全在验证集。这类数据泄漏在竞赛和真实业务里都很常见,也是最难排查的问题之一。
3. 交叉验证:让评估结果稳定,而不是靠运气
3.1 K折交叉验证的原理和Scikit-learn实现
单次训练测试划分的结果受随机因素影响很大,训练集和测试集怎么切,直接决定分数高低。为了更稳定,通常采用K折交叉验证:把训练数据分成K份,依次拿一份做验证、剩下K-1份做训练,重复K次,最后把K个验证分数取平均。这相当于做了K次独立的模拟考试,结果比单次划分可靠得多。
在Scikit-learn里最简单的方式是cross_val_score:
from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestClassifier scores = cross_val_score(model, X, y, cv=5, scoring='accuracy') print(scores, scores.mean(), scores.std())这里传入的是完整数据集X和y,cross_val_score会自动完成划分和评估,不再需要提前切train_test_split。cv=5表示做5折交叉验证。前文说过,当cv为整数且模型是分类器时,Scikit-learn默认用StratifiedKFold;当模型是回归器时,默认用普通KFold。很多人忽略这一点,其实它已经帮你做了分层采样。
那K值怎么选?K越大,每一折训练数据越多,模型学习得更充分,评估偏差越小;但同时每一折验证集也越小,验证分数波动会变大,计算成本也会成倍上升。5或10是实践中最常用的值。如果你的数据量只有一两百条,可以考虑更大的K,甚至用留一法。
3.2 cross_validate比cross_val_score多给你什么
cross_val_score只能评估一个指标,很多时候不够用。比如你既想看准确率,又想同时看F1和AUC,如果分别跑两次,不仅慢,而且两次的划分还不一定一样。Scikit-learn提供了cross_validate,可以一次返回多个指标,还能拿到训练时间等信息。
from sklearn.model_selection import cross_validate scores = cross_validate( model, X, y, cv=5, scoring={'accuracy': 'accuracy', 'f1': 'f1', 'roc_auc': 'roc_auc'} ) print(scores['test_accuracy']) print(scores['test_f1'])返回结果是一个字典,键名是test_accuracy、test_f1这类,直接对应你在scoring里指定的名称。多指标同时观察,能避免你只盯着一个指标做出偏颇的判断。比如准确率不错但F1很低,说明类别不平衡问题依然严重;AUC很高但精确率很低,说明模型排名能力不错但阈值选择不对。这些信息在cross_val_score里很难一次拿到。
3.3 KFold、StratifiedKFold、GroupKFold和RepeatedKFold怎么选
交叉验证策略不止一个,选错策略等于评估从源头就跑偏。我把常用几种整理成了对照表:
| 交叉验证器 | 主要用途 | 说明 |
|---|---|---|
| KFold | 普通回归/无分层需求的分类 | 将所有样本平均分成K份,轮流验证 |
| StratifiedKFold | 分类任务 | 每一折保持原始类别比例,比KFold更稳 |
| GroupKFold | 同一组样本不能分开的场景 | 按组划分,防止同组数据同时出现在训练和验证 |
| RepeatedKFold | 样本量小、结果波动大的场景 | 重复多次K折,用多次的平均值降低方差 |
| LeaveOneOut | 样本量极小的场景 | 每个样本单独做一次验证,计算量大 |
我平时用的最多的是StratifiedKFold,只要任务是分类,都会优先考虑。如果你自定义了交叉验证器,同时希望它分出来的折能保持类别比例,也可以在构建StratifiedKFold之前确认好样本顺序。RepeatedKFold则适合样本量不太大、单次K折分数起伏明显的情况,它会把整个K折过程重复若干次,所有折的验证分数合并统计,结果更稳定。在Scikit-learn里,这类重复交叉验证器是RepeatedKFold和RepeatedStratifiedKFold两个类。
3.4 留一法(LOOCV)什么时候值得用
留一法LeaveOneOut是最极端的K折:K等于样本数,每个样本单独作为验证集一次,其他所有样本用来训练。这意味着如果样本有100条,就要训练100次模型。它的好处是几乎用到了所有数据,评估结果偏差很小;坏处是方差可能偏大,而且计算成本极高,因为每次训练集高度相似,模型训练重复度大。
如果样本量只有几十条、模型训练也很快,LOOCV是可以考虑的选项。但大多数中等规模以上的数据集,我更推荐RepeatedStratifiedKFold,它的计算效率更高,而且多次重复能同时照顾偏差和方差。
4. 分类模型评估指标:准确率之外还有一整片海洋
4.1 混淆矩阵:所有分类指标的老家
很多人一上来就用accuracy_score,这个指标在类别平衡时还能用,在类别不平衡或业务场景复杂时很容易骗人。要看透分类指标,建议先从混淆矩阵开始。以二分类垃圾邮件过滤为例,真实情况是“这个邮件确实是垃圾”或“这不是垃圾”,模型预测也会给出“判断为垃圾”或“判断为正常”,于是产生四种组合:
| 实际垃圾邮件 | 实际正常邮件 | |
|---|---|---|
| 预测为垃圾 | TP(真阳性) | FP(假阳性) |
| 预测为正常 | FN(假阴性) | TN(真阴性) |
准确率就是(TP+TN)/总样本数,表示整体预测正确的比例。精确率precision是TP/(TP+FP),衡量的是“模型说这是垃圾邮件时,到底说对了几成”。召回率recall是TP/(TP+FN),衡量的是“真正的垃圾邮件里,模型抓回了几成”。F1则把精确率和召回率压缩成一个数值,是两者的调和平均。调和平均对较小值更敏感,所以只有当精确率和召回率都不错时,F1才会高,这符合大多数业务的实际需求。Scikit-learn中可以直接用classification_report一次性输出这些都指标。
4.2 精确率和召回率的取舍:看业务到底怕什么
精确率和召回率通常没法同时达到很高,你需要根据业务场景做取舍。垃圾邮件场景里,把一封重要用户邮件误判为垃圾邮件,代价可能是用户错过重要合同,所以企业往往更看重精确率,宁可漏掉一部分垃圾邮件,也不冤枉正常邮件。反过来,癌症早筛场景里,漏检一个真正患者带来的后果非常严重,模型宁可多做进一步检查,也要尽量把患者找出来,此时召回率优先。
实际用Scikit-learn时,除了F1,你还可以用fbeta_score来调整侧重。beta=1就是标准F1,beta>1时更看重召回率,beta<1时更看重精确率。我在做工业项目时,习惯把accuracy、precision、recall、f1全部打印出来看,而不是只挑一个。因为不同业务阶段、不同风险偏好下,最终取舍可能完全不一样。
4.3 类别不平衡:用PR曲线而不是准确率
当数据里正类占比很低时,准确率就变成了一个陷阱。假设1%的样本是垃圾邮件,模型把所有邮件都预测为正常邮件,准确率也能高达99%,但模型实际上一个垃圾邮件都没抓到。这种情况下真正应该看的是Precision-Recall曲线(PR曲线)。
Scikit-learn里可以用precision_recall_curve得到不同阈值下精确率和召回率的对应关系,用average_precision_score把这条曲线压缩成单个数值,方便不同模型之间对比。PR曲线对类别不平衡非常敏感,正类占比越低,基线越低,模型在PR曲线上表现越能反映真实能力。这里有一个特别容易犯的认知错误:PR曲线的基线和0.5没有关系,而是等于正类比例。正类只有1%时,无脑把所有样本判为正类,precision就是0.01。所以看到AP为0.2时,先对比基线0.01,才知道模型其实已经提升了不少。
4.4 ROC曲线与AUC:衡量排序能力
ROC曲线和AUC是又一对经典评估工具。ROC曲线以假阳性率FPR为横轴,真正率TPR为纵轴,AUC是曲线下方的面积。宏观理解,AUC表示模型把随机的正样本排在随机的负样本前面的概率,它只关心排序,不依赖具体阈值,所以常用来做模型选择。
Scikit-learn里用roc_curve和roc_auc_score就能完成计算。AUC=1是完美排序,AUC=0.5等于随机猜。类别不平衡时,AUC依然能给出相对稳定的判断,但当正类比例极低时,AUC也可能显得偏乐观,因为假阳性的大量积累要在曲线末端才体现出来。所以我的建议是:通用场景下可以同时报告ROC-AUC和PR-AUC,如果做的是异常检测、欺诈识别这类极度不平衡的任务,PR-AUC参考价值更高。
4.5 多分类:宏平均和微平均
多分类问题的评估要更复杂一些,因为每个类别都有自己的精确率、召回率和F1。Scikit-learn的classification_report会输出每一类的指标,以及macro avg和weighted avg两行汇总。宏平均是对所有类别的指标做简单算术平均,平等对待每个类别,稀有类别表现不好就会被明显拉低;weighted avg则按每个类别的样本量加权,样本多的类别占主导。
如果你的业务比较关心少数类,应该优先看macro avg和少数类自己的指标。比如工业生产里,99%是正常产品,1%是瑕疵产品,模型的宏平均F1会因为这个弱势类别而变得很难看,而这恰恰反映了业务痛点。只要在报告里能清楚看到每个类别的明细,你才能定位问题。
5. 回归模型评估指标:误差要放在同一把尺子下看
5.1 MSE、RMSE、MAE与R²各代表什么
回归问题和分类完全不同,不能用准确率来评估。最常用的是均方误差(MSE)、均方根误差(RMSE)、平均绝对误差(MAE)和决定系数R²。
MSE把所有误差取平方再平均,相当于对大的误差加了更重的惩罚。RMSE是MSE的平方根,单位和你预测的目标变量一致,方便直接解释。MAE则是误差绝对值的平均,它对每个样本的惩罚是线性的,不容易被极端值牵着走。R²是决定系数,表示模型解释了目标变量多少方差,1代表完美,0代表模型效果等同于直接预测均值,负数表示比直接预测均值还要差。
实际使用中,我建议把这些指标一起打印出来:
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error import numpy as np y_pred = model.predict(X_test) print('R2:', r2_score(y_test, y_pred)) print('RMSE:', np.sqrt(mean_squared_error(y_test, y_pred))) print('MAE:', mean_absolute_error(y_test, y_pred))如果RMSE和MAE差距大,说明误差分布里存在不少离群点,大的误差把RMSE拉高了。这时候你要么去找数据里的异常值,要么换用对噪声更稳健的评估方式。
5.2 残差图:比指标更完整的诊断工具
指标把所有样本的误差压成了一个数,好处是方便对比,坏处是压扁之后,误差的结构信息全丢了。比如模型在预测值小的时候误差很小,在预测值大的时候误差明显增大,这种异方差性用RMSE看不出来,但残差图一眼就能发现。
残差就是y_test - y_pred。画残差图时,横轴是预测值,纵轴是残差。理想情况下,残差应该随机散布在0附近,没有明显规律。如果残差随着预测值增大呈现出漏斗形扩散,说明大预测值的误差波动更大,可能需要给目标变量做对数变换或加权损失。如果残差呈现明显的曲线形状,说明模型漏掉了某些非线性关系,考虑加特征或换模型。我的习惯是,每次回归实验做完,先看残差图,再去看指标,因为指标告诉我“模型差多少”,残差图告诉我“差在哪”。
5.3 噪声数据下的回归评估:不要被离群点绑架
网上很多人把噪声数据和离群点混在一起谈,但实际上它们需要区别对待。噪声是测量误差、环境干扰带来的随机波动,离群点可能是真实但极端的情况。在评估回归模型时,如果数据里存在大量离群点,MSE会被少数几个异常值主导,RMSE高得离谱,但实际上模型在大多数正常样本上表现并不差。
应对方式有两种。第一,评估时改用MAE或者median_absolute_error,它们对离群点的敏感度远低于MSE。第二,训练时改用HuberRegressor这类鲁棒回归模型,它会在误差较小时使用类似MSE的更新,在误差较大时切换为线性损失,相当于自动给极端样本降权。我的经验是,先看数据分布,如果离群点占比很低,优先用鲁棒评估指标;如果离群点占比不低且包含重要业务信息,那就不能简单剔除,而要专门建模处理。
6. 模型选择与超参数调优:评估的最终落脚点
6.1 GridSearchCV:在网格里找到最优超参数
模型评估最终要服务于决策。最常见的决策就是选择模型超参数。GridSearchCV的做法是:把候选超参数组合成网格,对每一组都做交叉验证,选出平均分数最高的一组。
from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC param_grid = {'C': [0.1, 1, 10], 'gamma': [0.01, 0.1, 1]} gs = GridSearchCV(SVC(), param_grid, cv=5, scoring='accuracy') gs.fit(X_train, y_train) print(gs.best_params_, gs.best_score_) print(gs.score(X_test, y_test))这里有一个超级常见的坑:gs.best_score_是训练集上交叉验证得到的分数,它不等于测试集成绩。很多人混淆这两者,把best_score_当成模型最终效果,甚至直接拿它写报告,结果上线后被打脸。正确的做法是,用网格搜索选出的最优参数重新评估测试集,并且确保整个调参过程完全不碰测试集。
测试集应该像“考卷”一样被锁起来,所有网格搜索、比较模型、看学习曲线的操作,都应该在训练集的交叉验证上进行。GridSearchCV内部的CV机制,本质上是在训练集里再切出验证集来寻参,避免直接把超参数拟合到测试集上。
6.2 RandomizedSearchCV:高维空间里更高效的搜索方式
网格搜索的问题是组合爆炸。超参数一多,比如随机森林的n_estimators、max_depth、min_samples_split、max_features,每个参数给几个候选值,组合后的网格可能达到几千组,每组都要跑一次交叉验证,计算量根本无法接受。
RandomizedSearchCV的思路是固定一个预算n_iter,每次从参数的候选分布中随机抽取一组组合,只跑n_iter次交叉验证。虽然它不保证覆盖所有网格,但实际效果往往比同预算的网格搜索更好,因为很多超参数对模型性能的影响并不均衡,网格搜索会把大量预算浪费在无关紧要的维度上。新版本Scikit-learn还提供了HalvingGridSearchCV和HalvingRandomSearchCV,先在小样本上快速淘汰一半较差组合,再用更多数据评估剩下的候选,效率更高,逻辑也很直觉。
6.3 嵌套交叉验证:防止调参过程污染评估结果
假设你先在训练集上用GridSearchCV选出一组最优参数,然后用cross_val_score在这个训练集上再次做5折交叉验证,得到的分数是否有说服力?答案是要打折扣的,因为你在选参数时已经看过整个训练集的交叉验证结果,这会让后续的评估偏差往乐观方向走,这种偏差在样本量越小、超参数组合越多时越明显。
嵌套交叉验证是应对这个问题的严谨方案。外层循环负责评估模型在新数据上的泛化表现,内层循环负责调参。也就是说,外层每折划分后,都只针对外层的训练部分再做一次内部的交叉验证来选择超参数,选好后用外层的验证部分评分。计算量很大,但结果是可信的学术级评估。如果只是做业务项目,不一定每次都要嵌套,但在写论文、评估方法论对比这类场景,一定要用嵌套交叉验证,否则评审很容易指出评估漏洞。
6.4 学习曲线和验证曲线:判断下一步改哪里
模型评估不应该停留在“分数多少”,还应该告诉你“下一步该干什么”。learning_curve是判断是否要加数据的关键工具。它把训练集大小设为横轴,训练分数和验证分数设为纵轴。看到训练分数一直很高、验证分数较低且两者之间存在明显差距,说明模型处于过拟合状态,增加数据往往能有效缩小差距。看到两条曲线都偏低并且靠得很近,说明模型更可能是欠拟合,增加数据帮助不大,应该考虑换更强模型、增加特征或减少正则化。
validation_curve则是变化某个超参数,观察训练和验证分数的变化路径。比如决策树max_depth从1增加到20,你能在图上清楚看到验证分数什么时候见顶,这个点就是过拟合开始的位置。这类图形是我每次实验里一定会看的,因为我需要知道模型是“容量不够”还是“数据不够”还是“特征不够”,评估的目的就是让你少走弯路。
7. 常见问题与排查技巧实录
7.1 为什么交叉验证分数总是不稳定
不少人遇到过这种状况:同一份数据,连续跑两次交叉验证,分数忽高忽低,甚至差好几个百分点。最常见的原因是随机种子没固定。不设random_state,每次划分、每次模型内部采样都不同,结果自然飘。先把random_state和各类交叉验证器的状态固定下来,实验才有可能复现。
固定种子后分数还是波动大,就要考虑数据本身了。样本量太小、类别比例很不均匀、数据分布包含多个差异明显的子群体,都会让单次交叉验证的分数像坐过山车。我建议改用RepeatedStratifiedKFold,把K折交叉验证重复执行多轮,用所有轮次的平均值和标准差来报告结果。只要标准差不太大,这个平均分比单次K折分数要可靠得多。
7.2 测试集上的表现远差于交叉验证结果怎么办
这是最经典也最痛的问题。通常有三个原因:数据泄漏、分布漂移、调参污染。
数据泄漏往往发生在数据预处理阶段。如果先对全量数据做了标准化、PCA或者特征选择,然后再划分训练集和测试集,那么测试集的信息已经在预处理过程中被模型间接看到。正确做法是把预处理放进Pipeline里,让标准化在每个交叉验证折内部单独执行:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression pipe = Pipeline([ ('scaler', StandardScaler()), ('clf', LogisticRegression()) ]) cross_val_score(pipe, X, y, cv=5)这样每一折都是在训练部分fit标准化器,再用它transform验证部分,杜绝泄漏。这个坑我反复踩过,尤其是特征选择配合交叉验证时最容易出错。
分布漂移是另一个原因。训练数据来自过去几个月,测试数据来自最近一两周的线上流量,用户行为本身变了,模型效果自然下滑。这类问题需要在业务层面持续监测数据分布,不能指望一个模型永远有效。调参污染则是把测试集反复拿来比较模型、选择参数,最后模型对测试集也“背下来了”,线下分数虚高,线上回归。解决办法只有一个,把测试集锁死,只在验证集上做选择。
7.3 类别极度不平衡、噪声数据多时的评估建议
在异常检测、欺诈识别这类场景里,类别不平衡和噪声数据往往同时出现。我的建议是:评估时优先用StratifiedKFold,指标用PR-AUC、macro F1,同时单列少数类的精确率和召回率。准确率的参考价值在这种场景里几乎为零。
如果噪声特别多,先做探索性分析,画出目标变量的分布和箱线图,确认极端值的比例。离群点占比在1%以下,可以考虑用HuberRegressor或直接改用MAE作为评估指标;占比超过5%,就要认真考虑这些极端值是不是业务里真实存在的风险信号。另外特别提醒,SMOTE这类过采样方法只能作用于训练集,如果对整个数据集做SMOTE再划分,会造成严重的泄漏,评估结果完全失真。
7.4 预测概率校准:评估的最后一公里
如果模型不只是给出分类结果,还要输出概率,比如点击率预估、信用评分,那么指标之上还需要做概率校准检查。Scikit-learn里有calibration_curve,可以画出模型预测概率和真实频率之间的关系。如果预测概率0.8的样本里,真实正例比例只有0.6,说明概率严重失真,直接拿去做风控阈值会很危险。
解决方法是使用CalibratedClassifierCV做校准,它会用独立的验证集把模型输出概率映射到更接近真实频率的范围。这件事在单看AUC或F1时是察觉不到的,但对实际业务落地影响巨大。我的习惯是,凡是做概率输出的项目,正式评估报告里一定会附一张校准曲线图。
关于模型评估,我自己的体会是:它不是一个函数调用,而是一种思考方式。每次实验之前,先把评估方案定下来——用哪种数据划分策略、什么指标、是否重复交叉验证——再开始训练。否则你会发现自己在不断调参数、看分数,却始终说不清模型到底好在哪、差在哪。另一个小经验是,养成把评估指标和划分策略写进代码配置的习惯,每次实验固定下来,团队讨论时大家聊的是同一个“分数”,而不是各自跑了不同验证集、不同种子得出完全不同的结论,最后陷入无休止的口水战。