让它能训练,也让它能泛化
一个六层网络的 loss 卡在 ln 2,从逐项测量开始修好。然后是 double descent:40 个点上 5,000 个参数。
本页内容
第 5 章里的网络是有效的。它有九个参数,能学会 XOR,而且它的 gradient 与 PyTorch 对到小数点后十六位。
把它加深到六层,它就完全停止学习。不是学得慢——是完全不学。下面是一个用于双螺旋分类问题的六层网络,训练了 5000 步:
step 1: loss 0.693147
step 5000: loss 0.693147
accuracy: 50.0 %这个数字不是随便来的。 是一个对所有样本都输出概率 的模型的二元交叉熵,而在平衡数据集上,50 % 就是抛硬币。五千步之后,网络连一个数字都没有变化。没有崩溃,没有警告,gradient 仍然完全正确。
本章讲的是“网络能跑”和“网络能用”之间的差距。它分成两半,看起来像两个不同主题,其实是同一件事:让 loss 降下来,以及让它在模型从未见过的数据上也降下来。
为什么六层网络死了
链接到此部分:为什么六层网络死了先观察,而不是猜。把一批输入推过网络,打印每一层 activation 的标准差,然后再打印权重 gradient 的标准差:
def profile(model, x):
h = x
for layer in model:
h = layer(h)
if isinstance(layer, (nn.Tanh, nn.ReLU)):
print(f"activation std: {h.std().item():.4f}")
model(x).sum().backward()
for p in model.parameters():
if p.dim() == 2:
print(f"gradient std: {p.grad.std().item():.2e}")三种初始化,同一个架构,六层 :
| 初始化 | activation std,层 1→6 |
|---|---|
| normal, std | 0.0145 · 0.0016 · 0.0002 · 0.0000 · 0.0000 · 0.0000 |
| normal, std | 0.6573 · 0.9296 · 0.9585 · 0.9634 · 0.9637 · 0.9625 |
| Xavier | 0.1579 · 0.1493 · 0.1353 · 0.1333 · 0.1325 · 0.1403 |
| 初始化 | gradient std,第一层 → 最后一层 |
|---|---|
| normal, std | 3.20e-06 · 4.97e-07 · … · 6.40e-06 |
| normal, std | 1.94e+03 · 2.28e+02 · 1.22e+02 · 4.43e+01 · 1.85e+01 · 7.30e+00 |
| Xavier | 2.31e+00 · 4.50e-01 · 4.26e-01 · 3.89e-01 · 4.39e-01 · 4.73e-01 |
第一行就是上面的网络,它不是学得慢——而是已经没有信号了。到第四层时,activation 标准差在四位小数里已经下溢到零。每个输入都会产生相同输出,输出是一个常数,而常数的 gradient 什么也不是。权重被初始化得很小,原意是“安全一点”,但小到致命。
第二行是相反的失败,它值得理解,因为它很反直觉。activation 看起来很健康——大约 0.96——但那是 饱和了,被钉在它的极限附近,正是第 5 章测到 gradient 会损失近一万倍的区域。然而 gradient 又非常巨大:第一层是 1940。这两件事可以同时为真。每个反向步骤都会乘以 ,而在 128 个单位方差输入下,这个因子的增益大约是 ,足以压过饱和 带来的缩小。gradient 在回传途中按几何级数增长。这就是 exploding gradient,它会在任何真实训练中几步之内产生 nan 的 loss 值。
第三行才是你想要的:activation 的尺度在深度方向上大致恒定,gradient 的尺度也在深度方向上大致恒定。没有东西死亡,也没有东西爆炸。
归一化,以及哪一种活了下来
链接到此部分:归一化,以及哪一种活了下来良好的初始化会修好第零步的尺度。它不会让尺度一直固定:权重会移动,到第五千步时,那个精心设计的方差论证已经不再适用。
归一化层会持续强制尺度。给定一个 activation 向量,减去均值,除以标准差,然后应用一个学习到的缩放 和平移 ,这样如果层最后发现自己想撤销归一化,也可以做到:
唯一真正的问题是:你对什么取平均。Batch normalisation3 沿 batch 维度取 和 ,每个特征一个统计量。Layer normalisation4 沿特征维度取它们,每个样本一个统计量。
这个选择看起来很小,却决定了后面几乎所有事情:
BatchNorm 会让每个样本的输出依赖于同一个 batch 里碰巧出现的其他样本。在训练时,这是一种温和的正则化。在推理时,没有 batch,所以它必须保留训练期间收集到的统计量的移动平均——这意味着该层在训练模式和评估模式下行为不同,而忘记切换模式是这个领域最常见的 bug 之一。它在小 batch 下也会退化,并且对可变长度序列很别扭,因为“位置 40 上 batch 的均值”是由那些碰巧长到这个位置的序列计算出来的。
LayerNorm 会单独归一化每个样本。没有 batch 依赖,没有移动统计量,训练和推理行为一致,不在乎 batch 大小,也不在乎序列长度。一旦你是在为一个用户一次生成一个 token,这些性质每一个都不是锦上添花,而是硬性要求,而这正是第 13 章最终到达的地方。
这就是为什么你会在第 9 章再次见到原封不动的 LayerNorm:transformer block 使用它,而且使用它是因为右栏里的原因,不是因为它在抽象意义上更好。
一次只修一个东西,这才是真正的 skill
链接到此部分:一次只修一个东西,这才是真正的 skill死掉的网络有四个候选修复:Xavier 初始化、LayerNorm、residual connection,以及用 Adam 替代 SGD。诱惑是把四个全加上,然后继续往前。这样做,你永远不会知道到底哪个重要;下次再发生时,你没有方法——只有仪式。
所以一次只加一个。相同 seed,相同数据,相同架构,800 步:
| 添加了什么 | 最终 loss | 准确率 |
|---|---|---|
| 什么都不加 | 0.6931 | 50.0 % |
| Xavier 初始化 | 0.5692 | 60.4 % |
| LayerNorm | 0.6230 | 61.5 % |
| residual connection | 0.6651 | 56.6 % |
| Adam | 0.6787 | 58.7 % |
| 全部四个 | 0.0000 | 100.0 % |
如果你在凌晨 2 点读这张表,结论会是:没有一个单独有效,合在一起全都有效,所以 deep learning 是炼金术。这个结论是错的,而弄清楚为什么,是本章最有用的部分。
给每次运行六倍预算——5000 步而不是 800 步——结果完全变了:
| 添加了什么 | 最终 loss @ 5000 | 准确率 |
|---|---|---|
| 什么都不加 | 0.6931 | 50.0 % |
| Xavier 初始化 | 0.0007 | 100.0 % |
| LayerNorm | 0.0002 | 100.0 % |
| residual connection | 0.6653 | 56.7 % |
| Adam | 0.6908 | 53.4 % |
| Xavier + Adam | 0.0000 | 100.0 % |
| Xavier + LayerNorm | 0.0001 | 100.0 % |
现在图像清晰了,而且这是诊断,不是仪式。
只靠初始化就能修好。只靠归一化也能修好。 它们都处理了真正的病因——前向信号坍缩到零——任意一个都足够。在 800 步时,它们只是看起来像只拿了部分分数,因为它们已经解决了问题,只是还在爬出来。
residual connection 和 Adam 在任何预算下都修不好它。 不是因为它们不好,而是因为它们治疗的是另一种病。residual connection 给 gradient 一条绕过阻塞层的路径;当问题出在 gradient 上时,这非常有价值,但当前向信号已经是零时,它毫无价值,因为绕过死层的捷径仍然携带死值。Adam 会根据每个参数自己的 gradient 历史来重新缩放它的步长;当 gradient 大小差异极大时,这很有帮助,但它不能让一个输出不依赖输入的网络复活。
而“什么都不加”在五千步后仍然正好是 0.6931。不是 0.6929。它不是慢;它是死的。现在这个区别可以看出来了,因为你有了能作为对照的“修复有效”那一行。
配得上 PyTorch
链接到此部分:配得上 PyTorch从这里开始,本课程使用 PyTorch。这不应该只是宣布,而应该先挣到资格,所以这里精确说明它做了哪些你已经知道怎么做的事情。
优化器是一条把 gradient 转成参数更新的规则。普通的 gradient descent 使用 gradient。Momentum 使用它的移动平均,这会平滑噪声,并在方向一致的维度上积累速度:
v = beta * v + p.grad
p -= lr * v Adam5 保留两个移动平均——gradient 的平均,以及 gradient 平方的平均——然后用前者除以后者的平方根,让每个参数得到一个按它最近 gradient 大小缩放过的步长:
m = b1 * m + (1 - b1) * g # mean of the gradient
v = b2 * v + (1 - b2) * g * g # mean of the squared gradient
m_hat = m / (1 - b1 ** t) # bias correction: both averages start at zero
v_hat = v / (1 - b2 ** t)
p -= lr * m_hat / (v_hat.sqrt() + eps) 十行。把二者放到同一个问题上,与 torch.optim 对跑 50 步:
SGD+momentum by hand [2.7781870365142822, -1.0304985046386719]
torch [2.7781870365142822, -1.0304983854293823] max |diff| = 1.19e-07
Adam by hand [0.4893140196800232, -0.46317872405052185]
torch [0.48931416869163513, -0.46317875385284424] max |diff| = 1.49e-07在 float32 精度下完全一致。torch.optim.Adam 就是这五行,再加上几十年对边界情况的照料和一个 C++ kernel。这就是你从这里开始做的交易:不是用魔法换理解,而是用速度换掉你已经写过的那些行。
Adam 为什么存在:曲率
链接到此部分:Adam 为什么存在:曲率对 Adam 的常见解释是“每个参数自适应 learning rate”,这是一种描述,而不是原因。原因是几何,而且可以被测量。
取一个 loss,它在不同方向上的曲率不同:一个方向陡,另一个方向浅。SGD 只有一个全局 learning rate,所以它必须选一个足够小、能在最陡方向保持稳定的值——而这个值对较浅方向来说又太小了,进展像爬一样慢。这就是 gradient descent 在狭窄谷底里之字形下行那张经典图的原因。
两个曲率比,三个优化器,300 步,并且每个优化器都从 sweep 中拿到最佳 learning rate,保证没人吃亏:
| 曲率比 | SGD | SGD + momentum | Adam |
|---|---|---|---|
| 10 : 1 | error 0.000002 | error 0.000000 | error 0.000000 |
| 1000 : 1 | error 1.925485 | error 0.001432 | error 0.000000 |
| 在 (1000:1) 发散 | 8 个 rate 中 4 个 | 8 个 rate 中 4 个 | 6 个 rate 中 0 个 |
在十比一时,一切都能工作,没有什么可讨论的。在一千比一时,普通 SGD 在尝试过的任何 learning rate 下都到不了答案——它的最好结果仍然有 1.93 的误差——并且在一半 rate 下直接发散。Adam 精确落在目标上,而且一个都不发散。
最后一列就是 Adam 成为默认选择的实际原因。不是 Adam 会找到更好的解;在条件良好的问题上,调好的 SGD 常常能匹配甚至超过它。原因是 Adam 对你选的 learning rate 不那么敏感,而真实网络在数百万参数上的曲率比远远糟过一千。
这里还需要补上两个东西,而且都只是一行。Gradient clipping 会在 gradient 向量的 norm 超过阈值时重新缩放它,这能把诊断表里“loss 突然跳到巨大值”那一行变成无事发生。还有 learning rate schedules:前几百步从接近零开始做短暂的 warmup,因为 Adam 的方差估计在见过一些 gradient 之前都是垃圾,而在垃圾估计上走一个完整步长可能毁掉初始化;然后做朝零的 cosine decay,因为如果结束训练时还用着开始时的同样步长,就意味着你会在最小值附近抖动,而不是安定下来。
后半部分:完美拟合却什么也预测不了的模型
链接到此部分:后半部分:完美拟合却什么也预测不了的模型到目前为止,一切都在讲如何让 loss 降下来。现在进入更难的半部分,因为 loss 下降不是目标——它只是目标的代理,而这个代理会以一种具体且著名的方式失效。
从一个光滑函数里取十二个点,带一点噪声。拟合次数逐渐增加的多项式:
| 次数 | train RMSE | test RMSE |
|---|---|---|
| 1 | 0.764499 | 0.6985 |
| 3 | 0.252605 | 0.3031 |
| 5 | 0.164437 | 0.1568 |
| 9 | 0.088960 | 0.2347 |
| 11 | 0.000000 | 1.2094 |
11 次多项式穿过 12 个点,会精确穿过每一个点——train error 到六位小数都是零——但在未见过的数据上比 5 次多项式差八倍。让 3 次和 11 次在训练范围外一点的 处做预测:
degree 3: predicts -1.053 (truth -0.012)
degree 11: predicts +61.224 (truth -0.012)六十一,而答案大约是零。模型没有学到函数;它学到的是这十二个点,而在点与点之间,它只是在做算术要求它做的事。
这就是 overfitting,它的反面——1 次多项式,根本无法表示曲线,到处都差——就是 underfitting。经典说法把模型的期望误差分成三部分:bias,来自模型太僵硬、无法表示真实规律的误差;variance,来自模型太灵活、追逐这个特定样本中的噪声的误差;以及不可约噪声,什么都修不了。简单模型有 bias,灵活模型有高 variance,而经典处方是在中间找到甜点——上表里的 5 次。
标准工具都在攻击 variance 项:
- L2 正则化(weight decay)把 加到 loss 上,将权重拉向零,让函数更平滑。在上表中,11 次多项式最大的系数造成了破坏;惩罚大小可以拆掉它。
- L1 则改为加上 。差别不是表面文章:L2 的 gradient 与权重成正比,所以会随着权重变小而缩小,趋近零但不会到达;而 L1 的 gradient 是常数 ,会一路持续推动。因此 L1 会产生精确为零的权重——它会选择特征。L2 会产生小权重。想要平滑性时用 L2,想要稀疏性时用 L1。
- Dropout7 在每个训练 step 随机把一部分 activation 置零,让任何单元都不能依赖某个特定的其他单元一定存在。
- Early stopping 监控 validation loss,并在它开始上升时停止。
- Data augmentation 从已有样本制造更多训练样本,从源头上攻击问题:overfitting 既是数据短缺,也是参数过剩。
- Cross-validation 把数据分成 份并训练 次,当你的数据少到舍不得留出一个 held-out set 时,它能换来可靠的 test error 估计。
Double descent,或者为什么上一节不是全部故事
链接到此部分:Double descent,或者为什么上一节不是全部故事现在来看打破这幅图景的事实。
bias-variance 故事说,过了甜点之后,更多参数意味着更差的泛化。现有语言模型的参数量远远超过经典规则按它们所见数据允许的范围,却泛化得非常好。这两句话都是真的,而调和它们,是本章最有用的事。
四十个训练点,二十维输入,随机 ReLU 特征,把特征数量 从 2 sweep 到 5000——并且只要存在多个能拟合的解,就选择 minimum-norm 解:
| train RMSE | test RMSE | |||
|---|---|---|---|---|
| 10 | 0.25 | 0.8822 | 1.2520 | 1.89 |
| 20 | 0.50 | 0.5962 | 1.1634 | 2.59 |
| 30 | 0.75 | 0.3896 | 1.5323 | 4.15 |
| 38 | 0.95 | 0.1769 | 3.7163 | 10.25 |
| 40 | 1.00 | 0.0000 | 5.8140 | 14.83 |
| 42 | 1.05 | 0.0000 | 3.1623 | 9.35 |
| 60 | 1.50 | 0.0000 | 1.1058 | 2.78 |
| 200 | 5.00 | 0.0000 | 0.6638 | 0.98 |
| 1500 | 37.50 | 0.0000 | 0.5859 | 0.33 |
| 5000 | 125.00 | 0.0000 | 0.5664 | 0.18 |
分三段读。到 为止,经典故事完全成立:误差先下降,然后开始上升。在 ——interpolation threshold,也就是模型恰好有足够参数穿过每个训练点的地方——test error 达到峰值 5.81,比小模型差五倍。这个峰值就是经典警告,而且它是真实的。
然后它又下降了。而且继续下降,越过 ,越过 ,一直到 ,此时 0.5664 的 test error 好过任何 under-parameterised 模型曾经达到的最好结果。一个用 5000 个参数拟合 40 个点的模型,是表中最好的模型。
这就是 double descent,89 机制在最后一列里看得见。一旦 ,就存在无穷多个能精确拟合训练数据的参数设置,而你得到哪一个取决于你怎么选。minimum-norm 解会选择最小的那个,而 显示了这意味着什么:它在 threshold 处达到 14.83 的峰值——那里刚好只有一个插值解,不管它多极端,你都只能接受——然后随着 增长而单调下降,因为更多参数意味着有更多插值解可供选择,也意味着可选的最小解会变得更小。在 时,norm 是 0.18,比 threshold 处小八十倍。
所以额外参数并不是在增加复杂度。它们是在增加选择,而选择规则把这种选择花在了简单性上。正则化不在 loss function 里;它在算法里。从小初始化出发的 gradient descent 已被证明偏向小 norm 解,这就是为什么这种行为会出现在按普通方式训练的真实网络中,而不只存在于上面的线性代数里。
实际后果,也就是第 10 章所依赖的结论:“模型参数比数据多,所以它会 overfit”不是一个有效论证。当模型位于 threshold 左侧时,它曾是一条好规则。现在所有有趣的东西都远在它右侧,而规则在那里会反过来。
接下来去哪里
链接到此部分:接下来去哪里本章的工具已经足以训练一个能在表格数据上工作的网络:若干数字行,一列标签。
语言不是这样。在模型能预测下一个词之前,必须先有人决定“词”到底是什么——答案既不是字母,也不是单词,而是模型从训练数据的原始字节中学到的词表。这个在训练开始前一次性做出的决定,会决定模型能说多少东西、一次请求要花多少钱,以及为什么能通过法律考试的模型却无法可靠数出 strawberry 里有几个字母。
第 7 章会构建一个 tokenizer。
来源与方法
链接到此部分:来源与方法对于上文使用的 residual connection,见 He 等人,Deep Residual Learning for Image Recognition(arXiv:1512.03385)。Andrej Karpathy 的 Building makemore Part 3: Activations & Gradients, BatchNorm 在真实模型上讲解了 activation 直方图诊断,是本章前半部分最好的动手处理。Yaser Abu-Mostafa 的 Learning From Data 第 8 讲和第 11–13 讲系统讲解了经典泛化理论,包括本章压缩成一段的那些部分。
参考资料
链接到此部分:参考资料-
Glorot, X. and Bengio, Y. Understanding the difficulty of training deep feedforward neural networks. AISTATS (2010). 上方框中复现的是保方差论证。 ↩
-
He, K., Zhang, X., Ren, S. and Sun, J. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. arXiv:1502.01852 (2015). ↩
-
Ioffe, S. and Szegedy, C. Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. arXiv:1502.03167 (2015). 注意,标题里的“internal covariate shift”解释后来受到了大量质疑;这个层确实有效,但最初关于其原因的说法存在争议。 ↩
-
Ba, J. L., Kiros, J. R. and Hinton, G. E. Layer Normalization. arXiv:1607.06450 (2016). ↩
-
Kingma, D. P. and Ba, J. Adam: A Method for Stochastic Optimization. arXiv:1412.6980 (2014). ↩
-
Loshchilov, I. and Hutter, F. Decoupled Weight Decay Regularization. arXiv:1711.05101 (2017). ↩
-
Srivastava, N., Hinton, G., Krizhevsky, A., Sutskever, I. and Salakhutdinov, R. Dropout: A Simple Way to Prevent Neural Networks from Overfitting. JMLR 15, pp. 1929–1958 (2014). ↩
-
Belkin, M., Hsu, D., Ma, S. and Mandal, S. Reconciling modern machine-learning practice and the classical bias–variance trade-off. PNAS 116(32), pp. 15849–15854 (2019). 这篇论文命名了这一现象。 ↩
-
Nakkiran, P., Kaplun, G., Bansal, Y., Yang, T., Barak, B. and Sutskever, I. Deep Double Descent: Where Bigger Models and More Data Hurt. arXiv:1912.02292 (2019). 展示了真实深度网络中的这种效应,并且不仅沿模型大小轴,也沿 training time 轴展示。 ↩