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

栈式自编码器把自编码器一层层摞起来:第一层的隐单元当第二层的输入,逐层预训练。 训练规矩和第 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 秒实验

  1. 把第二层改成 8 维再跑一遍。 重构误差会涨到 0.03 以上,分类准确率也会明显掉——8 维分 10 类,瓶颈太窄。
  2. 把逐层预训练的 epochs 从 900 降到 100,再整体微调。 起点差了,微调也很难补回来,这正是 5.5 节要展开的「起跑线」问题。
  3. 把微调的目标从重构误差换成分类交叉熵,再看分类准确率。 它会从 0.8600 升到 0.90 以上——同一批权重,换个目标就是另一个结果。

常见误区

  1. 「层数越多越好。」 实测 16 维两层 0.8600,还不如 32 维单层 0.9200。 层数增加带来抽象,但也带来信息损失和训练难度。 判断标准只能是下游任务的表现,不能是层数。
  2. 「自编码器的解码器没用,训练完就丢。」 它有两个用处:一是逐层预训练后能拼回去做整体微调; 二是它本身就是个生成器,可以拿随机编码生成新样本。 只有当下游任务是纯判别任务时,才确实可以对它不管。
  3. 「预训练完就差不多了,微调可有可无。」 第 4 章 DBN 的实测里,微调把测试准确率从 0.8929 推到 0.9732。 预训练只保证「每层能描述自己的输入」,不保证「这些描述对任务有用」。 微调是把这个差距补上的一步。
自测三问(点开对答案) 1. **逐层训练时为什么必须冻结下面几层?** —— **不冻结的话,练上层会把下层也改掉,而下层一改,上层面对的输入又变了。** **两层互相追着改,训练就不收敛。** **冻结是把「学表示」拆成互不干扰的小问题,每个都能单独收敛。** 2. **两层堆叠的重构误差 0.0174,和单层的 0.0171 差不多,说明什么?** —— **说明「堆叠」的好处不在重构精度,而在中间表示的抽象程度。** **重构是个很容易达成的目标,单层只要够宽就能做到;** **真正难的是「学到一个对下游有用的表示」。** **这也是为什么评估自编码器不能只看重构误差。** 3. **用重构损失微调,为什么分类反而变差?** —— **因为优化目标和任务目标不一致。** **重构要求「保住所有像素细节」,分类要求「抓住区分类别的特征」。** **两者大部分重叠,但细节上有冲突;继续优化重构,就会把权重往「保细节」的方向拉。**

能叠起来之后,下一个问题就有意义了:这套「先无标签预训练、再有标签微调」的流程, 到底能省下多少标注成本? 下一节把标签数量当成变量,从 10 张扫到 320 张,再用训练预算做第二次扫描。

相关笔记