4. 多层感知机自编码器:引入隐藏层和激活函数
想学会非线性降维?咱们先从线性自编码器的“天花板”说起。我最早做降维时,习惯用 PCA 和线性自编码器比着玩。一开始两者结果差不多,但一碰到螺旋结构或弧形分布的数据,线性模型就彻底抓瞎了。
为什么会这样?说白了,线性变换只能做旋转、缩放、剪切,没法把卷曲的数据展平。而真实世界里,高维数据的流形大多是弯曲的。你需要一个能“折纸”的模型——这就是我们要引入隐藏层和激活函数的原因。
从线性到非线性:为什么要加隐藏层?
线性自编码器只有输入→编码层→输出,没有隐藏层。它的表达能力受限于矩阵乘法,本质上就是一个 PCA 的近似。我遇到过这样的情况:用线性自编码器压缩 784 维的 MNIST 图像到 32 维,还原出来的图像全是模糊的“平均脸”,结构信息全丢了。
加上一个隐藏层后,情况完全不同。你可以先通过隐藏层将数据映射到更高维空间(比如 512 维),再用激活函数做非线性变换,最后压缩到低维编码。这相当于让网络学会了“先展开、再折叠”的能力。
核心思想:隐藏层提供额外自由度,激活函数引入非线性,两者结合才能学习数据内在的弯曲流形。
激活函数的选择
激活函数不能乱选。我踩过最大的坑就是早期用 sigmoid 堆叠多层自编码器——梯度消失得干干净净,训练跟没训一样。
下面这个表是我在实际项目中总结的常用激活函数对比:
| 激活函数 | 输出范围 | 梯度特性 | 我用的场景 |
|---|---|---|---|
| ReLU | [0, +∞) | 正区间梯度恒为1,负区间为0 | 隐藏层首选,收敛快,稀疏性强 |
| tanh | (-1, 1) | 对称,零中心,梯度值在0~1之间 | 编码层做特征归一化时可用 |
| Sigmoid | (0, 1) | 两端饱和,梯度趋近0 | 输出层做概率映射,尽量少用在隐藏层 |
| Leaky ReLU | (-∞, +∞) | 负区间有小斜率,保留负值信息 | 防止死神经元时替换ReLU |
我曾经为了追求全正特征,把所有激活函数换成 ReLU,结果编码层输出全是 0,降维效果直接炸了。记住:编码层(瓶颈层)通常不用 ReLU,用线性或 tanh 更好。
实践:构建一个三层自编码器
咱们直接上代码。我用 PyTorch 写了一个带一个隐藏层的自编码器,用来压缩 28×28 的图片:
import torch
import torch.nn as nn
class MLPAutoencoder(nn.Module):
def __init__(self, input_dim=784, hidden_dim=256, latent_dim=32):
super().__init__()
# 编码器
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, latent_dim)
# 瓶颈层不加激活,保留线性输出
)
# 解码器
self.decoder = nn.Sequential(
nn.Linear(latent_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, input_dim),
nn.Sigmoid() # 输入归一化到 [0,1] 时用
)
def forward(self, x):
z = self.encoder(x)
x_hat = self.decoder(z)
return x_hat
# 训练循环(简化版)
model = MLPAutoencoder()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
for batch in dataloader:
x = batch.view(-1, 784)
x_hat = model(x)
loss = criterion(x_hat, x)
optimizer.zero_grad()
loss.backward()
optimizer.step()
你发现没有?我故意在瓶颈层没加激活函数。为什么?因为如果编码输出也过 ReLU,那所有负值的特征会被强行截断为零——相当于你主动丢掉了一半信息。嗯,这点很多人不注意。
非线性降维能力的验证
咱们用个简单实验验证一下。生成两个同心圆形状的二维数据(非线性可分),分别用线性自编码器和带隐藏层的 MLP 自编码器压缩到 1 维:
# 生成 Swiss Roll 型数据(非线性流形)
from sklearn.datasets import make_swiss_roll
X, _ = make_swiss_roll(n_samples=1000, noise=0.1)
# 线性自编码器压缩后的 1D 编码:只能学出 PCA 投影方向
# MLP 自编码器压缩后的 1D 编码:可以沿着流形展开
我测试下来,线性自编码器的编码值沿主方向线性拉伸,不同圈的数据混在一起;而 MLP 自编码器的编码值能按照流形的“真实顺序”排列——离原点近的编码小、远的编码大,原本螺旋折叠的结构被展平了。
这就是非线性降维的魅力:你不必假设数据是直线分布的。
下面这张图直观展示了 MLP 自编码器的结构。你可以看到数据流从输入层经过隐藏层的非线性变换,再压缩到瓶颈,最后再恢复:
我个人的调试习惯:先把隐藏层宽度设成输入维度的 2~4 倍,观察损失下降速度。如果损失降不下来,再逐层减少隐藏层尺寸,而不是一次堆太多层。
隐藏层数量和激活函数的搭配有点像调音。你堆太多层且都用 Sigmoid,训练时梯度可能消失;堆太少层,非线性表达能力不够。我建议你先从一层隐藏开始,验证非线性带来的效果提升,再逐步加深。
记住:引入隐藏层+激活函数,是自编码器从“线性工具”进化为“流形学习器”的关键一步。这一步走稳了,后面的变分自编码器、卷积自编码器才能玩得转。