我数据集的 99% 去了哪里

我着手构建一个包含几百万条训练样本的数据集。当流水线跑完时,它交给我的只有其中的百分之几——一个小到看起来像打字错误的零头。没有任何东西崩溃。没有任何错误被触发。数据并非生成失败;它只是悄悄地把自己几乎全部丢弃了,然后报告成功。

那个小得不可能是对的数字

这类 bug 的奇怪之处在于,它不会主动声张。一条崩溃的流水线是吵闹的。一条悄无声息地产出远远过少结果的流水线,只是跑完、把一个文件交给你,然后等着你自己去发现。如果我不是恰好记得自己大致期望多少条样本,我可能就在这些幸存者上做了训练,而永远不会知道其余的已经不见了。

唯一的症状是一个差了将近两个数量级的计数。而一个计数出错,是世上最容易被一眼略过的症状之一,因为一个比预期更小的数字,仍然看起来像一个真实的数字。它不像一个错误。它感觉像一个结果。

“为什么这么少?“是一类真实存在的 bug

我们被训练去调试那些坏掉的东西。但对于调试那些只是单纯缺席的东西——本应存在却不存在的数据、本应被创建却从未被创建的样本——我们要差得多。一条没走的路没有堆栈跟踪。流水线完全照我吩咐的去做了;只不过事实证明,我吩咐它做的事,在途中丢弃了大部分输入。

数据去了哪里

原因近乎令人难堪地平庸。在这个过程的某处,我以很大的步幅跨越时间线——看一个时刻,然后跳过一段长长的间隔到下一个,然后再跳过。每一个被我跨过去的位置,都是一个从未被检视、从未被创建的潜在样本。我很早就设定了那个步幅,出于一个我已经记不起来的理由,然后就忘了它的存在。

所以这个数据集并没有被损坏,没有被过滤,也没有被清洗掉。它只是从未被完整地看过一遍。绝大多数可用的数据就坐在那个循环步幅之外一步的地方,无人触碰,而流水线从它们旁边大步走过,报告说它完成了。

静默的丢失才是危险的那一种

这是我反复回想的部分。一次吵闹的失败是一份礼物:它让你停下来,指向它自己,要求你的注意。一次静默的丢失则什么都不给你。它产出看起来合情合理的输出,毫无怨言地运行,然后悄悄地改变了下游一切所依赖的根基。你可以训练一个模型、评估它、并从中得出结论,而这一切全都建立在一个偷偷只剩你以为所拥有的极小、毫无代表性的一丝碎片的数据集之上。

危险不仅在于那个数字是错的。而在于那个数字是错的而系统对此还很满意——一切都是绿的,一切都在运行,一切都建立在一个从第一步就背叛了你的根基之上。

修复之道:检视一切,然后选择保留什么

修复的办法是停止跳过。我不再大步跨越时间线、只检视那些我恰好落脚的点,而是让这个过程依次考虑每一个位置——然后把关于保留什么的决定,做成一个明确、刻意的过滤,而不是一个采样步骤的意外副产品。我丢弃的数据,如今是有意丢弃的,出于一个我能说得出口的理由。我保留的数据,我保留它是因为我真的看过它。

数据集回到了接近它本应有的规模。没有任何巧妙的手段促成这件事。我只是停止了无意间把数据扔掉。

数据工作中最廉价的检查

这个教训简单得几乎让人觉得它算不上一个教训:把你期望的规模和你实际得到的规模做个对比。这是一个我严重未充分利用的廉价合理性检查,而它本可以在几秒钟内抓住这个问题。我以为自己拥有多少数据实际存在多少数据之间的巨大差距,理应被当作一个 bug 来对待——即便,尤其是在没有任何东西崩溃来告诉你的时候。

如今,我对任何数据集做的第一件事不是看它的内容。而是看它的计数,并追问那个数字是否哪怕称得上合理。我遇到过的最糟糕的数据问题里,很多都没有损坏数据。它们只是静默地产出了比我所相信的更少的数据,然后任由我在这个差额之上继续构建。

一条不动声色地丢掉你大部分数据的流水线并没有崩溃——它正完全按照所写的那样运行,却依然在做错误的事。这个 bug 从来不是机器里的故障。它在我所假设的与我肯花力气去核查的之间的那道缝隙里。

— 没有信号,没有收益,非投资建议。