📗 第5章 自编码器
📄 一句话概括

第 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 秒实验

  1. 把 beta 从 0.05 改成 0,再看激活率那一行。 它会跳回 0.54 附近——一行参数之差,隐层的用法完全不同。
  2. 把降噪那一行的训练噪声改成 0.30,看误差怎么变。 会从 0.0630 涨到 0.08 附近,说明噪声强度过头了。
  3. 把栈式的第一层从 32 改成 48,再看 16 维输出的分类准确率。 中间表示更宽,压到 16 时丢的信息少一些,准确率会略升。

常见误区

  1. 「四种自编码器是四个独立的知识点。」 不是。它们的网络主体完全一样,差别只在训练目标。 理解成「一个模型、四种压力」,比记四个定义有用得多。
  2. 「重构误差低就说明表示好。」 恒等映射的重构误差可以接近 0,但表示毫无用处。 降噪自编码器在干净输入上的重构误差反而比普通自编码器高,但它的表示明显更抗噪。 评估表示要看下游任务,不是看重构。
  3. 「预训练已经过时了,现在都是端到端。」 作为「让深层网络训得动」的手段,它确实被 ReLU、批归一化、残差连接替代了。 但作为「用无标签数据换标注成本」的思路,它比任何时候都流行——今天的预训练语言模型就是它。
自测三问(点开对答案) 1. **四种自编码器的共同点是什么?** —— **网络主体完全一样:编码器加解码器,损失是重构误差。** **差别只在「怎么给训练加压力」:改输入(降噪)、改损失(稀疏)、改层数(栈式)。** **理解成「一个模型、四种压力」最省事。** 2. **第 4 章和第 5 章最大的区别是什么?** —— **编码是不是随机的。** **RBM 要按概率采样,所以只能近似梯度(对比散度)、要讲能量和配分函数。** **自编码器是确定函数,可以直接精确反传,不需要概率那一套。** **代价是丢掉了「数据分布」这个解释。** 3. **「预训练给起跑线不给终点线」对实际项目意味着什么?** —— **意味着预训练适合「算力紧、标签少、要快速出结果」的场景。** **如果算力充足、标签够多、追求最终精度,端到端训练可能更省事。** **判断之前先问自己:我缺的是时间,还是上限?**

到这儿,网络结构和训练套路都讲完了。但还有一个问题没解决: 为什么有的模型在训练集上满分、一到新数据就崩? 第 6 章讲提高泛化能力的方法:更多数据、更好的预处理、换激活函数、Dropout、DropConnect。 那些方法才是今天真正天天在用的东西。

相关笔记