你报告的分数本身就是一个随机变量
你已经知道那条最高准则:绝不要用模型学习时用过的数据来衡量它,因为泛化——在*未见过*的数据上的表现——才是唯一重要的东西。于是你做一次留出法(holdout):从数据集中切出比如 20%,用剩下的训练,再在留出的那一片上评分。干净又简单。但本文要围绕的那个令人不安的真相是:那个单一数字并不是你模型「真正的」准确率,它只是从一个分布中抽出的一个样本。
想想里面掺进了多少随机性。*哪* 20% 落进了测试集,本身就是一次抛硬币。抽到容易的那一片,你的数字就很漂亮;抽到难的那一片,同一个模型看起来就差。当测试集很小时,这种运气能让报告出来的准确率上下摆动好几个百分点。所以当有人说「我的模型准确率 91%」,诚实的读法是「91% ± 某个值」——而那个 *某个值* 往往大到足以抹掉他们正在炫耀的全部优势。
k 折:让每个数据点都当一次测试点
k 折交叉验证(k-fold cross-validation)是个优雅的解法。把数据打乱,切成 *k* 等份,每一份叫一折(fold)——五折或十折是常见选择。然后训练 *k* 次。每一轮里,有一折坐在场外当测试集,其余 *k*−1 折拿去训练。每个数据点都恰好当一次测试点、当 *k*−1 次训练点。最后你得到的是 *k* 个分数,而不是一个。
fold: [ A ][ B ][ C ][ D ][ E ] (k = 5)
round1 TEST train train train train -> 0.88
round2 train TEST train train train -> 0.91
round3 train train TEST train train -> 0.85
round4 train train train TEST train -> 0.90
round5 train train train train TEST -> 0.89
mean = 0.886 std = 0.022会掉出两个数字,两个你都该在意。这 *k* 个分数的均值,是比任何单次留出都稳得多的泛化估计,因为走运和倒霉的划分会互相抵消平均。各折之间的标准差,则正是你长久以来缺失的那个度量——*你的结果到底抖动多大*。一个平均 0.886、波动 0.02 的模型,确实强过一个平均 0.89、但各折从 0.80 散到 0.97 的对手——后者是个赌徒,不是个稳定选手。
这有个实际代价:*k* 折意味着要训练 *k* 个模型,所以十折大约是单次拟合的 10 倍算力。对表格数据上的逻辑回归来说毫无压力,但对一个训练一次就要好几天的大型深度网络往往是无法承受的。这个权衡是真实存在的,也正因如此,*k* 的选择——乃至要不要做交叉验证——取决于你的模型拟合一次有多贵。
训练—测试差距:把两个数字放在一起读
交叉验证告诉你模型有多好。训练—测试差距(train-test gap)则告诉你*为什么*,以及该往哪个方向修。它就是你在训练数据上的分数和在留出数据上的分数之差。这一个差距,是整个模型评估里最具诊断价值的数字,因为它能干净地拆成你之前见过的两种经典失败模式。
*大*差距——训练上近乎完美、测试上平平——是过拟合的标志:模型背下了它在新数据上无法复现的噪声。*小*差距但*两个*分数都差,则是欠拟合:模型太简单,根本抓不住规律,所以哪里都一样失败。这就是偏差—方差权衡以你能从屏幕上直接读出的数字露出真容。关键在于,两者的疗法方向相反,所以读错差距会让你走向错误的一边。
三分法:别烧掉你的测试集
这里有个连老手都会中招的微妙陷阱。真实项目不会只训练一个模型——而是会试几十个:不同的超参数、特征、架构。如果你靠测试分数来挑赢家,那测试集就悄悄变成了你训练过程的一部分。你*在测试集上调了参*,它的数字现在是被乐观地高估的——它告诉你的是你猜得多准,而不是你将来泛化得多好。
解法是训练/验证/测试三分法。在训练集上训练。在验证集上调参、比较每一个候选。然后,在所有决定都锁定之后,*仅此一次*,你拆封测试集并读出分数。那个最终数字之所以诚实,*正是因为*你从未让它影响过任何选择。实践中,k 折往往扮演验证的角色(你在训练+验证数据上做交叉验证来挑设置),而一个单独的测试集则原封不动,留给最终报告。
这一切底下还埋着一颗雷:数据泄漏。如果你在划分*之前*,用*整个*数据集算出的统计量去标准化特征、填补缺失值、或做特征选择,那么测试行的信息就向后泄进了训练——你那套漂亮的交叉验证现在成了谎言。规则很机械:每一个预处理步骤都要在每一折*内部*、只在训练那部分上拟合,再应用到留出那部分。先做划分,在那之前别碰其他任何东西。
当数据不是独立同分布时,如何诚实地划分
朴素 k 折假设你的每一行可以互换——随便打乱,任何划分都一样好。但情况常常并非如此,而一次天真的打乱会悄悄泄露答案。时间序列是经典例子:打乱会让模型用周五的数据去预测周一,而它在生产中永远没机会这么干。改用向前滚动的划分——永远用过去训练、在未来上测试。诚实的分数会更低,而那个更低的数字才是真的。
分组数据是另一个大坑。如果你有 100 个病人、每人 10 张照片,按*照片*划分会让同一个病人同时出现在训练和测试里——模型认的是病人,不是病,分数纯属虚构。要改成按*组*(病人)划分。而当某一类很稀少时,要用分层(stratified)k 折,让每一折都保持相同的类别比例;否则某一折里可能几乎一个少数类样本都没有,它的分数就毫无意义了。
把它串起来:一份诚实清单
这一切都不是为了得到一个*更高*的数字,而是为了得到一个你能*信任*的数字——而信任意味着同时知道它的取值和它的抖动。一个没有波动范围、对着一个你懒得去超越的基线、在一个有泄漏的划分上评出来的结果,比没有结果还糟,因为它把虚假的信心带进了现实。诚实的评估,多半就是「先不要自欺」这一条纪律。
- 先划分,在任何预处理之前——当行不独立时按时间或按组划分;当某类稀少时做分层。
- 在训练+验证上用 k 折来估计表现,并报告各折的均值*和*标准差——绝不只给一个孤零零的数字。
- 看训练—测试差距来诊断过拟合还是欠拟合,并朝正确的方向去修。
- 把测试集封存好;在所有决定都最终敲定之后,只拆封一次,得出你真正要报告的那一个数字。