深度学习学习教程,从入门到精通,自编码器 — 完整知识点与代码案例(14)

自编码器 --- 完整知识点与代码案例


一、欠完备自编码器 (Undercomplete Autoencoder)

1.1 核心知识点

自编码器(Autoencoder) 是一种神经网络,目标是学习输入数据的压缩表示。它由两部分组成:

  • 编码器(Encoder) :将输入 xxx 映射到隐藏表示 h=f(x)h = f(x)h=f(x)
  • 解码器(Decoder) :将隐藏表示映射回重构 x^=g(h)\hat{x} = g(h)x^=g(h)

欠完备(Undercomplete) 指隐藏层维度 hhh 小于输入维度 xxx,迫使网络学习数据中最重要的特征。损失函数为:

L(x,g(f(x)))L(x, g(f(x)))L(x,g(f(x)))

常用 MSE 或交叉熵。当 hhh 维度远小于 xxx 维度时,自编码器被迫学习数据的低维流形结构,效果类似于 PCA。

1.2 案例代码:基础欠完备自编码器(MNIST)

python 复制代码
import torch                          # PyTorch 深度学习框架
import torch.nn as nn                 # 神经网络模块,包含各种层和损失函数
import torch.optim as optim           # 优化器模块,包含SGD、Adam等
import torchvision                    # 计算机视觉工具库
import torchvision.transforms as transforms  # 数据预处理和增强工具
import matplotlib.pyplot as plt       # 绘图库,用于可视化结果

# ======================== 超参数设置 ========================
batch_size = 128          # 每批次处理的样本数,影响训练速度和内存占用
learning_rate = 1e-3      # 学习率,控制参数更新步长
num_epochs = 20           # 训练总轮数,遍历整个数据集的次数
encoding_dim = 32         # 编码维度(隐藏层大小),必须小于输入维度784

# ======================== 数据预处理 ========================
transform = transforms.Compose([
    transforms.ToTensor(),            # 将PIL图像转为Tensor,像素值从[0,255]变为[0,1]
])

# 下载并加载MNIST训练集
# MNIST包含60000张28x28的手写数字灰度图
train_dataset = torchvision.datasets.MNIST(
    root='./data',                    # 数据存储路径
    train=True,                       # 指定为训练集
    transform=transform,              # 应用预处理变换
    download=True                     # 如果本地没有则自动下载
)

# 创建数据加载器,支持自动批处理、打乱和多进程加载
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset,            # 传入数据集
    batch_size=batch_size,            # 每批次样本数
    shuffle=True,                     # 每个epoch打乱数据顺序,增加随机性
    num_workers=2                     # 用于数据加载的子进程数
)

# ======================== 定义欠完备自编码器 ========================
class UndercompleteAutoencoder(nn.Module):
    """
    欠完备自编码器:
    - 输入层: 784 (28x28 像素展平)
    - 编码层: 32 维 (远小于784,强制压缩)
    - 解码层: 784 维 (重构回原始维度)
    """
    def __init__(self, input_dim=784, encoding_dim=32):
        super(UndercompleteAutoencoder, self).__init__()
        
        # ---------- 编码器部分 ----------
        # 全连接层:将784维输入压缩到32维隐藏表示
        # 权重矩阵形状:[784, 32],偏置向量形状:[32]
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 256),       # 第一层:784 → 256,初步降维
            nn.ReLU(inplace=True),           # ReLU激活:f(x)=max(0,x),引入非线性
            nn.Linear(256, encoding_dim),    # 第二层:256 → 32,进一步压缩
            nn.ReLU(inplace=True)            # ReLU激活,确保编码非负
        )
        
        # ---------- 解码器部分 ----------
        # 全连接层:将32维隐藏表示还原到784维
        self.decoder = nn.Sequential(
            nn.Linear(encoding_dim, 256),    # 第一层:32 → 256,逐步扩展
            nn.ReLU(inplace=True),           # ReLU激活
            nn.Linear(256, input_dim),       # 第二层:256 → 784,重构输出
            nn.Sigmoid()                     # Sigmoid激活:输出值限制在[0,1],匹配像素范围
        )
    
    def forward(self, x):
        """
        前向传播:定义数据从输入到输出的流动路径
        参数x: 输入张量,形状 [batch_size, 784]
        返回: 重构张量,形状 [batch_size, 784]
        """
        encoded = self.encoder(x)            # 编码:输入 → 压缩表示
        decoded = self.decoder(encoded)      # 解码:压缩表示 → 重构
        return decoded                       # 返回重构结果
    
    def encode(self, x):
        """仅执行编码操作,用于获取数据的压缩表示"""
        return self.encoder(x)

# ======================== 模型初始化 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  # 自动选择GPU或CPU
model = UndercompleteAutoencoder(input_dim=784, encoding_dim=encoding_dim).to(device)  # 实例化并移至设备

# 均方误差损失:L = (1/n) * Σ(x_i - x̂_i)²
criterion = nn.MSELoss()

# Adam优化器:自适应学习率,momentum=0.9,适合稀疏梯度
optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5)

# ======================== 训练循环 ========================
losses = []  # 记录每个epoch的平均损失

for epoch in range(num_epochs):
    epoch_loss = 0.0  # 累积当前epoch的损失
    for batch_idx, (data, _) in enumerate(train_loader):
        # data形状: [batch_size, 1, 28, 28]
        # 注意:MNIST标签(_)在此处不需要,自编码器是无监督学习
        
        data = data.view(data.size(0), -1).to(device)  # 展平:[B,1,28,28] → [B,784],并移至设备
        
        # ---- 前向传播 ----
        reconstructed = model(data)          # 通过模型得到重构结果
        loss = criterion(reconstructed, data)  # 计算重构误差(MSE损失)
        
        # ---- 反向传播与优化 ----
        optimizer.zero_grad()                # 清除上一步的梯度(PyTorch默认累积梯度)
        loss.backward()                      # 反向传播:计算损失对所有参数的梯度
        optimizer.step()                     # 更新参数:θ = θ - lr * gradient
        
        epoch_loss += loss.item()            # 累加损失值(.item()从Tensor提取标量)
    
    # 计算并记录平均损失
    avg_loss = epoch_loss / len(train_loader)
    losses.append(avg_loss)
    print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.6f}')

# ======================== 可视化结果 ========================
# 选取测试样本进行重构展示
test_data = next(iter(train_loader))[0][:8]       # 取前8张图
test_flat = test_data.view(8, -1).to(device)       # 展平
with torch.no_grad():                              # 禁用梯度计算,节省内存
    reconstructed = model(test_flat)               # 执行重构

fig, axes = plt.subplots(2, 8, figsize=(16, 4))    # 创建2行8列子图
for i in range(8):
    axes[0, i].imshow(test_data[i].squeeze(), cmap='gray')        # 第一行:原始图像
    axes[0, i].axis('off')
    axes[1, i].imshow(reconstructed[i].cpu().view(28,28), cmap='gray')  # 第二行:重构图像
    axes[1, i].axis('off')
plt.suptitle('Top: Original | Bottom: Reconstructed', fontsize=14)
plt.tight_layout()
plt.show()

二、正则自编码器 (Regularized Autoencoder)

2.1 核心知识点

正则自编码器 通过在损失函数中添加正则化项来约束编码器的行为,即使编码维度不小于输入维度也能学到有意义的表示。常见正则化策略:

正则化方法 损失函数 效果
稀疏正则 L+λ∣h∣1L + \lambda |h|_1L+λ∣h∣1 编码向量稀疏,只有少量非零
权重衰减 L+λ∣θ∣22L + \lambda |\theta|_2^2L+λ∣θ∣22 防止权重过大,类似L2正则
变分正则 L+KL(q∣p)L + KL(q | p)L+KL(q∣p) 学习概率分布(VAE的基础)

关键区别:

  • 欠完备:通过架构约束(维度小)
  • 正则化:通过损失函数约束(即使维度大也能学到好的表示)

2.2 案例代码:稀疏自编码器

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt

# ======================== 超参数设置 ========================
batch_size = 128              # 批大小
num_epochs = 30               # 稀疏自编码器通常需要更多轮次
learning_rate = 1e-3          # 学习率
sparse_weight = 1e-3          # 稀疏正则化系数λ,控制稀疏惩罚强度
target_sparsity = 0.05        # 目标激活率ρ:每个神经元平均只有5%时间被激活
input_dim = 784               # 输入维度 28×28
hidden_dim = 256              # 隐藏层维度可以很大(甚至是过完备的)

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])

train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)

# ======================== 稀疏自编码器定义 ========================
class SparseAutoencoder(nn.Module):
    """
    稀疏自编码器:
    - 隐藏层维度可以大于或等于输入维度(过完备)
    - 通过稀疏正则化强制编码向量稀疏
    - KL散度作为稀疏惩罚项
    """
    def __init__(self, input_dim=784, hidden_dim=256):
        super(SparseAutoencoder, self).__init__()
        
        # 编码器:两层网络,输出使用Sigmoid确保值在[0,1]之间
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),   # 784 → 256
            nn.ReLU(inplace=True),              # ReLU激活
            nn.Linear(hidden_dim, hidden_dim),  # 256 → 256(可以过完备)
            nn.Sigmoid()                        # Sigmoid:输出在(0,1)之间,方便计算KL散度
        )
        
        # 解码器:对称结构
        self.decoder = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim),  # 256 → 256
            nn.ReLU(inplace=True),              # ReLU激活
            nn.Linear(hidden_dim, input_dim),   # 256 → 784
            nn.Sigmoid()                        # Sigmoid:输出像素值在[0,1]
        )
    
    def forward(self, x):
        """前向传播,同时返回编码用于计算稀疏惩罚"""
        h = self.encoder(x)                     # 编码:得到隐藏表示
        reconstructed = self.decoder(h)         # 解码:得到重构
        return reconstructed, h                 # 同时返回编码h,用于计算稀疏正则项

# ======================== KL散度稀疏惩罚计算 ========================
def kl_divergence_sparsity(encoded, target_sparsity, epsilon=1e-10):
    """
    计算KL散度稀疏惩罚:
    KL(ρ || ρ̂_j) = ρ·log(ρ/ρ̂_j) + (1-ρ)·log((1-ρ)/(1-ρ̂_j))
    
    其中:
    - ρ = target_sparsity(目标平均激活率,如0.05)
    - ρ̂_j = 实际平均激活率(对batch维度取均值)
    
    参数:
        encoded: 编码器输出,形状 [batch_size, hidden_dim]
        target_sparsity: 目标稀疏度ρ
        epsilon: 数值稳定的小常数,防止log(0)
    返回:
        标量KL散度值
    """
    # 计算每个神经元在当前batch上的平均激活率
    # dim=0 对batch维度取均值,得到形状 [hidden_dim]
    rho_hat = torch.mean(encoded, dim=0)
    
    # 将值限制在[epsilon, 1-epsilon]范围内,避免log(0)或除零
    rho_hat = torch.clamp(rho_hat, min=epsilon, max=1 - epsilon)
    
    # KL散度公式:鼓励ρ̂接近ρ
    # 当ρ̂ = ρ时,KL = 0(最小值)
    kl_div = target_sparsity * torch.log(target_sparsity / rho_hat) + \
             (1 - target_sparsity) * torch.log((1 - target_sparsity) / (1 - rho_hat))
    
    return torch.sum(kl_div)                    # 对所有隐藏神经元求和

# ======================== 模型与优化器 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SparseAutoencoder(input_dim, hidden_dim).to(device)
criterion = nn.MSELoss()                        # 重构损失:均方误差
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# ======================== 训练循环 ========================
total_losses = []      # 记录总损失
recon_losses = []      # 记录重构损失
sparse_losses = []     # 记录稀疏惩罚损失

for epoch in range(num_epochs):
    epoch_total = 0.0
    epoch_recon = 0.0
    epoch_sparse = 0.0
    
    for batch_idx, (data, _) in enumerate(train_loader):
        data = data.view(data.size(0), -1).to(device)  # 展平:[B,1,28,28] → [B,784]
        
        # ---- 前向传播 ----
        reconstructed, encoded = model(data)    # 获取重构和编码
        
        # ---- 计算损失 ----
        recon_loss = criterion(reconstructed, data)             # 重构损失
        kl_loss = kl_divergence_sparsity(encoded, target_sparsity)  # KL稀疏惩罚
        total_loss = recon_loss + sparse_weight * kl_loss       # 总损失 = 重构 + λ·稀疏
        
        # ---- 反向传播与优化 ----
        optimizer.zero_grad()                  # 清除旧梯度
        total_loss.backward()                  # 计算总损失的梯度
        optimizer.step()                       # 更新参数
        
        # 累加损失
        epoch_total += total_loss.item()
        epoch_recon += recon_loss.item()
        epoch_sparse += kl_loss.item()
    
    # 记录平均损失
    n = len(train_loader)
    total_losses.append(epoch_total / n)
    recon_losses.append(epoch_recon / n)
    sparse_losses.append(epoch_sparse / n)
    
    if (epoch + 1) % 5 == 0:                   # 每5个epoch打印一次
        print(f'Epoch [{epoch+1}/{num_epochs}] '
              f'Total: {total_losses[-1]:.6f}, '
              f'Recon: {recon_losses[-1]:.6f}, '
              f'Sparse: {sparse_losses[-1]:.4f}')

# ======================== 可视化损失曲线 ========================
plt.figure(figsize=(10, 4))
plt.plot(total_losses, label='Total Loss', linewidth=2)       # 总损失曲线
plt.plot(recon_losses, label='Reconstruction Loss', linewidth=2)  # 重构损失曲线
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Sparse Autoencoder Training Loss')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# ======================== 可视化稀疏激活 ========================
model.eval()                                    # 切换到评估模式
with torch.no_grad():
    sample = next(iter(train_loader))[0][:100]  # 取100个样本
    sample = sample.view(100, -1).to(device)
    _, h = model(sample)                        # 获取编码
    activations = h.cpu().numpy()               # 转为numpy

plt.figure(figsize=(12, 4))
plt.imshow(activations.T, cmap='hot', aspect='auto')  # 热力图:横轴=样本,纵轴=神经元
plt.xlabel('Sample Index')
plt.ylabel('Neuron Index')
plt.title('Sparse Activation Patterns (darker = more zeros)')
plt.colorbar(label='Activation')
plt.show()

三、表示能力、层的大小和深度 (Representational Capacity, Layer Size and Depth)

3.1 核心知识点

表示能力指的是自编码器能学习到的函数族的丰富程度。

关键原则:

设计选择 效果 注意事项
增加隐藏层宽度 能表示更复杂的映射 过宽可能导致恒等映射(不压缩信息)
增加网络深度 每层学到更抽象的特征 深层更高效:O(2n)O(2^n)O(2n) 的指数表达能力
线性激活 退化为PCA 无法捕获非线性结构
非线性激活 能表示任意复杂函数 理论上万能近似定理适用

深层 vs 浅层

  • 具有 kkk 个隐藏层的深度自编码器比仅用1层的浅层自编码器能用更少的参数达到相同重构质量
  • 深度网络学到的是层次化表示:低层→边缘/纹理,高层→语义/结构

3.2 案例代码:不同深度与宽度对比

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# ======================== 超参数 ========================
batch_size = 128
num_epochs = 15
learning_rate = 1e-3
input_dim = 784
encoding_dim = 64                # 最终编码维度(所有模型相同以便比较)

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)

# ======================== 模型1:浅而宽的自编码器 ========================
class ShallowWideAutoencoder(nn.Module):
    """
    浅而宽:1个隐藏层,宽度1024
    优点:简单,训练快
    缺点:参数多,可能过拟合,特征不够抽象
    """
    def __init__(self, input_dim=784, encoding_dim=64):
        super(ShallowWideAutoencoder, self).__init__()
        
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 1024),        # 单层宽隐藏层:784 → 1024(过完备)
            nn.ReLU(inplace=True),             # ReLU激活
            nn.Linear(1024, encoding_dim),     # 压缩到编码维度:1024 → 64
            nn.ReLU(inplace=True)
        )
        
        self.decoder = nn.Sequential(
            nn.Linear(encoding_dim, 1024),     # 64 → 1024
            nn.ReLU(inplace=True),
            nn.Linear(1024, input_dim),        # 1024 → 784
            nn.Sigmoid()                       # 输出[0,1]
        )
    
    def forward(self, x):
        return self.decoder(self.encoder(x))   # 编码后解码

# ======================== 模型2:深而窄的自编码器 ========================
class DeepNarrowAutoencoder(nn.Module):
    """
    深而窄:4个隐藏层,逐步压缩
    784 → 256 → 128 → 64 → encoding_dim
    优点:参数高效,层次化特征
    缺点:训练难度大,可能梯度消失
    """
    def __init__(self, input_dim=784, encoding_dim=64):
        super(DeepNarrowAutoencoder, self).__init__()
        
        # 编码器:逐层压缩,每层维度减半
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 256),         # 第1层:784 → 256
            nn.BatchNorm1d(256),               # 批归一化:稳定训练,加速收敛
            nn.ReLU(inplace=True),             # ReLU激活
            nn.Linear(256, 128),               # 第2层:256 → 128
            nn.BatchNorm1d(128),               # 批归一化
            nn.ReLU(inplace=True),
            nn.Linear(128, 64),                # 第3层:128 → 64
            nn.BatchNorm1d(64),
            nn.ReLU(inplace=True),
            nn.Linear(64, encoding_dim),       # 第4层:64 → encoding_dim
            nn.ReLU(inplace=True)
        )
        
        # 解码器:对称扩展
        self.decoder = nn.Sequential(
            nn.Linear(encoding_dim, 64),       # encoding_dim → 64
            nn.BatchNorm1d(64),
            nn.ReLU(inplace=True),
            nn.Linear(64, 128),                # 64 → 128
            nn.BatchNorm1d(128),
            nn.ReLU(inplace=True),
            nn.Linear(128, 256),               # 128 → 256
            nn.BatchNorm1d(256),
            nn.ReLU(inplace=True),
            nn.Linear(256, input_dim),         # 256 → 784
            nn.Sigmoid()
        )
    
    def forward(self, x):
        return self.decoder(self.encoder(x))

# ======================== 模型3:卷积自编码器(利用空间结构) ========================
class ConvAutoencoder(nn.Module):
    """
    卷积自编码器:利用图像的空间局部性
    比全连接更高效地处理2D图像数据
    编码路径:逐步减小空间尺寸,增加通道数
    解码路径:逐步增大空间尺寸,减少通道数
    """
    def __init__(self):
        super(ConvAutoencoder, self).__init__()
        
        # 编码器:卷积层逐步提取空间特征
        self.encoder = nn.Sequential(
            # 输入: [B, 1, 28, 28]
            nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
            # 卷积:1通道→16通道,stride=2使尺寸减半
            # 输出: [B, 16, 14, 14]
            nn.BatchNorm2d(16),                 # 通道维度批归一化
            nn.ReLU(inplace=True),
            
            nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1),
            # 卷积:16→32通道,尺寸再减半
            # 输出: [B, 32, 7, 7]
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            
            nn.Conv2d(32, 64, kernel_size=7),
            # 卷积:32→64通道,7x7核匹配特征图尺寸
            # 输出: [B, 64, 1, 1] → 压缩为64维向量
        )
        
        # 解码器:转置卷积逐步恢复空间尺寸
        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(64, 32, kernel_size=7),
            # 转置卷积(反卷积):64→32通道,恢复尺寸
            # 输出: [B, 32, 7, 7]
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            
            nn.ConvTranspose2d(32, 16, kernel_size=3, stride=2, padding=1, output_padding=1),
            # 转置卷积:32→16通道,stride=2使尺寸翻倍
            # 输出: [B, 16, 14, 14]
            nn.BatchNorm2d(16),
            nn.ReLU(inplace=True),
            
            nn.ConvTranspose2d(16, 1, kernel_size=3, stride=2, padding=1, output_padding=1),
            # 转置卷积:16→1通道,恢复原始尺寸
            # 输出: [B, 1, 28, 28]
            nn.Sigmoid()                        # 像素值[0,1]
        )
    
    def forward(self, x):
        z = self.encoder(x)                     # 编码:[B,1,28,28] → [B,64,1,1]
        reconstructed = self.decoder(z)         # 解码:[B,64,1,1] → [B,1,28,28]
        return reconstructed

# ======================== 训练与比较函数 ========================
def train_model(model, train_loader, num_epochs, learning_rate, device):
    """通用训练函数,返回每个epoch的损失列表"""
    model = model.to(device)
    criterion = nn.MSELoss()                    # 重构损失
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    losses = []
    
    for epoch in range(num_epochs):
        epoch_loss = 0.0
        for data, _ in train_loader:
            if isinstance(model, ConvAutoencoder):
                # 卷积模型:保留空间维度,不展平
                data = data.to(device)
            else:
                # 全连接模型:需要展平
                data = data.view(data.size(0), -1).to(device)
            
            output = model(data)                # 前向传播
            loss = criterion(output, data)      # 计算损失
            
            optimizer.zero_grad()               # 清梯度
            loss.backward()                     # 反向传播
            optimizer.step()                    # 更新参数
            
            epoch_loss += loss.item()
        
        avg = epoch_loss / len(train_loader)
        losses.append(avg)
        if (epoch + 1) % 5 == 0:
            print(f'  Epoch [{epoch+1}/{num_epochs}], Loss: {avg:.6f}')
    
    return losses

# ======================== 模型参数量统计 ========================
def count_parameters(model):
    """统计模型可训练参数总数"""
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 实例化三个模型
models = {
    'Shallow-Wide (1 hidden)': ShallowWideAutoencoder(input_dim, encoding_dim),
    'Deep-Narrow (4 hidden)': DeepNarrowAutoencoder(input_dim, encoding_dim),
    'Conv-Autoencoder': ConvAutoencoder()
}

# 打印参数量对比
print("=" * 50)
print("模型参数量对比:")
print("=" * 50)
for name, model in models.items():
    params = count_parameters(model)
    print(f'{name}: {params:,} parameters')     # 千位分隔符格式化

# ======================== 训练所有模型 ========================
all_losses = {}
for name, model in models.items():
    print(f'\nTraining {name}...')
    losses = train_model(model, train_loader, num_epochs, learning_rate, device)
    all_losses[name] = losses

# ======================== 损失对比图 ========================
plt.figure(figsize=(10, 6))
for name, losses in all_losses.items():
    plt.plot(losses, label=name, linewidth=2)
plt.xlabel('Epoch', fontsize=12)
plt.ylabel('MSE Loss', fontsize=12)
plt.title('Comparison: Shallow-Wide vs Deep-Narrow vs Conv Autoencoder', fontsize=13)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

四、随机编码器和解码器 (Stochastic Encoder and Decoder)

4.1 核心知识点

与确定性映射不同,随机编码器/解码器 将编码和解码过程建模为概率分布

  • 随机编码器 :q(h∣x)=N(μ(x),σ2(x))q(h|x) = \mathcal{N}(\mu(x), \sigma^2(x))q(h∣x)=N(μ(x),σ2(x)),输出均值和方差
  • 随机解码器 :p(x∣h)=N(μ(h),σ2)p(x|h) = \mathcal{N}(\mu(h), \sigma^2)p(x∣h)=N(μ(h),σ2) 或伯努利分布

这种方法是 变分自编码器(VAE) 的基础。训练目标变为变分下界(ELBO)

L=Eq(h∣x)log⁡p(x∣h)−DKL(q(h∣x)∥p(h))\mathcal{L} = \mathbb{E}{q(h|x)}\\log p(x\|h) - D{KL}(q(h|x) \| p(h))L=Eq(h∣x)logp(x∣h)−DKL(q(h∣x)∥p(h))

其中第一项是重构项,第二项是KL正则项,使后验 q(h∣x)q(h|x)q(h∣x) 接近先验 p(h)=N(0,1)p(h)=\mathcal{N}(0,1)p(h)=N(0,1)。

重参数化技巧(Reparameterization Trick)

h=μ+σ⋅ϵ,ϵ∼N(0,1)h = \mu + \sigma \cdot \epsilon, \quad \epsilon \sim \mathcal{N}(0,1)h=μ+σ⋅ϵ,ϵ∼N(0,1)

使得采样过程可微分,梯度可以通过 μ\muμ 和 σ\sigmaσ 反向传播。

4.2 案例代码:变分自编码器(VAE)

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# ======================== 超参数 ========================
batch_size = 128
num_epochs = 30
learning_rate = 1e-3
latent_dim = 20               # 潜在空间维度(z的维度)
input_dim = 784               # 输入维度

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)

# ======================== VAE定义 ========================
class VAE(nn.Module):
    """
    变分自编码器(Variational Autoencoder):
    - 编码器输出分布参数(μ, log σ²)而非确定性编码
    - 使用重参数化技巧使采样可微分
    - 损失 = 重构损失 + KL散度
    """
    def __init__(self, input_dim=784, latent_dim=20, hidden_dim=400):
        super(VAE, self).__init__()
        
        # ---- 编码器:输入 → 分布参数(μ, log σ²) ----
        self.encoder_fc = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),    # 784 → 400
            nn.ReLU(inplace=True),               # ReLU激活
            nn.Linear(hidden_dim, hidden_dim),   # 400 → 400
            nn.ReLU(inplace=True)
        )
        # 均值头:输出潜在分布的均值 μ
        self.fc_mu = nn.Linear(hidden_dim, latent_dim)          # 400 → 20
        # 对数方差头:输出 log(σ²),用log保证数值稳定
        self.fc_logvar = nn.Linear(hidden_dim, latent_dim)      # 400 → 20
        
        # ---- 解码器:潜在变量 → 重构 ----
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, hidden_dim),   # 20 → 400
            nn.ReLU(inplace=True),
            nn.Linear(hidden_dim, hidden_dim),   # 400 → 400
            nn.ReLU(inplace=True),
            nn.Linear(hidden_dim, input_dim),    # 400 → 784
            nn.Sigmoid()                         # 输出[0,1]
        )
    
    def encode(self, x):
        """
        编码过程:输入 → (均值, 对数方差)
        返回分布的参数,而非确定性编码
        """
        h = self.encoder_fc(x)                   # 提取特征
        mu = self.fc_mu(h)                       # 计算均值 μ
        logvar = self.fc_logvar(h)               # 计算 log(σ²)
        return mu, logvar
    
    def reparameterize(self, mu, logvar):
        """
        重参数化技巧:
        z = μ + σ · ε,其中 ε ~ N(0,1)
        
        这样采样过程的随机性来自ε,而μ和σ是确定性的,
        梯度可以通过μ和σ反向传播
        
        参数:
            mu: 均值,形状 [batch_size, latent_dim]
            logvar: 对数方差,形状 [batch_size, latent_dim]
        返回:
            z: 潜在变量,形状 [batch_size, latent_dim]
        """
        std = torch.exp(0.5 * logvar)           # σ = exp(0.5 * log σ²) = √(σ²)
        eps = torch.randn_like(std)              # ε ~ N(0,1),与std同形状
        z = mu + std * eps                       # 重参数化:z = μ + σ·ε
        return z
    
    def decode(self, z):
        """解码过程:潜在变量 → 重构"""
        return self.decoder(z)
    
    def forward(self, x):
        """
        完整前向传播:
        1. 编码得到分布参数
        2. 重参数化采样
        3. 解码重构
        
        返回: (重构, μ, log σ²)
        """
        mu, logvar = self.encode(x)              # 编码:获取分布参数
        z = self.reparameterize(mu, logvar)      # 采样:重参数化技巧
        reconstructed = self.decode(z)           # 解码:生成重构
        return reconstructed, mu, logvar

# ======================== VAE损失函数 ========================
def vae_loss_function(reconstructed, x, mu, logvar):
    """
    VAE损失函数 = 重构损失 + KL散度
    
    1. 重构损失:二元交叉熵或MSE
       L_recon = Σ_i [x_i log(x̂_i) + (1-x_i)log(1-x̂_i)]
    
    2. KL散度:使后验q(z|x)接近先验p(z)=N(0,I)
       L_KL = -0.5 * Σ_j (1 + log σ²_j - μ²_j - σ²_j)
    
    参数:
        reconstructed: 重构输出
        x: 原始输入
        mu: 编码器输出的均值
        logvar: 编码器输出的对数方差
    返回:
        total_loss, recon_loss, kl_loss
    """
    # 重构损失(BCE):逐像素比较
    # reduction='sum':对所有元素求和(而非求均值)
    recon_loss = nn.functional.binary_cross_entropy(
        reconstructed, x, reduction='sum'
    )
    
    # KL散度(解析公式):
    # KL(N(μ,σ²) || N(0,1)) = -0.5 * Σ(1 + log(σ²) - μ² - σ²)
    # 推导:对高斯分布的KL散度有闭式解
    kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    # logvar.pow(2) 是错误的,应该是 logvar.exp() 即 σ²
    
    total_loss = recon_loss + kl_loss           # ELBO = -总损失
    
    return total_loss, recon_loss, kl_loss

# ======================== 模型初始化与训练 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = VAE(input_dim, latent_dim).to(device)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 训练
for epoch in range(num_epochs):
    model.train()                               # 训练模式
    total_loss_epoch = 0
    recon_loss_epoch = 0
    kl_loss_epoch = 0
    
    for batch_idx, (data, _) in enumerate(train_loader):
        data = data.view(data.size(0), -1).to(device)  # 展平
        
        # 前向传播
        reconstructed, mu, logvar = model(data)
        
        # 计算损失
        total_loss, recon_loss, kl_loss = vae_loss_function(
            reconstructed, data, mu, logvar
        )
        
        # 反向传播
        optimizer.zero_grad()
        total_loss.backward()
        optimizer.step()
        
        # 累加损失
        total_loss_epoch += total_loss.item()
        recon_loss_epoch += recon_loss.item()
        kl_loss_epoch += kl_loss.item()
    
    n = len(train_loader.dataset)
    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}] '
              f'Total: {total_loss_epoch/n:.4f}, '
              f'Recon: {recon_loss_epoch/n:.4f}, '
              f'KL: {kl_loss_epoch/n:.4f}')

# ======================== 从潜在空间采样生成新图像 ========================
model.eval()                                    # 评估模式
with torch.no_grad():
    # 从标准正态分布采样随机潜在向量
    z = torch.randn(16, latent_dim).to(device)  # 16个随机向量,每个20维
    
    # 解码生成图像
    generated = model.decode(z).cpu()            # [16, 784]
    generated = generated.view(16, 1, 28, 28)   # 恢复为图像形状

# 显示生成的图像
fig, axes = plt.subplots(2, 8, figsize=(16, 4))
for i in range(16):
    ax = axes[i // 8, i % 8]
    ax.imshow(generated[i].squeeze(), cmap='gray')
    ax.axis('off')
plt.suptitle('VAE: Generated Samples from Random Latent Vectors', fontsize=14)
plt.tight_layout()
plt.show()

# ======================== 潜在空间插值 ========================
with torch.no_grad():
    # 取两个样本
    sample1, _ = train_dataset[0]               # 第一张图
    sample2, _ = train_dataset[1]               # 第二张图
    
    # 编码得到潜在向量
    s1 = sample1.view(1, -1).to(device)
    s2 = sample2.view(1, -1).to(device)
    mu1, _ = model.encode(s1)                    # 样本1的μ
    mu2, _ = model.encode(s2)                    # 样本2的μ
    
    # 在两个潜在向量之间线性插值
    n_interp = 10                                # 插值步数
    interpolated = []
    for alpha in np.linspace(0, 1, n_interp):
        z = (1 - alpha) * mu1 + alpha * mu2      # 线性插值:z = (1-t)z₁ + tz₂
        img = model.decode(z).cpu().view(28, 28) # 解码
        interpolated.append(img)

# 可视化插值结果
fig, axes = plt.subplots(1, n_interp, figsize=(20, 2.5))
for i, img in enumerate(interpolated):
    axes[i].imshow(img.numpy(), cmap='gray')
    axes[i].axis('off')
    if i == 0:
        axes[i].set_title('Sample 1', fontsize=10)
    elif i == n_interp - 1:
        axes[i].set_title('Sample 2', fontsize=10)
plt.suptitle('Latent Space Interpolation', fontsize=14)
plt.tight_layout()
plt.show()

五、去噪自编码器详解 (Denoising Autoencoder)

5.1 核心知识点

去噪自编码器(Denoising Autoencoder, DAE) 的核心思想:

  1. 对输入 xxx 施加噪声 得到 x~\tilde{x}x~
  2. 训练目标是:从损坏的输入 x~\tilde{x}x~ 重构出原始干净的输入 xxx

L(x,g(f(x~)))L(x, g(f(\tilde{x})))L(x,g(f(x~)))

为什么DAE能学到好的表示?

Bengio等人证明:DAE的训练等价于隐式地估计数据分布的得分函数 (score function)∇xlog⁡p(x)\nabla_x \log p(x)∇xlogp(x)。具体来说:

  • DAE学习的是数据流形的局部方向
  • 靠近流形的点被推回到流形上
  • 这使得DAE具有特征提取数据生成的双重能力

常用噪声类型:

噪声类型 适用场景 参数
高斯噪声 连续数据 x~=x+ϵ,ϵ∼N(0,σ2)\tilde{x} = x + \epsilon, \epsilon \sim \mathcal{N}(0, \sigma^2)x~=x+ϵ,ϵ∼N(0,σ2)
掩码噪声(Dropout) 通用 随机将部分输入置零
椒盐噪声 图像 随机将部分像素设为最大/最小值

5.2 案例代码:多种噪声类型的DAE

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# ======================== 超参数 ========================
batch_size = 128
num_epochs = 20
learning_rate = 1e-3
input_dim = 784
encoding_dim = 128            # 编码维度
noise_factor = 0.3            # 高斯噪声强度系数
mask_ratio = 0.3              # 掩码噪声的遮蔽比例
salt_pepper_ratio = 0.1       # 椒盐噪声比例

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
test_dataset = torchvision.datasets.MNIST(
    root='./data', train=False, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)
test_loader = torch.utils.data.DataLoader(
    dataset=test_dataset, batch_size=batch_size, shuffle=False, num_workers=2
)

# ======================== 噪声添加函数 ========================
def add_gaussian_noise(x, noise_factor=0.3):
    """
    添加高斯噪声:x̃ = x + σ·ε, ε~N(0,1)
    
    参数:
        x: 干净输入,像素值在[0,1]之间
        noise_factor: 噪声强度σ
    返回:
        加噪后并裁剪到[0,1]的输入
    """
    noise = torch.randn_like(x) * noise_factor   # 生成同形状的标准正态噪声,乘以强度
    noisy_x = x + noise                           # 叠加噪声
    noisy_x = torch.clamp(noisy_x, 0., 1.)       # 裁剪到[0,1],确保有效像素值
    return noisy_x

def add_mask_noise(x, mask_ratio=0.3):
    """
    添加掩码噪声(类似Dropout):随机将部分像素值设为0
    
    参数:
        x: 干净输入
        mask_ratio: 被遮蔽的像素比例(0到1之间)
    返回:
        部分像素被置零的输入
    """
    # 生成伯努利掩码:1的概率为(1-mask_ratio),0的概率为mask_ratio
    mask = torch.bernoulli(torch.full_like(x, 1 - mask_ratio))
    # torch.bernoulli:从伯努利分布中采样,参数为每个元素为1的概率
    return x * mask                               # 逐元素相乘,被遮蔽位置变为0

def add_salt_pepper_noise(x, ratio=0.1):
    """
    添加椒盐噪声:随机将像素设为0(椒)或1(盐)
    
    参数:
        x: 干净输入
        ratio: 被污染的像素总比例
    返回:
        受椒盐噪声污染的输入
    """
    noisy_x = x.clone()                           # 克隆输入,避免修改原始数据
    num_salt = int(ratio / 2 * x.numel())         # 盐噪声数量 = 总像素 × ratio/2
    num_pepper = int(ratio / 2 * x.numel())       # 椒噪声数量 = 总像素 × ratio/2
    
    # 盐噪声:随机位置设为1(最亮)
    # torch.randint生成随机索引
    salt_idx = torch.randint(0, x.numel(), (num_salt,))
    noisy_x.view(-1)[salt_idx] = 1.0
    
    # 椒噪声:随机位置设为0(最暗)
    pepper_idx = torch.randint(0, x.numel(), (num_pepper,))
    noisy_x.view(-1)[pepper_idx] = 0.0
    
    return noisy_x

# ======================== 去噪自编码器模型 ========================
class DenoisingAutoencoder(nn.Module):
    """
    去噪自编码器:
    - 输入:带噪声的数据
    - 目标:重构出干净数据
    - 编码器和解码器使用对称结构
    - 使用Dropout作为正则化手段
    """
    def __init__(self, input_dim=784, encoding_dim=128):
        super(DenoisingAutoencoder, self).__init__()
        
        # 编码器:带Dropout的深层网络
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 512),           # 784 → 512
            nn.BatchNorm1d(512),                 # 批归一化
            nn.ReLU(inplace=True),
            nn.Dropout(0.2),                     # Dropout:训练时随机丢弃20%神经元
            
            nn.Linear(512, 256),                 # 512 → 256
            nn.BatchNorm1d(256),
            nn.ReLU(inplace=True),
            nn.Dropout(0.2),
            
            nn.Linear(256, encoding_dim),        # 256 → 128
            nn.ReLU(inplace=True)
        )
        
        # 解码器:对称结构(解码器通常不用Dropout)
        self.decoder = nn.Sequential(
            nn.Linear(encoding_dim, 256),        # 128 → 256
            nn.BatchNorm1d(256),
            nn.ReLU(inplace=True),
            
            nn.Linear(256, 512),                 # 256 → 512
            nn.BatchNorm1d(512),
            nn.ReLU(inplace=True),
            
            nn.Linear(512, input_dim),           # 512 → 784
            nn.Sigmoid()                         # 输出[0,1]
        )
    
    def forward(self, x):
        """
        前向传播:
        x(带噪) → encoder → 编码(去噪特征) → decoder → 重构(干净)
        """
        encoded = self.encoder(x)                # 编码:从噪声输入中提取有用特征
        decoded = self.decoder(encoded)          # 解码:生成干净的重构
        return decoded

# ======================== 模型初始化 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = DenoisingAutoencoder(input_dim, encoding_dim).to(device)
criterion = nn.MSELoss()                         # 重构损失
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# ======================== 训练循环 ========================
train_losses = []

for epoch in range(num_epochs):
    model.train()                                # 训练模式(启用Dropout)
    epoch_loss = 0.0
    
    for batch_idx, (data, _) in enumerate(train_loader):
        data = data.view(data.size(0), -1).to(device)  # 展平
        
        # ---- 添加噪声 ----
        # 混合使用多种噪声,增强鲁棒性
        noisy_data = add_gaussian_noise(data, noise_factor)  # 高斯噪声
        
        # ---- 前向传播 ----
        reconstructed = model(noisy_data)        # 从带噪输入重构
        
        # 注意:损失是与干净数据比较,不是与噪声数据比较!
        loss = criterion(reconstructed, data)    # 目标是恢复干净数据
        
        # ---- 反向传播 ----
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        epoch_loss += loss.item()
    
    avg_loss = epoch_loss / len(train_loader)
    train_losses.append(avg_loss)
    
    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.6f}')

# ======================== 测试与可视化 ========================
model.eval()                                     # 评估模式(禁用Dropout)
with torch.no_grad():
    # 取一批测试数据
    test_data, _ = next(iter(test_loader))
    test_data = test_data.view(test_data.size(0), -1).to(device)
    
    # 用不同噪声类型测试
    gaussian_noisy = add_gaussian_noise(test_data, noise_factor)
    mask_noisy = add_mask_noise(test_data, mask_ratio)
    sp_noisy = add_salt_pepper_noise(test_data, salt_pepper_ratio)
    
    # 从各噪声版本重构
    recon_gaussian = model(gaussian_noisy)
    recon_mask = model(mask_noisy)
    recon_sp = model(sp_noisy)

# 可视化:原始 → 各噪声版本 → 各重构版本
n = 6  # 展示6个样本
fig, axes = plt.subplots(5, n, figsize=(15, 12))
row_labels = ['Original', 'Gaussian Noise', 'Recon from Gauss',
              'Mask Noise', 'Recon from Mask']

for i in range(n):
    axes[0, i].imshow(test_data[i].cpu().view(28,28), cmap='gray')
    axes[1, i].imshow(gaussian_noisy[i].cpu().view(28,28), cmap='gray')
    axes[2, i].imshow(recon_gaussian[i].cpu().view(28,28), cmap='gray')
    axes[3, i].imshow(mask_noisy[i].cpu().view(28,28), cmap='gray')
    axes[4, i].imshow(recon_mask[i].cpu().view(28,28), cmap='gray')

for i, label in enumerate(row_labels):
    axes[i, 0].set_ylabel(label, fontsize=11, rotation=0, labelpad=100)

for ax_row in axes:
    for ax in ax_row:
        ax.axis('off')

plt.suptitle('Denoising Autoencoder: Multiple Noise Types', fontsize=15)
plt.tight_layout()
plt.show()

六、使用自编码器学习流形 (Learning Manifolds with Autoencoders)

6.1 核心知识点

流形假设(Manifold Hypothesis):高维数据(如图像)实际上集中分布在低维流形附近。

自编码器学习流形的机制:

  • 编码器 :将高维数据投影到低维流形坐标(流形的参数化
  • 解码器 :将低维坐标映射回高维空间(流形的生成
  • 重构误差最小化迫使解码器的输出空间逼近数据流形

流形学习的关键特性:

  1. 流形是局部欧几里得的:流形上相近的点映射到编码空间中也相近
  2. 流形有内在维度:低于数据的嵌入维度(如MNIST图像784维,但流形可能只有10-20维)
  3. 解码器定义了一个从低维到高维的光滑映射

6.2 案例代码:流形学习可视化

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
from sklearn.manifold import TSNE              # t-SNE降维可视化

# ======================== 超参数 ========================
batch_size = 128
num_epochs = 30
learning_rate = 1e-3
latent_dim = 2                 # 设为2维,方便直接可视化流形

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)

# ======================== 2D流形学习自编码器 ========================
class ManifoldAutoencoder(nn.Module):
    """
    将数据压缩到2维潜在空间,直接可视化流形结构
    使用更深的网络来补偿极低的编码维度
    """
    def __init__(self, latent_dim=2):
        super(ManifoldAutoencoder, self).__init__()
        
        # 编码器:784维 → 2维
        self.encoder = nn.Sequential(
            nn.Linear(784, 512),                 # 784 → 512
            nn.LeakyReLU(0.2, inplace=True),    # LeakyReLU:负半轴斜率0.2,避免死神经元
            nn.Linear(512, 256),                 # 512 → 256
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 128),                 # 256 → 128
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(128, latent_dim)           # 128 → 2(极低维流形坐标)
        )
        
        # 解码器:2维 → 784维
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 128),          # 2 → 128
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(128, 256),                 # 128 → 256
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 512),                 # 256 → 512
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 784),                 # 512 → 784
            nn.Sigmoid()
        )
    
    def forward(self, x):
        z = self.encoder(x)                      # 编码到流形坐标
        reconstructed = self.decoder(z)          # 从流形坐标重构
        return reconstructed, z                  # 同时返回潜在坐标用于可视化

# ======================== 训练 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = ManifoldAutoencoder(latent_dim).to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
# 学习率调度器:每10个epoch学习率减半,帮助后期精细调整

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0.0
    
    for data, _ in train_loader:
        data = data.view(data.size(0), -1).to(device)
        reconstructed, z = model(data)
        loss = criterion(reconstructed, data)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()
    
    scheduler.step()                             # 更新学习率
    avg_loss = epoch_loss / len(train_loader)
    
    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.6f}, '
              f'LR: {scheduler.get_last_lr()[0]:.6f}')

# ======================== 流形可视化 ========================
model.eval()
all_z = []                                       # 存储所有样本的2D坐标
all_labels = []                                  # 存储对应标签

with torch.no_grad():
    for data, labels in train_loader:
        data = data.view(data.size(0), -1).to(device)
        _, z = model(data)                       # 获取2D潜在坐标
        all_z.append(z.cpu().numpy())
        all_labels.append(labels.numpy())

all_z = np.concatenate(all_z, axis=0)            # 拼接所有batch的坐标
all_labels = np.concatenate(all_labels, axis=0)

# 散点图:每个数字用不同颜色
plt.figure(figsize=(12, 10))
scatter = plt.scatter(
    all_z[:, 0],                                 # x坐标
    all_z[:, 1],                                 # y坐标
    c=all_labels,                                # 颜色按数字标签映射
    cmap='tab10',                                # 10色离散色图
    alpha=0.5,                                   # 透明度
    s=3                                          # 点大小
)
plt.colorbar(scatter, ticks=range(10), label='Digit')
plt.xlabel('Latent Dimension 1', fontsize=12)
plt.ylabel('Latent Dimension 2', fontsize=12)
plt.title('Learned 2D Manifold of MNIST', fontsize=14)
plt.grid(True, alpha=0.3)
plt.show()

# ======================== 流形网格采样:生成数字全景图 ========================
with torch.no_grad():
    # 在2D潜在空间中创建均匀网格
    n_points = 20                                # 每个维度20个点
    # 设定网格范围(根据数据分布调整)
    z1_range = np.linspace(-4, 4, n_points)      # 维度1的范围
    z2_range = np.linspace(-4, 4, n_points)      # 维度2的范围
    
    # 创建网格
    grid_z1, grid_z2 = np.meshgrid(z1_range, z2_range)
    # meshgrid生成2D网格坐标矩阵,每个形状[n_points, n_points]
    
    # 展平为一维并组合
    z_grid = np.column_stack([grid_z1.ravel(), grid_z2.ravel()])
    # ravel()展平为1D,column_stack组合为[N, 2]
    
    # 转为Tensor并解码
    z_tensor = torch.FloatTensor(z_grid).to(device)
    generated = model.decoder(z_tensor).cpu()     # [N, 784]
    generated = generated.view(-1, 1, 28, 28)     # [N, 1, 28, 28]

# 拼接为大图
fig, axes = plt.subplots(n_points, n_points, figsize=(20, 20))
for i in range(n_points):
    for j in range(n_points):
        idx = i * n_points + j                   # 网格索引映射到展平索引
        axes[i, j].imshow(generated[idx].squeeze(), cmap='gray')
        axes[i, j].axis('off')

plt.suptitle('Manifold Grid: Decoded Digits Across 2D Latent Space', fontsize=16, y=0.98)
plt.tight_layout()
plt.show()

七、收缩自编码器 (Contractive Autoencoder, CAE)

7.1 核心知识点

收缩自编码器(CAE) 通过惩罚编码器对输入的Jacobian矩阵来学习鲁棒表示。

核心损失函数:

LCAE=L(x,g(f(x)))+λ∥∂f(x)∂x∥F2L_{CAE} = L(x, g(f(x))) + \lambda \left\| \frac{\partial f(x)}{\partial x} \right\|_F^2LCAE=L(x,g(f(x)))+λ ∂x∂f(x) F2

其中 ∥Jf(x)∥F2\left\| J_f(x) \right\|_F^2∥Jf(x)∥F2 是编码器函数 fff 关于输入 xxx 的 Jacobian 矩阵的 Frobenius 范数

直观理解:

  • Jacobian 衡量编码对输入微小变化的敏感度
  • 惩罚 Jacobian 意味着:输入微小变化不应导致编码大幅变化
  • 这迫使编码器学习对噪声鲁棒的特征
  • 与DAE不同,CAE是解析地(而非随机地)实现对输入扰动的不变性

与DAE的联系:

  • DAE通过随机噪声实现对输入扰动的鲁棒性
  • CAE通过Jacobian惩罚直接约束编码函数的局部几何
  • 当噪声趋于零时,DAE的去噪误差等价于CAE的收缩惩罚

7.2 案例代码:收缩自编码器

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# ======================== 超参数 ========================
batch_size = 128
num_epochs = 20
learning_rate = 1e-3
input_dim = 784
encoding_dim = 64
contractive_weight = 1e-4     # 收缩正则化系数λ

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)

# ======================== 收缩自编码器定义 ========================
class ContractiveAutoencoder(nn.Module):
    """
    收缩自编码器:
    - 编码器输出h = σ(Wx + b)
    - 收缩惩罚 = ||∂h/∂x||²_F
    - 对于sigmoid激活:||J||²_F = Σ_j h_j²(1-h_j)² Σ_i W_ij²
    """
    def __init__(self, input_dim=784, encoding_dim=64):
        super(ContractiveAutoencoder, self).__init__()
        
        # 编码器:单层线性变换 + Sigmoid激活
        # 使用单层是为了方便解析计算Jacobian
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, encoding_dim),  # 784 → 64
            nn.Sigmoid()                         # Sigmoid激活
        )
        
        # 解码器
        self.decoder = nn.Sequential(
            nn.Linear(encoding_dim, input_dim),  # 64 → 784
            nn.Sigmoid()
        )
        
        # 保存编码器第一个线性层的引用(用于获取权重矩阵W)
        self.encoder_linear = self.encoder[0]    # nn.Linear(784, 64)
    
    def forward(self, x):
        h = self.encoder(x)                      # 编码
        reconstructed = self.decoder(h)          # 解码
        return reconstructed, h
    
    def contractive_loss(self, h):
        """
        计算收缩正则化项:
        
        对于单层Sigmoid编码器 h = σ(Wx + b):
        Jacobian J = ∂h/∂x = diag(h⊙(1-h)) · W
        
        ||J||²_F = Σ_j (h_j(1-h_j))² · ||W_j||²
        
        其中W_j是权重矩阵的第j行
        """
        # 获取权重矩阵 W,形状 [encoding_dim, input_dim]
        W = self.encoder_linear.weight           # 权重矩阵
        
        # 计算 h_j * (1 - h_j),形状 [batch_size, encoding_dim]
        # 这是sigmoid的导数:σ'(z) = σ(z)(1 - σ(z))
        h_deri = h * (1 - h)
        
        # Frobenius范数的平方
        # ||J||²_F = Σ_j [h_j(1-h_j)]² · ||W_j||²
        # 对batch取均值
        
        # (h_deri)² 的形状:[batch_size, encoding_dim]
        # W² 对每行求和得到 ||W_j||²,形状:[encoding_dim]
        # 两者逐元素相乘后求和
        
        # W.pow(2).sum(dim=1):对每个输出神经元,计算其权重向量的L2范数平方
        # 形状:[encoding_dim]
        W_squared_sum = W.pow(2).sum(dim=1)      # ||W_j||² for each j
        
        # 收缩惩罚 = (1/batch) * Σ_sample Σ_j [h_j(1-h_j)]² · ||W_j||²
        contractive = torch.sum(
            h_deri.pow(2) * W_squared_sum,        # [B, D] * [D] 广播
            dim=1                                  # 对编码维度求和
        ).mean()                                   # 对batch取均值
        
        return contractive

# ======================== 模型初始化 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = ContractiveAutoencoder(input_dim, encoding_dim).to(device)
criterion = nn.MSELoss()                         # 重构损失
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# ======================== 训练循环 ========================
losses_recon = []
losses_contractive = []
losses_total = []

for epoch in range(num_epochs):
    model.train()
    epoch_recon = 0.0
    epoch_contractive = 0.0
    epoch_total = 0.0
    
    for data, _ in train_loader:
        data = data.view(data.size(0), -1).to(device)
        
        # 前向传播
        reconstructed, h = model(data)
        
        # 计算各项损失
        recon_loss = criterion(reconstructed, data)              # 重构损失
        contra_loss = model.contractive_loss(h)                  # 收缩正则项
        total_loss = recon_loss + contractive_weight * contra_loss  # 总损失
        
        # 反向传播
        optimizer.zero_grad()
        total_loss.backward()
        optimizer.step()
        
        epoch_recon += recon_loss.item()
        epoch_contractive += contra_loss.item()
        epoch_total += total_loss.item()
    
    n = len(train_loader)
    losses_recon.append(epoch_recon / n)
    losses_contractive.append(epoch_contractive / n)
    losses_total.append(epoch_total / n)
    
    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}] '
              f'Recon: {losses_recon[-1]:.6f}, '
              f'Contractive: {losses_contractive[-1]:.6f}, '
              f'Total: {losses_total[-1]:.6f}')

# ======================== 可视化损失分量 ========================
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

ax1.plot(losses_recon, label='Reconstruction', linewidth=2)
ax1.plot(losses_total, label='Total', linewidth=2)
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.set_title('Reconstruction & Total Loss')
ax1.legend()
ax1.grid(True, alpha=0.3)

ax2.plot(losses_contractive, label='Contractive Penalty', linewidth=2, color='red')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Contractive Loss')
ax2.set_title('Contractive Regularization Term')
ax2.legend()
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# ======================== 学习到的编码器权重可视化 ========================
model.eval()
# 获取编码器权重矩阵 W,形状 [encoding_dim, input_dim]
W = model.encoder_linear.weight.data.cpu().numpy()

# 可视化前16个编码器神经元学到的权重(可视为特征检测器)
fig, axes = plt.subplots(2, 8, figsize=(16, 4))
for i in range(16):
    ax = axes[i // 8, i % 8]
    weight_img = W[i].reshape(28, 28)           # 将78维权重重塑为28x28图像
    ax.imshow(weight_img, cmap='RdBu_r',        # 红蓝双色图:正红负蓝
              vmin=-weight_img.std()*2,          # 对称色标
              vmax=weight_img.std()*2)
    ax.axis('off')
    ax.set_title(f'Neuron {i}', fontsize=8)

plt.suptitle('Contractive AE: Learned Encoder Weight Patterns', fontsize=14)
plt.tight_layout()
plt.show()

八、预测稀疏分解 (Predictive Sparse Decomposition, PSD)

8.1 核心知识点

预测稀疏分解(PSD) 结合了稀疏编码自编码器的思想:

  1. 稀疏编码 :对于输入 xxx,寻找稀疏系数 hhh 使得 x≈Dhx \approx Dhx≈Dh,其中 DDD 是字典
  2. 预测器 :训练一个编码器 f(x)f(x)f(x) 来预测最优稀疏系数,避免在线优化

目标函数:

min⁡D,θ∑i∥xi−Dhi∥22+λ∥hi∥1+γ∥hi−fθ(xi)∥22\min_{D, \theta} \sum_i \left \\\|x_i - D h_i\\\|_2\^2 + \\lambda \\\|h_i\\\|_1 + \\gamma \\\|h_i - f_\\theta(x_i)\\\|_2\^2 \\rightD,θmini∑∥xi−Dhi∥22+λ∥hi∥1+γ∥hi−fθ(xi)∥22

其中:

  • ∥x−Dh∥22\|x - Dh\|2^2∥x−Dh∥22:重构误差(字典 DDD 的约束通常有 ∥D⋅j∥2≤1\|D{\cdot j}\|_2 \leq 1∥D⋅j∥2≤1)
  • λ∥h∥1\lambda \|h\|_1λ∥h∥1:稀疏惩罚(L1范数,鼓励大部分 hj=0h_j=0hj=0)
  • γ∥h−f(x)∥22\gamma \|h - f(x)\|_2^2γ∥h−f(x)∥22:预测一致性惩罚(编码器输出应接近稀疏编码)

训练过程交替进行:

  1. 固定 DDD 和 θ\thetaθ,对每个样本求解 hhh(LASSO)
  2. 固定 hhh,更新 DDD(字典学习)和 θ\thetaθ(编码器训练)

8.2 案例代码:PSD简化实现

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import Lasso          # LASSO用于稀疏编码求解

# ======================== 超参数 ========================
batch_size = 128
num_epochs = 15
learning_rate = 1e-3
input_dim = 784
n_atoms = 256                # 字典原子数(字典D的列数)
sparse_lambda = 0.1          # 稀疏正则化系数λ
predictor_weight = 0.1       # 预测一致性系数γ
max_iter_lasso = 100         # LASSO最大迭代次数

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
)

# ======================== 稀疏编码预测器(编码器) ========================
class SparsePredictor(nn.Module):
    """
    预测网络:直接从输入预测稀疏编码
    替代在线LASSO求解,实现快速推理
    """
    def __init__(self, input_dim=784, n_atoms=256):
        super(SparsePredictor, self).__init__()
        
        self.network = nn.Sequential(
            nn.Linear(input_dim, 512),           # 784 → 512
            nn.ReLU(inplace=True),
            nn.Linear(512, 256),                 # 512 → 256
            nn.ReLU(inplace=True),
            nn.Linear(256, n_atoms),             # 256 → 256(预测稀疏编码维度)
            nn.Sigmoid()                         # Sigmoid:输出在(0,1)之间
            # 注意:这里用Sigmoid代替ReLU以获得有界的稀疏编码
        )
    
    def forward(self, x):
        """输入→预测的稀疏编码"""
        return self.network(x)

# ======================== PSD模型 ========================
class PSDModel:
    """
    预测稀疏分解模型:
    - 字典D:可学习参数
    - 预测器f_θ:神经网络
    - 交替训练:稀疏编码→字典更新→预测器更新
    """
    def __init__(self, input_dim, n_atoms, device):
        self.device = device
        self.input_dim = input_dim
        self.n_atoms = n_atoms
        
        # 初始化字典D:使用随机正交初始化
        # D的形状:[input_dim, n_atoms],每列是一个"原子"
        self.dictionary = nn.Parameter(
            torch.randn(input_dim, n_atoms, device=device) * 0.01
        )
        # 约束字典列的范数不超过1
        self._normalize_dictionary()
        
        # 预测器网络
        self.predictor = SparsePredictor(input_dim, n_atoms).to(device)
        
        # 优化器:同时优化字典和预测器参数
        self.optimizer = optim.Adam(
            [
                {'params': [self.dictionary], 'lr': learning_rate},      # 字典参数
                {'params': self.predictor.parameters(), 'lr': learning_rate}  # 预测器参数
            ],
            weight_decay=1e-5
        )
    
    def _normalize_dictionary(self):
        """将字典的每一列归一化到单位范数"""
        with torch.no_grad():
            # dim=0 对行求范数(每列的L2范数)
            norms = torch.norm(self.dictionary, dim=0, keepdim=True)
            norms = torch.clamp(norms, min=1e-8)   # 防止除零
            self.dictionary.data /= norms            # 归一化
    
    def sparse_encode_batch(self, x, n_iters=50, lr=0.01):
        """
        使用ISTA(迭代收缩阈值算法)对batch进行稀疏编码
        
        ISTA更新规则:
        h^(t+1) = S_λ(h^(t) - α·∇L/∇h)
        其中S_λ是软阈值函数:S_λ(x) = sign(x)·max(|x|-λ, 0)
        
        参数:
            x: 输入数据 [batch_size, input_dim]
            n_iters: ISTA迭代次数
            lr: ISTA学习率
        返回:
            h: 稀疏编码 [batch_size, n_atoms]
        """
        B = x.shape[0]
        h = torch.zeros(B, self.n_atoms, device=self.device)  # 初始化稀疏编码为零
        
        D = self.dictionary                                  # [input_dim, n_atoms]
        DtD = D.t() @ D                                      # [n_atoms, n_atoms] 预计算
        Dtx = D.t() @ x.t()                                  # [n_atoms, batch_size]
        Dtx = Dtx.t()                                        # [batch_size, n_atoms]
        
        for _ in range(n_iters):
            # 梯度下降步:∇_h ||x - Dh||² = -2D^T(x - Dh) = 2(DtD·h - Dtx)
            grad = 2.0 * (h @ DtD - Dtx)                    # [B, n_atoms]
            h = h - lr * grad                                # 梯度下降
            
            # 软阈值:S_λ(h) = sign(h) * max(|h| - λ, 0)
            h = torch.sign(h) * torch.clamp(torch.abs(h) - sparse_lambda * lr, min=0)
        
        return h
    
    def train_step(self, x):
        """
        一步训练:
        1. 用当前字典求稀疏编码h(ISTA)
        2. 用预测器预测h_pred
        3. 更新字典和预测器
        """
        # ---- 步骤1:稀疏编码 ----
        with torch.no_grad():
            h_sparse = self.sparse_encode_batch(x, n_iters=50)  # ISTA求解
        
        # ---- 步骤2:预测 ----
        h_pred = self.predictor(x)                   # 预测器输出
        
        # ---- 步骤3:计算损失 ----
        # 重构损失:||x - D·h||²
        x_recon = h_pred @ self.dictionary.t()       # [B, input_dim]
        recon_loss = nn.functional.mse_loss(x_recon, x)
        
        # 稀疏惩罚:||h||₁
        sparsity_loss = torch.mean(torch.abs(h_pred))
        
        # 预测一致性:||h_pred - h_sparse||²
        consistency_loss = nn.functional.mse_loss(h_pred, h_sparse)
        
        # 总损失
        total_loss = recon_loss + sparse_lambda * sparsity_loss + \
                     predictor_weight * consistency_loss
        
        # ---- 步骤4:更新 ----
        self.optimizer.zero_grad()
        total_loss.backward()
        self.optimizer.step()
        self._normalize_dictionary()                  # 归一化字典
        
        return total_loss.item(), recon_loss.item(), sparsity_loss.item()

# ======================== 训练PSD ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
psd = PSDModel(input_dim, n_atoms, device)

for epoch in range(num_epochs):
    epoch_total = 0.0
    epoch_recon = 0.0
    epoch_sparse = 0.0
    
    for data, _ in train_loader:
        data = data.view(data.size(0), -1).to(device)
        t_loss, r_loss, s_loss = psd.train_step(data)
        epoch_total += t_loss
        epoch_recon += r_loss
        epoch_sparse += s_loss
    
    n = len(train_loader)
    if (epoch + 1) % 3 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}] '
              f'Total: {epoch_total/n:.6f}, '
              f'Recon: {epoch_recon/n:.6f}, '
              f'Sparsity: {epoch_sparse/n:.6f}')

# ======================== 可视化学到的字典原子 ========================
# 取字典前64个原子,可视化为28x28图像
D = psd.dictionary.data.cpu().numpy()               # [784, 256]

fig, axes = plt.subplots(4, 16, figsize=(20, 5))
for i in range(64):
    ax = axes[i // 16, i % 16]
    atom = D[:, i].reshape(28, 28)                   # 第i个原子重塑为图像
    # 归一化到[0,1]便于显示
    atom = (atom - atom.min()) / (atom.max() - atom.min() + 1e-8)
    ax.imshow(atom, cmap='gray')
    ax.axis('off')

plt.suptitle('PSD: Learned Dictionary Atoms (First 64)', fontsize=14)
plt.tight_layout()
plt.show()

# ======================== 稀疏编码可视化 ========================
with torch.no_grad():
    sample = next(iter(train_loader))[0][:10].to(device)
    sample_flat = sample.view(10, -1)
    
    # 获取预测器的稀疏编码
    h_pred = psd.predictor(sample_flat).cpu().numpy()  # [10, 256]
    # 获取实际的稀疏编码(ISTA)
    h_sparse = psd.sparse_encode_batch(sample_flat).cpu().numpy()

fig, axes = plt.subplots(2, 10, figsize=(20, 5))
for i in range(10):
    # 上行:预测的稀疏编码
    axes[0, i].bar(range(n_atoms), h_pred[i], width=1.0, color='steelblue')
    axes[0, i].set_xlim(0, n_atoms)
    axes[0, i].set_ylim(0, 1)
    axes[0, i].axis('off')
    if i == 0:
        axes[0, i].set_ylabel('Predicted', fontsize=10)
    
    # 下行:ISTA求解的稀疏编码
    axes[1, i].bar(range(n_atoms), h_sparse[i], width=1.0, color='coral')
    axes[1, i].set_xlim(0, n_atoms)
    axes[1, i].set_ylim(0, h_sparse.max() * 1.1)
    axes[1, i].axis('off')
    if i == 0:
        axes[1, i].set_ylabel('ISTA Sparse', fontsize=10)

plt.suptitle('Top: Predicted Sparse Codes | Bottom: ISTA Sparse Codes', fontsize=14)
plt.tight_layout()
plt.show()

九、自编码器的应用 (Applications of Autoencoders)

9.1 核心知识点

自编码器在实际中有广泛的应用:

应用领域 使用方式 具体场景
数据降维 编码器输出作为低维特征 可视化、聚类、预处理
异常检测 正常数据训练,异常数据重构误差大 工业缺陷检测、网络入侵
图像去噪 DAE从噪声图像恢复干净图像 医学影像、老照片修复
数据压缩 编码器压缩,解码器解压 有损压缩、特征存储
迁移学习 预训练编码器作为特征提取器 跨域学习、少样本学习
数据生成 从潜在空间采样生成新数据 VAE生成图像

9.2 案例代码一:异常检测

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# ======================== 超参数 ========================
batch_size = 128
num_epochs = 20
learning_rate = 1e-3
normal_class = 0              # 正常类别:数字0
anomaly_threshold_percentile = 95  # 异常阈值:重构误差的95百分位

# ======================== 数据准备:正常 vs 异常 ========================
transform = transforms.Compose([transforms.ToTensor()])

train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
test_dataset = torchvision.datasets.MNIST(
    root='./data', train=False, transform=transform, download=True
)

# 只用正常类别(数字0)训练自编码器
# 筛选出标签为normal_class的样本
train_mask = train_dataset.targets == normal_class
train_data_normal = train_dataset.data[train_mask].float() / 255.0  # 归一化到[0,1]

# 创建DataLoader
train_loader = torch.utils.data.DataLoader(
    torch.utils.data.TensorDataset(train_data_normal, train_data_normal),
    batch_size=batch_size, shuffle=True
)

# ======================== 异常检测自编码器 ========================
class AnomalyDetectorAE(nn.Module):
    """
    用于异常检测的自编码器:
    - 仅用正常数据训练
    - 正常数据→低重构误差
    - 异常数据→高重构误差
    """
    def __init__(self):
        super(AnomalyDetectorAE, self).__init__()
        
        self.encoder = nn.Sequential(
            nn.Linear(784, 256),                 # 784 → 256
            nn.ReLU(inplace=True),
            nn.Linear(256, 64),                  # 256 → 64
            nn.ReLU(inplace=True),
            nn.Linear(64, 16),                   # 64 → 16(高度压缩)
            nn.ReLU(inplace=True)
        )
        
        self.decoder = nn.Sequential(
            nn.Linear(16, 64),                   # 16 → 64
            nn.ReLU(inplace=True),
            nn.Linear(64, 256),                  # 64 → 256
            nn.ReLU(inplace=True),
            nn.Linear(256, 784),                 # 256 → 784
            nn.Sigmoid()
        )
    
    def forward(self, x):
        z = self.encoder(x)
        return self.decoder(z), z

# ======================== 训练 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = AnomalyDetectorAE().to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0.0
    for data, target in train_loader:
        data = data.view(data.size(0), -1).to(device)
        target = target.view(target.size(0), -1).to(device)
        
        reconstructed, _ = model(data)
        loss = criterion(reconstructed, target)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()
    
    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss/len(train_loader):.6f}')

# ======================== 异常检测评估 ========================
model.eval()
test_errors = []                                  # 存储每个样本的重构误差
test_labels = []

with torch.no_grad():
    for i in range(len(test_dataset)):
        data, label = test_dataset[i]
        data_flat = data.view(1, -1).to(device)
        reconstructed, _ = model(data_flat)
        
        # 计算每个样本的MSE
        mse = torch.mean((data_flat - reconstructed) ** 2).item()
        test_errors.append(mse)
        test_labels.append(label)

test_errors = np.array(test_errors)
test_labels = np.array(test_labels)

# 设定阈值:正常类数据重构误差的某个百分位
normal_errors = test_errors[test_labels == normal_class]
threshold = np.percentile(normal_errors, anomaly_threshold_percentile)
print(f'\nAnomaly Threshold (P{anomaly_threshold_percentile}): {threshold:.6f}')

# 计算检测准确率
# 正常类应该低于阈值,异常类应该高于阈值
predictions = test_errors > threshold             # True = 预测为异常
is_anomaly = test_labels != normal_class           # True = 实际为异常

# 精确率和召回率
true_positive = np.sum(predictions & is_anomaly)      # 真正例:异常且被正确检测
false_positive = np.sum(predictions & ~is_anomaly)    # 假正例:正常但被误报
false_negative = np.sum(~predictions & is_anomaly)    # 假负例:异常但未被检测

precision = true_positive / (true_positive + false_positive + 1e-8)
recall = true_positive / (true_positive + false_negative + 1e-8)
f1 = 2 * precision * recall / (precision + recall + 1e-8)

print(f'Precision: {precision:.4f}')
print(f'Recall: {recall:.4f}')
print(f'F1 Score: {f1:.4f}')

# ======================== 可视化:重构误差分布 ========================
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 左图:不同数字的重构误差箱线图
errors_by_class = [test_errors[test_labels == d] for d in range(10)]
bp = axes[0].boxplot(errors_by_class, labels=range(10), patch_artist=True)
axes[0].axhline(y=threshold, color='r', linestyle='--', linewidth=2, label=f'Threshold={threshold:.4f}')
axes[0].set_xlabel('Digit', fontsize=12)
axes[0].set_ylabel('Reconstruction Error', fontsize=12)
axes[0].set_title('Reconstruction Error by Class', fontsize=13)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 右图:正常vs异常的误差直方图
axes[1].hist(normal_errors, bins=50, alpha=0.7, label='Normal (digit 0)', density=True)
axes[1].hist(test_errors[is_anomaly], bins=50, alpha=0.7, label='Anomaly (other digits)', density=True)
axes[1].axvline(x=threshold, color='r', linestyle='--', linewidth=2, label='Threshold')
axes[1].set_xlabel('Reconstruction Error', fontsize=12)
axes[1].set_ylabel('Density', fontsize=12)
axes[1].set_title('Normal vs Anomaly Error Distribution', fontsize=13)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

9.3 案例代码二:图像超分辨率自编码器

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt

# ======================== 超参数 ========================
batch_size = 64
num_epochs = 20
learning_rate = 1e-3

# ======================== 数据预处理 ========================
# 同时获取高分辨率(28x28)和低分辨率(14x14)版本
transform_hr = transforms.Compose([transforms.ToTensor()])     # 高分辨率目标
transform_lr = transforms.Compose([
    transforms.Resize(14),                  # 缩小到14x14(低分辨率输入)
    transforms.Resize(28),                  # 再放大回28x28(双线性插值,模糊)
    transforms.ToTensor()
])

train_dataset_hr = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform_hr, download=True
)
train_dataset_lr = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform_lr, download=True
)

train_loader = torch.utils.data.DataLoader(
    list(zip(train_dataset_lr, train_dataset_hr)),  # 将低分辨率和高分辨率配对
    batch_size=batch_size, shuffle=True
)

# ======================== 超分辨率自编码器 ========================
class SuperResolutionAE(nn.Module):
    """
    超分辨率自编码器:
    - 输入:低分辨率图像(模糊的28x28)
    - 输出:高分辨率重构(清晰的28x28)
    - 使用卷积结构保持空间信息
    """
    def __init__(self):
        super(SuperResolutionAE, self).__init__()
        
        # 编码器:从低分辨率提取特征
        self.encoder = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),    # [B,1,28,28] → [B,32,28,28]
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),  # → [B,64,14,14]
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1), # → [B,128,7,7]
            nn.LeakyReLU(0.2, inplace=True),
        )
        
        # 瓶颈层:特征融合
        self.bottleneck = nn.Sequential(
            nn.Conv2d(128, 128, kernel_size=3, padding=1), # → [B,128,7,7]
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1), # → [B,128,7,7]
            nn.LeakyReLU(0.2, inplace=True),
        )
        
        # 解码器:上采样重建高分辨率
        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1),
            # → [B,64,14,14]
            nn.LeakyReLU(0.2, inplace=True),
            nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1),
            # → [B,32,28,28]
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(32, 1, kernel_size=3, padding=1),    # → [B,1,28,28]
            nn.Sigmoid()
        )
    
    def forward(self, x):
        """低分辨率→编码→解码→高分辨率"""
        encoded = self.encoder(x)                # 下采样编码
        bottleneck = self.bottleneck(encoded)     # 特征融合
        decoded = self.decoder(bottleneck)        # 上采样解码
        return decoded

# ======================== 训练 ========================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SuperResolutionAE().to(device)

# 结合MSE和感知损失
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0.0
    
    for (lr_data, _), (hr_data, _) in train_loader:
        lr_data = lr_data.to(device)             # 低分辨率输入
        hr_data = hr_data.to(device)             # 高分辨率目标
        
        # 前向传播
        sr_output = model(lr_data)               # 超分辨率重构
        
        # 损失:与高分辨率目标比较
        loss = criterion(sr_output, hr_data)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()
    
    if (epoch + 1) % 5 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss/len(train_loader):.6f}')

# ======================== 可视化超分辨率结果 ========================
model.eval()
with torch.no_grad():
    test_lr, test_hr = next(iter(train_loader))
    test_lr = test_lr[:8].to(device)
    test_hr = test_hr[:8].to(device)
    sr_result = model(test_lr)

fig, axes = plt.subplots(3, 8, figsize=(16, 6))
for i in range(8):
    axes[0, i].imshow(test_lr[i].cpu().squeeze(), cmap='gray')    # 低分辨率输入
    axes[0, i].axis('off')
    axes[1, i].imshow(sr_result[i].cpu().squeeze(), cmap='gray')  # 超分辨率输出
    axes[1, i].axis('off')
    axes[2, i].imshow(test_hr[i].cpu().squeeze(), cmap='gray')    # 高分辨率目标
    axes[2, i].axis('off')

axes[0, 0].set_ylabel('Low-Res\nInput', fontsize=10, rotation=0, labelpad=60)
axes[1, 0].set_ylabel('Super-Res\nOutput', fontsize=10, rotation=0, labelpad=60)
axes[2, 0].set_ylabel('High-Res\nTarget', fontsize=10, rotation=0, labelpad=60)

plt.suptitle('Super-Resolution Autoencoder', fontsize=14)
plt.tight_layout()
plt.show()

9.4 案例代码三:特征提取 + 下游分类

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
from sklearn.svm import SVC                    # 支持向量机分类器
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# ======================== 数据加载 ========================
transform = transforms.Compose([transforms.ToTensor()])

train_dataset = torchvision.datasets.MNIST(
    root='./data', train=True, transform=transform, download=True
)
test_dataset = torchvision.datasets.MNIST(
    root='./data', train=False, transform=transform, download=True
)

# 为了速度只用部分数据
train_subset = torch.utils.data.Subset(train_dataset, range(10000))
test_subset = torch.utils.data.Subset(test_dataset, range(2000))

train_loader = torch.utils.data.DataLoader(
    train_subset, batch_size=128, shuffle=True
)

# ======================== 预训练自编码器 ========================
class FeatureExtractorAE(nn.Module):
    """作为特征提取器的自编码器"""
    def __init__(self):
        super(FeatureExtractorAE, self).__init__()
        
        self.encoder = nn.Sequential(
            nn.Linear(784, 512),
            nn.ReLU(inplace=True),
            nn.Linear(512, 256),
            nn.ReLU(inplace=True),
            nn.Linear(256, 128),
            nn.ReLU(inplace=True),
            nn.Linear(128, 32),                  # 最终特征维度:32
            nn.ReLU(inplace=True)
        )
        self.decoder = nn.Sequential(
            nn.Linear(32, 128),
            nn.ReLU(inplace=True),
            nn.Linear(128, 256),
            nn.ReLU(inplace=True),
            nn.Linear(256, 512),
            nn.ReLU(inplace=True),
            nn.Linear(512, 784),
            nn.Sigmoid()
        )
    
    def forward(self, x):
        z = self.encoder(x)
        return self.decoder(z)

# ---- 阶段1:无监督预训练 ----
print("Phase 1: Unsupervised Pre-training of Autoencoder...")
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
ae = FeatureExtractorAE().to(device)
optimizer = optim.Adam(ae.parameters(), lr=1e-3)
criterion = nn.MSELoss()

for epoch in range(15):
    ae.train()
    total_loss = 0
    for data, _ in train_loader:
        data = data.view(data.size(0), -1).to(device)
        recon = ae(data)
        loss = criterion(recon, data)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    if (epoch + 1) % 5 == 0:
        print(f'  Epoch [{epoch+1}/15], Loss: {total_loss/len(train_loader):.6f}')

# ---- 阶段2:提取特征 ----
print("\nPhase 2: Extracting Features...")
ae.eval()

def extract_features(dataset, model, device):
    """用预训练编码器提取特征"""
    features = []
    labels = []
    loader = torch.utils.data.DataLoader(dataset, batch_size=256, shuffle=False)
    with torch.no_grad():
        for data, label in loader:
            data = data.view(data.size(0), -1).to(device)
            z = model.encoder(data)              # 编码器输出作为特征
            features.append(z.cpu().numpy())
            labels.append(label.numpy())
    return np.concatenate(features), np.concatenate(labels)

train_features, train_labels = extract_features(train_subset, ae, device)
test_features, test_labels = extract_features(test_subset, ae, device)

print(f'  Train features shape: {train_features.shape}')   # [10000, 32]
print(f'  Test features shape: {test_features.shape}')     # [2000, 32]

# ---- 阶段3:用SVM分类 ----
print("\nPhase 3: SVM Classification on AE Features...")
svm = SVC(kernel='rbf', C=10, gamma='scale')   # RBF核SVM
svm.fit(train_features, train_labels)            # 训练
predictions = svm.predict(test_features)         # 预测

accuracy = accuracy_score(test_labels, predictions)
print(f'\nClassification Accuracy: {accuracy:.4f}')
print(f'\nDetailed Report:')
print(classification_report(test_labels, predictions))

# ---- 对比:用原始像素特征训练SVM ----
print("\nComparison: SVM on Raw Pixel Features...")
train_raw = np.array([train_subset[i][0].numpy().flatten() for i in range(len(train_subset))])
test_raw = np.array([test_subset[i][0].numpy().flatten() for i in range(len(test_subset))])

svm_raw = SVC(kernel='rbf', C=10, gamma='scale')
svm_raw.fit(train_raw, train_labels)
predictions_raw = svm_raw.predict(test_raw)

accuracy_raw = accuracy_score(test_labels, predictions_raw)
print(f'Raw Pixels Accuracy: {accuracy_raw:.4f}')
print(f'AE Features Accuracy: {accuracy:.4f}')
print(f'Improvement: {(accuracy - accuracy_raw)*100:.2f}%')

# ======================== t-SNE可视化特征 ========================
from sklearn.manifold import TSNE

# 用t-SNE将32维特征降到2D
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
features_2d = tsne.fit_transform(test_features[:1000])

plt.figure(figsize=(10, 8))
scatter = plt.scatter(
    features_2d[:, 0], features_2d[:, 1],
    c=test_labels[:1000], cmap='tab10', alpha=0.6, s=10
)
plt.colorbar(scatter, ticks=range(10), label='Digit')
plt.xlabel('t-SNE Dimension 1')
plt.ylabel('t-SNE Dimension 2')
plt.title('t-SNE Visualization of AE Learned Features')
plt.grid(True, alpha=0.3)
plt.show()

十、总结对照表

复制代码
┌─────────────────────┬────────────────────────┬──────────────────────────┐
│       类型          │       核心约束         │        损失函数          │
├─────────────────────┼────────────────────────┼──────────────────────────┤
│ 欠完备自编码器       │ dim(h) < dim(x)       │ L(x, g(f(x)))           │
│ 稀疏自编码器         │ h 稀疏                │ L + λ·KL(ρ ‖ ρ̂)        │
│ 深度自编码器         │ 多层非线性            │ L(x, g(f(x)))           │
│ VAE                 │ h ~ 分布              │ L_recon + KL(q‖p)       │
│ 去噪自编码器         │ 从噪声中恢复          │ L(x, g(f(x̃)))          │
│ 收缩自编码器         │ ‖∂f/∂x‖ 小          │ L + λ·‖J_f‖²_F          │
│ PSD                 │ h 稀疏且可预测        │ ‖x-Dh‖²+λ‖h‖₁+γ‖h-f(x)‖│
└─────────────────────┴────────────────────────┴──────────────────────────┘
相关推荐
来让爷抱一个1 小时前
2026 纹理一致性实战:贴图不许花脸,百智云精灵图把材质契约写进素材包
人工智能·机器学习·材质·贴图
零依赖极客1 小时前
Day 8·1 自注意力机制:Q·K^T/softmax/V的在线计算
c语言·arm开发·人工智能·llama
我命由我123451 小时前
人脸识别 - 去重时间窗口
java·开发语言·python·学习·java-ee·学习方法·python3.11
minglie11 小时前
espidf的espnow的配对
学习
幻影123!1 小时前
AlphaZero 五子棋实战(五):评估工具 —— 学会在错误的地方掉头
人工智能·机器学习·强化学习·alpha zero
jbk33111 小时前
PixiuCut「视频批量分割」支持智能镜头分割、按时长、按数量分割,可定义最小时长避免无效片段等多可选参数可设置
人工智能·音视频·剪辑软件·剪映自动化软件
逆境不可逃1 小时前
Pi Agent 学习笔记:一次提问背后的工具调用循环
java·笔记·学习
诺伦1 小时前
RiseClaw玄策:GEO优化工程实战,从零搭建生成式引擎优化体系
人工智能·chatgpt