训练集测试集划分

在机器学习的世界里,数据是驱动一切模型的燃料,然而,如何正确地使用这些燃料,决定了你的模型是能精准预测未来的智能引擎,还是一个只会死记硬背的复读机。

今天,我们将深入探讨机器学习中一个至关重要且基础的概念:训练集与测试集的划分。这是你构建任何可靠模型的第一步,也是评估模型真实能力的关键。

简单来说,训练集和测试集的划分,就像学生时代的学习与考试:

  • 训练集 是学生的教材和练习题,模型用它来学习数据中的规律和模式。
  • 测试集 是最终的期末考试,模型用它来检验自己是否真正掌握了知识,而不是仅仅记住了练习题(训练集)的答案。

为什么必须划分训练集和测试集?

想象一下,如果一个学生只复习了老师给的模拟题,并且考试题目就是一模一样的模拟题,他得了满分。这能证明他真正理解了这个学科吗?显然不能。他可能只是记住了答案。

在机器学习中,如果我们在全部数据上训练模型,然后又用这同一份数据去评估它的性能,就会犯同样的错误。模型会表现得异常出色,因为它已经"见过"并"记住"了所有数据的细节,包括其中的噪声和偶然性。这种现象被称为过拟合

过拟合的模型就像一个只会背诵例题的学生,一旦遇到新的、没见过的题目(新数据),就会表现得很差。它的"泛化能力"很弱。

因此,我们必须将数据分成两部分:

  1. 训练集:用于模型,让它学习。
  2. 测试集:用于模型,评估它处理从未见过的新数据的能力。

测试集必须与训练集完全隔离,在整个模型训练过程中都不能被模型看到。只有这样,测试集上的评估结果才能客观地反映模型的真实泛化能力。


如何划分:常用方法与策略

划分数据听起来简单,但其中也有不少学问。不同的划分策略适用于不同的场景。

1. 简单随机划分

这是最基础、最常用的方法。将整个数据集随机打乱,然后按一定比例切分成两部分。

实例

# 示例:使用 Python 的 scikit-learn 库进行随机划分
from sklearn.model_selection import train_test_split

# 假设 X 是特征数据,y 是标签数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"训练集样本数:{len(X_train)}")
print(f"测试集样本数:{len(X_test)}")

代码解析

  • train_test_split:这是 scikit-learn 中用于划分数据的核心函数。
  • X, y:输入的特征数据和对应的标签。
  • test_size=0.2:指定测试集的大小比例为 20%(即训练集占 80%)。你也可以用 train_size=0.8 来指定。
  • random_state=42:设置一个随机种子。这能确保每次运行代码时,划分的结果都是完全相同的,这对于实验的可复现性至关重要。你可以将其设置为任意整数。

2. 分层抽样划分

在分类问题中,如果数据集的类别分布不均衡(例如,90%是A类,10%是B类),简单的随机划分可能导致训练集和测试集中各类别的比例差异很大,影响评估的公平性。

分层抽样可以确保划分后的训练集和测试集中,各个类别的比例与原始数据集保持一致。

实例

# 示例:在分类问题中使用分层抽样
from sklearn.model_selection import train_test_split

# 假设 y 是分类标签
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

# 检查划分后的类别比例
from collections import Counter
print("原始数据类别分布:", Counter(y))
print("训练集类别分布:", Counter(y_train))
print("测试集类别分布:", Counter(y_test))

代码解析

  • stratify=y:这是关键参数。它告诉函数按照标签 y 的类别分布来进行分层抽样。

3. 时间序列数据划分

对于时间序列数据(如股票价格、每日气温),数据点之间存在时间上的依赖关系。我们不能随机打乱,因为未来的数据不能用来预测过去。

通常的做法是按时间顺序划分:用前 80% 时间的数据作为训练集,后 20% 的数据作为测试集

实例

# 示例:时间序列数据的顺序划分
split_index = int(len(X) * 0.8) # 计算80%位置的索引

X_train, X_test = X[:split_index], X[split_index:]
y_train, y_test = y[:split_index], y[split_index:]

print(f"训练集时间范围:前 {split_index} 个样本")
print(f"测试集时间范围:后 {len(X) - split_index} 个样本")

划分比例如何选择?

这是一个常见问题,但没有固定答案。常见的比例有:

比例 (训练集:测试集) 适用场景 优点 缺点
70:30 中小型数据集(数千到数万样本)的经典选择 平衡了训练数据量和评估可靠性 对于极小数据集,30%的测试集可能样本太少,评估不稳定
80:20 目前更流行的默认选择,尤其适用于深度学习 为模型提供了更多数据用于学习 测试集相对较小,评估的方差可能略大
90:10 或 95:5 数据量非常有限时 最大化利用有限数据进行训练 测试集太小,评估结果可能不可靠,置信度低

核心原则

  1. 确保训练集足够大:模型需要足够的数据来学习有效的模式。
  2. 确保测试集足够大:测试集需要提供统计上可靠的性能评估。通常,测试集至少应有几百个样本,评估结果才比较稳定。
  3. 数据量越大,分配给测试集的比例可以相对越小,因为即使很小的比例也可能代表大量的样本。

进阶概念:验证集与交叉验证

在实际项目中,我们不仅需要评估最终模型,还需要在训练过程中调整模型的超参数(如学习率、树的深度等)。如果直接用测试集来调整参数,那么测试集就又被"污染"了,失去了作为"最终考官"的公正性。

为此,我们引入了验证集

三数据集划分:训练集、验证集、测试集

  1. 训练集:用于模型参数的学习。
  2. 验证集:用于在训练过程中调整超参数、选择模型或进行早停。它相当于"模拟考"。
  3. 测试集:在模型和超参数都确定后,用于最终、一次性的性能评估。它是"最终大考"。

实例

# 示例:先划分出训练+验证集 与 测试集,再从训练+验证集中划分出验证集
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.15, random_state=42) # 先分出15%作为最终测试集
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.176, random_state=42) # 从剩下的85%中分出约15%作为验证集

# 计算比例: 0.85 * 0.176 ≈ 0.15, 最终比例约为 70:15:15
print(f"训练集:{len(X_train)}, 验证集:{len(X_val)}, 测试集:{len(X_test)}")

K折交叉验证

当数据量不大时,单独划分验证集会进一步减少训练数据。K折交叉验证是更强大的解决方案。

其流程如下,可以有效利用有限的数据:

实例

flowchart TD
    A[原始数据集] --> B[随机打乱并均匀分为K份]
    B --> C{进行K轮循环}
    C --> D[第i轮: 将第i份作为验证集]
    D --> E[其余K-1份合并作为训练集]
    E --> F[在该轮训练集上训练模型]
    F --> G[在该轮验证集上评估得分Si]
    G --> C
    C -- K轮完成后 --> H[计算K个得分的平均值作为最终评估]

实例

# 示例:使用5折交叉验证评估模型
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression

model = LogisticRegression()
scores = cross_val_score(model, X, y, cv=5) # cv=5 表示5折交叉验证

print(f"各折得分:{scores}")
print(f"平均得分:{scores.mean():.4f} (+/- {scores.std()*2:.4f})") # 输出平均分和标准差

交叉验证的优点

  • 充分利用所有数据进行训练和验证。
  • 评估结果更加稳定可靠(因为是多次评估的平均)。
  • 是中小数据集上进行模型选择和调参的黄金标准。

实践练习:动手体验数据划分

现在,让我们用一个简单的数据集来实践一下。

实例

# 1. 导入必要的库
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# 2. 加载鸢尾花数据集
iris = load_iris()
X, y = iris.data, iris.target
print(f"数据集形状:特征 {X.shape}, 标签 {y.shape}")

# 3. 简单随机划分 (80%训练, 20%测试)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print(f"随机划分 -> 训练集:{X_train.shape}, 测试集:{X_test.shape}")

# 4. 分层随机划分
X_train_s, X_test_s, y_train_s, y_test_s = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
print(f"分层划分 -> 训练集:{X_train_s.shape}, 测试集:{X_test_s.shape}")

# 5. 检查分层效果
print("\n原始数据类别分布:", np.bincount(y))
print("随机划分后测试集分布:", np.bincount(y_test)) # 可能不均衡
print("分层划分后测试集分布:", np.bincount(y_test_s)) # 应与原始分布成比例

你的任务

  1. 运行上面的代码,观察输出结果。
  2. 尝试修改 test_size 为 0.3,观察训练集和测试集大小的变化。
  3. 尝试修改 random_state 为另一个数字(如 7),再次运行,观察划分结果是否变化。
  4. (挑战)不设置 random_state 参数,多次运行代码,观察每次的划分结果是否相同。

总结与核心要点

  • 核心目的:划分训练集和测试集是为了评估模型的泛化能力,防止过拟合,确保模型能处理新数据。
  • 黄金法则测试集必须在整个训练过程中完全保密,仅用于最终评估。
  • 划分方法
    • 随机划分:最通用。
    • 分层划分:适用于分类问题中的不均衡数据。
    • 顺序划分:适用于时间序列数据。
  • 划分比例:没有绝对标准,需在"足够训练"和"可靠评估"间权衡。80:20 或 70:30 是常见起点。
  • 进阶工具
    • 验证集:用于模型调参,保护测试集的纯洁性。
    • K折交叉验证:中小数据集的评估和调参利器,结果更稳健。