数据划分中训练集、验证集、测试集的关系
一、数据划分的“三层结构”:训练集、验证集、测试集
为避免“模型过拟合测试数据”(即调参时依赖测试集性能,导致最终泛化能力评估不准),标准流程会将 原始数据集 DD 划分为三部分:
数据集 | 作用 | 是否参与“调参/模型选择”? |
---|---|---|
训练集 | 用于训练模型参数(如神经网络权重、决策树分裂规则) | 是(模型学习的基础数据) |
验证集 | 用于评估不同参数模型的性能,辅助调参(选择最优参数) | 是(调参的“裁判”) |
测试集 | 仅在模型确定后使用,评估最终模型的泛化能力(模拟未来真实数据表现) | 否(需严格“保密”) |
二、流程拆解:为什么需要“先分训练/验证集调参,再用全量数据重训”?
第一步:原始数据 DD 划分为“训练集 TT”和“测试集 SS”(互不重叠)
- 目的:确保测试集 SS 完全独立于模型训练过程,避免“用测试集信息调参”导致的评估偏差。
- 例如:将 DD 按 7:3 划分为 TT(70% 数据,用于训练和调参)和 SS(30% 数据,仅用于最终评估)。
第二步:将“训练集 TT”再划分为“子训练集 TtrainTtrain”和“验证集 TvalTval”
- 目的:在训练集 TT 内部完成“调参”,避免占用测试集 SS 的数据。
- TtrainTtrain:用于实际训练不同参数的模型(如用不同学习率训练 10 个模型)。
- TvalTval:作为“内部测试集”,评估这 10 个模型的性能,选出在 TvalTval 上表现最好的参数(即“调参”)。
- 例如:将 TT 按 8:2 划分为 TtrainTtrain(训练模型)和 TvalTval(验证调参)。
第三步:用“全量训练集 TT”重新训练最终模型
- 关键操作:当通过 TvalTval 选定最优参数后,需用 整个训练集 TT(包括 TtrainTtrain 和 TvalTval)重新训练模型。
- 为什么? 避免浪费数据:之前为了调参,TvalTval 未参与模型训练,现在参数确定后,用全部 TT 数据训练可让模型学习更充分。
第四步:用独立的“测试集 SS”评估最终模型
- 此时的模型是用 全量训练集 TT 训练的,参数是通过 TvalTval 选定的,而测试集 SS 从未被模型“见过”,因此其评估结果能真实反映泛化能力。
三、疑问解答:“验证集从哪来?”“测试集和验证集是否冲突?”
验证集的来源: 验证集 来自训练集内部的划分(即从 TT 中拆分出 TvalTval),而非原始数据 DD 单独划分。它是训练过程中的“临时裁判”,仅用于调参,调参结束后会“回归”训练集(即全量 TT 重训)。
测试集和验证集的关系:不冲突,各司其职
- 验证集:属于“训练阶段”的工具,用于选择参数,数据来自训练集,会参与最终模型的重训(因此可能存在一定过拟合风险,但通过独立测试集可修正)。
- 测试集:属于“评估阶段”的工具,数据完全独立于训练过程,不参与任何训练和调参,最终用于“检验”模型的真实泛化能力。
- 比喻:
- 训练集 TT = 学生的“课本知识”,验证集 TvalTval = 老师的“随堂测验”(帮学生找到薄弱点,调整学习方法),测试集 SS = “高考”(完全独立,检验真实水平)。
- 调参时用“随堂测验”(验证集)优化学习,最后用“课本全部内容”(全量训练集)复习,再参加“高考”(测试集)。
四、总结:完整流程示意图
核心结论
- 验证集来自训练集内部,用于调参;测试集独立于训练过程,用于最终评估,二者不冲突。
- “先用部分训练集调参,再用全量训练集重训” 是为了平衡“调参可靠性”和“数据利用率”——既通过验证集避免过拟合,又通过全量数据提升模型性能。
数据集划分函数调用---基于sklearn
1.导入库
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris # 采用鸢尾花数据集做测试
'''
train_test_split()
参数:
- *arrays:这里用于接收一到多个列表、numpy数组、稀疏矩阵、padas 中的 DataFrame
- test_size: 测试集大小。可以是一个介于 0.0 和 1.0 之间的浮点数,表示测试集占总数据集的比例;也可以是一个整数,表示测试集的绝对大小
- train_size: 训练集大小。可以是一个介于 0.0 和 1.0 之间的浮点数,表示训练集占总数据集的比例;也可以是一个整数,表示训练集的绝对大小。如果未指定,则根据 test_size 自动计算
- random_state: 控制随机抽样的伪随机数生成器的种子。可以是一个整数,用于生成固定的随机数序列;可以是一个 RandomState 实例,用于自定义随机状态;也可以是 None,此时每次调用都会产生不同的随机状态
- shuffle:分类任务通常需要打乱数据(shuffle=True),但时间序列数据需设为 False 以避免未来信息泄露。
- stratify:当标签 y 的类别分布不均衡时(如 90% 正例,10% 负例),使用 stratify=y 可保证训练集和测试集的类别比例相同。
默认:
shuffle: Any = True,
stratify: Any = None
- 返回值:
- train_test_split 函数返回一个元组,包含分割后的数据集。具体返回值的数量取决于传入的数组数量。如果传入两个数组(如 X 和 y),则返回四个数组;如果传入三个数组,则返回六个数组,依此类推,对于两个数组的情况,返回值如下:
'''
def test2():
dataset = load_iris()
print(dataset)
X = dataset.data
y = dataset.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
print("特征的训练数据集:", X_train)
print("特征的测试数据集:", X_test)
print("标签的训练数据集:", y_train)
print("标签的测试数据集:", y_test)
部分结果
通过初步学习,我们不免产生疑问如下:
疑问:打乱索引shuffle=True为什么最后标签和数据仍是一一对应的?
回答:
indices = [0, 1, 2, 3] # 原始索引
shuffled_indices = [2, 0, 3, 1] # 打乱后的索引
# 按打乱后的索引重新排列数据
X_shuffled = X[shuffled_indices] # X[2], X[0], X[3], X[1]
y_shuffled = y[shuffled_indices] # y[2], y[0], y[3], y[1]
# 详细往后看
必要性:
训练需求:机器学习模型通常假设数据是独立同分布(i.i.d)的。如果数据本身有隐含顺序(如按类别或时间排序),直接拆分会导致训练集和测试集分布不一致。
例如:原始数据按类别排序(前50个是类别A,后50个是类别B),如果不打乱,训练集可能全是类别A,测试集全是类别B。
泛化性:随机化能让模型学习到更全面的数据分布,避免因数据顺序引入偏差。
这里我手动实现两种划分数据集的方法,不采用train_test_split api.
手把手实现train_test_split功能
我们采用'titanic_train.csv'做测试,部分结构如下:
def homework_1():
df = pd.read_csv('titanic_train.csv')
# dataframe索引切片
X_df = df.iloc[:, 0:-1]
y_df = df.iloc[:, -1]
# 转为数组便于操作使X,y维度一致
X = X_df.values
y = y_df.values
print(type(X), X.shape)
print(type(y), y.shape)
# 将标签转为二维数组
y.shape = (len(y), 1)
print(y.shape)
# 不用随机数,选择训练集为0.8 测试集为0.2
# 去掉最后一个样本,方便划分
X = X[:-1, :]
y = y[:-1, :]
# 切片划分训练集和测试集
X_train, X_test = X[0:int(0.8 * len(X)), :], X[int(0.8 * len(X)):, :]
y_train, y_test = y[0:int(0.8 * len(y)), :], y[int(0.8 * len(y)):, :]
print("特征的训练数据集:", X_train)
print("特征的测试数据集:", X_test)
print("标签的训练数据集:", y_train)
print("标签的测试数据集:", y_test)
'''
考虑到前面说到的随机化能让模型学习到更全面的数据分布,避免因数据顺序引入偏差。
手写数据集划分:使用随机数
'''
def homework2():
dataset = pd.read_csv('titanic_train.csv')
# print(dataset.iloc[0:1])
# print(type(dataset))
print(len(dataset))
# 去掉最后一个样本,方便划分
dataset.drop(index=dataset.index[-1], axis='index', inplace=True)
print(len(dataset))
# 设置种子,确保每次划分一致
random.seed(42)
list = random.sample(range(len(dataset)), len(dataset)) # (可迭代对象,总数)
# print(shuffled_df)
# print(list)
# print(len(list))
# 替换每一行的顺序和list一致
shuffled_df = dataset.iloc[list]
# 切片划分数据集
X_train, X_test = shuffled_df.iloc[0:int(0.8 * len(shuffled_df)), :-1], shuffled_df.iloc[
int(0.8 * len(shuffled_df)):, :-1]
y_train, y_test = shuffled_df.iloc[0:int(0.8 * len(shuffled_df)), -1:], shuffled_df.iloc[
int(0.8 * len(shuffled_df)):, -1:]
print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
print(X_train)
print(X_test)
print(y_train)
print(y_test)
# print(i)