用PyTorch复现Hinton深度自编码器:从RBM预训练到微调的全流程解析
2006年,Geoffrey Hinton在《Science》发表的论文《Reducing the dimensionality of data with neural networks》点燃了深度学习复兴的火种。这篇开创性工作首次证明了深度神经网络可以通过逐层预训练有效学习数据的内在表示。本文将带您用现代PyTorch框架完整复现这一里程碑技术,从受限玻尔兹曼机(RBM)的对比散度训练,到深度自编码器的构建,最后通过反向传播微调模型。我们将重点关注如何用今天的工具实现昨天的思想,而不仅仅是理论描述。
1. 环境准备与数据加载
在开始构建模型前,我们需要配置合适的开发环境。推荐使用Python 3.8+和PyTorch 1.10+版本,这些版本在自动微分和GPU加速方面都有良好支持。
import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms import matplotlib.pyplot as plt import numpy as np # 检查GPU可用性 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using device: {device}")对于数据集,我们使用经典的MNIST手写数字作为示例,这与Hinton原始论文中的实验设置相似。MNIST的28x28像素图像非常适合展示降维效果。
# 数据预处理 transform = transforms.Compose([ transforms.ToTensor(), transforms.Lambda(lambda x: x.view(-1)) # 展平为784维向量 ]) # 加载数据集 train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform) # 创建数据加载器 batch_size = 64 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)提示:在实际应用中,可以根据需要调整batch_size。较大的batch_size会使训练更稳定,但可能降低泛化能力。
2. 受限玻尔兹曼机(RBM)实现
RBM是构建深度自编码器的关键组件,它是一种具有可见层和隐藏层的能量模型。我们将从零开始实现RBM,包括对比散度(CD)训练算法。
2.1 RBM模型结构
RBM由可见单元v和隐藏单元h组成,两者之间全连接但层内无连接。我们使用二进制单元,这在处理像MNIST这样的二值化数据时效果良好。
class RBM(nn.Module): def __init__(self, n_visible=784, n_hidden=128): super(RBM, self).__init__() self.W = nn.Parameter(torch.randn(n_hidden, n_visible) * 0.1) self.v_bias = nn.Parameter(torch.zeros(n_visible)) self.h_bias = nn.Parameter(torch.zeros(n_hidden)) self.n_visible = n_visible self.n_hidden = n_hidden def sample_h(self, v): """给定可见层,采样隐藏层""" activation = F.linear(v, self.W, self.h_bias) p_h = torch.sigmoid(activation) return p_h, torch.bernoulli(p_h) def sample_v(self, h): """给定隐藏层,采样可见层""" activation = F.linear(h, self.W.t(), self.v_bias) p_v = torch.sigmoid(activation) return p_v, torch.bernoulli(p_v) def forward(self, v, k=1): """对比散度-k算法""" # 正向传播 h0_prob, h0_sample = self.sample_h(v) # Gibbs采样 v_k = v for _ in range(k): _, h_k_sample = self.sample_h(v_k) v_k_prob, v_k_sample = self.sample_v(h_k_sample) # 计算梯度 positive_grad = torch.matmul(h0_sample.t(), v) negative_grad = torch.matmul(h_k_sample.t(), v_k_prob) return (positive_grad - negative_grad) / v.size(0) def free_energy(self, v): """计算自由能量""" vbias_term = v.mv(self.v_bias) wx_b = F.linear(v, self.W, self.h_bias) hidden_term = wx_b.exp().add(1).log().sum(1) return (-hidden_term - vbias_term).mean()2.2 RBM训练过程
训练RBM需要特别注意学习率和动量等超参数的设置。下面是训练循环的实现:
def train_rbm(model, train_loader, epochs=10, lr=0.01, momentum=0.9): optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=momentum) losses = [] for epoch in range(epochs): epoch_loss = 0 for batch_idx, (data, _) in enumerate(train_loader): data = data.to(device) data = (data > 0.5).float() # 二值化 # 计算梯度并更新权重 grad = model(data) for param in model.parameters(): param.grad = -grad if param is model.W else -grad.mean(0) optimizer.step() # 计算损失 loss = model.free_energy(data) epoch_loss += loss.item() avg_loss = epoch_loss / len(train_loader) losses.append(avg_loss) print(f'Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}') return losses # 初始化并训练RBM rbm = RBM(n_visible=784, n_hidden=500).to(device) losses = train_rbm(rbm, train_loader, epochs=20)训练过程中,我们可以监控自由能量的变化来评估模型收敛情况。自由能量下降表明模型正在学习数据的有用表示。
3. 构建深度自编码器
通过堆叠多个预训练的RBM,我们可以构建一个深度自编码器。这个过程分为两个阶段:逐层贪婪预训练和整体微调。
3.1 逐层预训练
我们首先训练第一个RBM,然后使用它的隐藏表示作为第二个RBM的输入,依此类推:
# 第一层RBM rbm1 = RBM(n_visible=784, n_hidden=500).to(device) train_rbm(rbm1, train_loader, epochs=15) # 第二层RBM - 使用第一层RBM的隐藏表示作为输入 def get_hidden_representations(rbm, data_loader): hidden_reps = [] for data, _ in data_loader: data = data.to(device) data = (data > 0.5).float() h_prob, _ = rbm.sample_h(data) hidden_reps.append(h_prob) return torch.cat(hidden_reps, dim=0) hidden_train = get_hidden_representations(rbm1, train_loader) hidden_dataset = torch.utils.data.TensorDataset(hidden_train, torch.zeros(len(hidden_train))) hidden_loader = DataLoader(hidden_dataset, batch_size=batch_size, shuffle=True) rbm2 = RBM(n_visible=500, n_hidden=250).to(device) train_rbm(rbm2, hidden_loader, epochs=15)3.2 构建完整自编码器
预训练完成后,我们将这些RBM"展开"成一个深度自编码器:
class DeepAutoencoder(nn.Module): def __init__(self, rbm1, rbm2): super(DeepAutoencoder, self).__init__() # 编码器 self.encoder = nn.Sequential( nn.Linear(rbm1.n_visible, rbm1.n_hidden), nn.Sigmoid(), nn.Linear(rbm1.n_hidden, rbm2.n_hidden), nn.Sigmoid() ) # 解码器 self.decoder = nn.Sequential( nn.Linear(rbm2.n_hidden, rbm1.n_hidden), nn.Sigmoid(), nn.Linear(rbm1.n_hidden, rbm1.n_visible), 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 decoded # 创建自编码器 autoencoder = DeepAutoencoder(rbm1, rbm2).to(device)4. 微调与结果分析
预训练为模型提供了良好的初始权重,但还需要通过反向传播进行微调以获得最佳性能。
4.1 微调自编码器
我们使用均方误差作为损失函数,并采用Adam优化器:
criterion = nn.MSELoss() optimizer = torch.optim.Adam(autoencoder.parameters(), lr=0.001) def train_autoencoder(model, train_loader, epochs=30): model.train() for epoch in range(epochs): total_loss = 0 for batch_idx, (data, _) in enumerate(train_loader): data = data.to(device) data = (data > 0.5).float() optimizer.zero_grad() output = model(data) loss = criterion(output, data) loss.backward() optimizer.step() total_loss += loss.item() print(f'Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(train_loader):.4f}') train_autoencoder(autoencoder, train_loader)4.2 可视化结果
让我们看看自编码器在测试集上的重建效果:
def visualize_reconstructions(model, test_loader, n_images=10): model.eval() with torch.no_grad(): data, _ = next(iter(test_loader)) data = data.to(device) data = (data > 0.5).float() reconstructions = model(data[:n_images]) plt.figure(figsize=(20, 4)) for i in range(n_images): # 原始图像 ax = plt.subplot(2, n_images, i + 1) plt.imshow(data[i].cpu().reshape(28, 28), cmap='gray') ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) # 重建图像 ax = plt.subplot(2, n_images, i + 1 + n_images) plt.imshow(reconstructions[i].cpu().reshape(28, 28), cmap='gray') ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) plt.show() visualize_reconstructions(autoencoder, test_loader)4.3 降维可视化
我们可以使用编码器的中间表示来可视化数据在低维空间的分布:
from sklearn.manifold import TSNE def visualize_latent_space(model, test_loader, n_samples=1000): model.eval() features = [] labels = [] with torch.no_grad(): for i, (data, target) in enumerate(test_loader): if len(features) >= n_samples: break data = data.to(device) data = (data > 0.5).float() encoded = model.encoder(data) features.append(encoded.cpu()) labels.append(target.cpu()) features = torch.cat(features)[:n_samples] labels = torch.cat(labels)[:n_samples] # 使用t-SNE降维到2D tsne = TSNE(n_components=2, random_state=42) features_2d = tsne.fit_transform(features.numpy()) plt.figure(figsize=(10, 8)) scatter = plt.scatter(features_2d[:, 0], features_2d[:, 1], c=labels.numpy(), cmap='tab10', alpha=0.6) plt.colorbar(scatter) plt.title('t-SNE visualization of latent space') plt.show() visualize_latent_space(autoencoder, test_loader)5. 预训练在现代深度学习中的价值
虽然Hinton的预训练方法在2006年取得了突破性成果,但在当今大数据和计算资源丰富的环境下,预训练的必要性值得讨论:
- 大数据场景:当训练数据量非常大时,随机初始化配合适当的正则化通常足够
- 小数据场景:对于有限的数据,预训练仍然可以提供更好的泛化性能
- 迁移学习:预训练作为特征提取器在其他任务上仍然有价值
- 模型初始化:预训练可以为模型提供更好的初始点,加速收敛
在实现过程中,我发现预训练确实能帮助模型更快收敛,特别是在网络较深时。不过,现代技术如批量归一化和更先进的优化器已经部分替代了预训练的作用。