ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Scikit-learn跑通机器学习全流程:数据切分、训练评估与调参

用Scikit-learn跑通机器学习全流程:数据切分、训练评估与调参

机器学习这个词这两年被说得太玄了。事实是,当你跳过概念包装,坐到电脑前准备跑通一个模型时,会发现整个流程远比想象中朴素:准备数据、训练模型、评估效果、调参数,最后把模型保存下来,就这一条线。这也是我推荐第一次接触机器学习的人直接用 Scikit-learn 的原因——它把常见的算法都封装成了同样的接口,让你能把注意力放在流程和数据上,而不是跟底层数学死磕。这篇文章我会用一个标准分类任务把这条链路完整走一遍:数据加载、预处理、切分、训练、评估、调参、保存。每个环节我都会解释为什么这么做,以及哪些地方新手最容易翻车。

1. 动手之前,先把目标定清楚

我见过不少初学者拿到教程就复制代码,跑通一个输出很兴奋,但第二天换一个数据集就完全不知道怎么办。问题不在代码能力,而在于没有建立“机器学习是一个流程”的认知。

1.1 为什么第一站是 Scikit-learn

选择第一个框架,最重要的不是功能多,而是接口统一、资料多、踩坑成本低。Scikit-learn 最舒服的一点,是几乎所有模型都遵循同样的三步:fit()训练、predict()预测、score()评估。这意味着你今天学会用随机森林,明天换成梯度提升或者 SVM,代码结构几乎不用变,只换一行模型名称就行。

另外,Scikit-learn 和 Python 数据生态的衔接非常自然。数据从 pandas 里读进来是 DataFrame,喂给模型之前可以用 numpy 数组,中间用 Scikit-learn 的预处理工具做转换,整个流程不需要在多种框架之间来回横跳。

对于第一个模型来说,这两点比算法本身的精度重要得多。你先要建立手感,而不是追求调参的极限。

1.2 一条主线走完的四个环节

很多教程上来就讲算法公式,我觉得顺序搞反了。第一次做机器学习,脑子里应该先有这条主线:

  1. 数据准备:数据长什么样、有没有缺失、特征是否连续、标签分布是否均匀。
  2. 特征处理:把原始数据转换成模型能理解的形式,比如标准化、编码分类变量。
  3. 模型训练:选择一个算法,在训练数据上拟合出规律。
  4. 评估与迭代:不是看一眼准确率就完事,要看模型在没见过的数据上表现如何,再决定要不要调参数。

这四个环节不是线性的,后续评估发现问题会让你回去改数据或特征。但顺序一定是先有数据意识,再考虑模型。很多人第一步就栽在“想用复杂模型”上,结果数据一堆问题没处理,再好的算法也白搭。

2. 数据准备与预处理,别急着训练

数据是一切模型的地基。这里我用一个经典数据集来演示:鸢尾花分类。它包含 150 个样本、4 个特征(花萼长度、花萼宽度、花瓣长度、花瓣宽度),标签是三种鸢尾花的类别。虽然简单,但它完整覆盖了分类任务的所有关键环节,很适合做第一个项目。

2.1 认识你的数据集

加载数据只需要一行代码:

from sklearn.datasets import load_iris data = load_iris() X = data.data # 特征矩阵,150 x 4 y = data.target # 标签数组,150

但加载完不是直接开训练,我建议你先花两分钟看看数据的结构和分布。

print(X.shape) # (150, 4) print(data.feature_names) print(set(y)) # {0, 1, 2}

做一个简单的统计分析也很有用,比如 pandas 里描述各特征的均值、标准差和取值范围。

import pandas as pd df = pd.DataFrame(X, columns=data.feature_names) df['label'] = y print(df.describe())

这一步不复杂,但能帮你建立对数据的直观感受。特征取值范围差异大不大?有没有明显的离群值?标签类别是否平衡?这些问题会影响后续的预处理方式。

2.2 切分与标准化的正确姿势

训练模型前有两件事必须做:切分数据集和特征标准化。这两步的顺序很重要,我单独拿出来说。

首先是切分。

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/20 是常见比例,数据量大时可以留 10%,数据很少时反而要留更多,否则测试集太薄没说服力。
  • random_state=42:固定随机种子,保证每次运行切分结果一样。这样你调参前后对比才有意义,不然每次效果不同,根本无法判断改动是否有效。
  • stratify=y:按标签比例分层抽样。比如三种花各占三分之一,切分后训练集和测试集里也大致是这个比例。如果标签不平衡,这一步尤其关键。

其次是标准化。

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

注意这里用的是fit_transform和transform的组合训练集和测试集。很多人第一次写会忽略顺序,我重点说明一下。

2.3 为什么要“偷看”测试集数据

StandardScaler的原理是计算每个特征的均值和标准差,然后做变换:

z = (x - mean) / std

问题在于,这个 mean 和 std 从哪里来。正确做法是只用训练集的数据计算 mean 和 std,然后同样作用于训练集和测试集。

你不能先对全部数据做标准化,再切分。如果这样做了,测试集的均值和标准差已经参与了训练时使用的统计量计算,测试集就不再是“没见过的数据”了。这属于数据泄漏,会导致评估结果虚高。部署到真实环境后,新数据是单独进来的,没有任何统计量从它身上预先计算出来,效果就会大打折扣。

所以代码里要写成:先fit_transform训练集,再用同一个 scaler 的transform处理测试集。这一步是新手最容易踩的坑之一,后面我会专门展开说。

另外说一句,随机森林、决策树这类基于树模型的算法对特征缩放不敏感,标准化对它们没坏处,但也不是必需的。如果第一个模型选的是逻辑回归、SVM、KNN 这类依赖距离或梯度的算法,标准化就是必需的。这里我用随机森林做演示,但数据预处理的标准流程先走一遍,后面换算法不会措手不及。

2.4 基线模型的作用

所谓基线模型,就是先不用复杂调参,直接跑通一个流程的模型。它的用处有两个:确认整条链路没有 bug;提供一个性能参考点。

比如先用最朴素的逻辑回归跑一遍:

from sklearn.linear_model import LogisticRegression baseline = LogisticRegression(max_iter=1000) baseline.fit(X_train_scaled, y_train) print("Baseline accuracy:", baseline.score(X_test_scaled, y_test))

甚至可以用DummyClassifier来做一个“无脑乱猜”的表现作为下限参考。有了这个下限,后面的模型如果还不如它,说明流程里有问题。

建立基线模型的思路,比一开始就追求高准确率更重要。你在一个个模型迭代中,才能看清哪些改动真正带来了提升。

3. 训练模型的全流程实战

现在进入核心环节。我用随机森林完整训练一个分类模型,然后给出可复制的代码和每一步的说明。

3.1 核心代码:让第一个模型跑起来

from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix # 1. 加载数据 data = load_iris() X, y = data.data, data.target # 2. 切分数据 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 3. 标准化处理 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 4. 初始化并训练模型 model = RandomForestClassifier(n_estimators=100, random_state=42) model.fit(X_train_scaled, y_train) # 5. 预测与评估 y_pred = model.predict(X_test_scaled) print("Accuracy:", model.score(X_test_scaled, y_test)) print(classification_report(y_test, y_pred, target_names=data.target_names)) print(confusion_matrix(y_test, y_pred))

我建议你把这段代码自己敲一遍,不要复制。这个过程会帮你记住整个流程的结构。

3.2 模型评估:不只是准确率

第一次跑通代码的输出里,除了 accuracy 还会有两样东西:分类报告和混淆矩阵。这两样东西的价值往往被新手忽略。

看一个分类报告例子(具体数字会因随机种子略有差异):

precision recall f1-score support setosa 1.00 1.00 1.00 10 versicolor 0.92 0.90 0.91 10 virginica 0.90 0.92 0.91 10 accuracy 0.93 30 macro avg 0.94 0.94 0.94 30 weighted avg 0.93 0.93 0.93 30

注意看support列,它表示每个类别在测试集里的样本数。因为用了stratify=y,三个类都是 10 个,分布均匀。

precision是“预测成这个类别的样本里,有多少真属于这个类别”;recall是“这个类别的真实样本里,有多少被正确找回来了”。这两个指标常常此消彼长,单独看任何一个都可能有偏差。

准确率在这里是 93%,听起来还不错。但如果这是类别不平衡的数据,比如 99% 的样本都是类别 A,那么一个模型什么都不学,全部预测 A,准确率也有 99%。所以准确率只能作为第一印象,不能作为唯一指标。

再看混淆矩阵,它的作用是把预测结果和真实标签放在一个表格里,让你直观看到模型在哪些类之间容易混淆。如果versicolor被错分成了virginica,说明这两个类在特征空间里确实很接近,你后续可以重点分析它们。

3.3 调参初体验:网格搜索与交叉验证

模型第一次跑出 93% 准确率后,常见的冲动是赶紧调参。这个想法没问题,但方式要注意。一个经典误区是反复在同一个测试集上试效果,试来试去,测试集就被你“记住”了,评估自然失真。

更稳妥的做法是引入交叉验证,把训练数据再切几份,轮流做验证集,综合多次结果来评判参数好坏。Scikit-learn 提供了GridSearchCV,它把网格搜索和交叉验证打包好了:

from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [50, 100, 200], 'max_depth': [None, 5, 10], 'min_samples_split': [2, 5, 10] } grid_search = GridSearchCV( RandomForestClassifier(random_state=42), param_grid, cv=5, scoring='accuracy' ) grid_search.fit(X_train_scaled, y_train) print("Best params:", grid_search.best_params_) print("Best CV score:", grid_search.best_score_)

这里的cv=5表示把训练集分成 5 份,轮流用其中 4 份训练、1 份验证,最终取平均得分。这样做出来的分数比单次切分更稳定,不容易被某一次运气好的切分影响。

拿到最优参数后,再用这个参数重新在完整训练集上训练模型,最后在X_test_scaled上评估一次。注意,测试集从头到尾只碰一次,这是原则。

best_model = grid_search.best_estimator_ y_pred_best = best_model.predict(X_test_scaled) print(classification_report(y_test, y_pred_best, target_names=data.target_names))

GridSearchCV 的缺点也很明显:它把每种参数组合都完整训练一遍,组合数多时耗时很长。这个数据集小还好,真实场景下可以先手动粗调,再用网格搜索精调。先理解原理,再考虑效率,顺序不能反。

4. 新手最易踩的坑与排查方法

我见过不少学生在同一个地方反复栽跟头。把这些坑列出来,比多讲十个算法都有用。

4.1 数据泄漏比模型差更致命

这个问题我在前面已经提到过,但值得单独拿出来:数据泄漏是新手常犯的错误。它的本质是,训练过程中使用了本不该被使用的信息——通常是来自测试集的信息——导致评估结果虚高。

常见的泄漏场景有这么几种:

  • 在全量数据上做标准化或归一化后再切分。
  • 用全量数据做特征选择(比如计算所有特征与标签的相关系数)之后才切分。
  • 缺失值使用全量数据的均值填充后再切分。
  • 对时间序列数据直接随机切分,导致测试集里出现未来数据。

排查起来很简单:问自己一个问题,“这条信息在模型实际部署预测时,能被提前知道吗?”如果不能,那它就不应该参与训练。

一旦发现泄漏,不要想着“只是损失一点可信度”。泄漏会让模型上线后表现远差于预期,而且原因极难排查。宁可重新跑一遍流程,也不要留着带病代码。

4.2 类别不平衡时怎么办

鸢尾花数据集三个类别一样多,很规整。但真实项目里,比如欺诈检测、故障预测,正负样本往往比例悬殊。这时用准确率评估几乎没意义,因为模型只要全部预测多数类就能拿到漂亮分数。

应对的思路主要有几类:

  • 评估指标改用查准率、查全率、F1 或 AUC,它们对少数类的关注更充分。
  • 在模型层面设置class_weight='balanced'。很多 Scikit-learn 分类器都支持这个参数,它会自动给少数类更高的权重,让模型把更多注意力放上去。
  • 在数据层面做重采样,比如用imbalanced-learn库做少量类过采样。这个方法要谨慎,过度过采样容易过拟合。

对第一个项目来说,我的建议是先不着急用高级重采样方法,先把评估指标从准确率切换到 F1,很可能就已经能发现问题了。

4.3 过拟合与特征选择

过拟合的表现是:训练集分数接近满分,测试集分数明显变差。模型背下了训练数据里的细节,包括噪声,却没有学到可泛化的规律。

最容易感受到这个问题是在数据量很小的时候,比如鸢尾花这种 150 样本的数据集。模型一复杂,很容易就把训练样本的特征“记住”了。

从第一次做模型就养成防过拟合的习惯,后面会省很多事:

  • 交叉验证的分数比单次切分更可信,先用它衡量模型。
  • 复杂度高的模型优先考虑正则化参数,比如树的max_depth、min_samples_split。
  • 观察特征重要性。随机森林训练完,可以通过feature_importances_看哪些特征对预测贡献更大,贡献极小的特征可以考虑剔除。
import numpy as np importance = best_model.feature_importances_ for name, imp in zip(data.feature_names, importance): print(f"{name}: {imp:.4f}")

特征重要性可以帮助你理解模型依赖的是什么,这也是这个工具最有价值的地方——它不只是给出预测,还能告诉你“为什么”。

5. 模型保存与使用:让模型真正可用

模型训练完成、评估过关之后,最后一个环节是把模型保存下来。很多人会忽略这一步,但实际工作里,模型最终要交给应用调用,保存和加载是必备技能。

5.1 保存与加载模型

Scikit-learn 官方推荐用joblib来保存模型,它比 Python 自带的pickle对大型 numpy 数组的序列化效率更高:

import joblib joblib.dump(best_model, 'iris_model.joblib')

加载也一样简单:

loaded_model = joblib.load('iris_model.joblib')

加载后的loaded_model和原来那个模型对象完全一样,直接调predict就行。

这里有一个容易被忽略的问题:不同版本的 Scikit-learn 之间可能存在兼容性问题。在 A 机器上保存的模型,拿到 B 机器上加载,如果 Scikit-learn 版本差太多,可能报错。所以保存模型时,最好把运行环境一并记录,尤其是依赖库的版本:

pip freeze > requirements.txt

不然半年后想重新部署,环境装不回来就麻烦了。

5.2 部署时的特征工程对齐问题

保存模型只是第一步,实际部署时更隐蔽的问题在于“特征工程对齐”这是一件我没少吃瘪的事。

怎么理解呢?训练时你写了这样一段代码:

X_train_scaled = scaler.fit_transform(X_train)

你保存模型的时候,如果只保存了分类器,没保存这个 scaler,到线上推理时,新数据是原始数值,根本没有被标准化。喂给模型的特征分布和训练时完全不一致,预测结果自然没有保证。

所以保存模型时,要把整个处理链一起保存。更规范的做法是把预处理和模型组合成一个Pipeline:

from sklearn.pipeline import Pipeline pipeline = Pipeline([ ('scaler', StandardScaler()), ('classifier', RandomForestClassifier(n_estimators=100, random_state=42)) ]) pipeline.fit(X_train, y_train) joblib.dump(pipeline, 'iris_pipeline.joblib')

线上加载后,直接传入原始特征就能完成整套流程,不用在业务代码里重新实现标准化逻辑。这样才能保证训练和推理阶段的数据处理是一致的。

另外,特征顺序也很重要。训练时特征顺序是[花萼长度, 花萼宽度, 花瓣长度, 花瓣宽度],线上传来的是同一顺序吗?如果线上特征来自数据库查询,列顺序变了,模型就会拿错数据。比较稳妥的做法是在保存模型的同时保存一份特征列表,加载后再核对输入数据的列名和顺序。这一步看似琐碎,但它决定了模型从实验到生产的迁移是否稳当。

我第一次做模型保存时,就漏掉了 scaler,线上预测结果惨不忍睹,排查了很久才发现是加载后直接调predict,没走标准化。后来养成一个习惯:所有预处理步骤和模型打包进Pipeline,训练时怎么处理,推理时就怎么处理,不给自己留手动操作的空间。这样踩过几次坑之后,后面再没出过同类问题。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进