DDPM 与 DDIM#
本页内容6 个部分
查看概览图
加噪有闭式解,
去噪才需要网络
DDPM 定义前向马尔可夫链,网络只学「这一步加了什么噪声」;DDIM 换掉反向过程的马尔可夫假设,把 1000 步压到 50 步。
高斯的可叠加性让任意步的 q(x_t|x₀) 有闭式解——训练时不必真的一步步加噪,直接采样 t 即可。
cosine 把中段拉长
linear schedule 在末端过早把信息噪没,cosine 让高信噪比区间占更多步数。
预测什么
同一个模型,不同的参数化
-
标准
ε-predDDPM 原始;小 t 端 ε 几乎全是 x_t,预测变平凡、方差不稳 -
稳
v-pred全时间步方差稳定;两端都不退化,渐进式蒸馏必备 -
少步
x₀-pred直接预测终点,适合一步 / 少步采样
反向每一步都被近似成一个高斯,只有在步长足够小时这个近似才成立;跳步等于让单个高斯去拟合一个跨度很大的真实后验,误差立刻放大。DDIM 换成非马尔可夫族,先估 x₀ 再跳,才绕开了这个限制。
组件阅读路线 · DDPM → DDIM
1. 加噪与去噪的基本机制#
1.1 DDPM & DDIM:核心逻辑#
DDPM#
前向过程:定义马尔可夫链 ,逐步加噪。由于高斯的可叠加性,可以直接写出任意步的边际分布 ,即 。
反向过程的推导:我们想要 ,但这个直接算不出来(需要对所有 积分)。不过如果已知 ,用贝叶斯公式可以精确算出后验:
由 和 线性组合得到, 只和 schedule 有关
网络的作用:实际采样时我们没有 ,所以用网络 预测噪声,通过前向公式反推 ,代入 公式得到均值。
采样:从高斯后验中采样 ,其中 。
为什么不能跳步:DDPM 把反向每一步建模为单个高斯 。这个近似成立的前提是相邻步噪声差异很小( 很小),此时真实的反向分布 确实接近高斯。但如果跳步(比如从 直接到 ),真实的 是对所有可能的 积分后的混合分布——高度多峰,一个高斯根本拟合不了。所以不是网络预测不准(网络在任意 上都能准确预测 ),而是单高斯的建模假设在大步长下崩了。
DDIM#
起点:和 DDPM 共享完全相同的前向边际分布 和同一个训练好的网络 。不需要重新训练。
核心思路:DDPM 的训练只依赖边际分布 ,不依赖联合分布的马尔可夫结构。所以可以构造其他反向过程,只要保持同样的边际。DDIM 提出一族带自由参数 的条件分布(详细推导见第 07 节):
确定性分量方差 + 随机分量方差 = ,总方差恒定,所以对任意 边际分布都和 DDPM 一致
的含义: 时退化为 DDPM; 时过程完全确定性,这就是 DDIM。
为什么能跳步:公式只涉及 和 两个累积系数,没有用到" 和 相邻"这个条件。可以定义任意子序列 (如从 1000 步里选 50 个),公式结构完全不变。同一个网络 本来就是按任意 训练的,所以在这些时间步上的预测仍然有效。
1.2 DDPM 的前向过程(加噪)#
直觉#
给干净图片 逐步加高斯噪声, 步后变成纯噪声 。每步加多少由 noise schedule 控制( 到 ,从小到大递增)。
💡 是怎么来的?
实验选择,非理论推导。 越大每步噪声越小、反向高斯近似越准,但采样越慢。后来 DDIM 解耦了训练步数和采样步数(训练仍用 ,采样可以只走 50 步)。再后来 Score-based SDE / Flow Matching 把离散时间步换成连续变量 ," 选多大"这个问题就彻底不存在了,采样时再选离散化步数即可。
一步步加噪的公式#
一步到位公式(最核心!)#
高斯分布的可加性——一步到位公式成立的数学基础#
两个独立高斯随机变量相加,结果还是高斯:
独立变量之和的均值、方差相加并非高斯独有;高斯的关键性质是独立线性组合仍为高斯。
没有这个性质,多步加噪没法合并成一步,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-ᾱ₂) · ε'
递推到任意 步。定义 ,:
从 和一个噪声 直接算出任意时间步的
何时足够接近 0#
。有限个小于 1 的数相乘并不必然接近零,需要 schedule 的累计噪声足够大。无限步极限也需该和趋于 。例如 1000 步、 从 线性增至 时,。
- :,纯净图像
- 小:,大部分原图 + 少量噪声
- 大:,大部分噪声
- :,纯噪声
预先算好存表#
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] 查表,不管 是 1 还是 999 计算量完全一样。
⚠️ 一步到位公式 ≠ 重参数化技巧
形式上都是 均值 标准差 ,但动机不同。VAE 的 是网络输出,重参数化是为了让梯度穿过采样操作。DDPM 的系数是预计算常数,梯度不需要穿过 的构造——这个公式的价值是计算效率(O(1) 得到任意步的 )。详见面试题 06。
1.3 DDPM 的反向过程(去噪)#
建模#
前向加噪固定已知,反向去噪才是要训练的。建模为高斯:。均值 由网络预测,方差 固定。
方差为什么固定不预测?#
反向过程方差 有一个理论上下界:。下面推导这两个界怎么来的,以及为什么它们差距很小。
下界 :已知 时的后验方差
从贝叶斯公式(后面详细推导)可以算出 是一个高斯分布。三个高斯的概率密度相乘时,指数部分合并,对 配方,方差项为:
精度(方差的倒数)= 来自 的精度 + 来自 的精度
通分化简:
其中用了
取倒数得到下界:
这是已知 时后验分布 的精确方差
上界 :完全不知道 时的方差
如果对 完全没有信息,反向一步的不确定性最大,等于前向一步加的噪声量 。这是方差的上界——"最坏情况"。
**结论:**比例因子 总是接近 1(相邻 差距很小),所以 ,上下界差距不到 1%,方差选哪个值影响都不大。走 1000 步时均值(方向)是关键,方差(步幅)影响可忽略。
但少步采样时(如 50 步 DDIM),每步变化大了,方差差异开始有影响。所以 Improved DDPM(2021)让网络预测插值系数 ,,在上下界之间学最优值。
预测 而不是 :一个参数化选择#
这是参数化方式的选择——知道 就能用公式算出 ,数学上完全等价,但优化更容易。
的推导链#
第一步:从前向公式反解
第二步:后验分布 的均值公式(从贝叶斯推导,下面详解):
和 的加权平均——一部分信息来自原始图像,一部分来自当前加噪图像
第三步:把第一步的 代入第二步,化简:
整个链条:网络预测 → 公式算 → 公式算 → 用 + 固定方差构建高斯 → 采样 。网络只输出一个东西,剩下全是公式。
贝叶斯推导: 怎么来的#
贝叶斯公式回顾:你想求 ,手上有 :
比如医院检测:
在 DDPM 里,所有概率都在"已知 "的背景下。条件于两个变量 并不特殊——就是"在已知这两件事的前提下"。可以把 看成一个整体,又回到条件于一个变量的情况。 是额外信息,让分布更确定。把 当共同背景:
右边三项全已知:
- :前向加噪一步(马尔可夫, 只依赖 ),
- :一步到位到
- :一步到位到
三个高斯代入,高斯乘高斯除高斯还是高斯,算出精确的均值 和方差 。但推理时不知道 (正是要生成的),所以用网络预测的 反解 估计值代入。
🔥 为什么预测噪声 而不是 或 ?
实验:DDPM 论文对比三种目标,-pred 最好。
直觉: 始终服从 ,不随 变化,学习目标均匀。预测 在 大时要从纯噪声猜原图,太难。
数学:预测 等价于学 score function (详见 SDE 统一框架)。
2. 训练目标与采样过程#
2.1 DDPM 的训练过程#
推导很复杂,但最终训练目标极其简洁:
预测噪声和真实噪声的 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 完整计算过程#
时间步 怎么注入#
问题: 只是一个标量整数(比如 500),但网络内部的特征是高维的(128/256/512 通道)。一个数字携带的信息太少,也没法直接和高维特征做加法。需要先把它"展开"成高维向量。
做法:Sinusoidal 编码——和 Transformer 位置编码完全一样,用 128 对不同频率的 sin/cos 把标量 展开成 256 维向量:
为什么要这么做?
- 维度匹配:网络特征是高维的,标量没法直接参与运算。展开成 256 维后才能和网络特征交互。
- 区分度:不同 编码后得到不同的向量。=500 和 =501 差一点,=500 和 =100 差很多
- 连续性:相邻时间步的编码向量距离近,差别大的距离远,符合去噪任务的性质——=500 和 =501 的去噪任务相似,编码也该相似。
和 Transformer 位置编码的关系:完全一样的公式,只是编码的对象不同——Transformer 编码的是 token 的位置("我是第几个词"),DDPM 编码的是时间步("现在加了多少噪声")。本质都是把标量展开成高维向量让网络使用。
编码后过全局 MLP 得到共享的 ,然后每层各有自己的线性投影层把 投影到该层的通道数:
# 算一次,所有层共享:
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 的投影层学到" 对低层特征的影响",Bottleneck 的学到" 对高层语义的影响"。
2.2 DDPM 的采样过程(生成)#
时加随机噪声 , 时
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
**为什么每步还要加噪声 ?**因为反向过程是一个随机过程(随机微分方程),需要随机性来保持分布的正确性。 通常取 或 。
采样就是反复调用同一个 U-Net 1000 次。每次约 50ms,总计 50 秒。这就是 DDIM 出现的动机。
3. DDIM 与跳步采样#
3.1 DDPM 为什么不能跳步?马尔可夫 + 单高斯近似#
DDPM 采样必须 逐步走,不能跳步。这是两个设计选择共同导致的:
马尔可夫结构(远因):前向和反向都设计成马尔可夫链——下一步只取决于当前状态。这让反向每步需要独立建模 ,自然地引导我们选择最简单的参数化形式。
单高斯近似(近因,直接原因):DDPM 把每步反向分布建模为单个高斯 。这个近似成立的前提是相邻步噪声差异很小( 很小),此时真实的 确实接近高斯。但如果跳步(比如从 直接到 ),真实的 是对所有可能 的加权混合——高度多峰,一个高斯根本拟合不了。
⚠️ "马尔可夫"本身并不必然导致慢
假设我们仍用马尔可夫结构,但把反向每步建模成更强的分布(如 mixture of Gaussians 或 normalizing flow),理论上可以拟合多峰分布,也就能跳更大的步。所以不能跳步的直接原因是单高斯近似在大步长下失效,马尔可夫只是让这个简单参数化变得自然的背景条件。
那为什么不用更强的分布?因为不划算——单高斯让 loss 简化为简单的 MSE,训练极其简洁;而 DDIM 证明了同一个网络换个采样公式就能跳步,代价几乎为零,没必要在每步建模上做复杂化。
3.2 DDIM:为什么更快,公式怎么来的#
DDIM 公式从哪来?为什么对任意 都成立?#
出发点:训练只依赖边际分布#
DDPM 训练时用的是一步到位公式 ,只涉及边际分布 。训练从来没有用过" 是从 一步加噪来的"这个马尔可夫结构。所以只要构造的反向过程保持同样的边际分布,训练好的网络就仍然适用——不需要重新训练。
DDIM 的构造:写一个含 的 ansatz#
DDIM 论文提出:给定 ,构造 如下:
, 是自由参数
这个公式不是"推导"出来的——它是一个猜测(ansatz),然后验证它确实保持边际分布一致。
验证:为什么边际分布一致?#
关键观察:公式中 是什么?从前向公式 移项,这就是 ——标准正态噪声。所以给定 ,公式变成:
和 都是独立的标准正态
验证均值和方差:
- 均值: ✓
- 方差:两个独立噪声方差相加 = ✓
所以 ——和 DDPM 的前向边际完全一致,对任意 都成立。 只是控制方差在确定性分量和随机分量之间的分配比例,总方差恒定。
💡 为什么这个验证就够了?
训练好的网络 学的是:给定 (服从 ),预测加进去的噪声 。只要采样过程中每一步产生的 仍然服从同样的 ,网络的预测就仍然有效。上面验证了 DDIM 产生的 和 DDPM 的边际分布完全相同,所以递推下去,每一步的 都在网络的训练分布上——网络不需要重新训练。
的两个极端#
- :退化为 DDPM 的后验方差,恢复 DDPM 随机采样
- :确定性分量承担全部方差,没有随机项——这就是 DDIM
实际采样时用 代替 #
验证时用的是真实的 ,但采样时我们没有 (正是要生成的)。用网络预测 代替 ,同时反推 代替 。核心操作分两步:
第一步:预测 #
第二步:从 出发走到任意目标步 #
(这是 的简化形式。)
本质:每步**"先跳回 估计,再走到目标时间步"**,打破了马尔可夫约束。公式只涉及 和 , 可以是任意时间步,因此可以跳步。
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 每步都在"预测 "并逐步精化。50 次纠正足够收敛。DDPM 每步只"去一点点噪声",必须走够 1000 步。
确定性采样#
DDPM 每步加随机噪声 ,同一个 生成不同图像。DDIM 在 时没有逐步随机项——在模型、条件、网格及确定性计算相同的前提下,同一个 生成同一张图像,潜在空间有语义结构,可做插值。
DDPM vs DDIM 对比#
| 特性 | DDPM | DDIM |
|---|---|---|
| 反向过程 | 马尔可夫(每步只看上一步) | 非马尔可夫(先预测 再跳任意步) |
| 是否随机 | 随机(每步加噪声 ) | 确定性() |
| 能否跳步 | 不能 | 能 |
| 典型步数 | 1000 | 50(质量接近 DDPM) |
| 同一噪声输入 | 每次不同图像 | 每次同一张图像 |
| 训练方式 | 完全相同——同一个模型,采样方式可以随时切换 |
4. 预测参数化与噪声日程#
4.1 ε-pred / x₀-pred / v-pred:三种预测目标#
网络的输入都是 ,但"预测什么"有三种选择。从前向公式出发:。
ε-pred(预测噪声)#
网络输出 。DDPM 原论文的选择。
优点: 永远服从 ,不管 多大多小,预测目标的分布都一样,学习目标均匀。
缺点: 很小时( 几乎没噪声), 对 的影响极小( 接近 0)。信噪比很低,学习效率差。
x₀-pred(预测原图)#
网络输出 。
优点: 小时很容易()。预测的 有直接视觉意义——采样中途可以看到"网络目前认为结果长什么样"。
缺点: 很大时从纯噪声直接预测原图,难度极大。不同 下难度差异很大。
v-pred(预测 velocity)#
"velocity" = 噪声方向和数据方向的加权差;如果把加噪看成从 到 的一条路径, 就是时间 处的切线方向(速度)
两端近似: 小时 ,所以 ; 大时反过来,。
关键性质: 的方差在任何 下恒为 1
系数 和 平方和恒为 1, 的尺度始终稳定
⚠️ 常见误解:v-pred 不是让两端都"变容易"
既然 小时 ,而 -pred 在 小时很困难,那 v-pred 不是也一样困难吗?
是的,v-pred 确实继承了两端的挑战——小 时要从接近干净的图检测微弱噪声,大 时要从纯噪声重建原图轮廓。它并没有消除这些难度。
v-pred 的优势是避免了两端的极端失效:
- -pred 小 时: 对 的贡献系数 ,就算预测错了, 的重建几乎不受影响——梯度信号接近消失,学了也没用
- -pred 大 时: 被噪声完全淹没,网络在做近乎"无中生有"的猜测
- v-pred:两端均为中等难度,没有"梯度消失"也没有"无中生有"——难度曲线更平
难度
高 │ ε-pred ↗↗(梯度渐消失) x₀-pred ↗↗(近乎猜测)
│
中 │ ─────────── v-pred ───────────────────────────────
│
低 │ x₀-pred(太简单) ε-pred(正常)
└──────────────────────────────────────────────────→ t
t 小(低噪声) t 大(高噪声)
缺点:概念上不如 -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 |
💡 三者可以互相转换
知道了 、,以及 中的任何一个,都能算出另外两个:
所以三种预测目标在数学上等价——差别只在训练时梯度加权方式不同,导致优化行为不同。
趋势:-pred 是经典默认选择,图像生成主流。v-pred 在视频生成(Stable Video Diffusion)和少步采样/蒸馏中越来越流行。Flow Matching(SD3、Flux)用向量场预测,和 v-pred 思路接近。-pred 和 -pred 基本没人单独用了。
4.2 Noise Schedule 的设计#
怎么选直接影响生成质量。
线性 schedule(DDPM 原版)#
,,中间线性插值。
问题:线性增长意味着后半段加噪太快, 在中间时间步急剧下降。很多时间步上 已经接近纯噪声,网络学不到东西——"已经全是噪声了,去噪也去不出什么"。
余弦 schedule(Improved DDPM,2021)#
按余弦函数下降,中间时间步衰减更缓慢:
是一个小常数,防止 在 接近 0 时太小
效果: 在中间时间步衰减更慢,保留更多有效训练时间步。质量明显提升,特别是低分辨率生成。
Scaled Linear(Stable Diffusion 用的)#
对 做线性插值而不是对 做线性插值。在隐空间操作时效果更好。
**面试知道这些就够了:**线性 schedule 是 DDPM 原版但不够好,余弦 schedule 是改进版,SD 用 scaled linear。核心思想是让 在中间时间步衰减不要太快,保留更多有效训练区间。
5. 理解检查与实现#
5.1 理解检查(6 道)#
面试题 01 扩散模型到底在学什么?
结论:学去噪——给定 预测噪声 (或等价地学 score function )。知道了噪声就能从加噪图像还原出干净图像。
训练时:往真实图片加噪声让网络预测。损失是简单的 MSE。
推理时:从纯噪声开始反复去噪,逐步得到干净图像。
面试题 02 为什么从噪声可以一步步还原图像?
数学:前向过程把 逐步变成 。学到反向(score function)就能从噪声走回数据分布。
直觉:想象山谷(数据高密度区域)。加噪 = 把石头从山谷滚到平地。score function = "指向山谷的方向"。从平地沿 score 走就能走回山谷。
面试题 03 为什么预测噪声不预测原图?
实验:-pred 最好。直觉: 始终 ,学习均匀。-pred 在 大时太难。数学:等价于 score matching。
追问准备:v-pred = ,是两者平衡。视频生成和少步采样在用。
面试题 04 DDIM 为什么更快?
根本原因:DDPM 是马尔可夫链不能跳步。DDIM 先预测 ,然后用一步到位公式从 加噪到任意目标步,步长自由。1000 步压缩到 50 步质量损失不大。
追问:DDIM 还有什么好处?确定性采样——同一噪声输入永远生成同一图,潜在空间有语义结构。
面试题 05 扩散模型比 GAN 好在哪里?
训练稳定:不需要平衡生成器和判别器,简单 MSE 回归。不模式崩塌。
多样性好:有明确概率建模,理论上覆盖数据分布所有模式。
有似然估计:可以计算近似的 log-likelihood。GAN 没有。
代价:采样慢(多步迭代 vs GAN 一步生成)。
面试题 06 重参数化技巧是什么?DDPM 里的一步到位公式和它什么关系?
重参数化技巧:把"从 采样"改写为 ,。让采样变成可导的确定性计算,梯度能穿过去。经典应用是 VAE—— 是 Encoder 网络输出,不做重参数化 Encoder 就没法训练。
DDPM 的一步到位公式:,数学形式一样,但系数是预计算的固定常数,不是网络输出。训练时梯度不需要穿过 的构造。这个公式的真正价值是计算效率——O(1) 得到任意时间步的 ,不用跑马尔可夫链。
什么时候真的需要重参数化:系数是可学习参数、梯度要穿过采样操作时(VAE 训练)。不需要:系数是固定常数(DDPM 前向加噪)、采样在计算图最前端(GAN 的 )、或不做反向传播(推理时)。
5.2 实现练习:PyTorch 手写实现#
代码题 1:|r - x²| < ε 的优化问题#
面试经典题:给定目标值 ,用梯度下降优化 使得 (等价于求 )。考察对 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() 自动求导 (链式法则) (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 索引 (3) .view(-1,1,1,1) 是为了广播到 [B,C,H,W] (4) 训练只预测 ,推理时用预测的 反推均值再加噪 (5) 最后一步不加噪声( 就是最终结果)。
代码题 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 训练:考察预计算 、一步到位加噪 x_t = √ᾱ·x0 + √(1-ᾱ)·ε、MSE 损失。
FM velocity:考察 v = x1 - x0、线性插值 x_t = (1-t)x0 + t·x1、和 DDPM 的对比。
扩散模型基础:DDPM → DDIM
多模态算法面试准备系列
原始资料:DDPM、DDIM。DDIM 包含不同噪声强度的采样族,不能把整个算法族都视为确定性。
延伸阅读#
- Stable Diffusion 的完整流程:文本、隐变量和去噪模型怎样组成一次生成流程?
- 扩散模型蒸馏:减少采样步数和减少每步前向次数分别需要什么方法?