训练集、验证集、测试集到底按什么比例划分?这是每个刚接触机器学习的同学都会遇到的问题,也是实际项目里最容易被“随手一拍”决定、却直接影响模型可信度的关键环节。标题里提到“训练集70%、验证集20%、测试集10%”和“我们验证和测试:3,7”,这两种说法其实代表了不同的划分策略——前者是经典的三段式划分,后者是更简化、更粗暴的“三七开”。我自己在项目里两种方案都长期用过,踩过不少坑,也总结了一些真正有用的经验。这篇就围绕“数据划分”这一点,把背后的逻辑、实操方法和常见问题一次讲透。
1. 三块数据的职责,为什么不能省
很多人一开始会困惑:训练集和测试集我能理解,为什么还要单独分一个验证集?直接把训练集里再切一小块出来当验证集不行吗?我先把这个基础问题掰清楚,后面所有比例讨论才有意义。
1.1 三个集合各管什么事
训练集(Training Set)是模型学习的素材,模型通过它更新参数、拟合规律。测试集(Test Set)是最终考核,用来评估模型在“从未见过”的数据上的表现,模拟真实上线后的效果。验证集(Validation Set)则是在训练过程中反复使用的“模拟考”,用来做模型选择、超参数调优、早停判断。
这里有一个关键区别:测试集是一次性的,只能碰一次;验证集是可以反复用的,你在训练过程中每次调参、每个epoch结束都可以看验证集结果。如果用测试集反复调参,模型会慢慢“记住”测试集的特征,最终评估结果虚高,这就叫测试集污染。所以验证集存在的本质,是把“调参用的数据”和“最终考核的数据”物理隔离。
1.2 为什么不能只有训练集和测试集
假设你只有训练集和测试集,训练过程中想看模型效果,只能拿测试集来验证。你调了十次参数,就看了十次测试集结果,最后报告出来的准确率其实已经带上了你的“人工拟合”成分,不再是模型真实泛化能力的体现。这好比考试前你把模拟卷答案看了十遍,真正考试时拿的是同一张卷子,分数再高也不能说明你学会了。
实际项目里,验证集还有一个用途是早停(Early Stopping)。深度学习训练经常出现训练loss还在下降、但验证集loss已经开始反弹的情况,这表示模型开始过拟合了。如果没有验证集,你根本不知道在哪个点停下来是最优的。
1.3 验证集和测试集可以合并吗
回到标题里的“3,7”这个问题。很多人说“我们就分训练集70%、测试集30%”,也就是三七开,不再单独留验证集。这种做法能不能成立?答案是:在小项目、快速原型验证阶段,可以;在正式比赛、论文实验、上线前评估阶段,不行。三七开的问题在于,你反复用那30%做调参决策,最终报出来的指标一定偏乐观。如果只是内部看个大概效果,问题不大;如果要对外声称模型精度,就必须有独立测试集。
我见过一个典型翻车案例:有人用7:3划分,反复调参后测试集准确率92%,上线后真实场景只有80%,差距非常大。核心原因就是模型已经在测试集上过拟合了,只是不自知。所以我的建议是:如果你的项目要交付、要写报告、要对比竞品,老老实实按70/20/10分;如果只是自己调试、做Demo、快速验证想法,7:3够用,但心里要清楚那个30%是“验证+测试二合一”,不是纯粹的测试集。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 比例怎么定:70/20/10 与 7:3 的适用边界
数据划分比例从来不是数学上算出来的最优解,而是根据数据量、任务类型、模型复杂度、算力成本综合权衡出来的经验值。但从技术角度,我们可以把决策逻辑讲清楚,让人知道什么情况下选什么比例。
2.1 数据量大小是决定性因素
数据量大(比如百万级、千万级)时,验证集和测试集的绝对数量更重要,比例反而次要。100万条数据,按99/0.5/0.5划分,测试集也有5000条,足够统计意义评估了。这时你可以把更多数据放进训练集,模型能学到更多规律。
数据量中等(万级到十万级),70/20/10是比较稳妥的选择。验证集和测试集各自有几千到一万条样本,既能保证评估置信度,又不至于让训练数据太少。
数据量小(几千条甚至几百条),固定比例划分就很危险了。假设总共1000条,按70/20/10分完,测试集只有100条,测试结果方差极大——换一个随机种子,准确率可能波动5个点以上。这种情况正确做法是用K折交叉验证,把每一折的结果平均,而不是简单切一刀。
2.2 任务类型的特殊要求
分类任务里,如果类别不平衡(比如正样本只占5%),划分时必须做分层抽样,保证训练集、验证集、测试集中正负样本比例和原始数据一致。否则很可能出现测试集里全是负样本的极端情况,模型评估直接失真。
时间序列数据(股票预测、销量预测、气象预测)有严格的时间顺序要求,绝对不能随机打乱划分。必须按时间点切分,比如前70%的时间段做训练,接下来20%做验证,最后10%做测试。这个限制条件下,70/20/10依然适用,但切分逻辑要从“随机抽”变成“按时间窗截取”。
目标检测类的视觉项目还有一个特殊问题:同一张图片里的多个目标框不能跨集合,也就是说一张图要么全进训练集,要么全进验证集或测试集。如果不小心把同一张图同时放进训练集和测试集,会造成数据泄露,模型评估虚高。这个细节我在实操中遇到过,后面问题排查部分详细说。
2.3 模型复杂度与调参频率的权衡
模型越复杂、超参数越多,需要用来调参的数据就越多,验证集占比应该适当提高。比如你用的是带大量超参数的GBDT或者深度网络,验证集占20%非常合理;如果你用的是逻辑回归这种参数很少的模型,验证集占10%可能也够用,可以省出更多数据给训练集。
另外要看你的调参频率。如果项目处于快速迭代期,每天要试几十组超参数,验证集太小会导致评估噪声大,无法判断参数A和参数B到底谁更好。这时候宁可牺牲一点训练数据,也要保证验证集的评估稳定性。我个人在深度学习中,如果数据量不太紧张,更倾向于验证集20%,因为深度网络对超参数(学习率、batch size、正则化系数)非常敏感,验证集大了才好判断趋势。
3. 实操:手把手完成一次合理的数据划分
理论说得再多,不如直接把代码思路和流程跑一遍。这一节我按不同场景给出可落地的操作方案,包括通用随机划分、分层划分、K折交叉验证、时间序列划分,并解释每一步为什么这样做。
3.1 通用场景:sklearn 一键划分
最常见的表格数据,直接用 train_test_split 做两次切分,就能得到训练、验证、测试三份数据。
python复制from sklearn.model_selection import train_test_split
import numpy as np
# 假设 X 是特征,y 是标签,一共 10000 条样本
X, y = np.random.rand(10000, 20), np.random.randint(0, 2, 10000)
# 第一步:分出测试集 10%,剩余 90%
X_train_val, X_test, y_train_val, y_test = train_test_split(
X, y, test_size=0.1, random_state=42, stratify=y
)
# 第二步:从剩余 90% 中分出验证集,使其占原始数据 20%
# 注意:这里 test_size 是相对于 X_train_val 的,90% * 0.222 ≈ 20%
X_train, X_val, y_train, y_val = train_test_split(
X_train_val, y_train_val, test_size=0.222, random_state=42, stratify=y_train_val
)
print(X_train.shape, X_val.shape, X_test.shape)
# 输出约等于:(7000, 20) (2000, 20) (1000, 20)
这里有个细节要解释。第二次切分的 test_size=0.222 不是随便写的,因为此时 X_train_val 已经占原始数据的90%,要让验证集占原始数据的20%,计算方式是 20% / 90% ≈ 22.2%。很多人第一次写这个代码,直接填 test_size=0.2,结果验证集只有 90% × 20% = 18%,看起来差不多,但严格来说不是预期的20%。数据量大时无所谓,数据量小时这2%的偏差可能会影响评估稳定性。
3.2 分层抽样:类别不平衡时的关键操作
上面的代码里我传了 stratify=y,这个参数就是在做分层抽样。当你的数据类别不平衡时,比如二分类中正样本只有5%,如果不分层,随机切分的极端情况下可能出现验证集或测试集里一个正样本都没有或者只有两三个,模型效果根本没法评估。
分层抽样的原理很简单:先统计整个数据集各类别的比例,然后在每个类别内部按比例随机抽取,保证划分后的数据保留原始分布。实操中,这个参数在 train_test_split 里一行搞定,但如果用的是自定义划分方式,就需要手动实现分层逻辑,或者借助 StratifiedKFold 做分层交叉验证。
python复制from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in skf.split(X, y):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
# 每一折训练集约80%,验证集约20%
# 且每一折中各类别比例与原始数据一致
这个方案的思路是:不单独切一个固定验证集,而是把训练数据切成5份,每次用4份训练、1份验证,轮流5次,最终取5次验证结果的平均值。这样每个样本都有机会参与验证,评估结果更可靠,特别适合数据量不大的场景。
3.3 时间序列数据:不能随机打乱
时间序列数据的划分是很多人容易踩坑的地方。如果你用 train_test_split 默认的随机打乱模式,把未来时间段的数据混进训练集,模型就等于作弊——它已经看过“未来”了,测试时自然表现很好,但真实预测未来时完全不是这么回事。
正确做法是按时间顺序切分:
python复制import pandas as pd
# 假设 df 是时间序列数据,按时间排序
df = df.sort_values('timestamp').reset_index(drop=True)
n = len(df)
train_end = int(n * 0.7)
val_end = int(n * 0.9)
train_df = df.iloc[:train_end]
val_df = df.iloc[train_end:val_end]
test_df = df.iloc[val_end:]
这里还有一个进阶注意点:时间序列模型的验证方式不能只是简单切一刀,因为相邻时间点的数据高度相关,模型可能记忆训练集末尾的模式,在验证集开头表现很好,但放到更远的未来就不行。实践中更稳妥的方案是使用滚动预测验证,比如训练集不断前移、验证集不断滚动,模拟真实的时序预测过程。
3.4 深度学习图像项目:目录结构划分法
图像分类、目标检测项目通常不用数组切分,而是按文件目录组织数据。这种情况下,我习惯先读取所有图片路径,用 train_test_split 切分路径列表,再按路径拷贝到不同目录,或者写一个数据加载器,在训练时根据路径索引决定样本属于哪个集合。
python复制import os
import shutil
from sklearn.model_selection import train_test_split
# 假设原始数据目录结构:data/class1/xxx.jpg, data/class2/xxx.jpg
all_images = []
for cls in os.listdir('data'):
cls_path = os.path.join('data', cls)
for img in os.listdir(cls_path):
all_images.append((cls, img))
train_files, temp_files = train_test_split(all_images, test_size=0.3, random_state=42, stratify=[c for c, _ in all_images])
val_files, test_files = train_test_split(temp_files, test_size=0.333, random_state=42, stratify=[c for c, _ in temp_files])
注意这里 temp_files 占原始数据的30%,要让验证集和测试集各占15%,那么 test_size=0.333 正好是 15% / 30%。和前面的计算方法一样,换汤不换药。
顺便提一句:在目标检测中,如果你用类似YOLO的工具训练,数据划分后还要检查有没有“同一张图的增强版本”同时出现在训练集和验证集里。比如你用随机翻转、色彩抖动做了数据增强,增强后的图片必须和原图保持同一个集合,否则验证集里会出现训练集的“近似复刻”,评估指标虚高。
4. 数据划分的实操心得与改进技巧
固定比例划分是基础方案,但实际项目中往往需要根据具体情况做一些改进。这里分享几个我在真实项目里用过的技巧,能有效提升模型评估的可靠性。
4.1 用 K 折交叉验证代替单次划分
如果数据量在几千到几万之间,我非常推荐在调参阶段用 K 折交叉验证,而不是单次划分出固定的验证集。原因很简单:单次划分的验证集结果存在随机性——换一个随机种子,指标可能上下浮动一两个点,如果你在调参时把这种噪声当成了参数效果差异,很容易做出错误决策。
K折交叉验证的思路是把训练数据分成K份(常用5或10),每次取1份作为验证集、剩余K-1份作为训练集,轮流训练K次,最终验证指标取K次的平均值和标准差。平均值反映模型的真实水平,标准差反映评估稳定性——标准差大说明模型对数据划分敏感,本身就不够稳健。
代价就是训练时间变成K倍。所以我的建议是:调参阶段用交叉验证,确定最终参数后,再用全部训练数据按70/20/10重新划分,训练出一个最终交付模型。
4.2 小数据集的“少切多验”策略
当数据量只有几百条时,固定划分70/20/10会产生两个问题:训练数据太少,模型学不到位;验证集和测试集太小,评估结果不靠谱。这种情况下我一般这样做:
- 用交叉验证评估模型,不单独留验证集。
- 测试集尽量控制在10%以内,保证测试集有一定绝对数量(至少50~100条)。
- 如果数据量实在太小(少于200条),甚至可以考虑把测试集单独留出来,训练和验证全部用交叉验证完成。
还有一个技巧是数据增强。图像类任务可以通过随机裁剪、旋转、色彩抖动等方式扩充训练集;文本类任务可以通过同义词替换、回译等方式扩充。但必须注意:增强数据的生成只能基于训练集,不能使用验证集和测试集的信息,否则会造成数据泄露。
4.3 数据划分的随机种子管理
写代码时 random_state=42 很常见,但实际项目中只设一个种子是不够的。我建议所有用到随机过程的环节,包括数据划分、模型初始化、数据加载器打乱顺序,都固定一个全局种子,或者做一个统一的种子管理模块。
为什么重要?假设你今天划分了数据开始训练,明天重启代码时如果不固定种子,数据划分会变,模型初始化也会变,训练结果自然不同。如果别人想复现你的实验,看到的结果却不一致,这会非常影响可复现性。深度学习训练过程本身就有随机性,你至少要保证数据划分是确定的,这样每次训练的差异才只来自模型初始化,排查问题更容易。
4.4 解决“测试集只有一次机会”的困境
前面反复强调测试集只能碰一次,但实际项目中经常遇到“测试集结果不理想,想回头调参”的情况。我的处理方式是:把测试集拆成两部分——公开测试集和个人测试集。公开测试集在开发过程中可以用,用于快速对比指标;个人测试集只在自己认为模型已经收敛的时候才用一次,作为最终评估。
这个策略在竞赛中很常见,实际上在业务项目里也很实用。你要对业务方负责,就不能拿同一个测试集翻来覆去地调模型,不然最后报给业务方的指标没有说服力。
5. 数据划分中的常见问题与排查思路
这一节我整理了自己和身边朋友在实际项目中遇到过的高频问题,每一个都给出具体的排查思路和解决方案。
5.1 训练集和测试集之间发生了数据泄露
现象:训练集准确率正常,验证集和测试集准确率却异常高,高到不真实。比如一个二分类任务分类准确率超过99%,但业务场景明显没那么简单。
常见原因有三个。第一,直接对全量数据做了预处理(比如标准化、归一化)再划分,导致测试集的信息早被“看”到了。正确做法是先划分训练集,用训练集计算出均值、方差等统计量,再应用到验证集和测试集。第二,同一份数据以不同形式重复出现,比如图片有了轻度压缩版本、缩放版本,被分到了不同集合。第三,特征中存在未来才有的信息和ID类字段,比如用户ID、订单号这种和标签隐含相关的特征。
排查方法很直接:把特征重要性排出来,看看排名靠前的特征是不是有明显问题;再对每一条测试样本去训练集里找最相似的样本,如果距离异常接近,要警惕泄露。
5.2 类别分布不一致:验证集和训练集差异巨大
现象:训练集里类别比例是7:3,到验证集里变成了9:1,模型验证结果忽高忽低,不稳定。
原因几乎都是没有做分层抽样。虽然分层不是百分百保证分布一致,但能极大程度减小波动。还有一种比较隐蔽的情况:原始数据里本身的类别分布就随时间变化,比如某个类别在早期样本中占比高,后期占比低,如果随机划分没打乱,训练集和验证集的类别分布就会有系统性差异。
解决方案就是前面讲的 stratify 参数,以及划分完成后做一个可视化检查——把训练集、验证集、测试集的标签分布各画一张条形图,直观对比是否一致。
5.3 数据量太小,划分后模型根本训练不出来
这是一种很无奈的情况:数据总共300条,按70/20/10切完,训练集只剩210条,模型极度容易过拟合。
解决思路不是调整比例,而是改变评估策略。首选K折交叉验证;其次用留一法(Leave-One-Out),即每次只留一条样本做验证,重复N次,适合几十条数据的极端情况;最后是考虑迁移学习和预训练模型,在别人训练好的基础上微调,而不是从零开始训练。
这里也顺带强调一下:数据量小时,简单的模型往往比复杂模型效果更好。这个反直觉的结论背后是偏差和方差的权衡——复杂模型在数据少时方差极大,泛化能力反而不如简单模型。
5.4 随机种子不同,结果差异巨大
现象:代码完全相同,只改了 random_state,最终模型准确率从88%掉到83%。
这种情况一般不是划分本身的问题,而是数据量太少、噪声太大导致的方差问题。但排查时先确认一件事:random_state 只影响数据划分,还是也影响了模型初始化?如果在框架代码里没固定全局种子,模型初始化本身就不同,结果差异会叠加。
从根上解决还是要扩大数据量,或者用交叉验证的报告平均值。另外,做实验对比时务必固定同一组随机种子,让所有对比方案的数据划分一致,这样比较才公平。
5.5 图像任务中数据增强导致验证集“混入”训练集变体
现象:验证集指标一路走高,超过业务预期,但上线后效果远不如验证集。
排查思路是:检查数据增强的实现方式。规范做法是数据增强只作用于训练集,验证集和测试集只用原始图。如果你用了 PyTorch 的 torchvision.transforms,通常在 Dataset 里同时定义了训练和验证的数据预处理逻辑,训练数据用了 RandomHorizontalFlip、RandomRotation 等增强项,验证数据只用 Resize 和 Normalize。
如果你不小心在验证集上用了增强,验证集里会出现大量和训练集“很像但又不完全一样”的图片,模型在验证时看到的其实是训练集的变体,评估指标自然虚高。
6. 我对数据划分比例的理解与建议
数据划分比例这件事,网上到处是“7:2:1”或者“8:1:1”的说法,但我做了这么多年项目后发现,这个比例只能作为起点,不能当成铁律。关键要看三个问题:数据量够不够大?评估结果稳不稳?测试集是不是真的独立?
我个人最常用的策略是这样:初步探索阶段,直接用7:3快速验证想法是否可行;进入正式模型开发和调参阶段,切换到70/20/10,保证验证集有足够的稳定性;数据量偏少时,直接用5折交叉验证替代固定的验证集;最终交付前,用全部训练数据重新训练模型,在固定测试集上评估一次,报告最终指标。
至于“验证和测试3,7”的说法——我觉得它反映的是很多人的真实做法:把精力放在训练数据上,测试集够用就好。这种思路在快速迭代阶段没有错,但如果项目要上线、要写技术报告、要对比模型优劣,一定要有一个独立的验证集,否则反复看测试集结果就像一个学生反复做同一张试卷,成绩再好也只是自我安慰。
最后再分享一个我在实战中养成的习惯:数据划分完成后,不要直接开始训练,先花10分钟检查一下划分结果。打印训练集、验证集、测试集的样本数量、类别分布,随机挑几张图片或者几条文本看看是不是真的来自原始数据。这些小检查看似简单,却能在第一时间拦截绝大多数数据划分的坑,省下后面好几个小时的排查时间。
