JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

过拟合与正则化

一个在训练数据上拿满分的模型,到了真实世界里照样可能翻车。这正是机器学习最核心的一出戏——以及我们用来打赢这场仗的工具箱:正则化、早停、还有那条学习曲线。

我们要的从来就不是训练集

在本阶梯前面的几篇里,你已经搭好了「学习」这台引擎:一个度量误差的损失函数,外加一套不断拧旋钮、把误差往下压的梯度下降。那么这里有个让人不太舒服的问题:如果「学习」就是把训练损失压到尽可能小,那为什么不干脆压到*零*?把每一个样本都背得滴水不漏,你就有了一个永不出错的模型。这难道不该是目标吗?

因为训练集我们再也不会遇到第二次了。我们收集了它,但模型真正的工作,是在它*从未见过*的数据上——明天的邮件、下个月的病人、下一位用户上传的照片。我们真正在乎的那个东西有个名字:[[generalization|泛化]],也就是在来自同一个世界的新数据上的表现。一个把训练样本背得很熟、却在新样本上栽跟头的模型,就是发生了[[overfitting|过拟合]]。相反的失败——模型太粗糙,连训练数据里的规律都抓不住——叫做[[underfitting|欠拟合]]

打个比方就很清楚了。想象一堆散落的点,大致沿着一条平缓的曲线分布,再带一点随机噪声。欠拟合的模型,会拿一条直线穿过去——太僵硬,把那个弯给漏掉了。好的模型,会顺着那条平缓的曲线走。而过拟合的模型,会扭来扭去地穿过*每一个点*,为了命中每一个而疯狂摆动——连噪声也一起命中了。换成新的点,那条疯狂扭动的蛇就彻底没辙,而那条沉稳的曲线却笑到最后。把噪声也背下来,正是这个陷阱。

偏差与方差:出错的两种方式

欠拟合和过拟合,是同一个旋钮的两端,经典的命名方式叫做[[bias-variance-tradeoff|偏差—方差权衡]]。*偏差*(bias)是来自错误假设的误差——模型太简单,弯不到真相弯的那个方向,于是不管你喂多少数据,它都一贯地偏;那条穿过曲线的直线就是高偏差。*方差*(variance)是来自敏感性的误差——模型把自己拧成正好贴合它碰巧看到的那批数据的样子,于是换一批训练样本,就会得出一个面目全非的模型;那条穿过每个点的蛇就是高方差。

*权衡*这个词,正是它的灵魂。把模型变得更灵活——更多层、更多参数、更高的容量——你压低了偏差,却抬高了方差;把它变简单,则正好反过来。几十年来,这幅图景是一条 U 形曲线:随着容量增加,总误差先下降,在某个甜蜜点触底,然后随着方差占上风又重新爬升。你的任务,就是找到这个 U 形的底部。

读懂学习曲线

看不见的东西没法修,所以在任何治疗之前,你都需要先做诊断:那就是[[learning-curve|学习曲线]]。这套做法来自前面的阶梯——你把数据切成训练集和验证集,用前者训练,并在训练过程中一个轮次接一个轮次地盯着*两边*的损失。一张图上的两条线,几乎能告诉你正处于哪一种失败模式。

如果两条线都又高又平,你就是在*欠拟合*——模型没有足够的容量或训练量来抓住规律;那就给它更多灵活性,或者训练更久。如果训练那条线一路下降,而验证那条线先触底、然后掉头*向上*,那两条线之间的缝隙,就是*过拟合*的招牌征兆:模型这会儿正在学训练数据里那些到别处反而有害的怪癖。这道越拉越大的缝隙,有时被称作泛化差距,而缩小它,正是本篇要玩的整个游戏。

for epoch in range(max_epochs):
    train_one_epoch(model, train_data)   # weights move
    train_loss = evaluate(model, train_data)
    val_loss   = evaluate(model, val_data)
    log(epoch, train_loss, val_loss)
    # diagnosis, read off the two curves:
    #   both high      -> underfitting
    #   gap widening   -> overfitting
    #   val at minimum -> best point to stop
学习曲线,无非就是每个轮次记录下来的两个损失数字——训练损失和验证损失。这两条线的形状,就是你的诊断结果。

正则化:温柔地惩罚复杂

治过拟合最根本的一招,是正则化:你不再要求优化器*只*去拟合数据,而是再加上一项,让它偏好更简单的模型。新的目标变成 `损失 = 数据误差 + lambda × 复杂度`。优化器这下要平衡两股拉力——既要拟合样本,又要保持简单——而 `lambda` 就是那个超参数旋钮,决定你往「简单」那边推得有多用力。把 `lambda` 拧到零,正则化就消失了;拧得太高,又会逼出欠拟合。

那「复杂度」要怎么量?最常见的答案是权重的大小。大的权重,会让模型对输入的微小变化做出剧烈反应——这正是那条扭动的蛇的行为——所以我们要惩罚它们。L2 正则化加上的是权重的平方和;它把每个权重都平滑地往零收缩,却又不会真的到零,于是影响力被摊薄到许多个小权重上。L1 正则化加上的是绝对值之和;它会把许多权重*恰好*推到零,等于删掉了一些特征,给出一个稀疏、更易解读的模型。这两者的经典搭配——岭回归对应 L2、套索对应 L1——正是从线性回归里来的。

在深度学习里,你会听到 L2 正则化被叫做[[weight-decay|权重衰减]],这两者几乎是同一个想法的两个视角。在损失里惩罚权重的平方,在数学上等价于:每走一步梯度,就把每个权重乘上一个略小于一的数——权重于是温柔地朝零「衰减」,除非数据不断把它们推回去。(用上 Adam 这类自适应优化器时,这两种形式会略有分歧,这也是为什么人们发明了「解耦」权重衰减——这个细微之处,知道它存在就好。)

早停,以及更大的工具箱

学习曲线还顺手递给你一个最简单、最省钱的正则化手段:[[early-stopping|早停]]。既然验证损失会先触底再爬升,那就*在底部停下来*呗。实操上,你盯着验证损失,记住目前为止见过的最佳模型,一旦它连续若干个轮次(这个数叫「耐心值」)都没再变好,就喊停——然后回滚到那个最佳存档点。你就这样几乎不花代价地,拿到了训练中段那个还没开始过拟合的、拟合得恰到好处的模型。

专门针对神经网络,最强力的招数当属[[dropout|随机失活]](dropout):在每一步训练里,随机关掉一部分神经元,逼着网络把赌注分散开,而不是依赖某一条单一通路。这就像一支队伍在排练时随机让几个成员缺席——没有谁能变成唯一的故障点。与之密切相关的,是去搞到*更多或更多样的数据*:数据增强会制造出新的训练样本(把图像翻转、加点噪声、把句子换种说法),它往往是抗过拟合里杠杆率最高的一招,因为这剂药直接对准了病根——相对于模型容量,样本太少了。

把它们串起来——再说句实在话

  1. 先训练,并画出学习曲线。两条线都又高又平?那是欠拟合——先加容量或训练更久,根本还轮不到操心正则化。
  2. 看到验证那条线掉头向上、而训练那条还在下降?那道缝隙就是过拟合。这时候才该伸手去拿工具箱。
  3. 能拿到更多或更多样的数据就先拿(包括增强)——它打的是病根。
  4. 加上权重衰减(L2),神经网络的话再加 dropout;在验证集上、而不是测试集上调它们的强度。
  5. 打开早停,这样你总能保住最佳存档点。最后一刻才打开测试集一次,报出一个诚实的数字。

退一步看,这种统一性令人惊叹:正则化、早停、dropout、更多数据——它们每一个,都是在对模型说一句*「别那么死心眼地全信你的训练数据」*。这份谦逊,正是全部的秘诀。一个把样本拟合得天衣无缝的学习器,学会的是过去;一个抵抗着不去完美拟合样本的学习器,才有机会赢得未来。

最后留一句实在的提醒。没有哪一种正则化对所有问题都最好——没有免费午餐定理保证了这一点,而你做的每一个选择,都内嵌了一种归纳偏置,也就是关于「简单」对你的数据该意味着什么的一个假设。正则化不会白送你泛化能力;它是拿一个你能讲清道理的偏差,去换一份你能量出来的方差下降。带着这份清醒去用它,它就是整个机器学习里最可靠的那根杠杆。