数据划分比例这件事,机器学习圈子里讨论得永远不够多。很多人拿到数据集,第一反应就是 train_test_split(test_size=0.2) 一把梭,然后就开始调模型。我见过不少项目,模型在验证集上漂亮得不行,一上测试集就露馅,最后排查半天,问题不是出在模型,而是出在最初那行划分代码上。
“训练集70%、验证集20%、测试集10%”是我见过最经典、也最常被拿来做默认配置的比例。但说实话,这个比例不是银弹,它背后有一整套关于“监督者如何不被收买”的逻辑。还有那句“我们验证和测试:3,7”,其实是在说一种更粗糙但很实用的划分习惯——先用三七开把训练和评估切出来,再在评估部分内部做文章。这篇文章我会把这两种思路都讲透,包括每一种划分背后的原因、适用的场景、以及你真正实操时会遇到的那些坑。
这篇内容适合刚入门深度学习、想搞明白为什么不能把所有数据都拿来训练的初学者,也适合已经跑过几个模型、但总觉得评估结果不可信的工程师。我会用带过项目的方式,把数据划分的原理、比例选择的逻辑、实操代码和常见翻车现场都过一遍。
1. 训练集、验证集、测试集,三者的职责边界
1.1 三个集合的本质区别
先说一个最基础但经常被混淆的概念:训练集、验证集、测试集不是“随便分成三份”,而是三个职责完全不同的角色。
训练集负责让模型学习参数,也就是“做题”。模型通过反向传播不断调整权重,目的是把训练集上的损失降下来。这里有一个容易忽略的点:训练集参与的是梯度计算,它直接改变了模型的参数。
验证集负责帮你做“模型选择”。你在训练过程中会尝试不同的超参数(学习率、网络层数、正则化系数等),验证集用来评估这些选择好不好。模型不直接在验证集上学习参数,但你会根据验证集的表现来调整训练策略。换句话说,验证集间接影响了模型的最终形态。
测试集是“最终考卷”。整个训练过程结束后,模型从未见过测试集,用它来评估模型的泛化能力。测试集的结果是你对外宣称模型性能的依据,也是你判断模型是否过拟合的最终标准。
我经常用一个学生备考的类比来解释:训练集是平时做的练习题,验证集是模拟考,测试集是高考。你平时刷题会记住答案,模拟考帮你调整复习策略,但只有高考成绩才是你真正拿出去说事的。如果你把高考题提前拿来当练习题做,那成绩还有意义吗?数据划分的核心逻辑,就是保证测试集的信息完全不泄露到训练过程中。
1.2 为什么不能用训练集评估模型
很多人刚开始跑模型时,喜欢看训练集上的准确率,发现已经99%了,就觉得自己模型无敌了。但这个数字基本没有参考价值,因为它衡量的是模型“死记硬背”的能力,而不是“理解应用”的能力。
深度学习模型的参数量通常远大于训练样本数,这意味着模型有足够的能力把训练集的特征“硬编码”进参数里。例如一个ResNet-50有超过2500万参数,如果只有1万张训练图片,模型完全可以“记住”每一张图,而不是学到通用的模式。这就是过拟合。
验证集和测试集的存在,就是为了检测这种“记忆力”是否过度。只有当模型在没见过的数据上表现好,才能说明它学到了可泛化的规律。这是机器学习的基本逻辑,也是数据划分这件事存在的根本意义。
1.3 “验证”和“测试”不是一回事
我在实际带项目时发现,很多工程师把验证集和测试集混在一起用。最常见的情况是,训练过程中反复用测试集验证效果,调了好几天超参,最后拿出来的“测试”结果其实是已经“看过”很多次的结果,泛化性能已经被严重高估。
验证集和测试集的核心区别在于使用频率和使用目的:
- 验证集:训练过程中反复使用,用来调超参、早停、模型选择。
- 测试集:最终只用一次,用来评估泛化性能。
如果你反复用测试集调参,那测试集实际上就变成了验证集,它的评估结果不再可信。这就是为什么划分比例时要同时保留验证集和测试集,而不是只留一个“评估集”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 70/20/10的由来,以及“3:7”的另一种理解
2.1 70/20/10为什么经典
70/20/10这个比例之所以流行,是因为它在三个目标之间取得了平衡:训练数据要足够多,验证数据要足够稳定,测试数据要足够可信。
训练数据如果太少,模型学不到足够的模式,容易欠拟合。验证数据如果太少,评估指标的方差会很大——你今天看到验证集准确率88%,明天重新随机划分就变成92%,你根本没法判断是调参有用还是运气好。测试数据如果太少,最终报告的指标就缺乏说服力,置信区间会非常宽。
这里可以做一个简单的统计估算。假设测试集有100个样本,模型真实准确率是90%,那么你测出来的准确率有大约95%的概率落在84%到96%之间(用二项分布近似计算,标准差约0.03)。如果测试集只有30个样本,这个区间就扩大到77%到97%。你拿着这样的结果去汇报,很难让人信服。
在总数据量适中的场景下(几千到几万条样本),70/20/10是一个稳妥的起点。训练集有足够数据学习,验证集和测试集都能保证几百个以上样本,统计上比较可靠。
2.2 大模型时代比例还要调整
但你可能会问:现在的深度学习动不动就百万级数据集,也用70/20/10吗?
答案是不一定。当数据量很大时(比如ImageNet有128万张图),验证集和测试集各留2万张,占比不到2%,但已经足够产生非常稳定的评估结果。这时候如果你还留20%做验证,意味着训练数据减少25万张,这对模型性能的负面影响远大于验证集稳定性带来的收益。
我个人的经验是:比例不是固定的,关键要看验证集和测试集的绝对数量。数据量越大,评估集占比可以越小。数据量小,反而要留更多比例,甚至要用交叉验证来弥补单次划分的随机性。
2.3 关于“验证和测试:3,7”的两种解读
标题里那句“我们验证和测试:3,7”,在实操中其实对应着两种常见做法,我说出来你肯定见过。
第一种做法:先把数据按70%训练、30%评估划分,这30%的评估集里,再按一定比例拆成验证集和测试集。如果30%里验证集和测试集按3:7拆,那最终就是训练集70%、验证集9%、测试集21%。这种做法的逻辑是先保证训练数据充足,再考虑评估部分的分配。我见过一些工业项目为了确保最终测试结果“足够有说服力”,会把测试集留得比验证集大,因为测试集只测一次,样本多点更稳。
第二种做法:训练集和验证集合计占70%,测试集占30%。这种做法常见于数据量较大的场景,验证集评估完模型后可以合并回训练集再训一轮,测试集独立留出。很多Kaggle玩家喜欢这种思路,因为比赛最后只看测试集成绩,他们在本地也要留足够大的测试集模拟最终评分。
你不需要纠结哪一种“正确”,关键是理解背后的取舍:验证集影响你调参的稳定性,测试集影响你最终评估的可信度。两者都要保证足够数量,但具体怎么分配,取决于你的数据总量和业务需求。
3. 实操中的数据划分方法与代码实现
3.1 最简单的随机划分
如果你拿到的是分布均匀的表格数据,比如用户行为日志、房屋价格预测这类,随机划分是最常规的操作。Sklearn的 train_test_split 是最常用的工具。
python复制from sklearn.model_selection import train_test_split
# 假设 X 是特征,y 是标签
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# 再把临时集拆成验证集和测试集
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=1/3, random_state=42, stratify=y_temp
)
注意上面这段代码里的 test_size=1/3:因为 X_temp 占总数据的30%,再取其中的1/3作为测试集,就得到总数据的10%,剩余20%就是验证集。这样正好实现70/20/10。
random_state 必须固定,否则每次运行划分结果不一样,实验结果无法复现。stratify=y 是做分层采样,保证划分后的训练集、验证集、测试集中的类别比例与原始数据一致。对于分类问题,这一步强烈建议加上。
3.2 为什么要设置随机种子,为什么不能每次都变
我见过不少团队,代码里完全没设置随机种子,每次训练结果都有一两个百分点的浮动,然后整个团队都在猜模型到底有没有改进。最后发现是数据划分每次都不一样。
固定 random_state 的意义在于,你比较的是“同一个测试集上的模型A vs 模型B”,而不是“不同测试集上的模型A vs 模型B”。前者能反映模型本身的差异,后者夹杂了数据划分的随机波动。
这里多说一句:如果你在做实验对比,光固定数据划分还不够,训练过程中的权重初始化、数据加载顺序、GPU算子随机性也都会影响结果。严谨的做法是把模型保存下来,多次重复实验报告均值和方差。但至少,从数据划分这一步开始固定,是实验可复现的底线。
3.3 类别不平衡时必须用分层采样
假设你做一个二分类任务,正样本只有5%,负样本95%。如果不做任何处理,随机划分时有一定的概率把某个小众类别都分到训练集里,验证集里一个正样本都看不到,模型评估直接失效。
stratify=y 就是解决这个问题。它会尽量保证每个集合中各类别比例与原始数据集一致。如果你做的是多标签分类、目标检测这种复杂任务,train_test_split 就不够用了,你需要按图片ID或者标注文件来分组划分,避免同一张图片的不同标注出现在训练集和验证集里。
对于目标检测这类任务,我通常先按图像ID分组,再对图像ID做分层划分,确保每个类别在训练集和验证集中的实例数比例尽量一致。这块处理起来比表格数据麻烦,但逻辑是一样的:划分的最小单位不是单条标注,而是“一张图”这个整体。
3.4 时间序列数据不能随机划分
如果你处理的是股票价格、传感器时序、天气预报这类数据,千万不要用随机划分。时间序列数据的核心假设是“未来数据不能泄露到过去”,如果你随机打乱再划分,等于用未来数据去预测过去,评估结果会非常乐观,但上线后立刻翻车。
正确做法是按时间顺序划分:
python复制# 假设 df 按时间升序排列
train_ratio = 0.7
val_ratio = 0.2
test_ratio = 0.1
train_size = int(len(df) * train_ratio)
val_size = int(len(df) * val_ratio)
train_df = df.iloc[:train_size]
val_df = df.iloc[train_size:train_size + val_size]
test_df = df.iloc[train_size + val_size:]
这就是最朴素的按时间切分。但注意,时序数据划分还涉及一个窗口问题:如果时间跨度很长,模型可能在旧数据上训练,而未来数据分布已经漂移。这时候你需要考虑的不只是比例,还有“用多长的历史窗口预测多长的未来”,这属于时序交叉验证的范畴,比普通划分复杂得多。
3.5 数据量少的时候,交叉验证比固定划分靠谱
当你的数据总量只有几百条时,70/20/10这种一次划分方式的“运气成分”会非常大。某次划分可能恰好把困难样本都放进测试集,结果模型得分很低;换一次随机种子,得分又变得很高。
这种情况下,K折交叉验证是更可靠的选择。把数据分成K份(通常5或10),每次取其中一份做验证,其余训练,循环K次,最后把K次的验证指标平均。这样做的好处是每条数据都有机会参与验证,评估结果更稳定。
python复制from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f'CV accuracy: {scores.mean():.4f} ± {scores.std():.4f}')
对于深度学习项目,K折交叉验证的成本比较高,因为要训练K次模型。但如果你的数据集只有几百到一两千张图、样本数不多,多训几次也值得,总比最后发现划分不合理要好。
4. 划分时的常见翻车点与避坑技巧
4.1 数据泄露:划分之后再归一化
这是新手最容易踩的坑——先对全部数据做标准化、归一化,再划分训练集和测试集。看起来很自然,但这是个严重错误。
原因很简单:归一化需要用训练集的均值和标准差。如果你用全部数据的统计量来归一化,测试集的信息已经通过均值和标准差“泄露”到了训练过程中。当测试集和训练集分布有差异时,这种泄露会导致测试指标虚高。
正确做法是:
python复制from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 只在训练集上fit
X_val_scaled = scaler.transform(X_val) # 验证集和测试集只transform
X_test_scaled = scaler.transform(X_test)
这个原则同样适用于特征选择、缺失值填充等所有需要“从数据中学习统计量”的操作。所有从数据集中学到的东西,都必须在训练集内部完成,然后应用到验证集和测试集。
4.2 数据增强不能作用在验证集和测试集上
做图像分类的朋友,训练时一般都会做随机翻转、随机裁剪、颜色抖动这些数据增强。这是为了提升模型泛化能力,没问题。但我见过有人把数据增强的预处理流程直接套用到整个数据集,连着验证集和测试集一起翻转裁剪了。
数据增强的本质是制造“新的训练样本”,它天然只应该出现在训练阶段。验证集和测试集应该保持原始数据,才能真实反映模型在真实场景下的表现。如果你把测试集也做数据增强,那你测出来的是模型对“增强后的图像”的识别能力,而不是对真实图像的识别能力。
4.3 验证集分布与线上分布不一致
这是工业场景里最常见的问题,比前面的坑都隐蔽得多。你在离线数据上70/20/10划分,模型验证集AUC 0.95,上线之后AUC只有0.75。排查来排查去,往往是验证集的分布和线上真实分布不一致。
比如你在做电商推荐模型,训练数据来自过去三个月,验证集从这三个月里随机抽取10%。但线上更新后,用户行为、商品池都发生了变化,验证集代表的是“历史分布”,而线上是“未来分布”。这就是为什么时序任务里我总是强调按时间切分验证集,因为你真正关心的是模型在未来数据上的表现。
更极端的情况是,你的训练数据里混入了多种来源的数据(比如多个城市、多个设备类型),随机划分会导致每个集合里都包含部分各种来源,模型没学到“跨来源泛化”,而是学会了“来源特征”。这时候建议按来源分组划分,或者至少检查一下每个集合中来源分布是否一致。
4.4 数据量极小时的特殊处理策略
如果数据量只有几十条样本,无论怎么划分,验证集和测试集都很难产生可靠的评估结果。这时候有几个备选策略:
第一,留一法交叉验证(LOO-CV),每次只留1条做验证,其余全部训练。这个策略计算成本高,但数据利用率最大,适合极小数据集。
第二,干脆不做验证集,通过交叉验证选好超参数后,用全部数据训练最终模型,然后单独留出的测试集只做一次最终评估。这种方式要求你必须禁得住诱惑,不能在评估之后又回头调模型。
第三,如果连测试集都不想留,可以用交叉验证的平均指标作为模型预期性能的估计。这在学术论文的benchmark里很常见,但在工业项目里我仍然建议至少留一个独立的测试集,因为业务方需要看到一个“没有参与过任何调参”的数据上的表现。
4.5 划分后检查分布是否合理
很多人在划分完成后直接开始训练,从不检查划分结果是否合理。我建议每个人都在划分后做一次简单但必要的检查。
第一步,检查各集合中的类别分布。打印出训练集、验证集、测试集中每个类别的样本数,确认与原始分布基本一致。
第二步,检查每个集合的特征分布。对数值特征,计算均值、标准差、分位数,确认没有明显偏移。对文本特征,查看高频词分布。
第三步,对于视觉任务,可视化一些样本,确认划分后的图片没有明显问题,比如某些重复图片出现在训练集和验证集中。
这些检查不需要很复杂,但能提前发现很多问题。尤其是“重复样本出现在不同集合中”这种情况,如果你做的是图片去重、文本相似度匹配之类的任务,非常容易出现,而一旦出现,测试指标会虚高到让你误以为模型已经SOTA了。
5. 不同场景下的划分比例调整建议
5.1 表格数据:70/20/10的经典适配
表格数据通常是数量和特征都比较可控的场景,70/20/10是很好的起点。如果数据量超过10万条,验证集和测试集各留5%可能就足够了,剩余90%做训练。如果数据量只有几千条,我建议用交叉验证替代固定划分,或者把比例调整为60/20/20,确保验证集和测试集都有足够的样本量。
另外表格数据里经常有很强的类别不平衡,分层采样基本是必须的。如果类别数量特别多(比如几千类),可以尝试按类别做分组划分,保证稀有类别在各集合中都有出现。
5.2 图像分类/目标检测:按图划分,按类分层
图像任务的主要风险是数据泄露,尤其是目标检测,一张图里可能有好几个物体标注。如果你按标注框而不是按图片划分,同一个物体的不同框可能同时出现在训练集和验证集里,导致验证指标虚高。
正确的做法是先拿到所有图片的ID列表,根据图片ID做划分,然后再按图片ID取对应的标注数据。我自己的习惯是写一个简单的自定义函数来做这件事,因为sklearn的train_test_split只能按整行划分,对图片标注格式支持不好。
分层方面,如果类别分布很不均匀,尽量保证每个集合中各类别实例数比例接近。对于长尾分布的数据集,有时候还需要人为提高稀有类别在验证集和测试集中的权重,否则验证集里稀有类别的样本只有几个,评估结果方差巨大。
5.3 自然语言处理:注意文档级别的划分问题
NLP任务里一个特殊的坑是文本数据可能包含大量重复或高度相似的文本。比如新闻数据,同一个事件可能会有多篇相似报道。如果你用句子级别的随机划分,相似的句子可能跨集合存在,导致“数据泄露”。
这种情况下,应该按文档、段落或者内容会话进行分组划分,先聚类或去重,再划分。最常见的场景是训练对话模型,同一个用户的多轮对话必须被划分到同一个集合,否则模型会看到“同一对话的上下文”分散在训练集和测试集里。
文本分类任务里,我还建议检查字符串完全重复的样本。用drop_duplicates去掉完全重复的样本,可以避免无效的数据泄露。
5.4 迁移学习/预训练模型微调:划分逻辑不同
如果你在做BERT微调或者用预训练模型做迁移学习,数据划分的逻辑会有一点变化。很多人会先把预训练模型的权重拿来,然后在自己的任务数据上做微调。这时候,70/20/10的划分原则依然适用,但要注意一点:验证集不仅用来调参,还用来判断“什么时候早停”。
微调阶段模型很容易过拟合,尤其是学习率设置不合理时,训练损失下降很快,验证集损失却在上升。早停策略依赖验证集,因此验证集的稳定性格外重要。如果你的验证集不够大,早停的时机判断就会不准确,模型可能提前停止或过度训练。
这种情况下,我建议把验证集比例适当放大到20%-25%,优先保证验证集评估的稳定性,训练数据减少一点通常影响不大,因为你有预训练权重作为基础。
6. 关于划分比例,最后帮你把思路捋清楚
关于“训练集70%、验证集20%、测试集10%”以及“3:7”这两种说法,我在项目里被问过很多次。每次我的回答都是:数字本身没有那么重要,重要的是你认清楚了三个集合各自承担什么职责。
70/20/10是一个在大多数场景下都不会出大错的起点。它保证了训练数据充分、验证集有足够样本做模型选择、测试集有足够的统计置信度作为最终评估。在数据量适中的项目中,这个配置既省心又可靠。
“训练30%、验证和测试这部分再按3:7”这种思路,本质上是在调整验证集和测试集之间的权衡。验证集大一些,调参更稳定;测试集大一些,最终评估更有说服力。很多时候我甚至会根据当前阶段的侧重点动态调整:模型探索阶段关注验证集稳定性,模型定稿阶段关注测试集规模。
我个人在实际操作中的体会是,数据划分这个环节一定不要怕麻烦,也不要觉得它“不涉及模型能力”就草草了事。数据划分决定了你评估结果的可信度,而可信的评估才是你做一切模型优化决策的前提。如果你之前从没在意过随机种子、分层采样、按时间切分这些细节,这篇文章看到这里,建议你回去检查一下自己的项目,花十分钟重新划分一遍数据,可能比你在模型调参上忙活一周带来的提升都大。
最后再分享一个小技巧:每次划分完数据后,把划分的索引或者ID保存下来,存成独立的文件。这样以后无论谁复跑实验、做对比,用的都是同一份划分,结果才具备可比性。这个习惯我保持了几年,每次团队协作复盘实验时,都省下了大量不必要的沟通成本。
