跳轉到

2021:噪聲中重建世界——擴散模型的 SDE 探案#

「先把蒙娜麗莎撕成噪聲,再把噪聲拼回蒙娜麗莎。」——生成模型審訊室的白板標語。

案件檔案 內容
案號 SGM-2021-005
案發時間 2021 年 ICLR/NeurIPS 立案
案發地點 影像、語音與科學數據的生成現場
報案人 被 GAN 不穩定折磨的生成模型工程師
偵探 Yang Song、Jascha Sohl-Dickstein、Diederik Kingma 等
失蹤者 被加噪前向過程毀掉的數據分佈
歸來者 Anderson(1982)逆時 SDE
兇器(實為鑰匙) 分數網路 \(s_\theta\) 與去噪分數匹配損失
狀態 已結案,生成式 AI 典範轉移

案發現場#

2021 年之前,生成模型分贓為兩派,互不服氣。

一派是 SMLD(分數匹配 Langevin 動力學):用多個噪聲尺度估計分數 \(\nabla_x \log p(x)\) ,再用 Langevin 採樣一格一格爬回數據流形。

另一派是 DDPM(去噪擴散概率模型):設定一條前向加噪馬可夫鏈,再學一條反向去噪鏈,一步步把噪聲洗掉。

兩派在樣本品質上都打敗了 GAN,卻各有怪癖:SMLD 的噪聲尺度要手工排,DDPM 的離散步數要上千步,理論上也看不出它們是親戚。

案發現場的證物有兩件:SMLD 的退火 Langevin 更新,與 DDPM 的反向高斯核。

偵探 Song 盯著它們看了很久,突然在筆記本上寫道:「它們都是同一個 SDE 的不同離散化。」

與此同時,檔案室裡一份 1982 年的舊卷宗被人調出——Anderson 的逆時擴散理論。

Anderson 早在四十年前就證明:只要知道分數 \(\nabla_x \log p_t(x)\) ,任何前向擴散都可以倒著播回去。

失蹤多年的祖師爺,就這樣被一樁生成模型的案子請回了現場。

偵查過程#

偵探的第一步是統一前向過程:把一切加噪寫成同一個 Itô SDE。

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

其中 \(x = x(t)\) 是狀態, \(f\) 是漂移係數, \(g\) 是擴散係數, \(w\) 是標準 Wiener 過程,而 \(t \in [0, T]\) 從數據走向噪聲。

SMLD 對應方差爆炸(VE)版本:

\[ dx = \sqrt{d[\sigma^2(t)] / dt} dw \]

DDPM 則對應方差保持(VP)版本:

\[ dx = -\frac{1}{2} \beta(t) x dt + \sqrt{\beta(t)} dw \]

其中 \(\sigma(t)\) 與 \(\beta(t)\) 分別是噪聲表,兩者在連續極限下只是 \(f\) 與 \(g\) 的不同選法。

下表是兩大門派的指紋比對:

門派 離散前向 連續極限( \(f\) , \(g\) ) 採樣方式
SMLD 多尺度加噪 \(x + \sigma_i z\) \(f = 0\) , \(g =\) VE 係數 退火 Langevin
DDPM \(x_t = \sqrt{\alpha_t} x_{t-1} + \sqrt{1 - \alpha_t} z\) \(f = -\beta x / 2\) , \(g = \sqrt{\beta}\) 反向高斯鏈
統一 SDE 同一條 \(dx = f dt + g dw\) 同左 逆時 SDE+校正子

第二步是請出 Anderson:一旦前向是 SDE,逆向必為 SDE。

逆時 SDE 的形式如下,其中 \(\bar{w}\) 是逆時 Wiener 過程, \(s_\theta\) 是學到的分數:

\[ dx = [f - g^2 s_\theta] dt + g d\bar{w} \]

嚴格而言,真正的逆時漂移應為 \(f(x,t) - g(t)^2 \nabla_x \log p_t(x)\) ,偵探用神經網路 \(s_\theta(x,t) \approx \nabla_x \log p_t(x)\) 代入。

這條公式是本案的定罪核心:生成,就是把前向 SDE 倒著解出來。

Anderson 在 1982 年寫下它時,世上還沒有深度學習能估計高維分數;四十年後,分數匹配補上了最後一塊拼圖。

第三步是訓練分數網路:去噪分數匹配(DSM)。

對連續時間加權,損失函數為:

\[ L(\theta) = \mathbb{E}_{t, x_0, x_t} [\lambda(t) \|s_\theta(x_t, t) - \nabla_{x_t} \log p_{0t}(x_t | x_0)\|^2] \]

其中 \(x_0 \sim p_{\mathrm{data}}\) , \(x_t \sim p_{0t}(\cdot | x_0)\) 是前向轉移核, \(\lambda(t) > 0\) 是時間權重。

由於前向核多為高斯,目標 \(\nabla \log p_{0t}\) 有解析式,訓練完全不需要知道真實分數,這正是 DSM 的狡猾之處。

偵探們還證明了概率流 ODE 的存在:去掉逆時 SDE 的布朗項,可得一條與之同邊際的確定性流,

\[ dx = [f - \frac{1}{2} g^2 s_\theta] dt \]

它可以用常微分方程求解器快速採樣,並能精確計算似然。

第四步是收網比對:統一框架下的新戰績。

任務 舊方法步數 SDE 框架步數/求解器 品質變化
CIFAR-10 生成 DDPM \(1000\) 步 逆時 SDE+PC 採樣 FID 大幅下降
高分辨率影像 SMLD 多尺度手調 VE/VP+自適應校正 紋理更銳利
似然計算 變分下界鬆弛 概率流 ODE 精確積分 可比自迴歸模型

PC(Predictor–Corrector)採樣是點睛之筆:每用逆時 SDE 預測一步,就用 Langevin 校正一步,SMLD 與 DDPM 在此握手言和。

結案報告#

2021 年之後,教科書改寫了生成模型的章節標題:不再是「SMLD 與 DDPM」,而是「基於 SDE 的生成模型」。

本案的遺產有三件。

第一,理論統一:兩個看似無關的模型被證明是同一前向 SDE 的不同離散化,爭論多年的門派之爭一夜終結。

第二,祖師爺歸位:Anderson(1982)的逆時公式從控制與濾波的冷宮走出,成為 Stable Diffusion、語音合成與分子生成的數學地基。

第三,工具箱爆炸:逆時 SDE、概率流 ODE、PC 採樣、DSM 損失構成四件套,後續的 DDIM、Score Distillation、流匹配全是它們的變體。

探長 Song 在結案陳詞中寫道:「我們沒有發明新的噪聲,我們只是發現所有的噪聲本來就是同一條河。」

此案之後,隨機微積分從華爾街與實驗室走進了每一台手機的相機:你按下的每一次「AI 修圖」,都是一次逆時擴散。

而 Anderson 的舊卷宗,則被複印無數份,掛在每個生成實驗室的牆上。

證據與工具#

核心證物一:前向 SDE \(dx = f dt + g dw\) ,VE 與 VP 只是 \(f\) 與 \(g\) 的不同取值。

核心證物二:逆時 SDE \(dx = [f - g^2 s_\theta] dt + g d\bar{w}\) ,Anderson 1982 的歸來,分數即鑰匙。

核心證物三:DSM 損失 \(L(\theta)\) 與概率流 ODE,前者負責訓練,後者負責快速採樣與似然計算。

工具 用途 出處
Anderson 逆時公式 從分數重建逆向漂移 Anderson 1982
SMLD 多尺度分數估計先驅 Song–Ermon 2019
DDPM 離散去噪鏈先驅 Ho 等 2020
去噪分數匹配 無需真實分數的訓練目標 Vincent 2011,Song 等 2021
PC 採樣與概率流 高品質與快速生成的雙引擎 Song 等 2021

辦案守則:先寫出前向 SDE,再請 Anderson 倒帶;分數學不準,一切免談;採樣時記得預測加校正,確定與隨機兩條腿走路。

補充:程式實作#

對應程式#

本節對應程式為 2021-score_diffusion_1d.py ,以一維雙峰分佈示範去噪分數匹配加退火 Langevin 生成。

理論呼應#

本文前向過程為 \(dx = f(x,t)dt + g(t)dw\) 形式,逆時生成靠 \(dx = [f - g^2 s_{\theta}]dt + g d\bar{w}\) 公式。 程式以 RBF 分數網路 \(s(x) = \Phi(x)w\) 逼近真實分數,呼應去噪分數匹配損失 \(L(\theta)\) 精神。 由大到小的噪聲位準退火正是 SMLD 與 DDPM 在統一 SDE 下的握手,取樣器即簡化版 PC 流程。 雙峰比例與峰谷比檢驗相當於一維版的 FID 量測,確認生成分佈確實回到資料流形。

執行方式#

在 隨機微積分 目錄下執行 python3 _code/2021-score_diffusion_1d.py 即可重現結果,只需 numpy。

實測輸出#

frac_pos = 0.4842 (err 0.0158, tol 0.1)
mean_neg = -1.9938 (target -2, tol 0.2)
mean_pos = 1.9970 (target +2, tol 0.2)
peak_l = 544, peak_r = 530, valley = 1, pv_ratio = 530.000 (tol > 2)
VERIFY frac_pos=0.4842 mean_neg=-1.9938 mean_pos=1.9970 pv=530.000

雙峰比例誤差僅 \(0.0158\) 且峰谷比高達 \(530.000\) ,兩峰均值亦緊貼 \(\pm 2\) 目標。

讀者實驗#

將每位準步數 STEPS = 100 改為 STEPS = 30 後重跑,觀察峰谷比 \(530.000\) 是否依然遠高於門檻。

完整程式如下:

"""2021 一維雙峰擴散生成:去噪分數匹配 + 多退火 Langevin。
對應 wiki:Song–Ermon (2019) NCSN 去噪分數匹配、Song et al. (2021)
  Score-based SDE / 退火 Langevin 動力學。
資料:0.5 N(-2, 0.4²) + 0.5 N(+2, 0.4²)。
模型:score s(x) = Φ(x) w,Φ 為 K=40 RBF 特徵
  φ_j = exp(-(x-c_j)²/(2h²)),c 均勻分佈 [-4,4],h=0.3,
  外加線性項 x 與常數項 1(共 K+2 維)以修正 RBF 在尾部→0、
  導致 Langevin 缺乏內拉力而樣本外偏的問題;
  以 numpy 手刻去噪分數匹配(DSM)閉式 ridge 解學各噪聲位準的 score:
  min_w E‖F(x̃)w + (x̃-x)/σ²‖²,x̃ = x + σε(RBF 懲罰 1.0,線性/常數 0.1)。
採樣:噪聲位準 [1.0, 0.7, 0.4, 0.2, 0.1] 由大到小退火,
  每位準 Langevin 100 步,步長 α=0.1σ²(score 截幅 ±20,狀態截幅 ±5),5000 點。
驗證:雙峰比例 |P(x>0)-0.5| < 0.1;兩峰均值 ≈ ±2 誤差 < 0.2;
  直方圖峰谷比 > 2(峰窗 ±[1.25,2.75] 對中央谷)。
只用 numpy,固定種子,不畫圖。
"""
import numpy as np

rng = np.random.default_rng(5)

# ---- 資料 ----
N_TRAIN = 10000
SIG_DATA = 0.4
comp = (rng.random(N_TRAIN) < 0.5).astype(float)  # 0 -> -2, 1 -> +2
x_data = np.where(comp < 0.5, -2.0, 2.0) + SIG_DATA * rng.standard_normal(N_TRAIN)

# ---- RBF + 線性/常數 ----
K = 40
centers = np.linspace(-4.0, 4.0, K)
h = 0.3


def feat_mat(x):
    d = x[:, None] - centers[None, :]
    Phi = np.exp(-0.5 * (d / h) ** 2)
    return np.column_stack([Phi, x, np.ones_like(x)])


# ---- 各噪聲位準 DSM ridge ----
sigmas = [1.0, 0.7, 0.4, 0.2, 0.1]
LAM = 1.0
Ws = []
for sg in sigmas:
    eps = rng.standard_normal(N_TRAIN)
    xt = x_data + sg * eps
    F = feat_mat(xt)  # (N, K+2)
    tgt = -eps / sg  # -(x̃-x)/σ²
    P = np.eye(K + 2) * LAM
    P[K, K] = 0.1
    P[K + 1, K + 1] = 0.1
    w = np.linalg.solve(F.T @ F + P, F.T @ tgt)
    Ws.append(w)

# ---- 退火 Langevin 採樣 ----
M_S = 5000
x = 2.0 * rng.standard_normal(M_S)  # N(0, 4) 初始化
STEPS = 100
for sg, w in zip(sigmas, Ws):
    alpha = 0.1 * sg ** 2
    sqa = np.sqrt(alpha)
    for _ in range(STEPS):
        s = feat_mat(x) @ w
        s = np.clip(s, -20.0, 20.0)
        x = x + 0.5 * alpha * s + sqa * rng.standard_normal(M_S)
        x = np.clip(x, -5.0, 5.0)

frac_pos = float(np.mean(x > 0))
ratio_err = abs(frac_pos - 0.5)
pos = x[x > 0]
neg = x[x <= 0]
mean_pos = float(np.mean(pos)) if pos.size else np.nan
mean_neg = float(np.mean(neg)) if neg.size else np.nan

# 峰谷比:32 bins 於 [-4, 4]
counts, edges = np.histogram(x, bins=32, range=(-4.0, 4.0))
mid = (edges[:-1] + edges[1:]) / 2
peak_l = float(np.max(counts[(mid >= -2.75) & (mid <= -1.25)]))
peak_r = float(np.max(counts[(mid >= 1.25) & (mid <= 2.75)]))
valley = float(counts[np.argmin(np.abs(mid - 0.0))])
pv_ratio = float(min(peak_l, peak_r) / max(valley, 1.0))

print(f"frac_pos = {frac_pos:.4f} (err {ratio_err:.4f}, tol 0.1)")
print(f"mean_neg = {mean_neg:.4f} (target -2, tol 0.2)")
print(f"mean_pos = {mean_pos:.4f} (target +2, tol 0.2)")
print(f"peak_l = {peak_l:.0f}, peak_r = {peak_r:.0f}, valley = {valley:.0f}, pv_ratio = {pv_ratio:.3f} (tol > 2)")
print(f"VERIFY frac_pos={frac_pos:.4f} mean_neg={mean_neg:.4f} mean_pos={mean_pos:.4f} pv={pv_ratio:.3f}")
assert ratio_err < 0.1, f"雙峰比例偏差過大: {frac_pos}"
assert abs(mean_pos - 2.0) < 0.2, f"右峰均值偏差: {mean_pos}"
assert abs(mean_neg + 2.0) < 0.2, f"左峰均值偏差: {mean_neg}"
assert pv_ratio > 2.0, f"峰谷比不足: {pv_ratio}"