去噪自编码器(DAE)是一种用于从噪声输入中重建干净数据的神经网络。此实践实现详细介绍了如何使用PyTorch构建和训练去噪自编码器。主要目标是接收含有噪声的图像,并教会自编码器重建出原始的、干净的版本。此练习不仅会显示DAE的去噪能力,还会说明它们如何通过被迫区分信号与噪声来学习有意义、有韧性的特征。我们将使用广受欢迎的MNIST手写数字数据集来完成此任务。1. 配置和导入库首先,让我们导入所需的库。我们需要PyTorch来构建神经网络,NumPy用于数值运算(尤其是添加噪声),以及Matplotlib用于可视化我们的结果。import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader, TensorDataset import numpy as np import matplotlib.pyplot as plt确保您的环境中已安装这些库。如果您遵循了第一章中的环境配置,您应该已准备就绪。2. 加载和准备MNIST数据集MNIST数据集包含 60,000 张训练图像和 10,000 张测试图像的手写数字,每张图像大小为 28x28 像素。# 定义一个转换,用于标准化数据和展平图像 # 将数据标准化到 [0, 1] 范围,以适应 Sigmoid 输出 transform = transforms.Compose([ transforms.ToTensor(), # 自动转换为 [0, 1] 范围 transforms.Lambda(lambda x: x.view(-1)) # 将 28x28 图像展平为 784 维 ]) # 加载 MNIST 数据集 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 将在稍后使用) # 我们将在训练循环中手动为每个批次处理噪声生成 # 这里无需使用 TensorDataset,因为 torchvision 数据集已提供数据和标签 train_loader_clean = DataLoader(train_dataset, batch_size=256, shuffle=True) test_loader_clean = DataLoader(test_dataset, batch_size=256, shuffle=False) # 获取一个样本以检查形状 sample_data, _ = next(iter(train_loader_clean)) print(f"x_train_flat 批次形状: {sample_data.shape}") print(f"展平图像大小: {sample_data.shape[1]}")训练自编码器不需要标签(_),因此我们忽略它们。我们将像素值标准化到 [0, 1] 范围,这是神经网络的常见做法,也适用于 Sigmoid 输出。我们将每个 28x28 图像展平为一个 784 维向量。3. 引入噪声到数据中去噪自编码器的核心思路是学习从损坏的版本中重建干净数据。让我们在训练期间动态地为 MNIST 图像添加一些噪声。高斯噪声是一种常见选择。# 添加高斯噪声的函数 def add_gaussian_noise(images_tensor, noise_factor=0.4): noisy_images = images_tensor + noise_factor * torch.randn_like(images_tensor) return torch.clamp(noisy_images, 0., 1.) # 裁剪到有效范围 [0, 1] # 让我们从一个样本批次中可视化一些原始和带噪声的数字 # 获取一个批次进行演示 sample_batch_clean, _ = next(iter(train_loader_clean)) sample_batch_noisy = add_gaussian_noise(sample_batch_clean, noise_factor=0.4) # 将展平的图像重新塑形回 28x28 以进行显示 def display_images(original_flat, noisy_flat, n=10): plt.figure(figsize=(20, 4)) for i in range(n): # 显示原始图像 ax = plt.subplot(2, n, i + 1) plt.imshow(original_flat[i].reshape(28, 28).cpu().numpy(), cmap='gray') ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) if i == 0: ax.set_title("原始图像", loc='left') # 显示带噪声图像 ax = plt.subplot(2, n, i + 1 + n) plt.imshow(noisy_flat[i].reshape(28, 28).cpu().numpy(), cmap='gray') ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) if i == 0: ax.set_title("带噪声图像", loc='left') plt.show() # 显示一些样本图像 display_images(sample_batch_clean, sample_batch_noisy)执行 display_images 函数将显示一排原始数字及其对应的带噪声版本。这有助于理解我们为自编码器设定的挑战。4. 构建去噪自编码器模型我们的去噪自编码器将是一个全连接神经网络。它由一个编码器组成,该编码器将输入映射到较低维的潜在表示,以及一个解码器,该解码器从这个潜在表示中重建输入。让我们定义架构:输入层: 784 个单元(用于展平的 28x28 图像)。编码器:一个具有 128 个单元和 ReLU 激活的线性层。瓶颈层: 一个具有 64 个单元和 ReLU 激活的线性层。这是我们的潜在表示。解码器:一个具有 128 个单元和 ReLU 激活的线性层。输出层: 一个具有 784 个单元和 Sigmoid 激活的线性层(用于输出介于 0 和 1 之间的像素值)。我们将此定义为标准的 PyTorch nn.Module。input_dim = 784 # 应为 784 encoding_dim = 64 # 潜在表示的大小 class DenoisingAutoencoder(nn.Module): def __init__(self, input_dim, encoding_dim): super(DenoisingAutoencoder, self).__init__() # 编码器 self.encoder = nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Linear(128, encoding_dim), nn.ReLU() # 瓶颈层 ) # 解码器 self.decoder = nn.Sequential( nn.Linear(encoding_dim, 128), nn.ReLU(), nn.Linear(128, input_dim), nn.Sigmoid() # 用于 [0, 1] 范围的输出激活 ) def forward(self, x): encoded = self.encoder(x) decoded = self.decoder(encoded) return decoded autoencoder = DenoisingAutoencoder(input_dim, encoding_dim) # 如果 GPU 可用,将模型移动到 GPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") autoencoder.to(device) print(autoencoder)autoencoder 的输出显示了层的结构。这对于验证架构很有用。5. 编译模型(定义损失和优化器)训练前,我们需要定义优化器和损失函数。优化器: torch.optim.Adam 是一个好的通用优化器。损失函数: 由于我们比较的是像素值(介于 0 和 1 之间且连续),均方误差(nn.MSELoss())是一个合适的损失函数。它衡量重建像素与原始干净像素之间的平均平方差。criterion = nn.MSELoss() optimizer = optim.Adam(autoencoder.parameters(), lr=0.001)6. 训练去噪自编码器现在,我们训练自编码器。与标准自编码器训练的主要区别在于,输入将是带噪声的图像,但目标(我们希望自编码器重建的内容)将是原始、干净的图像。我们为每个批次动态生成噪声。epochs = 20 noise_factor = 0.4 # 训练时使用的固定噪声因子 train_losses = [] val_losses = [] for epoch in range(epochs): # 训练 autoencoder.train() running_train_loss = 0.0 for clean_images, _ in train_loader_clean: clean_images = clean_images.to(device) noisy_images = add_gaussian_noise(clean_images, noise_factor) # 为当前批次添加噪声 optimizer.zero_grad() outputs = autoencoder(noisy_images) # 输入:带噪声数据 loss = criterion(outputs, clean_images) # 目标:干净数据 loss.backward() optimizer.step() running_train_loss += loss.item() * clean_images.size(0) epoch_train_loss = running_train_loss / len(train_loader_clean.dataset) train_losses.append(epoch_train_loss) # 验证 autoencoder.eval() running_val_loss = 0.0 with torch.no_grad(): for clean_images_val, _ in test_loader_clean: clean_images_val = clean_images_val.to(device) noisy_images_val = add_gaussian_noise(clean_images_val, noise_factor) # 为验证批次添加噪声 val_outputs = autoencoder(noisy_images_val) val_loss = criterion(val_outputs, clean_images_val) running_val_loss += val_loss.item() * clean_images_val.size(0) epoch_val_loss = running_val_loss / len(test_loader_clean.dataset) val_losses.append(epoch_val_loss) print(f'Epoch [{epoch+1}/{epochs}], Train Loss: {epoch_train_loss:.6f}, Val Loss: {epoch_val_loss:.6f}')我们可以可视化训练和验证损失,以检查是否过拟合或查看模型学习效果。plt.figure(figsize=(10, 5)) plt.plot(train_losses, label='训练损失') plt.plot(val_losses, label='验证损失') plt.title('模型训练和验证损失') plt.xlabel('周期') plt.ylabel('均方误差损失') plt.legend() plt.grid(True) plt.show()去噪自编码器的训练和验证损失曲线。理想情况下,两种损失都应下降并收敛。上图显示了损失曲线的示例。这是从您的 train_losses 和 val_losses 列表中生成的。7. 模型评估:可视化去噪图像我们去噪自编码器的真正检验是它从训练期间未曾见过的带噪声输入(测试集)中重建干净图像的能力。让我们使用我们训练好的 autoencoder 来预测(去噪)一些 x_test_noisy 图像。autoencoder.eval() # 将模型设置为评估模式 with torch.no_grad(): # 获取一批干净的测试图像 test_batch_clean, _ = next(iter(test_loader_clean)) test_batch_clean = test_batch_clean.to(device) # 创建这些图像的带噪声版本 test_batch_noisy = add_gaussian_noise(test_batch_clean, noise_factor) # 获取去噪输出 denoised_images = autoencoder(test_batch_noisy).cpu().numpy() # 移至 CPU 并转换为 NumPy # 可视化原始、带噪声和去噪图像 def display_reconstructions(original_flat, noisy_flat, reconstructed_flat, n=10): plt.figure(figsize=(20, 6)) for i in range(n): # 显示原始图像 ax = plt.subplot(3, n, i + 1) plt.imshow(original_flat[i].reshape(28, 28).cpu().numpy(), cmap='gray') ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) if i == 0: ax.set_title("原始", loc='left') # 显示带噪声输入 ax = plt.subplot(3, n, i + 1 + n) plt.imshow(noisy_flat[i].reshape(28, 28).cpu().numpy(), cmap='gray') ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) if i == 0: ax.set_title("带噪声输入", loc='left') # 显示重建结果 ax = plt.subplot(3, n, i + 1 + n * 2) plt.imshow(reconstructed_flat[i].reshape(28, 28), cmap='gray') ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) if i == 0: ax.set_title("去噪输出", loc='left') plt.show() display_reconstructions(test_batch_clean, test_batch_noisy, denoised_images)运行 display_reconstructions 时,您应该会看到三行图像:原始的、干净的测试图像。这些测试图像的带噪声版本(它们被输入到 DAE 中)。DAE 的输出(重建后的,希望能更干净的图像)。您应该会观察到 DAE 已学习去除大量噪声,生成的重建图像比带噪声的输入更接近原始图像。这显示了自编码器已学习捕捉数字的潜在结构。8. 使用编码器进行特征提取虽然去噪本身是一个有用的应用,本课程中我们的主要兴趣在于特征提取。我们的 DAE 的编码器部分已学习将输入图像(784 维)转换为压缩表示(本例中为 64 维)。这些潜在表示可以用作下游任务的特征。要提取特征,只需使用我们训练好的 autoencoder 模型的 encoder 属性:autoencoder.eval() # 将模型设置为评估模式 with torch.no_grad(): # 禁用梯度计算 # 通常,为下游任务从干净数据中提取特征会更有用 # 确保数据在正确的设备上 clean_test_images_tensor = test_loader_clean.dataset.tensors[0].to(device) encoded_features_clean = autoencoder.encoder(clean_test_images_tensor).cpu().numpy() # 如果您的用例需要,您也可以从带噪声数据中提取特征 # noisy_test_images_tensor = add_gaussian_noise(clean_test_images_tensor, noise_factor) # encoded_features_noisy = autoencoder.encoder(noisy_test_images_tensor).cpu().numpy() print(f"从干净数据中提取的特征形状: {encoded_features_clean.shape}") # 从干净数据中提取的特征形状: (10000, 64)encoded_features_clean(或者 encoded_features_noisy,如果您的需求如此)现在包含每个测试图像的 64 维特征向量。这些特征被学习为对噪声有韧性,并捕获重建数字的必要信息。您可以将这些特征用作分类器的输入。我们将在第七章中更全面地考察已提取特征的应用。总结说明在本次实践中,您成功使用 PyTorch 实现了一个去噪自编码器。您看到了它是如何被训练以接收带噪声输入并生成更干净、重建后的输出。这个过程迫使模型学习数据更有意义且有韧性的表示。该 DAE 的编码器部分随后可以用来提取这些已学习特征,它们通常比原始输入数据对其他机器学习任务更有用,尤其是在处理带噪声数据集时。您现在已为您的自编码器工具集添加了另一个强大的工具。接下来,我们将考察卷积自编码器,它们特别适用于图像数据。