自编码器是三层的:输入、一个很窄的隐层、输出。它要学的事只有一件——让输出尽量等于输入。 因为隐层比输入窄,信息必须挤过一个窄口,网络被迫只保留重点。 不需要标签,标签就是输入自己。 实测 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 秒实验
- 把
epochs从 800 降到 100,看误差涨多少。 会涨到 0.05 附近,说明这套数据要几百轮才收敛;轮数不足是「模型不行」的常见假象。 - 把隐层 h 设成 70(比输入还宽)再跑。 测试误差会非常低,但那是恒等映射——它什么都没学到。 记住:重构误差低不等于学到了特征。
- 把
hidden='tanh'换成'lin',再把 h 设成 2。 线性模型在 2 维时误差是 0.1170 附近,和 tanh 差不多——说明极窄时两者都救不了。
常见误区
- 「重构误差越低说明模型越好。」 不一定。瓶颈不比输入窄时,网络会学成恒等映射,重构误差接近 0,但毫无用处。 评估自编码器要看「瓶颈处的表示对下游任务有没有用」,不是看重构误差。
- 「自编码器是真正的无监督学习,不需要任何信息。」 它需要的信息就是输入本身,这叫自监督。 它不需要人工标签,但需要大量无标签数据, 而且学出来的东西仍然受数据分布限制——数据里没有的规律,它也学不到。
- 「它一定比主成分分析好,因为它是神经网络。」 线性自编码器和主成分分析在数学上等价,实测误差差 0.0004,几乎完全一样。 神经网络的优势只在加了非线性之后才出现,而且要有足够的数据支撑。
自测三问(点开对答案)
1. **为什么瓶颈必须比输入窄?** —— **不窄的话,网络有一条最省事的路:把输入原样搬到输出,中间层不起作用。** **这叫恒等映射,重构误差可以接近 0,但一个特征都学不到。** **瓶颈是「逼它筛选」的唯一手段。** 2. **64 个像素压到 16 个,丢了多少信息?** —— **维度少了四分之三,但测试误差只从 0.0117 涨到 0.0293,大约三倍。** **说明原来那 64 维里大部分是冗余的——十个数字其实只由七段笔画组成。** **这也是压缩能成立的根本原因:真实数据总有大把冗余。** 3. **线性自编码器和主成分分析有什么关系?** —— **数学上等价。线性自编码器学到的子空间就是主成分子空间。** **实测重构误差差 0.0004,可以认为相同。** **所以「用自编码器代替主成分分析」在只有线性层时没有意义,只是更慢更贵。**普通自编码器有一条偷懒的路:把输入原样搬到输出,中间什么都不用学。 瓶颈能挡住一部分,但如果瓶颈不够窄,它还是会偷懒。 下一节换一种堵法:把输入故意弄脏,让「原样搬运」直接变成错的。
- ☐ (本节完,下一节 5.2 降噪自编码器)