哈喽,大家好~
“模型在训练集上准确率都 99% 了,为什么一到线上就不行?”
面试官经常会这样问。很多刚接触机器学习的同学,也容易把训练集、验证集和测试集混在一起。
这篇文章我们就把这三个数据集讲清楚:它们分别做什么、为什么不能混用,以及如何用 Python 完成一次完整的数据划分、调参和最终评估~
01模型到底在做什么?
我们可以把机器学习模型理解成一个“找规律的函数”。
比如,我们想根据用户的年龄、消费次数、访问时长,判断这个用户是否会购买商品。模型会接收输入特征 ,输出预测结果 :
这里的 就是模型, 是模型内部需要学习的参数。
训练的过程,就是不断调整 ,让预测结果 尽量接近真实标签 。常见的损失函数可以写成:
就是:模型先做预测,计算错得有多严重,然后根据错误不断修正自己。
但问题来了:如果我们拿全部数据训练,模型只是在“背答案”,并不能说明它真的学会了规律。
所以,数据必须分成不同部分。
02三个数据集分别做什么?
最常见的划分方式是:
训练集:用来学习参数。
模型通过训练集学习规律,例如神经网络中的权重、逻辑回归中的系数,都是从训练集里学出来的。
验证集:用来选择模型和调整参数。
比如我们需要决定树的最大深度、正则化强度,或者神经网络训练多少轮。验证集就像模拟考试,帮助我们选择表现更好的方案。
测试集:只用来做最终验收。
测试集应该一直“藏起来”,直到模型和参数全部确定后,最后使用一次。它更接近模型面对真实新数据时的表现。
你可以这样理解:
训练集负责学习,验证集负责选择,测试集负责打分。
假设有1000条数据,可以按照60%、20%、20%划分:
-
训练集:600条 -
验证集:200条 -
测试集:200条
如果数据量比较少,也可以使用交叉验证。它会把训练数据分成多份,轮流使用其中一份验证,最后取平均结果。
03一个容易踩坑的问题:数据泄漏
假设我们先对全部数据做标准化,再划分训练集和测试集:
scaler.fit_transform(X)
这时测试集的均值和方差已经参与了处理,模型等于提前“偷看”了测试数据。
正确做法是:只在训练集上拟合预处理器,再处理验证集和测试集。
对于分类任务,划分数据时还要使用stratify=y,保证每个数据集中的类别比例大致一致。
04Python实现
下面,我们完成训练、验证、调参和测试。我们比较不同的逻辑回归正则化参数~
-
验证集准确率曲线:观察参数变化对模型的影响。 -
PCA二维投影图:观察测试样本的分类结果和错误样本。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
from sklearn.decomposition import PCA
# 1. 数据
X, y = make_classification(
n_samples=1800,
n_features=8,
n_informative=5,
n_redundant=1,
n_classes=2,
class_sep=1.15,
flip_y=0.08,
random_state=42
)
# 2. 划分训练集、验证集、测试集
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.4, stratify=y, random_state=42
)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42
)
# 3. 在验证集上选择正则化参数C
c_values = [0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30]
val_scores = []
for c in c_values:
model = Pipeline([
("scaler", StandardScaler()),
("lr", LogisticRegression(C=c, max_iter=1000))
])
model.fit(X_train, y_train)
val_scores.append(model.score(X_val, y_val))
best_c = c_values[np.argmax(val_scores)]
print("最佳C:", best_c)
print("验证集准确率:", max(val_scores))
# 4. 用训练集+验证集重新训练最终模型
X_train_final = np.vstack([X_train, X_val])
y_train_final = np.concatenate([y_train, y_val])
final_model = Pipeline([
("scaler", StandardScaler()),
("lr", LogisticRegression(C=best_c, max_iter=1000))
])
final_model.fit(X_train_final, y_train_final)
# 5. 测试集只评估一次
test_pred = final_model.predict(X_test)
print("测试集准确率:", accuracy_score(y_test, test_pred))
print(classification_report(y_test, test_pred))
# 6. 绘图
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 图一:参数与验证集表现
axes[0].plot(c_values, val_scores, marker="o",
color="#ff3366", linewidth=3)
axes[0].axvline(best_c, color="#2255ff", linestyle="--",
label=f"best C={best_c}")
axes[0].set_xscale("log")
axes[0].set_xlabel("Regularization C")
axes[0].set_ylabel("Validation Accuracy")
axes[0].set_title("Validation Performance")
axes[0].grid(alpha=0.25)
axes[0].legend()
# 图二:PCA投影后的测试集预测
pca = PCA(n_components=2)
X_test_2d = pca.fit_transform(
StandardScaler().fit_transform(X_test)
)
correct = test_pred == y_test
axes[1].scatter(
X_test_2d[correct, 0], X_test_2d[correct, 1],
c=y_test[correct], cmap="spring", s=42,
alpha=0.75, label="Correct"
)
axes[1].scatter(
X_test_2d[~correct, 0], X_test_2d[~correct, 1],
c="#00d4ff", edgecolors="#111111", marker="X",
s=90, label="Wrong"
)
axes[1].set_title("Test Set: PCA Projection")
axes[1].set_xlabel("Principal Component 1")
axes[1].set_ylabel("Principal Component 2")
axes[1].legend()
plt.tight_layout()
plt.show()
第一张图展示了不同 值对应的验证集准确率。 越大,正则化越弱,模型会更努力拟合训练数据; 越小,正则化越强,模型会更加平滑。
如果验证集准确率先上升后下降,通常说明模型从“学得不够”逐渐变成了“开始过拟合”。我们选择曲线最高点对应的参数,而不是凭感觉选择最大参数。
第二张图先使用PCA把8维特征压缩成二维,方便观察测试样本的空间分布。彩色圆点表示预测正确的样本,带黑边的蓝色叉号表示预测错误的样本。
注意,PCA图只是辅助分析,不能代替真实的测试指标。因为降维会损失部分信息,二维图上的距离不一定完全等同于原始特征空间中的距离。
05实验中应该怎么划分?
这里有几个原则很重要。
第一,测试集不要参与调参。你看了测试集结果,再修改模型,测试集就已经不再“客观”了。
第二,预处理要放进Pipeline。这样标准化、编码、特征选择等步骤都能和模型绑定,减少数据泄漏。
第三,类别不平衡时,不要只看准确率。还要关注Precision、Recall、F1以及ROC-AUC。
第四,时间序列数据不能随机打乱。例如预测未来销量时,训练数据应该来自过去,测试数据应该来自未来。
最后
训练集是模型学习的地方,验证集是选择模型的地方,测试集是最后验收的地方。三者分工清楚,评估结果才更接近模型真正的泛化能力。
一句话概括:不要让模型只会做见过的题,要用没有参与训练的新数据检验它是否真的学会了。
接下来大家可以尝试把逻辑回归替换成随机森林或XGBoost,比较不同模型的验证集表现;也可以增加噪声比例,观察数据质量变差后,训练集和测试集之间的差距如何变化~

