ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

扩散模型从DDPM到Stable Diffusion:原理推导与U-Net代码实战

2026/9/19 13:55:39 拓冰建站 浏览量
扩散模型从DDPM到Stable Diffusion:原理推导与U-Net代码实战 扩散模型这两年火得一塌糊涂但真正能把它的来龙去脉讲清楚的材料并不多。大部分教程要么一上来就甩一堆随机微分方程的公式把人劝退要么只给一个调库的Demo跑通了也不知道背后发生了什么。我自己从DDPM原始论文一路啃到U-Net的代码实现中间踩了不少坑也走了不少弯路比如一开始死活想不通为什么前向过程可以直接写成闭式解、为什么训练时只预测噪声而不是直接预测原图、U-Net里的时间步嵌入到底是怎么加进去的。这篇内容就是把这些东西从头到尾捋一遍从直觉到公式再到代码尽量让每个环节都能对上号。不管你是刚接触生成模型的新手还是已经用过Stable Diffusion但想搞清楚底层机制的老手应该都能从中找到有用的东西。1. 扩散模型到底在解决什么问题1.1 从“生成”这件事的本质说起生成模型的核心任务就一句话给定一批训练数据让模型学会这些数据的分布然后从学到的分布里采样出新的、但看起来像训练数据的东西。听起来简单做起来难。因为图像数据的维度极高——一张256×256的RGB图片有将近20万个像素值这20万个维度构成的联合分布空间大到无法想象。直接建模这个分布就像让你在一片没有地图的沙漠里找路。早几年的生成模型走了几条不同的路。GAN用对抗训练的方式让生成器和判别器互相博弈生成质量高但训练极不稳定模式坍塌是家常便饭。VAE用变分推断做近似训练稳定但生成的图偏模糊。Flow-based模型通过可逆变换精确计算似然但架构设计受限严重。扩散模型走了一条看起来最“笨”但实际最有效的路它不直接学从噪声到图像的映射而是学一个逐步去噪的过程。这个思路的灵感来自热力学。想象一杯清水里滴了一滴墨水墨水会逐渐扩散直到均匀分布——这是熵增的方向不可逆。扩散模型做的事情是先定义怎么把图像“扩散”成噪声前向过程然后训练一个网络学会逆转这个过程反向过程从纯噪声一步步恢复出图像。1.2 和GAN、VAE的核心差异在哪GAN的训练是min-max博弈判别器和生成器互相拉扯理论上有纳什均衡但实际很难达到。扩散模型的训练就是一个简单的回归任务——给定加噪后的图像和对应的时间步预测加进去的噪声。没有对抗没有博弈损失函数就是MSE。这让训练过程非常稳定几乎不会出现GAN那种崩掉的情况。VAE的编码器把图像压缩到一个低维隐空间解码器再从隐空间重建。问题在于编码过程是有损的重建出来的图会丢失高频细节。扩散模型没有编码器它的“隐空间”和原图同维度信息不会在编码阶段丢失。代价是采样速度慢——DDPM原始实现需要1000步才能生成一张图而GAN只需要一次前向传播。但扩散模型有一个GAN和VAE都不具备的优势它天然适合做条件生成。你想让模型根据文本描述生成图像只需要在去噪网络的输入里加上文本编码就行。你想做图像修复、超分辨率、风格迁移只需要修改前向过程或条件输入就行。这种灵活性是扩散模型能成为当前生成式AI主流方案的关键原因。1.3 为什么是现在火了扩散模型的理论基础其实在2015年就有了Sohl-Dickstein等人的工作但当时没引起太大关注。直到2020年DDPMDenoising Diffusion Probabilistic Models的论文出来大家才发现这东西生成的图像质量居然能超过GAN。2021年DDIM把采样步数从1000步降到50步左右2022年Latent Diffusion把扩散过程搬到隐空间计算量大幅下降Stable Diffusion就是基于这个方案。再往后各种加速采样、蒸馏、一致性模型层出不穷采样速度越来越快。所以扩散模型的爆发不是偶然的它是理论积累、算力增长和工程优化三者交汇的结果。理解了这个背景再看后面的公式推导就不会觉得是在看天书了。2. 前向过程把图像一步步变成噪声2.1 每一步加多少噪声前向过程forward process也叫扩散过程它的定义非常简洁给定一张图像 ( x_0 )每一步往上面加一点高斯噪声总共加 ( T ) 步最终得到一个近似标准正态分布的噪声图 ( x_T )。每一步的加噪公式是[ x_t \sqrt{1 - \beta_t} \cdot x_{t-1} \sqrt{\beta_t} \cdot \epsilon_{t-1} ]其中 ( \epsilon_{t-1} \sim \mathcal{N}(0, I) ) 是标准高斯噪声( \beta_t ) 是第 ( t ) 步的噪声方差通常从 ( 10^{-4} ) 到 ( 0.02 ) 线性增长。这个公式的物理含义很直观每一步保留一部分上一步的信号系数 ( \sqrt{1-\beta_t} )同时混入一部分新噪声系数 ( \sqrt{\beta_t} )。两个系数平方和为1保证方差不爆炸。为什么系数要这样设计因为如果直接写成 ( x_t x_{t-1} \epsilon )方差会随着步数线性增长到后面数值会大到无法处理。用 ( \sqrt{1-\beta_t} ) 和 ( \sqrt{\beta_t} ) 这种形式相当于在信号和噪声之间做一个加权平均每一步的方差都保持在可控范围内。2.2 为什么可以直接跳到任意时刻上面那个公式是逐步递推的但实际上我们不需要真的迭代 ( t ) 次来得到 ( x_t )。通过递推展开可以得到一个闭式解[ x_t \sqrt{\bar{\alpha}_t} \cdot x_0 \sqrt{1 - \bar{\alpha}_t} \cdot \epsilon ]其中 ( \alpha_t 1 - \beta_t )( \bar{\alpha}t \prod{s1}^{t} \alpha_s )( \epsilon \sim \mathcal{N}(0, I) )。这个闭式解的推导过程值得手推一遍。从 ( x_1 \sqrt{\alpha_1} x_0 \sqrt{1-\alpha_1} \epsilon_1 ) 开始代入 ( x_2 \sqrt{\alpha_2} x_1 \sqrt{1-\alpha_2} \epsilon_2 )展开后利用两个独立高斯分布之和仍是高斯分布的性质合并噪声项最终得到上面的形式。推导的关键在于两个独立高斯分布 ( \mathcal{N}(0, \sigma_1^2 I) ) 和 ( \mathcal{N}(0, \sigma_2^2 I) ) 相加结果是 ( \mathcal{N}(0, (\sigma_1^2 \sigma_2^2) I) )。这个闭式解的意义非常大训练时我们可以随机采样一个时间步 ( t )直接算出 ( x_t )不需要模拟整个加噪链条。这让训练效率提高了几个数量级。2.3 噪声调度表的设计考量( \beta_t ) 的选择不是随便定的。DDPM原始论文用的是线性调度从 ( \beta_1 10^{-4} ) 到 ( \beta_T 0.02 )( T 1000 )。后来研究发现线性调度在低分辨率图像上表现还行但在高分辨率图像上前向过程会把图像破坏得太快导致反向过程难以恢复细节。改进方案是cosine调度由Nichol和Dhariwal在2021年提出。它的核心思想是让 ( \bar{\alpha}_t ) 按照余弦函数衰减[ \bar{\alpha}_t \frac{f(t)}{f(0)}, \quad f(t) \cos\left(\frac{t/T s}{1 s} \cdot \frac{\pi}{2}\right)^2 ]其中 ( s ) 是一个小的偏移量通常取0.008防止 ( t0 ) 附近 ( \beta_t ) 太小。cosine调度的好处是在中间阶段加噪速度更均匀不会出现前期加噪太慢、后期加噪太快的问题。实际用的时候如果你是自己训练一个小模型做实验线性调度就够了。如果是复现Stable Diffusion级别的模型建议用cosine或更新的调度方案。这个细节在论文里往往一笔带过但对最终生成质量的影响不小。3. 反向过程从噪声里恢复图像3.1 反向过程为什么也服从高斯分布反向过程的目标是学习 ( p_\theta(x_{t-1} | x_t) )也就是给定 ( x_t ) 预测 ( x_{t-1} )。一个关键的理论结果是当 ( \beta_t ) 足够小的时候反向过程也近似服从高斯分布。这个结论来自Feller在1949年关于扩散过程的工作DDPM论文直接引用了这个结果。所以我们可以把反向过程参数化为[ p_\theta(x_{t-1} | x_t) \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \Sigma_\theta(x_t, t)) ]其中 ( \mu_\theta ) 是需要学习的均值( \Sigma_\theta ) 是方差。DDPM的做法是把方差固定为 ( \beta_t ) 或 ( \tilde{\beta}_t )只学习均值。后来OpenAI的Improved DDPM发现学习方差也能提升效果但训练会更复杂一些。3.2 均值怎么参数化给定 ( x_t ) 和 ( x_0 )反向过程的后验分布 ( q(x_{t-1} | x_t, x_0) ) 是可以精确写出来的[ q(x_{t-1} | x_t, x_0) \mathcal{N}(x_{t-1}; \tilde{\mu}_t(x_t, x_0), \tilde{\beta}_t I) ]其中[ \tilde{\mu}t \frac{\sqrt{\bar{\alpha}{t-1}} \beta_t}{1 - \bar{\alpha}t} x_0 \frac{\sqrt{\alpha_t}(1 - \bar{\alpha}{t-1})}{1 - \bar{\alpha}_t} x_t ][ \tilde{\beta}t \frac{1 - \bar{\alpha}{t-1}}{1 - \bar{\alpha}_t} \beta_t ]这个后验分布的推导用到了贝叶斯定理和高斯分布的性质。它的意义在于如果我们知道 ( x_0 )就能精确算出 ( x_{t-1} ) 的分布。但问题是在生成阶段我们不知道 ( x_0 )所以需要用神经网络来估计。3.3 为什么预测噪声而不是预测原图DDPM的核心洞察是与其让网络直接预测 ( x_0 ) 或 ( \mu_t )不如让它预测被加进去的噪声 ( \epsilon )。因为 ( x_t \sqrt{\bar{\alpha}_t} x_0 \sqrt{1-\bar{\alpha}_t} \epsilon )所以[ x_0 \frac{x_t - \sqrt{1-\bar{\alpha}_t} \epsilon}{\sqrt{\bar{\alpha}_t}} ]把这个代入后验均值公式经过化简可以得到[ \mu_\theta(x_t, t) \frac{1}{\sqrt{\alpha_t}} \left( x_t - \frac{\beta_t}{\sqrt{1-\bar{\alpha}t}} \epsilon\theta(x_t, t) \right) ]所以网络只需要预测 ( \epsilon_\theta(x_t, t) )就能算出均值进而采样出 ( x_{t-1} )。为什么预测噪声比预测原图更好有几个原因。第一从量级上看( \epsilon ) 是标准高斯噪声数值范围相对稳定而 ( x_0 ) 的像素值范围是[0, 255]或[-1, 1]不同数据集差异大。第二预测噪声相当于让网络学习“当前图像里哪些部分是噪声”这个任务在不同时间步之间更一致。第三从信息论角度看( x_t ) 中的噪声成分和信号成分是正交的预测噪声不会干扰信号的重建。3.4 训练损失函数的推导DDPM的损失函数最终形式非常简洁[ L_{\text{simple}} \mathbb{E}{t, x_0, \epsilon} \left[ | \epsilon - \epsilon\theta(\sqrt{\bar{\alpha}_t} x_0 \sqrt{1-\bar{\alpha}_t} \epsilon, t) |^2 \right] ]这个损失函数的完整推导涉及变分下界ELBO的展开。原始论文里用了好几页来推导但最终化简后就是一个加权的MSE损失。加权的权重来自 ( \beta_t^2 / (2\sigma_t^2 \alpha_t (1-\bar{\alpha}_t)) )DDPM发现把这个权重去掉也就是所有时间步等权效果反而更好所以最终用的是简化版损失。这个简化损失有一个直观解释它就是在教网络“给定一张加噪图把加进去的噪声找出来”。任务简单直接训练稳定这也是扩散模型比GAN好训的根本原因。4. U-Net架构去噪网络的具体设计4.1 为什么选U-Net而不是Transformer去噪网络的输入是一张噪声图 ( x_t ) 和一个时间步 ( t )输出是预测的噪声 ( \epsilon_\theta )。输入输出都是同维度的图像这天然适合编码器-解码器结构。U-Net最初是为医学图像分割设计的它的核心特点是编码器逐步下采样提取语义特征解码器逐步上采样恢复空间分辨率同时通过跳跃连接把编码器的浅层特征直接传给解码器。为什么不用Transformer在DDPM发表的2020年Vision Transformer还没证明自己在密集预测任务上的优势。而且U-Net的归纳偏置局部性、平移等变性对图像去噪任务非常合适。虽然后来DiTDiffusion Transformer证明了Transformer也能做去噪网络但U-Net在计算效率和参数量上仍然有优势Stable Diffusion用的就是U-Net。4.2 时间步嵌入是怎么加进去的时间步 ( t ) 是一个标量但网络需要根据不同的 ( t ) 调整行为——在 ( t ) 大的时候做粗去噪在 ( t ) 小的时候做精细调整。怎么把这个标量信息注入到卷积网络里DDPM用的是正弦位置编码和Transformer里的位置编码类似[ \text{PE}(t, 2i) \sin(t / 10000^{2i/d}) ][ \text{PE}(t, 2i1) \cos(t / 10000^{2i/d}) ]其中 ( d ) 是嵌入维度。这个编码把标量 ( t ) 映射成一个 ( d ) 维向量不同频率的正弦波让网络能区分不同的时间步。然后通过两层MLP把这个编码投影到和特征图通道数相同的维度再通过广播加到每个残差块的特征图上。实际实现的时候有一个细节时间步嵌入通常是在每个残差块里加的而不是只在网络入口加一次。这样每个层都能感知到当前的时间步去噪行为可以逐层调整。4.3 残差块和注意力块的组合DDPM的U-Net每个分辨率层级包含若干个残差块和一个注意力块。残差块的结构是GroupNorm → SiLU激活 → 卷积 → 时间步嵌入相加 → GroupNorm → SiLU → 卷积 → 残差连接。注意力块用的是自注意力机制在较低分辨率如16×16上计算。为什么不在高分辨率上用注意力因为自注意力的计算复杂度是 ( O(N^2) )( N ) 是像素数。在64×64的特征图上( N 4096 )注意力矩阵是4096×4096计算量和显存都吃不消。所以DDPM只在16×16和8×8的分辨率上加了注意力。Stable Diffusion的U-Net在此基础上做了改进加入了Cross-Attention层让文本编码可以作为条件注入。Cross-Attention的Query来自图像特征Key和Value来自文本编码这样图像生成就能受到文本的引导。4.4 代码实现的关键细节下面是一个简化版的U-Net残差块实现用PyTorch写import torch import torch.nn as nn import math 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 class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels, time_emb_dim): super().__init__() self.time_mlp nn.Sequential( nn.SiLU(), nn.Linear(time_emb_dim, out_channels) ) self.conv1 nn.Conv2d(in_channels, out_channels, 3, padding1) self.conv2 nn.Conv2d(out_channels, out_channels, 3, padding1) self.norm1 nn.GroupNorm(8, in_channels) self.norm2 nn.GroupNorm(8, out_channels) self.act nn.SiLU() 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 self.act(h) h self.conv1(h) t_emb self.time_mlp(t_emb) h h t_emb[:, :, None, None] h self.norm2(h) h self.act(h) h self.conv2(h) return h self.residual_conv(x)这段代码里有几个容易踩坑的地方。第一时间步嵌入的维度要和特征图通道数匹配所以需要过一个线性层做投影。第二时间步嵌入相加的时候要扩展维度从[B, C]变成[B, C, 1, 1]才能和特征图广播。第三GroupNorm的组数要能整除通道数通常取8或32。第四残差连接在输入输出通道数不一致的时候需要用1×1卷积做投影。4.5 下采样和上采样的实现选择下采样通常用步长为2的卷积或者最大池化。DDPM用的是步长为2的卷积因为卷积是可学习的比池化更灵活。上采样用最近邻插值加卷积或者转置卷积。转置卷积容易产生棋盘格伪影所以现在更多用插值加卷积的方式。在Stable Diffusion的U-Net里下采样和上采样还涉及到通道数的变化。通常每下采样一次通道数翻倍每上采样一次通道数减半。这样设计是为了在低分辨率时用更多通道捕捉语义信息在高分辨率时用更少通道处理细节。5. 从DDPM到DDIM采样加速的关键改进5.1 DDPM采样为什么慢DDPM的采样过程是马尔可夫的必须从 ( tT ) 一步步走到 ( t0 )总共1000步。每一步都要跑一次完整的U-Net前向传播计算量巨大。生成一张256×256的图在V100上大概需要20秒。这个速度在实际应用中是没法接受的。慢的根本原因在于DDPM的反向过程被约束为马尔可夫链每一步只依赖前一步。虽然理论上可以跳步但跳步会引入误差生成质量会下降。5.2 DDIM的非马尔可夫视角DDIMDenoising Diffusion Implicit Models的核心洞察是扩散模型的训练目标只依赖于边缘分布 ( q(x_t | x_0) )而不依赖于联合分布 ( q(x_{1:T} | x_0) )。也就是说我们可以构造一个不同的反向过程只要它满足相同的边缘分布就能用同一个训练好的网络。DDIM定义了一个非马尔可夫的反向过程[ x_{t-1} \sqrt{\bar{\alpha}{t-1}} \underbrace{\left( \frac{x_t - \sqrt{1-\bar{\alpha}t} \epsilon\theta(x_t, t)}{\sqrt{\bar{\alpha}t}} \right)}{\text{预测的 } x_0} \sqrt{1 - \bar{\alpha}{t-1} - \sigma_t^2} \cdot \epsilon_\theta(x_t, t) \sigma_t \epsilon ]其中 ( \sigma_t ) 是一个控制随机性的参数。当 ( \sigma_t 0 ) 时采样过程完全确定给定 ( x_T ) 就能得到唯一的 ( x_0 )。当 ( \sigma_t \sqrt{\tilde{\beta}_t} ) 时就退化成DDPM。5.3 跳步采样的实现和效果DDIM允许我们只采样一个子序列 ( \tau_1 \tau_2 \cdots \tau_S )其中 ( S \ll T )。比如从1000步里取50步采样速度提升20倍生成质量下降很小。实际用的时候DDIM的采样步数通常取20到100步。步数太少如10步会丢失细节步数太多如200步收益递减。Stable Diffusion默认用50步DDIM采样在质量和速度之间取得了不错的平衡。DDIM还有一个额外的好处它是确定性的这意味着同样的 ( x_T ) 总是生成同样的图像。这对调试和复现非常有用。而DDPM是随机的每次采样结果都不同。6. 潜在扩散模型Stable Diffusion的核心思路6.1 为什么要搬到隐空间DDPM直接在像素空间做扩散生成一张512×512的图U-Net的输入输出都是512×512×3。这个计算量非常大训练需要几百张GPU跑好几周。Stable Diffusion的核心改进是把扩散过程搬到一个低维隐空间。具体做法是先用一个VAE的编码器把图像压缩成隐空间表示比如512×512×3的图压缩成64×64×4的隐向量空间维度缩小8倍通道数从3变成4。然后在隐空间上做扩散U-Net的输入输出都是64×64×4。计算量降低了大约64倍8×8的空间压缩再考虑通道变化。生成完成后再用VAE的解码器把隐向量恢复成像素图像。VAE在这里只负责压缩和解压不参与扩散过程。6.2 VAE编码器的训练细节VAE的编码器和解码器是单独训练的用的是重建损失加KL散度的标准VAE损失。但Stable Diffusion对VAE做了一些改进用了对抗损失来提升重建质量避免VAE输出过于模糊。VAE的压缩率是一个需要权衡的参数。压缩率太高如16倍重建质量会下降生成图像的细节会丢失。压缩率太低如4倍计算量节省不够。Stable Diffusion用的是8倍压缩在质量和效率之间取得了平衡。有一个容易忽略的细节VAE的缩放因子。Stable Diffusion的VAE输出会乘以一个常数0.18215这个常数是训练时统计的隐空间标准差。如果不做这个缩放扩散过程的噪声调度需要重新调整。6.3 条件注入Cross-Attention机制Stable Diffusion的U-Net和DDPM最大的区别是加入了Cross-Attention层用来注入文本条件。文本先经过CLIP编码器变成77×768的序列然后作为Key和Value传入Cross-Attention图像的隐向量作为Query。Cross-Attention的计算公式和标准自注意力一样[ \text{Attention}(Q, K, V) \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right) V ]区别在于Q来自图像特征K和V来自文本编码。这样每个图像位置都能“关注”到文本中相关的词实现文本引导的图像生成。实际实现的时候文本编码会先经过一个线性层投影到和图像特征相同的维度。Stable Diffusion的Cross-Attention维度是768和CLIP的文本编码维度一致。6.4 实际部署时的资源考量Stable Diffusion的U-Net大约有8.6亿参数VAE有8300万参数文本编码器有1.23亿参数。FP16精度下模型权重大约2GB。生成一张512×512的图在RTX 3060上大约需要5秒50步DDIM。如果显存不够可以用注意力切片attention slicing把注意力计算分块进行或者用xFormers的memory-efficient attention。这些优化能把显存占用降低30%到50%代价是速度稍微慢一点。还有一个实用技巧如果只是做推理可以把U-Net的梯度计算关掉用torch.no_grad()包裹采样过程显存占用能进一步降低。7. 训练自己的扩散模型从数据到采样7.1 数据集准备和预处理训练扩散模型的第一步是准备数据。数据集的质量直接决定生成质量。如果是做图像生成建议至少准备1万张以上的图片分辨率统一到模型的目标分辨率。图片要多样化避免单一风格导致模式坍塌。预处理包括调整大小、中心裁剪、归一化到[-1, 1]或[0, 1]。归一化的选择要和噪声调度匹配。如果用[-1, 1]前向过程的噪声也要相应调整。对于小规模实验CIFAR-1032×32或CelebA64×64是不错的起点。这些数据集规模适中训练时间可控方便快速验证想法。7.2 训练循环的代码骨架下面是一个简化的训练循环import torch from torch.utils.data import DataLoader def train_step(model, x0, optimizer, betas, alphas_bar): batch_size x0.shape[0] t torch.randint(0, len(betas), (batch_size,), devicex0.device) noise torch.randn_like(x0) sqrt_alpha_bar alphas_bar[t].sqrt().view(-1, 1, 1, 1) sqrt_one_minus_alpha_bar (1 - alphas_bar[t]).sqrt().view(-1, 1, 1, 1) x_t sqrt_alpha_bar * x0 sqrt_one_minus_alpha_bar * noise predicted_noise model(x_t, t) loss torch.nn.functional.mse_loss(predicted_noise, noise) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()这个循环里每次迭代随机采样一个时间步 ( t )计算 ( x_t )让模型预测噪声算MSE损失反向传播。整个过程非常简洁没有GAN那种复杂的训练技巧。7.3 训练中的常见问题和调参经验第一个常见问题是损失不下降。检查一下噪声调度的 ( \bar{\alpha}_t ) 是否计算正确特别是 ( t0 ) 附近。如果 ( \bar{\alpha}_t ) 在 ( t0 ) 时不是接近1说明调度有问题。第二个问题是生成图像模糊。可能的原因有训练不充分、模型容量不够、噪声调度太激进。可以尝试增加训练轮数、增大模型通道数、改用cosine调度。第三个问题是采样时出现颜色偏移。这通常是训练和采样时的时间步处理不一致导致的。检查一下采样时用的 ( t ) 是否和训练时一致特别是DDIM跳步采样时的时间步映射。学习率方面DDPM原始论文用的是2e-4配合Adam优化器。如果训练不稳定可以降到1e-4。Batch size建议至少64太小会导致梯度噪声大。7.4 采样和评估训练完成后采样过程就是从 ( x_T \sim \mathcal{N}(0, I) ) 开始逐步去噪torch.no_grad() def sample(model, n_samples, img_size, channels, betas, alphas, alphas_bar): x torch.randn(n_samples, channels, img_size, img_size) for t in reversed(range(len(betas))): t_batch torch.full((n_samples,), t, dtypetorch.long) predicted_noise model(x, t_batch) alpha_t alphas[t] alpha_bar_t alphas_bar[t] beta_t betas[t] if t 0: noise torch.randn_like(x) else: noise 0 x (1 / alpha_t.sqrt()) * (x - (beta_t / (1 - alpha_bar_t).sqrt()) * predicted_noise) beta_t.sqrt() * noise return x评估生成质量常用的指标是FIDFrechet Inception Distance它计算生成图像和真实图像在Inception网络特征空间中的分布距离。FID越低越好但FID对样本数量敏感样本太少会导致FID偏高。还有一个实用技巧采样时可以用EMA指数移动平均的模型权重而不是训练时的原始权重。EMA权重更平滑生成质量通常更好。DDPM原始论文就用了EMA衰减率取0.9999。8. 扩散模型的改进方向与实战避坑8.1 U-Net架构的改进思路U-Net虽然是扩散模型的主流选择但也有很多改进空间。第一个方向是注意力机制的位置和数量。DDPM只在低分辨率加注意力后来发现中间分辨率加注意力也能提升效果但计算量会增加。Stable Diffusion在32×32、16×16、8×8三个分辨率上都加了注意力。第二个方向是归一化和激活函数的选择。GroupNorm在扩散模型里表现稳定但也有一些工作尝试用LayerNorm或RMSNorm。激活函数方面SiLU也叫Swish是默认选择但GELU在某些任务上表现更好。第三个方向是残差块的连接方式。除了标准的残差连接还可以用DenseNet式的密集连接或者用Transformer里的Pre-Norm结构。这些改进在小规模实验里可以尝试但大规模训练时稳定性需要验证。8.2 采样加速的工程手段除了DDIM还有几种采样加速方案。DPM-Solver把扩散过程的ODE求解用高阶数值方法加速20步就能达到DDPM 1000步的质量。UniPC是DPM-Solver的改进版支持更灵活的步数选择。一致性模型Consistency Models是另一条路线它训练一个网络直接把噪声映射到图像一步生成。但一步生成的质量通常不如多步采样适合对速度要求极高的场景。实际部署时如果用的是Stable Diffusion可以直接用Hugging Face的Diffusers库它内置了多种采样器切换采样器只需要改一个参数。不同采样器的效果差异挺大建议多试几种找到最适合自己任务的。8.3 训练不收敛的排查清单训练扩散模型时遇到不收敛可以按以下顺序排查排查项可能问题解决方案噪声调度( \bar{\alpha}_t ) 计算错误检查累积乘积确保 ( t0 ) 时接近1数据归一化数据范围与噪声不匹配统一到[-1, 1]或[0, 1]学习率太大导致震荡降到1e-4或更低Batch size太小导致梯度噪声大增大到64以上模型初始化权重初始化不当用默认初始化检查是否有NaN时间步嵌入嵌入维度不匹配检查线性层输入输出维度还有一个容易被忽略的问题如果用的是混合精度训练AMP要注意损失缩放。扩散模型的损失值通常不大如果损失缩放设置不当可能导致梯度下溢。建议先用FP32训练确认能收敛再尝试AMP。8.4 实际项目中的经验教训第一个教训不要一上来就训练大模型。先用小分辨率32×32或64×64和小模型验证整个流程能跑通再逐步放大。我见过太多人直接上512×512结果训练几天不收敛连问题出在哪都不知道。第二个教训采样步数不是越多越好。DDPM 1000步和DDIM 50步的生成质量差异很小但速度差20倍。实际部署时优先用DDIM或DPM-Solver步数取20到50之间。第三个教训EMA权重的衰减率需要根据训练步数调整。如果总训练步数只有几万步0.9999的衰减率会导致EMA权重更新太慢跟不上训练进度。这时候可以降到0.999或0.9995。第四个教训VAE的重建质量对最终生成质量影响很大。如果VAE重建出来的图就已经模糊了扩散模型生成得再好也没用。训练VAE时要仔细调重建损失和KL散度的权重必要时加入感知损失或对抗损失。8.5 扩散模型在其他领域的延展扩散模型不只能做图像生成。在音频领域它被用来做语音合成和音乐生成。在分子设计领域它被用来生成新的药物分子结构。在时序数据领域有工作把它用于股票预测和气象预报。甚至在地震数据 processing 领域也有研究把扩散模型用于地震信号去噪和重建。思路是一样的把干净信号看作 ( x_0 )加噪过程模拟信号退化反向过程学习恢复。只要能把问题建模成“从噪声恢复信号”扩散模型就能派上用场。不过跨领域应用时要注意不同领域的数据分布差异很大噪声调度的参数需要重新调整。图像上好用的cosine调度在音频或时序数据上不一定最优。建议在新领域应用时先用小规模数据做消融实验找到合适的调度方案。9. 写在最后扩散模型从理论到落地中间有大量的工程细节需要打磨。公式推导只是第一步真正把模型训起来、采样出高质量结果还需要对数据、架构、训练策略有深入理解。我自己在复现DDPM和Stable Diffusion的过程中最大的体会是不要怕公式但也不要只盯着公式。很多问题的答案不在论文里而在代码和实验里。如果你刚开始接触扩散模型建议从DDPM的官方代码或Hugging Face的Diffusers库入手先跑通一个CIFAR-10的小实验再逐步深入。遇到不收敛的情况按照上面的排查清单一步步来大部分问题都能定位到。扩散模型的学习曲线确实陡但一旦跨过那个坎后面的路会越走越宽。