扩散模型从DDPM到Stable Diffusion:原理推导与U-Net代码实战
2026/9/19 13:55:35 网站建设 项目流程

扩散模型这两年火得一塌糊涂,但真正能把它的来龙去脉讲清楚的材料并不多。大部分教程要么一上来就甩一堆随机微分方程的公式把人劝退,要么只给一个调库的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年DDPM(Denoising 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{s=1}^{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,防止 ( t=0 ) 附近 ( \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的归纳偏置(局部性、平移等变性)对图像去噪任务非常合适。虽然后来DiT(Diffusion 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, 2i+1) = \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, device=device) * -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, padding=1) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1) 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的采样过程是马尔可夫的,必须从 ( t=T ) 一步步走到 ( t=0 ),总共1000步。每一步都要跑一次完整的U-Net前向传播,计算量巨大。生成一张256×256的图,在V100上大概需要20秒。这个速度在实际应用中是没法接受的。

慢的根本原因在于:DDPM的反向过程被约束为马尔可夫链,每一步只依赖前一步。虽然理论上可以跳步,但跳步会引入误差,生成质量会下降。

5.2 DDIM的非马尔可夫视角

DDIM(Denoising 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-10(32×32)或CelebA(64×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,), device=x0.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 ) 是否计算正确,特别是 ( t=0 ) 附近。如果 ( \bar{\alpha}_t ) 在 ( t=0 ) 时不是接近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, dtype=torch.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

评估生成质量常用的指标是FID(Frechet 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 ) 计算错误检查累积乘积,确保 ( t=0 ) 时接近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的小实验,再逐步深入。遇到不收敛的情况,按照上面的排查清单一步步来,大部分问题都能定位到。扩散模型的学习曲线确实陡,但一旦跨过那个坎,后面的路会越走越宽。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询