机器学习中的数据分片,指的是将原始数据集切分成多个互不重叠的子集,用于训练、验证和测试模型。这么做是为了在评估模型效果时,模拟模型从未见过的数据,从而避免"自卖自夸"式的过度乐观评估。
📊 常见分片方法
| 方法 | 核心做法 | 适用场景/要点 |
|---|---|---|
| 留出法 | 随机将数据分为训练集(约70-80%)和测试集(约20-30%),训练集用于训练,测试集用于最终评估。 | 数据量较大时简单高效。注意需随机划分,且测试集不能参与任何训练步骤。 |
| K折交叉验证 | 将数据等分为K份(如5份或10份)。每次取其中1份作测试集,其余K-1份作训练集,重复K次,最终结果取平均值。 | 数据量较小时,能更充分地利用数据,评估结果更稳定可靠。计算成本也更高。 |
| 分层采样 | 在划分时,保证训练集和测试集中各类别的比例与原始数据一致。 | 处理类别不平衡数据(如欺诈检测)时的关键步骤,能防止某类样本在某个子集中完全缺失。 |
⚠️ 核心原则:防止数据泄露
数据分片最重要的原则是:"先分片,后预处理"。
如果先对全量数据做标准化或缺失值填充,再划分训练/测试集,那么测试集的统计信息(如均值、方差)就会"泄露"给模型。这会导致模型在测试集上表现虚高,但在真实场景中失效。正确的流程是在分片后,仅在训练集上计算这些参数,再应用到测试集
以下是上述三种数据分片方法的 scikit-learn 实现代码。为了更直观,我使用一个模拟的二分类数据集来演示。
0. 准备模拟数据
import numpy as np
from sklearn.datasets import make_classification
# 生成一个包含1000个样本、20个特征、且类别不平衡(比例约9:1)的数据集
X, y = make_classification(
n_samples=1000,
n_features=20,
n_informative=15,
n_redundant=5,
weights=[0.9, 0.1], # 类别不平衡
random_state=42
)
print(f"数据集大小: {X.shape}, 标签分布: {np.bincount(y)}")
1. 留出法 (Hold-out)
使用 train_test_split。关键参数是 stratify=y,用于在划分时保持类别比例一致。
from sklearn.model_selection import train_test_split
# 第一次划分:训练集 + 临时集(验证集+测试集)
X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.3, # 临时集占30%
random_state=42,
stratify=y # 分层采样,保持类别比例
)
# 第二次划分:将临时集拆分为验证集和测试集
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5, # 验证集和测试集各占一半(即总数据的15%)
random_state=42,
stratify=y_temp
)
print("留出法划分结果:")
print(f"训练集: {X_train.shape[0]} 样本, 标签分布: {np.bincount(y_train)}")
print(f"验证集: {X_val.shape[0]} 样本, 标签分布: {np.bincount(y_val)}")
print(f"测试集: {X_test.shape[0]} 样本, 标签分布: {np.bincount(y_test)}")
2. K折交叉验证 (K-Fold Cross-Validation)
使用 KFold 或 StratifiedKFold。对于分类问题,强烈推荐使用 StratifiedKFold 以保证每折的类别比例一致。
from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
print("K折交叉验证划分结果(展示每折的验证集大小):")
for fold, (train_index, val_index) in enumerate(skf.split(X, y), 1):
X_train_fold, X_val_fold = X[train_index], X[val_index]
y_train_fold, y_va
3. 分层采样 (Stratified Sampling)
分层采样的核心逻辑已经包含在 train_test_split(stratify=y) 和 StratifiedKFold 中。下面单独展示一个自定义比例的分层抽样例子,比如从每个类别中按相同比例抽取样本。
from sklearn.model_selection import StratifiedShuffleSplit
# StratifiedShuffleSplit 可以指定只划分一次,且支持分层
sss = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
print("分层采样划分结果:")
for train_index, test_index in sss.split(X, y):
X_train_strat, X_test_strat = X[train_index], X[test_index]
y_train_strat, y_test_strat = y[train_index], y[test_index]
print(f"训练集: {X_train_strat.shape[0]} 样本, 标签分布: {np.bincount(y_train_strat)}")
print(f"测试集: {X_test_strat.shape[0]} 样本, 标签分布: {np.bincount(y_test_strat)}")
# 验证比例是否一致
train_ratio = np.bincount(y_train_strat) / len(y_train_strat)
test_ratio = np.bincount(y_test_strat) / len(y_test_strat)
print(f"训练集类别比例: {train_ratio}")
print(f"测试集类别比例: {test_ratio}")
⚠️ 核心原则:防止数据泄露的正确流程
以下代码演示了错误做法 和正确做法的区别,以标准化(Standardization)为例。
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
# 划分数据
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# ❌ 错误做法:先对全量数据做标准化,再划分
# scaler_wrong = StandardScaler()
# X_scaled_wrong = scaler_wrong.fit_transform(X) # 测试集信息泄露到了训练过程中
# X_train_wrong, X_test_wrong, y_train_wrong, y_test_wrong = train_test_split(
# X_scaled_wrong, y, test_size=0.2, random_state=42
# )
# ✅ 正确做法:先划分,再仅在训练集上 fit 预处理器
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 只在训练集上计算均值和方差
X_test_scaled = scaler.transform(X_test) # 用训练集的参数来转换测试集
# 训练模型并评估
model = LogisticRegression(max_iter=1000, class_weight='balanced')
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
print(f"正确流程下的测试集准确率: {accuracy_score(y_test, y_pred):.4f}")
总结:
-
留出法 :
train_test_split,记得加stratify=y。 -
K折交叉验证 :
StratifiedKFold,分类问题首选。 -
分层采样 :
train_test_split或StratifiedShuffleSplit的stratify参数。 -
防泄露 :任何预处理(标准化、缺失值填充、PCA等)都必须在划分后,仅用训练集 fit,再 transform 测试集。