JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

训练集、验证集、测试集:为什么要切分数据

为什么严肃的机器学习在训练开始之前,就先把数据分成三堆——以及为什么偷看了错误的那一堆,是最悄无声息的自欺方式。

一场你无法预习的考试

想象一个学生把去年那份试卷上的每一个答案都背了下来。再把那份完全相同的卷子递给他,他能拿满分。但考试的意义从来都不在那份旧卷子上——而是要弄清楚他是否真正理解了这门学科,能不能应对从没见过的新题目。一个机器学习模型面对的正是这种危险。它擅长在自己已经学过的数据上拿高分;真正难回答的问题是:它是否真的学到了能迁移到明天数据上的东西。

这种在从未见过的数据上也能表现良好的能力,叫做泛化,而一旦模型离开你的笔记本电脑,它就是唯一重要的东西。我们之所以要把一个数据集切分成几堆,正是为了手里始终握着一个对泛化能力的诚实估计。如果我们只在模型训练过的那些例子上去衡量它,我们什么有用的东西都学不到——我们衡量的不过是它的记忆力。切分,就是一种始终把一部分题目藏起来的办法,这样它在这些题目上的表现,才能公平地预演真实世界。

三堆数据,三种用途

标准做法——训练/验证/测试切分——把你的数据分成三堆,每一堆都有一份绝不允许它干第二次的工作。训练集是课本:模型直接学习它,通过梯度下降或它所采用的任何学习规则,调整自己的参数去拟合这些例子。这通常是最大的一堆,往往占 70%–80% 左右,因为学习材料越多,模型一般准备得越充分。

验证集是模拟考。你不在它上面训练——而是用它来对模型做各种选择:要多少层、学习率设多大、用多少随机失活、什么时候停。这些你手动拨动的旋钮就是超参数,而验证集正是你比较各种设置好坏的地方。因为你不断地拿它来检查、并朝着有效的方向去调整,所以验证集会悄悄地塑造模型——尽管模型从不直接在它上面训练。

测试集是真正的期末考,它有一条神圣的规则:你只在最后、在所有选择都已敲定之后,看它一次。它是你对「模型在从未见过的数据上将如何表现」的唯一一张诚实快照。提前去碰它——哪怕只是偷看一眼——它就不再是一场公平的考试了,因为你必然会不自觉地把自己的决定往它身上靠。典型的切分会把 10%–20% 留在这里,原封不动地锁在抽屉里,直到工作完成。

data = shuffle(all_examples)        # break any hidden ordering
train = data[0   : 70%]             # learn parameters here
val   = data[70% : 85%]             # choose hyperparameters here
test  = data[85% : 100%]            # touch only ONCE, at the end

model.fit(train)                    # train repeatedly
tune_until_happy(model, val)        # compare on val, never on test
final_score = model.eval(test)      # the one honest number
先打乱,再切成三堆。训练用来调参数,验证用来选超参数,测试集只读取一次。

为什么测试集必须锁起来

这里有一个连老手都会中招的微妙陷阱。假设你跳过验证集,直接拿测试集来调超参数——试五十种设置,留下在测试集上得分最高的那个。这感觉无伤大雅;毕竟你又没在它上面训练过。但当你挑出那个恰好在这些特定测试样本上获胜的设置时,你已经悄悄让测试集渗进了你的决策。它的分数现在偏乐观了,而你在真实世界里的表现,将会是一个令人失望的意外。

这是数据泄漏的一种形式:你本应去预测的那部分数据,其信息偷偷溜进了模型的构建过程。每瞄一眼测试集,都会花掉它一点诚实,而这份诚实不会再生。验证集存在的意义,恰恰就是用来承受这种磨损。你被允许「滥用」验证集——检查一千次、把你的选择过度贴合它——因为它本来就不该是最终定论。而保持纯净的测试集,正是在你不小心把决策过拟合到验证集上时,能把你抓个正着的那个。

切分远不止「切一刀」那么简单

人们很容易以为切分不过是把一个列表砍成三段,但一刀切得马虎,就会悄悄毒害下游的一切。如果你的数据本来就是排好序的——所有猫的照片在前,所有狗的照片在后——那么从最前面切出来当训练集,会让模型直到考试当天才第一次见到狗。这就是为什么要先打乱、再切分。而光是打乱,有时也并不够。

当某一类很稀少时——比如欺诈只占交易的 1%——一次普通的随机切分,可能让你的测试集里几乎一条欺诈都没有,从而让它的分数失去意义。解决办法是分层切分,它让每一堆都保持相同的类别比例;在严重的类别不平衡之下,这一点尤其重要。而对于活在时间里的数据——股价、传感器日志——你绝不能打乱,因为那会让模型用未来去预测过去。在那种情况下,你要按时间切分:用较旧的数据训练,用较新的数据测试。

还有一条规则,决定了整件事是否诚实:先切分,后清洗。如果你要算一个平均值来填补缺失值,或者用数据的均值和离散程度去缩放你的特征,那就只用训练集来算——然后把这同一套数字套用到验证集和测试集上。要是把所有数据放在一起算,你就已经让测试集把它的统计量悄悄说进了训练里。这还是同一种泄漏,只是换上了一身更体面的伪装。

当数据稀少时:交叉验证

三分切分有一个实打实的代价:它要花掉数据。如果你总共只有 300 个例子,切出 45 个当验证集,剩给你的就是一个摇摇晃晃的估计——换一批不同的 45 个,分数就能蹦跶好几个百分点,告诉你的更多是运气,而不是你的模型。在小数据集上,单独一个验证集的噪声实在太大,靠不住。

交叉验证是优雅的出路。最常见的形式 k 折,把训练数据切成 k 份等大的薄片——比如五份。你训练五次:每一轮,用四份教模型,留出的第五份给它打分。轮流让每一份都当一次评卷人,然后把五个分数平均起来。现在每个例子都既帮过训练、又帮过评估,只是从不在同一轮里——于是你得到一个稳定得多的估计,却没浪费任何数据。

交叉验证替代的是验证集,而不是测试集。那场诚实的期末考依然躺在它上锁的抽屉里:你所有的 k 折调参都在训练那部分上进行,唯有当每一个决定都尘埃落定,你才去读一次测试集。我们会在后面的指南里深入它的机制——分层折、留一法、在巨型模型上跑它的代价。眼下,先记住这个核心想法:数据充裕时,一次干净的三分切分就够了;数据稀少时,就轮转起来。

切分真正守护的是什么

剥去所有的操作细节,切分守护的只有一件事:你对自己的模型说真话的能力。如果一个排行榜上的数字,是在模型早已尝过的数据上测出来的,那它什么也不意味着。现实世界里那些最尴尬的 AI 翻车,往往并不离奇——不过是一个在实验室里看起来很棒的模型,因为某个人、在某个环节,让测试数据泄漏了,而那个闪闪发光的分数从一开始就不是真的。

所以,把这三堆数据当作一种纪律,而不是一道手续。在开始之前就定好怎么切,记清楚哪些例子归哪一堆,绝不让测试集通过清洗、调参或一次偷瞄而泄漏,并且只报告一次测试分数——哪怕它令人失望。正是这份诚实,把一个你敢部署的模型,和一个只是演示得好看的模型区分开来。认真切分这个并不光鲜的习惯,归根结底,正是让你在它之上构建的一切都值得信任的根基。