生成模型 / 扩散与 Flow Matching 原理笔记入门 25 分钟阅读 查看源文件 ↗

DDPM 与 DDIM#

本页内容6 个部分
查看概览图
FOUNDATIONS · 03

加噪有闭式解,
去噪才需要网络

DDPM 定义前向马尔可夫链,网络只学「这一步加了什么噪声」;DDIM 换掉反向过程的马尔可夫假设,把 1000 步压到 50 步。

DATA x₀ 干净图像
FORWARD q x_t 一步到位加噪
NETWORK ε_θ(x_t, t) 预测加了什么噪声
REVERSE p x_{t−1} 去噪一步
训练和采样共用同一个网络——采样方式可以随时切换
边际分布x_t = √ᾱ_t · x₀ + √(1−ᾱ_t) · ε

高斯的可叠加性让任意步的 q(x_t|x₀) 有闭式解——训练时不必真的一步步加噪,直接采样 t 即可。

SCHEDULEᾱ_t 怎么衰减

cosine 把中段拉长

t →ᾱ_t
linearcosine

linear schedule 在末端过早把信息噪没,cosine 让高信噪比区间占更多步数。

两个旋钮 何时加噪
预测什么
TARGET三种预测目标

同一个模型,不同的参数化

  • ε-predDDPM 原始;小 t 端 ε 几乎全是 x_t,预测变平凡、方差不稳
    标准
  • v-pred全时间步方差稳定;两端都不退化,渐进式蒸馏必备
    稳
  • x₀-pred直接预测终点,适合一步 / 少步采样
    少步
特性DDPMDDIM
反向过程马尔可夫(每步只看上一步)非马尔可夫(先预测 x₀ 再跳任意步)
是否随机随机(每步加噪声 z)确定性(σ_t = 0)
能否跳步不能能
典型步数100050(质量接近 DDPM)
同一噪声输入每次不同图像每次同一张图像
训练方式完全相同——同一个模型,采样方式可以随时切换
为什么 DDPM 不能跳步
马尔可夫链 + 单高斯近似

反向每一步都被近似成一个高斯,只有在步长足够小时这个近似才成立;跳步等于让单个高斯去拟合一个跨度很大的真实后验,误差立刻放大。DDIM 换成非马尔可夫族,先估 x₀ 再跳,才绕开了这个限制。

前向无参数,反向才学ε / v / x₀ 三种参数化DDIM 确定性 · 可跳步

组件阅读路线 · DDPM → DDIM

1. 加噪与去噪的基本机制#

1.1 DDPM & DDIM:核心逻辑#

DDPM#

前向过程:定义马尔可夫链 q(xt∣xt−1)=N(1−βt xt−1,  βtI)q(x_t|x_{t-1}) = \mathcal{N}(\sqrt{1-\beta_t}\,x_{t-1},\;\beta_t I),逐步加噪。由于高斯的可叠加性,可以直接写出任意步的边际分布 q(xt∣x0)=N(αˉt x0,  (1−αˉt)I)q(x_t|x_0) = \mathcal{N}(\sqrt{\bar\alpha_t}\,x_0,\;(1-\bar\alpha_t)I),即 xt=αˉt x0+1−αˉt ϵx_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon。

反向过程的推导:我们想要 q(xt−1∣xt)q(x_{t-1}|x_t),但这个直接算不出来(需要对所有 x0x_0 积分)。不过如果已知 x0x_0,用贝叶斯公式可以精确算出后验:

q(xt−1∣xt,x0)=N(μ~t,  β~tI)q(x_{t-1}|x_t, x_0) = \mathcal{N}(\tilde\mu_t,\;\tilde\beta_t I)

μ~t\tilde\mu_t 由 xtx_t 和 x0x_0 线性组合得到,β~t\tilde\beta_t 只和 schedule 有关

网络的作用:实际采样时我们没有 x0x_0,所以用网络 ϵθ(xt,t)\epsilon_\theta(x_t, t) 预测噪声,通过前向公式反推 x^0=xt−1−αˉt ϵθαˉt\hat{x}_0 = \frac{x_t - \sqrt{1-\bar\alpha_t}\,\epsilon_\theta}{\sqrt{\bar\alpha_t}},代入 μ~t\tilde\mu_t 公式得到均值。

采样:从高斯后验中采样 xt−1=μ~θ(xt,t)+β~t zx_{t-1} = \tilde\mu_\theta(x_t, t) + \sqrt{\tilde\beta_t}\,z,其中 z∼N(0,I)z\sim\mathcal{N}(0,I)。

为什么不能跳步:DDPM 把反向每一步建模为单个高斯 pθ(xt−1∣xt)=N(μθ,σt2I)p_\theta(x_{t-1}|x_t) = \mathcal{N}(\mu_\theta, \sigma_t^2 I)。这个近似成立的前提是相邻步噪声差异很小(βt\beta_t 很小),此时真实的反向分布 q(xt−1∣xt)q(x_{t-1}|x_t) 确实接近高斯。但如果跳步(比如从 tt 直接到 t−50t-50),真实的 q(xt−50∣xt)q(x_{t-50}|x_t) 是对所有可能的 x0x_0 积分后的混合分布——高度多峰,一个高斯根本拟合不了。所以不是网络预测不准(网络在任意 tt 上都能准确预测 ϵ\epsilon),而是单高斯的建模假设在大步长下崩了。

DDIM#

起点:和 DDPM 共享完全相同的前向边际分布 q(xt∣x0)q(x_t|x_0) 和同一个训练好的网络 ϵθ(xt,t)\epsilon_\theta(x_t, t)。不需要重新训练。

核心思路:DDPM 的训练只依赖边际分布 q(xt∣x0)q(x_t|x_0),不依赖联合分布的马尔可夫结构。所以可以构造其他反向过程,只要保持同样的边际。DDIM 提出一族带自由参数 σt\sigma_t 的条件分布(详细推导见第 07 节):

xt−1=αˉt−1 x^0⏟预测的 clean image+1−αˉt−1−σt2 ϵθ(xt,t)⏟指向噪声方向的确定性分量+σt z⏟随机分量x_{t-1} = \sqrt{\bar\alpha_{t-1}}\,\underbrace{\hat{x}_0}_{\text{预测的 clean image}} + \underbrace{\sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}\,\epsilon_\theta(x_t,t)}_{\text{指向噪声方向的确定性分量}} + \underbrace{\sigma_t\,z}_{\text{随机分量}}

确定性分量方差 + 随机分量方差 = (1−αˉt−1−σt2)+σt2=1−αˉt−1(1-\bar\alpha_{t-1}-\sigma_t^2) + \sigma_t^2 = 1-\bar\alpha_{t-1},总方差恒定,所以对任意 σt\sigma_t 边际分布都和 DDPM 一致

σt\sigma_t 的含义:σt=β~t\sigma_t = \tilde\beta_t 时退化为 DDPM;σt=0\sigma_t = 0 时过程完全确定性,这就是 DDIM。

为什么能跳步:公式只涉及 αˉt\bar\alpha_t 和 αˉt−1\bar\alpha_{t-1} 两个累积系数,没有用到"tt 和 t−1t-1 相邻"这个条件。可以定义任意子序列 τ1,τ2,…,τS\tau_1, \tau_2, \dots, \tau_S(如从 1000 步里选 50 个),公式结构完全不变。同一个网络 ϵθ\epsilon_\theta 本来就是按任意 tt 训练的,所以在这些时间步上的预测仍然有效。

1.2 DDPM 的前向过程(加噪)#

直觉#

给干净图片 x0x_0 逐步加高斯噪声,TT 步后变成纯噪声 xT∼N(0,I)x_T \sim \mathcal{N}(0, I)。每步加多少由 noise schedule {βt}\{\beta_t\} 控制(β1=0.0001\beta_1=0.0001 到 βT=0.02\beta_T=0.02,从小到大递增)。

💡 T=1000T=1000 是怎么来的?

实验选择,非理论推导。TT 越大每步噪声越小、反向高斯近似越准,但采样越慢。后来 DDIM 解耦了训练步数和采样步数(训练仍用 T=1000T=1000,采样可以只走 50 步)。再后来 Score-based SDE / Flow Matching 把离散时间步换成连续变量 t∈[0,1]t \in [0,1],"TT 选多大"这个问题就彻底不存在了,采样时再选离散化步数即可。

一步步加噪的公式#

xt=1−βt xt−1+βt ϵt,ϵt∼N(0,I)x_t = \sqrt{1-\beta_t}\, x_{t-1} + \sqrt{\beta_t}\, \epsilon_t, \quad \epsilon_t \sim \mathcal{N}(0, I)

一步到位公式(最核心!)#

高斯分布的可加性——一步到位公式成立的数学基础#

两个独立高斯随机变量相加,结果还是高斯:

X∼N(μ1,σ12),  Y∼N(μ2,σ22)  ⇒  X+Y∼N(μ1+μ2,  σ12+σ22)X \sim \mathcal{N}(\mu_1,\sigma_1^2),\; Y \sim \mathcal{N}(\mu_2,\sigma_2^2) \;\Rightarrow\; X+Y \sim \mathcal{N}(\mu_1+\mu_2,\; \sigma_1^2+\sigma_2^2)

独立变量之和的均值、方差相加并非高斯独有;高斯的关键性质是独立线性组合仍为高斯。

没有这个性质,多步加噪没法合并成一步,DDPM 的训练框架就不 work。

推导过程#
x_2 = sqrt(1-β₂) · x₁ + sqrt(β₂) · ε₂
    = sqrt(1-β₂) · [sqrt(1-β₁) · x₀ + sqrt(β₁) · ε₁] + sqrt(β₂) · ε₂
    = sqrt((1-β₁)(1-β₂)) · x₀ + sqrt((1-β₂)β₁) · ε₁ + sqrt(β₂) · ε₂
                                  └──── 两个独立高斯的线性组合 ────┘
用可加性,方差相加:
  (1-β₂)·β₁ + β₂ = 1 - (1-β₁)(1-β₂) = 1 - α₁α₂ = 1 - ᾱ₂
所以:x₂ = sqrt(ᾱ₂) · x₀ + sqrt(1-ᾱ₂) · ε'

递推到任意 tt 步。定义 αt=1−βt\alpha_t = 1-\beta_t,αˉt=∏i=1tαi\bar{\alpha}_t = \prod_{i=1}^t \alpha_i:

xt=αˉt x0+1−αˉt ϵ,ϵ∼N(0,I)x_t = \sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1-\bar{\alpha}_t}\, \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)

从 x0x_0 和一个噪声 ϵ\epsilon 直接算出任意时间步的 xtx_t

αˉT\bar{\alpha}_T 何时足够接近 0#

log⁡αˉT=∑t=1Tlog⁡(1−βt)\log\bar\alpha_T=\sum_{t=1}^T\log(1-\beta_t)。有限个小于 1 的数相乘并不必然接近零,需要 schedule 的累计噪声足够大。无限步极限也需该和趋于 −∞-\infty。例如 1000 步、β\beta 从 10−410^{-4} 线性增至 0.020.02 时,αˉT≈4.04×10−5\bar\alpha_T\approx4.04\times10^{-5}。

  • t=0t=0:αˉ0=1\bar{\alpha}_0=1,纯净图像
  • tt 小:αˉt≈0.95\bar{\alpha}_t \approx 0.95,大部分原图 + 少量噪声
  • tt 大:αˉt≈0.05\bar{\alpha}_t \approx 0.05,大部分噪声
  • t=Tt=T:αˉT≈0\bar{\alpha}_T \approx 0,纯噪声
αˉt\bar{\alpha}_t 预先算好存表#
betas = torch.linspace(0.0001, 0.02, 1000)
alphas = 1.0 - betas
alpha_bars = torch.cumprod(alphas, dim=0)   # 累积乘积 cumprod
# alpha_bars[0]≈0.9999  alpha_bars[499]≈0.05  alpha_bars[999]≈0.00005

训练时直接 alpha_bars[t] 查表,不管 tt 是 1 还是 999 计算量完全一样。

⚠️ 一步到位公式 ≠ 重参数化技巧

形式上都是 均值 ++ 标准差 ×ϵ\times \epsilon,但动机不同。VAE 的 μ,σ\mu, \sigma 是网络输出,重参数化是为了让梯度穿过采样操作。DDPM 的系数是预计算常数,梯度不需要穿过 xtx_t 的构造——这个公式的价值是计算效率(O(1) 得到任意步的 xtx_t)。详见面试题 06。

1.3 DDPM 的反向过程(去噪)#

建模#

前向加噪固定已知,反向去噪才是要训练的。建模为高斯:pθ(xt−1∣xt)=N(xt−1;  μθ(xt,t),  σt2I)p_\theta(x_{t-1}|x_t) = \mathcal{N}(x_{t-1};\; \mu_\theta(x_t,t),\; \sigma_t^2 I)。均值 μθ\mu_\theta 由网络预测,方差 σt2\sigma_t^2 固定。

方差为什么固定不预测?#

反向过程方差 σt2\sigma_t^2 有一个理论上下界:σt2∈[β~t,  βt]\sigma_t^2 \in [\tilde{\beta}_t, \; \beta_t]。下面推导这两个界怎么来的,以及为什么它们差距很小。

下界 β~t\tilde{\beta}_t:已知 x0x_0 时的后验方差

从贝叶斯公式(后面详细推导)可以算出 q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0) 是一个高斯分布。三个高斯的概率密度相乘时,指数部分合并,对 xt−1x_{t-1} 配方,方差项为:

1β~t=1−βtβt+11−αˉt−1\frac{1}{\tilde{\beta}_t} = \frac{1-\beta_t}{\beta_t} + \frac{1}{1-\bar{\alpha}_{t-1}}

精度(方差的倒数)= 来自 q(xt∣xt−1)q(x_t|x_{t-1}) 的精度 + 来自 q(xt−1∣x0)q(x_{t-1}|x_0) 的精度

通分化简:

1β~t=(1−βt)(1−αˉt−1)+βtβt(1−αˉt−1)=1−βt−αˉt−1+βtαˉt−1+βtβt(1−αˉt−1)\frac{1}{\tilde{\beta}_t} = \frac{(1-\beta_t)(1-\bar{\alpha}_{t-1}) + \beta_t}{\beta_t(1-\bar{\alpha}_{t-1})} = \frac{1-\beta_t - \bar{\alpha}_{t-1} + \beta_t\bar{\alpha}_{t-1} + \beta_t}{\beta_t(1-\bar{\alpha}_{t-1})}

=1−αˉt−1(1−βt)βt(1−αˉt−1)=1−αˉtβt(1−αˉt−1)= \frac{1 - \bar{\alpha}_{t-1}(1-\beta_t)}{\beta_t(1-\bar{\alpha}_{t-1})} = \frac{1 - \bar{\alpha}_t}{\beta_t(1-\bar{\alpha}_{t-1})}

其中用了 αˉt=αˉt−1⋅αt=αˉt−1(1−βt)\bar{\alpha}_t = \bar{\alpha}_{t-1} \cdot \alpha_t = \bar{\alpha}_{t-1}(1-\beta_t)

取倒数得到下界:

β~t=1−αˉt−11−αˉt⋅βt\boxed{\tilde{\beta}_t = \frac{1-\bar{\alpha}_{t-1}}{1-\bar{\alpha}_t} \cdot \beta_t}

这是已知 x0x_0 时后验分布 q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0) 的精确方差

上界 βt\beta_t:完全不知道 x0x_0 时的方差

如果对 x0x_0 完全没有信息,反向一步的不确定性最大,等于前向一步加的噪声量 βt\beta_t。这是方差的上界——"最坏情况"。

**结论:**比例因子 1−αˉt−11−αˉt\frac{1-\bar{\alpha}_{t-1}}{1-\bar{\alpha}_t} 总是接近 1(相邻 αˉ\bar{\alpha} 差距很小),所以 β~t≈βt\tilde{\beta}_t \approx \beta_t,上下界差距不到 1%,方差选哪个值影响都不大。走 1000 步时均值(方向)是关键,方差(步幅)影响可忽略。

但少步采样时(如 50 步 DDIM),每步变化大了,方差差异开始有影响。所以 Improved DDPM(2021)让网络预测插值系数 vv,σ2=exp⁡(vlog⁡βt+(1−v)log⁡β~t)\sigma^2 = \exp(v \log \beta_t + (1-v)\log\tilde{\beta}_t),在上下界之间学最优值。

预测 ϵ\epsilon 而不是 μ\mu:一个参数化选择#

这是参数化方式的选择——知道 ϵθ\epsilon_\theta 就能用公式算出 μθ\mu_\theta,数学上完全等价,但优化更容易。

ϵθ→x0→μθ\epsilon_\theta \to x_0 \to \mu_\theta 的推导链#

第一步:从前向公式反解 x0x_0

x0=xt−1−αˉt ϵαˉtx_0 = \frac{x_t - \sqrt{1-\bar{\alpha}_t}\, \epsilon}{\sqrt{\bar{\alpha}_t}}

第二步:后验分布 q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0) 的均值公式(从贝叶斯推导,下面详解):

μ~t=αˉt−1βt1−αˉtx0+αt(1−αˉt−1)1−αˉtxt\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

x0x_0 和 xtx_t 的加权平均——一部分信息来自原始图像,一部分来自当前加噪图像

第三步:把第一步的 x0x_0 代入第二步,化简:

μθ(xt,t)=1αt(xt−βt1−αˉtϵθ(xt,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 → 公式算 x0x_0 → 公式算 μθ\mu_\theta → 用 μθ\mu_\theta + 固定方差构建高斯 → 采样 xt−1x_{t-1}。网络只输出一个东西,剩下全是公式。

贝叶斯推导:q(xt−1∣xt,x0)q(x_{t-1}|x_t,x_0) 怎么来的#

贝叶斯公式回顾:你想求 P(A∣B)P(A|B),手上有 P(B∣A)P(B|A):

P(A∣B)=P(B∣A)⋅P(A)P(B)P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}

比如医院检测:P(得病∣阳性)=P(阳性∣得病)⋅P(得病)P(阳性)=0.99×0.0010.01=9.9%P(\text{得病}|\text{阳性}) = \frac{P(\text{阳性}|\text{得病}) \cdot P(\text{得病})}{P(\text{阳性})} = \frac{0.99 \times 0.001}{0.01} = 9.9\%

在 DDPM 里,所有概率都在"已知 x0x_0"的背景下。条件于两个变量 (xt,x0)(x_t, x_0) 并不特殊——就是"在已知这两件事的前提下"。可以把 (xt,x0)(x_t, x_0) 看成一个整体,又回到条件于一个变量的情况。x0x_0 是额外信息,让分布更确定。把 x0x_0 当共同背景:

q(xt−1∣xt,x0)=q(xt∣xt−1,x0)⋅q(xt−1∣x0)q(xt∣x0)q(x_{t-1}|x_t,x_0) = \frac{q(x_t|x_{t-1},x_0) \cdot q(x_{t-1}|x_0)}{q(x_t|x_0)}

右边三项全已知:

  • q(xt∣xt−1,x0)=q(xt∣xt−1)q(x_t|x_{t-1},x_0) = q(x_t|x_{t-1}):前向加噪一步(马尔可夫,xtx_t 只依赖 xt−1x_{t-1}),N(1−βtxt−1,βtI)\mathcal{N}(\sqrt{1-\beta_t}x_{t-1}, \beta_t I)
  • q(xt−1∣x0)=N(αˉt−1x0,  (1−αˉt−1)I)q(x_{t-1}|x_0) = \mathcal{N}(\sqrt{\bar{\alpha}_{t-1}}x_0,\; (1-\bar{\alpha}_{t-1})I):一步到位到 t−1t-1
  • q(xt∣x0)=N(αˉtx0,  (1−αˉt)I)q(x_t|x_0) = \mathcal{N}(\sqrt{\bar{\alpha}_t}x_0,\; (1-\bar{\alpha}_t)I):一步到位到 tt

三个高斯代入,高斯乘高斯除高斯还是高斯,算出精确的均值 μ~t\tilde{\mu}_t 和方差 β~t\tilde{\beta}_t。但推理时不知道 x0x_0(正是要生成的),所以用网络预测的 ϵθ\epsilon_\theta 反解 x0x_0 估计值代入。

🔥 为什么预测噪声 ϵ\epsilon 而不是 x0x_0 或 μ\mu?

实验:DDPM 论文对比三种目标,ϵ\epsilon-pred 最好。

直觉:ϵ\epsilon 始终服从 N(0,I)\mathcal{N}(0,I),不随 tt 变化,学习目标均匀。预测 x0x_0 在 tt 大时要从纯噪声猜原图,太难。

数学:预测 ϵ\epsilon 等价于学 score function ∇xlog⁡p(x)\nabla_x \log p(x)(详见 SDE 统一框架)。

2. 训练目标与采样过程#

2.1 DDPM 的训练过程#

推导很复杂,但最终训练目标极其简洁:

L=Ex0,ϵ,t[∥ϵ−ϵθ(xt,t)∥2]L = \mathbb{E}_{x_0,\epsilon,t}\left[\|\epsilon - \epsilon_\theta(x_t,t)\|^2\right]

预测噪声和真实噪声的 MSE

**为什么训练目标这么简单?**DDPM 的理论推导经过了变分下界(ELBO)、KL 散度等复杂数学。但最终发现直接优化简单的 MSE loss 效果比完整的 ELBO 还好。"简化比完整更好"是 DDPM 的一个重要贡献。

训练的每一步(伪代码)#

for each training step:
    1. 取一张图 x_0
    2. 随机 t ~ Uniform(1, T)
    3. 随机 ε ~ N(0, I)
    4. 查表 ᾱ_t,算 x_t = sqrt(ᾱ_t)·x_0 + sqrt(1-ᾱ_t)·ε
    5. 网络预测: ε_θ = UNet(x_t, t)
    6. 损失: L = MSE(ε, ε_θ)
    7. 反向传播更新

网络架构:U-Net 完整计算过程#

时间步 tt 怎么注入#

问题:tt 只是一个标量整数(比如 500),但网络内部的特征是高维的(128/256/512 通道)。一个数字携带的信息太少,也没法直接和高维特征做加法。需要先把它"展开"成高维向量。

做法:Sinusoidal 编码——和 Transformer 位置编码完全一样,用 128 对不同频率的 sin/cos 把标量 tt 展开成 256 维向量:

为什么要这么做?

  • 维度匹配:网络特征是高维的,标量没法直接参与运算。展开成 256 维后才能和网络特征交互。
  • 区分度:不同 tt 编码后得到不同的向量。tt=500 和 tt=501 差一点,tt=500 和 tt=100 差很多
  • 连续性:相邻时间步的编码向量距离近,差别大的距离远,符合去噪任务的性质——tt=500 和 tt=501 的去噪任务相似,编码也该相似。

和 Transformer 位置编码的关系:完全一样的公式,只是编码的对象不同——Transformer 编码的是 token 的位置("我是第几个词"),DDPM 编码的是时间步("现在加了多少噪声")。本质都是把标量展开成高维向量让网络使用。

编码后过全局 MLP 得到共享的 t_embt\_emb,然后每层各有自己的线性投影层把 t_embt\_emb 投影到该层的通道数:

# 算一次,所有层共享:
t=500 → Sinusoidal编码 → (B,256) → 全局MLP → (B,256)

# 每层各自有投影层,维度不同:
Enc1 通道128 → Linear(256,128) → (B,128) → 扩展成 (B,128,1,1) → 广播加到特征上
Enc2 通道256 → Linear(256,256) → (B,256) → (B,256,1,1)
Enc3 通道512 → Linear(256,512) → (B,512) → (B,512,1,1)
Bottleneck 1024 → Linear(256,1024) → (B,1024) → (B,1024,1,1)
U-Net 完整前向传播#
输入: x_t = (B, 3, 64, 64)     时间: t = 500

═══ Encoder(下采样)═══

Enc1: Conv + t注入 + Conv
  (B,3,64,64) → (B,128,64,64)     保存副本 h1 ← skip connection 用

Downsample: stride=2 卷积
  (B,128,64,64) → (B,128,32,32)

Enc2: Conv + t注入 + Conv
  (B,128,32,32) → (B,256,32,32)   保存副本 h2

Downsample → (B,256,16,16)

Enc3: Conv + t注入 + Conv
  (B,256,16,16) → (B,512,16,16)   保存副本 h3

Downsample → (B,512,8,8)

═══ Bottleneck ═══

Conv + Self-Attention + t注入 + Conv
  (B,512,8,8) → (B,1024,8,8)
  Self-Attention 在 8×8=64 个位置上,O(64²) 算得动

═══ Decoder(上采样)═══

Upsample → (B,1024,16,16)
Skip: cat[..., h3] → (B,1536,16,16)    ← 通道维度拼接 1024+512=1536
Dec3: Conv + t注入 + Conv → (B,512,16,16)

Upsample → (B,512,32,32)
Skip: cat[..., h2] → (B,768,32,32)     ← 512+256=768
Dec2: Conv + t注入 + Conv → (B,256,32,32)

Upsample → (B,256,64,64)
Skip: cat[..., h1] → (B,384,64,64)     ← 256+128=384
Dec1: Conv + t注入 + Conv → (B,128,64,64)

═══ 输出 ═══

Final 1×1 Conv: (B,128,64,64) → (B,3,64,64)

输出: ε_θ = (B,3,64,64)     ← 和输入 x_t 一样大的噪声预测

Skip Connection 的作用:Encoder 保留高分辨率细节(边缘、纹理),通过 skip 拼接到 Decoder 对应层。没有 skip,Decoder 只能从 8×8 低分辨率恢复,细节丢失。

每层投影层独立学习:Sinusoidal 编码 + 全局 MLP 只算一次,但每层各有自己的 Linear(256, 通道数)。Enc1 的投影层学到"tt 对低层特征的影响",Bottleneck 的学到"tt 对高层语义的影响"。

2.2 DDPM 的采样过程(生成)#

xt−1=1αt(xt−βt1−αˉtϵθ(xt,t))+σtz,z∼N(0,I)x_{t-1} = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}}\epsilon_\theta(x_t,t)\right) + \sigma_t z, \quad z \sim \mathcal{N}(0,I)

t>1t>1 时加随机噪声 zz,t=1t=1 时 z=0z=0

1. x_1000 ~ N(0,I)                    ← 从纯噪声开始
2. for t = 1000, 999, ..., 1:
       ε_θ = UNet(x_t, t)             ← 同一个网络,每次不同的 x_t 和 t
       x_{t-1} = 去噪公式(x_t, ε_θ, t)
3. 输出 x_0

**为什么每步还要加噪声 zz?**因为反向过程是一个随机过程(随机微分方程),需要随机性来保持分布的正确性。σt\sigma_t 通常取 βt\beta_t 或 β~t\tilde{\beta}_t。

采样就是反复调用同一个 U-Net 1000 次。每次约 50ms,总计 50 秒。这就是 DDIM 出现的动机。

3. DDIM 与跳步采样#

3.1 DDPM 为什么不能跳步?马尔可夫 + 单高斯近似#

DDPM 采样必须 x1000→x999→⋯→x0x_{1000} \to x_{999} \to \cdots \to x_0 逐步走,不能跳步。这是两个设计选择共同导致的:

马尔可夫结构(远因):前向和反向都设计成马尔可夫链——下一步只取决于当前状态。这让反向每步需要独立建模 pθ(xt−1∣xt)p_\theta(x_{t-1}|x_t),自然地引导我们选择最简单的参数化形式。

单高斯近似(近因,直接原因):DDPM 把每步反向分布建模为单个高斯 pθ(xt−1∣xt)=N(μθ,σt2I)p_\theta(x_{t-1}|x_t) = \mathcal{N}(\mu_\theta, \sigma_t^2 I)。这个近似成立的前提是相邻步噪声差异很小(βt\beta_t 很小),此时真实的 q(xt−1∣xt)q(x_{t-1}|x_t) 确实接近高斯。但如果跳步(比如从 tt 直接到 t−50t-50),真实的 q(xt−50∣xt)=∫q(xt−50∣xt,x0) q(x0∣xt) dx0q(x_{t-50}|x_t) = \int q(x_{t-50}|x_t, x_0)\, q(x_0|x_t)\, dx_0 是对所有可能 x0x_0 的加权混合——高度多峰,一个高斯根本拟合不了。

⚠️ "马尔可夫"本身并不必然导致慢

假设我们仍用马尔可夫结构,但把反向每步建模成更强的分布(如 mixture of Gaussians 或 normalizing flow),理论上可以拟合多峰分布,也就能跳更大的步。所以不能跳步的直接原因是单高斯近似在大步长下失效,马尔可夫只是让这个简单参数化变得自然的背景条件。

那为什么不用更强的分布?因为不划算——单高斯让 loss 简化为简单的 MSE,训练极其简洁;而 DDIM 证明了同一个网络换个采样公式就能跳步,代价几乎为零,没必要在每步建模上做复杂化。

3.2 DDIM:为什么更快,公式怎么来的#

DDIM 公式从哪来?为什么对任意 σt\sigma_t 都成立?#

出发点:训练只依赖边际分布#

DDPM 训练时用的是一步到位公式 xt=αˉt x0+1−αˉt ϵx_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon,只涉及边际分布 q(xt∣x0)=N(αˉt x0,  (1−αˉt)I)q(x_t|x_0) = \mathcal{N}(\sqrt{\bar\alpha_t}\,x_0,\;(1-\bar\alpha_t)I)。训练从来没有用过"xtx_t 是从 xt−1x_{t-1} 一步加噪来的"这个马尔可夫结构。所以只要构造的反向过程保持同样的边际分布,训练好的网络就仍然适用——不需要重新训练。

DDIM 的构造:写一个含 σt\sigma_t 的 ansatz#

DDIM 论文提出:给定 (xt,x0)(x_t, x_0),构造 xt−1x_{t-1} 如下:

xt−1=αˉt−1 x0⏟clean image+1−αˉt−1−σt2⋅xt−αˉt x01−αˉt⏟确定性分量(方向=噪声方向)+σt z⏟随机分量x_{t-1} = \sqrt{\bar\alpha_{t-1}}\,\underbrace{x_0}_{\text{clean image}} + \underbrace{\sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}\cdot\frac{x_t - \sqrt{\bar\alpha_t}\,x_0}{\sqrt{1-\bar\alpha_t}}}_{\text{确定性分量(方向=噪声方向)}} + \underbrace{\sigma_t\,z}_{\text{随机分量}}

z∼N(0,I)z \sim \mathcal{N}(0,I),σt\sigma_t 是自由参数

这个公式不是"推导"出来的——它是一个猜测(ansatz),然后验证它确实保持边际分布一致。

验证:为什么边际分布一致?#

关键观察:公式中 xt−αˉt x01−αˉt\frac{x_t - \sqrt{\bar\alpha_t}\,x_0}{\sqrt{1-\bar\alpha_t}} 是什么?从前向公式 xt=αˉt x0+1−αˉt ϵx_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon 移项,这就是 ϵ\epsilon——标准正态噪声。所以给定 x0x_0,公式变成:

xt−1=αˉt−1 x0+1−αˉt−1−σt2 ϵ+σt zx_{t-1} = \sqrt{\bar\alpha_{t-1}}\,x_0 + \sqrt{1-\bar\alpha_{t-1}-\sigma_t^2}\,\epsilon + \sigma_t\,z

ϵ\epsilon 和 zz 都是独立的标准正态

验证均值和方差:

  • 均值:E[xt−1∣x0]=αˉt−1 x0+0+0=αˉt−1 x0\mathbb{E}[x_{t-1}|x_0] = \sqrt{\bar\alpha_{t-1}}\,x_0 + 0 + 0 = \sqrt{\bar\alpha_{t-1}}\,x_0 ✓
  • 方差:两个独立噪声方差相加 = (1−αˉt−1−σt2)+σt2=1−αˉt−1(1-\bar\alpha_{t-1}-\sigma_t^2) + \sigma_t^2 = 1-\bar\alpha_{t-1} ✓

所以 xt−1∣x0∼N(αˉt−1 x0,  (1−αˉt−1)I)x_{t-1}|x_0 \sim \mathcal{N}(\sqrt{\bar\alpha_{t-1}}\,x_0,\;(1-\bar\alpha_{t-1})I)——和 DDPM 的前向边际完全一致,对任意 σt\sigma_t 都成立。σt2\sigma_t^2 只是控制方差在确定性分量和随机分量之间的分配比例,总方差恒定。

💡 为什么这个验证就够了?

训练好的网络 ϵθ(xt,t)\epsilon_\theta(x_t, t) 学的是:给定 xtx_t(服从 q(xt∣x0)q(x_t|x_0)),预测加进去的噪声 ϵ\epsilon。只要采样过程中每一步产生的 xtx_t 仍然服从同样的 q(xt∣x0)q(x_t|x_0),网络的预测就仍然有效。上面验证了 DDIM 产生的 xt−1∣x0x_{t-1}|x_0 和 DDPM 的边际分布完全相同,所以递推下去,每一步的 xtx_t 都在网络的训练分布上——网络不需要重新训练。

σt\sigma_t 的两个极端#
  • σt=β~t=1−αˉt−11−αˉt⋅βt\sigma_t = \sqrt{\tilde\beta_t} = \sqrt{\frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t}\cdot\beta_t}:退化为 DDPM 的后验方差,恢复 DDPM 随机采样
  • σt=0\sigma_t = 0:确定性分量承担全部方差,没有随机项——这就是 DDIM
实际采样时用 ϵθ\epsilon_\theta 代替 ϵ\epsilon#

验证时用的是真实的 ϵ\epsilon,但采样时我们没有 x0x_0(正是要生成的)。用网络预测 ϵθ(xt,t)\epsilon_\theta(x_t,t) 代替 ϵ\epsilon,同时反推 x^0=xt−1−αˉt ϵθαˉt\hat{x}_0 = \frac{x_t - \sqrt{1-\bar\alpha_t}\,\epsilon_\theta}{\sqrt{\bar\alpha_t}} 代替 x0x_0。核心操作分两步:

第一步:预测 x0x_0#

x^0=xt−1−αˉt ϵθ(xt,t)αˉt\hat{x}_0 = \frac{x_t - \sqrt{1-\bar{\alpha}_t}\,\epsilon_\theta(x_t,t)}{\sqrt{\bar{\alpha}_t}}

第二步:从 x^0\hat{x}_0 出发走到任意目标步 s<ts < t#

xs=αˉs x^0+1−αˉs ϵθ(xt,t)x_s = \sqrt{\bar{\alpha}_s}\,\hat{x}_0 + \sqrt{1-\bar{\alpha}_s}\,\epsilon_\theta(x_t,t)

(这是 σt=0\sigma_t=0 的简化形式。)

本质:每步**"先跳回 x0x_0 估计,再走到目标时间步"**,打破了马尔可夫约束。公式只涉及 αˉt\bar{\alpha}_t 和 αˉs\bar{\alpha}_s,ss 可以是任意时间步,因此可以跳步。

DDPM(马尔可夫,不能跳步):
  1000 → 999 → 998 → ... → 1 → 0                (1000步)
DDIM(非马尔可夫,可以跳步):
  1000 → 900 → 800 → ... → 100 → 0              (10步)
  1000 → 800 → 600 → 400 → 200 → 0              (5步)

**为什么 50 步 DDIM 质量接近 1000 步 DDPM?**DDIM 每步都在"预测 x0x_0"并逐步精化。50 次纠正足够收敛。DDPM 每步只"去一点点噪声",必须走够 1000 步。

确定性采样#

DDPM 每步加随机噪声 zz,同一个 xTx_T 生成不同图像。DDIM 在 η=0\eta=0 时没有逐步随机项——在模型、条件、网格及确定性计算相同的前提下,同一个 xTx_T 生成同一张图像,潜在空间有语义结构,可做插值。

DDPM vs DDIM 对比#

特性 DDPM DDIM
反向过程 马尔可夫(每步只看上一步) 非马尔可夫(先预测 x0x_0 再跳任意步)
是否随机 随机(每步加噪声 zz) 确定性(σt=0\sigma_t=0)
能否跳步 不能 能
典型步数 1000 50(质量接近 DDPM)
同一噪声输入 每次不同图像 每次同一张图像
训练方式 完全相同——同一个模型,采样方式可以随时切换

4. 预测参数化与噪声日程#

4.1 ε-pred / x₀-pred / v-pred:三种预测目标#

网络的输入都是 (xt,t)(x_t, t),但"预测什么"有三种选择。从前向公式出发:xt=αˉtx0+1−αˉtϵx_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon。

ε-pred(预测噪声)#

网络输出 ϵθ(xt,t)≈ϵ\epsilon_\theta(x_t,t) \approx \epsilon。DDPM 原论文的选择。

优点:ϵ\epsilon 永远服从 N(0,I)\mathcal{N}(0,I),不管 tt 多大多小,预测目标的分布都一样,学习目标均匀。

缺点:tt 很小时(xtx_t 几乎没噪声),ϵ\epsilon 对 xtx_t 的影响极小(1−αˉt\sqrt{1-\bar{\alpha}_t} 接近 0)。信噪比很低,学习效率差。

x₀-pred(预测原图)#

网络输出 x0,θ(xt,t)≈x0x_{0,\theta}(x_t,t) \approx x_0。

优点:tt 小时很容易(xt≈x0x_t \approx x_0)。预测的 x0x_0 有直接视觉意义——采样中途可以看到"网络目前认为结果长什么样"。

缺点:tt 很大时从纯噪声直接预测原图,难度极大。不同 tt 下难度差异很大。

v-pred(预测 velocity)#

v=αˉt ϵ−1−αˉt x0v = \sqrt{\bar{\alpha}_t}\, \epsilon - \sqrt{1-\bar{\alpha}_t}\, x_0

"velocity" = 噪声方向和数据方向的加权差;如果把加噪看成从 x0x_0 到 ϵ\epsilon 的一条路径,vv 就是时间 tt 处的切线方向(速度)

两端近似:tt 小时 αˉt≈1,  1−αˉt≈0\sqrt{\bar{\alpha}_t}\approx 1,\;\sqrt{1-\bar{\alpha}_t}\approx 0,所以 v≈ϵv\approx\epsilon;tt 大时反过来,v≈−x0v\approx -x_0。

关键性质:vv 的方差在任何 tt 下恒为 1

Var(v)=αˉt⋅Var(ϵ)⏟=1+(1−αˉt)⋅Var(x0)⏟≈1=αˉt+(1−αˉt)=1\mathrm{Var}(v) = \bar{\alpha}_t\cdot\underbrace{\mathrm{Var}(\epsilon)}_{=1} + (1-\bar{\alpha}_t)\cdot\underbrace{\mathrm{Var}(x_0)}_{\approx 1} = \bar{\alpha}_t + (1-\bar{\alpha}_t) = 1

系数 αˉt\sqrt{\bar{\alpha}_t} 和 1−αˉt\sqrt{1-\bar{\alpha}_t} 平方和恒为 1,vv 的尺度始终稳定

⚠️ 常见误解:v-pred 不是让两端都"变容易"

既然 tt 小时 v≈ϵv\approx\epsilon,而 ϵ\epsilon-pred 在 tt 小时很困难,那 v-pred 不是也一样困难吗?

是的,v-pred 确实继承了两端的挑战——小 tt 时要从接近干净的图检测微弱噪声,大 tt 时要从纯噪声重建原图轮廓。它并没有消除这些难度。

v-pred 的优势是避免了两端的极端失效:

  • ϵ\epsilon-pred 小 tt 时:ϵ\epsilon 对 xtx_t 的贡献系数 1−αˉt→0\sqrt{1-\bar{\alpha}_t}\to 0,就算预测错了,x0x_0 的重建几乎不受影响——梯度信号接近消失,学了也没用
  • x0x_0-pred 大 tt 时:x0x_0 被噪声完全淹没,网络在做近乎"无中生有"的猜测
  • v-pred:两端均为中等难度,没有"梯度消失"也没有"无中生有"——难度曲线更平
难度
 高 │  ε-pred ↗↗(梯度渐消失)       x₀-pred ↗↗(近乎猜测)
    │
 中 │  ─────────── v-pred ───────────────────────────────
    │
 低 │  x₀-pred(太简单)             ε-pred(正常)
    └──────────────────────────────────────────────────→ t
       t 小(低噪声)                             t 大(高噪声)

缺点:概念上不如 ϵ\epsilon-pred 直觉,实现略复杂一点。

三种预测目标的对比#

预测目标 网络输出 t 小时 t 大时 使用场景
ε-pred 噪声 ϵ 梯度趋于消失,
学了也无用
正常 DDPM 原版,SD 1.x / 2.x
x₀-pred 原始图像 x0 过于简单,
无学习信号
近乎猜测,
极度困难
部分模型用于初步预测
v-pred velocity v 中等难度
(继承 ε-pred 挑战)
中等难度
(继承 x₀-pred 挑战)
SD 2.x(可选),Stable Video Diffusion

💡 三者可以互相转换

知道了 xtx_t、tt,以及 ϵ/x0/v\epsilon / x_0 / v 中的任何一个,都能算出另外两个:

x0=xt−1−αˉt ϵαˉtx_0 = \frac{x_t - \sqrt{1-\bar{\alpha}_t}\, \epsilon}{\sqrt{\bar{\alpha}_t}}

ϵ=xt−αˉt x01−αˉt\epsilon = \frac{x_t - \sqrt{\bar{\alpha}_t}\, x_0}{\sqrt{1-\bar{\alpha}_t}}

v=αˉt ϵ−1−αˉt x0v = \sqrt{\bar{\alpha}_t}\, \epsilon - \sqrt{1-\bar{\alpha}_t}\, x_0

所以三种预测目标在数学上等价——差别只在训练时梯度加权方式不同,导致优化行为不同。

趋势:ϵ\epsilon-pred 是经典默认选择,图像生成主流。v-pred 在视频生成(Stable Video Diffusion)和少步采样/蒸馏中越来越流行。Flow Matching(SD3、Flux)用向量场预测,和 v-pred 思路接近。x0x_0-pred 和 μ\mu-pred 基本没人单独用了。

4.2 Noise Schedule 的设计#

{βt}\{\beta_t\} 怎么选直接影响生成质量。

线性 schedule(DDPM 原版)#

β1=0.0001\beta_1 = 0.0001,βT=0.02\beta_T = 0.02,中间线性插值。

问题:线性增长意味着后半段加噪太快,αˉt\bar{\alpha}_t 在中间时间步急剧下降。很多时间步上 xtx_t 已经接近纯噪声,网络学不到东西——"已经全是噪声了,去噪也去不出什么"。

余弦 schedule(Improved DDPM,2021)#

αˉt\bar{\alpha}_t 按余弦函数下降,中间时间步衰减更缓慢:

αˉt=f(t)f(0),f(t)=cos⁡(t/T+s1+s⋅π2)2\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.008s = 0.008 是一个小常数,防止 βt\beta_t 在 tt 接近 0 时太小

效果:αˉt\bar{\alpha}_t 在中间时间步衰减更慢,保留更多有效训练时间步。质量明显提升,特别是低分辨率生成。

Scaled Linear(Stable Diffusion 用的)#

对 βt\sqrt{\beta_t} 做线性插值而不是对 βt\beta_t 做线性插值。在隐空间操作时效果更好。

**面试知道这些就够了:**线性 schedule 是 DDPM 原版但不够好,余弦 schedule 是改进版,SD 用 scaled linear。核心思想是让 αˉt\bar{\alpha}_t 在中间时间步衰减不要太快,保留更多有效训练区间。

5. 理解检查与实现#

5.1 理解检查(6 道)#

面试题 01 扩散模型到底在学什么?

结论:学去噪——给定 (xt,t)(x_t,t) 预测噪声 ϵ\epsilon(或等价地学 score function ∇xlog⁡p(x)\nabla_x \log p(x))。知道了噪声就能从加噪图像还原出干净图像。

训练时:往真实图片加噪声让网络预测。损失是简单的 MSE。

推理时:从纯噪声开始反复去噪,逐步得到干净图像。

面试题 02 为什么从噪声可以一步步还原图像?

数学:前向过程把 p(x0)p(x_0) 逐步变成 N(0,I)\mathcal{N}(0,I)。学到反向(score function)就能从噪声走回数据分布。

直觉:想象山谷(数据高密度区域)。加噪 = 把石头从山谷滚到平地。score function = "指向山谷的方向"。从平地沿 score 走就能走回山谷。

面试题 03 为什么预测噪声不预测原图?

实验:ϵ\epsilon-pred 最好。直觉:ϵ\epsilon 始终 N(0,I)\mathcal{N}(0,I),学习均匀。x0x_0-pred 在 tt 大时太难。数学:等价于 score matching。

追问准备:v-pred = αˉtϵ−1−αˉtx0\sqrt{\bar{\alpha}_t}\epsilon - \sqrt{1-\bar{\alpha}_t}x_0,是两者平衡。视频生成和少步采样在用。

面试题 04 DDIM 为什么更快?

根本原因:DDPM 是马尔可夫链不能跳步。DDIM 先预测 x0x_0,然后用一步到位公式从 x0x_0 加噪到任意目标步,步长自由。1000 步压缩到 50 步质量损失不大。

追问:DDIM 还有什么好处?确定性采样——同一噪声输入永远生成同一图,潜在空间有语义结构。

面试题 05 扩散模型比 GAN 好在哪里?

训练稳定:不需要平衡生成器和判别器,简单 MSE 回归。不模式崩塌。

多样性好:有明确概率建模,理论上覆盖数据分布所有模式。

有似然估计:可以计算近似的 log-likelihood。GAN 没有。

代价:采样慢(多步迭代 vs GAN 一步生成)。

面试题 06 重参数化技巧是什么?DDPM 里的一步到位公式和它什么关系?

重参数化技巧:把"从 N(μ,σ2)\mathcal{N}(\mu,\sigma^2) 采样"改写为 z=μ+σ⋅ϵz = \mu + \sigma \cdot \epsilon,ϵ∼N(0,I)\epsilon \sim \mathcal{N}(0,I)。让采样变成可导的确定性计算,梯度能穿过去。经典应用是 VAE——μ,σ\mu, \sigma 是 Encoder 网络输出,不做重参数化 Encoder 就没法训练。

DDPM 的一步到位公式:xt=αˉtx0+1−αˉtϵx_t = \sqrt{\bar{\alpha}_t}x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon,数学形式一样,但系数是预计算的固定常数,不是网络输出。训练时梯度不需要穿过 xtx_t 的构造。这个公式的真正价值是计算效率——O(1) 得到任意时间步的 xtx_t,不用跑马尔可夫链。

什么时候真的需要重参数化:系数是可学习参数、梯度要穿过采样操作时(VAE 训练)。不需要:系数是固定常数(DDPM 前向加噪)、采样在计算图最前端(GAN 的 zz)、或不做反向传播(推理时)。

5.2 实现练习:PyTorch 手写实现#

代码题 1:|r - x²| < ε 的优化问题#

面试经典题:给定目标值 rr,用梯度下降优化 xx 使得 ∣r−x2∣<ϵ|r - x^2| < \epsilon(等价于求 r\sqrt{r})。考察对 PyTorch 自动微分和优化循环的理解:

import torch

def optimize_sqrt(r, epsilon=1e-6, lr=0.01, max_steps=10000):
    """
    用梯度下降找 x,使得 |r - x²| < epsilon

    Args:
        r: 目标值(正数)
        epsilon: 收敛阈值
        lr: 学习率
        max_steps: 最大迭代次数

    Returns:
        x: 优化后的值,满足 x² ≈ r(即 x ≈ √r)
    """
    # x 是可学习参数,随机初始化为正数
    x = torch.rand(1, requires_grad=True)
    optimizer = torch.optim.Adam([x], lr=lr)

    for step in range(max_steps):
        loss = (x ** 2 - r) ** 2

        with torch.no_grad():
            residual = torch.abs(r - x ** 2).item()
            if residual < epsilon:
                print(f"收敛!步数={step}, x={x.item():.8f}, |r-x²|={residual:.2e}")
                return x.detach()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print(f"未收敛,|r-x²|={residual:.2e}")
    return x.detach()

# 示例
r = torch.tensor(9.0)
x_opt = optimize_sqrt(r, epsilon=1e-6)
# 预期:x ≈ 3.0(因为 √9 = 3)

更精简的写法(面试白板版)

import torch

r = 9.0
x = torch.tensor([1.0], requires_grad=True)
opt = torch.optim.SGD([x], lr=0.01)

for i in range(1000):
    loss = (x ** 2 - r) ** 2                # |r - x²|²
    if torch.abs(x ** 2 - r).item() < 1e-6:
        break
    opt.zero_grad()
    loss.backward()
    opt.step()

print(f"x = {x.item()}")                   # ≈ 3.0(√9)

面试考察点:(1) requires_grad=True 让 x 可学习 (2) loss.backward() 自动求导 ∂L∂x=4x(x2−r)\frac{\partial L}{\partial x} = 4x(x^2 - r)(链式法则) (3) optimizer.zero_grad() 在 backward() 之前清零梯度 (4) 收敛判断用 torch.no_grad() 或 .item() 避免图跟踪。

代码题 2:DDPM 完整训练流程#

面试要求"写一个图像训练的完整流程",以下是 DDPM 的标准实现:

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import os

# ====== 1. 自定义 Dataset ======
class ImageDataset(Dataset):
    def __init__(self, img_dir, transform=None):
        self.img_dir = img_dir
        self.transform = transform
        self.img_names = [f for f in os.listdir(img_dir)
                          if f.endswith(('.png', '.jpg', '.jpeg'))]

    def __len__(self):
        return len(self.img_names)

    def __getitem__(self, idx):
        path = os.path.join(self.img_dir, self.img_names[idx])
        img = Image.open(path).convert('RGB')
        if self.transform:
            img = self.transform(img)
        return img                    # 无监督任务,不需要 label

transform = transforms.Compose([
    transforms.Resize(64),
    transforms.CenterCrop(64),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize([0.5]*3, [0.5]*3),   # 归一化到 [-1, 1]
])
dataset = ImageDataset('data/train', transform=transform)
loader  = DataLoader(dataset, batch_size=128, shuffle=True, num_workers=4, pin_memory=True)

# ====== 2. 噪声调度(预计算所有系数)======
T = 1000
betas = torch.linspace(0.0001, 0.02, T)       # β_t: 线性增长
alphas = 1.0 - betas                         # α_t = 1 - β_t
alpha_bar = torch.cumprod(alphas, dim=0)     # ᾱ_t = ∏α_i
sqrt_alpha_bar = torch.sqrt(alpha_bar)       # √ᾱ_t
sqrt_one_minus_ab = torch.sqrt(1 - alpha_bar) # √(1-ᾱ_t)

# ====== 3. 模型和优化器 ======
device = torch.device('cuda:0')
model = UNet(in_ch=3, out_ch=3, ch=128)       # U-Net 骨干
model = model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=2e-4)

# ====== 4. 训练循环 ======
model.train()
for epoch in range(100):
    for images in loader:
        images = images.to(device)            # [B, 3, 64, 64]

        # Step 1: 随机采样时间步
        t = torch.randint(0, T, (images.shape[0],), device=device)

        # Step 2: 随机采样噪声
        noise = torch.randn_like(images)      # ε ~ N(0, I)

        # Step 3: 前向加噪(一步到位公式)
        # x_t = √ᾱ_t · x_0 + √(1-ᾱ_t) · ε
        a = sqrt_alpha_bar[t].view(-1, 1, 1, 1).to(device)
        b = sqrt_one_minus_ab[t].view(-1, 1, 1, 1).to(device)
        x_t = a * images + b * noise

        # Step 4: 网络预测噪声
        noise_pred = model(x_t, t)            # ε_θ(x_t, t)

        # Step 5: MSE 损失
        loss = F.mse_loss(noise_pred, noise)

        # Step 6: 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print(f"Epoch {epoch}, Loss: {loss.item():.4f}")

# ====== 5. 采样(生成)======
@torch.no_grad()
def sample(model, n_samples=16, img_size=64):
    model.eval()
    x = torch.randn(n_samples, 3, img_size, img_size).to(device)

    for t in reversed(range(T)):              # T-1, ..., 0
        t_batch = torch.full((n_samples,), t, device=device, dtype=torch.long)

        # 预测噪声
        eps = model(x, t_batch)

        # 计算去噪后的均值
        alpha_t = alphas[t]
        alpha_bar_t = alpha_bar[t]
        beta_t = betas[t]

        mean = (1.0 / alpha_t.sqrt()) * (
            x - (beta_t / (1 - alpha_bar_t).sqrt()) * eps
        )

        # 除最后一步外加随机噪声
        if t > 0:
            sigma = beta_t.sqrt()
            x = mean + sigma * torch.randn_like(x)
        else:
            x = mean

    return x  # [n_samples, 3, 64, 64] 生成图像

训练流程速记(面试白板 5 行版)

# DDPM 训练核心(5 行)
t     = torch.randint(0, T, (B,))           # 1. 随机时间步
noise = torch.randn_like(x0)                # 2. 随机噪声
x_t   = sqrt_abar[t]*x0 + sqrt_1mabar[t]*noise  # 3. 加噪
eps_pred = model(x_t, t)                    # 4. 预测噪声
loss  = F.mse_loss(eps_pred, noise)         # 5. MSE 损失

面试关键点:(1) 自定义 Dataset 需实现 __len__ 和 __getitem__,DDPM 是无监督任务只返回图像 (2) 预计算 αˉt\bar\alpha_t 是一个一维向量,训练时用 t 索引 (3) .view(-1,1,1,1) 是为了广播到 [B,C,H,W] (4) 训练只预测 ϵ\epsilon,推理时用预测的 ϵ\epsilon 反推均值再加噪 (5) 最后一步不加噪声(t=0t=0 就是最终结果)。

代码题 3:Flow Matching 训练流程(对比 DDPM)#

面试可能同时问 DDPM 和 Flow Matching,可以对比着写:

# ====== Flow Matching 训练(对比上面的 DDPM)======
# 注意:FM 不需要预计算任何系数表!

model = DiT(in_ch=3, hidden_dim=768, depth=12)  # 用 DiT 或任意网络
model = model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

model.train()
for epoch in range(100):
    for images in loader:
        x1 = images.to(device)               # 数据 = x1(FM 约定)
        B  = x1.shape[0]

        # Step 1: 采样噪声 x0
        x0 = torch.randn_like(x1)            # x0 ~ N(0, I)

        # Step 2: 连续时间采样
        t = torch.rand(B, device=device)     # t ~ U(0, 1)

        # Step 3: 线性插值(比 DDPM 简单得多)
        t_exp = t.view(B, 1, 1, 1)
        x_t = (1 - t_exp) * x0 + t_exp * x1 # 直线插值

        # Step 4: velocity 目标
        v_target = x1 - x0                   # 常数!

        # Step 5: 网络预测 velocity
        v_pred = model(x_t, t)

        # Step 6: MSE 损失
        loss = F.mse_loss(v_pred, v_target)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

# ====== Flow Matching 采样(对比上面的 DDPM 采样)======
@torch.no_grad()
def fm_sample(model, n_samples=16, img_size=64, steps=50):
    x = torch.randn(n_samples, 3, img_size, img_size).to(device)
    dt = 1.0 / steps

    for i in range(steps):
        t = torch.full((n_samples,), i * dt, device=device)
        v = model(x, t)
        x = x + dt * v                      # 就这一行!

    return x

三题总结:面试代码核心考点

|r-x²|<ε 优化:考察 requires_grad=True + backward() + optimizer.step() 的基本功,以及链式法则求导。

DDPM 训练:考察预计算 αˉt\bar\alpha_t、一步到位加噪 x_t = √ᾱ·x0 + √(1-ᾱ)·ε、MSE 损失。

FM velocity:考察 v = x1 - x0、线性插值 x_t = (1-t)x0 + t·x1、和 DDPM 的对比。

扩散模型基础:DDPM → DDIM

多模态算法面试准备系列

原始资料:DDPM、DDIM。DDIM 包含不同噪声强度的采样族,不能把整个算法族都视为确定性。

延伸阅读#

相关阅读路线

理解生成模型 ↗