1. 扩散模型的前世今生
我第一次接触DDPM是在2021年做图像生成项目时,当时被它"先破坏再重建"的思路惊艳到了。想象一下,你有一幅名画,每天往上面撒一点盐,直到完全看不清原貌——这就是前向扩散过程。神奇的是,模型能学会如何一步步把盐粒去掉,还原出原始画作!
传统生成模型如GAN和VAE各有痛点:GAN训练不稳定,VAE生成质量欠佳。而扩散模型通过渐进式去噪的思路,在图像质量上实现了突破。2020年Ho等人的论文《Denoising Diffusion Probabilistic Models》正式提出DDPM框架,其核心是把数据生成建模为一个马尔可夫链的逆过程。
实际项目中我发现,相比GAN那种"一步到位"的生成方式,扩散模型更像是个精益求精的工匠——它通过几十甚至上千步的精细调整,逐步雕琢出完美结果。这种特性让它在图像生成质量上远超前辈,OpenAI的DALL·E 2和Google的Imagen都采用了这种技术路线。
2. 数学原理拆解
2.1 前向扩散:有序的破坏
前向过程就像把一杯清水慢慢滴入墨水。定义q(xₜ|xₜ₋₁)为在时间步t加入噪声的转移概率:
def forward_process(x0, t, betas): """前向扩散过程 Args: x0: 原始图像 [B,C,H,W] t: 时间步 [B,] betas: 噪声调度表 [T,] Returns: xt: 加噪后的图像 noise: 实际添加的噪声 """ alphas = 1 - betas alpha_bars = torch.cumprod(alphas, dim=0) sqrt_alpha_bar = torch.sqrt(alpha_bars[t])[:,None,None,None] sqrt_one_minus_alpha_bar = torch.sqrt(1 - alpha_bars[t])[:,None,None,None] noise = torch.randn_like(x0) xt = sqrt_alpha_bar * x0 + sqrt_one_minus_alpha_bar * noise return xt, noise这里有个关键技巧:任意时刻的xₜ可以直接从x₀计算得到(不必逐步加噪),这大大提高了训练效率。βₜ是噪声调度参数,通常从β₁=1e-4线性增长到β_T=0.02。
2.2 逆向过程:智能重建
逆向过程pθ(xₜ₋₁|xₜ)是模型需要学习的核心。根据论文推导,当βₜ足够小时,逆过程也服从高斯分布:
q(xₜ₋₁|xₜ,x₀) = N(xₜ₋₁; μ̃ₜ(xₜ,x₀), β̃ₜI)
其中均值μ̃ₜ和方差β̃ₜ都可以解析表示。在实际实现中,我们让神经网络预测噪声εθ(xₜ,t),然后通过重参数化技巧得到均值:
def reverse_process(xt, t, model): """逆向去噪过程 Args: xt: 噪声图像 [B,C,H,W] t: 时间步 [B,] model: 噪声预测模型 Returns: x_prev: 去噪后的图像 """ # 预测噪声 pred_noise = model(xt, t) # 计算均值方差 alpha_t = 1 - betas[t] alpha_bar_t = torch.cumprod(alphas, dim=0)[t] beta_t = betas[t] sqrt_alpha_t = torch.sqrt(alpha_t) sqrt_one_minus_alpha_bar_t = torch.sqrt(1 - alpha_bar_t) # 重参数化计算 mean = (xt - beta_t/sqrt_one_minus_alpha_bar_t * pred_noise) / sqrt_alpha_t variance = beta_t # 采样 if t == 0: return mean else: noise = torch.randn_like(xt) return mean + torch.sqrt(variance) * noise3. PyTorch实战指南
3.1 模型架构设计
DDPM通常采用U-Net结构,我在项目中对其进行了改进:
class TimeEmbedding(nn.Module): def __init__(self, dim): super().__init__() self.dim = dim half_dim = dim // 2 emb = math.log(10000) / (half_dim - 1) emb = torch.exp(torch.arange(half_dim) * -emb) self.register_buffer('emb', emb) def forward(self, t): emb = t[:, None] * self.emb[None, :] return torch.cat((emb.sin(), emb.cos()), dim=-1) class UNetBlock(nn.Module): def __init__(self, in_c, out_c, time_emb_dim): super().__init__() self.time_mlp = nn.Sequential( nn.SiLU(), nn.Linear(time_emb_dim, out_c) ) self.conv = nn.Sequential( nn.Conv2d(in_c, out_c, 3, padding=1), nn.BatchNorm2d(out_c), nn.SiLU(), nn.Conv2d(out_c, out_c, 3, padding=1), nn.BatchNorm2d(out_c), nn.SiLU() ) def forward(self, x, t_emb): h = self.conv(x) t_emb = self.time_mlp(t_emb) return h + t_emb[:,:,None,None]关键点在于:
- 时间步通过正弦位置编码嵌入网络
- 每个残差块都接收时间嵌入信息
- 使用SiLU激活函数(Swish)提升梯度流动
3.2 训练流程详解
训练时我们发现三个调优技巧特别有效:
- 噪声调度优化:余弦调度比线性调度效果更好
- 混合损失函数:L1+L2损失组合
- EMA模型:使用滑动平均模型稳定训练
def train_step(model, x0, optimizer, loss_fn): model.train() # 随机采样时间步 t = torch.randint(0, T, (x0.size(0),), device=device) # 前向加噪 xt, noise = forward_process(x0, t) # 预测噪声 pred_noise = model(xt, t) # 计算损失 loss = loss_fn(pred_noise, noise) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() # 更新EMA模型 update_ema(model, ema_model) return loss.item()4. 采样生成的艺术
采样过程是从纯噪声逐步去噪的迭代过程:
@torch.no_grad() def sample(model, num_samples): model.eval() x = torch.randn((num_samples, 3, 32, 32), device=device) for t in reversed(range(T)): t_batch = torch.full((num_samples,), t, device=device) x = reverse_process(x, t_batch, model) # 最后一步不加噪声 if t > 0: noise = torch.randn_like(x) x = x + torch.sqrt(betas[t]) * noise return x.clamp(-1, 1)实际应用中,我们可以通过调节采样步数在速度和质量间权衡。50步的快速采样和1000步的精细采样效果差异明显——前者适合实时应用,后者适合追求极致质量。