三步:切数据、训练、验证。测试集在训练时绝对不能看。 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 秒实验
- 把训练点的噪声从 ±0.5 改成 ±0.1,重跑,看最优 k 往哪边挪。(噪声小,k 可以更小。)
- 把训练点从 20 个减到 8 个,重跑,看最优 k 变成多少。
- 把测试点改成和训练点重合,看训练误差和测试误差是不是变得一样——这就是「数据泄漏」的极端例子。
常见误区
- 「训练误差 0 说明模型完美。」训练误差 0 通常说明过拟合,尤其是数据带噪声的时候。
- 「测试集随便用,反正数据多。」用一次就污染一次。反复在测试集上调参,等于把测试集变成了训练集。
- 「模型上线就完事了。」真实世界会变,模型会漂。不监控的模型等于没有模型。
自测三问(点开对答案)
1. 为什么 k=1 的训练误差一定是 0? —— 因为每个训练点距离自己最近,邻居就是自己,预测值直接等于真实值。 这恰恰说明它没学到规律,只是记住了答案。 2. 训练误差和测试误差都高,是过拟合还是欠拟合? —— 欠拟合。模型太笨,连训练集都拟合不好,更别说新数据。过拟合的特征是训练误差低、测试误差高。 3. 为什么「先洗数据再切训练测试集」是错的? —— 因为洗数据用的统计量(均值、方差、分位数)包含了测试集的信息,等于提前把答案漏给了模型。 要先切再各自算。下一篇讲为什么加一层隐藏层,某些问题就突然能学了。
- ☐ (本篇完,下一篇 4.3 深度学习:火种从哪来)