训练用数据

3 个论断,用 25 个你可以亲手驱动的图来证明。第一,数据集里的误差是 4 种不同的东西、对应 4 份不同的预算,只有一种能靠「多收一点」解决。第二,把测试集泄漏进训练的每一种方式,都是无声地、向上地出错 —— 这正是它们能通过评审的原因。第三,加权、重采样和挪阈值,是同一根杠杆的 3 个名字。

01

你手上真正有的那堆数据

模型知道的,就是它那些行知道的。后面 6 个 section 都在修补这一节没做好的地方。

数据集是一个样本:某处存在一个你真正关心的总体,而你手上的只是某个周二下午脚本抓到的那一部分。这里会坏掉 4 件事,只有第一件能靠「多收一点」解决 —— 拖动滑块增加你留下的行,看它们的均值走向总体的均值:

已采集 6 行,样本均值 45.8 分钟

注意它稳定得有多快。6 行时估计值是 45.8 分钟,真值 39.2;60 行时是 38.1,最后那 180 行几乎没再挪动它。抽样噪声按 1 / √n 缩小 —— 这是唯一一种你能用钱买掉的误差。

选择偏差不会缩小,因为「能否触达」几乎从不独立于你要测的东西。拖动那道墙收窄抽样框,看它报出的均值怎样脱离真实的那一个:

抽样框触达 240 个用户中的 240 个。拖动可移动抽样框的边界;方向键每次移动一列,Home 键复位
抽样框触达 240 个用户,其均值偏离 0.0 分钟

因为重度用户正是抽样框最先丢掉的那批,把它砍到 120 个用户,估计值就从 39.2 分钟掉到 28.2。在这个框里多收 10 倍的行,买到的只是一个更精确、同样错误的数。这是唯一一种「数据越多越糟」的误差 —— 多出来的数据只让它显得更可信。

第三是覆盖度。生产流量有长尾,样本里只会有它碰巧撞上的类 —— 把滑块从 8 行拖到 行,看第 8 类一直停在零:

抽取 8 行,最稀有的一类出现 0 次

只有 8 行时,8 个类里有 5 个连一个样本都没有。最稀有的那一类只占流量的 0.72%,所以抽 128 行仍有 40% 的概率错过它;抽到 1,024 行,它也只有 10 个样本,而第 1 类有 709 个。只有 10 个样本的类,上线后一定会做错,而且没有指标会扣分。

第四件事与「你有哪些行」无关。两个标注者看同一行会给出不同答案 —— 把错误率拖到 ,看那些干脆就是错的标签给所有人将来能报出的分数封上一个顶:

0 个标签是错的,因此没有模型能测出高于 100.0% 的分数

翻转 240 个里的 26 个,一个对每个真实标签都判断正确的预测器,对照记录下来的标签也只能拿 89.2%。 Northcutt 等人发现,本领域被引用最多的 10 个基准,其测试集标签至少有 3.3% 是错的。4 种误差、4 份预算:行数、更好的抽样框、定向采集、更好的标注。只有第一种是数据量问题,而它偏偏是唯一有人肯出钱的。

02

清洗是一连串决定

数据行会以 4 种方式坏掉,而每一种修补都会改变你将来要引用的某个数字。

这些都不是「整理」:4 件事每一件都是关于数据含义的选择,由写加载脚本的人一次性做完,被之后所有实验继承。先看那个不知道自己见过某页面的爬虫 —— 拖动滑块,看副本怎样压过真正新的文档:

抓取 24 篇文档,其中 17 篇是唯一的

到 时,只有 98 篇是新的:142 篇是副本,最糟的那一篇出现了 23 次。每一份副本都会成倍放大它对梯度的拉力 —— 而正如 §04 会讲到的,一份同时落在切分两侧的副本,是伪造漂亮分数最省事的办法。

接着是窟窿。20 行、4 列,而收入最高的人恰恰是拒绝填写收入的那些人 —— 切换控件,看每一种修补对那一列窟窿做了什么:

修补后的列报出的均值是 34.9

注意均值一直没动:真实均值是 42.0,3 种修补都报出 34.9,因为缺失的那些行正是高值那些。真正动的是离散程度。用均值填补会保住全部 20 行,并报出 13.3 的标准差,而真值是 20.7 —— 凭空造出 6 行谁也没观测过的一致性。

离群值的问题正相反:不是缺席,而是影响力过大。最小二乘按一行离中心多远来分配它的投票权 —— 把带圈的那一行拖到图里任意位置,看直线跟着它走:

把带圈的那一行拖到图里任意位置;方向键可移动它,Home 键让它回到直线上
拟合斜率是 0.66

静止时那一行就落在直线上,拟合几乎察觉不到:带上它斜率 0.66,去掉它 0.67。把它拖到右下角,斜率掉到 0.42 —— 23 行里的 1 行挪动了整个模型,因为它在那里的杠杆值是 0.19,而平均一行只有 0.087。

第 4 种能躲过其他所有检查。4 个抓取来的字符串,本意是同一个词 —— 依次切到去空白、折叠大小写和 Unicode NFC,看4 串不同的字节塌缩成一串:

规范化之后还剩 4 个不同的字符串

抓下来时它们是 6、5、6、5 个字节,第 3 个把 é 写成 e 加一个组合音标 —— 组合形式 3 个字节,预组合形式 2 个。在这一步之前跑过的每一个精确匹配去重器,都把1 篇文档数成了4 篇。顺序和步骤同样重要:先规范化再去重,先去重再切分,在填补之前先决定「缺失」意味着什么。

03

切成三份

3 份切片、3 项职责 —— 而你报出的那个数,精度只等于其中最小的那一份。

训练行是优化器能看到的;验证行是你用来在模型之间做选择的;测试行只在最后打开一次。是 3 份不是 2 份,因为只要有一次验证结果改动了超参数,那一份就已经开始泄漏。拖动留出的比例,看衡量你的那一份要花掉训练你的那一份多少行:

1,000 行测试数据,把分数测到正负 1.86 个百分点

拖动时盯住右边那个数。10% 的测试切片 —— 1,000 行 —— 能把一个 90% 的模型测到 95% 置信度下的 ±1.86 个百分点。,区间就宽到 ±2.63。测试集不是浪费掉的余量,它就是你自己那块记分牌的分辨率。

这个区间不是走形式。这里是同一个模型,一字未改,在 12 个独立抽出的测试集上打分 —— 拖动规模,看这 12 次测量如何向它真正的准确率收拢:

12 次结果落在 83.1% 与 96.7% 之间

一旦测试集变小,这个测量就不再有意义:50 行时同一个模型测出来可以是 83.1%,也可以是 96.7%,于是两个团队从这样的测试集上报出 84% 和 96%,交付的其实是一模一样的模型。推到 ,跨度是 88.9% 到 91.1% —— 仍然宽到足以吞掉大多数论文里报告的提升。

规模不是切片唯一会出错的地方。1,000 行里有 20 个正例,其中 100 行被抽去做测试 —— 拖动滑块换过 40 次抽样,看随机切片抓到的正例数怎样飘:

这次随机切片有 3 个正例,而分层切片有 2 个

这 40 次里,它抓到的正例从 0 个到 6 个不等,还有 12% 的概率一个都没抓到 —— 那是一个无法给你唯一关心的类打分的测试集。分层切片每次都恰好抓到 2 个,因为它是按类构建的,而不是按行。

而且行与行之间不一定独立。40 位患者、每人 6 张片子:模型可以学会认出这位患者,而不是那种病 —— 把切分方式从「按行」换成「按患者」,看横跨那堵墙的组消失:

31 位患者在墙的两边都有数据

因为每位患者有 6 行数据,按行切分会让 40 位里的 31 位在墙的两边都有片子,于是 60 行测试数据全都有一个模型已经拟合过的同伴;按患者切分把这两个数清零,报出的分数也跟着掉。那次下跌本身就是结论。当一个小数据集的 10% 少到什么都测不出来时,k 折交叉验证用算力换分辨率:训练 k 次,每次留出不同的第 k 份,报告跨折的均值以及跨度。

04

把考卷提前给模型看的 4 种方式

泄漏从不抛异常。它只会让你的指标变好 —— 这正是它能通过评审的原因。

这里的每一种失败都只是一行代码放错了位置,而且每一种对你已经在跑的测试都是隐形的。先看每个 notebook 都有的那一种 —— 先拟合标准化器,再切分数据。拖动测试比例,比较在全部行上拟合出的均值和只在训练行上拟合出的均值:

在全部数据上拟合均值是 51.5,只在训练行上拟合则是 48.8

注意这两条横线并不重合:全部数据上是 51.5,只在训练行上是 48.8。于是测试行比前者高 1.14 个标准差,比后者高 1.64 个,而那个见过测试行的标准化器已经把模型即将撞上的漂移抹平了。

第 2 种形状是一列伪装过的答案:流失模型里的 refund_issued、死亡率模型里的 discharge_ward —— 拖动这一列带走了多少标签,看两个类怎样被拉开:

验证 AUC 为 0.756,线上为 0.756

在 下,验证 AUC 是 0.967,而诚实的特征只有 0.756 —— 线上拿到的正是 0.756,因为在需要预测的那一刻这一列还没被写入。请对照「必须做出预测的那一刻」去问每一列何时写入,凡答案为「之后」的一律删掉。

第 3 种是 §02 的重复数据迟到了:先切分再去重,副本就会落在两侧 —— 拖动模型已经读过的测试行,看你会报出的分数怎样脱离你真正能拿到的分数:

这次跑出 81.0%,而诚实的分数是 81.0%

因为模型对背下来的行一定答对,60 行里只要有 18 行被污染,就把诚实的 81.0% 变成报出的 86.7%;污染一半测试集则变成 90.5%。整个运行过程看不出任何异常。这正是语言模型的基准污染:训练语料里含着基准题,于是基准不再测量任何东西。

第 4 种是时间。如果每行都有日期,随机切分就会用未来训练、拿过去测试 —— 沿时间线拖动那堵墙,比较按行切分和按日期切分:

墙落在第 180 天(共 240 天). 沿时间线拖动这堵墙;方向键每次移动一天,Home 键复位
有 0 行训练数据的日期晚于第一行测试数据

随机切分时,180 行训练数据里有 176 行的日期晚于第一行测试数据:你把下周交给模型,让它去预测上周。按日期切分把这个数变成 0。这 4 种有一个共同的诊断信号 —— 一个远超预期的验证分数,来自一个你本来并不看好的改动。把它当成一份缺陷报告,而不是一项成果。

05

当你要找的东西很稀有

欺诈、疾病、故障、流失。有意思的那一类永远是小的那一类,而所有默认值都是为另一类调的。

不平衡是世界的属性,不是模型的属性,它会以 3 种不同方式弄坏指标、损失函数和阈值。一夜之间进来 240 笔交易 —— 把其中真正是欺诈的那些往「一批只有 1 笔」拖,看一个对什么都回答「正常」的模型能拿多少分:

240 例中有 12 例,一律答否的模型能拿 95.0%

盯住那个什么都不做的模型。240 笔里的 12 笔就是 5%,对它们一律答「正常」就能拿到 95.0%,同时什么也没找到。把它拖到 ,它拿 99.6%。在不平衡问题上,准确率就是多数类占比穿了一件「测量」的外衣。

于是模型给出一个分数,总得有人来划一条线。这里是欺诈拿到的分数,以及其余一切拿到的分数 —— 拖动这条线,看你抓到的欺诈怎样和纯属虚惊的告警互相换:

召回 50% · 精确率 54%. 在分数轴上拖动这条线;方向键可移动它,Home 键复位
召回 50%,精确率 54%

静止时这条线抓到一半的欺诈,它标出来的东西里有 54% 是真的。往左拖到召回 84%,精确率掉到 22% —— 在 5% 的流行度下,每 4.6 条告警才有 1 条是真的。你要哪一头是一道成本题:漏掉一笔欺诈,对分析师的一小时。

人们常报的那两条曲线,对这件事的重要程度并不一致。同一个分类器、同样两个分布,一条画成 ROC(召回对假正率),另一条画成精确率对召回:拖动流行度,看其中只有一条会动:

AUROC 保持在 0.921,而平均精确率是 0.921

从平衡一路拖到 ,ROC 曲线纹丝不动:AUROC 一直是 0.921,因为它只由那两个分布决定。平均精确率却从 0.921 掉到 0.181。在稀有事件问题上一个漂亮的 AUROC,只是一个还没有人算过账的数。

标准做法是在损失里给稀有类加权。拖动权重,看决策边界实际上跑到了哪里:

权重 1 与阈值 0.500 是同一个决策

对一个校准过的分数来说,给正类加权 w,与未加权模型在阈值 1/(1+w) 处做出的决策完全相同: 就是阈值 0.077,召回 78%、精确率 27%。好的工作点,却不是新信息 —— 而且概率的校准坏了,下游消费它的东西现在都错了。重采样到比例 r 是同一次移动外加重复行。加权、重采样和调阈值是同一根杠杆的 3 个名字,谁都变不出更多正例。

06

你没有采集过的行

数据增广是唯一一种不出门就能加行的办法。它建立在一个关于不变性的断言之上,也在那个断言失效处失效。

断言是:存在一组变换,在它们作用下标签不变。如果一个 6 被平移之后还是 6,那么每一个被平移过的 6 都是合法训练数据,你就可以批量制造。按播放或拖动轨道,走过流水线从采集来的那个样本造出的副本 ——每一个都是一行新的训练数据:

原始采集:与原图重叠 1.00

注意各种变换的代价有多不均匀。上下左右各平移一格,121 格里有 30 格变了,与原图的重叠掉到 0.52;而旋转 12 度只改了 12 格,重叠还有 0.80。这 5 个都被当作 6 交给优化器 —— 这就是那个不变性断言,用数据说出来。

每一个这样的断言都有边界,而这一个很容易越过。把旋转拖过四分之一圈,看副本还有多像 6与它已经多像 9交叉:

旋转 0 度;标签仍然成立

这两个数在 77° 处收拢并交换,到 时副本就是一个 9,却仍然挂着 6 的标签。中间还有一整段区间,它两个数字都不是。越过那个交叉点后,你生成的每一个副本都是一行「答案是错的」训练数据 —— 也就是 §01 的标签噪声,只不过是自己造的,而且按数据加载器的速度量产。

于是剩下那个没人大声问的问题:一个副本值多少?误差随行数按幂律下降,所以副本的价值就是它把等效行数推了多远 —— 先设定副本数量,再设定一个副本相对一行真实数据值多少:

这些副本相当于 1.00 倍的数据,把误差带到 3.9%

横轴是对数轴,所以固定的倍数就是固定的一段右移,与行数无关 —— 这既是这笔交易值得做的理由,也是它终将耗尽的理由。、每个值四分之一行真实数据,就是 6.75 倍的数据量,把误差从 3.9% 带到 2.2%;把价值设成 0.05,同样的副本只值 2.15 倍、误差 3.1%。第二个数字没人能替你填:它是「变换把样本推了多远」除以「任务真实的变化把它推了多远」。 ImageNet-1k 有 1,281,167 张真实训练图像,裁剪加翻转值它的好几倍,仍然不值第二个 ImageNet。

07

速查

整个页面其实只在问一个「顺序」问题,而这里就是那个顺序。

从原始文件到训练好的模型之间有 8 个步骤,其中恰好一个是墙:墙以上的一切看得到所有行,墙以下的一切只看得到训练行 —— 把切分线在梯子上上下拖动,看出错的那些步骤怎样变化:

有 1 步落在切分之下,本该在其之上

完全没有红色。墙以上只放那两个「必须看到全部数据才谈得上正确」的步骤 —— 读入和去重,因为切分之后才删掉的重复,早就横跨了切分。墙以下的一切都是拟合出来的。

落到代码上,业界最常见的泄漏就是一条语句放错了顺序,任何测试、任何类型系统都抓不到它:

# 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.0 个百分点的提升,每一边需要 13,493 行测试数据

看见 1 个百分点的提升,每一边要 13,493 行测试数据 —— 是§03 那一份的 13 倍。每一种失败都无声地、向上地打破同一条不变式:打分那一刻,碰过测试集的东西不曾挪动任何参数。

  • 分数忽然涨了,说不清为什么。先过 §04 的 4 种形状。
  • 某个特征几乎和整个模型一样好。去问那一列何时写入。
  • 测试准确率高过标签噪声的天花板。没人能超过自己的标注者。
  • 在不平衡问题上报准确率。先要基准率。
  • 报到小数点后一位的测试分数。1,000 行时区间是 ±1.9 点。