ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

DDPM扩散模型核心解析:从加噪去噪原理到PyTorch实现

DDPM扩散模型核心解析:从加噪去噪原理到PyTorch实现 简介这是一份面向人工智能与图像生成学习者的经典论文原文 PDF系统讲解去噪扩散概率模型DDPM的核心思想适合机器学习研究者、计算机视觉方向学生以及关注扩散模型应用的工程师阅读。论文从非平衡热力学视角引入潜变量模型将正向扩散与反向采样过程串联起来并借助 Langevin 动力学和去噪评分匹配设计加权变分目标同时讨论了与 GAN、VAE、能量模型等生成方法的关联以及图像、文本、语音等多模态应用前景。实验部分在 CIFAR10 与 256×256 LSUN 等数据集上展示了优异的无条件图像生成效果反映扩散模型在生成质量上的竞争力。整份 PDF 为 NeurIPS 2020 发表的完整论文共 1 个文件、约 9.79MB目前已有 428 人学习便于离线阅读或对照公式推导。对想从原理层面理解 AIGC 图像生成的读者来说这份一手文献能帮助建立扩散模型的知识框架并为后续研究或复现提供明确起点。1. 一篇PDF如何重新定义生成模型如果你只读一篇关于扩散模型的论文那应该是这篇《Denoising Diffusion Probabilistic Models》。它没有GAN那种精巧的对抗博弈也没有VAE那种复杂的变分推断而是用了一个在物理和数学里存在了很久的概念——逐步加噪、再逐步去噪——把图像生成做到了当时的最优水平。论文核心不在于提出一个全新网络而在于把扩散过程的训练目标化简成了一个非常简单的东西让模型学会预测噪声本身。这个化简让训练变得极其稳定也让后续所有工作——DDIM、DPM-Solver、Stable Diffusion——都在这篇文章的基础上展开。这篇博客面向两类读者一类是只听过名字、想在本地跑通最小复现的工程师另一类是已经在跑扩散模型、想回头补齐数学细节的研究者。我会把论文里最关键的公式、参数设定和训练陷阱逐个拆开直接给可复现代码。2. 前向加噪与反向去噪DDPM 依赖的一组关键公式2.1 前向过程T 步逐步加噪的数学表达DDPM 的前向过程定义为一个马尔可夫链。给定一张真实图像x_0每步按一个预先设定好的方差表beta_t添加高斯噪声q(x_t | x_{t-1}) N(x_t; sqrt(1 - beta_t) * x_{t-1}, beta_t * I)t从 1 到 TT 默认取 1000。beta_t的值从第一行的1e-4线性增长到最后一行的0.02这是论文里直接给出的默认值。这个表在训练前就固定好不参与学习。这里有一个关键技巧因为每一步都是独立的高斯转换我们可以直接从x_0跳到任意第t步的加噪结果而不需要逐步迭代。定义alpha_t 1 - beta_talpha_bar_t为所有alpha的连乘那么x_t sqrt(alpha_bar_t) * x_0 sqrt(1 - alpha_bar_t) * epsilon其中epsilon是从标准正态分布采样的噪声。这一步极其重要它意味着训练时的每个样本只需一次随机采样就能生成任意时间步的加噪结果而不是跑完整个链。这一条公式也直接决定了后面损失函数怎么写。2.2 反向过程训练一个神经网络去拟合噪声反向过程的目标是从纯噪声x_T逐步还原出x_0。DDPM 把它建模成另一个马尔可夫链每一步都是一个高斯分布p_theta(x_{t-1} | x_t) N(x_{t-1}; mu_theta(x_t, t), sigma_t^2 * I)这里的mu_theta是一个带时间步编码的神经网络输入当前图像和当前时间步t输出均值。方差sigma_t^2论文直接固定为已知值不需要网络预测。论文作者推导后得到一个反直觉的结论网络不需要直接预测均值只需要预测噪声epsilon。关键在于x_t是有噪声的如果我们能猜出前面加进去的噪声是多少就能把它减去再经过一步重采样得到x_{t-1}。原论文把训练目标化简成L_simple E_{t, x_0, epsilon} [ || epsilon - epsilon_theta(x_t, t) ||^2 ]这是一个纯粹的 MSE 损失没有对抗损失没有额外的正则项。这就是为什么扩散模型训练起来如此稳定——它不存在两个网络互相博弈、谁也没法收敛的状态。从变分下界VLB的角度看这个简化目标丢弃了几个不影响生成质量的项保留了最核心的噪音预测项。论文用大篇幅证明了L_simple实际上比完整 VLB 更容易优化生成效果也更好。所以你在读取论文代码时看到mse_loss(predicted_noise, noise)时不要觉得它简化得不够严谨。2.3 从公式到代码一张参数表直接落地上面的公式可以逐行翻译成代码。先定义整张噪声表这是整个模型的所有超参数基础。import torch def linear_beta_schedule(timesteps1000, start1e-4, end0.02): return torch.linspace(start, end, timesteps) betas linear_beta_schedule() alphas 1.0 - betas alpha_cumprod torch.cumprod(alphas, dim0) sqrt_alpha_cumprod torch.sqrt(alpha_cumprod) sqrt_one_minus_alpha_cumprod torch.sqrt(1.0 - alpha_cumprod)alpha_cumprod的连乘操作是本段代码的核心它让第 2.1 节的跳步公式可以直接用。sqrt_alpha_cumprod乘以x_0得到信号分量sqrt_one_minus_alpha_cumprod乘以epsilon得到噪声分量。两张预计算表在任何时刻t都能直接索引。训练时需要关心的几个参数分别是timesteps决定噪声步数默认 1000start和end决定噪声表的最大最小值过小的边界会让模型难以区分相邻步。这四个参数的组合是后面所有调参工作的起点。3. UNet 与训练循环在本地跑通 DDPM 的最小实现3.1 模型结构选择为什么 UNet 是默认主力论文本身使用 UNet 作为噪声预测网络原因是 UNet 的下采样和上采样结构可以在不同尺度上同时保留全局结构和局部细节。扩散模型的输入输出是同一张噪声图和预测噪声尺寸完全一致UNet 的跳跃连接机制天然适合这种输入输出对齐的任务。UNet 的另一个核心组件是时间步嵌入。论文使用的是正弦位置编码将t编码为向量后加到每个残差块中。这一设计的直觉是模型需要知道当前是哪一步的噪声才能决定预测噪声的强度。没有它网络对第 10 步和第 800 步的生成信号就无法区分。import torch.nn as nn import torch.nn.functional as F class SinusoidalPositionEmbedding(nn.Module): def __init__(self, dim): super().__init__() self.dim dim def forward(self, t): half_dim self.dim // 2 emb torch.log(torch.tensor(10000.0)) / (half_dim - 1) emb torch.exp(torch.arange(half_dim, devicet.device) * -emb) emb t[:, None] * emb[None, :] return torch.cat([torch.sin(emb), torch.cos(emb)], dim-1)dim是嵌入维度一般设为model_channels的 4 倍或 8 倍。嵌入向量会在 UNet 的每个残差块中通过线性层变换后加到特征图上让时间信息在网络内部逐层传递。3.2 DDPM 训练循环的完整 PyTorch 代码下面是一个能直接跑的迷你训练循环。省略了 UNet 完整内部结构重点展示训练逻辑。U-Net 可自行实现或复用开源库中的现成组件。import torch import torch.nn as nn def train_step(model, optimizer, x0, t, device): x0 x0.to(device) noise torch.randn_like(x0) alpha_bar_t alpha_cumprod[t].to(device).view(-1, 1, 1, 1) sqrt_ab sqrt_alpha_cumprod[t].to(device).view(-1, 1, 1, 1) sqrt_one_ab sqrt_one_minus_alpha_cumprod[t].to(device).view(-1, 1, 1, 1) x_t sqrt_ab * x0 sqrt_one_ab * noise predicted_noise model(x_t, t) loss nn.functional.mse_loss(predicted_noise, noise) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item() # 使用示例 t torch.randint(0, 1000, (x0.size(0),), devicedevice).long() loss train_step(model, optimizer, x0_batch, t, device)t是批量随机采样的整数张量范围在[0, 1000)之间。alpha_cumprod[t]直接索引预计算表得到本步的累积 alpha。view(-1,1,1,1)为批维度和多通道图像形状对齐而设。x_t由x_0和noise按权重相加训练收敛的标志是 loss 下降到稳定值通常不需要额外准确率指标。这段代码中需要注意t必须传入模型否则推理时无法控制去噪步数。另外mse_loss 的两个输入分别是预测噪声和真实采样的噪声这一组是整篇论文训练目标「简化版」的直译。3.3 训练时间步的采样策略离散均匀采样还是分段采样论文默认是从0到999均匀采样时间步。这种做法最简单且在大多数任务上表现够用。但后续研究发现不同时间步的噪声难度差异很大——高噪声步容易收敛低噪声步细节难学。如果均匀采样模型可能偏向容易步忽视慢步。我常用的改进是引入重要性采样权重给高噪声步加权给低噪声步降权。这需要维护一个动态损失权重表每 100 步更新一次。这里给出一个简单实现。weights torch.ones(1000, devicedevice) tracked_loss torch.zeros(1000, devicedevice) # 训练循环内 t torch.multinomial(weights, batch_size, replacementTrue) loss train_step(...) # 每 100 步更新 if step % 100 0: weights F.softmax(tracked_loss * 2.0, dim-1) tracked_loss.zero_()torch.multinomial按权重采样时间步tracked_loss记录每个步的平均 loss。权重更新公式中* 2.0是平滑系数越大越激进地偏向高损失步。这个方法并不改变目标函数只改变采样分布理论上仍然是无偏的。4. 采样流程与加速从 1000 步降到 50 步的关键调整4.1 反向采样标准 DDPM 的逐像素重参数化训练完成后真正的验证在采样阶段。DDPM 的采样必须从tT逐渐回退到t1每次用一个神经网络预测噪音并重新采样。def sample_ddpm(model, img_size, batch_size, device): x torch.randn(batch_size, 3, img_size, img_size, devicedevice) for t in reversed(range(0, 1000)): t_tensor torch.full((batch_size,), t, devicedevice).long() predicted_noise model(x, t_tensor) alpha_t alphas[t].to(device) alpha_bar_t alpha_cumprod[t].to(device) alpha_bar_prev alpha_cumprod[t - 1].to(device) if t 0 else torch.tensor(1.0).to(device) posterior_var (1 - alpha_bar_prev) / (1 - alpha_bar_t) * (1 - alpha_t) mean (1 / torch.sqrt(alpha_t)) * (x - (1 - alpha_t) / torch.sqrt(1 - alpha_bar_t) * predicted_noise) if t 0: x mean torch.sqrt(posterior_var) * torch.randn_like(x) else: x mean return x循环中每次输出x作为下一步输入。mean的公式源自高斯分布的均值推导将x_t减去预测噪声。posterior_var是反向过程的理论方差它决定了每一步的重采样强度。最后t0时不加新噪声直接返回生成图。这段代码里的关键参数是posterior_var中的比例因子可以通过调整它获得更稳定的采样效果但标准 DDPM 是固定计算不需要额外调参。生成 64×64 图像、共 1000 步时这一步循环比较耗时单卡推理通常需要几十秒。4.2 加速方案DDIM 隐式采样原理与代码DDPM 的 1000 次逐步推理在训练或实验阶段还能忍受但生产环境必须加速。DDIM 观察到一个事实反向采样不需要严格遵循马尔可夫链可以在小于 T 的任意步数下完成只要保持边缘分布一致性。DDIM 先确定一个子序列[0, 10, 20, ...]然后在这个稀疏序列上迭代反向去噪。def sample_ddim(model, img_size, batch_size, device, steps50, eta0.0): x torch.randn(batch_size, 3, img_size, img_size, devicedevice) step_indices torch.linspace(999, 0, steps, dtypetorch.long, devicedevice) for i, t in enumerate(step_indices[:-1]): t_next step_indices[i 1] t_tensor torch.full((batch_size,), t, devicedevice).long() predicted_noise model(x, t_tensor) alpha_bar alpha_cumprod[t].to(device) alpha_bar_next alpha_cumprod[t_next].to(device) pred_x0 (x - torch.sqrt(1 - alpha_bar) * predicted_noise) / torch.sqrt(alpha_bar) if eta 0: # 确定性采样 sigma 0.0 else: sigma eta * torch.sqrt((1 - alpha_bar_next) / (1 - alpha_bar) * (1 - alpha_bar / alpha_bar_next)) sigma sigma.to(device) c1 torch.sqrt(alpha_bar_next) c2 torch.sqrt(1 - alpha_bar_next - sigma ** 2) x c1 * pred_x0 c2 * predicted_noise sigma * torch.randn_like(x) return xeta0时是确定性采样相当于把 1000 步压缩成 50 步生成质量略有下降但速度提升巨大。eta越大引入随机性越多越接近 DDPM 的随机行为。注意pred_x0是模型预测的原始图像DDIM 的核心在两步的规律差距——直接用alpha_bar_next控制噪声递减幅度避免了每个子步重新采样的开销。4.3 评估生成效果先看 FID 还是先看样本分布评估生成模型时最常用的客观指标是 FID。FID 用 InceptionV3 的特征分布距离衡量生成图与真实图在语义空间上的距离。除 FID 外还应检查训练数据的数据分布和生成样本的分布是否重叠——如果你的训练集主要是 64×64 的低分辨率图像别要求模型生成 512×512 的高清图网络并没有这种外推能力。这里给出一个简化的评估流程从生成模型中采样 5000 张图像。用预训练的 ImageNet 特征提取器提取特征向量。分别计算真实特征和生成特征的均值与协方差矩阵。按 FID 公式计算。不要盲目追求更低 FID 而忽视了人类主观感受。通常我在调参时只用 FID 作为检查项最终拍板时看真实生成的图像质量。FID 下降并不代表每张图都更清晰它反映了整体分布的一致性。5. 噪声调度、步数 T 与随机性注入的几条经验第 5 章里不再重复基础流程直接把几组最踩坑的参数经验写清楚。第一是噪声表。默认的线性调度1e-4到0.02在大多数数据集够用但在高分辨率 128×128 以上任务中线性表会让前几百步的噪声增加过快扩散过程在早期就失掉太多细节。常见替代方案是余弦调度def cosine_beta_schedule(timesteps1000, s0.008): x torch.linspace(0, timesteps, timesteps 1) alphas_cumprod torch.cos((x / timesteps s) / (1 s) * math.pi / 2) ** 2 alphas_cumprod alphas_cumprod / alphas_cumprod[0] betas 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1]) return torch.clip(betas, 0.0001, 0.02)余弦调度在中间步的噪声变化更平滑训练更稳定。第二是步数 T 的截断。如果显存不够把 T 从 1000 降到 500 也可以跑通但会牺牲生成质量此时需要增大噪声表的上限值。第三是采样时的随机注入。DDIM 的 eta 值建议先在 0 和 1 之间试三档观察细节纹理和整体风格的取舍eta0 通常能获得更稳定的形状。单一随机种子可能掩盖模型的不稳定请务必多试几组随机种子再下结论。最后训练时的 batch size 对噪声估计影响较大建议在 batch size 小于 32 时不要使用 BatchNorm 而是切换为 GroupNorm因为小 batch 的均值方差波动会传导噪声估计误差。这几条经验做完了你对扩散模型的理解已经超过多数只会读摘要的人了。本文还有配套的精品资源点击获取
返回列表