第 5 章讲了四张自编码器,网络主体完全一样:编码器加解码器、平方误差。 变的全是「训练目标」和「训练顺序」——改输入、改损失、改层数。 贯穿全章的一个词是瓶颈:信息必须挤过窄口,才有压缩和筛选。 瓶颈不比输入窄,网络会直接学成恒等映射,什么都学不到。 与第 4 章的关系:RBM 靠概率、靠采样、靠近似梯度;自编码器靠确定函数、靠反传、靠精确梯度。 两章的微调步骤完全一样,只是积木换了。
这一节把四种自编码器的关键实验各跑一遍,并把两章逐项对照。 所有数字都能复现,改一处参数就能看到变化。 如果只记一句话,记这个: 「想让它学什么,就改损失去逼它」——这是自编码器这一类方法最实用的地方。
一、四张自编码器,四种逼法
普通自编码器靠「瓶颈窄」逼它筛选。 降噪自编码器靠「输入弄脏」逼它学结构。 稀疏自编码器靠「惩罚激活率」逼它省着用。 栈式自编码器靠「多层叠加」逼它学抽象。
| 类型 | 改了什么 | 想得到什么 |
|---|---|---|
| 普通自编码器 | 无 | 压缩本身 |
| 降噪自编码器 | 输入加噪声,目标不变 | 抗干扰的表示 |
| 稀疏自编码器 | 损失里加激活率惩罚 | 省单元、分工 |
| 栈式自编码器 | 把多层摞起来逐层训练 | 更高层的抽象 |
| 注意这四行里没有一行是「改网络结构」的(除了栈式加了层)。 | ||
| 改的都是目标函数或者训练顺序。 | ||
| 这是自编码器这类方法最灵活的地方——不需要设计新结构,改个损失就能换一种学习压力。 | ||
| 第 6 章的方法也是同一个思路的延伸:Dropout 改的是「训练时的连接」, | ||
| 权重衰减改的是「损失里加一项」,本质都是「给训练加点压力,逼出更好的解」。 |
二、这一章的实验数字
四张自编码器的关键结果列在一起,方便回头查:
| 实验 | 结果 |
|---|---|
| 普通自编码器(64 到 16) | 重构 0.0169(训练)/0.0184(测试) |
| 瓶颈扫描(h 从 2 到 64) | 测试误差 0.1170 到 0.0117 |
| 线性自编码器与主成分对比 | 误差差 0.0004,几乎完全一致 |
| 降噪与干净训练(坏 20%) | 0.0687 与 0.0984 |
| 降噪噪声扫描 | 最佳在 0.10 到 0.20,0.45 时最差 |
| 稀疏(beta 0 到 0.10) | 激活率 0.4636 到 0.0617,误差 0.0206 到 0.0609 |
| 栈式两层分类(16 维) | 0.7200 到 0.8600(不同切分下不同) |
| 预训练与随机(150 步) | 0.386 与 0.268,差 11.8 个百分点 |
| 预训练与随机(1200 步) | 0.492 与 0.488,追平 |
| 这张表里有两处要留意口径。 | |
| 一是栈式那一行,0.7200 和 0.8600 来自不同脚本、不同数据切分与随机种子,不能互相对账。 | |
| 二是所有误差都是「平方误差的均值」,不同脚本的归一化方式可能不同,跨脚本比数值要小心。 |
三、与第 4 章逐项对照
这两章讲的东西看起来差很远,其实在解决同一个问题的两侧。
| 对比项 | 第 4 章 RBM 与 DBN | 第 5 章自编码器 |
|---|---|---|
| 编码方式 | 按概率采样 0 或 1 | 确定函数,同输入同输出 |
| 理论基础 | 能量与概率分布 | 只有重构误差,没有概率 |
| 训练算法 | 对比散度近似梯度 | 直接反向传播,梯度精确 |
| 逐层预训练 | 每层一个 RBM | 每层一个自编码器 |
| 微调 | 整体反向传播 | 整体反向传播 |
| 最后一行的「微调」是两章唯一的完全一致处——套路都是「预训练给起点,反传给终点」,积木换了, | ||
| 架子没换。 | ||
| 如果只能带走一个方法论,带走这个: | ||
| 先用容易拿到的数据(无标签)把表示学出来,再用难拿到的数据(有标签)把任务对齐。 | ||
| 这套流程在 2006 年救活了深层网络,在今天是预训练语言模型的标准做法。 |
四、动手 Demo:四张自编码器同台,与第 4 章逐项对照
这个 Demo 不引入新东西,只把四种自编码器各跑一遍最小实验,看数字是否对得上。 它同时也是这套笔记的自检:哪一节改了参数,跑一遍就知道前后是否还自洽。
# 四张自编码器,各跑一遍最小实验
# 1) 普通:64 到 16 到 64
p16 = train_ae(Xtr, Xtr, 16, seed=1)
print(mse(rec(Xtr, p16), Xtr), mse(rec(Xte, p16), Xte))
# 2) 降噪:输入打坏 15%,目标仍是干净的
p_dn = train_ae(corrupt(Xtr, 0.15, seed=1), Xtr, 16, seed=2)
print(mse(rec(Xte_bad, p16), Xte), mse(rec(Xte_bad, p_dn), Xte))
# 3) 稀疏:beta 0 与 0.05 各练一次
p_s0 = train_ae(Xtr, Xtr, 16, beta=0.0, out_sig=True, seed=3)
p_s1 = train_ae(Xtr, Xtr, 16, beta=0.05, out_sig=True, seed=3)
print(enc(Xtr, p_s0, True).mean(), enc(Xtr, p_s1, True).mean())
# 4) 栈式:64 到 32 到 16,逐层训练后接分类器
pA = train_ae(Xtr, Xtr, 32, seed=4)
H1tr = np.tanh(Xtr @ pA[0] + pA[1])
pB = train_ae(H1tr, H1tr, 16, seed=5)
H2tr = np.tanh(H1tr @ pB[0] + pB[1])
实跑输出:
--------------------------------------------------------------------
小结:四张自编码器同台,与第 4 章逐项对照
--------------------------------------------------------------------
[1] 四张自编码器各跑一遍最小实验
● 普通自编码器(5.1):64 到 16 到 64
训练集重构误差 0.0169,测试集 0.0184
● 降噪自编码器(5.2):训练时输入打坏 15%,目标仍是干净的
坏 20% 的测试图上:干净训练的模型 0.0874,降噪训练的模型 0.0630
● 稀疏自编码器(5.3):给隐层激活率加惩罚
beta 0:平均激活率 0.5458,重构误差 0.0362
beta 0.05:平均激活率 0.2337,重构误差 0.0431
● 栈式自编码器(5.4):64 到 32 到 16,逐层训练
第一层输出(32 维)分类测试准确率 0.9800
第二层输出(16 维)分类测试准确率 0.7200
[2] 四种改法,改的都是同一个地方
类型 改了什么 想得到什么
普通自编码器 无 压缩本身
降噪自编码器 输入加噪声,目标不变 抗干扰的表示
稀疏自编码器 损失里加激活率惩罚 省单元、分工
栈式自编码器 把多层摞起来逐层训练 更高层的抽象
注意四者的网络主体完全一样:都是编码器加解码器,都是平方误差。
改的都是「训练目标」或者「训练顺序」,不是网络结构本身。
这是自编码器这类方法最实用的地方:想让它学什么,就改损失函数去逼它。
[3] 与第 4 章的五处对照
对比项 第 4 章 RBM 与 DBN 第 5 章自编码器
编码方式 按概率采样 0 或 1 确定函数,同输入同输出
理论基础 能量与概率分布 只有重构误差,没有概率
训练算法 对比散度近似梯度 直接反向传播,梯度精确
逐层预训练 每层一个 RBM 每层一个自编码器
微调 整体反向传播 整体反向传播
最后一行的「微调」是两章唯一的完全一致处——
套路都是「预训练给起点,反传给终点」,积木换了,架子没换。
[4] 三句话收尾
一、自编码器的全部本事来自「瓶颈」:信息必须挤过窄口,才有压缩和筛选。
瓶颈不比输入窄,网络会直接学成恒等映射,什么都学不到。
二、想让它学什么,就改损失函数。降噪改输入,稀疏改隐层,两种都不动结构。
三、预训练给的是起跑线,不是终点线。实测训练预算一宽,优势就消失了。
[5] 下一章
到这儿,网络结构和训练套路都讲完了。但还有一个问题没解决:
为什么有的模型在训练集上满分、一到新数据就崩?
第 6 章讲提高泛化能力的方法:更多数据、更好的预处理、换激活函数、Dropout。
那些方法才是今天真正天天在用的东西。
四行结果都能和前几节对上(个别数值因为切分和种子不同略有差异)。 普通自编码器 0.0169/0.0184;降噪 0.0630 对 0.0874;稀疏 0.5458 到 0.2337; 栈式 32 维 0.9800、16 维 0.7200。 稀疏那一行要特别注意:这里 beta 0.05 只把激活率从 0.5458 压到 0.2337, 而 5.3 节压到了 0.1321。 原因是这个脚本只跑了 900 轮,5.3 节跑了 1200 轮。 训练越久,惩罚累积得越多,激活率压得越低——超参数之间是耦合的。 栈式那一行的 16 维只有 0.7200,比 5.4 节的 0.8600 低。 同样是因为轮数(900 对 900,但数据切分与种子不同)和分类器训练轮数不同。 这里如实说明,不硬凑数字——跨脚本对账本来就不该期待完全一致。 最后一段的三句话是这一章的骨架。 如果只记一句,记「想让它学什么,就改损失去逼它」。 这句话在第 6 章会继续用:Dropout、权重衰减、数据增强,全都是这个思路。
30 秒实验
- 把
beta从 0.05 改成 0,再看激活率那一行。 它会跳回 0.54 附近——一行参数之差,隐层的用法完全不同。 - 把降噪那一行的训练噪声改成 0.30,看误差怎么变。 会从 0.0630 涨到 0.08 附近,说明噪声强度过头了。
- 把栈式的第一层从 32 改成 48,再看 16 维输出的分类准确率。 中间表示更宽,压到 16 时丢的信息少一些,准确率会略升。
常见误区
- 「四种自编码器是四个独立的知识点。」 不是。它们的网络主体完全一样,差别只在训练目标。 理解成「一个模型、四种压力」,比记四个定义有用得多。
- 「重构误差低就说明表示好。」 恒等映射的重构误差可以接近 0,但表示毫无用处。 降噪自编码器在干净输入上的重构误差反而比普通自编码器高,但它的表示明显更抗噪。 评估表示要看下游任务,不是看重构。
- 「预训练已经过时了,现在都是端到端。」 作为「让深层网络训得动」的手段,它确实被 ReLU、批归一化、残差连接替代了。 但作为「用无标签数据换标注成本」的思路,它比任何时候都流行——今天的预训练语言模型就是它。
自测三问(点开对答案)
1. **四种自编码器的共同点是什么?** —— **网络主体完全一样:编码器加解码器,损失是重构误差。** **差别只在「怎么给训练加压力」:改输入(降噪)、改损失(稀疏)、改层数(栈式)。** **理解成「一个模型、四种压力」最省事。** 2. **第 4 章和第 5 章最大的区别是什么?** —— **编码是不是随机的。** **RBM 要按概率采样,所以只能近似梯度(对比散度)、要讲能量和配分函数。** **自编码器是确定函数,可以直接精确反传,不需要概率那一套。** **代价是丢掉了「数据分布」这个解释。** 3. **「预训练给起跑线不给终点线」对实际项目意味着什么?** —— **意味着预训练适合「算力紧、标签少、要快速出结果」的场景。** **如果算力充足、标签够多、追求最终精度,端到端训练可能更省事。** **判断之前先问自己:我缺的是时间,还是上限?**到这儿,网络结构和训练套路都讲完了。但还有一个问题没解决: 为什么有的模型在训练集上满分、一到新数据就崩? 第 6 章讲提高泛化能力的方法:更多数据、更好的预处理、换激活函数、Dropout、DropConnect。 那些方法才是今天真正天天在用的东西。
- ☐ (本章完,下一节 6.1 训练样本)