资讯详情

机器学习模型评估实战:Scikit-learn数据划分与指标选型

📅 2026/10/18 11:15:07 | 华诺云谱 👁 阅读
机器学习模型评估实战:Scikit-learn数据划分与指标选型
模型评估这件事看起来是所有机器学习工作流里最正规的一环实际却是翻车率最高的一环。我见过太多人用accuracy_score打完一个数就收工也见过不少项目在训练集上跑出96%准确率、一上测试集直接掉到78%然后整个团队开始怀疑数据预处理有问题。说实话大部分情况下不是预处理的问题而是评估方式本身就没建立好——数据划分随意、指标选择和业务目标脱节、只看单一指标、不做交叉验证、也不检查偏差和方差的关系。这篇文章我就用Scikit-learn把模型评估这件事从头到尾捋一遍包括数据划分策略、分类和回归指标的选型逻辑、学习曲线诊断、以及我在实际项目中踩过且真实存在的坑。目标是让读者看完之后能直接上手不再凭感觉评估模型。1. 评估模型之前先解决数据怎么分的问题很多人一上来就train_test_split(X, y, test_size0.2)然后就开始训练。这没错但远远不够。数据划分不只是把数据切成两份它决定了你后面所有指标有没有意义。1.1 一次性划分适合快速验证不适合最终结论一次性划分简单粗暴但它的风险在于你只在一个随机切片上做了评估这个切片运气好分数就高运气差分数就低。尤其是数据量不大的时候比如几千条样本换一个随机种子结果能差好几个点。Scikit-learn里默认的train_test_split是纯随机划分不经过任何处理。我后来在项目里养成了一个习惯random_state必须固定下来并且要在代码注释里写清楚用的是哪个值、为什么用这个值。不然实验没法复现调参的时候你根本分不清是参数变好了还是换了个随机划分变好的。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )上面把stratifyy打开了这一行经常被忽略但非常重要。分层抽样会在划分时保持训练集和测试集里类别比例一致避免某一类样本在测试集里消失或者占比失衡。如果做二分类且正样本只有8%不做分层抽样测试集里很可能出现只有2%正样本甚至没有正样本的情况那时候算出来的指标就没有任何代表性。1.2 交叉验证把评估从碰运气变成算概率分布交叉验证的价值在于它用多份数据上的结果告诉你模型表现的稳定性和平均线。5折交叉验证相当于训练5次、评估5次每一次的测试数据都不重叠最终得到5个分数。如果5个分数之间跳动很大说明模型不稳定可能是数据太少、方差太高也可能是特征里混入了不该有的东西。from sklearn.model_selection import cross_val_score, KFold, StratifiedKFold kfold StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(model, X, y, cvkfold, scoringf1) print(scores) print(f均值: {scores.mean():.4f}, 标准差: {scores.std():.4f})用cross_val_score的时候评分函数scoring参数需要显式指定默认它算的是准确率这在很多场景下不够用。建议根据任务类型提前想清楚指标再传给这个参数。另外KFold和StratifiedKFold的区别有必要说清楚普通KFold把数据平均分成k份每份的类别比例是随机的StratifiedKFold会让每个折的类别分布尽量和全量数据一致。分类问题我基本只用StratifiedKFold回归问题用普通KFold即可。1.3 交叉验证的变体GroupKFold和TimeSeriesSplit如果你手头的数据有分组结构或时间结构标准KFold就不适用了。分组结构比如同一个用户产生了多条行为记录模型可能在测试时遇到训练里见过的用户导致评估分数虚高。这种情况下用GroupKFold按组划分保证同一个组的样本不会同时出现在训练集和测试集里。时间结构销售预测、时序特征明显的场景直接用随机划分会造成未来数据泄漏到训练集。这种情况下应该用TimeSeriesSplit它让训练集始终是测试集之前的时间段模拟真实的预测场景。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): X_train_fold, X_val_fold X[train_idx], X[val_idx]我见过有人做时序预测时用普通KFold结果是模型在过去预测未来的任务里得分极高上线之后全线崩盘。这就是评估策略和业务场景不匹配的典型案例。2. 分类指标那一堆字母到底该看谁准确率、混淆矩阵、精准率/召回率、AUC分类任务的指标选项特别多而且每个指标的侧重点都不同。新手最容易犯的错是只看准确率但这个指标在很多场景下会误导人。2.1 准确率为什么不可信先看混淆矩阵准确率的计算方式是对样本数取平均它默认了每个样本同等重要。但现实任务里往往不是这样。举个例子一个欺诈检测场景正样本欺诈占2%。如果模型把所有样本都判为正常准确率也有98%。这时候你拿98%的准确率跟老板汇报老板以为模型很聪明实际上这个模型什么事都没干把所有欺诈都放过了。真正应该先看的是混淆矩阵它告诉你模型在哪些类上犯了什么错。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay cm confusion_matrix(y_test, y_pred) disp ConfusionMatrixDisplay(confusion_matrixcm) disp.plot()混淆矩阵里有四个格子真正例TP、假正例FP、真负例TN、假负例FN。这四个数字是后面所有指标的地基。看混淆矩阵的时候除了看总数更要关注漏掉的方向是FP还是FN——在欺诈场景里FN把欺诈当正常的代价远高于FP把正常误判为欺诈。2.2 精准率和召回率业务目标决定调哪个精准率Precision TP / (TP FP)分母是模型预测为正类的所有样本它衡量的是模型说正例有多少是真的正例。召回率Recall TP / (TP FN)分母是真实正类的所有样本它衡量的是真实正例里模型找回来了多少。这两个指标天然是有冲突的——你想让召回率提高往往会把阈值调低多抓一些疑似正样本但这样精准率会下降反过来阈值调高精准率上升但召回率下降。实际项目中到底优化哪个取决于漏报和误报哪个代价更高。Scikit-learn里用precision_score和recall_score可以直接算但更常用的方式是用交叉验证同时看两个指标from sklearn.metrics import precision_score, recall_score, f1_score from sklearn.model_selection import cross_validate scoring [precision, recall, f1] results cross_validate(model, X, y, cv5, scoringscoring) for metric in scoring: print(f{metric}: {results[ftest_{metric}].mean():.4f})如果项目里需要一个综合指标F1是精准率和召回率的调和平均适合两者都不想放弃的情况。但注意F1也对类别分布有隐性假设如果正负类别极不平衡F1依然可能偏高或偏低需要结合业务细看。2.3 AUC-ROC和PR曲线分清两者的适用边界AUC-ROC是分类里最常被提起的指标之一。它不依赖具体的分类阈值衡量的是模型把正样本排在负样本前面的能力取值在0到1之间。0.5表示纯随机0.7~0.8算能用0.9以上在多数业务场景里已经很好了。ROC曲线的特点是在正负样本极不平衡时它可能过于乐观。因为ROC的计算用了大量负样本当负样本占绝大多数时假正例率的微小变化对AUC影响不大AUC看起来很高实际上模型在正样本上的表现可能很差。这时候应该看PR曲线Precision-Recall曲线。PR曲线的横轴是召回率纵轴是精准率它直接反映正类被识别的情况在类别不平衡时更加真实。from sklearn.metrics import roc_auc_score, average_precision_score auc roc_auc_score(y_test, y_proba) ap average_precision_score(y_test, y_proba) print(fAUC-ROC: {auc:.4f}) print(fAP: {ap:.4f})average_precision_score是PR曲线下面积的近似值也是不平衡场景里常用的替代指标。我现在的习惯是两个都算一起看。如果AUC很高但AP很低说明模型对多数负样本区分得不错但对少数正样本的识别能力不足这时候模型上线价值有限。3. 回归问题的评估别只看R²残差才是灵魂回归任务的评估同样有陷阱。R²是最常见的回归指标但它只能告诉你模型解释了多大比例的目标变量方差无法告诉你误差的结构和分布。3.1 回归指标逐个拆解MAE、MSE、RMSE、MAPEMAE平均绝对误差对预测值和真实值的差的绝对值取平均。它的单位和目标变量一致解释起来最直观。缺点是梯度不够平滑且对异常值不敏感容易被大量中等误差掩盖掉个别大误差。MSE均方误差对误差的平方取平均。它放大了大误差的影响所以对异常值非常敏感。如果你希望模型尽量少犯大错MSE是合适的指标。RMSE均方根误差MSE开根号单位回到目标变量的维度。但它对异常值的敏感程度和MSE一样。MAPE平均绝对百分比误差每个样本的误差除以该样本真实值再取平均百分比。它的坑在于真实值接近0的样本百分比会爆炸导致整体指标被个别点带偏。Scikit-learn里回归评分函数默认是R²。实际项目中我不太建议只看它一个因为R²对数据分布的依赖很重。我通常的做法是同时输出R²、RMSE和MAE三个指标放在一个表里对比。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae mean_absolute_error(y_test, y_pred) rmse mean_squared_error(y_test, y_pred, squaredFalse) r2 r2_score(y_test, y_pred)顺带一提R²可能是负的。如果模型效果比直接取目标变量均值还差R²就是负数说明模型没有学到任何有意义的模式。3.2 残差分析把误差画出来看回归模型评估里被很多人忽略、但极其重要的一步是画残差图——以预测值为横轴以残差真实值减预测值为纵轴画散点图。理想状态下残差应该随机地分布在0附近看不出任何趋势。但如果残差图上有明显的形状比如漏斗形状预测值变大时残差也变大说明模型的误差存在异方差性如果残差随预测值系统性地偏向一侧说明模型存在偏差可能漏掉了非线性关系或重要特征。import matplotlib.pyplot as plt residuals y_test - y_pred plt.scatter(y_pred, residuals, alpha0.5) plt.axhline(y0, colorred, linestyle--) plt.xlabel(预测值) plt.ylabel(残差) plt.show()我调试过一个销售预测模型R²到了0.9以上看着很漂亮但残差图一画出来高客单价的订单几乎全部被低估。原因也简单——高客单价样本在训练集里数量少模型没学到这个区间段的规律。如果不画残差图这个问题几乎不可能被发现。所以我现在对回归类模型的要求是R²达标不算数残差分布必须合理才算。4. 用学习曲线和验证曲线诊断模型的真实状态模型评估不只是算一个分这么简单。很多时候你需要搞清楚一件事模型表现不好到底是偏差问题还是方差问题这两者的调试方向完全不同。4.1 学习曲线看训练集和验证集的误差如何随样本量变化学习曲线的核心逻辑是横轴是训练样本数量纵轴是模型得分或误差分别画出训练集和交叉验证集上的曲线。如果训练集分数很高、验证集分数很低且两条曲线之间有巨大差距说明模型过拟合了——学得太多训练集特有的噪声。如果两条曲线都很低且随样本量增加没有明显上升趋势说明模型欠拟合——太简单了学不动数据的规律。from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores learning_curve( model, X, y, cv5, train_sizes[0.2, 0.4, 0.6, 0.8, 1.0], scoringneg_mean_squared_error ) train_mean -train_scores.mean(axis1) val_mean -val_scores.mean(axis1)学习曲线对判断加数据是否有用也非常直接。如果曲线在样本量增大时训练和验证分数都趋于收敛说明再加数据效果提升有限如果两条曲线仍在靠近说明加数据还有帮助。4.2 验证曲线调参时判断参数敏感度验证曲线查看的是某个超参数在不同取值下模型的性能表现例如树的深度、正则化系数等。它帮助你发现参数的最佳区间以及模型对参数变化的敏感程度。from sklearn.model_selection import validation_curve param_range [1, 3, 5, 7, 10] train_scores, val_scores validation_curve( model, X, y, param_namemax_depth, param_rangeparam_range, cv5 ) val_mean val_scores.mean(axis1)这里有个容易踩的坑validation_curve里用的是同一个交叉验证划分但不同参数下最优模型对应的最佳阈值不一样。有些模型有概率输出调参时如果只是用固定的阈值比如0.5去评估很可能得出错误的结论。更严谨的做法是在交叉验证里的每一折用该折的训练部分选出最优阈值再在验证部分计算最终分数。5. 模型评估中我实际踩过的几个坑前面讲的是评估方法的框架这里聊聊我在实际项目里真实遇到过的坑。这些都不是教科书例题但几乎每个都是血的教训。5.1 数据泄漏评估流程里最隐蔽的杀手第一类坑是预处理时的数据泄漏。最常见的做法是直接在整个数据集上先做标准化或缺失值填充然后再划分训练集和测试集。这在工程上省了一行代码但测试集的信息比如均值、标准差已经进入了训练过程模型评估结果虚高。正确做法是先划分、再拟合预处理器。可以在训练集上fit然后在测试集上只transform或者直接用Pipeline串联from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression pipeline Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression()) ]) scores cross_val_score(pipeline, X, y, cv5, scoringroc_auc)Pipeline会把预处理的fit过程放到每一折的训练部分里从机制上避免了泄漏。5.2 类别不平衡时指标会严重失真第二个坑在不平衡分类里。我参与过一个风控反欺诈项目正样本比例不到2%前期评估模型时用的指标是AUC。AUC在0.85以上看起来不错但模型真正跑起来后对正样本的召回率只有三成左右大量风险样本漏掉了。原因是AUC对负样本占比高度敏感负样本多的时候AUC被大量容易判对的负样本抬高了。后来把所有评估指标都切到PR类指标AP、F1、recallprecision_threshold模型优化的方向才开始和业务对齐。5.3 多分类问题明确宏平均和微平均的差别第三个坑在多分类。Scikit-learn里f1_score有average参数包括macro和micro。宏平均是对每个类别分别算F1再取平均每个类别权重相同微平均是所有类别的TP、FP、FN加起来之后再算F1本质是全局统计。在类别不均衡的多分类任务里这两种平均方式会给出很不同的结果。宏平均能突出小类的表现微平均则会被大头类别带飞。所以我现在的习惯是macro和weighted都打印出来确认小类没有被牺牲掉。5.4 随机种子与实验结果的可复现性最后一个是工程上的细节随机种子。每次跑同一个模型如果不固定random_state数据划分、模型初始化都会有随机性结果自然不一样。我在项目里最终报告的实验都是用固定的随机种子、固定交叉验证划分跑出来的。即使这样报告结果也要交代清楚seed和划分方式否则别人复现的时候对不上数会非常痛苦。写在最后的实操习惯评估模型这件事现在我基本把它拆成固定几步先按业务场景确定数据划分方式分组、时序、还是普通分层再选定和业务目标对齐的指标交叉验证算均值和标准差画混淆矩阵或残差图最后用学习曲线确认模型状态。每一步都写进代码里固化下来。最后分享一个我个人摸索出来的经验评估分数出炉之后永远多问一句这个分数是在什么划分、什么指标、什么随机状态下算出来的。很多项目矛盾的本质就是评估口径不一致。把评估框架统一了整个团队的沟通成本会低很多模型能不能上线也会更容易达成共识。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑