从玻尔兹曼机到深度自编码器:用PyTorch复现Hinton的降维革命
2006年,当大多数研究者还在浅层神经网络中徘徊时,Geoffrey Hinton和他的学生在《Science》上发表了一篇里程碑式的论文《Reducing the dimensionality of data with neural networks》。这篇论文不仅证明了深度神经网络能够有效学习数据低维表示,更重要的是提出了一套切实可行的训练方法——RBM预训练+微调的范式,为后续深度学习革命埋下了种子。今天,我们将用PyTorch完整复现这个经典实验,看看15年前的思想如何在现代框架中焕发新生。
1. 实验背景与技术脉络
在2000年代初,神经网络研究正处于低谷期。虽然反向传播算法理论上可以训练多层网络,但在实践中,超过三层的网络往往难以收敛。Hinton团队的突破在于发现了一种分层无监督预训练的策略:
- 受限玻尔兹曼机(RBM):作为构建块学习数据的层次化特征表示
- 逐层贪婪训练:每次只训练一个RBM层,固定下层权重后再训练上层
- 展开微调:将堆叠的RBM转换为深度自编码器后进行端到端微调
这种方法的精妙之处在于:
- 预训练阶段通过无监督学习捕获数据内在结构
- 微调阶段利用少量标注数据调整网络整体性能
- 解决了深度网络梯度消失的问题
有趣的是,Hinton在论文中提到:"当计算机足够快、数据足够大、初始权重足够好时,反向传播就能在深度网络中工作"——这正是现代深度学习成功的三个关键条件。
2. 实验环境与数据准备
我们将使用MNIST数据集复现论文中的实验,这个选择基于两个考虑:一是与原始论文保持一致,二是MNIST足够简单,便于我们聚焦于模型本身。
2.1 环境配置
首先确保安装了必要的Python包:
pip install torch torchvision numpy matplotlib然后导入所需的库:
import torch import torch.nn as nn import torch.nn.functional as F from torchvision import datasets, transforms from torch.utils.data import DataLoader import numpy as np import matplotlib.pyplot as plt2.2 数据预处理
原始论文使用了28×28的MNIST图像,我们先进行标准化处理:
transform = transforms.Compose([ transforms.ToTensor(), transforms.Lambda(lambda x: x.view(-1)), # 展平为784维向量 transforms.Normalize((0.1307,), (0.3081,)) # MNIST均值标准差 ]) train_data = datasets.MNIST('./data', train=True, download=True, transform=transform) test_data = datasets.MNIST('./data', train=False, transform=transform) batch_size = 64 train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_data, batch_size=batch_size)3. 构建RBM模块
受限玻尔兹曼机是本次实验的核心组件,让我们先实现这个关键模块。
3.1 RBM的基本原理
RBM是一种具有可见层和隐藏层的能量模型,其能量函数定义为:
E(v,h) = -aᵀv - bᵀh - vᵀWh其中:
- v是可见层单元
- h是隐藏层单元
- W是连接权重
- a,b是偏置项
RBM的训练采用对比散度(CD)算法,核心步骤如下:
- 正向传播:计算隐藏层概率分布
- 采样隐藏层状态
- 反向重构:计算可见层概率分布
- 更新参数
3.2 PyTorch实现
class RBM(nn.Module): def __init__(self, visible_dim, hidden_dim): super(RBM, self).__init__() self.W = nn.Parameter(torch.randn(hidden_dim, visible_dim) * 0.01) self.v_bias = nn.Parameter(torch.zeros(visible_dim)) self.h_bias = nn.Parameter(torch.zeros(hidden_dim)) def forward(self, v): # 计算隐藏层概率 p_h_given_v = torch.sigmoid(F.linear(v, self.W, self.h_bias)) return p_h_given_v def sample_h(self, v): p_h = self.forward(v) return p_h.bernoulli() def backward(self, h): # 计算可见层概率 p_v_given_h = torch.sigmoid(F.linear(h, self.W.t(), self.v_bias)) return p_v_given_h def sample_v(self, h): p_v = self.backward(h) return p_v.bernoulli() def contrastive_divergence(self, v0, k=1): # CD-k算法 vk = v0 for _ in range(k): hk = self.sample_h(vk) vk = self.sample_v(hk) # 计算梯度 positive_phase = torch.matmul(v0.t(), self.forward(v0)) negative_phase = torch.matmul(vk.t(), self.forward(vk)) grad_W = (positive_phase - negative_phase) / v0.size(0) grad_v = torch.mean(v0 - vk, dim=0) grad_h = torch.mean(self.forward(v0) - self.forward(vk), dim=0) return grad_W, grad_v, grad_h def update_weights(self, v0, lr=0.01): grad_W, grad_v, grad_h = self.contrastive_divergence(v0) self.W.data += lr * grad_W self.v_bias.data += lr * grad_v self.h_bias.data += lr * grad_h4. 分层预训练策略
按照论文方法,我们需要先训练一系列RBM,然后将它们堆叠起来形成深度自编码器。
4.1 第一层RBM训练
# 训练参数 visible_dim = 784 # 28x28 hidden_dim1 = 1000 epochs = 10 lr = 0.01 # 初始化RBM rbm1 = RBM(visible_dim, hidden_dim1) # 训练循环 for epoch in range(epochs): epoch_loss = 0 for batch_idx, (data, _) in enumerate(train_loader): data = data.view(-1, visible_dim) rbm1.update_weights(data, lr) # 计算重构误差 v_sample = rbm1.sample_v(rbm1.sample_h(data)) loss = F.mse_loss(v_sample, data) epoch_loss += loss.item() print(f'Epoch {epoch+1}/{epochs}, Loss: {epoch_loss/len(train_loader):.4f}')4.2 第二层RBM训练
训练完第一层后,我们用它提取特征作为第二层RBM的输入:
hidden_dim2 = 500 rbm2 = RBM(hidden_dim1, hidden_dim2) # 获取第一层特征 def get_rbm1_features(data): with torch.no_grad(): return rbm1.forward(data) for epoch in range(epochs): epoch_loss = 0 for batch_idx, (data, _) in enumerate(train_loader): data = data.view(-1, visible_dim) h1 = get_rbm1_features(data) rbm2.update_weights(h1, lr) h1_sample = rbm2.sample_v(rbm2.sample_h(h1)) loss = F.mse_loss(h1_sample, h1) epoch_loss += loss.item() print(f'Epoch {epoch+1}/{epochs}, Loss: {epoch_loss/len(train_loader):.4f}')5. 构建深度自编码器
预训练完成后,我们将这些RBM"展开"形成一个对称的自编码器结构:
class DeepAutoencoder(nn.Module): def __init__(self, rbm1, rbm2): super(DeepAutoencoder, self).__init__() # 编码器部分 self.encoder = nn.Sequential( nn.Linear(rbm1.W.shape[1], rbm1.W.shape[0]), nn.Sigmoid(), nn.Linear(rbm2.W.shape[1], rbm2.W.shape[0]), nn.Sigmoid() ) # 解码器部分 self.decoder = nn.Sequential( nn.Linear(rbm2.W.shape[0], rbm2.W.shape[1]), nn.Sigmoid(), nn.Linear(rbm1.W.shape[0], rbm1.W.shape[1]), nn.Sigmoid() ) # 初始化权重 self.encoder[0].weight.data = rbm1.W.data.t() self.encoder[0].bias.data = rbm1.h_bias.data self.encoder[2].weight.data = rbm2.W.data.t() self.encoder[2].bias.data = rbm2.h_bias.data self.decoder[0].weight.data = rbm2.W.data self.decoder[0].bias.data = rbm2.v_bias.data self.decoder[2].weight.data = rbm1.W.data self.decoder[2].bias.data = rbm1.v_bias.data def forward(self, x): encoded = self.encoder(x) decoded = self.decoder(encoded) return decoded6. 微调整个网络
最后一步是对整个自编码器进行端到端的微调:
model = DeepAutoencoder(rbm1, rbm2) optimizer = torch.optim.Adam(model.parameters(), lr=0.001) criterion = nn.MSELoss() # 微调训练 fine_tune_epochs = 20 for epoch in range(fine_tune_epochs): total_loss = 0 for batch_idx, (data, _) in enumerate(train_loader): data = data.view(-1, visible_dim) optimizer.zero_grad() reconstructed = model(data) loss = criterion(reconstructed, data) loss.backward() optimizer.step() total_loss += loss.item() print(f'Fine-tuning Epoch {epoch+1}/{fine_tune_epochs}, Loss: {total_loss/len(train_loader):.4f}') # 可视化重建效果 if epoch % 5 == 0: with torch.no_grad(): test_sample, _ = next(iter(test_loader)) test_sample = test_sample.view(-1, visible_dim) reconstructed = model(test_sample) fig, axes = plt.subplots(2, 5, figsize=(10, 4)) for i in range(5): axes[0, i].imshow(test_sample[i].view(28, 28), cmap='gray') axes[1, i].imshow(reconstructed[i].view(28, 28), cmap='gray') axes[0, i].axis('off') axes[1, i].axis('off') plt.show()7. 实验分析与现代启示
复现完成后,我们获得了约0.02的重建误差,这与论文结果相当。通过这个实验,我们可以得到几点重要启示:
- 预训练的价值:即使在今天,无监督预训练在某些场景下仍然有效,特别是当标注数据稀缺时
- 模型初始化:RBM预训练提供了一种优秀的参数初始化方法
- 深度学习本质:分层特征学习是深度学习强大表征能力的关键
与现代自编码器相比,这个15年前的架构有几个明显不同:
| 特性 | Hinton 2006 | 现代实现 |
|---|---|---|
| 预训练 | 必需 | 通常省略 |
| 激活函数 | Sigmoid | ReLU/LeakyReLU |
| 优化器 | 基础SGD | Adam/RMSprop |
| 正则化 | 无 | Dropout/BatchNorm |
| 计算资源 | CPU集群 | GPU/TPU |
在复现过程中,我遇到了几个典型的"坑":
- RBM训练对学习率非常敏感,需要仔细调整
- 原始论文中的动量参数在现代优化器中已被自适应方法取代
- 批量大小对CD算法的影响比预想的大
这个实验最令人惊叹的是,Hinton在2006年就预见到了深度学习成功的三个必要条件:算力、数据量和参数初始化。今天看来,这几乎就是深度学习发展的路线图。