📑 本页目录(点开跳转)
02 · 加噪:一条不用学的公式
⏱ 32 分钟 | ⭐ 前向过程零个可训练参数,而它决定了训练能不能并行
🎯 一句话
扩散有两条方向相反的路,只有反向那条要学。 这一页讲前向:加噪是一条写死的公式,零个可训练参数 —— 而它最要紧的性质是能一步跳到任意时刻,训练能并行全靠这一条。
🌫️ 一、前向过程:一条不用学的公式
从原图 $x_0$ 出发,每一步往里掺一点高斯噪声:
$$x_t = \sqrt{1-\beta_t}\; x_{t-1} + \sqrt{\beta_t}\; \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, I)$$
人话:把上一步的图按 $\sqrt{1-\beta_t}$ 缩小一丁点,再掺进 $\sqrt{\beta_t}$ 那么多的纯噪声。$\beta_t$ 是个很小的数(DDPM 原文从 0.0001 线性涨到 0.02),所以单看一步几乎什么都没变 —— 变化是 $T$ 步(典型 $T=1000$)累出来的。
⭐ 三件事现在就说清楚:
| 事实 | 说明 |
|---|---|
| 这里没有一个参数 | $\beta_1 \dots \beta_T$ 是事先定好的一张表,从头到尾不参与训练,也不需要梯度 |
| 它是一条马尔可夫链 | $x_t$ 只依赖 $x_{t-1}$,跟更早的怎么来的无关 |
| 那两个系数不是随手挑的 | 它们凑成一对,让方差在整条链上不变 —— 见下一小节 |
🧪 为什么偏偏是 √(1−β) 和 √β
假设原图每个像素的方差是 1。代进公式:$\mathrm{Var}(x_t) = (1-\beta_t)\cdot 1 + \beta_t \cdot 1 = 1$。加完噪声,方差还是 1。 这就是所谓的 variance preserving(方差保持):整条链上每一步的数值范围都差不多,网络不会在 $t=10$ 时看到 $\pm 1$ 的输入、在 $t=900$ 时看到 $\pm 30$ 的输入。
跑一下就看得见:
# √(1-β_t) 这个系数不是随手挑的:它让方差在整条链上保持不变
import torch
torch.manual_seed(0)
T = 1000
abar = torch.cumprod(1 - torch.linspace(1e-4, 0.02, T), dim=0)
for name, x0 in [("方差=1 的数据", torch.randn(200000)),
("方差≠1 的数据", torch.randn(200000) * 3)]:
stds = []
for t in [1, 250, 500, 750, 1000]:
a = abar[t - 1]
xt = a.sqrt() * x0 + (1 - a).sqrt() * torch.randn_like(x0)
stds.append(f"{xt.std():.3f}")
print(f"{name}(std={x0.std():.2f})→ 各步的 std:{' '.join(stds)}")
真实输出:
方差=1 的数据(std=1.00)→ 各步的 std:1.000 0.998 0.999 1.000 0.999
方差≠1 的数据(std=3.00)→ 各步的 std:3.003 2.283 1.277 1.016 1.001
⚠️ 看第二行:数据方差不是 1 的时候,输入尺度会从 3.00 一路漂到 1.00 —— 同一个网络在不同 $t$ 上看到的量级差三倍。
⭐ 这就是「图像要先归一化到 −1 到 1」的真正理由。 不是玄学预处理,是方差保持这条性质要求输入方差在 1 附近才成立。
⚡ 二、闭式采样:训练能并行的全部原因
记 $\alpha_t = 1-\beta_t$,$\bar\alpha_t = \alpha_1 \alpha_2 \cdots \alpha_t$。那么任意时刻都可以一步到位:
$$x_t = \sqrt{\bar\alpha_t}\; x_0 + \sqrt{1-\bar\alpha_t}\; \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, I)$$
人话:想知道第 $t$ 步长什么样,不用真的走 $t$ 步 —— 把原图乘一个数、把一份噪声乘另一个数,加起来就完了。$\sqrt{\bar\alpha_t}$ 是「原图还剩多少」,$\sqrt{1-\bar\alpha_t}$ 是「噪声占了多少」,两者平方和恒等于 1。
📐 为什么能合并(想看再点)
把两步展开:
$$x_2 = \sqrt{\alpha_2}\left(\sqrt{\alpha_1} x_0 + \sqrt{1-\alpha_1}\,\varepsilon_1\right) + \sqrt{1-\alpha_2}\,\varepsilon_2$$
$$= \sqrt{\alpha_1\alpha_2}\, x_0 + \underbrace{\sqrt{\alpha_2(1-\alpha_1)}\,\varepsilon_1 + \sqrt{1-\alpha_2}\,\varepsilon_2}_{\text{两个独立高斯相加}}$$
⭐ 关键一步:两个独立的零均值高斯相加,结果还是高斯,而且方差直接相加:
$$\alpha_2(1-\alpha_1) + (1-\alpha_2) = 1 - \alpha_1\alpha_2$$
所以那一整坨等价于一个方差为 $1-\alpha_1\alpha_2$ 的高斯。归纳下去就得到 $\bar\alpha_t = \prod_{s\le t}\alpha_s$。
⚠️ 合并成立只因为噪声与数据无关、而且每步都是线性的。反向那条路两个条件都不满足,所以合并不了(见第五节)。
🔬 亲手验证一下
两种算法应该给出同一个分布 —— 迭代法每步重采一次噪声、走 $t$ 次;闭式法只采一次噪声、算一次乘加:
# 验证:一步步加 T 次噪 vs 闭式一步跳到 t —— 两种算法得到同一个分布
import time
import torch
torch.manual_seed(0)
T = 1000
betas = torch.linspace(1e-4, 0.02, T) # DDPM 原论文的 linear 调度
alphas = 1.0 - betas
abar = torch.cumprod(alphas, dim=0) # ᾱ_t = α_1 α_2 … α_t
x0 = torch.full((20000,), 3.0) # 2 万份「同一张图」,这里把图简化成一个数
def iterative(x0, t): # 走法 A:老老实实迭代 t 次
x = x0.clone()
for i in range(t):
eps = torch.randn_like(x) # ⭐ 每一步都要重新采一次噪声
x = alphas[i].sqrt() * x + betas[i].sqrt() * eps
return x
def closed_form(x0, t): # 走法 B:闭式,一步到位
eps = torch.randn_like(x0) # ⭐ 只采【一次】噪声
return abar[t - 1].sqrt() * x0 + (1 - abar[t - 1]).sqrt() * eps
for t in [10, 100, 500, 1000]:
s = time.perf_counter(); a = iterative(x0, t); ta = time.perf_counter() - s
s = time.perf_counter(); b = closed_form(x0, t); tb = time.perf_counter() - s
print(f"t={t:4d} | 迭代 mean={a.mean():+.4f} std={a.std():.4f} ({ta*1000:6.1f} ms)"
f" | 闭式 mean={b.mean():+.4f} std={b.std():.4f} ({tb*1000:5.2f} ms)"
f" | 理论 mean={(abar[t-1].sqrt()*3).item():+.4f} std={(1-abar[t-1]).sqrt().item():.4f}")
真实输出:
t= 10 | 迭代 mean=+2.9967 std=0.0435 ( 3.1 ms) | 闭式 mean=+2.9971 std=0.0438 ( 0.22 ms) | 理论 mean=+2.9972 std=0.0435
t= 100 | 迭代 mean=+2.8393 std=0.3217 ( 15.4 ms) | 闭式 mean=+2.8381 std=0.3187 ( 0.29 ms) | 理论 mean=+2.8413 std=0.3209
t= 500 | 迭代 mean=+0.8287 std=0.9592 ( 90.2 ms) | 闭式 mean=+0.8384 std=0.9678 ( 0.18 ms) | 理论 mean=+0.8410 std=0.9599
t=1000 | 迭代 mean=+0.0265 std=1.0017 ( 182.5 ms) | 闭式 mean=+0.0140 std=0.9954 ( 0.29 ms) | 理论 mean=+0.0191 std=1.0000
三列的均值和标准差逐行对得上,闭式解不是近似,是恒等。⚠️ 毫秒数每次跑都不一样,要看的是数量级:闭式那一列不随 $t$ 增长,迭代那一列线性涨。
⭐⭐ 这一条是训练能并行的全部原因。 训练时 $t$ 是随机抽的:一个 batch 里 512 张图,每张各抽各的 $t$,全部一次乘加算完。 要是没有闭式解,你得为 batch 里每一张图各跑一遍长达几百步的循环 —— 训练成本会直接乘上几百倍。
🌑 三、终点 x_T:和原图已经没关系了
一直加下去,最后剩什么?直接量 $x_0$ 和 $x_t$ 的相关系数:
# x_T 还剩多少「原图的影子」:直接量 x_0 和 x_t 的相关系数
import torch
torch.manual_seed(1)
T = 1000
abar = torch.cumprod(1 - torch.linspace(1e-4, 0.02, T), dim=0)
x0 = torch.rand(200000) * 2 - 1 # 20 万个像素值,归一化到 [-1, 1]
for t in [1, 100, 300, 500, 700, 900, 1000]:
a = abar[t - 1]
xt = a.sqrt() * x0 + (1 - a).sqrt() * torch.randn_like(x0)
corr = torch.corrcoef(torch.stack([x0, xt]))[0, 1].item()
print(f"t={t:4d} sqrt(abar_t)={a.sqrt():.4f} corr(x0, x_t) = {corr:+.4f}")
真实输出:
t= 1 sqrt(abar_t)=0.9999 corr(x0, x_t) = +0.9998
t= 100 sqrt(abar_t)=0.9471 corr(x0, x_t) = +0.8611
t= 300 sqrt(abar_t)=0.6296 corr(x0, x_t) = +0.4215
t= 500 sqrt(abar_t)=0.2803 corr(x0, x_t) = +0.1696
t= 700 sqrt(abar_t)=0.0835 corr(x0, x_t) = +0.0462
t= 900 sqrt(abar_t)=0.0166 corr(x0, x_t) = +0.0126
t=1000 sqrt(abar_t)=0.0064 corr(x0, x_t) = +0.0031
到 $t=1000$,相关系数只剩 0.0031 —— $x_T$ 已经和原图基本无关,就是一团标准高斯噪声。
⭐ 这才是生成能启动的原因。 如果 $x_T$ 还带着原图的影子,你就没法「凭空开始」—— 得先有一张图才能生成。 正因为终点和输入无关,采样时才可以直接
torch.randn(...)起手。
⚠️ 但「几乎无关」不是「完全无关」。 看最后一行:$\sqrt{\bar\alpha_T} = 0.0064$,不是 0。也就是说训练时模型在最后一步见到的输入里,还残留着原图约 0.6% 的信息 —— 其中最显眼的是整幅画面的平均亮度。而采样时你喂进去的是纯噪声,那 0.6% 是空的。这个「训练时有、推理时没有」的口径不一致叫 signal leak,症状是模型很难生成整体极亮或极暗的画面(结果的平均亮度总往中灰靠)。修法是把调度改成终点严格 $\bar\alpha_T = 0$,即 zero terminal SNR。
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| 03 · 训练目标为什么是预测噪声 | 闭式采样是训练循环的第 4 步 —— 那一页讲拿它拼出 $x_t$ 之后要让网络做什么 |
| 02b · 调度表与反向过程 | β 这张表具体该长什么样,以及反向那一半为什么不能照抄前向 |
| 机器学习的数学原理 01b · KL 散度 | 「两个独立高斯相加、方差直接相加」这类性质,是后面变分下界推导的地基 |
| NumPy 与向量化思维 03 · 广播的三条规则 | 闭式采样要对一整个 batch 同时算不同的 $t$,靠的就是广播 |
✅ 检查点
- 前向过程里有多少个可训练参数?$\beta_t$ 是学出来的吗?
- 为什么系数写成 $\sqrt{1-\beta_t}$ 和 $\sqrt{\beta_t}$ 而不是别的?这条性质反过来对输入数据提了什么要求?
- 闭式采样式 $x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\varepsilon$ 是精确的还是近似的?它成立靠的是哪个数学事实?
- 如果没有闭式采样,训练要多付出什么代价?
- $t=1000$ 时 $x_0$ 和 $x_t$ 的相关系数是多少?这个数字为什么决定了「生成能不能凭空启动」?
👀 答案
- 零个。 $\beta_t$ 是一张事先定好的表(DDPM 里从 0.0001 线性涨到 0.02),不参与训练、不需要梯度。整个前向过程是写死的公式。
- 因为 $(1-\beta_t) + \beta_t = 1$,所以方差保持不变(variance preserving):输入方差是 1,加噪后还是 1。代码里量到方差为 1 的数据各步 std 是
1.000 0.998 0.999 1.000 0.999。要求:输入数据方差必须在 1 附近 —— 这就是图像要归一化到 −1 至 1 的理由;方差为 3 的数据 std 会从 3.00 一路漂到 1.00。 - 精确的,是恒等不是近似。 靠的是「两个独立零均值高斯相加仍是高斯、且方差直接相加」:$\alpha_2(1-\alpha_1) + (1-\alpha_2) = 1-\alpha_1\alpha_2$。
- 一个 batch 里每张图抽的 $t$ 都不同,没有闭式解就得为每张图各跑一遍几百步的循环,训练成本乘上几百倍。代码里 $t=1000$ 时迭代法 182.5 ms、闭式法 0.29 ms,而且闭式那一列不随 $t$ 增长。
- +0.0031(此时 $\sqrt{\bar\alpha_T} = 0.0064$)。因为终点几乎和输入无关,采样时才能直接从
torch.randn(...)起手 —— 否则你得先有一张图才能生成。
🛑 可以停在这里
读到这里,扩散的前向那一半你已经完整了:它是一条写死的公式,而闭式采样让训练能并行。
⚠️ 什么时候看下一页:你想知道 β 那张表具体该怎么定,或者好奇「既然前向能一步跳,反向为什么不能」。
⚡ 走神救援
先记住这几件事
- 前向过程按固定规则加噪;反向过程才需要学习数据的分布。
- 闭式采样能从干净样本直接得到任意时刻的噪声样本,训练不必逐步加噪。
- 核对噪声调度、输入尺度与采样时刻,分清信号系数和噪声系数。
下一节 👉 02b-调度表与反向过程.md