训练集上分数高不算本事,新数据上站得住才算。 这一章五种手段都在做同一件事:把训练与测试之间那道沟压小。 第一条路是加样本。同一个网络,训练集从 20 张加到 320 张,测试准确率从 0.3387 爬到 0.8900, 而训练准确率一直顶在 1.0000。 第二条路是造样本:40 张标签平移放大到 320 张,测试从 0.4863 跳到 0.7913。 第三条路是清样本:标签错四成时,测试从 0.9250 掉到 0.6787。
前五章都在讲「网络怎么搭」:卷积、池化、RBM、自编码器。 这一章换个方向,讲「搭好了怎么让它在新数据上管用」。 这一章的五种手段有一个共同点:都不改网络的核心结构。 改的是喂进去的数据、算子的形状、以及训练时的随机性。 这也意味着它们能叠在任何一个模型上,包括下一章要讲的现成框架。
一、先看那道沟
先说清「泛化」这个词。 模型在训练集上答对,只能说明它记住了这些题;在新数据上答对,才说明它学会了规律。 记住和学会,是两件事。 这一节的网络是 64 到 32 到 16 到 10,一共 2704 个参数。 训练集给 400 张图,平均每个参数摊到不到 0.15 张。 参数比样本多这么多,网络完全有能力「背下」训练集——它也确实背下来了。 衡量这件事的指标叫「沟」:训练准确率减去测试准确率。 沟小,说明两边表现一致;沟大,说明模型在训练集上表现好只是因为见过。 这一章所有的手段,目标都是把这个沟压小。
二、第一条路:加样本
最直接的办法就是多给数据。 固定网络结构和训练步数,只改训练集的张数:
| 训练张数 | 训练准确率 | 测试准确率 |
|---|---|---|
| 20 | 1.0000 | 0.3387 |
| 40 | 1.0000 | 0.4688 |
| 80 | 1.0000 | 0.6562 |
| 160 | 1.0000 | 0.8700 |
| 320 | 1.0000 | 0.8900 |
| 20 张时训练准确率就到 1.0000,测试只有三成三——这是过拟合最典型的样子。 | ||
| 张数一路加上去,训练准确率一格没动,测试准确率从三成三爬到八成九。 | ||
| 注意最后两行:160 张 0.8700,320 张 0.8900,只差两个点。 | ||
| 光靠加数据也到了天花板。 | ||
| 剩下的差距要靠别的手段补,这就是本章后面四节要讲的东西。 |
三、第二条路:造样本
标注是有成本的:一张图可能要人看几秒到几分钟。 但如果你已经知道「某种变化不该改变答案」,就可以自己造样本。 数字挪一格还是同一个数字,这个规律不用问别人。 手里只有 40 张带标签的图,靠平移和加噪点变出多个版本:
| 做法 | 等效样本数 | 测试准确率 |
|---|---|---|
| 只训 40 张,不增强 | 40 | 0.4863 |
| 平移加减 1 像素,放大 4 倍 | 160 | 0.6637 |
| 平移加减 1 像素,放大 8 倍 | 320 | 0.7913 |
| 平移加减 2 像素加 2 个噪点,放大 8 倍 | 320 | 0.5000 |
| 同样的 40 张标签,只做平移就让测试从 0.4863 跳到 0.7913。 | ||
| 这是整章性价比最高的一步:不需要新数据,只需要你懂任务。 | ||
| 但最后一行往下掉了将近三成。 | ||
| 多加噪点并没有帮上忙,因为噪点不属于「已知不该改答案的变化」——它只是把图弄脏, | ||
| 等于教模型认脏图。 | ||
| 增强的前提是「你确定这个变换保标签」,不确定就别加。 |
四、第三条路:清样本
现实中标签也会错:人工标注会看走眼,众包会有人乱点。 下面故意把一部分标签随机改成别的数字,看模型被带偏多少:
| 标签错的比例 | 训练准确率 | 测试准确率 |
|---|---|---|
| 0.00 | 1.0000 | 0.9250 |
| 0.05 | 0.9575 | 0.8850 |
| 0.10 | 0.9125 | 0.8538 |
| 0.20 | 0.8075 | 0.7800 |
| 0.40 | 0.6600 | 0.6787 |
| 训练准确率这一列是拿「被打乱的标签」算的,所以它自己先塌。 | ||
| 这不是模型变差了,是我们在用错答案给自己打分。 | ||
| 测试准确率才是真账:标签错四成时,从 0.9250 掉到 0.6787,掉了二十多个百分点。 | ||
| 而且这个损失是加数据补不回来的——错标签越多,你需要的新数据越多。 | ||
| 所以清洗标签的回报很高,尤其是小数据集。 | ||
| 它也是最容易被忽略的一条:很多人宁愿去调网络结构,也不愿意回头看一眼标注。 |
五、动手 Demo:三条路各跑一遍:加样本、造样本、清样本
数据用七段数码管的仿制图:8 乘 8 像素,10 个数字。
训练集 400 张(每个数字 40 张,只做平移),测试集另做 800 张(多敲一个噪点)。
代码里的 fit 和 acc 就是第 2 章那两个函数,这里不再重复贴。
def make_data(n=40, jitter=1, noise=0, seed=0):
u"""每个数字 n 张;随机平移 jitter 格,再随机敲掉 noise 个像素。"""
rg = np.random.default_rng(seed)
X, y = [], []
for k in range(10):
base = one_digit(k)
for _ in range(n):
img = base.copy()
if jitter:
dr, dc = rg.integers(-jitter, jitter + 1, 2)
img = np.roll(np.roll(img, dr, 0), dc, 1)
x = img.ravel().copy()
if noise:
idx = rg.choice(64, noise, replace=False)
x[idx] = 1.0 - x[idx]
X.append(x)
y.append(k)
return np.array(X), np.array(y, dtype=int)
Xtr, ytr = make_data(40, jitter=1, noise=0, seed=0) # 400 张训练
Xte, yte = make_data(80, jitter=1, noise=1, seed=99) # 800 张测试,另做一批
# 第一条路:只改训练张数,结构和步数都不动
for N in (20, 40, 80, 160, 320):
pick = np.random.default_rng(31).choice(400, N, replace=False)
m = fit(Xtr[pick], ytr[pick], steps=1500, seed=1)
print(N, acc(Xtr[pick], ytr[pick], m), acc(Xte, yte, m))
# 第二条路:拿 40 张,靠平移和噪点变出 320 张
pick = np.random.default_rng(31).choice(400, 40, replace=False)
base_X, base_y = Xtr[pick], ytr[pick]
for mult, jit, noi in ((4, 1, 0), (8, 1, 0), (8, 2, 2)):
XX, yy = [], []
rg = np.random.default_rng(77)
for _ in range(mult):
for xa, ya in zip(base_X, base_y):
img = xa.reshape(8, 8).copy()
dr, dc = rg.integers(-jit, jit + 1, 2)
img = np.roll(np.roll(img, dr, 0), dc, 1)
v = img.ravel().copy()
if noi:
ii = rg.choice(64, noi, replace=False)
v[ii] = 1.0 - v[ii]
XX.append(v)
yy.append(ya)
m = fit(np.array(XX), np.array(yy), steps=1500, seed=2)
print(len(XX), acc(Xte, yte, m))
# 第三条路:故意把一部分标签改错
for rate in (0.0, 0.05, 0.10, 0.20, 0.40):
rg = np.random.default_rng(41)
yb = ytr.copy()
nbad = int(rate * len(yb))
bad = rg.choice(len(yb), nbad, replace=False)
yb[bad] = rg.integers(0, 10, nbad)
m = fit(Xtr, yb, steps=1500, seed=3)
print(rate, acc(Xtr, yb, m), acc(Xte, yte, m))
实跑输出:
--------------------------------------------------------------------
训练样本:加样本、造样本、清样本
--------------------------------------------------------------------
[1] 泛化问题:训练集上分数高,不等于在新数据上好用
网络有 2704 个参数,训练集给 400 张图
参数一多,它就有能力「背下」训练集而不学规律
测试集另做 800 张,同样平移一格再敲一个噪点,比训练集略难
这一节看三个办法:加样本、造样本(数据增强)、清样本(去标签噪声)
[2] 第一条路:加样本
固定网络结构和训练步数,只改训练集的张数:
训练张数 训练准确率 测试准确率
20 1.0000 0.3387
40 1.0000 0.4688
80 1.0000 0.6562
160 1.0000 0.8700
320 1.0000 0.8900
20 张时训练准确率就到 1.0000,测试只有三成三——这就是过拟合的样子
张数上去之后,训练准确率一直顶在 1.0000,测试准确率却从三成三爬到八成九
最后两行几乎一样,说明光靠加数据也到了天花板,剩下的要靠别的手段
所以「加数据」是提高泛化最有效的手段,没有之一
[3] 第二条路:造样本(数据增强)
手里只有 40 张带标签的图,但可以拿每张图变换出多个版本:
平移一两个像素、随机翻转几个像素,标签保持不变
下面对比四种量级的增强:
做法 等效样本数 测试准确率
只训 40 张,不增强 40 0.4863
平移加减 1 像素,噪点 0 个,放大 4 倍 160 0.6637
平移加减 1 像素,噪点 0 个,放大 8 倍 320 0.7913
平移加减 2 像素,噪点 2 个,放大 8 倍 320 0.5000
同样的 40 张标签,只平移就够用了:测试准确率从四成八跳到七成九
增强的本质是把「你已知的规律」写成数据:数字挪一格还是同一个数字
但最后一行比上一行掉了将近三成——多加噪点并没有帮上忙
噪点不是「已知不该改答案的变化」,它只是把图弄脏,等于教模型认脏图
[4] 第三条路:清样本(标签噪声)
现实中标签也会错。下面故意把一部分标签随机改掉,看模型被带偏多少:
标签错的比例 训练准确率 测试准确率
0.00 1.0000 0.9250
0.05 0.9575 0.8850
0.10 0.9125 0.8538
0.20 0.8075 0.7800
0.40 0.6600 0.6787
训练准确率已经按「被打乱的标签」算,所以它自己先塌下去
测试准确率才是真账,它掉得慢一些,但一路往下
错四成标签时训练准确率只剩六成六,测试从九成二掉到六成八
结论很朴素:清洗标签比换模型更划算,尤其是小数据集
[5] 三条路的账
加样本:最有效,但最贵——一张图可能要好几分钟标注
造样本:最便宜,但只在你「已经知道什么变化不该改变答案」时才成立
清样本:最容易被忽略,回报却很高,尤其标签是人手工打的时候
下一节讲预处理:不改数据量,只改数据的「摆放方式」,也能明显改善训练
第一段说明两件事。
一是 make_data 的 jitter 和 noise 两个参数能独立控制「平移」和「敲点」;
二是测试集用另一个种子(99)另做一批,而不是从训练集里切——这样测试集才算真正没见过。
第二段那张表最值得盯的是训练准确率那一列:五行全是 1.0000。
这说明「加样本」并没有让模型在训练集上变差,它只是让模型没机会死记。
换个说法:数据一多,模型想背也背不完。
第三段最后一行掉到 0.5000,这是个反例。
平移 2 格加 2 个噪点,看起来「增强得更狠」,实际上是让标签和图像对不上了:
数字 1 平移两格可能撞上数字 7 的形状,噪点多了也可能把 3 变成 8。
增强的强度不是越大越好,得贴着真实变化来。
30 秒实验
- 把测试集的
noise从 1 改成 0 再跑一遍。 所有准确率都会涨,而且「加样本」那条曲线会更早饱和——因为测试集变简单了,天花板变低了。 - 把训练集的
jitter从 1 改成 2,再看纯平移的增强还有没有用。 会有用,但幅度变小:模型见过的位移范围已经够宽了。 - 把标签噪声那一组的种子从 41 换成别的。 曲线的形状会变,但「错得越多掉得越多」这个趋势不会变。
常见误区
- 「训练准确率 100% 说明模型很好。」 恰恰相反,在小数据集上这是个危险信号。 这一节的 20 张那组训练准确率就是 1.0000,测试只有 0.3387。 要看的是测试准确率,以及训练减测试那道沟。
- 「数据增强越猛越好,能变出十倍样本。」 实测把平移加到 2 格、再加 2 个噪点,测试从 0.7913 掉到 0.5000。 增强的每一种变换都得满足「标签不变」,否则你是在往训练集里灌错标签。
- 「标签错一点没关系,模型能自己纠回来。」 错 20% 时测试已经掉到 0.7800,错 40% 掉到 0.6787。 模型不会纠错,它只会老老实实拟合你给的答案。
自测三问(点开对答案)
1. **为什么「加样本」比「调结构」更有效?** —— **因为过拟合的根源是「参数能表达的东西比数据能约束的多」。** **调结构是在减少参数能表达的东西,加样本是在增加约束。** **后者不损失容量,所以上限不会掉,这才是它更有效的原因。** 2. **训练准确率用「被打乱的标签」算,为什么会塌?** —— **因为答案本身就是矛盾的:同一张图,这次标 3,下次标 7。** **模型再强也学不会这种矛盾,只能学到一个折中的输出,于是对任何一份标签都只有部分正确。** 3. **数据增强的「标签不变」这个前提,怎么判断?** —— **问一个具体问题:这个变换做完,人还能一眼认出是同一个东西吗?** **数字左右翻转就不行(6 会变 9),数字平移一格就行。** **靠常识判断,不要靠「多增强总没坏处」的直觉。**三条路都在改「喂什么」。 下一节改另一件事:数据的「摆放方式」——同样的像素,减个均值、除个标准差、白化一下, 训练难度会明显不同。
- ☐ (本节完,下一节 6.2 预处理)