news 2026/6/7 2:10:59

别再只提反向传播了!手把手复现Hinton 2006年《Science》论文中的降维实验(附PyTorch代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再只提反向传播了!手把手复现Hinton 2006年《Science》论文中的降维实验(附PyTorch代码)

从玻尔兹曼机到深度自编码器:用PyTorch复现Hinton的降维革命

2006年,当大多数研究者还在浅层神经网络中徘徊时,Geoffrey Hinton和他的学生在《Science》上发表了一篇里程碑式的论文《Reducing the dimensionality of data with neural networks》。这篇论文不仅证明了深度神经网络能够有效学习数据低维表示,更重要的是提出了一套切实可行的训练方法——RBM预训练+微调的范式,为后续深度学习革命埋下了种子。今天,我们将用PyTorch完整复现这个经典实验,看看15年前的思想如何在现代框架中焕发新生。

1. 实验背景与技术脉络

在2000年代初,神经网络研究正处于低谷期。虽然反向传播算法理论上可以训练多层网络,但在实践中,超过三层的网络往往难以收敛。Hinton团队的突破在于发现了一种分层无监督预训练的策略:

  1. 受限玻尔兹曼机(RBM):作为构建块学习数据的层次化特征表示
  2. 逐层贪婪训练:每次只训练一个RBM层,固定下层权重后再训练上层
  3. 展开微调:将堆叠的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 plt

2.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)算法,核心步骤如下:

  1. 正向传播:计算隐藏层概率分布
  2. 采样隐藏层状态
  3. 反向重构:计算可见层概率分布
  4. 更新参数

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_h

4. 分层预训练策略

按照论文方法,我们需要先训练一系列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 decoded

6. 微调整个网络

最后一步是对整个自编码器进行端到端的微调:

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的重建误差,这与论文结果相当。通过这个实验,我们可以得到几点重要启示:

  1. 预训练的价值:即使在今天,无监督预训练在某些场景下仍然有效,特别是当标注数据稀缺时
  2. 模型初始化:RBM预训练提供了一种优秀的参数初始化方法
  3. 深度学习本质:分层特征学习是深度学习强大表征能力的关键

与现代自编码器相比,这个15年前的架构有几个明显不同:

特性Hinton 2006现代实现
预训练必需通常省略
激活函数SigmoidReLU/LeakyReLU
优化器基础SGDAdam/RMSprop
正则化Dropout/BatchNorm
计算资源CPU集群GPU/TPU

在复现过程中,我遇到了几个典型的"坑":

  • RBM训练对学习率非常敏感,需要仔细调整
  • 原始论文中的动量参数在现代优化器中已被自适应方法取代
  • 批量大小对CD算法的影响比预想的大

这个实验最令人惊叹的是,Hinton在2006年就预见到了深度学习成功的三个必要条件:算力、数据量和参数初始化。今天看来,这几乎就是深度学习发展的路线图。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!