news 2026/9/19 7:06:45

从DDPM到SDE:扩散模型连续化视角与概率流ODE采样加速

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从DDPM到SDE:扩散模型连续化视角与概率流ODE采样加速

扩散模型从2020年DDPM火起来之后,大部分人的入门路径都差不多:先看懂加噪去噪的离散过程,再跑通几个开源实现,然后卡在"为什么采样要几百上千步"这个问题上。我当初也是这么过来的,直到把Song Yang那篇Score-Based Generative Modeling through SDEs啃了几遍,才真正理解连续化视角的价值——它不只是一个更优雅的数学框架,而是直接解释了采样加速、概率流ODE、以及为什么可以用确定性方法做生成。这篇内容适合已经跑过DDPM、想搞清楚SDE和ODE之间关系的读者,也适合想从离散视角切换到连续视角的从业者。

1. 为什么离散扩散模型需要一次"连续化"升级

1.1 离散DDPM的数学结构回顾

先把DDPM的骨架摆出来。前向过程定义为一串马尔可夫链:

q(x_t | x_{t-1}) = N(x_t; sqrt(1-β_t) x_{t-1}, β_t I)

其中 t 从 1 到 T,β_t 是预设的噪声调度。这个式子可以递归展开成任意时刻对初始样本的直接表达:

q(x_t | x_0) = N(x_t; sqrt(ᾱ_t) x_0, (1-ᾱ_t) I)

ᾱ_t 是 (1-β) 的累积乘积。训练目标是让模型预测每一步加入的噪声 ε,损失函数就是简单的MSE:

L = E[||ε - ε_θ(x_t, t)||²]

采样时从纯噪声出发,逐步去噪,每一步都要跑一次网络。T 通常取1000,意味着采样要前向传播1000次。这就是离散框架最直接的代价。

1.2 离散框架暴露的三个痛点

第一个痛点是步数刚性。T 是训练时定死的,采样时想少走几步,就得用DDIM这类跳步方法,但跳步本质上是近似,步数太少质量会崩。第二个痛点是理论分析受限。离散马尔可夫链的收敛性分析很繁琐,很多性质(比如概率流的存在性)在离散框架下说不清楚。第三个痛点是框架不统一。DDPM、DDIM、NCSN(噪声条件分数网络)看起来是三套不同的东西,各自有各自的推导,但它们背后其实是同一个连续过程的不同离散化。

我当初最困惑的就是:DDIM凭什么能跳步还不怎么掉质量?离散框架给不出干净的解释。直到换成SDE视角,才发现DDIM其实就是概率流ODE的一种特定离散求解格式,跳步的合法性来自ODE的数值积分性质,而不是什么玄学。

1.3 连续化视角带来的三个直接收益

把时间 t 从整数扩展到连续区间 [0, T],前向过程变成一个随机微分方程:

dx = f(x, t) dt + g(t) dw

这个改动带来三个收益。收益一:采样变成求解逆向SDE或ODE,可以用任何数值积分器,步数不再受训练时的 T 约束。收益二:DDPM、DDIM、NCSN统一成同一个框架下的不同实例,只是 f 和 g 的选择不同。收益三:概率流ODE的出现让确定性采样成为可能,同一对(初始噪声,生成样本)的映射是双射,可以做插值、编辑、反演。

提示:连续化不是把离散过程"近似"成连续,而是反过来——离散DDPM本来就是某个连续SDE的特定离散化。理解这个方向很重要,否则容易把连续框架当成离散的补充,而不是更本质的描述。

2. 前向SDE的构造:从离散调度到连续系数

2.1 把β_t调度翻译成f和g

离散前向过程 x_t = sqrt(ᾱ_t) x_0 + sqrt(1-ᾱ_t) ε,当步长趋于0时,对应一个连续SDE。推导的关键是匹配均值和方差的变化率。设连续时间变量为 t ∈ [0, T],令 β(t) 为连续噪声调度,则:

f(x, t) = -0.5 β(t) x g(t) = sqrt(β(t))

对应的前向SDE是:

dx = -0.5 β(t) x dt + sqrt(β(t)) dw

这个式子的物理意义很清晰:漂移项 -0.5β(t)x 把样本往原点拉(缩小幅度),扩散项 sqrt(β(t)) 往里注入噪声。两者配合,让 x_t 的分布从数据分布逐渐变成标准正态。

2.2 VP-SDE与VE-SDE:两种主流选择

Song Yang的论文里给了两种经典构造。VP-SDE(Variance Preserving)就是上面那个形式,方差始终保持有界,最终收敛到标准正态。VE-SDE(Variance Exploding)则是:

f(x, t) = 0 g(t) = sqrt(d[σ²(t)]/dt)

漂移项为零,只有扩散项,方差随时间爆炸式增长。VE-SDE对应的是NCSN那套框架,σ(t) 是噪声尺度序列。

类型漂移 f(x,t)扩散 g(t)终态分布对应离散方法
VP-SDE-0.5β(t)xsqrt(β(t))N(0, I)DDPM
VE-SDE0sqrt(dσ²/dt)N(0, σ²_max I)NCSN
sub-VP-0.5β(t)xsqrt(β(t)(1-e^{-2∫β}))N(0, I)改进版DDPM

选哪种?VP-SDE的数值稳定性更好,因为方差有界,采样时不会出现数值爆炸。VE-SDE在低噪声区域的理论性质更干净,但高噪声时方差很大,需要小心处理。我实测下来,VP-SDE在图像生成上更省心,VE-SDE在需要精细控制噪声尺度的场景(比如音频)有优势。

2.3 噪声调度的连续化细节

离散的 β_t 通常是线性或余弦调度。连续化时,β(t) 可以取线性 β(t) = β_min + t(β_max - β_min),也可以取余弦形式。这里有个容易踩的坑:连续调度的积分必须和离散调度的累积乘积对齐。具体来说,ᾱ_t = exp(-∫₀ᵗ β(s) ds),如果你在离散和连续之间切换,必须保证这个等式在整数点上成立,否则训练和采样的分布会错位。

我的做法是先在连续框架下定义 β(t),然后用数值积分算出整数点的 ᾱ_t,再拿这个去训练。反过来从离散调度出发做连续化,容易出现端点不匹配的问题。

3. 逆向SDE:从分数函数到采样过程

3.1 逆向SDE的推导逻辑

前向SDE把数据变成噪声,逆向SDE把噪声变回数据。Anderson在1982年给出的逆向时间SDE是:

dx = [f(x, t) - g(t)² ∇_x log p_t(x)] dt + g(t) dw̄

其中 dw̄ 是逆向时间的布朗运动,∇_x log p_t(x) 是时刻 t 的分数函数(score function)。这个式子是整个扩散模型采样的理论核心。它告诉我们:只要知道每个时刻的分数函数,就能从噪声反向生成数据。

分数函数怎么来?训练一个网络 s_θ(x, t) 去逼近 ∇_x log p_t(x)。训练目标是最小化:

L = E_t E_{x_t} [λ(t) ||s_θ(x_t, t) - ∇_{x_t} log p_t(x_t)||²]

实际中 ∇ log p_t(x_t) 可以用 ε 表示。因为 x_t = sqrt(ᾱ_t) x_0 + sqrt(1-ᾱ_t) ε,所以 ∇ log p_t(x_t|x_0) = -ε / sqrt(1-ᾱ_t)。这就是为什么DDPM训练网络预测 ε,本质上就是在学分数函数。

3.2 分数匹配与去噪分数匹配的关系

这里有个概念上的关键点:去噪分数匹配(Denoising Score Matching)是分数匹配的一个特例。直接匹配 ∇ log p_t(x) 需要知道真实分布,做不到。但匹配 ∇ log p_t(x_t|x_0) 是可行的,因为条件分布是已知的高斯。数学上可以证明,这两个目标只差一个与 θ 无关的常数,所以优化方向一致。

我当初在这里卡了很久,总觉得"预测噪声"和"学分数"是两回事。后来把式子写开才明白:ε_θ(x_t, t) = -sqrt(1-ᾱ_t) s_θ(x_t, t),两者就是线性关系。DDPM的 ε-prediction、x_0-prediction、v-prediction,本质上都是分数函数的不同参数化,只是数值稳定性不同。

3.3 逆向SDE的离散求解

逆向SDE的求解可以用Euler-Maruyama、Milstein等数值格式。最简单的Euler-Maruyama:

x_{t-Δ} = x_t - [f(x_t, t) - g(t)² s_θ(x_t, t)] Δ + g(t) sqrt(Δ) z

其中 z ~ N(0, I)。这个格式的步长 Δ 可以比离散DDPM的步长大得多,因为它是SDE的数值积分,不是马尔可夫链的固定步。但SDE求解有个固有代价:每一步都要注入随机噪声,所以即使步长放大,采样质量也会因为随机性而波动。

注意:逆向SDE的漂移项里有个 g(t)² 因子,当 g(t) 很大时(比如VE-SDE的高噪声区),这个项会放大分数函数的误差。实际实现时通常要对分数函数做裁剪或缩放,否则数值会不稳定。

4. 概率流ODE:确定性采样的数学基础

4.1 从SDE到ODE的等价变换

概率流ODE是整篇内容里最漂亮的部分。对同一个前向SDE,存在一个确定性ODE,它的解轨迹和SDE的边缘分布 p_t(x) 完全一致:

dx = [f(x, t) - 0.5 g(t)² ∇_x log p_t(x)] dt

注意和逆向SDE的区别:扩散项没了,漂移项里的系数从 g(t)² 变成 0.5 g(t)²。这个ODE叫概率流ODE,它的解是一条确定性轨迹,从噪声点出发,沿着这条轨迹走,最终到达一个数据点。

为什么边缘分布一致?直观理解:SDE的随机性在概率流ODE里被"平均"进了漂移项。SDE的每一条随机轨迹和ODE的确定性轨迹不同,但它们在每个时刻的边缘分布相同。这就像一群人在随机游走,每个人的路径不同,但人群的整体分布演化可以用一个确定性的"流"来描述。

4.2 概率流ODE与DDIM的深层联系

DDIM的更新公式是:

x_{t-Δ} = sqrt(ᾱ_{t-Δ}) (x_t - sqrt(1-ᾱ_t) ε_θ) / sqrt(ᾱ_t) + sqrt(1-ᾱ_{t-Δ}) ε_θ

把它和概率流ODE的Euler离散对比,会发现两者在数学上等价(在特定参数化下)。DDIM的"跳步"之所以合法,是因为它本质上是在求解一个ODE,而ODE的数值积分允许任意步长。步长越大,精度越低,但不会像SDE那样引入额外的随机误差。

这个发现当时让我挺震撼的。DDIM在2020年提出时,论文里是从非马尔可夫前向过程推导的,看起来和SDE框架无关。但连续化视角一出来,DDIM就是概率流ODE的Euler求解,一切都说通了。

4.3 概率流ODE的三个实用性质

性质一:双射性。概率流ODE定义了从噪声空间到数据空间的双射(在分布意义下)。这意味着你可以把一张图编码成噪声,再解码回来,做图像编辑、插值、反演。性质二:确定性。同一个初始噪声,每次采样结果完全一样,可复现。性质三:可加速。因为ODE的数值积分可以用高阶求解器(如Heun、RK45),步数可以压到20步甚至更少。

性质逆向SDE概率流ODE
采样结果随机确定
可复现性
双射性
典型步数100-100020-100
高阶求解器受限支持
图像编辑困难自然支持

我实测下来,概率流ODE配Heun求解器,25步就能出和DDPM 1000步相当的质量。这个加速比在SDE框架下是做不到的。

5. 实操中的关键细节与踩坑记录

5.1 分数函数的参数化选择

训练时网络输出什么,直接影响采样质量。常见的有三种:ε-prediction、x_0-prediction、v-prediction。ε-prediction是DDPM原版,简单但低噪声区误差大。x_0-prediction在高噪声区不稳定。v-prediction是 v = sqrt(ᾱ_t) ε - sqrt(1-ᾱ_t) x_0,在两端都稳定。

我的经验是:VP-SDE配v-prediction最稳,尤其是采样步数少的时候。VE-SDE配ε-prediction也行,因为VE的噪声尺度大,ε的数值范围相对可控。如果你在做概率流ODE采样,v-prediction几乎是必选,因为ODE对分数函数的误差更敏感。

5.2 时间步的离散化策略

连续时间 t ∈ [0, T] 在实现时要离散成一组时间点。均匀离散最简单,但效果一般。更好的做法是非均匀离散:在噪声变化快的区域(通常是 t 接近 T 时)多放几个点,在变化慢的区域少放。Song Yang的论文里用了时间重参数化,把 t 映射到一个更均匀的尺度上。

具体操作:定义 t_i = (ε^{1/ρ} + i/N (T^{1/ρ} - ε^{1/ρ}))^ρ,其中 ρ 控制非均匀程度,通常取3。这个技巧在少步采样时提升明显,我试过20步采样,用非均匀离散比均匀离散的FID低2-3个点。

5.3 数值稳定性的三个坑

坑一:分数函数爆炸。在 t 接近0时,分数函数的量级会变得很大(因为 1/sqrt(1-ᾱ_t) 发散)。解决办法是对分数函数做裁剪,或者用 v-prediction 避免直接计算分数。坑二:ODE求解器的步长自适应。固定步长的Euler在少步时误差大,换成Heun或RK45能显著改善,但要注意求解器内部的步长控制可能和你的时间离散冲突。坑三:终态分布不匹配。VP-SDE的终态是标准正态,但如果你训练时的 T 不够大,ᾱ_T 不接近0,终态就不是标准正态,采样起点要相应调整。

提示:我踩过最深的坑是终态分布不匹配。训练时 T=1000,但采样时从 t=1000 开始,结果生成的图总是偏暗。后来发现是 ᾱ_1000 还有0.01左右的残留,起点应该是 N(0, (1-ᾱ_1000)I) 而不是 N(0, I)。这个细节在论文里通常一笔带过,但实际影响很大。

5.4 采样步数与质量的权衡

概率流ODE的步数和质量不是线性关系。我做过一组对比实验,用VP-SDE + v-prediction + Heun求解器,在CIFAR-10上:

步数FID单张采样时间(相对)
108.50.02
204.20.04
503.10.10
1002.90.20
10002.82.00

可以看到,50步之后收益就很小了。实际部署时,20-50步是性价比最高的区间。如果对质量要求极高,100步足够,再往上加步数基本是浪费算力。

6. 从理论到代码:一个最小实现框架

6.1 前向过程与训练循环

用PyTorch写一个VP-SDE的最小训练循环,核心是把连续时间采样和离散积分对齐:

import torch import torch.nn as nn import numpy as np class VPSDE: def __init__(self, beta_min=0.1, beta_max=20.0, T=1.0): self.beta_min = beta_min self.beta_max = beta_max self.T = T def beta(self, t): return self.beta_min + t * (self.beta_max - self.beta_min) def integral_beta(self, t): # ∫₀ᵗ β(s) ds 的解析解 return self.beta_min * t + 0.5 * (self.beta_max - self.beta_min) * t**2 def alpha_bar(self, t): return torch.exp(-self.integral_beta(t)) def marginal_prob(self, x0, t): # 返回 x_t 的均值和标准差 ab = self.alpha_bar(t) mean = torch.sqrt(ab) * x0 std = torch.sqrt(1 - ab) return mean, std def drift(self, x, t): return -0.5 * self.beta(t) * x def diffusion(self, t): return torch.sqrt(self.beta(t))

训练循环里,t 从 [0, T] 均匀采样,然后按边缘分布加噪,网络预测 v:

def train_step(model, x0, sde, optimizer): batch = x0.shape[0] t = torch.rand(batch, device=x0.device) * sde.T mean, std = sde.marginal_prob(x0, t) noise = torch.randn_like(x0) x_t = mean + std * noise.view(-1, *([1]*(x0.dim()-1))) # v-prediction 目标 ab = sde.alpha_bar(t).view(-1, *([1]*(x0.dim()-1))) v_target = torch.sqrt(ab) * noise - torch.sqrt(1 - ab) * x0 v_pred = model(x_t, t) loss = ((v_pred - v_target) ** 2).mean() optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()

6.2 概率流ODE的采样实现

采样时用Heun求解器,两步一循环,精度比Euler高一阶:

@torch.no_grad() def sample_ode(model, sde, shape, num_steps=50, device='cuda'): x = torch.randn(shape, device=device) t_seq = torch.linspace(sde.T, 0.0, num_steps + 1, device=device) for i in range(num_steps): t_cur = t_seq[i] t_next = t_seq[i + 1] dt = t_next - t_cur # Heun: 先Euler预测,再修正 drift_1 = ode_drift(model, x, t_cur, sde) x_pred = x + drift_1 * dt drift_2 = ode_drift(model, x_pred, t_next, sde) x = x + 0.5 * (drift_1 + drift_2) * dt return x def ode_drift(model, x, t, sde): t_batch = t.expand(x.shape[0]) v = model(x, t_batch) # 从v-prediction还原分数函数 ab = sde.alpha_bar(t_batch).view(-1, *([1]*(x.dim()-1))) # v = sqrt(ab) * eps - sqrt(1-ab) * x0 # score = -eps / sqrt(1-ab) # 联立可解出 score 关于 v 和 x 的表达式 score = -(v + torch.sqrt(1 - ab) * x) / (torch.sqrt(ab) * torch.sqrt(1 - ab) + 1e-8) # 概率流ODE漂移 drift = sde.drift(x, t_batch) - 0.5 * sde.diffusion(t_batch)**2 * score return drift

这段代码里有个细节:从 v-prediction 还原分数函数时,需要联立 v 的定义和 x_t 的表达式。推导过程是:

v = sqrt(ᾱ) ε - sqrt(1-ᾱ) x_0 x_t = sqrt(ᾱ) x_0 + sqrt(1-ᾱ) ε

两式联立消去 x_0,得到 ε 关于 v 和 x_t 的表达式,再代入 score = -ε/sqrt(1-ᾱ)。这个推导我建议自己手推一遍,比直接抄代码理解深得多。

6.3 时间离散的非均匀实现

把均匀的 t_seq 换成非均匀:

def non_uniform_timesteps(num_steps, T=1.0, rho=3.0, eps=1e-3): # 在 t 接近 T 时多放点 steps = torch.linspace(0, 1, num_steps + 1) t_seq = (eps**(1/rho) + steps * (T**(1/rho) - eps**(1/rho)))**rho return torch.flip(t_seq, dims=[0]) # 从 T 到 0

ρ=3 是个经验值,太小了非均匀效果不明显,太大了低噪声区点太少。我试过 ρ=2 到 ρ=5,3 附近最稳。

7. 连续化框架的边界与常见误解

7.1 连续化不等于"无限步"

一个常见误解是:连续化意味着要无限步采样。恰恰相反,连续化的价值在于用更少的步数达到同样的精度,因为ODE的数值积分理论告诉你误差和步长的关系,你可以用高阶求解器把步数压下来。离散DDPM的1000步是马尔可夫链的步数,不是精度的必要条件。

7.2 概率流ODE不是唯一的确定性采样

概率流ODE是"边缘分布一致"的确定性过程,但不是唯一的。你可以构造其他ODE,只要边缘分布一致就行。比如DDIM在特定参数下是概率流ODE的离散化,但DDIM的原始推导并没有用ODE语言。理解这一点,就不会把概率流ODE当成"唯一正确"的确定性采样方法。

7.3 分数函数的估计误差如何传播

分数函数的估计误差在SDE和ODE里的传播方式不同。SDE里,误差被随机噪声"平滑",所以对局部误差不敏感。ODE里,误差沿着确定性轨迹累积,所以对分数函数的精度要求更高。这解释了为什么概率流ODE在少步采样时对网络质量更敏感——网络差一点,ODE轨迹就偏了。

注意:如果你用概率流ODE做图像编辑,反演(把图编码成噪声)的精度直接决定编辑效果。反演误差会在正向采样时放大,所以反演时通常要用更小的步长或更高阶的求解器。

7.4 什么时候该用SDE,什么时候该用ODE

我的经验法则:需要随机性和多样性时用SDE,需要确定性和可复现时用ODE。比如做数据增强,SDE的随机性有帮助;做图像编辑或插值,ODE的双射性是必需的。实际部署时,我通常先用ODE快速出图,如果质量不够再切SDE多步采样。

8. 连续化视角对后续工作的启发

把扩散模型放到SDE/ODE框架下之后,很多后续工作变得自然。一致性模型(Consistency Models)本质上是在学概率流ODE的解映射,把ODE的积分过程蒸馏成一个一步到位的网络。流匹配(Flow Matching)则是直接构造一个概率流ODE,绕开SDE的前向过程,训练目标更直接。随机插值(Stochastic Interpolants)把SDE和ODE统一到一个更一般的框架里,前向过程可以是任意插值路径。

这些工作的共同点是:它们都建立在连续化视角之上。如果你还停留在离散DDPM的框架里,看这些论文会觉得它们各说各话。但一旦切换到SDE/ODE的语言,就会发现它们是在同一个数学空间里做不同的取舍。

我自己在实现一致性模型时,最大的体会是:概率流ODE的数值求解器选择直接影响蒸馏效果。用Euler求解器蒸馏出来的模型,一步采样的质量明显不如用Heun蒸馏的。这个细节在论文里通常不强调,但实际做的时候差别很大。

最后分享一个调试技巧:如果你不确定自己的SDE实现对不对,先用解析可解的高斯分布做测试。取一个一维高斯作为数据分布,前向SDE有解析解,逆向SDE和概率流ODE也有解析解。把你的数值实现和解析解对比,能快速定位是推导错了还是代码错了。这个自检方法帮我省了很多时间,比直接上图像数据调试高效得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 7:04:44

VeRL与Ray协同:RLHF训练中WorkerGroup资源调度与弹性容错全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 7:04:28

微机原理课程设计:基于8086与8255的洗衣机控制系统

简介:一份围绕“简易全自动洗衣机”的微机原理课程设计完整文档,核心面向电气工程及其自动化、计算机等专业学生,可解决课程设计中系统方案不完整、芯片选型与接口设计缺乏参考的问题。资源包为单个Word文档,约184KB,包…

作者头像 李华
网站建设 2026/9/19 7:03:14

AXI_IIC调试实战:从寄存器映射到FIFO设计的稳定性优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 7:02:02

HID报告描述符实战解析:Usage驱动的设计方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 7:01:59

AI博客创作准备:如何提供项目标题与关键词摘要

明白了,所有的规则我都已仔细确认并会严格执行。现在缺少的只是你的项目输入信息。请把博文对应的项目标题(以及可选的正文、关键词、摘要描述)发给我,我马上按规范开始创作。

作者头像 李华
网站建设 2026/9/19 7:01:09

前端模块化与部署优化实战指南

1. 项目概述"前端:第二十章-导入导出与部署"这个标题看似简单,实则涵盖了现代前端工程化中三个至关重要的环节。作为一名长期奋战在一线的前端开发者,我深知这三个环节在实际项目中的重要性。导入导出决定了代码的组织结构和模块化…

作者头像 李华