训练用数据
3 个论断,用 25 个你可以亲手驱动的图来证明。第一,数据集里的误差是 4 种不同的东西、对应 4 份不同的预算,只有一种能靠「多收一点」解决。第二,把测试集泄漏进训练的每一种方式,都是无声地、向上地出错 —— 这正是它们能通过评审的原因。第三,加权、重采样和挪阈值,是同一根杠杆的 3 个名字。
你手上真正有的那堆数据
模型知道的,就是它那些行知道的。后面 6 个 section 都在修补这一节没做好的地方。
数据集是一个样本:某处存在一个你真正关心的总体,而你手上的只是某个周二下午脚本抓到的那一部分。这里会坏掉 4 件事,只有第一件能靠「多收一点」解决 —— 拖动滑块增加你留下的行,看它们的均值走向总体的均值:
注意它稳定得有多快。6 行时估计值是 45.8 分钟,真值 39.2;60 行时是 38.1,最后那 180 行几乎没再挪动它。抽样噪声按 1 / √n 缩小 —— 这是唯一一种你能用钱买掉的误差。
选择偏差不会缩小,因为「能否触达」几乎从不独立于你要测的东西。拖动那道墙收窄抽样框,看它报出的均值怎样脱离真实的那一个:
因为重度用户正是抽样框最先丢掉的那批,把它砍到 120 个用户,估计值就从 39.2 分钟掉到 28.2。在这个框里多收 10 倍的行,买到的只是一个更精确、同样错误的数。这是唯一一种「数据越多越糟」的误差 —— 多出来的数据只让它显得更可信。
第三是覆盖度。生产流量有长尾,样本里只会有它碰巧撞上的类 —— 把滑块从 8 行拖到 行,看第 8 类一直停在零:
只有 8 行时,8 个类里有 5 个连一个样本都没有。最稀有的那一类只占流量的 0.72%,所以抽 128 行仍有 40% 的概率错过它;抽到 1,024 行,它也只有 10 个样本,而第 1 类有 709 个。只有 10 个样本的类,上线后一定会做错,而且没有指标会扣分。
第四件事与「你有哪些行」无关。两个标注者看同一行会给出不同答案 —— 把错误率拖到 ,看那些干脆就是错的标签给所有人将来能报出的分数封上一个顶:
翻转 240 个里的 26 个,一个对每个真实标签都判断正确的预测器,对照记录下来的标签也只能拿 89.2%。 Northcutt 等人发现,本领域被引用最多的 10 个基准,其测试集标签至少有 3.3% 是错的。4 种误差、4 份预算:行数、更好的抽样框、定向采集、更好的标注。只有第一种是数据量问题,而它偏偏是唯一有人肯出钱的。
清洗是一连串决定
数据行会以 4 种方式坏掉,而每一种修补都会改变你将来要引用的某个数字。
这些都不是「整理」:4 件事每一件都是关于数据含义的选择,由写加载脚本的人一次性做完,被之后所有实验继承。先看那个不知道自己见过某页面的爬虫 —— 拖动滑块,看副本怎样压过真正新的文档:
到 时,只有 98 篇是新的:142 篇是副本,最糟的那一篇出现了 23 次。每一份副本都会成倍放大它对梯度的拉力 —— 而正如 §04 会讲到的,一份同时落在切分两侧的副本,是伪造漂亮分数最省事的办法。
接着是窟窿。20 行、4 列,而收入最高的人恰恰是拒绝填写收入的那些人 —— 切换控件,看每一种修补对那一列窟窿做了什么:
注意均值一直没动:真实均值是 42.0,3 种修补都报出 34.9,因为缺失的那些行正是高值那些。真正动的是离散程度。用均值填补会保住全部 20 行,并报出 13.3 的标准差,而真值是 20.7 —— 凭空造出 6 行谁也没观测过的一致性。
离群值的问题正相反:不是缺席,而是影响力过大。最小二乘按一行离中心多远来分配它的投票权 —— 把带圈的那一行拖到图里任意位置,看直线跟着它走:
静止时那一行就落在直线上,拟合几乎察觉不到:带上它斜率 0.66,去掉它 0.67。把它拖到右下角,斜率掉到 0.42 —— 23 行里的 1 行挪动了整个模型,因为它在那里的杠杆值是 0.19,而平均一行只有 0.087。
第 4 种能躲过其他所有检查。4 个抓取来的字符串,本意是同一个词 —— 依次切到去空白、折叠大小写和 Unicode NFC,看4 串不同的字节塌缩成一串:
抓下来时它们是 6、5、6、5 个字节,第 3 个把 é 写成 e 加一个组合音标 —— 组合形式 3 个字节,预组合形式 2 个。在这一步之前跑过的每一个精确匹配去重器,都把1 篇文档数成了4 篇。顺序和步骤同样重要:先规范化再去重,先去重再切分,在填补之前先决定「缺失」意味着什么。
切成三份
3 份切片、3 项职责 —— 而你报出的那个数,精度只等于其中最小的那一份。
训练行是优化器能看到的;验证行是你用来在模型之间做选择的;测试行只在最后打开一次。是 3 份不是 2 份,因为只要有一次验证结果改动了超参数,那一份就已经开始泄漏。拖动留出的比例,看衡量你的那一份要花掉训练你的那一份多少行:
拖动时盯住右边那个数。10% 的测试切片 —— 1,000 行 —— 能把一个 90% 的模型测到 95% 置信度下的 ±1.86 个百分点。,区间就宽到 ±2.63。测试集不是浪费掉的余量,它就是你自己那块记分牌的分辨率。
这个区间不是走形式。这里是同一个模型,一字未改,在 12 个独立抽出的测试集上打分 —— 拖动规模,看这 12 次测量如何向它真正的准确率收拢:
一旦测试集变小,这个测量就不再有意义:50 行时同一个模型测出来可以是 83.1%,也可以是 96.7%,于是两个团队从这样的测试集上报出 84% 和 96%,交付的其实是一模一样的模型。推到 ,跨度是 88.9% 到 91.1% —— 仍然宽到足以吞掉大多数论文里报告的提升。
规模不是切片唯一会出错的地方。1,000 行里有 20 个正例,其中 100 行被抽去做测试 —— 拖动滑块换过 40 次抽样,看随机切片抓到的正例数怎样飘:
这 40 次里,它抓到的正例从 0 个到 6 个不等,还有 12% 的概率一个都没抓到 —— 那是一个无法给你唯一关心的类打分的测试集。分层切片每次都恰好抓到 2 个,因为它是按类构建的,而不是按行。
而且行与行之间不一定独立。40 位患者、每人 6 张片子:模型可以学会认出这位患者,而不是那种病 —— 把切分方式从「按行」换成「按患者」,看横跨那堵墙的组消失:
因为每位患者有 6 行数据,按行切分会让 40 位里的 31 位在墙的两边都有片子,于是 60 行测试数据全都有一个模型已经拟合过的同伴;按患者切分把这两个数清零,报出的分数也跟着掉。那次下跌本身就是结论。当一个小数据集的 10% 少到什么都测不出来时,k 折交叉验证用算力换分辨率:训练 k 次,每次留出不同的第 k 份,报告跨折的均值以及跨度。
把考卷提前给模型看的 4 种方式
泄漏从不抛异常。它只会让你的指标变好 —— 这正是它能通过评审的原因。
这里的每一种失败都只是一行代码放错了位置,而且每一种对你已经在跑的测试都是隐形的。先看每个 notebook 都有的那一种 —— 先拟合标准化器,再切分数据。拖动测试比例,比较在全部行上拟合出的均值和只在训练行上拟合出的均值:
注意这两条横线并不重合:全部数据上是 51.5,只在训练行上是 48.8。于是测试行比前者高 1.14 个标准差,比后者高 1.64 个,而那个见过测试行的标准化器已经把模型即将撞上的漂移抹平了。
第 2 种形状是一列伪装过的答案:流失模型里的 refund_issued、死亡率模型里的 discharge_ward —— 拖动这一列带走了多少标签,看两个类怎样被拉开:
在 下,验证 AUC 是 0.967,而诚实的特征只有 0.756 —— 线上拿到的正是 0.756,因为在需要预测的那一刻这一列还没被写入。请对照「必须做出预测的那一刻」去问每一列何时写入,凡答案为「之后」的一律删掉。
第 3 种是 §02 的重复数据迟到了:先切分再去重,副本就会落在两侧 —— 拖动模型已经读过的测试行,看你会报出的分数怎样脱离你真正能拿到的分数:
因为模型对背下来的行一定答对,60 行里只要有 18 行被污染,就把诚实的 81.0% 变成报出的 86.7%;污染一半测试集则变成 90.5%。整个运行过程看不出任何异常。这正是语言模型的基准污染:训练语料里含着基准题,于是基准不再测量任何东西。
第 4 种是时间。如果每行都有日期,随机切分就会用未来训练、拿过去测试 —— 沿时间线拖动那堵墙,比较按行切分和按日期切分:
随机切分时,180 行训练数据里有 176 行的日期晚于第一行测试数据:你把下周交给模型,让它去预测上周。按日期切分把这个数变成 0。这 4 种有一个共同的诊断信号 —— 一个远超预期的验证分数,来自一个你本来并不看好的改动。把它当成一份缺陷报告,而不是一项成果。
当你要找的东西很稀有
欺诈、疾病、故障、流失。有意思的那一类永远是小的那一类,而所有默认值都是为另一类调的。
不平衡是世界的属性,不是模型的属性,它会以 3 种不同方式弄坏指标、损失函数和阈值。一夜之间进来 240 笔交易 —— 把其中真正是欺诈的那些往「一批只有 1 笔」拖,看一个对什么都回答「正常」的模型能拿多少分:
盯住那个什么都不做的模型。240 笔里的 12 笔就是 5%,对它们一律答「正常」就能拿到 95.0%,同时什么也没找到。把它拖到 ,它拿 99.6%。在不平衡问题上,准确率就是多数类占比穿了一件「测量」的外衣。
于是模型给出一个分数,总得有人来划一条线。这里是欺诈拿到的分数,以及其余一切拿到的分数 —— 拖动这条线,看你抓到的欺诈怎样和纯属虚惊的告警互相换:
静止时这条线抓到一半的欺诈,它标出来的东西里有 54% 是真的。往左拖到召回 84%,精确率掉到 22% —— 在 5% 的流行度下,每 4.6 条告警才有 1 条是真的。你要哪一头是一道成本题:漏掉一笔欺诈,对分析师的一小时。
人们常报的那两条曲线,对这件事的重要程度并不一致。同一个分类器、同样两个分布,一条画成 ROC(召回对假正率),另一条画成精确率对召回:拖动流行度,看其中只有一条会动:
从平衡一路拖到 ,ROC 曲线纹丝不动:AUROC 一直是 0.921,因为它只由那两个分布决定。平均精确率却从 0.921 掉到 0.181。在稀有事件问题上一个漂亮的 AUROC,只是一个还没有人算过账的数。
标准做法是在损失里给稀有类加权。拖动权重,看决策边界实际上跑到了哪里:
对一个校准过的分数来说,给正类加权 w,与未加权模型在阈值 1/(1+w) 处做出的决策完全相同: 就是阈值 0.077,召回 78%、精确率 27%。好的工作点,却不是新信息 —— 而且概率的校准坏了,下游消费它的东西现在都错了。重采样到比例 r 是同一次移动外加重复行。加权、重采样和调阈值是同一根杠杆的 3 个名字,谁都变不出更多正例。
你没有采集过的行
数据增广是唯一一种不出门就能加行的办法。它建立在一个关于不变性的断言之上,也在那个断言失效处失效。
断言是:存在一组变换,在它们作用下标签不变。如果一个 6 被平移之后还是 6,那么每一个被平移过的 6 都是合法训练数据,你就可以批量制造。按播放或拖动轨道,走过流水线从采集来的那个样本造出的副本 ——每一个都是一行新的训练数据:
注意各种变换的代价有多不均匀。上下左右各平移一格,121 格里有 30 格变了,与原图的重叠掉到 0.52;而旋转 12 度只改了 12 格,重叠还有 0.80。这 5 个都被当作 6 交给优化器 —— 这就是那个不变性断言,用数据说出来。
每一个这样的断言都有边界,而这一个很容易越过。把旋转拖过四分之一圈,看副本还有多像 6与它已经多像 9交叉:
这两个数在 77° 处收拢并交换,到 时副本就是一个 9,却仍然挂着 6 的标签。中间还有一整段区间,它两个数字都不是。越过那个交叉点后,你生成的每一个副本都是一行「答案是错的」训练数据 —— 也就是 §01 的标签噪声,只不过是自己造的,而且按数据加载器的速度量产。
于是剩下那个没人大声问的问题:一个副本值多少?误差随行数按幂律下降,所以副本的价值就是它把等效行数推了多远 —— 先设定副本数量,再设定一个副本相对一行真实数据值多少:
横轴是对数轴,所以固定的倍数就是固定的一段右移,与行数无关 —— 这既是这笔交易值得做的理由,也是它终将耗尽的理由。、每个值四分之一行真实数据,就是 6.75 倍的数据量,把误差从 3.9% 带到 2.2%;把价值设成 0.05,同样的副本只值 2.15 倍、误差 3.1%。第二个数字没人能替你填:它是「变换把样本推了多远」除以「任务真实的变化把它推了多远」。 ImageNet-1k 有 1,281,167 张真实训练图像,裁剪加翻转值它的好几倍,仍然不值第二个 ImageNet。
速查
整个页面其实只在问一个「顺序」问题,而这里就是那个顺序。
从原始文件到训练好的模型之间有 8 个步骤,其中恰好一个是墙:墙以上的一切看得到所有行,墙以下的一切只看得到训练行 —— 把切分线在梯子上上下拖动,看出错的那些步骤怎样变化:
完全没有红色。墙以上只放那两个「必须看到全部数据才谈得上正确」的步骤 —— 读入和去重,因为切分之后才删掉的重复,早就横跨了切分。墙以下的一切都是拟合出来的。
落到代码上,业界最常见的泄漏就是一条语句放错了顺序,任何测试、任何类型系统都抓不到它:
# wrong - the scaler saw every row X = scaler.fit_transform(X) Xtr, Xte = split(X) # right - the split comes first Xtr, Xte = split(X) Xtr = scaler.fit_transform(Xtr) Xte = scaler.transform(Xte)
最后一笔预算 —— §03 的那个区间隐含着一个没人问的问题。拖动你想看见的提升幅度,读出它需要的测试行数,对照你手上的 1,000 行:
看见 1 个百分点的提升,每一边要 13,493 行测试数据 —— 是§03 那一份的 13 倍。每一种失败都无声地、向上地打破同一条不变式:打分那一刻,碰过测试集的东西不曾挪动任何参数。
- 分数忽然涨了,说不清为什么。先过 §04 的 4 种形状。
- 某个特征几乎和整个模型一样好。去问那一列何时写入。
- 测试准确率高过标签噪声的天花板。没人能超过自己的标注者。
- 在不平衡问题上报准确率。先要基准率。
- 报到小数点后一位的测试分数。1,000 行时区间是 ±1.9 点。