尧图精选

扩散模型DDPM深度拆解:从高斯噪声到图像生成的数学原理与工程实践

🕒 发布时间:2026/9/16 7:53:41 📁 来源:尧图网络
很多刚接触Diffusion Model的人看到的第一张图通常是这样一排图片从清晰逐渐变模糊最后变成一堆纯噪点另一排从噪点开始一步步恢复成一张高清图片。这张图太直观了以至于绝大多数人忽略了背后一堆真正关键的问题为什么每一步加的必须是高斯噪声为什么网络预测的是噪声本身而不是直接预测图像为什么一行简单的MSE Loss最后能训练出比GAN还稳定的生成效果我当初第一次跑通DDPM的代码时对着loss函数愣了半天整个训练循环不超过40行既没有对抗损失也没有复杂的正则项就是一个再普通不过的MSE。但它就是能生成出高质量图片而且训练过程稳定得让人不太敢相信。这篇博文我不打算做科普式复读而是从数学推导到代码实现把DDPM这条技术线的每个关键选择讲透最后再分享一些实际训练和采样中踩过的坑。适合两类人看一类是准备在Stable Diffusion系项目里做定制开发或微调的人另一类是刚读完论文但代码写不出来、想彻底搞清楚原理的读者。1. 扩散模型到底在解决什么问题先理解生成任务为什么难1.1 为什么一步直接生成的路都走得不太顺图像生成这个任务用大白话说就是从一堆训练图像里学到一个分布然后从这个分布里采样出新图像。难点在于图像分布处在一个非常高的维度空间里——一张256×256的RGB图片是196608维而真实图片在这个高维空间里只占一个极小极小的低维流形。你让网络直接学习这个流形分布本质上是在让它在几乎全空的巨大空间里找到一条小路这就导致大多数直接拟合的方法都会遇到同一个问题要么模型崩溃要么生成的图千篇一律要么始终模模糊糊。早期的GAN用对抗博弈的思路绕开了显式密度估计生成器和判别器互相博弈生成器学到的分布越来越接近真实分布这条路在当时效果很好但训练非常脆弱动不动就出现模式坍缩——生成器发现只要反复生成几张能骗过判别器的图片就能把loss压低于是多样性全部丢光。VAE换了个思路用隐变量z定义一个概率生成过程训练时最大化变分下界生成质量偏模糊原因是ELBO里重构项和先验项之间存在难以调和的冲突网络倾向于把隐变量空间里的点映射到平均的、平滑的图像上细节就丢了。所以无论是GAN还是VAE本质上都试图用一步到位的方式解决一个本质上需要多步精修的任务。这和让一个刚学画画的人直接默写一幅油画一样实在太难了。1.2 从做旧到修复DDPM的核心思想扩散模型最反直觉的一点是训练网络之前你先把所有训练图片做旧到面目全非。这不是比喻而是字面意义上的操作——对一张干净图片反复叠加高斯噪声加了几百步之后图片就变成了一个接近标准正态分布的纯噪声图。然后再训练一个网络学会逆转这个过程给它一张被噪声污染过的图让它一步步去掉噪声每一步恢复一点点细节。整个过程和文物修复师的工作逻辑是一模一样的你先把一件新文物人为做旧到看不出本来面目然后再训练一个人让他学会从做旧的各种中间状态里一步步还原。听起来像个玩笑但核心洞察就在这里——前向加噪是一个有明确数学定义的过程每一步加多少噪声、图像会变成什么分布全部可以精确推导逆向去噪的每一步任务则被拆得非常简单网络只需要去掉一点点噪声就够了。一个巨大的复杂任务被拆成了1000个微小且简单的子任务这就是它的众多强大之处。这种拆细任务的思想其实也是课程学习的思路。网络不需要一步学会所有东西它只需要在每一步学会一个很简单的去噪规则但关键是这样组合起来的整体能力非常强。1.3 三个生成范式放在同一张表里看维度GANVAEDiffusion Model建模思路判别器与生成器对抗博弈隐变量变分推断逐步加噪再逐步去噪训练目标对抗损失变分下界ELBO噪声预测的MSE是否显式建模密度否是隐式是通过score生成质量好但不稳定模糊、细节弱高、清晰多样性容易坍缩一般好且可控训练稳定性差稳定很稳定采样速度快快慢需多步这张表基本概括了Diffusion Model能在近年超过GAN成为主流的原因训练过程极其稳定多样性高而且通过后续的引导技术classifier-free guidance等还能精细控制生成内容。代价就是采样速度慢原来GAN一次前向就能出图扩散模型要循环几十上百次。这也正是后文要讲的加速技术存在的原因。2. 前向加噪把一张图片彻底毁掉的数学过程2.1 单步加噪的严格定义每一步都在做一个高斯转移前向过程的核心是定义如何把一张干净图片变成噪声图。DDPM中加噪是一个马尔可夫链给定当前步图像下一步的概率分布是一个高斯分布均值为上一张图的某个缩放版本方差则由一个逐步变化的超参数β_t控制。q(x_t | x_{t-1}) N(x_t; √(1-β_t) · x_{t-1}, β_t · I)这里t从1到TT在DDPM原论文里取1000β_t是一个预设好的、逐步增大的值论文里默认从0.0001线性增加到0.002。每次加噪都是采样一个高斯噪声叠加在图像上但会乘一个√(1-β_t)的缩放系数这个系数不是为了好看而是为了控制整个过程的方差稳定避免图像信号随着加噪步数增加而无限膨胀或萎缩。从图像角度理解每一步都让原图的保留程度下降一点点让噪声的占比上升一点点。随着t增大β_t越来越大等于说每一步破坏得更狠一些最终t1000的时候图像已经彻底被噪声淹没几乎完全看不出原图任何信息。2.2 为什么必须是高斯噪声两个不可替代的性质很多初学者会问加噪为什么不用均匀噪声或者伯努利噪声答案是高斯分布有两个对后面的推导来说决定性的性质。第一个是可闭式累积性。高斯分布叠加高斯分布结果还是高斯分布而且均值和方差可以解析写下。这意味着我们不需要真的把加噪过程从第1步一步步模拟到第t步可以直接一步算出任意中间状态x_t的分布。如果没有这个性质训练时每一步都要串行跑1000次加噪代码效率就直接崩了。第二个是可重参数化性。高斯分布的采样可以写成均值 标准差 × 标准正态噪声的形式这样随机性被隔离在一个独立的标准正态变量ε里梯度可以通过这条路径回传。VAE之所以能训练也是靠这个技巧。扩散模型把这两条都用到了极致。2.3 累积加噪与重参数化从x₀直接跳到x_t推导一下前面的单步公式把每一步的高斯分布反复叠乘起来可以得到一个重要结论任意时刻t的图像x_t可以直接从原始图像x_0和一维标准正态噪声ε表示出来。x_t √(ᾱ_t) · x_0 √(1 - ᾱ_t) · ε, 其中 α_t 1 - β_t, ᾱ_t ∏_{s1}^{t} α_s这个公式就是整个扩散模型训练效率的基础。训练时不需要真的跑1000步加噪只需要随机采一个t用这个公式一步就能得到x_t。其中√(ᾱ_t)表示信号保留的比例√(1-ᾱ_t)表示噪声比例。当t0时ᾱ_t1x_t就是原图当t很大时ᾱ_t趋近于0x_t接近纯高斯噪声。代码实现里就是一行x_t torch.sqrt(alpha_bar[t]) * x_0 torch.sqrt(1 - alpha_bar[t]) * eps。用这行代码你可以在任何噪声水平下拿到x_t送入网络训练。这是所有Diffusion代码里出现频率最高的一行。2.4 噪声调度beta_schedulelinear和cosine怎么选β_t从0.0001到0.002线性增长这是DDPM原论文的默认配置被称为linear schedule。但真正上手跑实验时你会发现linear schedule有个隐藏问题在t还很小的时候ᾱ_t就已经快速下降图像的原始信号在前几百步里就被破坏得差不多了后面的步几乎全是在已经接近纯噪声的图像上做无用功。换句话说加噪速度前快后慢早期信息丢失太快。后续论文提出了cosine schedule让信号衰减曲线近似呈余弦形状先缓慢后加速再缓慢整个加噪过程对信息的破坏更均匀。实测下来cosine schedule在同样步数下训练更稳定生成质量也能小幅提升尤其是高分辨率图像。Stable Diffusion系列普遍用的是加了偏移的cosine或scaled linear变体原因就是为了让加噪过程更温和。我的经验是复现基础实验时先用linear schedule跑通换cosine后注意观察不同t时刻的中间状态图你就能直观感受到两者的区别。3. 逆向去噪网络到底在学什么3.1 逆向过程先假设一个高斯分布前向过程已经确定了给定x_{t-1}x_t的分布是高斯。那么反过来给定x_tx_{t-1}的分布长什么样在β_t足够小的情况下逆向转移分布也可以用高斯来近似。这是一个数学上的结论直觉上可以理解为当每一步的破坏非常轻微时从被微扰的状态恢复到之前的状态的路径不会有太多复杂性用高斯分布描述已经足够。于是我们把这个逆向分布参数化为p_θ(x_{t-1} | x_t) N(x_{t-1}; μ_θ(x_t, t), Σ_θ(x_t, t))其中网络需要预测的是均值μ_θ方差Σ_θ在很多实现里被固定为预设值而不去学习。这个高斯假设是整个逆向过程能够被简化到极致的前提。3.2 网络预测的是噪声残差不是直接预测图像训练网络时最核心的问题是让网络的输出对应什么目标理论上可以直接让网络预测x_0但实际操作效果不好因为从被严重污染的x_t直接预测原图是个高度不适定问题同一个x_t可能对应大量不同的x_0网络学起来非常困难输出容易变模糊。DDPM的做法是让网络预测加进去的噪声ε。注意到前向过程x_t √(ᾱ_t) · x_0 √(1-ᾱ_t) · ε这其实是一条线性混合关系。网络如果精确预测出ε那么就可以反解出x_0x_0 (1/√(ᾱ_t)) · (x_t - √(1-ᾱ_t) · ε)。更重要的是有了对噪声的估计逆向过程中x_{t-1}的均值也可以被解析表述出来。所以预测噪声等价于掌握了完整的去噪路径。从另一个视角看预测噪声等价于学习数据分布的score——也就是对数概率密度的梯度。这个视角来自score-based generative model研究线扩散模型和score matching在数学上被证明是同一类东西。直观理解就是在浓雾弥漫的山坡上你不需要知道山脚在哪你只需要每一步感受脚下的坡度沿着最陡的方向往下走一步一步就能下山。预测噪声就是让网络学会感知当前在什么坡度上、该往哪个方向调整。3.3 为什么U-Net是标配扩散模型的输入和输出都是图像尺寸完全一致这是一个典型的图像到图像任务。U-Net天然适合这个场景Encoder通过多级下采样逐步提取抽象特征Decoder通过上采样把特征放大回原图尺寸中间的skip connection把Encoder每一层的细节特征直接拼到Decoder对应层上确保浅层的颜色、纹理、边缘信息不丢失。在文生图模型里U-Net的中间层还会加入cross-attention模块把文本编码器的输出作为条件注入到图像特征中从而实现注意力机制决定图片里哪里应该长什么。在Stable Diffusion这类潜在扩散模型中U-Net作用在压缩后的潜在空间上分辨率更小、计算量更低但结构本质没有变化。3.4 时间步嵌入让网络知道现在毁到了什么程度同样一张加了噪的图如果网络不知道当前处于整个加噪过程的第几步它就无法判断我应该均匀去噪还是锐化边缘。所以t作为一个条件一定要喂进网络。DDPM的做法是把t编码成一个高维向量做法和Transformer里的位置编码类似用正弦余弦函数生成不同频率的嵌入然后通过残差块调制到每一层的特征上。这个时间嵌入看起来是个小细节但删掉它训练几乎必然失败或者效果大幅退化。它给网络提供了时间感让同一个网络可以为不同噪声水平服务。实现中常用的方式是FiLM调制时间嵌入经过线性层变换成scale和shift参数对中间特征做缩放和偏移让网络按当前时间选择最合适的去噪策略。4. 训练目标一个简单到令人怀疑的MSE Loss4.1 从变分下界到简化损失扩散模型本质上是潜在变量模型最理想的目标是最大化对数似然 log p_θ(x_0)但这个量无法直接计算所以转而最大化它的变分下界ELBO。把前向加噪过程和逆向去噪过程代入ELBO之后可以逐项展开展开后会发现ELBO可以分解为一系列KL散度之和每一项都是在约束某个逆向高斯分布去匹配某个前向条件分布。由于两个分布都是高斯KL散度存在解析形式经过一大堆配项整理之后DDPM作者发现这些项里绝大部分权重影响很小甚至可以去掉最后整个训练目标退化成极其简洁的形式。L_simple E_{t, x_0, ε} [ || ε - ε_θ(x_t, t) ||² ]也就是说我们随机采样一个时间步t随机采样一个高斯噪声ε按2.3节公式合成x_t让网络预测ε算这个预测和真实ε之间的MSE就完成了一次训练。整个过程没有任何对抗博弈没有任何复杂的正则项就是一个标准的回归任务。4.2 为什么这个简洁的loss能生成高质量图片很多人会困惑回归loss不是容易让输出变模糊吗为什么扩散模型最后能得到锐利的图像关键在于扩散模型训练时的回归不是一次性预测图像而是在每个时间步上只预测噪声残差这一小增量。每个小任务本身是确定且有明确解的网络学到的是一系列该往哪个方向微调的梯度场也就是数据分布的score。采样时从纯噪声出发每走一步都沿着这个梯度场往高密度区域挪动一小步本质上是在做随机微分方程的数值求解。由于是逐步迭代即使每一步的预测带有一定误差几百步下来依然能收敛到清晰的高概率区域不会像单步回归那样靠平均来和稀泥。这也是为什么扩散模型在相同网络容量下往往比GAN更能覆盖数据分布的多个模式因为它不做单点映射而是在高维空间里沿着流形漫游多样性天然就高。4.3 训练循环在PyTorch里的真实样子贴上我在实验中实际使用的基础训练循环代码很简短for batch, (x0, y) in enumerate(train_loader): # x0: [B, C, H, W], 像素值归一化到 [-1, 1] optimizer.zero_grad() t torch.randint(0, T, (x0.size(0),), devicedevice).long() eps torch.randn_like(x0) # 前向加噪一步到位 x_t torch.sqrt(alpha_bar[t]) * x0 torch.sqrt(1 - alpha_bar[t]) * eps # 输入噪声图和时间步预测噪声 eps_pred model(x_t, t) loss nn.functional.mse_loss(eps_pred, eps) loss.backward() optimizer.step()有几个细节需要在代码里注意。alpha_bar数组必须先按T1000预计算好且全部移到训练所用的设备上。t要作为tensor传入模型模型内部会把它转成时间嵌入。loss就是mse_loss不需要加任何权重虽然理论上不同t对应的项权重不同但DDPM实验证明去掉权重训练更稳定生成质量也更好。5. 采样过程与加速技巧从1000步缩到20步5.1 标准DDPM采样一步一步往回走训练好模型后生成图像的过程就是反向模拟逆向马尔可夫链从纯高斯噪声x_T出发对t从T到1依次采样x_{t-1}。每一次的重建公式如下。x_{t-1} (1/√(α_t)) · (x_t - (1-α_t)/√(1-ᾱ_t) · ε_θ(x_t, t)) σ_t · z式子第一项是在按照预测的噪声方向去噪让图像从高噪声状态回到低噪声状态第二项是加上一个随机高斯扰动zσ_t通常取√β_t。最后一步t1时不加噪声避免给最终输出引入无谓的随机性。这个随机扰动很关键它保证了采样过程是带有探索性的如果每一步都只走确定性的去噪方向模型容易陷入低质量区域。标准DDPM采样需要跑满全部1000步在GPU上生成单张图虽然不至于太慢但放到实际应用里根本无法接受。这也催生了各种采样加速方法。5.2 DDIM加速把随机项去掉变成确定性采样DDIM的核心洞察是DDPM的采样公式里去噪项 随机噪声项的每一步其实是在模拟一个随机微分方程但这个随机微分方程存在一条等价的概率流常微分方程意思是你可以用确定性的方式对同一条分布路径做离散化。去掉随机噪声项后采样变成了一条确定的轨迹x_{t-1} √(ᾱ_{t-1}) · (x_t - √(1-ᾱ_t) · ε_θ(x_t, t)) / √(ᾱ_t) √(1-ᾱ_{t-1} - σ_t²) · ε_θ(x_t, t)实际用起来DDIM最大的好处就是可以大幅跳步原本必须t从1000到1逐次递推现在可以取一个步长比如每20步采样一次总共50步就能生成质量相当不错的图像。速度直接提升一个数量级。我的使用经验是原版DDIM跑50步输出的图像和DDPM 1000步非常接近但内存占用和耗时都下降很多。目前主流的采样器如DPM-Solver、DPM、Euler等基本都是在DDIM这个思路上进一步优化离散化格式追求用更少的函数求值次数达到同样的采样质量。5.3 Classifier-Free Guidance可控生成的开关扩散模型本身是条件生成模型训练时你可以把类别或文本条件y一起输入让网络学条件分布p(x_t | y)。但实际采样如果完全依赖条件生成结果容易丢失多样性完全不依赖条件又无法控制生成内容。Classifier-Free Guidance简称CFG的思路很巧妙在训练时随机让部分样本的条件被丢弃用空标签替代让同一个模型同时具备有条件预测和无条件预测的能力。采样时把两者的预测差值作为引导信号放大。ε_hat (1 w) · ε_θ(x_t, t, y) - w · ε_θ(x_t, t, ∅)当w0时就是普通条件采样w越大生成结果越遵循条件提示但多样性也随之下降。Stable Diffusion里CFG尺度默认7.5是在忠实于提示词和图像自然度之间比较均衡的经验值。文本生成图像时这个技术几乎绕不开它本质上是提供了一根调节条件约束强度的旋钮。5.4 采样器与步数怎么配合实测经验接触过WebUI或ComfyUI的人一定见过一大串采样器名字但很少有人理解它们之间的差异。按类别分其实很简单采样器类别典型代表步数建议特点多步噪声型DDPM1000训练验证用实际部署太少确定性加速型DDIM20-50平衡好通用性强高阶ODE求解型DPM-Solver、DPM 2M15-30步数越少优势越明显一阶快速型Euler、Heun20-40简单稳定适合初学调试我的实际建议是复现论文时老老实实用DDPM的1000步流程验证模型的正确性一旦验证通过就切到DDIM 50步做迭代这样的开发效率最高。如果做视频或实时交互场景再考虑DPM-Solver类采样器压到15步左右配合蒸馏技术甚至能到4步出图。6. 实际训练和采样时踩过的坑几个可复现的排查经验6.1 训练不收敛先从这些点自查扩散模型虽然训练稳定但不代表什么都不管就能跑出好结果。如果训练了很久loss不降或者生成的都是灰蒙蒙的图按下面的顺序排查大概率能定位问题第一图像归一化是否正确。扩散模型约定图像数值范围在[-1, 1]很多代码库会把0-255的图除以127.5再减1如果你用了[0,1]区间网络输出和目标的数值尺度就不对齐loss会偏高生成效果也会偏色。第二β_schedule是否从很小的值开始。如果初始β_t太大前几步加噪就把信息全毁了网络在大量死图上训练学不到有效特征。第三模型是否接收了正确的时间嵌入。检查t是否真正通过嵌入层送到了每个残差块里很多人写结构时漏了这步模型退化成无时间条件的盲去噪器训练曲线看着下降但生成效果很差。第四学习率是否过高。DDPM常见的学习率在2e-4到2e-5之间用了Adam优化器过高的学习率会导致loss剧烈震荡甚至NaN。6.2 数值稳定性细微的浮点问题能造成大影响训练和采样过程中数值稳定性几乎是最容易被忽略又最常出问题的环节。比如计算x_t时如果ᾱ_t在t接近T时已经非常非常接近0√(1-ᾱ_t)接近1而√(ᾱ_t)非常小此时x_t几乎完全由噪声主导加噪公式本身没问题但在反向采样DDIM时如果代码里出现1 - alpha_bar[t]当alpha_bar[t]因浮点误差略大于1时1 - alpha_bar[t]会变成负数再开根号就是NaN。所以采样代码里必须对alpha_bar做clamp(0, 1)这是个非常容易踩的坑。另一个常见问题是使用半精度训练时的溢出。loss本身是MSE数值不大但中间计算噪声和方差时可能产生极端值用混合精度时要格外留意最好先在FP32下跑通小规模实验再切AMP。6.3 EMA一个免费提升生成质量的操作指数移动平均可以说是扩散模型训练里的免费午餐。训练时对模型参数做滑动平均保留一份慢参数采样时使用这份EMA参数而不是实时参数。因为扩散模型训练过程中的loss曲线会上下抖动实时参数可能处于震荡的峰值附近而EMA参数相当于对历史多个最优状态做了平滑往往能带来更稳定的生成结果和更低的FID。具体实现一般会维护一个复制模型的影子参数shadow_param decay * shadow_param (1 - decay) * paramdecay取0.9999左右训练完成后加载shadow参数用于采样。这个操作用几行代码就能实现收益却非常明显我在CIFAR-10和FFHQ上都验证过EMA后的FID通常能比无EMA低10-20%。6.4 起步实验怎么设计别一上来就追求高分辨率最后分享一个非常实际的经验想快速验证某个扩散模型改进点是否有效千万别直接上256×256或512×512。先在MNIST 28×28或CIFAR-10 32×32上训练几百轮epoch要不了多久哪怕在单张消费级显卡上也能顺利跑完。训练过程中定期采样并且把不同t的x_t和重建效果可视化出来你就能直观看到整个去噪过程是否合理问题出在哪个噪声水平阶段。等你确认31×31小尺寸上的效果、训练稳定性和采样视觉效果都符合预期之后再逐步提升分辨率、增加通道数。这样既能快速试错又能避免在高分辨率上烧了大量算力才发现模型架构本身有bug。FID等其他指标可以作为量化评估的最终标准但日常调试的第一判断依据永远是训练曲线和可视化图。最后一个我自己在实际操作中的小习惯在训练过程中把每个固定间隔步数采样的图像按时间拼接成动画观察从纯噪声到清晰图像的整个演变过程。如果前几十步图像变化幅度很小说明高噪声区域的去噪能力不足如果最后几步还在大幅变化说明低噪声区域拟合还不够细。这个观察方式比任何指标都直观能帮你迅速定位扩散模型训练中的问题出在哪一段噪声水平上。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →