Three Ways Your Training Data Lies to You (And None of Them Throw an Error)
Three Ways Your Training Data Lies to You (And None of Them Throw an Error)
训练数据欺骗你的三种方式(且都不会报错)
Every failure I am about to describe produced a clean run. No exception, no stack trace, no red build. Each one produced a plausible number that I believed for longer than I should have. That is the category of bug I have come to fear most. A crash tells you it crashed. A silently broken dataset tells you nothing at all, and your metrics will politely agree with it. Here are three from the last year, all from my own work, all found late. 我即将描述的每一个失败案例,程序运行起来都非常“干净”。没有异常,没有堆栈跟踪,构建过程也没有报错。每一个案例都产生了一个看似合理的数据,让我信以为真了比应有时间更久。这是我最害怕的一类 Bug。程序崩溃会告诉你它崩溃了,但一个静默损坏的数据集什么都不会告诉你,而你的指标还会礼貌地予以附和。以下是我去年遇到的三个案例,全部来自我自己的工作,且都发现得很晚。
1. The dataset that was 92% one category
1. 92% 为单一类别的数据集
I had a training set of 688 records for a multi-category vision-language task. Thirteen categories. Reasonable size for a fine-tune, already used in a completed training run whose results I had written up. While preparing a stratified split, I joined the records back against the source annotations and actually counted the categories. 630 of 688 were a single category: scene captions. Zero examples of traffic signals. Zero of planning. Zero of uncertainty. Several categories the evaluation explicitly measured had no representation in training at all. 我有一个用于多类别视觉语言任务的训练集,包含 688 条记录,共 13 个类别。对于微调来说,这个规模是合理的,而且我已经用它完成了一次训练并写好了结果报告。在准备分层拆分(stratified split)时,我将记录与原始标注重新关联,并真正统计了各个类别。结果发现 688 条记录中有 630 条属于同一个类别:场景描述(scene captions)。交通信号灯的例子为零,规划的例子为零,不确定性的例子也为零。评估中明确衡量的几个类别在训练集中根本不存在。
The previous fine-tune had shown gains on some of those very categories. I had interpreted this as the model learning the task. The real explanation was duller and more useful: the model had learned the answer format from caption supervision, and format alignment alone was enough to move a multiple-choice score. Nothing category-specific had been learned, because nothing category-specific had been shown. The root cause was upstream and boring. The conversion script I inherited only rewrote file paths and dropped records with missing frames. It faithfully preserved a caption-only selection made further up the chain. It had no opinion about balance because nobody had asked it to have one. 之前的微调在其中一些类别上确实显示出了提升。我曾将其解读为模型学会了任务。但真正的解释更枯燥也更有用:模型只是通过描述性监督学会了答案格式,仅凭格式对齐就足以提高多项选择题的分数。模型并没有学到任何特定类别的知识,因为它根本没见过这些类别。根本原因在于上游,且非常无聊。我继承的转换脚本只是重写了文件路径并丢弃了缺失帧的记录,它忠实地保留了上游链条中筛选出的“仅描述”数据。它对数据平衡没有任何判断,因为没人要求它这样做。
What I changed: the composition of a training set is now an artifact I generate and inspect before any run, not a property I assume. A category histogram takes seconds. I had not looked, for months. 我的改进:现在,训练集的构成是我在运行前必须生成并检查的产物,而不是我预设的属性。生成一个类别直方图只需几秒钟,而我却几个月没看一眼。
2. The 18-hour run that converged perfectly to nothing
2. 完美收敛但毫无意义的 18 小时训练
Large model, QLoRA, multi-GPU, 8 camera views per sample. Roughly a full day of compute. The loss curve was beautiful. It fell from 19.4 to 15.7 over the first 26 steps, then to 0.078 by step 51, then flattened near 0.02 and stayed there. Token accuracy reached 0.99. Gradient norms decayed smoothly. Nothing in the training telemetry looked wrong. The task metric was 0.10. 大模型,QLoRA,多 GPU,每个样本 8 个摄像机视角。大约一整天的计算量。损失曲线非常漂亮:前 26 步从 19.4 降至 15.7,到第 51 步降至 0.078,然后平稳在 0.02 左右。Token 准确率达到了 0.99。梯度范数平滑衰减。训练遥测数据中没有任何异常。但任务指标只有 0.10。
The cause: I was supervising on free-text answers, median 11 words, phrased like “One should keep to the right side of the road and drive slowly.” The evaluation was four-option multiple choice, scored on the index of the chosen option. The model learned, quickly and correctly, to reproduce the style of the training answers. That is genuinely what the loss was asking for, and it maxed it out in about 50 steps. It was never asked to select an option, so it never learned to. Training and evaluation were measuring different tasks. Both were internally consistent. Neither could detect the other’s disagreement. 原因在于:我监督的是自由文本答案(中位数 11 个词),措辞类似于“应该靠右行驶并减速”。而评估是四选一的多项选择题,根据所选选项的索引评分。模型快速且正确地学会了复现训练答案的风格。这确实是损失函数所要求的,它在约 50 步内就达到了极限。模型从未被要求选择选项,所以它从未学会这样做。训练和评估衡量的是不同的任务。两者内部逻辑自洽,但谁也无法检测到对方的不一致。
What I changed: before a long run starts, I now write down what the eval measures and what the loss optimizes, in one line each, and check they describe the same thing. When loss collapses to near zero within a few dozen steps, I treat that as an alarm rather than a success. Genuine learning on a hard task does not look like that. 我的改进:在长任务开始前,我会写下评估衡量什么、损失函数优化什么(各一行),并检查它们描述的是否为同一件事。当损失在几十步内坍缩到接近零时,我将其视为警报而非成功。在困难任务上的真正学习过程绝不是这样的。
3. The dataset that was 40% smaller than its file size claimed
3. 比文件大小声称的小 40% 的数据集
Assessing a large public robotics corpus for adoption. The headline: 3.91 TB compressed, 4.65 TB extracted, hundreds of shards, dozens of buildings, thousands of hours of teleoperated manipulation. The number that mattered was buried in the paper’s method section. Only about 60% of the raw data converted into reliable 3D flow annotations. The rest failed depth estimation, camera pose optimization, or point tracking. Then a further filter kept only trajectories with actual robot-object contact and real object motion. Nominal size: thousands of hours. Training-ready size after both filters: roughly 500 hours. 评估一个大型公共机器人语料库以供采用。标题数据:压缩后 3.91 TB,解压后 4.65 TB,数百个分片,数十栋建筑,数千小时的远程操作数据。但真正重要的数字埋在论文的方法部分:只有约 60% 的原始数据转换成了可靠的 3D 流标注。其余部分在深度估计、相机姿态优化或点跟踪环节失败了。随后,进一步的过滤只保留了包含实际机器人-物体接触和真实物体运动的轨迹。名义规模:数千小时。经过两次过滤后,可用于训练的规模:约 500 小时。
Every plan built on the first number was wrong by a factor of several. Storage estimates, download time, compute budget, and most importantly the question of whether the corpus was even large enough for what we wanted. What I changed: I now treat yield as the primary dataset metric, not size. What fraction of raw capture survives every stage into training-ready data? For collected data this is a hard operational KPI, and it is the one that determines cost per usable hour. Bytes on disk is a storage figure. It tells you almost nothing about what you can train on. 基于第一个数字制定的所有计划都出现了数倍的偏差。存储估算、下载时间、计算预算,以及最重要的问题——该语料库是否足够大以满足我们的需求。我的改进:现在我将“产出率”(yield)视为主要数据集指标,而非规模。原始采集数据中有多少比例能通过每一阶段转化为可训练数据?对于采集数据而言,这是一个硬性的运营 KPI,它决定了每可用小时的成本。磁盘上的字节数只是一个存储指标,它几乎无法告诉你你能用它训练出什么。
The pattern
模式
All three share a shape. A pipeline stage did exactly what it was written to do. A metric moved in the direction that metric moves when things go well. And the thing I actually cared about was not being measured by anything at all. 这三个案例都有一个共同点:流水线的某个阶段完全按照编写逻辑执行了;指标朝着“一切正常”的方向变动了;而我真正关心的东西却完全没有被衡量。
The defenses that work for me: 对我有效的防御措施:
-
Check that work happened, not just that output exists. A gate that verifies “400 rows were produced” while never checking “any row succeeded” will pass a completely dead run. I once had an evaluation return zero correct on every row for 45 minutes because a kernel was unavailable on that GPU. Zero is a valid ratio. Row counts agreed. The gate passed and the sweep moved on. 检查工作是否真正完成,而不仅仅是检查输出是否存在。 如果一个关卡只验证“生成了 400 行”而不检查“是否有行成功”,那么完全死掉的运行也会通过。我曾遇到过评估在 45 分钟内每一行都返回零正确,因为该 GPU 上内核不可用。零是一个有效的比率,行数也对得上,关卡通过了,扫描任务继续进行。
-
Count infrastructure failures separately, and require zero. A weak model produces well-formed output and scores badly. A broken pipeline produces nothing and scores identically. Collapsing those two into one number destroys the only signal that distinguishes them. 将基础设施故障单独统计,并要求零故障。 弱模型会产生格式正确的输出但得分很低;损坏的流水线什么都不产生,得分也一样低。将这两者合并为一个数字,会摧毁区分它们的唯一信号。
-
Emit progress with errors first. Anything running longer than a minute should print failure count before position before metric. Silence is not neutral. A job working perfectly and a job failing on every single item look exactly the same from outside when neither prints anything. 优先输出错误进度。 任何运行超过一分钟的任务,都应在打印位置和指标之前先打印失败计数。沉默并非中立。当一个完美运行的任务和一个在每一项上都失败的任务都不打印任何信息时,从外部看它们完全一样。
-
Validate splits as their own gated job. I run split validation as a separate step that must pass before training is allowed to start. It checks class balance, domain distribution across sites, and leakage between splits. It has caught things that… 将拆分验证作为独立的关卡任务。 我将拆分验证作为一个单独的步骤,必须在允许开始训练之前通过。它检查类别平衡、跨站点的领域分布以及拆分之间的泄露。它已经捕捉到了……