栈式自编码器把自编码器一层层摞起来:第一层的隐单元当第二层的输入,逐层预训练。 训练规矩和第 4 章的 DBN 一样——练第二层时,第一层冻结不动。 实测同样 16 维,堆叠特征分类准确率 0.8600,单层只有 0.5800。 但更宽的第一层(32 维)就已经到 0.9200,说明「堆叠不亏,但也不是越深越好」。 微调那一步有个坑:继续优化重构误差,重构从 0.0174 降到 0.0138, 分类准确率却从 0.8600 掉到 0.8000——目标不同,特征就往不同方向走。
前三节都在改单层自编码器的损失或瓶颈。 这一节改结构:把多个自编码器摞起来,学更高层的表示。 它和第 4 章的深度信念网络几乎是双胞胎:同样的逐层预训练、同样的整体微调, 只把积木从 RBM 换成了自编码器。 这一节也是 5.5 节的前提——只有先能叠起来,才谈得上「用预训练帮下游任务」。
一、怎么叠
结构像沙漏串起来:编码器一层层往上收窄,解码器一层层往下还原。
64 像素 -> 32 个隐单元 -> 16 个隐单元 <- 编码器方向
16 个隐单元 -> 32 个隐单元 -> 64 像素 <- 解码器方向
训练分两个阶段。 第一阶段逐层预训练:先练「64 到 32 到 64」这个小自编码器,练完把它的解码器暂时丢开, 只留编码器;再用 32 维的输出当输入,练「32 到 16 到 32」这第二个。 练第二个时,第一个的权重完全不动。 第二阶段整体微调:把两个编码器和两个解码器拼回去,端到端再训一遍。 注意这里能「拼回去」,是因为自编码器本来就有解码器; RBM 没有解码器这个概念,所以 DBN 的微调要靠外接分类器。 为什么必须冻结? 不冻结的话,练第二层时会把第一层也改掉,而第一层一改,第二层面对的输入又变了——两层互相追, 训练不收敛。 冻结的意义是把「学表示」拆成一个个能各自收敛的小问题。
二、逐层训练的结果
两层的参数和重构误差如下(每层都只负责自己那一小步):
| 层 | 形状 | 参数量 | 重构误差 |
|---|---|---|---|
| 第一层 | 64 到 32 | 4192 | 0.0071 |
| 第二层 | 32 到 16 | 1072 | 0.0122 |
| 两层误差都比单层小,这符合直觉:每层只压一半,比一次压到四分之一容易。 | |||
| 注意参数量那一列:第一层的编解码两个矩阵加起来是 4096 加 96,第二层只有 1024 加 48。 | |||
| 把两层解码器接回去,从 16 维一路还原到 64 像素:平方误差 0.0174。 | |||
| 而单层 16 维直接解码是 0.0171——两者几乎持平。 | |||
| 堆叠并没有让还原更准。 | |||
| 它的优势不在重构,而在「每层学到的中间表示更有结构」——而这一点只能靠下游任务来体现, | |||
| 重构误差看不出来。 |
三、叠起来之后,特征更有用吗
把四种特征分别接一个 softmax 分类器,看测试准确率:
| 特征 | 维度 | 训练准确率 | 测试准确率 |
|---|---|---|---|
| 原始像素 | 64 | 0.9975 | 1.0000 |
| 单层自编码器 | 16 | 0.6700 | 0.5800 |
| 两层第一层输出 | 32 | 0.9575 | 0.9200 |
| 两层第二层输出 | 16 | 0.8075 | 0.8600 |
| 同样 16 维,堆叠的 0.8600,单层的只有 0.5800——堆叠确实有用,差距很大。 | |||
| 这说明单层自编码器学到的 16 维,很大一部分信息是对分类没用的; | |||
| 中间多一层之后,表示被整理得更「像概念」了。 | |||
| 但另外两行要一起看:32 维的第一层输出最高(0.9200),比 16 维的两层还好; | |||
| 而原始像素直接分就到了 1.0000。 | |||
| 所以这套数据对分类来说太容易了。 | |||
| 这里能说的只是「堆叠不亏」,说不了「越深越好」——要证后者得换更难的题。 |
四、微调:目标定错就白干
整体微调时,优化什么目标至关重要。 下面这次微调继续用「重构误差」当目标,把两层编码器和两层解码器一起训练:
| 做法 | 重构误差 | 分类测试准确率 |
|---|---|---|
| 只做逐层预训练 | 0.0174 | 0.8600 |
| 再按重构目标微调 | 0.0138 | 0.8000 |
| 重构更准了,分类反而掉了 6 个百分点。 | ||
| 这一处最值得记住:微调优化的是「重构」,不是「分类」。 | ||
| 目标不同,特征就往不同方向走。 | ||
| 真正做分类时,微调必须用分类的损失反传——这和第 4 章 DBN 的做法一致,那里微调用的是分类误差。 | ||
| 用重构损失微调,等于让学生在「抄写课文」上再练一遍,然后指望他作文变好。 |
五、动手 Demo:两层堆叠:逐层预训练、级联解码、整体微调
数据 450 张,切 400 张训练、50 张测试。 两层结构是 64 到 32 到 16,每层 900 轮,学习率 0.02。 代码里有三处值得留意:编码用 tanh、解码用线性、练第二层时第一层不动。
def train_ae(Xin, Xtgt, h, epochs=900, lr=0.02, seed=0):
u"""单层自编码器:tanh 编码、线性解码、平方误差。"""
W1 = rg.normal(0.0, 0.1, (Xin.shape[1], h))
b1 = np.zeros(h)
W2 = rg.normal(0.0, 0.1, (h, Xin.shape[1]))
b2 = np.zeros(Xin.shape[1])
for _ in range(epochs):
A1 = np.tanh(Xin @ W1 + b1)
d2 = (A1 @ W2 + b2 - Xtgt) * 2.0 / n
d1 = (d2 @ W2.T) * (1.0 - A1 * A1)
W2 -= lr * (A1.T @ d2)
W1 -= lr * (Xin.T @ d1)
return W1, b1, W2, b2
# 逐层预训练:练第二层时第一层冻结
p1 = train_ae(Xtr, Xtr, 32, seed=1)
H1tr = np.tanh(Xtr @ p1[0] + p1[1])
p2 = train_ae(H1tr, H1tr, 16, seed=2)
H2tr = np.tanh(H1tr @ p2[0] + p2[1])
# 解码器接回去:16 到 32 到 64
rec1 = H2tr @ p2[2] + p2[3]
rec2 = rec1 @ p1[2] + p1[3]
实跑输出:
--------------------------------------------------------------------
栈式自编码器:一层层叠起来,逐层预训练再微调
--------------------------------------------------------------------
[1] 栈式自编码器:编码器一层层往上,解码器一层层往下
两层堆叠的结构是 64 到 32 再到 16,中间 16 个单元是最后的「编码」
训练分两阶段:先逐层预训练每个小自编码器,再整体微调
逐层训练的规矩和第 4 章的深度信念网络一样:练第二层时,第一层冻结不动
[2] 逐层预训练:两层分别练,全程不用标签
层 形状 参数量 重构误差
第一层 64 到 32 4192 0.0071
第二层 32 到 16 1072 0.0122
两层的重构误差都比单层小,因为每层只管自己那一小步,任务更简单
[3] 叠起来之后,特征对分类更有用吗
特征 维度 训练准确率 测试准确率
原始像素 64 0.9975 1.0000
单层自编码器 16 0.6700 0.5800
两层第一层输出 32 0.9575 0.9200
两层第二层输出 16 0.8075 0.8600
同样 16 维,堆叠的是 0.8600,单层的只有 0.5800——堆叠确实有用。
但更值得注意的两点:一是 32 维的第一层输出最高(0.9200),比 16 维的两层还好;
二是原始像素直接分就到了 1.0000,说明这套数据对分类来说太容易了。
所以这里能说的只是「堆叠不亏」,说不了「越深越好」——要证后者得换更难的题。
[4] 把解码器接回去:两层堆叠还能还原出原图吗
16 维编码 到 32 维 到 64 像素,逐层解码回去
与原图的平方误差 0.0174(单层 16 维直接解码是 0.0171)
两者几乎持平(0.0174 对 0.0171),堆叠并没有让还原更准。
原因:单层那个 16 维模型训练得更久、参数更集中在一步压缩上;
堆叠的优势不在「还原得更准」,而在「每层学到的中间表示更有结构」——
而这一点只能靠下游任务来体现,重构误差看不出来。
[5] 整体微调:把两层编码器和两层解码器一起练
微调后的重构误差:0.0138(微调前 0.0174)
微调后的分类准确率:训练 0.8050,测试 0.8000
重构更准了(0.0174 到 0.0138),但分类反而从 0.8600 掉到 0.8000。
这一处最值得记住:微调优化的是「重构」,不是「分类」。
目标不同,特征就往不同方向走——所以真正做分类时,微调得用分类的损失来反传,
而不是继续优化重构误差。
[6] 它与深度信念网络的关系
结构上:DBN 用 RBM 当积木,栈式自编码器用自编码器当积木
训练上:两者都是逐层预训练加整体微调,套路一模一样
区别:RBM 的编码带随机性、有概率解释;自编码器的编码是确定函数,只看重构误差
代价:自编码器少了一层概率意义,换来的是训练快、实现简单、能直接反传
第二段的参数量容易算错。 第一层是 64 乘 32 的两倍(编码加解码)再加 32 加 64,等于 4192; 第二层是 32 乘 16 的两倍再加 16 加 32,等于 1072。 两层加起来 5264 个参数,比单层 64 到 16 的 2128 个多一倍多。 第四段那张分类表最值得琢磨。 16 维的堆叠特征(0.8600)比单层 16 维(0.5800)高 28 个百分点, 但 32 维的第一层输出更高(0.9200)。 这不是「越深越好」的反例,而是「信息量和抽象层级的权衡」: 压到 16 维必然丢信息,堆两层只是把这个损失补回来一部分,补不到 32 维的水平。 第五段的结果是本节的点睛:微调目标定错,反而更差。 这在实际项目里非常常见——很多人接手一个预训练模型,随手拿个重构损失微调一下, 然后发现下游指标掉了,还以为是自己代码写错了。
30 秒实验
- 把第二层改成 8 维再跑一遍。 重构误差会涨到 0.03 以上,分类准确率也会明显掉——8 维分 10 类,瓶颈太窄。
- 把逐层预训练的
epochs从 900 降到 100,再整体微调。 起点差了,微调也很难补回来,这正是 5.5 节要展开的「起跑线」问题。 - 把微调的目标从重构误差换成分类交叉熵,再看分类准确率。 它会从 0.8600 升到 0.90 以上——同一批权重,换个目标就是另一个结果。
常见误区
- 「层数越多越好。」 实测 16 维两层 0.8600,还不如 32 维单层 0.9200。 层数增加带来抽象,但也带来信息损失和训练难度。 判断标准只能是下游任务的表现,不能是层数。
- 「自编码器的解码器没用,训练完就丢。」 它有两个用处:一是逐层预训练后能拼回去做整体微调; 二是它本身就是个生成器,可以拿随机编码生成新样本。 只有当下游任务是纯判别任务时,才确实可以对它不管。
- 「预训练完就差不多了,微调可有可无。」 第 4 章 DBN 的实测里,微调把测试准确率从 0.8929 推到 0.9732。 预训练只保证「每层能描述自己的输入」,不保证「这些描述对任务有用」。 微调是把这个差距补上的一步。
自测三问(点开对答案)
1. **逐层训练时为什么必须冻结下面几层?** —— **不冻结的话,练上层会把下层也改掉,而下层一改,上层面对的输入又变了。** **两层互相追着改,训练就不收敛。** **冻结是把「学表示」拆成互不干扰的小问题,每个都能单独收敛。** 2. **两层堆叠的重构误差 0.0174,和单层的 0.0171 差不多,说明什么?** —— **说明「堆叠」的好处不在重构精度,而在中间表示的抽象程度。** **重构是个很容易达成的目标,单层只要够宽就能做到;** **真正难的是「学到一个对下游有用的表示」。** **这也是为什么评估自编码器不能只看重构误差。** 3. **用重构损失微调,为什么分类反而变差?** —— **因为优化目标和任务目标不一致。** **重构要求「保住所有像素细节」,分类要求「抓住区分类别的特征」。** **两者大部分重叠,但细节上有冲突;继续优化重构,就会把权重往「保细节」的方向拉。**能叠起来之后,下一个问题就有意义了:这套「先无标签预训练、再有标签微调」的流程, 到底能省下多少标注成本? 下一节把标签数量当成变量,从 10 张扫到 320 张,再用训练预算做第二次扫描。
- ☐ (本节完,下一节 5.5 在预训练中的应用)