DDPM扩散模型原理与PyTorch实现全指南
发布时间:2026/9/29 2:29:49来源:尧图网络
简介这是发表于 NeurIPS 2020 的去噪扩散概率模型论文原题为《Denoising Diffusion Probabilistic Models》简称 DDPM面向人工智能、计算机视觉与生成模型方向的算法工程师、研究人员和学生。资源从非平衡热力学角度引入潜变量模型系统阐述前向扩散与反向去噪过程并借助朗之万动力学和去噪评分匹配设计加权变分训练目标可帮助读者完整理解扩散模型的数学原理和训练方法。整份资源包含 1 个 PDF 文件压缩包约 9.79 MB内含摘要、方法推导、实验设置、生成效果图以及与其他生成模型的对比分析。已有 430 人学习下载适合作为图像生成入门、论文精读或复现扩散模型的参考资料。论文在 CIFAR10 上取得 Inception 分数 9.46、FID 3.17 的结果在 256×256 LSUN 上样本质量接近 ProgressiveGAN读者能从中获取关键超参数、目标函数设计及评估细节为后续理解 Stable Diffusion 等应用打下基础。1. 先从标题说起一篇论文PDF为什么到今天还值得逐行读Denoising Diffusion Probabilistic Models.pdf是生成模型领域绕不开的一份原始文献大家习惯简称 DDPM。在 2020 年它刚出现时不少人以为它只是 GAN 的又一个替代品真正跑过之后才发现这套「逐步加噪、再逐步去噪」的思路把生成任务的稳定性提到了一个新高度。现在看到的 Stable Diffusion、Midjourney 底层都延续了它的核心框架。这篇文章的目标读者不是只看公式推导的研究者而是准备把扩散模型用到实际任务里的工程师。读完你会知道它的数学本质是什么、最小训练代码怎么写、超参数从哪里开始调以及那些论文里没写但跑代码一定会遇到的坑。适合正在选择生成方案的技术负责人也适合第一次上手扩散模型、对着满屏公式不知道先动哪一行代码的人。2. 拆解 DDPM 的数学骨架前向加噪与反向去噪想绕过数学直接调库也不是不行但那样遇到训练不收敛、生成结果崩坏时你连日志里的 loss 是哪里算出来的都不知道。DDPM 的数学并不复杂核心就两件事把数据变成噪声再学一个从噪声变回数据的逆过程。2.1 前向过程把一张清晰图逐步抹成噪点前向过程是一个固定的马尔可夫链每一步往里加一点高斯噪声。设原始数据是x0经过 T 步之后得到x1, x2, ... xT每一步的关系是x_t sqrt(1 - beta_t) * x_{t-1} sqrt(beta_t) * epsilon这里的epsilon是标准正态噪声beta_t是一个预先设定好的噪声方差。论文里把beta_t从 0.0001 线性增加到 0.02T 取 1000也就是说到第 1000 步时原始图像的信息几乎完全被噪声覆盖。这个公式的意义在于它定义了一个从清晰到模糊的退化路径。如果 T 足够大、每步加的噪声足够小前向过程实际上变成了一个连续的高斯扰动过程图像在每一步只损失一点点细节。这样一个好处是反向过程也不需要一步到位而是可以一步一步地去掉少量噪声每步只做一个简单的去噪动作。直接按这个公式一步一步跑 T 次会很慢。好在前向过程有重参数化技巧可以一步算到任意第 k 步的分布x_t sqrt(alpha_bar_t) * x0 sqrt(1 - alpha_bar_t) * epsilon其中alpha_t 1 - beta_talpha_bar_t是前 t 个alpha的累乘。这个公式是训练时的主要工具有了它就能一次性采样任意时刻的噪声图不需要循环跑 1000 步。训练时真正输入网络的是x_t和时间步t网络负责预测混入的噪声epsilon。2.2 反向过程让神经网络学会一步步去噪反向过程的目标是从一个纯高斯噪声x_T出发逐步还原出x_0。理论上这个反向条件分布是高斯分布的形式但均值未知需要用神经网络来拟合。在 DDPM 的设定里网络通常是一个 U-Net 结构输入是噪声图和时间步编码输出是预测的噪声。每个时间步的逆过程可以写成x_{t-1} 1/sqrt(alpha_t) * (x_t - beta_t/sqrt(1-alpha_bar_t) * epsilon_theta(x_t, t)) sigma_t * z式子拆开看网络先预测当前图中混入的噪声epsilon_theta然后从x_t里减去这部分噪声再补回一个较小的随机扰动sigma_t * z。这个随机扰动保证了生成过程不是确定性的同一份噪声可以生成不同的图像这也是扩散模型多样性好的原因之一。实际实现时sigma_t可以直接取beta_t的开方此时反向过程与前向过程的随机性保持一致。如果去掉这个扰动项生成就完全确定变成了 DDIM 的雏形这是后面加速采样的基础。2.3 训练目标从变分下界化成一个 MSE论文从变分下界出发推导训练目标但最终的简化形式非常简洁预测噪声。损失函数可以写成loss MSE(epsilon, epsilon_theta(x_t, t))这意味着训练时随机取一个时间步 t对一批真实图像加噪得到x_t让网络去预测这个已知的噪声用均方误差衡量预测效果。不需要对抗判别器不需要额外的正则项就是这么朴素。训练的直觉是如果网络能准确地预测任意时间步下混入的噪声那反向去噪过程自然就能一步步把噪声从图中剥离出来。这也解释了为什么 DDPM 训练比 GAN 稳定它没有两个网络互相博弈的过程纯粹是一个回归任务。但要注意这个简化损失等价于原始变分下界的加权版本它在不同时间步上的权重不同。简单 MSE 忽略了这些权重实际训练时模型会自然更关注信噪比更高、加噪更少的时间步。这不是 bug反而让训练更稳定生成质量在经验上更好。3. 把论文公式搬进 PyTorch训练一个最小 DDPM数学看懂之后接下来要把公式落成能跑的代码。下面这套最小实现依赖 PyTorch模型用一个小 U-Net 就够在 MNIST 上出效果。硬件方面一张 1080Ti 级别的显卡就能训练到可以生成数字的水平。3.1 噪声调度从 β 到 α_bar 的准备工作在写训练循环之前需要先把所有与时间步有关的系数提前算好。这些系数在训练和采样时都要用到应该作为常量一次性生成import torch import math T 1000 beta torch.linspace(0.0001, 0.02, T) alpha 1.0 - beta alpha_bar torch.cumprod(alpha, dim0) alpha_bar_prev torch.cat([torch.tensor([1.0]), alpha_bar[:-1]]) sqrt_alpha_bar torch.sqrt(alpha_bar) sqrt_one_minus_alpha_bar torch.sqrt(1.0 - alpha_bar) sqrt_recip_alpha torch.sqrt(1.0 / alpha) posterior_variance beta * (1.0 - alpha_bar_prev) / (1.0 - alpha_bar)这段代码把论文里的β_t、α_t、α_bar_t全部预计算出来。posterior_variance是反向过程每个时间步的方差求导过程比较复杂但直接用上面这个公式就能算采样时会用到它来控制每一步噪声的大小。3.2 数据准备与归一化进入模型前的最后一公里MNIST 的图像范围是 0 到 255但扩散模型期望输入数据的值域在 0 到 1 甚至 -1 到 1 之间。常见做法是把像素归一化到 -1 到 1这样原始图像的均值为 0加上噪声后不至于让信号整体偏移transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) dataloader DataLoader(dataset, batch_size128, shuffleTrue, num_workers4)归一化不只是为了收敛速度。如果图像范围是 0 到 1前向加噪之后图像均值不再为 0模型要额外学习一个偏移量收敛会变慢。把数据拉到 -1 到 1 之后加噪和去噪的数值行为都更对称。3.3 训练循环损失计算与参数更新训练循环非常短核心操作是采样随机时间步、加噪、预测噪声、计算损失model SimpleUNet().to(device) optimizer torch.optim.Adam(model.parameters(), lr2e-4) for epoch in range(50): for x0, _ in dataloader: x0 x0.to(device) t torch.randint(0, T, (x0.shape[0],), devicedevice) eps torch.randn_like(x0) x_t sqrt_alpha_bar[t, None, None, None] * x0 sqrt_one_minus_alpha_bar[t, None, None, None] * eps eps_pred model(x_t, t) loss torch.nn.functional.mse_loss(eps_pred, eps) optimizer.zero_grad() loss.backward() optimizer.step()关键是用到了前向过程的闭式解一次就能得到x_t。t是全批次共享的随机时间步也可以是每个样本独立的随机值效果没有明显差异。网络内部需要把时间步t做正弦位置编码然后融合进特征图如果模型结构忘了处理时间步整个训练会完全无效。3.4 无条件生成从纯噪声开始采样训练完成后采样是一个循环过程从x_T开始逐时间步去噪。这里有两点值得注意最后一步不添加随机扰动中间步骤使用预计算的方差控制噪声大小model.eval() x torch.randn(16, 1, 28, 28, devicedevice) for t in reversed(range(T)): t_tensor torch.full((16,), t, devicedevice, dtypetorch.long) eps_pred model(x, t_tensor) x sqrt_recip_alpha[t] * (x - beta[t] / sqrt_one_minus_alpha_bar[t] * eps_pred) if t 0: noise torch.randn_like(x) sigma torch.sqrt(posterior_variance[t]) x x sigma * noise x (x 1) / 2采样一个批次在 CPU 上也只需要几秒因为模型很小。核心逻辑是每步先用网络预测噪声然后按反向公式剥离噪声。t 0才加噪声这一步很重要保证最后输出是干净的图像而是不残留随机扰动。4. 超参数怎么定论文默认值之外的经验参考DDPM 论文给出的超参数组合适用于 32×32 到 256×256 的图像生成但直接照搬到别的数据集上不一定是最优。下面这些是我在实际项目中验证过的调整方向。4.1 T1000 的取舍与减步长采样T 决定前向过程的总步数。步数太少每步的噪声方差会变大反向去噪每步承担的压力也变大生成质量会下降。步数太多训练时采样更多时间步但训练本身只在随机一个时间步做一次预测所以训练成本不会线性增加真正变慢的是采样阶段。T1000 在绝大多数场景下是一个安全起点。如果只是跑 MNIST 或 CIFAR 这类小图可以降到 500训练曲线更快下降生成效果不会明显变差。做 256×256 以上的大图建议保持 1000甚至有人用 4000 来降低每步噪声幅度代价是采样时反向循环次数成倍增加。减步长采样是另一个实用技巧。训练时用 T1000采样时只取其中一部分时间步比如每 10 步取 1 步最后采样 100 步。这种方式相当于在反向过程跳过部分时间步生成质量略有下降但速度提升明显适合做实验期间的快速验证。4.2 学习率与 batch size 的调节逻辑DDPM 原文用的是 Adam学习率 2e-4没有学习率预热。实际复现时我建议加一个前 5000 步的线性预热从 1e-5 爬升到 2e-4这样在大 batch 下更容易稳定。batch size 推荐 128如果显存不足降到 64学习率按比例降到 1e-4 左右。一个常见的坑是学习率过大导致 loss 前期下降后突然跳变。扩散模型的 loss 曲线不像分类任务那样平滑它会出现周期性波动因为不同时间步的损失尺度不同。我习惯记录 loss 的指数滑动平均而不是原始值这样更容易判断训练是否真的收敛。4.3 EMA 的作用与设置EMA 是扩散模型训练中性价比最高的技巧。它维护模型参数的滑动平均版本用这个版本去采样样本质量通常比直接用训练权重更好ema_decay 0.9999 ema_params {} for name, param in model.named_parameters(): ema_params[name] param.data.clone() def update_ema(): for name, param in model.named_parameters(): ema_params[name].mul_(ema_decay).add_(param.data, alpha1 - ema_decay) def load_ema(): for name, param in model.named_parameters(): param.data.copy_(ema_params[name])EMA 的衰减率在 0.999 到 0.9999 之间取值。取 0.9999 时相当于平均了最近约 10000 步的参数在 batch size 128 下对应约 78 个 epoch。训练过程中每步更新一次 EMA采样前加载 EMA 权重即可。这个技巧基本不增加显存开销但能稳定提升生成质量尤其是训练后期。5. 复现 DDPM 的避坑记录我踩过的五个让人抓狂的问题这部分是跑了十几个版本之后积累的经验。每一个坑都是先出现诡异现象再花时间定位到具体原因最后写进代码注释里的。希望能让你少走几个月弯路。5.1 归一化不一致导致生成图像发灰现象训练 loss 正常下降但采样出来的图像整体偏灰对比度很低好像蒙了一层雾。原因前向加噪时假设输入数据在 -1 到 1 之间但训练时数据加载部分忘记做 Normalize实际输入是 0 到 1 的范围。模型学到的去噪逻辑基于错误的数值范围采样结果自然不对。解决统一数据加载与生成后处理。训练数据归一化到 -1 到 1采样结束后再反归一化回 0 到 255。检查方法很简单把加噪后的x_t直接可视化看它是否在合理范围内。5.2 训练早期 loss 不降一直停在某个常数附近现象loss 在前几千步几乎没有变化看数值接近 1.0 左右仿佛模型没有在学。原因这其实是正常现象并不总是 bug。扩散模型的初始 loss 接近真实噪声的方差因为模型一开始的预测接近零。如果输入归一化到 -1 到 1噪声的方差为 1.0所以 loss 起步在 1.0 附近是合理的。解决先确认时间步编码是否正确传入模型通常加噪步数较多的样本贡献大模型会先学会处理高噪声再逐渐处理低噪声。如果训练了 5000 步后 loss 仍然毫无下降趋势才需要检查网络结构和时间步 embedding 是否拼接到位。5.3 采样结果全是噪声完全看不出结构现象训练结束后的采样结果是一堆随机颗粒与训练数据没有任何相似性。原因这种情况大概率是采样公式写错了。一个典型错误是在反向过程的最后一步也加了随机噪声导致输出混杂了噪声。另一个可能是把网络的输出直接当成了x0而不是噪声。DDPM 网络预测的是噪声而不是图像本身采样时要用x_t减去预测噪声再除以sqrt(alpha_bar_t)才能估计出x0。解决把反向采样的每一步中间结果打印出来检查图像结构是否逐渐出现。如果第 500 步还是纯噪点说明模型确实没有学到有效的去噪逻辑需要检查训练数据是否出现乱码或者标签错位。5.4 多卡训练后 EMA 模型参数没有同步现象单卡训练一切正常改成 DataParallel 之后生成质量突然下降loss 曲线也出现奇怪的阶梯状。原因DataParallel 会把主模型的参数广播到每张卡上但 EMA 权重是在主进程上保存的参数副本并没有被 DataParallel 正确同步。每次 forward 之后模型参数变了EMA 在错误的时间点被更新。解决最省事的做法是让 EMA 在 forward 之后单独用主进程的参数更新不依赖 DataParallel 的内部机制。或者直接用 PyTorch 的 DistributedDataParallel它只负责梯度同步不干扰 EMA 的逻辑。5.5 生成质量不错但 FID 偏高图像多样性不足现象生成的图像单张看很清晰但整体对比参考集发现很多相似结构多样性下降导致 FID 评分偏高。原因可能是 EMA 衰减率设置过高模型对近期参数变化不敏感最后等于冻结在较早的某个时间点。也可能是采样过程中噪声方差sigma_t被设成了 0相当于用了确定性采样。解决先检查采样代码里t 0时是否加了噪声。如果加了但多样性仍然不够把 EMA 衰减率从 0.9999 降到 0.999 再试。6. 进阶用法从无条件生成到可控图像合成DDPM 原始论文解决的是无条件生成问题但工程落地的场景通常需要一定的控制条件。把模型升级成条件版本并不需要改动训练框架只需要在网络内部增加条件信息的注入方式。6.1 条件生成的最小改法类别条件注入以 MNIST 为例给每个数字类别一个 embedding把类别嵌入加到时间步嵌入上一起输入网络class_embed nn.Embedding(10, time_emb_dim) cond class_embed(label) combined time_emb cond在训练时每个 batch 传一个随机标签或者固定顺序的标签模型就能学会按指定类别生成图像。这个改法不需要重新设计网络结构只是多了一个 embedding 层和一次加法运算。它在语义上把生成分布从全局分布拆成了每个类别各自的条件分布。如果要控制更细的属性比如人脸的角度、光照方向做法类似输入不再是一维类别索引而是一个特征向量通过一个线性层投影到与时间步嵌入相同的维度再相加。关键原则是条件信息必须参与网络中每一层的信息流动只加在 U-Net 的最初层容易被后续层稀释掉。6.2 采样加速DDIM 把 1000 步压缩到 50 步DDIM 是 DDPM 最重要的后续改进它改写了反向采样公式去掉随机噪声项用确定的系数进行采样。实现上的改动非常小但采样步数可以缩减到原来的十分之一eta 0.0 for t in t_schedule: # 预选的采样时间步如 [999, 949, ..., 0] t_next max(t - stride, 0) eps_pred model(x, t_tensor) x0_pred (x - sqrt_one_minus_alpha_bar[t] * eps_pred) / sqrt_alpha_bar[t] x sqrt(alpha_bar[t_next]) * x0_pred math.sqrt(1 - alpha_bar[t_next]) * eps_predDDIM 的核心思路是让反向过程不再是离散随机采样而是一条从噪声到图像的确定性路径。因此生成速度大幅提升代价是多样性可能略微下降。实际使用时把eta设为 0.5 左右做折中既能保留一定随机性也能加速收敛。6.3 验证模型效果的三个步骤跑出图像之后先不要急着调参。第一步是检查训练数据确认它不是全黑或者重复样本。第二步做过拟合测试拿同一批数据训练看 loss 能否降到很低如果不能说明模型容量或学习率有问题。第三步才进入正题采样生成图观察整体分布是否接近训练集而不是只看一张图是否好看。最后提醒一句如果你想在这个方向投入时间正确的顺序是先跑通无条件生成再改条件生成。在不理解基本训练逻辑的情况下直接上手大模型遇到问题会没有任何头绪。Diffusion 的调试周期比普通 CV 模型长但它的稳定性值得投入。希望这篇笔记能帮你少踩几个坑早日跑出自己满意的效果。本文还有配套的精品资源点击获取
网站建设高端定制企业官网