
扩散模型从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-β) 的累积乘积。训练目标是让模型预测每一步加入的噪声 ε损失函数就是简单的MSEL 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-SDEVariance Preserving就是上面那个形式方差始终保持有界最终收敛到标准正态。VE-SDEVariance 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)DDPMVE-SDE0sqrt(dσ²/dt)N(0, σ²_max I)NCSNsub-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-Maruyamax_{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终态就不是标准正态采样起点要相应调整。提示我踩过最深的坑是终态分布不匹配。训练时 T1000但采样时从 t1000 开始结果生成的图总是偏暗。后来发现是 ᾱ_1000 还有0.01左右的残留起点应该是 N(0, (1-ᾱ_1000)I) 而不是 N(0, I)。这个细节在论文里通常一笔带过但实际影响很大。5.4 采样步数与质量的权衡概率流ODE的步数和质量不是线性关系。我做过一组对比实验用VP-SDE v-prediction Heun求解器在CIFAR-10上步数FID单张采样时间相对108.50.02204.20.04503.10.101002.90.2010002.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_min0.1, beta_max20.0, T1.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] 均匀采样然后按边缘分布加噪网络预测 vdef train_step(model, x0, sde, optimizer): batch x0.shape[0] t torch.rand(batch, devicex0.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_steps50, devicecuda): x torch.randn(shape, devicedevice) t_seq torch.linspace(sde.T, 0.0, num_steps 1, devicedevice) 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, T1.0, rho3.0, eps1e-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 到 ρ53 附近最稳。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也有解析解。把你的数值实现和解析解对比能快速定位是推导错了还是代码错了。这个自检方法帮我省了很多时间比直接上图像数据调试高效得多。