📊 第4章 机器学习的基础知识
📄 一句话概括

三步:切数据、训练、验证。测试集在训练时绝对不能看。 k=1 时训练误差 0.0000、测试误差 0.0697;k=3 时训练 0.0472、测试 0.0291。 训练误差低、测试误差高叫过拟合;两个都高叫欠拟合。 判断就这么简单。

📝 本章和前面几章的关系

上一篇讲模型怎么来的。这一篇讲怎么判断它好不好。 教材 4.2、4.3、4.4 三节:数据的准备和预处理、模型构建与系统化、正式发布和性能提升。 三节合起来就是「学习过程」的前中后篇。 本篇用一个 k 近邻的例子把这三步走完,重点在过拟合这个坑。

一、第一步:切数据

演示造了 20 个训练点,规律是 y = sin(2x),每个点叠了 ±0.5 的噪声。 又另外造了 19 个测试点,故意落在训练点的正中间——这样模型必须「插值」,不能靠背。 测试集在训练时绝对不能看。 这句话不是建议,是纪律。

一旦看了,你调的每个参数都带着测试集的信息,测出来的效果就是虚的。这叫数据泄漏。 数据泄漏最隐蔽的形式是预处理:用全体数据的均值做标准化, 再切训练测试集——测试集的均值信息就漏进训练里了。 正确顺序永远是:先切,再各自算统计量。这一条能避免很多莫名其妙的「线上效果暴跌」。

二、第二步和第三步:训练、验证

这里只有一个参数可调:预测时看几个邻居(k)。 k=1 的意思是「找最近的那个点,直接用它的值」。k=5 是「找最近的五个,取平均」。 挨个试一遍,每个 k 都算两个误差:在训练集上的,和在测试集上的。

结果是本篇最该记住的一组数:

k 训练误差 测试误差
1 0.0000 0.0697
2 0.0654 0.0412
3 0.0472 0.0291
5 0.1021 0.0622
9 0.2370 0.2027
15 0.5160 0.4269

k=1 的训练误差是 0——每个点最近的邻居就是它自己,预测值完全等于真实值。 但它的测试误差是所有 k 里最高的。 换一批没见过的点,它一个都猜不准。这就是过拟合。

三、过拟合长什么样

演示把 k=1 在训练集上的前五个预测打出来:每一个都和真实值一模一样,小数点后四位都相同。 这不是模型厉害,是它把每个点的噪声也记住了。换一个没见过的 x,它没有任何依据。 k=3 的预测有偏差(比如 x=0 时真实 0.27、预测 0.69),但偏差不大,整体趋势是对的。

过拟合的本质是「把训练集里的偶然当成了规律」。 噪声是偶然,曲线形状才是规律。 为什么会这样?因为模型太灵活。k=1 的模型能贴合任意形状,包括噪声造成的小抖动。 对付过拟合的办法就是「限制模型的灵活度」:k 调大、加正则、减少参数、多给数据。

四、欠拟合是另一头

k 调太大也不行。k=15 时训练误差 0.5160,测试误差 0.4269——两个都高。 这叫欠拟合:模型太笨,连训练集都学不像。k=15 等于把半个周期的点平均掉,曲线被抹平了。 判断标准很简单:训练误差低、测试误差高 = 过拟合;两个都高 = 欠拟合。

中间的甜点区在 k=3 附近:训练误差 0.0472、测试误差 0.0291。 注意一个反直觉的地方:测试误差比训练误差还低。 这在这个例子里是正常的, 因为测试点没有噪声(我造的是纯 sin 值),而训练点有。 真实场景里测试误差一般略高于训练误差,高太多就是过拟合。

五、上线之后还要一直看

教材 4.4 节讲「正式发布和性能提升」,说的是模型上线不是终点。 真实世界会变:用户行为变、商品价格变、季节性变。今天测试误差 0.03,三个月后可能变成 0.15。 这叫模型漂移。它不是 bug,是必然。所以上线之后要持续监控效果,定期用新数据重训。

工程上通常留一部分流量做「影子运行」:新模型先不接真实请求,只记日志,对比老模型的表现。 这也是第 1.3 篇说的:实验跑通一次和系统每天跑通,是两个量级的事。

六、动手 Demo:k 从 1 调到 15,训练误差和测试误差各怎么走

造 20 个训练点(带噪声)和 19 个测试点(落在训练点中间)。 用 k 近邻回归,k 从 1 试到 15。 每个 k 都算训练误差和测试误差,看两条曲线怎么分叉。

def knn(tr_x, tr_y, k, x):
    d = sorted((abs(x - a), b) for a, b in zip(tr_x, tr_y))
    return sum(b for _, b in d[:k]) / float(k)

for k in (1, 2, 3, 5, 9, 15):
    tr = mse(lambda t: knn(tr_x, tr_y, k, t), tr_x, tr_y)
    te = mse(lambda t: knn(tr_x, tr_y, k, t), te_x, te_y)
    print('k=%2d  训练 %.4f  测试 %.4f' % (k, tr, te))

实跑输出:

--------------------------------------------------------------------
训练三步加过拟合:k 调大调小差在哪
--------------------------------------------------------------------

  ① 第一步:把数据切成两半
  --------------------------------------------------------------------
  真实规律是 y = sin(2x),训练点 20 个,每个都叠了 ±0.5 的噪声
  测试集 19 个点,故意落在训练点的正中间
  测试集必须训练时看不到。 否则你测的是「背下来的答案」,不是「会不会做新题」。
  这一步看着简单,实际最容易出错:先洗数据再切,或者用全体数据的均值做标准化,
  都会让测试集的信息漏进训练里。这叫数据泄漏。
  训练点故意造得又少又吵——真实项目里早期数据往往就是这样,不制造这种局面,看不出过拟合。

  ② 第二步:训练;第三步:验证
  --------------------------------------------------------------------
  这里只有一个参数可调:看几个邻居(k)。挨个试一遍。

| 邻居数 | 训练误差 | 测试误差 |
|---:|---:|---:|
| k = 1 | 0.0000 | 0.0697 |
| k = 2 | 0.0654 | 0.0412 |
| k = 3 | 0.0472 | 0.0291 |
| k = 5 | 0.1021 | 0.0622 |
| k = 9 | 0.2370 | 0.2027 |
| k = 15 | 0.5160 | 0.4269 |

  k=1 时训练误差是 0——每个点最近的邻居就是它自己,预测值完全等于真实值。
  但它的测试误差是所有 k 里最高的。 换一批没见过的点,它一个都猜不准。
  这就是过拟合:把噪声也背下来了。
  k 挪到 3,测试误差降到最低。
  再往上调,训练误差和测试误差一起涨——k=15 的时候两个都到了 0.4 以上,这叫欠拟合:模型太笨,
  学不到形状。
  所以「调参」调的就是这个平衡:太小记噪声,太大看不见形状。

  ④ 过拟合长什么样
  --------------------------------------------------------------------
  k=1 在训练集上的预测值,前五个:
    x=0.00  真实 0.4506  预测 0.4506
    x=0.20  真实 0.3479  预测 0.3479
    x=0.40  真实 0.9685  预测 0.9685
    x=0.60  真实 1.2340  预测 1.2340
    x=0.80  真实 1.1249  预测 1.1249
  一模一样。这不是模型厉害,是它把每个点的噪声都记住了。换一个没见过的 x,它就抓瞎。
  k=3 同样五个点:
    x=0.00  真实 0.4506  预测 0.5890
    x=0.20  真实 0.3479  预测 0.5890
    x=0.40  真实 0.9685  预测 0.8501
    x=0.60  真实 1.2340  预测 1.1091
    x=0.80  真实 1.1249  预测 1.1008
  有偏差,但偏差不大。它学到的是趋势,不是每个点。

  ⑤ 所以「训练得好」不等于「模型好」
  --------------------------------------------------------------------

| 情况 | 训练误差 | 测试误差 | 叫什么 |
|---|---|---|---|
| k 太小 | 很低(接近 0) | 很高 | 过拟合:把噪声背下来了 |
| k 合适 | 中等 | 最低 | 刚好 |
| k 太大 | 高 | 高 | 欠拟合:模型太笨 |

  这三行的判断标准很简单:训练误差低、测试误差高 = 过拟合;两个都高 = 欠拟合。
  教材 4.2 到 4.4 三节讲的就是这个流程:准备数据、建模、上线。
  上线之后还要继续看测试误差——因为真实世界会变。

第一段那两个「故意」很重要:训练点带噪声、测试点落在中间。不制造这种局面,过拟合根本显现不出来。 第二段那张表的形状是标准的:训练误差随 k 单调上升,测试误差先降后升。两条线中间那块就是「刚好」。 第三段 k=1 那五个「一模一样」的预测值,是过拟合最直观的样子:它不是在预测,是在复述。 最后一段提到测试误差比训练误差还低。这在真项目里几乎不会出现,这里是因为测试集没噪声。 看到这种反常要回去查数据,不要当成好事。

30 秒实验

  1. 把训练点的噪声从 ±0.5 改成 ±0.1,重跑,看最优 k 往哪边挪。(噪声小,k 可以更小。)
  2. 把训练点从 20 个减到 8 个,重跑,看最优 k 变成多少。
  3. 把测试点改成和训练点重合,看训练误差和测试误差是不是变得一样——这就是「数据泄漏」的极端例子。

常见误区

  1. 「训练误差 0 说明模型完美。」训练误差 0 通常说明过拟合,尤其是数据带噪声的时候。
  2. 「测试集随便用,反正数据多。」用一次就污染一次。反复在测试集上调参,等于把测试集变成了训练集。
  3. 「模型上线就完事了。」真实世界会变,模型会漂。不监控的模型等于没有模型。
自测三问(点开对答案) 1. 为什么 k=1 的训练误差一定是 0? —— 因为每个训练点距离自己最近,邻居就是自己,预测值直接等于真实值。 这恰恰说明它没学到规律,只是记住了答案。 2. 训练误差和测试误差都高,是过拟合还是欠拟合? —— 欠拟合。模型太笨,连训练集都拟合不好,更别说新数据。过拟合的特征是训练误差低、测试误差高。 3. 为什么「先洗数据再切训练测试集」是错的? —— 因为洗数据用的统计量(均值、方差、分位数)包含了测试集的信息,等于提前把答案漏给了模型。 要先切再各自算。

下一篇讲为什么加一层隐藏层,某些问题就突然能学了。

相关笔记