监督学习 入门

9 套房子,每套一个数,以及一次训练所做的全部事情。这一页在这 9 笔成交上证明 3 件事:模型是从你挑的函数族里选出来的一个函数;训练是一条算术规则反复执行直到某个斜率归零;以及在手头数据上完美的拟合,恰恰是它在手头没有的数据上会一无是处的最可靠信号。

01

一个标签能买到什么

9 套房子成交了,每套的面积和价格我们都知道。监督学习就是把这 9 条事实变成一套还没卖的房子的价格。

监督问题总是成对到来。每个样本带一个特征—— 答案揭晓之前你就能量到的东西 —— 和一个标签,也就是答案本身。这里特征是建筑面积,标签是成交价,一共 9 对。

滑块一次问一个面积。在真有人成交过的那 9 个面积上,标签就摆在那儿可以直接读;把这次询问拖到别处就没有了:

1,500 ft² → 1,070k

注意我们拿到的有多少:9 个价格,中间什么都没有。在 处图上是一个问号,因为根本没有这样一笔成交。这份入门里的每个方法,都是对空隙里填什么的一种回答,而且谁都没有比这更多的数据。

最便宜的回答是画一条直线,价格直接从线上读。这条线是 ŷ = w·x + b;这里 b 先钉住,滑块只动w,也就是多一平方英尺值多少钱:

w = 200 $/ft² · rmse 592k

盯右边那个数。那是拟合误差—— 均方根,所以单位和价格一样是美元。w = 200 时这条线每套房差 $592k;拖到 它掉到 $74k,而这就是这个函数族的极限。

钉住 b 只是图省事,不是规矩。第二个参数把整条线抬起来而不改斜率,所以直接抓住这条线:左右改斜率,上下改高度:

w 200 · b 700k. 拖这条线:左右改斜率,上下改高度;方向键微调,Home 回到初始那条线
w 200 · b 700k · rmse 338k

两个旋钮都能动,搜索的就不再是一排线而是一个平面上的线,平面上最好的那点是 w = 484、b = $377k,误差 $74k。两个数。前沿语言模型大约有一万亿个,搜索还是同一个搜索。

函数族是一个关于「世界长什么样」的赌注,而赌注是会输的。开关在同样这 9 笔成交上,把拟合出来的函数在 4 个族之间来回换:

0 次 · 要学 1 个数

注意水平线无论那一个数怎么调都下不到 $448k以下:这个族根本没办法表达「面积越大价格越高」。这叫欠拟合,它是族的性质,不是训练的性质。抖动用 9 个数把误差压到 0 —— 而 §04 讲的正是为什么那才是更糟的失败。

所以一个监督模型就是 3 个决定,按顺序:你量什么、你在哪个族里搜、什么算拟合得好。第一个在这里被我们手头的数据钉死了。第二个就是上面那个赌注。第三个是下一节的全部内容。

02

「错」值多少钱

要改进拟合,先得给它打分。你挑哪种分数,就决定了模型被允许在哪些房子上出错。

对单独一套房,错在哪很好说:模型预测的价格和这套房真实成交价之间的差距。整个设计问题在于,9 个这样的差距一起来了该怎么办。

这些差距挂在每笔成交和这条线之间。滑块动线,9 条差距一起变:

w = 300 $/ft² · 差距 341k

右边那个数是它们的普通平均值,w = 300 时是 $341k。把这些差距等权重加起来就是平均绝对误差 —— 一个完全合格的损失,却几乎不是任何人的默认选择,因为每套房一票,不管它被错得多离谱。

默认做法是先把每段差距平方,而这不是形式:平方后的差距是一块面积,损失就是这 9 个正方形盖住的总面积。滑块还是那个斜率:

w = 300 $/ft² · mse 149,965

拖的时候盯住最差的那套房。平方让惩罚涨得比错误本身还快,所以即使在 上,9 套里也有 1 套独占损失的 21%。这就是均方误差的性格:它宁可 9 次小错,也不肯 1 次大错。

这里模型只有 1 个自由的数,所以损失也是 1 个数的函数 —— 而一个数的函数可以直接画出来。滑块还是那个w;曲线是你可能拿到的每一个拟合误差:

w = 300 $/ft² · mse 149,965

因为模型是线性的、损失是平方的,这条曲线就恰好是一条抛物线,不是差不多,而是恰好;它的最低点落在w = 484。训练就是去找那一点。损失读作 1,006,452;谷底是 5,516。

平方有它的失效方式,而且你可以亲手把它开出来。一套房的成交价在你手里:往上拖,看平方误差那条线追着它跑,而绝对误差那条线不理它:

1,333k. 上下拖动改这套房子的成交价;方向键微调,Home 回到真实价格
1,333k · 平方 w 484

平方的导数随差距变大,所以一个坏标签就能把整个拟合拽向自己;绝对误差那条线一旦站错了边就不再在意差多少。真实数据集里就是有坏标签。平方误差是对的默认值,也是最不该不假思索就用的那个。

并不是每个标签都是数。当答案是一个类别时,模型给它输出一个概率,而代价是这个概率的负对数 —— 概率由滑块给定:

p = 0.50 · 0.69 nats

注意这里的不对称。答对几乎不要钱;笃定地答错则代价无上界。在 时代价是 4.61 nats,差不多是抛硬币那 0.69 的 7 倍 —— 正是这种无上界,让语言模型在意那个差点漏掉的 token。

你真正在乎的那个数,通常不是你能下降的那个。滑块在 8 个已经打过分的样本上移动一个偏置,0/1 损失和交叉熵同时看着:

阈值 0.00 · 0/1 损失 0.25 · 交叉熵 0.49

0/1 损失只会数数,所以它是一段楼梯:处处平坦,而恰恰在它变化的地方没有定义。没有方向可以跟。交叉熵是一个光滑的替身,它对「哪边更好」的判断和 0/1 一致,而训练真正下降的是它。

所以损失不是事后挂上去的记分牌,它就是优化器要走的那张曲面。平方误差把这张面变成抛物线。交叉熵让它对分数是凸的。准确率把它变成一段楼梯 —— 这就是没人拿它训练的原因。

03

怎么走到谷底

到这里为止还没有训练过任何东西。训练就是一条规则,把损失的形状变成参数的下一个取值。

一般情况下你解不出谷底 —— 线性拟合有闭式解,神经网络没有 —— 所以真正能用的办法是局部的:测出往哪边是下坡,然后迈一步。

这只碗就是上一节那个损失。沿着它拖,切线会跟着走 —— 它的斜率就是梯度,整个训练全靠这一个数:

w = 200 $/ft². 左右拖动沿曲线移动;方向键微调,Home 回到初始那点
w 200 · 曲线斜率:-2,427

光看符号就知道该往哪边走:斜率为负,往右。w = 200 时读作 −2,427;到了这只碗的谷底读作 0 —— 这不是巧合,这就是谷底的定义。

大小决定走多远。一步是 w ← w − η·斜率,而η—— 学习率,就在滑块上 —— 是这条规则里唯一由你来挑的部分:

η 0.020 · w 100 → 166

看 η = 0.020 时那个箭头:从 w = 100 出发落在 166,损失从 635,423 掉到 438,344。它没走够,因为斜率是在这一步出发的地方量的。推到 ,一步就能吃掉大半段距离。

只要允许重复,走不够并不要紧。播放键跑 20 步,第二个滑块定η;这段下降从 w = 0 开始,也就是那条水平线:

第 0 步,共 20 步 · w 0

每一步都把剩下的距离乘上同一个系数,所以收敛是几何式的而不是线性的:η = 0.020 跑满 20 步还停在 472,而 η = 0.050 只要 12 步就到 483。η 翻一倍,步数大致减半 —— 但只到某个界限为止。

那个界限可以精确定位,不是口味问题。把η推过去,刚才还在收敛的同一条规则就开始往碗外爬:

η = 0.100

因为这条曲线是曲率为 8.553 的抛物线,每一步都把到谷底的距离乘上 |1 − η·8.553|,所以只有 η 小于 0.234 才收敛,大于就发散。在 处没有任何东西提醒你:损失只是在几百步之后变成 NaN。

而 0.234 不是问题的性质,是单位的性质。开关用 4 种不同的单位重新量同样这 9 个面积,再把仍然收敛的步长范围画一遍:

÷ 1 · η < 2.34e-7

这条轴是对数的 —— 每一格是上一格的 10 倍 —— 所以那些跨度要当比值来读。用原始平方英尺量面积,η 的上限是 2.34e−7;把面积除以 1,000,上限变成 0.234,大了一百万倍,因为曲率带的是特征的平方。流水线在迈第一步之前先做归一化,原因就在这里。

这一节有两件事会活到你以后遇到的每一个优化器里。步长正比于梯度,所以平坦区域等多久都走得慢;而安全步长被曲率卡死 —— 这正是 Adam 要为每个参数估一个自己的尺度,而不是让 10 亿个参数共用一个 η 的原因。

04

拟合不等于学会

拟合误差为零什么都证明不了。唯一重要的问题是,模型碰到一套没见过的房子会怎么样。

这个问题真能回答,因为同一个市场、同一段时间里还有另外 9 套房子成交,而我们把它们每一套都排除在拟合之外。

留出的那些成交画成空心圈,而这次拟合 —— 一条普通直线 —— 一套都没见过。把它们一次一套拖到图上:

留出 0 笔

图上现在有 2 个数而不是 1 个:拟合过是在建线用的那 9 笔上量的,没见过是在另外 9 笔上量的。对一条直线来说,它们分别落在 $74k 和 $76k。挨得很近 —— 这正是直线的全部好处。

现在开始花容量。它一开始还是那条直线,滑块定拟合出来的多项式的次数 —— 0 次是水平线,8 次恰好穿过每一笔训练成交:

1 次 · 拟合过 74k · 没见过 76k

看拟合过一路往下掉,到 时是 $0;而没见过在 4 次那里掉头,最后停在 $220k。模型没有学会这个市场。它背下了 9 套房子,中间想怎么编就怎么编。

把这 2 个数对着每个次数一起画出来:拟合过在跌,没见过在掉头,滑块所在的次数从两条曲线上穿过:

1 次 · 拟合过 74k · 没见过 76k

纵轴是对数的 —— 每个标签是下面那个的 2 倍 —— 所以你在拟合过和没见过之间读到的是一个比值。它们一直并肩走到 4 次,之后分开。那道分开就是泛化间隙,也是这一节唯一的诊断指标。

这道间隙是有形状的,而且你可以亲手摸到它。被询问的面积能沿着横轴拖;越过最后一笔成交之后,8 次的那条拟合就完全是在无凭无据地作答:

2,000 ft². 左右拖动改要问的面积;方向键微调,Home 回到数据范围内
2,000 ft² → 1,391k

在 —— 比它见过的最大那套房还多 500 ft² —— 这条拟合预测出 $81,854k,是数据里最贵那套房价格的 40 多倍。它不报错,也不说自己没把握。它就返回一个数,而下游系统会拿去用。

出路有 2 条,第一条是你未必买得起的那条。滑块把训练集从 8 笔成交一路涨到 400 笔,容量固定在 4 次:

8 笔 · 没见过 108k

两条曲线都在动,方向相反:拟合过在涨,因为 8 个点容易讨好而 不好讨好;没见过掉到 $69k 就走平了。那个地板是噪声 —— 每个价格里我们这一个特征装不下的那部分,大约每套 $70k —— 再多数据也推不动它。

还有一种失败,而且是真会上线的那种。滑块把留出成交里的一部分换成拟合里已经有的房子:同一条挂牌,录了两遍:

重复 0 笔 · 报出来的 220k · 诚实的 220k

看报出来的误差往下掉,而诚实的那个一动不动。9 笔里重复 9 笔时,报告宣称在模型「从没见过」的数据上只差 $12k,而那 9 套每一套都是它背下来的。这就是数据泄漏,而且它安静地失败。

不变量只有一句话:碰过拟合的东西,一律不许出现在评分里。不许是那一行,不许是它的近似重复,也不许是用它算出来的特征 —— 在划分之前对整个数据集做归一化的那一列,已经把测试集的均值泄漏进训练里了。

05

把它拽回来

加数据是诚实的解法,也常常是买不到的解法。另一条路是让 9 笔成交买一个更小的模型。

8 次拟合不是错在有 9 个系数,而是错在被允许把系数做得极大:训练点之间那些抖动,就是一个 48,365 的系数买来的。

那就为「大」收费。现在拟合最小化的是平方误差加上 λ 乘以系数平方和,滑块把λ从几乎为零往上抬:

λ = 1.0e-7 · 拟合过 11k · 没见过 174k

看曲线放松下来。拟合过一路变差 —— 它必须变差,因为带惩罚的拟合按定义就不再是最好的拟合 —— 而没见过先改善到 ,然后它也开始变差。函数族一点没变。变的只是用它要付多少钱。

惩罚同时作用在每个系数上,所以诚实的画法是把 8 个都画出来,滑块还是同一个。最大的那个画满,其余 7 个画成灰的:

λ = 1.0e-7 · max |c| 48,365

这里两条轴都是对数的 —— 每个标签是上一个的 100 倍 —— 所以图上一条直的下坡就是一条幂律。最大的那个系数在整段扫描里从 48,365 降到 25。拟合并没有失去弯曲的能力,它失去的是急弯的能力。

这个取舍有一个最优点,找到它的办法和 §04 里找到 4 次那次一样 —— 只看留出误差,绝不看拟合误差。滑块还是 λ:

λ = 1.0e-7 · 没见过 174k

没见过的最低点 —— λ = 0.010 时的 $69k —— 和 4 次拟合早就给出的结果只差 1,000 美元以内。所以惩罚并没有赢过一个挑得好的函数族,它只是免去了你必须先挑对这件事;而当函数族多到没法一个个数时,这件事非常值钱。

3 种手段里最便宜的那个根本不是惩罚。下面还是那条 8 次拟合,用梯度下降从全零系数开始拟合,而滑块是已经跑了多少步:

第 0 步 · 拟合过 1,351k · 没见过 1,394k

因为下降会先关掉问题里容易的那些方向,拟合在走向「背下来」的路上,会先路过一个好模型:没见过在 触底到 $65k,到 2,048 步又回到 $78k。提前停是白送的正则化,而且在这里它赢过另外两种。

这 3 招都是把拟合被允许到达的函数集合缩小。权重衰减就是这个惩罚换了个名字;dropout、数据增强、换个更小的模型,都是同一招的不同写法。这个旋钮永远在留出数据上挑,绝不在训练损失上挑。

06

完整的一步

上面这些全都是同一个循环。这里就在那 9 笔成交上跑一遍,循环产出的每个数都在屏幕上。

真实训练不会每一步都用全部数据。它取一个小批次 —— 这里是 9 里面的 3 笔 —— 因为同样的算力下,跑 30 次带噪声的梯度好过跑 1 次精确的。

播放键把一步训练走过它的 5 个阶段,作用在这个批次上,w 从 300 开始,η 是 0.05:

第 1 阶段,共 5 段 · 取一个批次

注意第 4 阶段的梯度:这个批次上是 −1,698,全部 9 笔上是 −1,572。这个批次偏了 8%,而这点误差就是「不看全量数据」的全部代价 —— 它用多走 3 倍的步数把这笔账赚了回来。

for batch in loader:            # 1
    yhat = model(batch.x)       # 2
    loss = mse(yhat, batch.y)   # 3
    g    = grad(loss, w)        # 4
    w   -= eta * g              # 5

那 8% 只是一次抽样。开关把同样这 9 笔按 1 笔、3 笔、9 笔分批,画出每个批次给出的估计和精确的斜率:

批次 9 笔 · 散布 ±0

批次 9 笔时只有 1 个估计,而且精确。3 笔时散布是 ±311。1 笔时是 ±1,716 —— 比斜率本身还大,所以单独一笔成交可能陡出一倍,也可能几乎没斜率;把它们平均掉,才把准确度买回来。

这 5 行就是监督学习。前沿训练改变的只是规模:第 2 行变成一万亿个参数,第 3 行是词表上的交叉熵,第 4 行是反向传播,eta 变成一条调度 —— GPT-3 那次 1,750 亿参数的训练峰值 η = 6 × 10⁻⁵,在头 3.75 亿个 token 上预热(Brown 等,2020)。问题不变:留出损失还在往下走吗,有没有东西漏进批次里。