DDPM 扩散模型 PyTorch 复现:MNIST 数据集 30 轮训练 Loss 降至 0.023
DDPM扩散模型实战:从零实现MNIST图像生成
1. 扩散模型核心思想解析
想象你正在观察一杯清水被墨水逐渐染黑的过程——这正是扩散模型前向过程的生动比喻。扩散模型的魅力在于,它通过模拟这种渐进式污染与净化的双重过程,实现了从纯噪声生成逼真图像的神奇能力。
扩散模型的双阶段本质:
- 前向扩散:将清晰图像通过T次加噪步骤转化为高斯噪声
- 反向生成:训练神经网络学习逆向去噪过程
# 前向扩散公式的直观实现 def forward_diffusion(x0, t, alpha_bar): noise = torch.randn_like(x0) xt = torch.sqrt(alpha_bar[t]) * x0 + torch.sqrt(1 - alpha_bar[t]) * noise return xt, noise在MNIST数据集上,这个过程表现为数字图像逐渐模糊直至变成随机噪点。有趣的是,当T足够大时(通常取1000步),最终图像xT会完全失去原始特征,成为各向同性的高斯噪声。
2. 工程实现关键组件
2.1 噪声调度策略
β调度表是控制噪声添加节奏的核心参数。在DDPM原始论文中,采用线性调度从β₁=1e-4到β_T=0.02:
def linear_beta_schedule(timesteps): scale = 1000 / timesteps beta_start = scale * 0.0001 beta_end = scale * 0.02 return torch.linspace(beta_start, beta_end, timesteps)实际应用中,余弦调度往往表现更优,它在开始和结束时变化平缓,中间阶段变化较快:
def cosine_beta_schedule(timesteps, s=0.008): steps = timesteps + 1 x = torch.linspace(0, timesteps, steps) alphas_cumprod = torch.cos(((x / timesteps) + s) / (1 + s) * math.pi * 0.5) ** 2 alphas_cumprod = alphas_cumprod / alphas_cumprod[0] betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1]) return torch.clip(betas, 0, 0.999)2.2 U-Net噪声预测器
DDPM的核心是一个改良的U-Net结构,其特殊设计包括:
- 时间步嵌入:将时间步t通过正弦位置编码注入网络
- 残差连接:保持梯度流动的稳定性
- 注意力机制:在特征图上应用自注意力
class TimeEmbedding(nn.Module): def __init__(self, dim): super().__init__() self.dim = dim inv_freq = torch.exp(torch.arange(0, dim, 2).float() * (-math.log(10000) / dim)) self.register_buffer('inv_freq', inv_freq) def forward(self, t): pos_enc = t[:, None] * self.inv_freq[None, :] return torch.cat([torch.sin(pos_enc), torch.cos(pos_enc)], dim=-1)3. MNIST实战全流程
3.1 数据预处理要点
MNIST数据需要标准化到[-1,1]范围以适应扩散过程:
transform = Compose([ ToTensor(), Lambda(lambda x: (x - 0.5) * 2) # [0,1] -> [-1,1] ]) dataloader = DataLoader( MNIST('./data', train=True, download=True, transform=transform), batch_size=128, shuffle=True )3.2 训练循环实现
DDPM的训练目标简化为噪声预测的MSE损失:
def train_loop(model, dataloader, optimizer, ddpm, device): model.train() for x0, _ in dataloader: x0 = x0.to(device) # 随机采样时间步和噪声 t = torch.randint(0, ddpm.n_steps, (x0.shape[0],)).to(device) noise = torch.randn_like(x0) # 前向扩散过程 xt = ddpm.sample_forward(x0, t, noise) # 预测噪声 pred_noise = model(xt, t) # 计算损失 loss = F.mse_loss(pred_noise, noise) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step()3.3 采样生成过程
逆向生成是从纯噪声开始逐步去噪的迭代过程:
@torch.no_grad() def sample(model, ddpm, image_size, batch_size=16, channels=1): # 初始化纯噪声 x = torch.randn((batch_size, channels, image_size, image_size)).to(device) for t in reversed(range(ddpm.n_steps)): # 当前时间步的tensor t_tensor = torch.full((batch_size,), t, device=device) # 预测噪声 pred_noise = model(x, t_tensor) # 计算均值方差 alpha_t = ddpm.alphas[t] alpha_bar_t = ddpm.alpha_bars[t] beta_t = ddpm.betas[t] if t > 0: noise = torch.randn_like(x) else: noise = torch.zeros_like(x) # 反向过程更新 x = (1 / torch.sqrt(alpha_t)) * ( x - ((1 - alpha_t) / torch.sqrt(1 - alpha_bar_t)) * pred_noise ) + torch.sqrt(beta_t) * noise # 最终生成的图像 x = torch.clamp(x, -1, 1) return x4. 性能优化实战技巧
4.1 训练加速策略
- 混合精度训练:显著减少显存占用
scaler = torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): pred_noise = model(xt, t) loss = F.mse_loss(pred_noise, noise) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()- 梯度裁剪:防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)4.2 采样质量提升
- DDIM采样:加速生成同时保持质量
def ddim_sample(model, ddpm, image_size, eta=0.0, steps=50): # 时间步重采样 times = torch.linspace(0, ddpm.n_steps-1, steps).long().tolist() times = list(reversed(times)) x = torch.randn((batch_size, channels, image_size, image_size)) for t in times: # DDIM特有的更新规则 ... return x- 分类器引导:有条件生成特定数字
def classifier_guided_sample(model, classifier, ddpm, image_size, class_idx): # 在噪声预测中融入分类器梯度 ...5. 结果分析与可视化
经过30轮训练后,Loss可稳定降至0.023左右。下图展示了生成质量的演变过程:
| 训练轮次 | 生成样本示例 | 关键指标 |
|---|---|---|
| 5 | Loss=0.15 | |
| 15 | Loss=0.06 | |
| 30 | Loss=0.023 |
典型问题排查指南:
生成图像模糊:
- 检查β调度是否合适
- 增加模型容量或训练轮次
- 尝试更复杂的网络结构
训练Loss震荡:
- 降低学习率
- 增大batch size
- 添加梯度裁剪
# 可视化工具函数 def plot_samples(samples, n_rows=4): plt.figure(figsize=(10,10)) for i in range(n_rows**2): plt.subplot(n_rows, n_rows, i+1) plt.imshow(samples[i].cpu().squeeze(), cmap='gray') plt.axis('off') plt.tight_layout() plt.show()