机器学习的一次完整流程:准确率 96.7% 的模型未必最好
在鸢尾花数据集上,k=1 的 KNN 测试准确率 96.7%,看起来比 k=5 的 93.3% 更好。但换 20 种随机划分再跑一遍,k=1 的成绩在 86.7% 到 100% 之间来回跳。单次划分的测试成绩噪声很大,拿它来挑参数,挑出来的多半是运气。
本文用一个最小的分类任务走完「切分 → 预处理 → 训练 → 调参 → 评估」的流程,重点放在最容易做错的几步。所有数字都在 Python 3、scikit-learn 1.9.1 上实测,数据集是 scikit-learn 自带的鸢尾花(150 条样本、4 个特征、3 个类别),不需要下载。
一、先说结论
- 测试集要在最开始切出去,只在最后用一次。用它挑参数,测试成绩就不再是「没见过的数据」上的成绩。
- 调参用交叉验证:训练集内部轮流留出一折做验证。实测 k=1 的单次测试成绩最高,交叉验证却选出 k=5。
- 预处理要放进 Pipeline:标准化的均值和方差只能从训练集算。把一个特征的单位放大 1000 倍,不做标准化的 KNN 准确率从 93.3% 掉到 66.7%。
- 训练准确率 100% 不是好消息:k=1 时每个样本最近的邻居就是它自己,训练成绩必然满分。
- 简单数据集上模型差别不大:KNN、逻辑回归、决策树的交叉验证成绩都是 96.0%。数据和评估方法对结果的影响,往往比换模型大。
二、流程全貌
对应的代码很短,关键在于把标准化和模型包进同一个 Pipeline:
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
X, y = load_iris(return_X_y=True)
# 1. 最先切出测试集;stratify 保证三个类别在两边比例一致
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y)
# 2. 标准化和模型放在一起,交叉验证时每一折都只用该折的训练部分计算均值方差
pipeline = make_pipeline(StandardScaler(), KNeighborsClassifier())
# 3. 只在训练集上用 5 折交叉验证选参数
search = GridSearchCV(pipeline, {"kneighborsclassifier__n_neighbors": [1, 3, 5, 7, 9, 15, 25, 50]}, cv=5)
search.fit(X_train, y_train)
print(search.best_params_) # {'kneighborsclassifier__n_neighbors': 5}
# 4. 测试集只用这一次
print(search.score(X_test, y_test)) # 0.933环境准备只需要一个虚拟环境:python3 -m venv venv && ./venv/bin/pip install scikit-learn。NumPy、SciPy 会作为依赖一起安装。
三、实验一:单次划分的成绩不可信
同一次划分下,逐个 k 值对比训练集准确率、测试集准确率和训练集上的 5 折交叉验证准确率:
| k | 训练集 | 测试集(30 条) | 5 折交叉验证 |
|---|---|---|---|
| 1 | 1.000 | 0.967 | 0.942 ± 0.042 |
| 3 | 0.958 | 0.933 | 0.958 ± 0.037 |
| 5 | 0.975 | 0.933 | 0.967 ± 0.031 |
| 7 | 0.975 | 0.967 | 0.958 ± 0.037 |
| 9 | 0.958 | 0.967 | 0.958 ± 0.026 |
| 15 | 0.967 | 0.967 | 0.958 ± 0.026 |
| 25 | 0.958 | 0.900 | 0.900 ± 0.033 |
| 50 | 0.883 | 0.833 | 0.858 ± 0.033 |
如果只看测试集,k=1、7、9、15 并列第一,很可能随手选了 k=1。但测试集只有 30 条,错一条就是 3.3 个百分点。把随机种子从 0 换到 19,重复 20 次划分,k=1 的测试准确率最低 0.867、最高 1.000、平均 0.953。单次测试成绩里差出来的那一两条,几乎全是划分方式带来的噪声。
交叉验证把训练集分成 5 份,轮流用 4 份训练、1 份验证,取平均。它用满了训练数据,还能给出波动范围(表中的 ± 是 5 折之间的标准差)。数据量小的时候,比较参数和模型都应该看交叉验证,测试集只用来报告最终成绩。
表里也能看到两种失败:
- 过拟合:k=1 时训练准确率恒为 1.000,模型只是记住了训练集。
- 欠拟合:k=50 时每次预测都要参考 50 个邻居,而训练集每类只有 40 条,边界被抹平,训练和验证成绩一起下降。
四、实验二:为什么要标准化,以及在哪里标准化
KNN 按欧氏距离找邻居,数值范围大的特征会主导距离。鸢尾花的四个特征都是厘米,差异还不明显。模拟一次「单位不一致」:把花萼宽度乘以 1000,相当于这一列改用微米记录。
| 同一份数据、k=5 | 测试准确率 |
|---|---|
| 不做标准化 | 0.667 |
| 标准化后再训练 | 0.933 |
不做标准化时,距离几乎只由被放大的那一列决定,而它恰好是四个特征里区分度最低的一个。业务数据里这种情况很常见,比如「金额(分)」和「评分(1–5)」放在一起。
标准化本身也会造成泄漏。常见的写法是先对全量数据 fit_transform,再切分训练集和测试集。这样算出来的均值和方差包含了测试集的信息,测试成绩因此偏乐观。把 StandardScaler 放进 Pipeline 以后,fit 只会看到训练部分,交叉验证的每一折也是如此。
五、实验三:准确率之外看混淆矩阵
k=5 的模型在测试集上的混淆矩阵(行是真实类别,列是预测类别):
| 预测 Setosa | 预测 Versicolor | 预测 Virginica | |
|---|---|---|---|
| 真实 Setosa | 10 | 0 | 0 |
| 真实 Versicolor | 0 | 10 | 0 |
| 真实 Virginica | 0 | 2 | 8 |
准确率是 28 / 30 = 0.933,但错误全部集中在一个方向:两朵 Virginica 被判成了 Versicolor。分到类别上看:
- Versicolor 的精确率 10 / 12 = 0.833(预测为它的样本里有两个不是),召回率 10 / 10 = 1.000。
- Virginica 的精确率 8 / 8 = 1.000,召回率 8 / 10 = 0.800(有两个没找出来)。
这就是只看准确率的风险。在业务里,把欺诈交易判成正常和把正常交易判成欺诈,代价完全不同。选指标之前先问清楚哪一类错误更贵:漏报贵就盯召回率,误报贵就盯精确率,两者都要兼顾时看 F1。类别不平衡时(比如 99% 的交易都正常),准确率几乎没有参考价值。
六、实验四:换模型的收益
在全部 150 条数据上做 5 折交叉验证:
| 模型 | 交叉验证准确率 |
|---|---|
| KNN(k=5,标准化) | 0.960 |
| 逻辑回归(标准化) | 0.960 |
| 决策树(不限深度) | 0.960 |
| 决策树(最大深度 2) | 0.933 |
三种原理完全不同的模型成绩相同。数据简单、特征有效的时候,模型选择不是主要矛盾。反过来,特征没有区分度的时候,换什么模型都救不回来。实验二里被放大的那一列,就是一个没区分度却被当成主力特征的例子。
七、放到工程里
对服务端来说,一个训练好的模型就是一个函数加一份评估报告。接入时要关心的是这几件事:
- 训练和线上的特征处理必须一致。标准化参数、编码方式、缺失值填充都属于模型的一部分,要和模型一起导出、一起版本化。线上重写一遍特征处理逻辑,是「离线效果好、线上效果差」的常见原因。
- 评估集要固定下来。每次重新训练或调整参数,都在同一份评估集上跑,结果才可比。这和大模型接入时的评测集是同一个思路。
- 上线后持续监控输入分布。模型只对和训练数据相似的输入负责,业务变化导致特征分布漂移时,准确率会悄悄下降。
- 先做一个简单的基线。逻辑回归或规则系统的成绩就是后续复杂方案要超过的底线,也是复杂方案出故障时的降级方案。
八、常见误区
- 「测试集成绩最高的参数就是最好的」:测试集参与了选择,就不再能代表未见过的数据。
- 「先标准化全量数据,再切分」:测试集的统计信息泄漏到了训练过程中。
- 「训练准确率 100%,模型很好」:更可能是记住了训练集,要看验证成绩。
- 「准确率高就行」:类别不平衡或错误代价不对称时,要看精确率、召回率和混淆矩阵。
- 「效果不好就换更复杂的模型」:先检查特征、数据质量和评估方法。
小结
机器学习流程里最容易出问题的不是模型本身,而是评估:测试集要最先切出、最后使用,调参用交叉验证,预处理放进 Pipeline,指标按错误代价来选。鸢尾花的实验里,这几条规则分别对应一个具体的数字:单次划分 86.7%~100% 的波动、66.7% 对 93.3% 的标准化差距、两个集中在同一方向的误判。
文本数据怎么变成模型输入,以及从词袋到 Transformer 的演进,见 从词袋到 Transformer。
配套实验
- codesphere-labs/ai/ml-and-nlp-basics:鸢尾花上的交叉验证选参、20 种划分下 k=1 的成绩波动、标准化、混淆矩阵与模型对比;在固定版本的 Python 3.14.7 容器里运行(验证记录)
参考资料