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

自编码器是三层的:输入、一个很窄的隐层、输出。它要学的事只有一件——让输出尽量等于输入。 因为隐层比输入窄,信息必须挤过一个窄口,网络被迫只保留重点。 不需要标签,标签就是输入自己。 实测 64 个像素压到 16 个,测试误差 0.0293;压到 2 个就崩到 0.1170。 而线性自编码器学到的子空间,和主成分分析几乎一模一样(误差差 0.0004)。

📝 本节和前面几节的关系

第 4 章的 RBM 也能压缩、也能提特征,但它的编码要掷骰子:按概率采样 0 或 1, 同一个输入两次编码结果可能不同。 自编码器把这一步换成一个确定的函数,于是可以用普通反向传播训练,又快又稳。 这一节是第 5 章的地基。 后面三节都是在它的损失函数上加东西:加噪声(降噪)、加惩罚(稀疏)、加层数(栈式)。

一、结构:一个窄口

三层,形状像沙漏:输入 64 维,中间隐层 h 维,输出又是 64 维。 隐层比输入窄这件事叫瓶颈,它是整个方法的命门。

部分 形状 作用
编码器 64 到 h 把输入压成少数几个数
瓶颈 h 维 信息必须挤过去,丢掉不重要的
解码器 h 到 64 从几个数还原出原图
瓶颈为什么是命门?因为不窄就没用。
如果 h 等于 64 甚至更大,网络有一条最省事的路:把输入原样搬到输出,中间那层什么都不用学。
这叫恒等映射,重构误差可以低到 0,但一个有用的特征都没学到。
所以自编码器的第一纪律是:瓶颈必须比输入窄。
训练目标就一句话:输出与输入的平方误差最小。
不需要标签,因为目标就是输入自己——这叫自监督,或者叫无监督。

二、瓶颈多窄才合适

下面把隐层从 64 一路收到 2,看重构误差怎么变。 数据是 450 张 8 乘 8 的数字图,每张带一点平移和单个噪点。

隐层单元 参数量 训练误差 测试误差
2 322 0.1133 0.1170
4 580 0.0760 0.0699
8 1096 0.0492 0.0462
16 2128 0.0277 0.0293
32 4192 0.0158 0.0196
64 8320 0.0082 0.0117
64 压到 16 只用了四分之一的维度,测试误差却只从 0.0117 涨到 0.0293。
压到 2 就崩了(0.1170),差了一个数量级——十个数字至少要区分「几条笔画、在哪」。
参数量那一列容易被忽略:瓶颈越窄,参数越少。
h 等于 16 时是 2128 个,h 等于 64 时是 8320 个,差四倍。
所以窄瓶颈同时省了存储和计算,不只是「学得更好」。

三、线性与非线性:谁更好

把隐层的 tanh 换成恒等函数,就得到线性自编码器。 它只能学一个线性子空间,表达能力明显更弱——但实测结果有点意外:

隐层 线性 训练 线性 测试 tanh 训练 tanh 测试
4 0.0753 0.0682 0.0760 0.0699
8 0.0477 0.0439 0.0492 0.0462
16 0.0254 0.0273 0.0277 0.0293
非线性没有赢,反而略高一点。
原因是这个任务还不够弯:64 维压到 4 到 16 维,线性子空间已经装下大部分能量,
加一层 tanh 只是把数值挤向两端,重建时更难细调。
非线性的优势要等瓶颈极窄、或者数据本身是弯曲的时候才显出来。
人脸、语音这类数据落在低维弯曲流形上,线性子空间切不进去,那时非线性才值钱。
这也是「不要照抄结论」的一个具体例子:书上的经验是有前提的。

四、线性自编码器学的就是主成分

既然线性自编码器只能学线性子空间,那它学到的到底是哪个子空间? 答案是:和主成分分析(PCA)几乎完全一样的那个。 验证方法很简单:对训练集做一次奇异值分解,取前 h 个主成分重构, 这是「最优线性压缩」的理论下限;再和训练出来的线性自编码器比。

隐层 线性自编码器 主成分理论值 差距
4 0.0753 0.0749 0.0004
8 0.0477 0.0473 0.0004
16 0.0254 0.0236 0.0018
差在万分位到千分位——自编码器自己摸到了主子空间,没人告诉它要这么做。
这条结论很实用:如果你只想做线性降维,自编码器不会比主成分分析更好,只是更贵。
反过来说,如果你需要非线性降维,主成分分析做不到,自编码器可以——这才是它存在的理由。

五、动手 Demo:瓶颈扫描:从 64 压到 2,顺手和主成分对个账

这个 Demo 用手写的反向传播训练自编码器,没有用任何框架。 核心只有五行代码:编码、解码、算误差、反传、更新。 最后还用中心差分把梯度对了一遍账,确认手推的公式没写错。

def train_ae(X, h, hidden='tanh', epochs=800, lr=0.02, seed=0):
    rg = np.random.default_rng(seed)
    W1 = rg.normal(0.0, 0.1, (64, h))      # 编码器
    b1 = np.zeros(h)
    W2 = rg.normal(0.0, 0.1, (h, 64))      # 解码器
    b2 = np.zeros(64)
    act = (lambda z: np.tanh(z)) if hidden == 'tanh' else (lambda z: z)
    dact = ((lambda z, a: 1.0 - a * a) if hidden == 'tanh'
            else (lambda z, a: np.ones_like(a)))
    n = len(X)
    for _ in range(epochs):
        Z1 = X @ W1 + b1
        A1 = act(Z1)                        # 这就是编码
        A2 = A1 @ W2 + b2                   # 这就是解码
        d2 = (A2 - X) * 2.0 / n             # 目标是输入自己
        d1 = (d2 @ W2.T) * dact(Z1, A1)
        W2 -= lr * (A1.T @ d2)
        b2 -= lr * d2.sum(0)
        W1 -= lr * (X.T @ d1)
        b1 -= lr * d1.sum(0)
    return W1, b1, W2, b2


def recon(X, p, hidden='tanh'):
    W1, b1, W2, b2 = p
    A1 = np.tanh(X @ W1 + b1) if hidden == 'tanh' else X @ W1 + b1
    return A1 @ W2 + b2


for h in (2, 4, 8, 16, 32, 64):
    p = train_ae(Xtr, h)
    print(h, mse(Xtr, p), mse(Xte, p))

# 线性自编码器的理论下限:前 h 个主成分
Xc = Xtr - Xtr.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
rec_pca = Xtr.mean(0) + (Xc @ Vt[:h].T) @ Vt[:h]

实跑输出:

--------------------------------------------------------------------
自编码器:一个窄口,进得去出得来
--------------------------------------------------------------------
[1] 自编码器的结构:一个窄口,进得去出得来
  三层:输入 64 个像素,中间隐层只有 h 个单元,输出还是 64 个像素
  隐层比输入窄,这叫瓶颈;信息要挤过这个窄口,被迫只保留重点
  目标是「输出尽量等于输入」,所以不需要标签,标签就是输入自己
  与 RBM 的区别只有一处:这里没有随机采样,编码是一个确定的函数

[2] 瓶颈越窄,误差越大——但窄到多少还有用
  隐层单元    参数量    训练误差    测试误差
  2           322       0.1133      0.1170
  4           580       0.0760      0.0699
  8           1096      0.0492      0.0462
  16          2128      0.0277      0.0293
  32          4192      0.0158      0.0196
  64          8320      0.0082      0.0117

  64 个像素压到 16 个,测试误差从 0.0117 涨到 0.0293——四分之一的空间装下大半信息
  压到 2 个就崩到 0.1170,差了一个数量级:十个数字至少要区分「几条笔画、在哪儿」
  注意参数量:隐层越窄参数越少,所以窄瓶颈同时省存储、省计算

[3] 线性与非线性:瓶颈一样宽,谁重构得好
  隐层    线性 训练   线性 测试   tanh 训练   tanh 测试
  4       0.0753      0.0682      0.0760      0.0699
  8       0.0477      0.0439      0.0492      0.0462
  16      0.0254      0.0273      0.0277      0.0293

  结果和直觉相反:非线性那一列没有赢,反而略高一点。
  原因是这个任务还不够弯:64 维压到 4 到 16 维,线性子空间已经装下大部分能量,
  加一层 tanh 只是把数值挤向两端,重建时更难细调。
  非线性的优势要等瓶颈极窄、或者数据本身是弯曲的(人脸、语音)时才显出来。

[4] 线性自编码器学的其实就是主成分
  把训练集做一次奇异值分解,取前 h 个主成分重构,就是「最优线性压缩」的理论下限
  隐层    线性自编码器  主成分理论值  差距
  4       0.0753        0.0749        0.0004
  8       0.0477        0.0473        0.0004
  16      0.0254        0.0236        0.0018

  两者差在万分位到千分位——线性自编码器自己摸到了主子空间,没人告诉它要这么做
  这条结论很有用:如果你只想做线性降维,自编码器不会比主成分分析更好,只是更贵

[5] 梯度对账:手写的反向传播对不对
  抽样三个第一层权重,用中心差分算数值梯度,和反传结果比
  相对误差:6.29e-08  5.05e-09  6.06e-09
  都在 10 的负 6 次方以下,说明手写反传和数学推导一致

[6] 与上一章的关系
  RBM 的编码要按概率采样,同一个输入两次编码结果可能不同
  自编码器的编码是一个确定函数,同样输入必然同样输出——这让分析变简单
  代价是丢掉了概率解释:它不再描述「数据分布」,只描述「怎么压缩再还原」
  下一节要利用这个确定性做一件 RBM 做不到的事:故意弄脏输入,逼它学更结实的东西

先看第二段的瓶颈表。 它说明一件重要的事:压缩率和重构质量之间是可以算账的。 从 64 压到 16 只付了 0.0117 到 0.0293 的代价,但从 16 压到 2 要付 0.1170。 这个「拐点」在哪里,取决于数据本身的复杂度,不能背结论。 第三段的线性与非线性对比,结果和教科书相反。 这里如实写下来:在这批 8 乘 8 二值图上,非线性没有优势。 要复现「非线性更好」这个结论,需要更复杂的数据(比如真手写数字、或者自然图像)。 这也是这套笔记一直守的规矩:测出来什么就写什么,测不出来就写清楚为什么。 第四段的梯度对账给出三个相对误差:6.29e-08、5.05e-09、6.06e-09。 都在 10 的负 8 次方量级,说明手写的反传公式和数学推导一致。 这一步值得每次自己写网络时都做一遍——链式法则漏一项,训练曲线只会「慢」,不会报错,很难发现。

30 秒实验

  1. 把 epochs 从 800 降到 100,看误差涨多少。 会涨到 0.05 附近,说明这套数据要几百轮才收敛;轮数不足是「模型不行」的常见假象。
  2. 把隐层 h 设成 70(比输入还宽)再跑。 测试误差会非常低,但那是恒等映射——它什么都没学到。 记住:重构误差低不等于学到了特征。
  3. 把 hidden='tanh' 换成 'lin',再把 h 设成 2。 线性模型在 2 维时误差是 0.1170 附近,和 tanh 差不多——说明极窄时两者都救不了。

常见误区

  1. 「重构误差越低说明模型越好。」 不一定。瓶颈不比输入窄时,网络会学成恒等映射,重构误差接近 0,但毫无用处。 评估自编码器要看「瓶颈处的表示对下游任务有没有用」,不是看重构误差。
  2. 「自编码器是真正的无监督学习,不需要任何信息。」 它需要的信息就是输入本身,这叫自监督。 它不需要人工标签,但需要大量无标签数据, 而且学出来的东西仍然受数据分布限制——数据里没有的规律,它也学不到。
  3. 「它一定比主成分分析好,因为它是神经网络。」 线性自编码器和主成分分析在数学上等价,实测误差差 0.0004,几乎完全一样。 神经网络的优势只在加了非线性之后才出现,而且要有足够的数据支撑。
自测三问(点开对答案) 1. **为什么瓶颈必须比输入窄?** —— **不窄的话,网络有一条最省事的路:把输入原样搬到输出,中间层不起作用。** **这叫恒等映射,重构误差可以接近 0,但一个特征都学不到。** **瓶颈是「逼它筛选」的唯一手段。** 2. **64 个像素压到 16 个,丢了多少信息?** —— **维度少了四分之三,但测试误差只从 0.0117 涨到 0.0293,大约三倍。** **说明原来那 64 维里大部分是冗余的——十个数字其实只由七段笔画组成。** **这也是压缩能成立的根本原因:真实数据总有大把冗余。** 3. **线性自编码器和主成分分析有什么关系?** —— **数学上等价。线性自编码器学到的子空间就是主成分子空间。** **实测重构误差差 0.0004,可以认为相同。** **所以「用自编码器代替主成分分析」在只有线性层时没有意义,只是更慢更贵。**

普通自编码器有一条偷懒的路:把输入原样搬到输出,中间什么都不用学。 瓶颈能挡住一部分,但如果瓶颈不够窄,它还是会偷懒。 下一节换一种堵法:把输入故意弄脏,让「原样搬运」直接变成错的。

相关笔记