资讯动态

PyTorch实现DDPM:扩散模型训练与采样全解析

发布时间:2026/10/14 5:44:25 来源:尧图企业网站定制
简介面向深度学习与扩散模型研究者的一份可运行PyTorch源码包完整复现了DDPM去噪扩散概率模型的构建、训练与采样全流程。压缩包共27个文件大小约22.21MB包含8个Python脚本、1个模型权重文件、MNIST训练与测试数据集、依赖清单及README说明脚本按数据获取、DDPM类定义、去噪神经网络、训练与采样等模块划分目录结构清晰便于直接运行和二次修改。目前已有225人学习下载。资源采用U-Net作为去噪网络在MNIST上完成了从数据集处理、模型设计到训练与结果分析的完整实验生成图像在视觉上与真实样本接近同时对比了不同网络架构对生成效果的影响并总结了复现过程中的关键点、常见问题与注意事项。适合希望快速上手扩散模型的学生、研究者或工程人员是一份可直接运行、省去从零搭建时间的参考实现。1. 扩散模型不是黑匣子DDPM一个能稳定生成图像的 PyTorch 实现第一次跑通 DDPMDenoising Diffusion Probabilistic Models时最让我意外的不是生成效果而是它的训练居然比 GAN 简单那么多没有判别器没有对抗损失只有一个预测噪声的 MSE 和一个不断加噪的前向过程。把这两件事拼起来居然就能从纯噪声里还原出清晰的图像。这份「DDPM 的 PyTorch 实现[可运行源码]」正是围绕这条主线展开的——它把前向扩散、UNet 主干、训练循环和反向采样完整地串在一起适合两类人一类是刚接触扩散模型、想搞清楚「加噪再去噪」到底怎么落地的初学者另一类是已经跑过分类模型、想把生成式方法快速用手跑一遍的工程师。下面我按自己复现这份源码时走过的路线来拆解它哪些是核心逻辑哪些参数不能乱动哪些地方玄学但有效都放在后面。2. 前向扩散与 UNet先搞懂噪声怎么加、模型怎么接2.1 前向过程的数学结构与代码对应DDPM 的前向过程看着复杂其实核心只有一句话给定一张干净图像 x0按时间步 t 逐步往里面加高斯噪声加噪加到 T 步之后图像几乎变成纯噪声。这个过程的公式是xt sqrt(alpha_bar_t) * x0 sqrt(1 - alpha_bar_t) * epsilon其中 epsilon 是标准高斯噪声alpha_bar_t 是前 t 步噪声调度 alpha 的累积乘积。这里最关键的一点是DDPM 的作者在推导时发现不需要一步步把 x0 加到 xt可以直接用这个闭合公式一步跳到任意时间步 t。我在复现时就是靠这一条跳过了一个大坑——如果按朴素方式循环加噪训练速度会慢一个量级而且在梯度反传时容易出数值问题。噪声调度的选择在源码里通常是线性调度linear schedulebeta 从 1e-4 均匀升到 0.02T 取 1000。这个范围不是拍脑袋定的理论上讲beta 太大会让前向过程过早进入纯噪声模型来不及学中间状态太小则 1000 步结束之后噪声占比不足xT 不接近标准高斯分布采样起点就会偏离训练分布。所以复现时尽量不要把 T 改成 500 或 2000 去「试效果」除非你连 alpha_bar 的累积计算一并调整。我在实际测试中把 T 改成 500 后训练 loss 确实降得更快但生成的图明显有偏灰的色带原因就是噪声没有完全盖过数据分布。看源码里对应的实现一般长这样# diffusion.py 中的前向加噪部分 def q_sample(x_start, t, alphas_cumprod, noiseNone): x_start: 干净图像形状为 [B, C, H, W] t: 随机采样的时间步形状为 [B] alphas_cumprod: 预先算好的累积 alpha 乘积 if noise is None: noise torch.randn_like(x_start) # 每个像素独立的标准高斯噪声 sqrt_alphas_cumprod_t torch.sqrt(alphas_cumprod[t]) sqrt_one_minus_alphas_cumprod_t torch.sqrt(1.0 - alphas_cumprod[t]) # 注意这里的形状调整t 是 [B]要变成 [B, 1, 1, 1] 才能和图像广播相乘 sqrt_alphas_cumprod_t sqrt_alphas_cumprod_t[:, None, None, None] sqrt_one_minus_alphas_cumprod_t sqrt_one_minus_alphas_cumprod_t[:, None, None, None] x_noisy sqrt_alphas_cumprod_t * x_start sqrt_one_minus_alphas_cumprod_t * noise return x_noisy, noise代码逻辑上分三步先判断外面是否传入了 noise如果没有就自己生成一份标准高斯噪声然后把累积 alpha 的平方根和 1 减去它后的平方根算出来最后按公式做线性组合。这里有个容易看走眼的地方t 作为整数下标进入 alphas_cumprod 张量后得到的形状是 [B] 而不是 [B, 1]如果不加[:, None, None, None]这层 reshapePyTorch 会报广播错误或者更隐蔽的情况下广播到错误的维度导致加噪结果错位。参数说明上alphas_cumprod 的定义是torch.cumprod(1 - betas, dim0)得到的。也就是说先有 betas再有 alphas 1 - betas最后算累积乘积。如果你想改用余弦调度cosine schedule需要自己按论文公式重算 betas不能直接复用线性调度的 alphas_cumprod否则前向过程的信噪比分布和假设完全不匹配训练出来的模型采样时会发现后半段去噪步长很奇怪。2.2 UNet 的设计选择和时间步嵌入前向过程负责制造训练样本真正干活的是逆过程的神经网络。DDPM 用的主干是 UNet理由不复杂图像生成任务里输入和输出都是完整图像UNet 通过下采样提取语义特征、再上采样还原细节的结构天然适合这种 dense prediction 任务。但 DDPM 里的 UNet 比图像分割用的 UNet 多了两个关键设计。第一个是时间步嵌入time embedding。因为模型要处理不同时间步的噪声图像每一步对应的噪声强度不一样模型必须知道「现在这个输入是在哪一步被加噪的」否则它无法区分细节该恢复多少。源码里通常的做法是把 t 先做正弦位置编码再过一个小的 MLP得到一个向量然后在每个残差块里通过 scale 和 shift 的方式注入特征图。我一般会把 embedding 维度设成 256 或者和 UNet 第一层通道数一致这不算敏感参数但太小比如 32会明显影响训练速度——模型需要花更多迭代才能学会时间条件的作用。第二个是残差块加注意力。在 UNet 的中间层分辨率最低的地方插入多头注意力模块是让扩散模型能生成全局一致结构的关键。没有注意力时模型很容易出现「局部清晰、整体构图散乱」的问题在简单数据集上不明显一旦换到分辨率稍高或者物体结构复杂的数据集问题就会被放大。源码里一般只在中层和最深一两层加注意力避免计算量失控。UNet 各层通道数对显存和效果的影响也很直接。一个常见的配置是初始通道 64然后每下采样一次翻倍到最底层 512上采样路径再对称降回来。如果你的显存只有 8GB 左右初始通道要降到 32或者减少下采样层数否则直接在 64x64 分辨率上训练会 OOM。但要注意通道数减半之后层数如果不变感受野没变生成效果下降主要是模型容量不够不是训练迭代数的问题需要同时增加训练步数来弥补。# model.py 中的基础 UNet 块结构简化版 class UNetBlock(nn.Module): def __init__(self, in_ch, out_ch, time_emb_dim, has_attnFalse): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, padding1) self.conv2 nn.Conv2d(out_ch, out_ch, 3, padding1) self.norm1 nn.GroupNorm(8, in_ch) self.norm2 nn.GroupNorm(8, out_ch) self.time_mlp nn.Sequential( nn.SiLU(), nn.Linear(time_emb_dim, out_ch * 2) ) def forward(self, x, t_emb): h self.norm1(x) h nn.functional.silu(h) h self.conv1(h) # 注入时间条件让模型知道当前噪声程度 time_scale_shift self.time_mlp(t_emb) scale, shift time_scale_shift.chunk(2, dim1) scale scale[:, :, None, None] shift shift[:, :, None, None] h h * (1 scale) shift h self.norm2(h) h nn.functional.silu(h) h self.conv2(h) return h x if in_ch out_ch else h self.skip_conv(x)这里我把时间注入放在第一个卷积之后、归一化之前这是比较常见的 FiLM 式做法。scale 和 shift 分别对特征图做乘性和加性变换让模型能根据时间步动态调整特征响应的幅度。注意 skip connection 处理如果输入输出通道不一致必须加一个 1x1 卷积把 x 投影到 out_ch否则维度对不上直接报错。我在第一次复现时忽略了一个细节GroupNorm 的组数。很多分类模型里习惯用 BatchNorm但扩散模型里 BatchNorm 是坑——batch size 小的时候统计量抖动很大生成效果非常不稳定。用 GroupNorm 就稳得多基本不受 batch size 变化影响。源码里一般用 num_groups8如果你的输入通道数是 648 组就是每组 8 个通道这个比例是安全的。改通道数时记得同时检查组数是否能被整除。3. 训练思路与代码实现预测噪声而不是预测图像3.1 为什么损失函数选 MSE 噪声预测DDPM 训练的核心逻辑说起来反直觉网络输入一张加了噪声的图像和对应时间步输出不是去噪后的图像而是「我加的噪是什么」。也就是预测噪声 epsilon然后用预测值和真实噪声算 MSE 损失。为什么不直接预测 x0理论上预测 x0 也可以收敛但实践中有两个问题。第一x0 的数值范围在归一化之后是 [-1, 1]而不同时间步加入噪声后的 xt 分布方差差别很大直接预测 x0 在不同时间步上损失贡献不均衡模型会更倾向于优化噪声占比小的步导致高频细节学不好。第二预测噪声在数学推导中和变分下界直接对应训练更稳。所以源码里的损失函数一般就是这段逻辑# diffusion.py 中的训练损失计算 def p_losses(denoise_model, x_start, t, noiseNone): denoise_model: UNet输入噪声图像和时间步输出预测噪声 x_start: 干净原图 t: 随机采样的时间步 if noise is None: noise torch.randn_like(x_start) x_noisy, _ q_sample(x_start, t, alphas_cumprod, noise) # 模型输出通道数和输入一致都是 3 通道图像 predicted_noise denoise_model(x_noisy, t) # 直接对原始噪声计算 MSE不做加权 loss F.mse_loss(predicted_noise, noise) return loss这里有个值得注意的选择损失没有对不同时间步做加权。原论文在简化目标里就是简单 MSE不加任何权重。我之前试过给中间步加权重理论上应该更合理实际效果反而变差。原因大概是中间步的样本量足够多网络容量有限不加权重时模型会自动把容量分配给更常出现的噪声尺度加权反而破坏了这种自然的容量分配。参数说明训练时 t 是从 0 到 T-1 里均匀随机采样每个 batch 内每张图的 t 可以不同这个随机性是训练稳定的关键。如果改成固定顺序采样比如从 0 到 999 循环模型会在相近的噪声级别上反复训练泛化能力大打折扣。3.2 训练循环核心与超参设定训练循环本身比常见分类任务还简单主要步骤是从数据加载器取一个 batch 的图归一化到 [-1, 1]随机采样 t计算加噪后的图送入 UNet 预测噪声算 MSE反传更新。这里没有判别器、没有对抗损失、没有梯度惩罚代码写起来反而清爽。# 训练配置示例 --batch_size 32 --epochs 200 --lr 1e-4 --T 1000 --image_size 64 --channels 3 --save_dir ./checkpoints这个配置在单张 24GB 显卡上可以跑 64x64 的 CIFAR-10 级别数据集。如果你的显卡是 12GBbatch size 要降到 16 甚至 8但注意学习率要跟着降——我发现 batch size 从 32 降到 8 时lr 保持 1e-4 会导致 loss 曲线明显震荡降到 5e-5 就平稳很多。训练循环的骨架# train.py 核心循环 optimizer torch.optim.AdamW(model.parameters(), lrargs.lr, weight_decay0.0) for epoch in range(args.epochs): for step, (images, _) in enumerate(train_loader): images images.to(device) # 图像归一化到 [-1, 1]扩散模型训练的前置条件 images images * 2.0 - 1.0 t torch.randint(0, args.T, (images.size(0),), devicedevice) loss p_losses(model, images, t) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() if step % 100 0: print(fEpoch {epoch} Step {step} Loss {loss.item():.4f})AdamW 的 weight_decay 我建议保持 0。这不是原论文的明确要求但我在复现中发现加 L2 正则对扩散模型没有正面作用因为模型本身没有过拟合到需要正则化反而拖慢了收敛。梯度裁剪设 max_norm1.0 是一个性价比很高的操作扩散模型训练早期梯度可能突然变大导致 loss 变成 NaN裁剪之后这种问题基本消失。学习率的设定上我用经验值 1e-4 起步配合 cosine 衰减到 0。没有用 warmup 也能训但损失在最初几千步内会从很高的值快速下降如果不想看到 loss 曲线早期飞太高加一个 500 步的 warmup 会更平滑。EMA指数滑动平均在训练结束前的作用很关键这个放到后面专门讲。优化器的选择也有讲究。Adam 和 AdamW 都可以但我发现 AdamW 在 batch size 比较小时的稳定性更好一点原因可能是权重衰减的实现方式不同带来的数值差异属于玄学范畴不过既然两种都能跑通跟着源码用 AdamW 就行。混合精度训练用 auto_cast 能省不少显存但要注意前向加噪的 q_sample 里涉及 sqrt 和乘加操作把 alphas_cumprod 保持在 float32 可以避免精度损失。我自己实践时会把模型、优化器和混合精度都设成 fp16但 alphas_cumprod 这个调度张量固定用 float32否则采样时会出现细微的噪声偏差。训练过程的监控指标就是 loss 本身没有 val loss 的概念。你只需要看它是否在平稳下降。我一般会跑到 loss 从最初的 1.5 左右降到 0.2 以下才开始评估生成效果。如果 loss 在 0.2 附近就完全不降了常见原因是数据集太大而模型容量不够或者学习率衰减太快。4. 反向采样从纯噪声一步步还原图像4.1 采样公式的推导直觉训练完成后生成图像是一个反向过程从标准高斯噪声 xT 开始用训练好的 UNet 逐步预测并去除噪声最终得到 x0。采样时每步的核心公式是x_{t-1} 1 / sqrt(alpha_t) * (x_t - beta_t / sqrt(1 - alpha_bar_t) * epsilon_theta) sigma_t * z这里 epsilon_theta 是模型预测的噪声sigma_t 是这一反推步引入的随机噪声的标准差。理解这个公式不需要把推导完全吃透你只需要抓住一个直觉模型告诉你这一步图像里「有哪些内容其实是噪声」你把这些噪声按比例减掉回到更接近干净图像的一步但因为单步去噪不够完美还要补一点随机噪声来维持分布多样性。这里有个容易误解的地方采样时的 sigma_t 如果设成 0就是 DDIM 的确定性采样生成仍然有效但多样性下降如果按 DDPM 原版取 sigma_t^2 beta_t就是带随机性的采样。源码里通常用的是后者。我最初为了图省事直接把 sigma_t 设成 0结果确实能生成图像但同一类样本的构图非常雷同说明失去随机性导致多样性受损。所以除非你有明确的加速需求否则不要轻易去掉这一步随机噪声。倒数第一步有个细节当 t0 时一般不施加随机噪声直接输出均值。原因是最后一步已经接近干净图像再加噪声反而会让图像出现颗粒感。源码里一般通过判断 t0 来跳过 sigma 项。4.2 完整采样流程与后处理采样代码是完整可运行的内核整个生成过程如下# sample.py 中的采样循环 torch.no_grad() def p_sample(model, x_t, t, betas, alphas, alphas_cumprod, alphas_cumprod_prev): 单步反向采样从 x_t 得到 x_{t-1} t: 当前时间步标量 beta_t betas[t] alpha_t alphas[t] alpha_cumprod_t alphas_cumprod[t] alpha_cumprod_prev_t alphas_cumprod_prev[t] # 模型预测当前噪声 pred_noise model(x_t, t) # 计算去噪后的均值项 mu (x_t - beta_t / torch.sqrt(1.0 - alpha_cumprod_t) * pred_noise) / torch.sqrt(alpha_t) if t 0: return mu # 后验方差这一步的随机性来自反向过程的方差 var beta_t * (1.0 - alpha_cumprod_prev_t) / (1.0 - alpha_cumprod_t) noise torch.randn_like(x_t) return mu torch.sqrt(var) * noise def p_sample_loop(model, shape, betas, alphas, alphas_cumprod, alphas_cumprod_prev): 从纯噪声逐步生成图像 shape: 生成图像的形状 [B, C, H, W] device next(model.parameters()).device x_t torch.randn(shape, devicedevice) for t in reversed(range(len(betas))): t_tensor torch.full((x_t.size(0),), t, devicedevice, dtypetorch.long) x_t p_sample(model, x_t, t_tensor, betas, alphas, alphas_cumprod, alphas_cumprod_prev) # 从 [-1, 1] 还原到 [0, 1] x_t torch.clamp(x_t, -1.0, 1.0) images (x_t 1.0) / 2.0 return images逻辑说明整个循环从 t999 反向走到 t0每一步都调用 p_sample。reversed(range(len(betas))) 生成的顺序是 999, 998, ..., 0正好对应从高噪声到低噪声的过程。t_tensor 的构造是因为模型期望接收一个 batch 的时间步张量即使 batch 里所有图用的都是同一个 t形状还是要符合 [B]。参数说明这里有个细节值得记住——模型中输入的时间步 t 应该用原始整数还是归一化后的值源码里一般直接用整数因为时间嵌入层内部会做正弦位置编码不需要预先归一化。如果你发现模型生成效果很差检查一下采样时传入的时间步是否和训练时一致我在一次复现中因为把时间步当成浮点数传入位置编码维度错位生成结果几乎全是花斑噪声排查了很久才发现是这个原因。采样速度上完整 1000 步迭代意味着要调用 1000 次 UNet 前向这是 DDPM 被诟病最多的地方。如果训练用的是 64x64 分辨率、batch size 8单张 24GB 显卡上生成一批图大约需要几十秒到几分钟不等。如果嫌慢可以只step 500 步做快速验证但注意 alpha_cumprod_prev 要相应截取不能用完整序列去和 500 步的采样过程匹配否则方差计算会错位。5. DDPM 复现常见问题与排查五条实测翻车记录5.1 现象清单与原因分析我在拆解这份源码时专门做了几次破坏性实验把常见问题按「现象 → 原因 → 解决」整理成下面几条基本覆盖了新手最容易踩的坑。坑一生成图像全是黑白颗粒噪点。现象训练结束后采样输出的图像像是没有训练过全是椒盐噪声。原因图像归一化方向反了。如果训练时把 [-1, 1] 的数据映射成了 [0, 1]或者采样后处理时没有做 clamp输出会超出可视化范围。解决检查训练前归一化是否做了images * 2 - 1采样后是否做了(x 1) / 2并 clamp。这个问题最常见因为前向加噪公式假设图像范围在 [-1, 1]一旦输入范围不对信噪比计算全部偏差。坑二loss 降得很低但生成图是灰色雾面。现象训练 loss 已经降到 0.2 以下但生成的图像清晰度很差像隔着一层雾边缘完全模糊。原因T 设置得太大或者 beta 上界过高导致前向过程在时间步还比较小的时候信噪比就已经特别低模型学到的主要是纯噪声到噪声的映射真正的图像结构没有充分暴露。解决把 beta 上界从 0.02 降回 0.01 到 0.015 试试或者保持 beta 不变但减小 T 到 800重新训练观察变化。我在实验中把 beta 上界设成 0.03 时同样迭代数下生成效果明显劣化。坑三CUDA OOM但模型看起来并不大。现象batch size 32、初始通道 64、64x64 图像显存 24GB 却爆了。原因不是模型参数太大而是训练循环中 q_sample 里的中间张量占用了显存。sqrt_alphas_cumprod 和 sqrt_one_minus 两个张量虽然都是 [B, 1, 1, 1] 形状但和 [B, C, H, W] 的图像相乘时会广播出完整的中间结果。另外混合精度没有生效时临时张量都是 fp32。解决先把 batch size 降到 16然后检查是否用了torch.cuda.amp.autocast()如果还在爆把初始通道降到 48或者减少一个下采样层。坑四不同 batch size 训练效果差异巨大。现象同一条代码batch size 8 训 100 轮效果明显好于 batch size 32 训 100 轮。原因如果 UNet 里用了 BatchNormbatch size 小的统计量噪声反而可能起到正则作用而 batch size 大时统计量更稳定但模型容量不变每轮更新次数少所以总迭代数不足。解决用 GroupNorm 替代 BatchNorm让模型对 batch size 不敏感同时把训练步数作为主要控制变量而不是 epoch 数。我在读源码时注意它内部统一用的是 GroupNorm如果你改名成 BatchNorm大概率会遇到这个问题。坑五训练 loss 正常但采样时出现 NaN。现象训练阶段 loss 没异常采样循环跑到中间某一步时输出突然变成 NaN。原因最常见的是 alpha_cumprod_prev 计算错误。如果 alphas_cumprod_prev 的定义是F.pad(alphas_cumprod[:-1], (1, 0), value1.0)那么当 t0 时alpha_cumprod_prev_t 应该是 1.0但如果代码里没有做这个 padding或者用的是alphas_cumprod[:-1]直接索引就会下标越界或者取到错误的值导致方差项变成负数或零sqrt 之后就是 NaN。解决检查 alpha_cumprod_prev 的构造确保长度是 T1另一个可能是模型输出变成了 NaN这时需要检查是否开启了混合精度且没有做梯度裁剪。5.2 如何快速定位问题遇到模型生成效果不对时不要盲目调参我有一套固定排查流程。第一步先做「重建测试」取一张训练集中的图 x0不经过训练直接加噪到某个中等时间步 t比如 500看模型预测的噪声和真实噪声相差多少。如果这一步都错得很离谱说明模型训练本身有问题和采样无关。第二步检查时间步嵌入是否有效可以把 t 全部替换成 0如果模型输出变化不大说明时间条件没有被有效利用问题多半出在 UNet 时间注入的结构上。第三步检查图像范围在训练循环里加一个 assert确保输入 x_start 的范围是 [-1, 1]。这一步能过滤掉绝大多数「生成结果奇怪」的问题。一个有用的诊断脚本片段# 重建测试加噪到中间步看模型预测与实际噪声的差异 with torch.no_grad(): img next(iter(train_loader))[0][:1].to(device) img img * 2.0 - 1.0 t torch.tensor([500], devicedevice) x_noisy, real_noise q_sample(img, t, alphas_cumprod) pred_noise model(x_noisy, t) diff (pred_noise - real_noise).abs().mean().item() print(f重建噪声误差: {diff:.4f})如果把 t 选择为 500 时误差在 0.1 以下说明模型对中间噪声步的理解已经到位如果误差在 0.3 以上先检查训练步数够不够再看图像归一化是否正确。如果训练已经很久误差还是偏高考虑降低初始通道数因为模型容量可能没有充分利用这也算一种玄学诊断容量不够时 lr 跑不稳loss 会呈现出锯齿状下降重建误差也会卡在某个值附近。6. 进阶技巧EMA、采样加速与验证6.1 EMA 与混合精度的习惯训练快结束时我发现一个现象直接用模型权重采样图像偶尔会出现细小的条纹伪影而用 EMA 权重采样伪影几乎消失整体也更干净。EMA 的做法是维护一个权重影子副本每次 optimizer.step 之后把模型参数按一定比例融合进去常用衰减系数是 0.999。这个系数很敏感太大则收敛慢太小则起不到平滑作用。实践下来训练步数总数在十万到几十万之间时0.999 是比较保险的选择如果你迭代数少降到 0.995 更合适。混合精度在这个资源里属于性价比很高的优化项。开启 autocast 之后显存占用能减少约 30%训练速度也能提升。但有两个注意点一是梯度裁剪要在 gradients 被缩放之前做否则裁剪阈值会被放大 2 的幂次倍导致裁剪失效二是 q_sample 中的 alpha_cumprod 建议保持在 float32避免反复 sqrt 后精度离散化让采样过程中的方差项产生轻微的系统性偏移。这部分的实践代码可以直接在训练循环里加ema_params {} for name, param in model.named_parameters(): ema_params[name] param.detach().clone() # optimizer.step 之后更新 EMA for name, param in model.named_parameters(): ema_params[name] decay * ema_params[name] (1 - decay) * param.detach()6.2 定量验证FID 之外先做重建检查很多人复现完第一件事就是跑 FID但我觉得 FID 对初学者是个陷阱——它需要一批真实图像做参考还要预处理到特定尺寸统计分布差异时对生成质量的小问题不敏感。我最推荐先做两件事第一是刚才说的重建误差检查它告诉你模型对加噪-去噪这个映射学得是否准确第二是「自洽性检查」采样一张图后再把它加噪到较大的 t然后用同一个模型逐步还原看还原出来的图是否和原始生成图一致。如果自洽性都做不好FID 再好看也说明模型只是碰巧抓住了分布。我在复现这份源码时习惯把训练损失曲线的形状也当作质量信号。DDPM 的 loss 曲线应该是一条平滑下降的指数形状前 1/5 的步数里下降最快之后渐趋平缓。如果你看到曲线在某个平台值附近反复震荡不是学习率的问题而更可能是 UNet 容量不够或者时间步嵌入维度太小。那条平台值对应的模型能力上限很难通过继续训练突破换参数比硬训更值得投入时间。回顾整个复现过程最深刻的教训就是扩散模型的门槛不在代码本身而在于对噪声调度和图像范围这两个看似不起眼的细节保持敏感。现在每当我换一个数据集或分辨率跑这套流程都会强制走一遍重建测试和自洽性检查这两个过程能在两小时内暴露八成的问题。希望这份拆解能帮你在复现 DDPM 时少走这些我已经踩过的弯路直接看到清晰而稳定的生成结果。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑