7.3 擴散模型的數學直覺:DDPM 與 Score-based Models(2015-2021)#
一、起點:非平衡熱力學(Sohl-Dickstein et al., 2015)#
2015 年,Sohl-Dickstein、Weiss、Maheswaranathan & Ganguli 在 ICML 發表 Deep Unsupervised Learning using Nonequilibrium Thermodynamics。這篇論文的標題聽起來玄妙,但它的核心想法極為簡單,而且簡單到令人懷疑為何要寫成論文的形式:
把生成過程想像成一個物理系統的「反向」:先把資料慢慢加熱到純粹的雜訊,再學會把這個非平衡過程倒轉回去。
1.1 物理直覺#
擴散模型的名字來自物理中的擴散(diffusion)現象:一滴墨水在水中會擴散開來,最後整杯水變成均勻的墨色。這個過程有兩個關鍵特徵:
- 不可逆的趨勢:自然界總是朝著更大的熵前進。
- 但統計上可逆:如果我們非常仔細地記錄墨水粒子的每一步隨機運動,我們其實可以倒推回去。熱力學的時間反演是可能的,只是概率極低。
Sohl-Dickstein et al. 的洞見是:雖然物理過程不可逆,但我們可以用一個可學的網路去近似那個逆過程。既然逆過程只是「逐步把雜訊從影像裡挑出來」,而「從雜訊裡挑出物體」恰好是第 2.1 節去噪問題的形式化版本。
1.2 兩個馬可夫鏈#
整個框架由兩個馬可夫鏈定義:
- 前向(forward / diffusion)過程 \(q\) :從真實資料出發,逐步加入高斯雜訊,最終變成純高斯雜訊。這是已知的、固定的,不需要學。
- 反向(reverse / generative)過程 \(p_\theta\) :從純高斯雜訊出發,逐步去除雜訊,最終變回影像。這是需要學的。
flowchart LR
subgraph F["前向過程 q(固定、不學)"]
direction LR
X0["x0 真實影像"] --> X1["x1 加一點雜訊"] --> X2["x2 加更多雜訊"] --> XT["xT 純高斯雜訊"]
end
subgraph P["反向過程 p_theta(要學)"]
direction RL
XT2["xT 純高斯雜訊"] --> XT1["xT-1"] --> XT3["xT-2"] --> X0b["x0 重建的影像"]
end
F -.->|"逐步對應"| P這個「上圖/下圖鏡像對稱」是擴散模型的全部美麗之處:上圖不需要學,下圖只需要學;而下圖的每一步都是「給定帶噪影像,預測雜訊」——一個純粹的回歸問題。
1.3 前向過程的定義#
前向鏈定義為逐步加高斯雜訊的馬可夫鏈:
其中 \(\beta_t \in (0,1)\) 是第 \(t\) 步的噪訊強度(或稱 variance schedule)。注意這個形式的三個關鍵設計:
- ** \(\sqrt{1-\beta_t}\) 的縮放:加雜訊的同時把訊號稍微縮小,使得總方差不變**(這在 2.2 節會被推導出來)。
- ** \(\beta_t I\) 對角協方差**:各像素獨立加噪。
- ** \(\beta_t\) 隨 \(t\) 遞增**:早期步驟加的雜訊少( \(\beta_t \approx 10^{-4}\) ),晚期步驟加得多( \(\beta_T \approx 0.02\) )。這對應物理中「溫度隨時間升高」。
原始論文使用的 schedule 是線性的: \(\beta_1 = 10^{-4}\) 到 \(\beta_T = 0.02\) , \(T = 1000\) 。
二、DDPM(Dhariwal & Nichol, 2021):完整的數學#
DDPM 論文 Diffusion Models Beat GANs on Image Synthesis 是擴散模型的轉折點。它的數學核心有四層:前向過程的閉式解、反向過程的參數化、變分下界的分解、以及「預測雜訊」的等價性。我們逐層推導。
2.1 第一層:前向過程的閉式解(完整歸納推導)#
目標:求 \(q(x_t \mid x_0)\) ,即「從 \(x_0\) 一步跳到第 \(t\) 步的加噪分佈」,而不必真的跑 \(t\) 步。
記號定義:
命題: \(q(x_t \mid x_0) = \mathcal{N}\!\left(x_t;\; \sqrt{\bar{\alpha}_t}\, x_0,\; (1-\bar{\alpha}_t) I\right)\)
等價地,可以寫成一個隨機變數的線性組合:
完整歸納推導。
基底情形( \(t = 1\) ):
而 \(\bar{\alpha}_1 = \alpha_1\) , \(1 - \bar\alpha_1 = 1-\alpha_1\) ,故命題對 \(t=1\) 成立。
歸納步驟:假設命題對 \(t\) 成立,即 \(x_t = \sqrt{\bar{\alpha}_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon_t\) 。由前向過程的一步轉移,
代入歸納假設:
前兩項的合併已經完成。關鍵在於後兩項:** \(\epsilon_t\) 與 \(\epsilon_{t+1}\) 是獨立的標準高斯變數**,而兩個獨立標準高斯變數的加權和仍是標準高斯(只要權重的平方和為 1)。具體地,
係數整理:
因此存在 \(\tilde{\epsilon} \sim \mathcal{N}(0,I)\) 使得
歸納完成。命題得證。
這個閉式解的實務意義是決定性的:訓練時不需要模擬前向鏈。我們可以隨機抽一個 \(t\) ,一次前向傳播就得到任意時間步的加噪版本。這讓擴散模型的訓練是完全並行的——沒有任何序列依賴(這是它相對自迴歸模型的核心優勢,也是 5.4 節的對比點)。
「為什麼要乘 \(\sqrt{1-\beta_t}\) ?」 這個縮放保證了方差的守恆。若前向過程是 \(x_t = x_{t-1} + \sqrt{\beta_t}\epsilon_t\) (不加縮放),則
累積下來 \(\mathrm{Var}[x_t] = \mathrm{Var}[x_0] + \sum_t \beta_t\) ,會隨 \(t\) 發散。而加上縮放後:
若 \(\mathrm{Var}[x_{t-1}] = 1\) ,則 \(\mathrm{Var}[x_t] = (1-\beta_t) + \beta_t = 1\) 。完美守恆。這就是為什麼要乘 \(\sqrt{1-\beta_t}\) 。
2.2 第二層:反向過程#
反向過程同樣是馬可夫鏈,但方向相反,且需要學:
注意方向:給定 \(x_t\) (比較髒的版本),產生 \(x_{t-1}\) (比較乾淨的版本)。
DDPM 論文取 \(\Sigma_t = \sigma_t^2 I\) ,且 \(\sigma_t^2 = \beta_t\) 或 \(\sigma_t^2 = \tilde{\beta}_t\) (後者更常用,效果略好):
為什麼要固定協方差? 因為在變分下界的推導中(2.3 節), \(\Sigma_t\) 項會產生一個與 \(\theta\) 無關的加性常數(因為它只出現在 KL 的第一項),所以最優解與 \(\Sigma_t\) 的選擇無關——既然如此,就固定成一個簡單的形式。這是「參數化選擇只影響效率,不影響最優解」的漂亮例子。
生成過程(演算法 2):
x_T ~ N(0, I)
for t = T, T-1, ..., 1:
z ~ N(0, I) if t > 1 else 0
x_{t-1} = mu_theta(x_t, t) + sigma_t * z
return x_0
2.3 第三層:變分下界的分解#
我們要最大化的目標仍是邊際似然 \(\log p_\theta(x_0)\) ,同 7.2 節的 VAE:
用 Jensen 不等式(與 7.2 節完全相同的技巧),得到下界:
其中 \(q(x_{1:T}|x_0) = \prod_{t=1}^{T} q(x_t \mid x_{t-1})\) 是前向鏈。
分解為各個時間步。標準的分解(DDPM 論文式 3):
- 第一項:前向鏈終點與先驗的差距。先驗 \(p(x_T) = \mathcal{N}(0,I)\) 與 \(q(x_T|x_0) = \mathcal{N}(\sqrt{\bar\alpha_T}x_0, (1-\bar\alpha_T)I)\) 的 KL。由於 \(\bar\alpha_T \approx 0\) ,這個值幾乎為常數,不含可學參數。
- 中間項:每一步反向轉移與真實後向轉移的差距。這是唯一含參數的項。
- **最後一項 \(L_0\) **:給定 \(x_1\) 重構 \(x_0\) 的重建項。若解碼器是離散的(把 \(\mu_\theta(x_1,1)\) round 成整數),就沒有這個項。
關鍵的一步: \(q(x_{t-1}\mid x_t, x_0)\) 有閉式解。這是整個推導能順利進行的原因:
其中
推導(這一步值得寫出,因為它是用高斯條件分佈公式的直接應用):
由 \(q(x_t \mid x_{t-1}, x_0) \propto \exp\left(-\frac{\|x_t - \sqrt{\alpha_t}x_{t-1}\|^2}{2\beta_t}\right)\) 與 \(q(x_{t-1}|x_0) = \mathcal{N}(\sqrt{\bar\alpha_{t-1}}x_0, (1-\bar\alpha_{t-1})I)\) 相乘並對 \(x_{t-1}\) 完成平方,得到均值
(其中的後驗精度為 \(\frac{1-\bar\alpha_t}{\beta_t}\) ,這是標準的高斯-高斯共軛結果。)
現在關鍵的一步:把 \(\tilde\mu_t\) 用前向閉式解 \(x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\epsilon\) 反解出 \(x_0\) :
代入 \(\tilde\mu_t\) :
整理 \(x_t\) 與 \(\epsilon\) 的係數:
(這裡用到了 \(\frac{\bar\alpha_{t-1}}{\bar\alpha_t} = \frac{1}{\alpha_t}\) 。)
這個式子是整個 DDPM 的樞紐,因為它把「真實後向均值」寫成了「 \(x_t\) 與 \(\epsilon\) 的線性組合」——而 \(\epsilon\) 正是我們要預測的目標。
2.4 第四層:簡化訓練目標與三種參數化的等價性#
KL 的化簡。由於 \(q(x_{t-1}|x_t,x_0)\) 與 \(p_\theta(x_{t-1}|x_t)\) 都是高斯,KL 有解析式:
(常數項與 \(\theta\) 無關。)
由於 \(\tilde\mu_t\) 是 \(\epsilon\) 的仿射函數,最小化 \(\|\tilde\mu_t - \mu_\theta\|^2\) 等價於最小化 \(\|\epsilon - \epsilon_\theta\|^2\) ,其中 \(\epsilon_\theta\) 是網路的輸出、 \(\mu_\theta\) 由它導出。這樣的參數化我們稱為「預測雜訊」(predict the noise)。
三種參數化的相互轉換。這是實作上非常實用的部分,因為不同論文用不同參數化,混用會出錯。
| 參數化 | 網路預測 | 從網路輸出還原 \(\mu_\theta\) |
|---|---|---|
| **預測 \(\epsilon\) ** | \(\epsilon_\theta(x_t, t)\) | \(\mu_\theta = \dfrac{1}{\sqrt{\alpha_t}}\left(x_t - \dfrac{\beta_t}{\sqrt{1-\bar\alpha_t}}\epsilon_\theta\right)\) |
| **預測 \(x_0\) ** | \(\hat{x}_0(x_t,t)\) | \(\mu_\theta = \dfrac{\sqrt{\bar\alpha_{t-1}}\beta_t}{1-\bar\alpha_t}x_t + \dfrac{\sqrt{\alpha_t}(1-\bar\alpha_{t-1})}{1-\bar\alpha_t}\hat{x}_0\) |
| **預測 \(\mu\) ** | \(\mu_\theta(x_t,t)\) | \(\epsilon_\theta = \dfrac{x_t-\sqrt{\alpha_t}\mu_\theta}{\sqrt{1-\bar\alpha_t}}\) |
轉換式(從 \(\epsilon\) 出發):
實務建議:DDPM 論文推薦**預測 \(\epsilon\) ;後來的 Imagen(Saharia et al., 2022)推薦預測 \(v\) **(velocity),定義為 \(v = \sqrt{\bar\alpha_t}\epsilon - \sqrt{1-\bar\alpha_t}\hat{x}_0\) ,好處是在 \(\bar\alpha_t \to 0\) 與 \(\bar\alpha_t \to 1\) 兩端都不會數值爆炸。
最終的精簡損失(DDPM 論文式 12):
訓練演算法(Algorithm 1):
repeat
x0 ~ q(x0 | D) # 取一個真實樣本
t ~ Uniform{1, ..., T} # 隨機時間步
eps ~ N(0, I) # 隨機雜訊
xt = sqrt(abar[t]) * x0 + sqrt(1 - abar[t]) * eps
loss = || eps - eps_theta(xt, t) ||^2
對 loss 對 theta 做梯度下降
until 收斂
注意這個演算法的三個優點:
- 完全並行:不需要跑前向鏈,也沒有任何時間步之間的依賴。可以把整個資料集切成大 batch 一次算完。
- 目標是簡單的均方誤差回歸——沒有 minmax、沒有對手、沒有鞍點。
- 每一個樣本在每一個迭代都被用來做回歸,因此模型沒有任何機制可以「忽略某些模式」——這是結構性的 mode collapse 免疫(見 5.2 節)。
2.5 與第 2.1 節去噪的呼應#
擴散模型與第 2.1 節的關係可以用一句話總結:
擴散模型把「用網路去噪」這件事,從一個單次的輸入→輸出映射,升級成了一個 \(T\) 步的可學馬可夫鏈。
第 2.1 節的濾波器(高斯濾波、雙邊濾波)是手工設計的、線性的、參數固定的去噪器。擴散模型把三件事都換掉了:
| 第 2.1 節的濾波 | 擴散模型 | |
|---|---|---|
| 去噪器 | 固定的線性/非線性算子 | 學到的深度網路 \(\epsilon_\theta(x_t, t)\) |
| 噪訊模型 | 高斯白雜訊 | 高斯白雜訊(相同) |
| 迭代次數 | 1-2 次 | \(T\) 步(可到數千) |
| 參數來源 | 人工選擇半徑 \(\sigma\) | 從資料學出 |
| 能力 | 只能移除特定頻段 | 學到整體的自然影像先驗 |
更深刻的呼應:擴散模型實際上學到的是「自然影像的先驗分佈」的一個逐步展開的表示。 \(\epsilon_\theta(x_t,t)\) 這個函數族的意義是:
給定一個在第 \(t\) 步的帶噪影像,告訴我「在最好的猜測下,這一步該去掉多少什麼樣的雜訊」。
當 \(t\) 很大時, \(x_t\) 幾乎是純雜訊,網路學到的其實是「給定純雜訊,該往資料流形的那個方向走」——這就是 score function 的角色(第六節)。
三、DDIM(Song et al., 2020):把採樣變成確定性積分#
3.1 問題:DDPM 採樣要 1000 步#
DDPM 的反向過程是隨機的(每步加入 \(\sigma_t z\) ),這意味著:
- 同一個起點 \(x_T\) 每次採樣都得到不同結果(這是好事——多樣性來自這裡);
- 但需要 1000 次網路前向傳播才能生成一張圖。這在實際應用中不可接受(生成一張 512×512 圖需要數十秒到數分鐘)。
3.2 非馬可夫前向過程#
DDIM(Denoising Diffusion Implicit Models)的核心洞見是:生成過程不一定要是馬可夫鏈。
DDIM 把前向過程推廣為一族由向量 \(\sigma \in \mathbb{R}^T\) 參數化的非馬可夫過程:
其中
注意:這個式子把 \(\epsilon_\theta\) 直接寫進了前向分佈裡——這是 DDIM 的漂亮之處:它用網路自己的預測來定義前向過程,從而讓反向過程變得確定。
3.3 DDIM 的反向過程(確定性)#
其中
確定性的來源: \(\sigma_t = 0\) 時,最後的隨機項消失,整個採樣過程是 \(x_T\) 的確定函數。這就像 ODE 的解。
** \(\sigma_t\) 的選擇。DDIM 論文給出一般形式,並證明-DDIM 與 DDPM 共享同一個訓練目標**:
- \(\eta = 1\) :恢復為 DDPM(隨機採樣)。
- \(\eta = 0\) :DDIM(確定性採樣)。
最關鍵的性質:用 DDPM 訓練的模型,可以用 DDIM 以任意少的步數採樣,且品質衰減很小。
| 步數 | DDPM | DDIM ( \(\eta=0\) ) |
|---|---|---|
| 1000 | 基準 | 幾乎無損 |
| 100 | 明顯劣化 | 輕微劣化 |
| 50 | 明顯劣化 | 輕微劣化(10-20 倍加速) |
| 20 | 嚴重劣化 | 可用 |
| 10 | 不可用 | 明顯劣化 |
這是擴散模型在 2021-2022 年能被部署的技術前提(Stable Diffusion 的 DPM-Solver 與 DDIM 是同一族的工作)。
3.4 確定性帶來的三個額外好處#
- 語意插值:因為 \(x_0 = f(x_T)\) 是確定性映射,改變 \(x_T\) 會連續地改變 \(x_0\) 。這使得「在兩個 latent 之間插值生成」成為可能(GAN 需要 StyleGAN 那些額外設計才有類似能力)。
- 可重現:給定 \(x_T\) 與採樣步數,結果完全確定。
- 隱藏語意:DDIM 論文展示,把 \(x_T\) 換成另一個樣本的 \(x_T\) ,會生成另一個樣本——但它保留了原樣本的高階結構。這是「latent 語意」的早期證據。
四、Classifier-Free Guidance(Ho & Salimans, 2021)#
4.1 背景:從 classifier guidance 到 classifier-free guidance#
Classifier guidance(Dhariwal & Nichol, 2021)用一個外部訓練的分類器 \(p_\phi(y|x_t)\) 來引導生成:
其中 \(s\) 是 guidance scale。直覺:沿著「讓這張圖更像類別 \(y\) 」的方向推。
問題:
- 需要另外訓練一個分類器(在帶噪影像上),代價高昂;
- 分類器會被对抗樣本攻擊而失效(生成分佈會「鑽分類器的空子」);
- 分類器本身是有偏的。
Classifier-free guidance(CFG) 的洞見:不需要分類器。只要在訓練時隨機丟掉條件,模型就會同時學到條件與無條件兩個模型,然後把兩者的預測外插。
4.2 訓練:條件 dropout#
在訓練時,以機率 \(p_{\text{uncond}}\) (通常 0.1-0.2)把條件標籤替換成空標籤:
其中 \(c\) 有機率被設為 \(\varnothing\) (空條件)。這樣同一個網路就學會了:
- \(\epsilon_\theta(x_t,t,\varnothing)\) :無條件預測(學到整個資料分佈);
- \(\epsilon_\theta(x_t,t,c)\) :條件預測(學到給定 \(c\) 的分佈)。
4.3 引導公式#
在採樣時,把兩個預測外插:
這是「以無條件預測為基準,沿著條件與無條件之差的方向走 \(w\) 倍」。
與另一種常見寫法的等價性。有些文獻(含本節題綱)把它寫成
這是把「無條件」與「條件」的角色互換後的同一個外插:此時 \(\epsilon_\theta(x_t,t)\) 是條件預測、 \(\epsilon_\theta(x_t,t,c)\) 是無條件預測, \(w\) 的符號相應反轉。兩種寫法在文獻中都出現,引用時務必確認哪個符號是條件的——這是本節最常見的誤解來源。
本文採用第一種寫法( \(w=1\) 即為純條件預測, \(w=0\) 即為純無條件預測),並把第二種寫法視為「把 \(w \to -w\) 並交換標籤」的等價形式。
4.4 \(w\) 的取捨與「過飽和」#
\(w\) 越大,條件一致性越強,但代價是:
- 多樣性下降: \(\tilde\epsilon\) 的方向遠離整體資料流形,生成結果集中在「最典型」的樣本上,變化減少。
- 過飽和(over-saturation):影像的色彩對比與銳利度被放大,看起來不自然、像「已經過後製」。
- 對無分類資訊的條件(例如純文字 prompt)效果特別明顯。
實務經驗值:
| 用途 | 建議 \(w\) |
|---|---|
| ImageNet 類別條件(DDPM 論文) | 1.5 - 3.0 |
| 文字條件(Stable Diffusion 類) | 7.0 - 9.0 |
| 影像條件 / 風格遷移 | 3 - 6 |
注意 ImageNet 與文字條件的 \(w\) 差異極大。原因是:類別條件是資訊量很小但高度相關的(一個類別對應很多不同的影像),所以適度引導即可;文字條件則資訊量很大(一句 prompt 描述了姿勢、背景、風格等諸多細節),需要較強的引導才能把條件「壓」進去。
為什麼 CFG 有效? 它可以理解為對 log 分佈做一個外插:
因為 \(\nabla\log p = -\epsilon/\sqrt{1-\bar\alpha_t}\) ,所以
這說明 CFG 相當於在分佈層面做「無條件分佈的 \((1-w)\) 次方 × 條件分佈的 \(w\) 次方」的幾何混合——這是 classifier guidance 的經典做法,只不過 classifier guidance 只能沿著類別後驗的方向推,CFG 則沿著條件與無條件的差方向推,後者的方向性更強、對文字這類複雜條件更有效。
4.5 CFG 的代價#
CFG 需要兩次網路前向傳播(一次無條件、一次條件),或把兩者批次合併成一次(把 batch 複製兩份,條件部分填 \(c\) 、一部分填 \(\varnothing\) )。這讓採樣成本加倍。
五、為什麼擴散模型勝出:五個結構性理由#
現在我們可以回答本章最重要的問題:擴散模型為什麼在 2021 年之後全面取代 GAN?
5.1 理由一:目標是回歸式的,梯度穩定#
GAN 的目標是零和 minimax(鞍點)。擴散模型的目標是
這是一個標準的均方誤差回歸:
- 沒有對手:不存在「對手變強導致梯度失效」的機制;
- 梯度有界:L2 損失的梯度對殘差是線性的,不會爆炸也不會消失到無資訊;
- 損失值有絕對意義:可以看著 train loss 判斷是否在學(這正是 7.1 節「問題一」的完整答案);
- 對超參不敏感:學習率、初始化、架構深度的容忍度都遠高於 GAN。
比較表:
| GAN | VAE | 擴散模型 | |
|---|---|---|---|
| 目標型態 | minimax / 零和 | 變分下界(最大化) | 回歸(MSE 最小化) |
| 目標有界 | 否 | 是 | 是 |
| 梯度來源 | 依賴對手狀態 | 解析 KL + 抽樣 | 純資料 |
| 訓練監控 | 只能靠 FID | ELBO | loss 曲線 + FID |
| 超參敏感度 | 高 | 低 | 低 |
| 常見失敗模式 | mode collapse | 模糊、posterior collapse | 幾乎無(但採樣慢) |
5.2 理由二:結構性的 mode collapse 免疫#
這是最值得詳細說明的一點,因為它是機制層面的保證,不是經驗觀察。
機制:DDPM 的損失是
這個期望是對資料集中的每一個 \(x_0\) 與每一個 \(t\) 取的。因此:
- 如果資料集裡有 10% 的樣子是貓,而模型完全不會生成貓,那麼在那些貓的樣本上,損失會很大——梯度會強烈地把它拉回來。
- 模型沒有任何機制可以「忽略」某些資料。對比 GAN:生成器可以把 \(z\) 空間的體積都映射到一個模式上,而被忽略的模式在損失中不產生任何信號(因為判別器只看「整體像不像」,看不到「少了什麼」)。
更精確的對照:GAN 的損失不包含任何「覆蓋率」的項;擴散模型的損失每一項都是對具體資料樣本的回歸。所以擴散模型是一個逐樣本、逐時間步的密集監督學習問題,而 GAN 是一個稀疏的、全域的對抗信號。
這個差異是本章「因果驅動」主線的最終答案:7.1 節的模式崩坍 → 催生了對「穩定且覆蓋」的目標的需求 → 7.2 節的 VAE 用 forward KL 提供覆蓋,但代價是模糊 → 7.3 節的擴散模型用「逐樣本回歸」同時提供了穩定與覆蓋,且藉由多步反覆去噪提供銳利度。
5.3 理由三:訓練完全並行#
| 訓練時的時間步依賴 | 採樣時的順序依賴 | |
|---|---|---|
| 自迴歸(PixelCNN, PixelCNN++) | 有(像素依序預測) | 有 |
| GAN | 無 | 無 |
| VAE | 無 | 無 |
| 擴散模型 | 無(時間步隨機抽樣) | 有(必須逐步去噪) |
擴散模型的訓練可以像訓練任何 ResNet 一樣:把整個 batch 攤平,用 \(T\) 個網路(或時間步嵌入的單一網路)處理。相較之下,像 PixelCNN++ 這類自迴歸模型在訓練時必須逐像素展開(雖然可以用 teacher forcing 平行化一部分),在影像上效率遠不如擴散模型。
5.4 理由四:代價——採樣慢#
擴散模型的主要劣勢是需要多步去噪。DDPM 的 \(T = 1000\) ,即使有 DDIM 加速到 20-50 步,仍遠慢於 GAN 的單次前向傳播。
這個劣勢的根源正是 7.1 節「問題三」的鏡像:
GAN 用精心設計的架構(StyleGAN 的十幾個零件)換取單次前向的取樣速度;擴散模型用單次前向的簡潔架構換取多步的取樣速度。
值得強調的是,擴散模型不需要那些精心設計的架構零件——不需要 AdaIN、不需要逐層噪訊注入、不需要風格混合、不需要映射網路。一個簡單的 U-Net + 時間步嵌入 + 注意力殘差塊就夠了。這是極大的工程簡化。
5.5 理由五:代價——訓練資料量大,但可用影像積分運算壓縮#
擴散模型通常需要比 GAN 更大的資料集與更大的模型。但 7.2 節的 normalizing flow 給出了一個關鍵工具:影像積分運算(integrating inverse problems)。
回到變分定理:
如果我們想把一個已經訓練好的擴散模型(例如在 64×64 上訓練的)套用到更高解析度(例如 512×512),可以插入一個 flow \(f\) 把 512×512 影像「摺疊」成 64×64 影像空間。這樣:
- 擴散模型在低解析度上訓練(資料量需求小、記憶體需求小);
- flow 負責把低解析度結果「摺疊回」高解析度;
- 訓練開銷大幅下降。
Latent Diffusion(Rombach et al., 2021) 正是這個思想的推廣:把擴散模型放在一個固定的 VAE 編碼器所產生的潛空間( \(64\times64\times4\) 而不是 \(512\times512\times3\) )中運行。這把訓練與採样的計算量降到大約 \((64/512)^2 \times (4/3) \approx 1/96\) ,而生成結果(經 VAE 解碼器)可達 512×512。這是 2022 年 Stable Diffusion 的直接技術前身(見第 8.2 節)。
5.6 與第 4.2 節 ImageNet 的對比#
DDPM 論文 Diffusion Models Beat GANs on Image Synthesis 的核心實驗結論就是:在 ImageNet 256×256 上,擴散模型在 FID 上正式超越 BigGAN。
| 模型 | 年份 | ImageNet 256×256 FID | 備註 |
|---|---|---|---|
| BigGAN-deep | 2018 | ~6.95 | 當時的 SOTA GAN |
| BigGAN-deep(截斷技巧) | 2018 | ~9.6 | 截斷改善多樣性但劣化 FID |
| ADM(無 guidance) | 2021 | ~3.9 | 分類器導向的擴散模型 |
| ADM + classifier guidance | 2021 | ~2.9 | 需外部分類器 |
| DDPM + classifier-free guidance | 2021 | ~3.2 | 不需要外部分類器 |
關鍵的歷史意義:這是生成模型領域的一次典範轉移——擴散模型在 2021 年正式超越 GAN 成為品質標準,並且是用更簡單的架構做到的(沒有 StyleGAN 那樣的十幾個零件)。
必須公允指出的取捨:這個「勝利」不是免費的。
- 擴散模型的 FID 優勢主要來自更高解析度的訓練(ADM 用 256×256,而 BigGAN 主要在 128×128 訓練再上採樣)——這裡有硬體紅利的成分;
- 擴散模型的取樣慢數百倍;
- 擴散模型在類別平衡、多樣性指標(recall) 上並不一定全面領先;
- 這個時期的 GAN(StyleGAN2、BigGAN-deep + tricks)在人臉等特定領域仍然非常強。
六、Score-based Models(Song et al., 2021):統一的另一條路#
6.1 問題:雜訊預測等價於什麼?#
DDPM 用「預測雜訊」來訓練。但在 \(\epsilon\) 、 \(x_0\) 、 \(\mu\) 之間的參數化轉換裡,我們已經看到一個線索:** \(\epsilon\) 的方向其實是「密度對數的梯度」的反向**。
這自然引出一個問題:能不能直接估計那個「密度對數的梯度」本身?
6.2 Score function 的定義#
對一個分佈 \(p(x)\) ,定義其 score function 為
它是「往密度增加最快的方向」。score matching 是一個經典的統計方法:找一個參數化的 \(s_\theta\) 最小化
這就是統計物理與統計學習之間歷史悠久的橋樑。
6.3 核心定理:預測雜訊 ≡ 估計 score#
定理:若 \(x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon\) ,其中 \(x_0 \sim p_{\text{data}}\) 、 \(\epsilon \sim \mathcal{N}(0,I)\) 且兩者獨立,則
推導。由 2.1 節的前向過程, \(q(x_t \mid x_0) = \mathcal{N}(\sqrt{\bar\alpha_t}x_0, (1-\bar\alpha_t)I)\) ,所以
代入高斯密度:
對 \(x_t\) 求梯度:
現在用一個關鍵的技巧把括號裡的量換成雜訊。我們知道
因此 \(x_t - \sqrt{\bar\alpha_t}x_0 = \sqrt{1-\bar\alpha_t}\,\epsilon\) 。取期望(注意 \(\mathbb{E}[\epsilon \mid x_t] \neq 0\) ,這是關鍵):
於是
而 \(\mathbb{E}[\epsilon \mid x_t]\) 正是 \(\epsilon_\theta(x_t,t)\) 試圖預測的量(在最佳解時)。因此
定理得證。
這個定理的意義:它說明擴散模型與 score matching 是同一件事的兩種表述。這不是類比,而是精確的等價關係。Song et al. (2021) 的 Score-Based Generative Modeling through Stochastic Differential Equations 正是以此為基礎,把 DDPM、noise-conditional score networks(NCSN, Song & Ermon 2019)與 SDE 理論統一在一個框架下。
常見誤解:「score 是資料分佈的梯度,所以無法學。」可以學——因為在帶噪的尺度上,score 可以從資料估計(用噪訊預測),這正是上面的定理。
6.4 反向 SDE#
現在把擴散模型寫成隨機微分方程(SDE)的形式。前向過程是一個 SDE:
其中 \(w\) 是標準布朗運動。在方差保持(variance preserving)的設定下, \(f(x,t) = -\frac{1}{2}\beta(t)x\) 、 \(g(t) = \sqrt{\beta(t)}\) 。
時間反演定理(Anderson, 1982 / Haussmann & Pardoux, 1986)給出反向 SDE:
其中 \(\bar{w}\) 是反向的布朗運動。
逐項解讀:
- \(f(x,t)\mathrm{d}t\) :原本的漂移項(把 \(x\) 拉回資料中心)。
- \(-g(t)^2 \nabla_x \log p_t(x)\mathrm{d}t\) :score 引導項。這是生成的核心——沿著「密度上升最快」的方向推。
- \(g(t)\mathrm{d}\bar{w}\) :隨機性。這保證了生成分佈的多樣性(對應 7.1 節 mode collapse 的對立面)。
這個式子把擴散模型與 GAN 做了一個深刻的對照:GAN 的生成器對抗的是一個判別器;擴散模型的生成器對抗的是物理定律本身(布朗運動),而 score 提供方向。
6.5 Probability Flow ODE:把採樣變成確定性積分#
把反向 SDE 的隨機性去掉,得到概率流 ODE(probability flow ODE):
為什麼是 \(\frac{1}{2}\) ? 因為 SDE 與 ODE 的 Fokker-Planck 方程有對應關係:ODE 的概率流(probability flow)與 SDE 的邊際分佈演化有相同的速度場,但 ODE 的速度場是 SDE 漂移的兩倍的一半——標準的 SDE→ODE 對應(同一個 Fokker-Planck 方程的兩種表示)。
這個 ODE 的意義:
- 確定性:給定 \(x_T\) ,解是唯一的。
- 可用高階 ODE 求解器加速:如 Runge-Kutta、scipy 的
solve_ivp、DPM-Solver 等。 - 連續時間:不需要離散時間步的網格。
6.6 SDE 框架的三個分支#
Song et al. (2021) 把所有方法統一為兩類 SDE:
| Variance Preserving SDE | Variance Exploding SDE | |
|---|---|---|
| 前向 SDE | \(\mathrm{d}x = -\frac{1}{2}\beta(t)x\,\mathrm{d}t + \sqrt{\beta(t)}\,\mathrm{d}w\) | \(\mathrm{d}x = \sqrt{\beta(t)}\,\mathrm{d}w\) |
| 前向特徵 | 訊號縮放 + 加噪 | 只加噪,不改變尺度 |
| 對應的 DDPM | 是 | 否(對應 NCSN) |
| 對應的 SDE solver | Reverse-diffusion / DDPM | 對應 2019 的 NCSN |
| 對應的 ODE solver | Probability flow ODE | 同 |
| 數值穩定性 | 好 | 需要小心 |
為什麼 variance exploding 在影像上較差? 因為它不改變訊號尺度,資料在早期就被雜訊淹沒得比較徹底,反向過程比較難處理。Variance preserving 正是 DDPM 用的形式,也解釋了為什麼 DDPM 的數值表現好。
七、其他加速與蒸餾方向#
7.1 DPM-Solver(2022)#
DPM-Solver 觀察到:ODE 求解器的精度主要由「解對數密度比值」這一件事決定,其他細節不重要。因此設計了專門求解擴散 ODE 的高階(可達 3 階)方法。效果:10 步以內達到 DDIM 50 步的品質。
7.2 Progressive Distillation(Salimans & Ho, 2022)#
蒸餾的思路:把 \(T\) 步的教師模型蒸餾成 \(2\) 步的學生模型。核心技巧是把中間步驟「跳過」並重新參數化,讓學生直接學會「兩步走完」。
結果:1-4 步就能生成高品質影像。代價是學生模型的多樣性通常低於教師。
7.3 Consistency Models(Song et al., 2023)#
更激進的目標:直接學一個「跳到任意時間步」的一致性函數:
也就是說, \(f_\theta(x_t,t)\) 應該直接輸出乾淨影像,且對任何 \(t\) 都給出相同的結果(這是「一致性」的由來):
兩種訓練方式:
- Consistency Training (CT):用 ODE 的相鄰點配對( \(x_{t}\) 與 \(x_{t-\epsilon}\) ),損失為
- Consistency Distillation (CD):用教師模型(DDPM)的預測作為目標:
一步生成的結果在 ImageNet 256×256 上達到 FID ~2.9(CT)/ ~2.9(CD),品質驚人地接近多步的 DDPM。
7.4 演進時間線#
timeline
title 擴散模型的演進(2015-2023)
2015 : Sohl-Dickstein 等人提出非平衡熱力學框架
: 前向加噪 + 反向去噪的馬可夫鏈
2019 : NCSN 首次以 score matching 訓練生成模型
: 連續時間 SDE 框架
2020 : DDIM 提出非馬可夫前向過程
: 確定性採樣,20-50 步即可
2021 : DDPM 簡化訓練目標
: ImageNet 256 上 FID 超越 BigGAN
: Classifier-free guidance 發表
: Song 等人的 SDE 統一框架
2021 : Latent Diffusion 為 Stable Diffusion 鋪路
2022 : DPM-Solver 將採樣降至 10 步以內
: Progressive Distillation 蒸餾至 2-4 步
2023 : Consistency Models 實現一步生成八、實作:DDPM 的前向加噪、訓練與 DDIM 採樣#
8.1 噪訊 schedule 與閉式加噪#
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
def linear_beta_schedule(timesteps=1000, beta_start=1e-4, beta_end=0.02):
"""線性 variance schedule(DDPM 原論文)。"""
return torch.linspace(beta_start, beta_end, timesteps)
def cosine_beta_schedule(timesteps=1000, s=0.008):
"""餘弦 schedule(Nichol & Dhariwal 2021)。
想法:讓 x_t 的 SNR 從幾乎無限大平滑衰減到幾乎為零,
在高解析度影像上比線性 schedule 效果更好。
"""
steps = timesteps + 1
t = torch.linspace(0, timesteps, steps) / timesteps
alphas_cumprod = torch.cos((t + s) / (1 + s) * math.pi / 2) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
return torch.clamp(betas, max=0.999)
class DiffusionSchedule:
"""封裝 schedule 的所有衍生量。"""
def __init__(self, timesteps=1000, schedule="linear", device="cpu"):
self.T = timesteps
if schedule == "linear":
betas = linear_beta_schedule(timesteps)
elif schedule == "cosine":
betas = cosine_beta_schedule(timesteps)
else:
raise ValueError(f"未知的 schedule: {schedule}")
self.betas = betas.to(device)
self.alphas = 1.0 - self.betas
self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)
# 前向過程用到的量
self.sqrt_alphas_cumprod = torch.sqrt(self.alphas_cumprod)
self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - self.alphas_cumprod)
# 反向過程用到的量
self.sqrt_recip_alphas = torch.sqrt(1.0 / self.alphas)
self.sqrt_recipm1_alphas_cumprod = torch.sqrt(1.0 / self.alphas_cumprod - 1)
# tilde beta:方差選項 (2)
posterior_variance = (
self.betas * (1.0 - self.alphas_cumprod[:-1]) / (1.0 - self.alphas_cumprod[1:])
)
self.posterior_variance = torch.cat(
[posterior_variance, torch.tensor([betas[-1] * (1.0 - alphas_cumprod[-2]) /
(1.0 - alphas_cumprod[-1])])]
) if len(betas) > 1 else torch.tensor([0.0])
self.posterior_log_variance_clipped = torch.log(
torch.cat([posterior_variance[1:],
torch.tensor([betas[-1] * (1.0 - alphas_cumprod[-2]) /
(1.0 - alphas_cumprod[-1])])])
)
def q_sample(self, x0, t, noise=None):
"""前向加噪的閉式解:x_t = sqrt(abar_t) x_0 + sqrt(1-abar_t) eps。
這是整個 DDPM 訓練的核心:不需要模擬 t 步,一次到位。
"""
if noise is None:
noise = torch.randn_like(x0)
a = self.sqrt_alphas_cumprod[t].view(-1, *([1] * (x0.dim() - 1)))
s = self.sqrt_one_minus_alphas_cumprod[t].view(-1, *([1] * (x0.dim() - 1)))
return a * x0 + s * noise, noise
def extract(arr, t, x_shape):
"""把 schedule 上的向量依時間步索引取出,並 reshape 成可廣播的形狀。"""
out = arr.to(t.device).gather(0, t)
return out.view(-1, *([1] * (len(x_shape) - 1)))
8.2 去噪網路:時間步嵌入的 U-Net#
class SinusoidalPosEmb(nn.Module):
"""Transformer 式的位置編碼,用來嵌入離散的時間步 t。"""
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, t):
device = t.device
half = self.dim // 2
freqs = torch.exp(
-math.log(10000) * torch.arange(half, device=device).float() / half
)
args = t[:, None].float() * freqs[None]
return torch.cat([torch.sin(args), torch.cos(args)], dim=-1)
class ResBlock(nn.Module):
"""殘差塊,把時間步嵌入加進來。"""
def __init__(self, in_ch, out_ch, t_dim, dropout=0.1):
super().__init__()
self.norm1 = nn.GroupNorm(8, in_ch)
self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1)
self.t_proj = nn.Linear(t_dim, out_ch)
self.norm2 = nn.GroupNorm(8, out_ch)
self.dropout = nn.Dropout(dropout)
self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1)
self.skip = nn.Conv2d(in_ch, out_ch, 1) if in_ch != out_ch else nn.Identity()
def forward(self, x, t_emb):
h = self.conv1(F.silu(self.norm1(x)))
# 時間步嵌入以 FiLM 方式注入:scale 與 shift
t = self.t_proj(F.silu(t_emb))[:, :, None, None]
h = self.norm2(h) * (1 + t) + t
h = self.conv2(self.dropout(F.silu(h)))
return h + self.skip(x)
class UNet(nn.Module):
"""DDPM 使用的 U-Net 去噪網路。"""
def __init__(self, in_ch=3, base=64, t_dim=256):
super().__init__()
self.t_embed = nn.Sequential(
SinusoidalPosEmb(base),
nn.Linear(base, t_dim),
nn.SiLU(),
nn.Linear(t_dim, t_dim),
)
self.stem = nn.Conv2d(in_ch, base, 3, padding=1)
self.down1 = ResBlock(base, base, t_dim)
self.down2 = ResBlock(base, base * 2, t_dim)
self.down3 = ResBlock(base * 2, base * 4, t_dim)
self.mid = ResBlock(base * 4, base * 4, t_dim)
self.up3 = ResBlock(base * 8, base * 2, t_dim)
self.up2 = ResBlock(base * 4, base, t_dim)
self.up1 = ResBlock(base * 2, base, t_dim)
self.out = nn.Sequential(
nn.GroupNorm(8, base),
nn.SiLU(),
nn.Conv2d(base, in_ch, 3, padding=1),
)
def forward(self, x, t):
t_emb = self.t_embed(t)
h0 = self.stem(x)
h1 = self.down1(h0, t_emb)
h2 = self.down2(F.avg_pool2d(h1, 2), t_emb)
h3 = self.down3(F.avg_pool2d(h2, 2), t_emb)
m = self.mid(h3, t_emb)
u2 = self.up3(torch.cat([m, h3], dim=1), t_emb)
u1 = self.up2(torch.cat([F.interpolate(u2, scale_factor=2, mode="nearest"), h2], dim=1), t_emb)
u0 = self.up1(torch.cat([F.interpolate(u1, scale_factor=2, mode="nearest"), h1], dim=1), t_emb)
return self.out(u0)
8.3 訓練迴圈#
def train_ddpm(model, schedule, loader, device, epochs=100, lr=2e-4, ema_decay=0.999):
opt = torch.optim.Adam(model.parameters(), lr=lr)
ema = {k: v.detach().clone() for k, v in model.state_dict().items()}
for epoch in range(epochs):
running = 0.0
for batch, _ in loader:
x0 = batch.to(device)
b = x0.size(0)
# 1. 隨機抽時間步
t = torch.randint(0, schedule.T, (b,), device=device)
# 2. 前向加噪(閉式解,一次到位)
xt, noise = schedule.q_sample(x0, t)
# 3. 網路預測雜訊
pred_noise = model(xt, t)
# 4. 均方誤差回歸損失
loss = F.mse_loss(pred_noise, noise)
opt.zero_grad(set_to_none=True)
loss.backward()
opt.step()
# 5. EMA 更新(擴散模型幾乎都需要 EMA 來穩定採樣品質)
with torch.no_grad():
for k, v in model.state_dict().items():
if v.dtype.is_floating_point:
ema[k].mul_(ema_decay).add_(v.detach(), alpha=1 - ema_decay)
else:
ema[k] = v.clone()
running += loss.item() * b
print(f"epoch {epoch}: loss={running/len(loader.dataset):.5f}")
return model
為什麼需要 EMA? 擴散模型的採樣過程是數百次連續的網路呼叫,任何一次呼叫的參數抖動都會沿著鏈條累積。EMA(指數移動平均)參數提供了一個更平滑的解。實務上 \(\text{decay} = 0.9999\) 是常見值。
為什麼 batch size 很重要? 擴散模型對 batch size 敏感(不像 GAN 對 batch size 極度敏感,但擴散模型在小 batch 下會明顯劣化)。這是因為每個時間步的回歸都需要足夠的樣本來覆蓋多樣的模式。
8.4 DDPM 採樣#
@torch.no_grad()
def sample_ddpm(model, schedule, n, shape, device, use_ema=True):
"""完整的 DDPM 採樣:從純雜訊逐步去噪回到影像。"""
state = {k: v.detach().clone() for k, v in model.state_dict().items()}
if use_ema:
model.load_state_dict(ema_state)
x = torch.randn(n, *shape, device=device)
for i in reversed(range(schedule.T)):
t = torch.full((n,), i, device=device, dtype=torch.long)
# 1. 預測雜訊
eps = model(x, t)
# 2. 由 eps 推出 x_0 與 mu
a = extract(schedule.sqrt_alphas_cumprod, t, x.shape)
s = extract(schedule.sqrt_one_minus_alphas_cumprod, t, x.shape)
x0_pred = (x - s * eps) / a
# 3. 由 mu_theta 推出 x_{t-1}
recip = extract(schedule.sqrt_recip_alphas, t, x.shape)
rm1 = extract(schedule.sqrt_recipm1_alphas_cumprod, t, x.shape)
# mu = (1/sqrt(alpha_t)) * (x_t - beta_t/sqrt(1-abar_t) * eps)
beta = extract(schedule.betas, t, x.shape)
coef = beta / s
mu = recip * (x - coef * eps)
# 4. 加噪(t > 0 時)
if i > 0:
var = extract(schedule.posterior_log_variance_clipped, t, x.shape)
z = torch.randn_like(x)
x = mu + torch.exp(var) * z
else:
x = mu
return x
8.5 DDIM 採樣(20-50 步)#
@torch.no_grad()
def sample_ddim(model, schedule, n, shape, device, num_steps=50, eta=0.0):
"""DDIM 採樣:非馬可夫、確定性(eta=0),可用遠少於 T 的步數。"""
T = schedule.T
# 均勻取步數(包含 0 與 T-1)
step_indices = torch.linspace(T - 1, 0, num_steps).long().to(device)
alphas_cumprod = schedule.alphas_cumprod
x = torch.randn(n, *shape, device=device)
for i, t_cur in enumerate(step_indices):
t = torch.full((n,), t_cur, device=device, dtype=torch.long)
eps = model(x, t)
a_t = alphas_cumprod[t_cur]
a_prev = alphas_cumprod[step_indices[i + 1]] if i + 1 < len(step_indices) else torch.tensor(1.0, device=device)
# x0 的預測
x0_pred = (x - torch.sqrt(1 - a_t) * eps) / torch.sqrt(a_t)
x0_pred = x0_pred.clamp(-1, 1) # 常用 clip 以提升品質
# sigma_t:eta 插值
sigma_t = (
eta * torch.sqrt((1 - a_prev) / (1 - a_t)) * torch.sqrt(1 - a_t / a_prev)
).sqrt() if eta > 0 else torch.tensor(0.0, device=device)
dir_xt = torch.sqrt((1 - a_prev - sigma_t ** 2)) * eps
x = torch.sqrt(a_prev) * x0_pred + dir_xt
if eta > 0 and i + 1 < len(step_indices):
x = x + sigma_t * torch.randn_like(x)
return x
關鍵對照:把 num_steps 從 1000 改成 50,採樣速度提升 20 倍,而 FID 只從 ~3.9 劣化到 ~4.0 上下(DDIM 論文報告的數據)。這是 DDIM 貢獻最重要的地方。
8.6 Classifier-Free Guidance 的實作#
class CFGModel(nn.Module):
"""把條件與無條件打包進同一個網路。
做法:把 batch 複製成兩份,一份帶條件 c,一份帶空條件。
這樣只需一次前向傳播就能拿到兩個預測。
"""
def __init__(self, net, num_classes=0):
super().__init__()
self.net = net
self.num_classes = num_classes
def forward(self, x, t, cond=None, guidance_scale=7.5):
if cond is None or self.num_classes == 0:
return self.net(x, t)
b = x.size(0)
x2 = torch.cat([x, x], dim=0)
t2 = torch.cat([t, t], dim=0)
c2 = torch.cat([cond, torch.zeros_like(cond)], dim=0) # 後一半是空條件
eps = self.net(x2, t2, c2)
eps_cond, eps_uncond = eps.chunk(2, dim=0)
# 外插:以無條件為基準,沿著條件方向走 w 倍
return eps_uncond + guidance_scale * (eps_cond - eps_uncond)
訓練時(把條件 dropout 加進去):
def train_step_cfg(model, schedule, x0, labels, optimizer, p_uncond=0.1, label_smooth=0.0):
b = x0.size(0)
t = torch.randint(0, schedule.T, (b,), device=x0.device)
xt, noise = schedule.q_sample(x0, t)
# 以機率 p_uncond 把標籤換成空條件
mask = (torch.rand(b, device=x0.device) < p_uncond).float().view(-1, 1)
cond = labels.float() * (1 - mask) # 假設空條件用 0 表示
pred = model.net(xt, t, cond)
if label_smooth > 0:
target = noise * (1 - label_smooth) + torch.randn_like(noise) * label_smooth
else:
target = noise
loss = F.mse_loss(pred, target)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
return loss.item()
8.7 完整的最小可用實作組合#
def main():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
transform = transforms.Compose([
transforms.Resize(32),
transforms.CenterCrop(32),
transforms.ToTensor(),
transforms.Normalize((0.5,) * 3, (0.5,) * 3),
])
ds = datasets.CelebA("./data", transform=transform, download=True)
loader = DataLoader(ds, batch_size=128, shuffle=True, num_workers=4)
schedule = DiffusionSchedule(timesteps=1000, schedule="cosine", device=device)
model = UNet(in_ch=3, base=64).to(device)
train_ddpm(model, schedule, loader, device, epochs=50)
# 採樣比較:DDPM 1000 步 vs DDIM 50 步
x_ddpm = sample_ddpm(model, schedule, 16, (3, 32, 32), device)
x_ddim = sample_ddim(model, schedule, 16, (3, 32, 32), device, num_steps=50)
# 視覺化與 FID 計算見 7.1 節
九、本節小結#
- 起點是物理直覺:Sohl-Dickstein et al. (2015) 把生成視為「非平衡熱力學過程的時間反演」。前向鏈固定且已知,反向鏈需要學;整個框架由兩個鏡像對稱的馬可夫鏈構成。
- 前向過程的閉式解 \(x_t = \sqrt{\bar\alpha_t}x_0 + \sqrt{1-\bar\alpha_t}\epsilon\) 是整個方法可用的技術前提——它讓訓練完全並行(隨機抽時間步、一次前向)。
- DDPM 的三層數學:變分下界分解(只有中間項含參數)→ 高斯 KL 化簡為均方誤差 → 預測雜訊的參數化。 \(\epsilon\) / \(x_0\) / \(\mu\) 三種參數化互相可轉換。
- 擴散模型把第 2.1 節的「去噪」從單次映射升級為 \(T\) 步的可學馬可夫鏈,用學到的自然影像先驗取代手工設計的濾波器。
- DDIM 引入非馬可可夫前向過程與確定性採樣, \(\sigma_t\) 由 \(\eta\) 插值( \(\eta=0\) 為 DDIM, \(\eta=1\) 為 DDPM),用 20-50 步取代 1000 步。
- Classifier-free guidance 用「條件 dropout + 預測外插」取代外部分類器: \(\tilde\epsilon = \epsilon_{\text{uncond}} + w(\epsilon_{\text{cond}} - \epsilon_{\text{uncond}})\) 。 \(w\) 越大條件一致性越強,但多樣性下降且畫面過飽和。
- Score-based models 統一了兩條路線:核心定理 \(\nabla_{x_t}\log p_t(x_t) = -\epsilon_\theta(x_t,t)/\sqrt{1-\bar\alpha_t}\) 說明預測雜訊就是估計 score。反向 SDE 提供隨機生成,probability flow ODE 提供確定性積分。
- 擴散模型勝出的五個理由:回歸式目標帶來穩定;結構性免疫 mode collapse(每一個資料樣本在每一個時間步都產生回歸信號);訓練完全並行;代價是採樣慢與需要大資料;可用 flow 的影像積分運算壓縮(Latent Diffusion → Stable Diffusion)。
- 2021 年 DDPM 在 ImageNet 256×256 上的 FID 正式超越 BigGAN(~3.2 vs ~6.95),且是用更簡單的架構做到的——這是生成模型領域的一次典範轉移。
下一章我們將進入第 8 章:CLIP 如何讓文字與影像第一次共享同一個語義空間,而 Stable Diffusion 如何把擴散模型與 CLIP 接在一起,最終讓「用文字寫出影像」成為可能。