2. 扩散模型基础:前向扩散过程、逆向去噪过程、马尔可夫链、扩散核与噪声调度

好,咱们正式进入扩散模型的核心。说实话,我刚接触这个概念时也觉得有点绕,但一旦理解了它的根本逻辑,你会发现它其实非常优雅。

扩散模型的核心思想,说白了就是三步:先往数据里慢慢加噪声,直到它变成纯噪声;然后学一个“逆过程”,一步步把噪声变回干净的数据。 就这么简单。但每一步背后都有扎实的数学支撑,我们一个个来看。

2.1 前向扩散过程:加噪的艺术

前向过程,也叫扩散过程。它定义了一个马尔可夫链,逐步向原始数据 x₀ 中添加高斯噪声,经过 T 步后,数据会变成各向同性的高斯噪声,也就是 xT ~ N(0, I)。

为什么必须是马尔可夫链?这里有个关键点:每一步的加噪只依赖当前状态,跟之前的历史无关。 这极大简化了问题。我个人习惯把这个过程想象成“烤焦面包”——每烤一秒,面包就更黑一点,直到变成一块黑炭。你没法从黑炭状态直接看出它本来是白面包还是全麦面包。

数学上,前向过程定义为:

q(x₁, x₂, ..., xT | x₀) = ∏t=1T q(xt | xt-1)

其中,单步转移核:
q(xt | xt-1) = N(xt ; √(1-βt) · xt-1, βtI )

这里的 βt 就是噪声调度参数,控制每一步加多少噪声。嗯,这里要注意:βt 通常很小,比如 0.0001 到 0.02 之间,确保变化是渐进的。

关键性质:得益于高斯分布的可叠加性,我们可以直接从 x₀ 一步采样到任意 xt,而不需要迭代整个链。

即:q(xt | x₀) = N(xt ; √(ᾱt) · x₀, (1-ᾱt)I )

其中 αt = 1-βt,ᾱt = ∏i=1t αi。

我记得在最初实现时,很多人会写一个循环去生成每一步的 xt,其实没必要。直接用这个封闭公式就能高效采样出任意时间步的噪声数据,训练时效率高很多。

2.2 逆向去噪过程:从噪声中恢复数据

逆向过程是扩散模型的“灵魂”。如果我们知道前向过程的逆分布 q(xt-1 | xt),就能从纯噪声 xT 一步步还原出原始数据。但问题是——这个逆分布我们算不出来。它依赖于整个数据分布。

怎么办呢?我们用一个神经网络去近似这个逆过程。记作 pθ(xt-1 | xt)。这也是一个高斯分布,但均值和方差由神经网络预测。

pθ(xt-1 | xt) = N(xt-1 ; μθ(xt, t), σt²I )

其中,μθ(xt, t) 是需要学习的值,σt² 通常固定为 βt 或 (1-ᾱt)/(1-ᾱt-1)·βt。

这里有个经典的“避坑”经验:直接预测 μθ 效果不好。 我曾经试过,模型训练半天就是不收敛。后来发现,更好的做法是让网络去预测噪声 ε,而不是直接预测图像本身。也就是:

μθ(xt, t) = (1/√αt) · ( xt - βt/(√(1-ᾱt)) · εθ(xt, t) )

采样过程重新参数化后:
xt-1 = (1/√αt) · ( xt - βt/(√(1-ᾱt)) · εθ(xt, t) ) + σt · z,  其中 z ~ N(0, I)

为什么这么做?你想想看,预测噪声本质上是一个回归问题,目标值分布稳定;而预测原始图像则涉及大量低频和高频信息,学习难度大很多。

2.3 马尔可夫链与扩散核

扩散模型中的“链”就是我们前面提到的马尔可夫链。但这里有个概念容易混淆:扩散核 (Diffusion Kernel) 和 转移核 (Transition Kernel) 是两码事。

  • 转移核:定义单步转移 q(xt | xt-1),即从 t-1 到 t 的分布。
  • 扩散核:定义从初始状态 x₀ 到任意 xt 的分布 q(xt | x₀)。

扩散核的作用在于:训练时我们不需要模拟整个链来生成 xt,而是直接从 x₀ 一步跳到 xt。我把它叫做“时光跳跃”——跳过中间所有步骤,直接拿到加了指定量噪声的结果。

实用技巧:在你的训练代码中,每次迭代都应该这样做:

  1. 从数据集中随机采样一个 x₀。
  2. 随机采样时间步 t(通常均匀采样自 1..T)。
  3. 采样噪声 ε ~ N(0, I)。
  4. 用扩散核直接计算 xt = √(ᾱt) · x₀ + √(1-ᾱt) · ε。
  5. 网络预测噪声 εθ(xt, t),与真实噪声 ε 计算 MSE 损失。

这样训练效率大幅提升,因为不需要迭代 t 步。

2.4 噪声调度策略

噪声调度 {βt} 或 {ᾱt} 的选择直接影响模型性能。常用的调度策略有:

调度名称 定义 特点
线性调度 (Linear) βt 从 0.0001 线性增长到 0.02 DDPM 原文方案,简单稳定
余弦调度 (Cosine) ᾱt = cos²( (t/T + s) / (1+s) · π/2 ) 加噪更平滑,末期仍有信息留存
平方根调度 (Sqrt) ᾱt = 1 - √(t/T) 的变形 强调前期的扩散速度

我个人更偏爱余弦调度。为什么呢?我曾经用线性调度训练过分子生成模型,发现生成的小分子有很多无效结构。后来换成余弦调度,加噪过程更温和,在后期步骤中保留了更多结构信息,生成的化学有效性提升了不少。

下面是余弦调度的PyTorch实现示例:

def cosine_beta_schedule(timesteps, s=0.008):
    """
    生成余弦噪声调度
    :param timesteps: 扩散步数 T
    :param s: 平滑参数,通常 0.008
    :return: β 序列, 长度 timesteps
    """
    steps = timesteps + 1
    t = torch.linspace(0, timesteps, steps, dtype=torch.float64)
    alphas_cumprod = torch.cos(
        (t / timesteps + s) / (1 + s) * torch.pi * 0.5
    ) ** 2
    alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
    betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
    return torch.clamp(betas, max=0.999)

注意:分子生成与图像生成不同。分子的有效结构非常脆弱,稍微过度的噪声就可能破坏化学键信息。建议将余弦调度的最大 β 限制在 0.5 以内,而不是像图像任务那样可以到 0.999。

📐 扩散模型核心知识框架(第2章) 前向扩散过程 马尔可夫链逐步加噪 x₀ → x₁ → ... → xT ~ N(0,I) 逆向去噪过程 神经网络近似逆分布 xT → ... → x₁ → x₀ 学习逆分布 马尔可夫链 (核心假设) q(xt|xt-1) 每一步只依赖前一步状态 扩散核 (高效采样) q(xt|x₀) = N(√ᾱt·x₀, (1-ᾱt)I) 无需迭代,一步跳跃到任意 t 噪声调度 {βt} 线性调度 / 余弦调度 / 平方根调度 控制加噪速度与信息保有量 核心公式总结 ① 前向转移核:q(xt|xt-1) = N(xt ; √(1-βt)·xt-1, βtI) ② 扩散核:q(xt|x₀) = N(xt ; √ᾱt·x₀, (1-ᾱt)I) ③ 重参数化采样:xt-1 = (1/√αt)·(xt - βt/(√(1-ᾱt))·εθ(xt,t)) + σt·z 训练阶段 1. 采样 (x₀, t, ε) 2. 扩散核算 xt 3. 网络预测 εθ(xt, t) 4. 损失: L = ||ε - εθ(xt, t)||² 采样阶段(生成) 1. 从 xT ~ N(0, I) 开始 2. for t = T, T-1, ..., 1: 用重参数化公式计算 xt-1 3. 输出 x₀ 即为生成结果

2.5 损失函数与训练机制

训练扩散模型其实就是最小化一个简单的MSE损失。我记得第一次跑通训练时,看到loss曲线稳定下降,心里还挺爽的。

简化版损失函数:

Lsimple(θ) = Et, x₀, ε[ ||ε - εθ(√ᾱt·x₀ + √(1-ᾱt)·ε, t)||² ]

这个损失函数的含义是:对每个时间步 t,我们都让网络学会预测当前加进去的那个噪声。所有时间步的权重相同——这也是“simple”名字的由来。虽然简单,但它就是DDPM实际用的目标函数。

经验分享:实际训练时,我通常使用 AdamW 优化器,学习率设为 2e-4。对于分子生成,batch size 在 128-256 之间效果不错。另外,建议在训练过程中对损失按时间步做无偏估计,这样每个 t 都能被公平训练到。

说到这里,你应该已经掌握了扩散模型最核心的四块拼图:前向过程、逆向过程、马尔可夫链和相关公式、噪声调度策略。这些是后面所有变体(DDIM、Score-based、Latent Diffusion)的基石。