别再只提反向传播了!手把手复现Hinton 2006年那篇‘神作’:用PyTorch实现深度自编码器降维
用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(fUsing 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, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 创建数据加载器 batch_size 64 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse)提示在实际应用中可以根据需要调整batch_size。较大的batch_size会使训练更稳定但可能降低泛化能力。2. 受限玻尔兹曼机(RBM)实现RBM是构建深度自编码器的关键组件它是一种具有可见层和隐藏层的能量模型。我们将从零开始实现RBM包括对比散度(CD)训练算法。2.1 RBM模型结构RBM由可见单元v和隐藏单元h组成两者之间全连接但层内无连接。我们使用二进制单元这在处理像MNIST这样的二值化数据时效果良好。class RBM(nn.Module): def __init__(self, n_visible784, n_hidden128): 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, k1): 对比散度-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, epochs10, lr0.01, momentum0.9): optimizer torch.optim.SGD(model.parameters(), lrlr, momentummomentum) 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(fEpoch {epoch1}/{epochs}, Loss: {avg_loss:.4f}) return losses # 初始化并训练RBM rbm RBM(n_visible784, n_hidden500).to(device) losses train_rbm(rbm, train_loader, epochs20)训练过程中我们可以监控自由能量的变化来评估模型收敛情况。自由能量下降表明模型正在学习数据的有用表示。3. 构建深度自编码器通过堆叠多个预训练的RBM我们可以构建一个深度自编码器。这个过程分为两个阶段逐层贪婪预训练和整体微调。3.1 逐层预训练我们首先训练第一个RBM然后使用它的隐藏表示作为第二个RBM的输入依此类推# 第一层RBM rbm1 RBM(n_visible784, n_hidden500).to(device) train_rbm(rbm1, train_loader, epochs15) # 第二层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, dim0) 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_sizebatch_size, shuffleTrue) rbm2 RBM(n_visible500, n_hidden250).to(device) train_rbm(rbm2, hidden_loader, epochs15)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(), lr0.001) def train_autoencoder(model, train_loader, epochs30): 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(fEpoch {epoch1}/{epochs}, Loss: {total_loss/len(train_loader):.4f}) train_autoencoder(autoencoder, train_loader)4.2 可视化结果让我们看看自编码器在测试集上的重建效果def visualize_reconstructions(model, test_loader, n_images10): 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), cmapgray) 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), cmapgray) 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_samples1000): 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_components2, random_state42) features_2d tsne.fit_transform(features.numpy()) plt.figure(figsize(10, 8)) scatter plt.scatter(features_2d[:, 0], features_2d[:, 1], clabels.numpy(), cmaptab10, alpha0.6) plt.colorbar(scatter) plt.title(t-SNE visualization of latent space) plt.show() visualize_latent_space(autoencoder, test_loader)5. 预训练在现代深度学习中的价值虽然Hinton的预训练方法在2006年取得了突破性成果但在当今大数据和计算资源丰富的环境下预训练的必要性值得讨论大数据场景当训练数据量非常大时随机初始化配合适当的正则化通常足够小数据场景对于有限的数据预训练仍然可以提供更好的泛化性能迁移学习预训练作为特征提取器在其他任务上仍然有价值模型初始化预训练可以为模型提供更好的初始点加速收敛在实现过程中我发现预训练确实能帮助模型更快收敛特别是在网络较深时。不过现代技术如批量归一化和更先进的优化器已经部分替代了预训练的作用。