1. 从零手写DDPM为什么我不建议你直接跑现成仓库很多人第一次接触扩散模型都是从调用现成的推理脚本开始的——输入一句提示词等几秒出一张图感觉很神奇。但如果你真的想搞明白扩散模型到底在干什么想自己改结构、调采样、做实验那绕不开的一步就是亲手把DDPMDenoising Diffusion Probabilistic Models实现一遍。我这次做的事情就是在一个图像生成项目里从零实现DDPM的完整训练和采样流程然后把它和Stable Diffusion的潜空间扩散思路做对比。整个过程踩了不少坑有些是数学推导上的理解偏差有些是工程实现上的细节疏忽还有一些是训练稳定性方面的经验缺失。这篇文章就是把这些东西全部摊开来讲给同样想动手实现DDPM的朋友省点时间。DDPM这个东西表面上看公式不多核心就是前向加噪和反向去噪两个过程。但真正写代码的时候你会发现每一个变量的维度、每一个系数的计算方式、每一个时间步的调度策略都会直接影响最终能不能训出东西来。它不像分类网络那样结构对了、损失对了基本就能跑。扩散模型对细节的敏感度要高得多。这篇文章适合谁看如果你已经了解过扩散模型的基本概念知道前向过程是逐步加高斯噪声、反向过程是学习去噪但还没有完整实现过一遍那这篇内容就是为你准备的。如果你已经在跑Stable Diffusion的推理想深入理解它背后的DDPM训练逻辑这篇文章也能帮你把底层链路串起来。我会从整体设计思路讲起然后逐层拆解核心细节接着给出完整的实操流程和参数选择依据最后把我在训练过程中遇到的典型问题和排查方法整理出来。整个实现基于PyTorch数据集用的是一个小规模的图像集合具体名称不重要任何32x32或64x64的图像集都可以替代训练环境是一块消费级显卡。代码结构不复杂但每一个模块的设计都有它的道理我会把这些道理讲清楚。2. 整体设计思路与方案选型拆解2.1 为什么选择DDPM而不是其他生成模型在动手之前先想清楚一个问题生成模型那么多GAN、VAE、Flow-based模型都能做图像生成为什么偏偏选DDPM我当时的考量是这样的。GAN的训练稳定性是出了名的难搞判别器和生成器的博弈很容易崩溃调参成本极高。VAE生成的图像往往偏模糊因为它的重建损失和KL散度之间存在天然的张力。Flow-based模型要求可逆变换结构设计受限很大。而DDPM的优势在于训练目标非常简单——就是预测噪声本质上是一个回归任务训练过程稳定不像GAN那样需要精心平衡两个网络生成质量上限高这一点后来被Stable Diffusion等大规模模型验证了。当然DDPM的缺点也很明显采样速度慢。标准DDPM需要上千步才能生成一张图这也是后来DDIM、DPM-Solver等加速采样方法出现的原因。但对于学习和理解扩散模型的核心机制来说DDPM是最合适的起点。提示如果你只是想快速出图直接用现成的推理管线就行。但如果你想理解扩散模型为什么能work手写DDPM是最短路径。2.2 前向过程与反向过程的数学直觉DDPM的核心思想可以用一句话概括把一张图像逐步加噪变成纯高斯噪声然后训练一个网络学会从噪声中逐步恢复出图像。前向过程是一个马尔可夫链定义为q(x_t | x_{t-1}) N(x_t; sqrt(1-β_t) * x_{t-1}, β_t * I)其中β_t是每一步的噪声方差通常从1e-4线性增加到0.02总共T步一般T1000。这个过程的妙处在于它允许我们直接计算任意时间步t的加噪结果q(x_t | x_0) N(x_t; sqrt(ᾱ_t) * x_0, (1-ᾱ_t) * I)其中α_t 1 - β_tᾱ_t是α_t的累积乘积。这意味着我们不需要真的迭代t次加噪而是一步就能得到x_t。这个性质在训练时极其重要因为它让每次训练迭代的计算成本与T无关。反向过程则是学习一个网络ε_θ(x_t, t)来预测加入的噪声然后据此逐步去噪。训练损失就是预测噪声和真实噪声之间的MSEL E[||ε - ε_θ(sqrt(ᾱ_t) * x_0 sqrt(1-ᾱ_t) * ε, t)||²]这个损失函数看起来简单得不像话但它背后有变分下界的数学推导支撑。实际实现时你不需要去推导ELBO只需要记住训练就是让网络学会从加噪图像中预测出被加入的噪声。2.3 网络结构的选择U-Net为什么是标配DDPM原文用的是U-Net结构这不是随便选的。U-Net的编码器-解码器结构配合跳跃连接能够同时捕捉全局语义信息和局部细节。对于去噪任务来说网络需要理解整张图像的上下文才能判断哪些是噪声、哪些是信号而跳跃连接又能保留高频细节避免在降采样过程中丢失太多信息。我在实现时对U-Net做了几个关键设计时间步嵌入把整数时间步t通过正弦位置编码转成向量再经过两层MLP注入到每个残差块中。这样网络能感知当前处于哪个噪声水平。注意力机制在U-Net的中间层和部分下采样层加入自注意力让网络能在不同空间位置之间建立长距离依赖。残差块每个分辨率层级用两个残差块配合GroupNorm和SiLU激活函数。这些设计在DDPM原文和后续实现中都是标配我基本沿用了但在通道数、注意力头数等超参数上根据自己的数据集做了调整。2.4 训练策略的整体框架训练流程可以概括为从数据集中采样一批图像x_0为每张图像随机采样一个时间步t采样标准高斯噪声ε计算加噪图像x_t sqrt(ᾱ_t) * x_0 sqrt(1-ᾱ_t) * ε把x_t和t输入网络得到预测噪声ε_θ计算MSE损失并反向传播这个流程每个step只需要一次前向和一次反向计算效率很高。但有几个细节需要注意时间步t的采样策略、噪声调度的选择、学习率的warmup等这些都会影响训练效果。3. 核心细节解析与实操要点3.1 噪声调度线性还是余弦噪声调度决定了β_t如何随时间步变化。DDPM原文用的是线性调度从β_11e-4线性增加到β_T0.02。但我实际训练时发现线性调度在低分辨率图像上表现还行但在稍高分辨率的数据上后期时间步的信噪比下降太快导致模型在接近纯噪声的区域学不到有效信息。后来我换成了余弦调度这是Improved DDPM论文提出的方案ᾱ_t cos²((t/T s) / (1 s) * π/2) / cos²(s / (1 s) * π/2)其中s是一个小的偏移量通常取0.008防止β_t在t0附近太小。余弦调度的优势在于它让ᾱ_t在中间时间步下降得更平缓保证了每个时间步的信噪比分布更均匀模型在各个噪声水平上都能得到充分的训练。实测下来余弦调度在相同训练步数下生成的图像细节更丰富尤其是在纹理复杂的区域。代价是实现稍微复杂一点需要预先计算好ᾱ_t的数组。注意如果你用的是线性调度建议把β_T从0.02适当降低到0.01左右避免后期信噪比过低。但这个调整需要配合你的数据集和分辨率来定没有万能值。3.2 时间步嵌入的实现细节时间步嵌入是DDPM实现中容易被忽视但非常关键的一环。网络需要知道当前处理的是哪个噪声水平才能做出正确的去噪决策。如果时间步嵌入做得不好模型可能会在不同噪声水平之间混淆导致生成质量下降。我采用的是正弦位置编码加MLP的方案class SinusoidalPositionEmbedding(nn.Module): def __init__(self, dim): super().__init__() self.dim dim def forward(self, t): device t.device half_dim self.dim // 2 emb math.log(10000) / (half_dim - 1) emb torch.exp(torch.arange(half_dim, devicedevice) * -emb) emb t[:, None] * emb[None, :] emb torch.cat([emb.sin(), emb.cos()], dim-1) return emb这个编码方式的好处是不同时间步之间的编码向量有平滑的过渡网络能学到时间步之间的连续性。MLP部分我用了两层线性层加SiLU激活维度从128扩展到512再映射回目标维度。踩过的坑一开始我把时间步嵌入的维度设得太小只有32结果模型在中间时间步的去噪效果很差生成的图像在中等噪声水平下会出现明显的伪影。后来把嵌入维度提高到128问题就解决了。经验是时间步嵌入的维度不要低于64对于64x64以上的图像建议128起步。3.3 损失函数的设计与权重调整DDPM的标准损失是简单的MSE但实际训练时我发现不同时间步的损失量级差异很大。在t接近0时加噪很少预测噪声很容易在t接近T时图像几乎全是噪声预测难度也相对固定。真正难的是中间时间步此时信号和噪声混合在一起网络需要仔细分辨。Improved DDPM提出了一个改进方案让网络同时预测噪声和方差并用一个混合损失来训练。但我实际试下来对于小规模数据集单纯预测噪声的简单MSE已经足够加入方差预测反而增加了训练不稳定性。不过我确实对损失做了一个小调整对不同时间步的损失进行加权。具体来说我按照1/sqrt(1-ᾱ_t)的权重对每个时间步的损失进行缩放让中间时间步的损失获得更大的梯度。这个权重来自DDPM原文的简化推导实测能让模型更快收敛。def compute_loss(model, x_0, t): noise torch.randn_like(x_0) alpha_bar_t extract(alpha_bars, t, x_0.shape) x_t torch.sqrt(alpha_bar_t) * x_0 torch.sqrt(1 - alpha_bar_t) * noise predicted_noise model(x_t, t) loss F.mse_loss(predicted_noise, noise, reductionnone) loss loss.mean(dim[1, 2, 3]) weight 1.0 / torch.sqrt(1 - alpha_bar_t.squeeze()) loss (loss * weight).mean() return loss提示损失加权不是必须的但如果你发现模型在中间时间步的去噪效果不理想可以试试这个方案。权重系数可以根据实际情况微调。3.4 采样过程的实现与加速训练完成后采样过程就是从纯噪声x_T开始逐步去噪得到x_0。标准DDPM采样需要迭代T次通常1000步每一步都要调用一次网络速度很慢。采样公式为x_{t-1} (1/sqrt(α_t)) * (x_t - (β_t/sqrt(1-ᾱ_t)) * ε_θ(x_t, t)) σ_t * z其中σ_t sqrt(β_t)z是标准高斯噪声t0时不加。这个公式的推导来自贝叶斯后验的均值方差计算实现时直接套用即可。为了加速采样我实现了DDIM采样。DDIM的核心思想是把采样过程变成一个确定性的过程或者可以调节随机性并且允许跳步。具体来说DDIM采样时不需要每一步都加噪声而是用一个可调的η参数控制随机性。当η0时采样完全确定可以用更少的步数比如50步或100步得到不错的结果。def ddim_sample(model, shape, ddim_steps100, eta0.0): x torch.randn(shape) times torch.linspace(T-1, 0, ddim_steps).long() for i in range(len(times)-1): t times[i] t_next times[i1] alpha_bar_t alpha_bars[t] alpha_bar_next alpha_bars[t_next] pred_noise model(x, t) pred_x0 (x - torch.sqrt(1-alpha_bar_t) * pred_noise) / torch.sqrt(alpha_bar_t) pred_x0 torch.clamp(pred_x0, -1, 1) sigma eta * torch.sqrt((1-alpha_bar_next)/(1-alpha_bar_t) * (1-alpha_bar_t/alpha_bar_next)) c torch.sqrt(1 - alpha_bar_next - sigma**2) x torch.sqrt(alpha_bar_next) * pred_x0 c * pred_noise sigma * torch.randn_like(x) return x实测下来DDIM用100步采样的效果已经接近DDPM用1000步的效果速度提升了10倍。如果对生成质量要求不高50步也能出可用的结果。4. 完整实操流程与核心环节实现4.1 环境准备与依赖安装环境配置这块我踩的第一个坑就是版本兼容性。PyTorch的版本和CUDA驱动之间的匹配问题就不多说了这是老生常谈。我想说的是扩散模型的实现中用到的一些操作比如GroupNorm、SiLU在不同PyTorch版本下的行为可能有细微差异建议固定一个稳定版本。我用的环境是Python 3.9PyTorch 1.13.1 CUDA 11.7torchvision 0.14.1numpy、tqdm、matplotlib用于可视化安装命令很简单pip install torch1.13.1cu117 torchvision0.14.1cu117 -f https://download.pytorch.org/whl/torch_stable.html pip install numpy tqdm matplotlib数据集方面我用的是一个32x32的图像集合通过torchvision的DataLoader加载做了随机水平翻转和归一化到[-1, 1]的预处理。归一化到[-1, 1]而不是[0, 1]是因为DDPM的噪声调度假设数据均值为0这样加噪后的分布更接近标准高斯。4.2 噪声调度参数的计算与存储在训练开始之前需要预先计算好所有时间步的α_t、ᾱ_t、β_t等参数。这些参数在训练和采样中都会反复用到预先计算好存成数组避免重复计算。T 1000 beta_start 1e-4 beta_end 0.02 # 线性调度 betas torch.linspace(beta_start, beta_end, T) alphas 1.0 - betas alpha_bars torch.cumprod(alphas, dim0) # 余弦调度备选 # s 0.008 # steps torch.arange(T 1, dtypetorch.float32) # f torch.cos((steps / T s) / (1 s) * math.pi / 2) ** 2 # alpha_bars f / f[0] # betas 1 - alpha_bars[1:] / alpha_bars[:-1] # alphas 1.0 - betas # alpha_bars alpha_bars[1:]这里有一个细节alpha_bars的最后一个值应该接近0表示x_T几乎是纯噪声。如果alpha_bars[T-1]太大比如大于0.01说明加噪不够充分采样时从纯高斯噪声开始会与训练分布不匹配。我一开始用线性调度时alpha_bars[T-1]大约是0.00004没问题。但如果你调整了beta_end一定要检查这个值。注意alpha_bars的计算必须用cumprod不能用cumsum。这个坑我踩过当时不小心写成了cumsum结果训练损失一直不下降排查了半天才发现是这里的问题。4.3 U-Net模型的搭建与参数配置U-Net的结构我按照DDPM原文的设计来但做了一些简化。基础通道数设为64通道倍数设为[1, 2, 4]即三个分辨率层级。每个层级包含两个残差块中间层加自注意力。class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels, time_emb_dim): super().__init__() self.norm1 nn.GroupNorm(8, in_channels) self.conv1 nn.Conv2d(in_channels, out_channels, 3, padding1) self.time_mlp nn.Linear(time_emb_dim, out_channels) self.norm2 nn.GroupNorm(8, out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, 3, padding1) self.residual_conv nn.Conv2d(in_channels, out_channels, 1) if in_channels ! out_channels else nn.Identity() def forward(self, x, t_emb): h self.norm1(x) h F.silu(h) h self.conv1(h) t_emb self.time_mlp(F.silu(t_emb)) h h t_emb[:, :, None, None] h self.norm2(h) h F.silu(h) h self.conv2(h) return h self.residual_conv(x)参数配置方面我总结了几条经验基础通道数32x32图像用64就够了64x64建议128起步。通道数太少会导致模型容量不足生成图像模糊太多则训练慢且容易过拟合。注意力头数中间层用4个头每个头32维。头数太多在小分辨率下收益不大。GroupNorm的组数固定为8或者设为通道数的约数。组数太少会导致归一化效果不好太多则计算开销增加。Dropout在残差块中加0.1的Dropout有助于防止过拟合尤其是在小数据集上。模型总参数量大约在30M左右对于32x32图像来说足够了。训练时用AdamW优化器学习率2e-4配合余弦退火调度和1000步的warmup。4.4 训练循环的实现与监控训练循环的实现看起来直接但有几个细节决定了训练能不能收敛。def train_step(model, x_0, optimizer): model.train() batch_size x_0.shape[0] t torch.randint(0, T, (batch_size,), devicex_0.device).long() noise torch.randn_like(x_0) alpha_bar_t alpha_bars.to(x_0.device)[t] alpha_bar_t alpha_bar_t.view(-1, 1, 1, 1) x_t torch.sqrt(alpha_bar_t) * x_0 torch.sqrt(1 - alpha_bar_t) * noise predicted_noise model(x_t, t) loss F.mse_loss(predicted_noise, noise) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() return loss.item()关键点说明时间步采样用torch.randint均匀采样。有研究表明对时间步进行非均匀采样比如更关注中间时间步可能效果更好但我实测下来均匀采样已经够用。梯度裁剪必须加阈值设为1.0。扩散模型的梯度在某些时间步可能会突然变大不裁剪容易导致训练不稳定。混合精度训练用torch.cuda.amp可以显著减少显存占用并加速训练。但要注意混合精度下MSE损失可能会溢出需要用GradScaler来处理。训练过程中我监控三个指标损失值、梯度范数、以及每隔一定步数生成的样本。损失值不用太在意绝对值关键是看趋势是否稳定下降。梯度范数如果持续很大说明学习率可能太高。生成的样本是最直观的反馈能帮你判断模型是否在学到有意义的东西。我训练了大约50个epoch在单卡上花了不到一天时间。损失从最初的0.15左右降到了0.03左右生成的图像虽然不算精致但已经能看出清晰的轮廓和纹理。4.5 采样与结果可视化训练完成后采样过程就是前面提到的DDPM或DDIM采样。我写了一个简单的可视化脚本把采样过程中的中间结果保存下来方便观察去噪的逐步过程。torch.no_grad() def sample_and_visualize(model, n_samples16, ddim_steps100): model.eval() samples ddim_sample(model, (n_samples, 3, 32, 32), ddim_stepsddim_steps) samples (samples 1) / 2 samples samples.clamp(0, 1) grid torchvision.utils.make_grid(samples, nrow4) plt.figure(figsize(8, 8)) plt.imshow(grid.permute(1, 2, 0).cpu().numpy()) plt.axis(off) plt.savefig(samples.png, dpi150, bbox_inchestight)实测下来DDIM用100步采样的结果和DDPM用1000步的结果在视觉上差异很小但速度快了10倍。如果只是做实验验证DDIM是更好的选择。5. 常见问题与排查技巧实录5.1 训练损失不下降或震荡这是最常见的问题可能的原因有多个。我按照排查优先级列一下问题现象可能原因排查方法解决方案损失完全不降alpha_bars计算错误检查alpha_bars[T-1]是否接近0用cumprod而非cumsum损失震荡剧烈学习率过高打印梯度范数降低学习率或加warmup损失下降后反弹过拟合对比训练集和验证集损失加Dropout或数据增强损失卡在某个值时间步嵌入有问题可视化时间步嵌入向量增大嵌入维度或检查实现我遇到过一次损失完全不降的情况排查后发现是alpha_bars的计算用错了函数。这个错误很隐蔽因为代码不会报错只是数值不对。建议在训练前打印几个关键时间步的alpha_bars值确认它们符合预期。5.2 生成图像全是噪声或模糊训练损失正常下降但采样出来的图像全是噪声或者非常模糊这通常说明模型没有真正学到去噪能力。可能的原因采样步数太少DDPM需要足够的步数才能逐步去噪如果步数太少去噪不充分。建议至少100步DDIM或500步DDPM。网络容量不足如果U-Net太浅或通道数太少模型无法拟合复杂的去噪函数。尝试增加通道数或层数。训练不充分扩散模型通常需要较长时间训练小数据集上至少几十个epoch。如果损失还在下降继续训。数据归一化问题如果数据没有归一化到[-1, 1]加噪后的分布会和标准高斯有偏差影响训练。检查你的数据预处理。我踩过的一个坑是采样时忘记把模型设为eval模式Dropout还在起作用导致生成的图像有随机噪声。这个错误很低级但很容易忽略。5.3 显存不足与训练速度优化扩散模型的训练显存占用主要来自U-Net的中间激活值。对于32x32图像和30M参数的模型batch size64时大约需要8GB显存。如果显存不够有几个优化方向减小batch size最直接的方法但可能会影响训练稳定性。可以用梯度累积来补偿。混合精度训练用torch.cuda.amp显存占用可以减少约40%。梯度检查点用torch.utils.checkpoint用计算时间换显存空间。减小模型通道数如果数据集分辨率低不需要太大的模型。训练速度方面除了混合精度还可以用channels_last内存格式和cudnn.benchmarkTrue来加速卷积运算。实测下来这些优化能让训练速度提升30%左右。5.4 采样速度太慢的优化方案标准DDPM采样1000步每步都要调用一次网络速度确实慢。除了前面提到的DDIM还有几个加速方案DPM-Solver用高阶求解器20步左右就能达到不错的效果。实现比DDIM复杂但加速效果更明显。知识蒸馏训练一个学生模型来模仿教师模型的采样轨迹可以用更少的步数。但训练成本高。潜空间扩散这就是Stable Diffusion的思路先把图像编码到低维潜空间在潜空间做扩散最后解码回像素空间。潜空间的维度远低于像素空间采样速度自然快。我实际对比过DDPM 1000步在32x32图像上大约需要20秒单张DDIM 100步只需要2秒DPM-Solver 20步不到1秒。如果做实验DDIM的性价比最高。5.5 与Stable Diffusion的关联与差异实现完DDPM之后再看Stable Diffusion很多设计就变得清晰了。Stable Diffusion本质上是在潜空间上做DDPM更准确地说是DDPM的变体核心差异在于扩散空间DDPM在像素空间扩散Stable Diffusion在VAE编码的潜空间扩散。潜空间维度通常是64x64x4对于512x512图像比像素空间小得多。条件机制Stable Diffusion通过交叉注意力注入文本条件DDPM是无条件的。噪声调度Stable Diffusion用了scaled linear调度针对潜空间做了调整。采样器Stable Diffusion支持多种采样器DDIM、Euler、DPM-Solver等用户可以根据需要选择。理解了DDPM的训练和采样逻辑再看Stable Diffusion的代码就不会觉得陌生了。它只是在DDPM的基础上加了条件控制和潜空间映射核心的去噪训练目标是一样的。6. 一些实操心得与后续扩展方向6.1 训练扩散模型的几个经验法则经过这次实现我总结了几个训练扩散模型的经验法则不一定适用于所有情况但可以作为参考从简单数据集开始不要一上来就用高分辨率、复杂的数据集。32x32的CIFAR级别数据是很好的起点训练快、容易调试。先跑通再优化不要一开始就追求SOTA效果。先把训练和采样流程跑通确认能生成有意义的图像再逐步调优。监控采样结果比监控损失更重要损失值只能告诉你模型是否在拟合噪声预测任务但生成质量需要看采样结果。建议每几个epoch就采样一次看看。超参数不要一次调太多扩散模型的超参数之间有关联一次调一个记录变化才能搞清楚每个参数的作用。6.2 从DDPM到Stable Diffusion的扩展路径如果你已经实现了DDPM想进一步扩展到Stable Diffusion级别的系统大致路径是加入条件机制把类别标签或文本嵌入通过交叉注意力注入U-Net。引入潜空间训练一个VAE把图像编码到低维潜空间在潜空间做扩散。扩大模型和数据Stable Diffusion的U-Net参数量在800M以上训练数据是数十亿级别的图像-文本对。优化采样器实现DDIM、DPM-Solver等快速采样器支持少步生成。工程优化混合精度、梯度检查点、分布式训练等支撑大规模训练。每一步都有大量的细节需要处理但核心的DDPM逻辑是不变的。把DDPM吃透后面的扩展就是工程和规模的问题了。6.3 我踩过的最大的三个坑最后分享三个我踩过的最大的坑希望能帮你避开第一个坑alpha_bars的计算。前面提过用cumsum代替cumprod导致训练完全失效。这个错误的隐蔽性在于代码不报错损失也在下降因为模型在拟合一个错误的分布但采样出来的全是噪声。排查方法很简单打印alpha_bars的前几个值和最后一个值确认它们符合预期。第二个坑时间步嵌入的维度。一开始设了32维训练损失正常但采样结果在中间时间步出现明显伪影。后来把维度提高到128问题解决。经验是时间步嵌入的维度不要省它直接影响模型对不同噪声水平的区分能力。第三个坑采样时的模型模式。忘记调用model.eval()Dropout在采样时仍然生效导致生成的图像带有随机噪声。这个错误很低级但在调试时很容易忽略因为训练时的损失看起来完全正常。6.4 后续可以尝试的改进方向如果你已经跑通了基础DDPM以下几个方向可以作为下一步的尝试改进噪声调度试试余弦调度或sigmoid调度对比生成质量。加入方差预测让网络同时预测噪声和方差用混合损失训练可能提升生成质量。条件生成加入类别标签或文本条件实现可控生成。加速采样实现DPM-Solver或知识蒸馏把采样步数降到10步以内。潜空间扩散训练VAE在潜空间做扩散为扩展到Stable Diffusion级别做准备。我个人在实际操作中的体会是扩散模型的实现难度不在于代码量而在于对细节的把握。每一个变量、每一个系数、每一个时间步的处理都会影响最终结果。把DDPM从头实现一遍比读十篇论文都管用。希望这篇内容能帮你少走一些弯路顺利跑通自己的扩散模型。