跳轉到

7.2 變分自動編碼器與流動模型(2013-2016)#


一、起點:GAN 之外,還有另一條路#

2013 年 12 月,Kingma 與 Welling 在 arXiv 上發表 Auto-Encoding Variational Bayes。這篇論文的歷史意義不在於它產生了漂亮的圖(它產生的是模糊的圖),而在於它做了一件極為重要的事:把「生成模型」重新定義成一個明確的機率推論問題,並且給出了一個可以直接用隨機梯度下降最佳化的目標函數。

在第 7.1 節我們已經看到,GAN 的三個未解問題之一是「訓練不穩定且沒有可監控的目標函數」。VAE 正是針對這一點的回應。它的核心洞見是:

不要直接去建模複雜的分佈 \(p(x)\) (做不到),而是引入一個隱變數 \(z\) ,把難題拆成兩個簡單題;然後用變分推論把不可解的後驗換成可解的下界。

這是一個「用一個明確但保守的近似,換取可計算性與可驗證性」的策略。它的代價是模糊,而模糊的根源有非常精確的數學來源——本章會把它推導出來。

1.1 問題設定:為什麼需要隱變數#

我們想建模一個複雜的資料分佈 \(p_{\text{data}}(x)\) ,其中 \(x \in \mathbb{R}^d\) (一張影像)。最一般的方法是直接寫下密度函數:

\[ p_\theta(x) = \frac{1}{Z(\theta)} \exp\big(f_\theta(x)\big) \]

但這有兩個困難:

  • (一)不可積:機率密度必須可積才能正規化, \(Z(\theta)\) 一般沒有解析解。
  • (二)不可分解:即使能正規化, \(\log p_\theta(x)\) 也難以分解成可求和的形式。

隱變數模型(latent variable model) 的做法是引入 \(z\) ,把複雜分佈因子化:

\[ p_\theta(x, z) = p_\theta(x \mid z)\, p(z), \qquad p_\theta(x) = \int p_\theta(x,z)\,\mathrm{d}z \]

\(p_\theta(x \mid z)\) (解碼器/生成器)可以選成一個簡單但 expressive 的形式(例如神經網路輸出一個高斯的參數), \(p(z)\) (先驗)通常取標準高斯或均勻分佈。難題現在變成:邊際化積分。

而實務上我們真正需要的是後驗 \(p_\theta(z \mid x)\) ——「看到這張圖之後, \(z\) 應該是什麼?」。由貝葉斯公式:

\[ p_\theta(z \mid x) = \frac{p_\theta(x \mid z)\, p(z)}{p_\theta(x)} = \frac{p_\theta(x \mid z)\, p(z)}{\int p_\theta(x', z)\, \mathrm{d}z'} \]

分母就是那個不可解的積分。所以後驗不可解——這是整個領域的起點。


二、變分自動編碼器(Kingma & Welling, 2013)#

2.1 核心想法:用 \(q_\phi(z \mid x)\) 近似後驗#

VAE 的策略是:放棄精確後驗,改為用一個參數化的分佈 \(q_\phi(z \mid x)\) 來近似它,然後選擇 \(\phi\) 使得近似「夠好」。

\(q_\phi\) 稱為近似後驗(approximate posterior)或變分分佈(variational distribution),它通常被設計成與先驗同族:

\[ q_\phi(z \mid x) = \mathcal{N}\!\left(\mu_\phi(x),\, \mathrm{diag}\big(\sigma^2_\phi(x)\big)\right) \]

\(\mu_\phi\) 、 \(\sigma_\phi\) 由神經網路(編碼器 encoder)從 \(x\) 算出。

為什麼「用一個分佈去近似一個分佈」叫作「變分」? 因為我們不是直接最佳化對數概似(那需要解一個無法閉合的等式),而是轉而最佳化某個依賴 \(q_\phi\) 的函數的下界,這類手法通稱為變分最佳化(variational optimization)。而「自動編碼器」之名來自網路結構:encoder 把 \(x\) 壓成 \(z\) ,decoder 把 \(z\) 展開回 \(x\) ,是一個自動編碼器;「變分」是因為中間層不是確定的,而是分佈。

2.2 從邊際似然到 ELBO:完整推導#

我們真正想最佳化的是邊際對數概似:

\[ \log p_\theta(x) = \log \int p_\theta(x,z)\,\mathrm{d}z \]

**第一步:乘除一個 \(q_\phi(z \mid x)\) **。

\[ \log p_\theta(x) = \log \int p_\theta(x,z)\,\mathrm{d}z = \log \int q_\phi(z \mid x)\, \frac{p_\theta(x,z)}{q_\phi(z \mid x)}\,\mathrm{d}z \]

第二步:套用 Jensen's inequality。對凹函數 \(\log\) ,有 \(\mathbb{E}[f(Y)] \le f(\mathbb{E}[Y])\) ,因此

\[ \log \mathbb{E}_{q_\phi}\!\left[\frac{p_\theta(x,z)}{q_\phi(z \mid x)}\right] \le \mathbb{E}_{q_\phi}\!\left[\log \frac{p_\theta(x,z)}{q_\phi(z \mid x)}\right] \]

代入上式,得到

\[ \log p_\theta(x) \;\ge\; \mathbb{E}_{q_\phi(z \mid x)}\!\left[\log \frac{p_\theta(x,z)}{q_\phi(z \mid x)}\right] \;\equiv\; \mathcal{L}(\theta, \phi) \]

這就是 Evidence Lower Bound(ELBO,證據下界)。「證據」指 \(p_\theta(x)\) (把 \(x\) 當作「證據」),「下界」是因為我們只能最佳化它而不能直接最佳化 \(\log p_\theta(x)\) 。

第三步:分解 ELBO。利用 \(p_\theta(x,z) = p_\theta(x \mid z)\,p(z)\) 與 \(q_\phi(z\mid x)\) :

\[ \mathcal{L}(\theta,\phi) = \mathbb{E}_{q_\phi}\!\left[\log p_\theta(x \mid z)\right] + \mathbb{E}_{q_\phi}\!\left[\log p(z)\right] - \mathbb{E}_{q_\phi}\!\left[\log q_\phi(z \mid x)\right] \]

第二、三項合併即為負的 KL 散度(因為 \(\mathrm{KL}(q \| p) = \mathbb{E}_q[\log q - \log p]\) ):

\[ \mathcal{L}(\theta,\phi) = \underbrace{\mathbb{E}_{q_\phi(z \mid x)}\!\left[\log p_\theta(x \mid z)\right]}_{\text{reconstruction}} \;-\; \underbrace{\mathrm{KL}\!\left(q_\phi(z \mid x)\,\|\, p(z)\right)}_{\text{regularization}} \]

這個分解是整章最重要的一個式子,因為它把「重建影像」和「保持潛變數分佈接近先驗」兩個诉求明確地對立又統一起來:

  • 重構項要 \(z\) 保留足夠資訊重建 \(x\) ;
  • KL 項要 \(q_\phi(z|x)\) 接近 \(p(z)\) ,否則無法從先驗採樣。

兩者拉鋸,產生了 VAE 最重要的行為特性:多樣性來自 KL 項,銳利度來自重構項。

等價的分解(更常用):ELBO 也可以寫成

\[ \mathcal{L}(\theta,\phi) = \mathbb{E}_{q_\phi(z\mid x)}\!\left[\log p_\theta(x \mid z)\right] - \mathrm{KL}\!\left(q_\phi(z\mid x)\,\|\,p(z)\right) = - \mathrm{KL}\!\left(q_\phi(z\mid x)\,\|\,p_\theta(z\mid x)\right) + \log p_\theta(x) \]

這第二個形式揭示了 ELBO 的另一層意義:它量化了近似後驗與真實後驗之間的差距。當 \(q_\phi = p_\theta(\cdot\mid x)\) 時,ELBO 恰好等於 \(\log p_\theta(x)\) ,兩項完全相等。這就是為什麼把這個下界推得越高越好。

第四步:資料集的 ELBO。整個資料集的對數概然估計是

\[ \log p_\theta(\mathcal{D}) = \sum_{i=1}^{N} \log p_\theta(x^{(i)}) \]

實作中我們最大化其下界,並實務上常用「每次抽一個 minibatch、求平均」的方式。訓練損失(最小化形式):

\[ \mathcal{L}_{\text{ELBO}} = -\, \mathbb{E}_{i}\Big[\mathbb{E}_{q_\phi(z|x^{(i)})}\big[\log p_\theta(x^{(i)} \mid z)\big] - \mathrm{KL}\big(q_\phi(z \mid x^{(i)}) \| p(z)\big)\Big] \]

2.3 Jensen 不等式等號成立的條件(以及「差距」是什麼)#

Jensen 不等式取等號當且僅當被隨機變數 \(\frac{p_\theta(x,z)}{q_\phi(z|x)}\) 在 \(q_\phi\) 下是常數,即 \(q_\phi(z\mid x) \propto p_\theta(z \mid x)\) 。這再次印證:ELBO 的 tightness 就是近似後驗的品質。

2.4 重參數化技巧(Reparameterization Trick)#

這是 VAE 最重要、也最容易被誤解的技術細節。

問題:ELBO 包含期望 \(\mathbb{E}_{q_\phi(z|x)}[\cdot]\) ,而 \(z\) 是隨機變數。若直接把隨機抽樣寫進計算圖,反向傳播會斷掉——因為「從哪裡抽的」不是一個函數的導數。

重新表述:既然 \(q_\phi\) 是對角高斯,抽樣可以寫成

\[ z = \mu_\phi(x) + \sigma_\phi(x) \odot \epsilon, \qquad \epsilon \sim \mathcal{N}(0, I) \]

此時 \(z\) 是一個確定性函數,輸入是 \(x\) (確定)與 \(\epsilon\) (固定的隨機噪音)。我們可以對 \(x\) 求導:

\[ \frac{\partial z}{\partial x} = \frac{\partial \mu_\phi(x)}{\partial x} + \frac{\partial \sigma_\phi(x)}{\partial x} \odot \epsilon \]

隨機性全部被隔離在 \(\epsilon\) 裡,而 \(\epsilon\) 在一次前向傳播中是常數。

這就是重參數化技巧:把隨機變數 \(z \sim q_\phi(z|x)\) 改寫成 \(z = g_\phi(x, \epsilon)\) ,其中 \(g_\phi\) 是對 \(x\) 可微的確定性函數,而 \(\epsilon\) 的分佈不依賴任何可學習參數。

注意它的一般性:這不只適用於高斯。任何能寫成「參數化分佈 + 固定噪音」的形式都適用(指數分佈、Gamma、Dirichlet 皆可)。這是後來所有連續隱變數模型的基礎,也——這是本章通向 7.3 節的關鍵——擴散模型的雜訊預測正是同一個技巧的另一個應用(雖然那裡的「噪音」不是待求導的變數,而是訓練的目標)。

常見誤解:「重參數化是 VAE 的專利。」不對。它是從計算圖角度處理隨機變數的通用手法,在 VAE 出現前就以其他形式存在(REINFORCE 的 score function 方法是它的對偶路徑),但 Kingma & Welling 把它標準化並與 ELBO 結合,才使 VAE 可訓練。

估計 ELBO 的兩種方式:

  • 取樣估計(實務標準):抽一個 \(\epsilon\) ,計算
\[ \hat{\mathcal{L}} = \log p_\theta(x \mid z(\epsilon)) - \mathrm{KL}\big(q_\phi(z\mid x)\,\|\,p(z)\big) \]

這個估計器是無偏的(KL 項是解析計算的),但方差大。實務上每個樣本抽 1 個 \(\epsilon\) 就好(因為網路會把噪聲平均掉)。

  • 重參數化抽樣:多抽 \(L\) 個 \(\epsilon\) 取平均,降低方差。

2.5 高斯 KL 的閉式解#

這是讓 VAE 真正可訓練的關鍵:KL 項不需數值積分。

對兩個對角高斯 \(q = \mathcal{N}(\mu_1, \sigma_1^2)\) 與 \(p = \mathcal{N}(\mu_2, \sigma_2^2)\) :

\[ \mathrm{KL}(q \| p) = \log\frac{\sigma_2}{\sigma_1} + \frac{\sigma_1^2 + (\mu_1 - \mu_2)^2}{2\sigma_2^2} - \frac{1}{2} \]

代入先驗 \(p(z) = \mathcal{N}(0, I)\) (即 \(\mu_2 = 0\) 、 \(\sigma_2 = 1\) ):

\[ \mathrm{KL}\!\left(\mathcal{N}(\mu_\phi(x), \sigma_\phi^2(x)) \,\|\, \mathcal{N}(0, I)\right) = -\frac{1}{2}\sum_{j=1}^{d}\left(1 + \log \sigma_\phi^2(x)_j - \mu_\phi(x)_j^2 - \sigma_\phi^2(x)_j\right) \]

(等價寫法: \(\frac{1}{2}\sum_j \big(\mu_j^2 + \sigma_j^2 - 1 - \log\sigma_j^2\big)\) 。)

逐項解讀:

  • \(\mu_j^2\) :編碼器把 \(x\) 編碼到離原點很遠的地方 → 懲罰。這是「把資料集中到原點」的機制。
  • \(\sigma_j^2 - 1 - \log \sigma_j^2\) :注意此項在 \(\sigma_j = 1\) 時取最小值 0。當 \(\sigma_j \ll 1\) (編碼器過於確定)時, \(-\log\sigma_j^2\) 變得很大 → 懲罰。當 \(\sigma_j \gg 1\) 時, \(\sigma_j^2\) 項變大 → 懲罰。這是「不要過度確定、也不要過度模糊」的機制。

KL 項完全不依賴解碼器,只依賴 encoder 與先驗。所以它是一個正則化器,把 encoder 的輸出「拉回」到標準高斯的形狀。這正是為什麼 \(q_\phi(z|x)\) 必須被限制成對角高斯——否則就沒有閉式解。

2.6 解碼器的選擇決定了一切#

2.6.1 Bernoulli 解碼器(原始論文)#

原始 VAE 論文假設 \(x \in \{0,1\}^d\) (MNIST),解碼器是逐像素獨立的 Bernoulli 分佈:

\[ p_\theta(x \mid z) = \prod_{i=1}^{d} \mathrm{Bern}\!\left(x_i; \theta_i(z)\right) \]

對數似然為

\[ \log p_\theta(x \mid z) = \sum_{i=1}^{d}\Big[x_i \log \theta_i(z) + (1 - x_i)\log(1-\theta_i(z))\Big] \]

這與二元的交叉熵完全相同。

2.6.2 高斯解碼器(影像的標準選擇)#

對連續影像,解碼器通常假設各像素獨立的高斯:

\[ p_\theta(x \mid z) = \mathcal{N}\!\left(x; \mu_\theta(z),\, \sigma_\theta^2 I\right) \]

若固定 \(\sigma\) 為常數(例如 \(\sigma = 1\) ),則重構項化為

\[ \log p_\theta(x \mid z) = -\frac{1}{2\sigma^2}\|x - \mu_\theta(z)\|_2^2 + \text{const} \]

也就是說,高斯解碼器的重構項就是一個像素級的均方誤差(MSE)。這個觀察極其重要,因為它完全解釋了 VAE 影像模糊的原因(見第五節)。

若讓網路同時輸出 \(\sigma_\theta(z)\) ,則得到異方差回歸(heteroscedastic regression),可以讓模型對「哪些像素不確定」給出更大的方差,但不解決模糊問題(模糊來自均值預測的中間性,不是來自方差建模不足)。

2.6.3 Categorical 解碼器(VQ-VAE 的先驅)#

對彩色影像,也可以把每個像素視為 \(K\) 種顏色的 categorical 變數,得到所謂的 discrete VAE。這是 2017 年 VQ-VAE(van den Oord et al.)的直系前身,也是後來 DALL-E 的先驅(見第 8 章)。

2.7 完整訓練目標(實務形式)#

以高斯解碼器為例,最小化形式的損失:

\[ \mathcal{L} = \underbrace{\frac{1}{2\sigma^2}\big\|x - \mu_\theta(z)\big\|_2^2}_{\text{reconstruction}} + \underbrace{\beta \cdot \frac{1}{2}\sum_{j}\left(1 + \log\sigma_\phi^2(x)_j - \mu_\phi(x)_j^2 - \sigma_\phi^2(x)_j\right)}_{\text{KL}} \]

注意這個公式裡沒有任何「對抗」或「競賽」。它是一個標準的、單人的、可用 Adam 穩定最佳化的目標函數。這正是 VAE 相對 GAN 的核心優勢,也是 7.1 節「問題一」的直接答案。


三、VAE 的兩副面孔,以及本章最重要的概念對比#

3.1 面孔一:變分推論#

在推論這一面,VAE 是一個嚴謹的貝葉斯方法:

  • 真實後驗 \(p_\theta(z \mid x)\) 通常不可解;
  • 我們用 \(q_\phi(z \mid x)\) 近似它;
  • ELBO 的 tightness 就是這個近似的品質;
  • \(\mathrm{KL}(q_\phi \| p_\theta) = \log p_\theta(x) - \mathcal{L}\) 給出了近似的誤差上界。

這一面是可以驗證的:你可以把 ELBO 拿來比較不同模型(在相同資料集上),ELBO 越高越好。

3.2 面孔二:生成模型#

在生成這一面:

  • 從先驗 \(p(z)\) 抽 \(z\) (因為 KL 項保證了 \(q_\phi(z|x) \approx p(z)\) );
  • 用解碼器 \(p_\theta(x \mid z)\) 產生 \(x\) 。

這一面沒有對應的似然保證:ELBO 是下界,所以「ELBO 高」不代表「樣本好」。事實上,ELBO 高但樣子模糊是 VAE 的常態。

3.3 兩種 KL 方向的行為差異:mode covering vs mode seeking#

這是理解整個生成模型領域的最重要單一概念。讓我們把它講清楚。

設定:資料分佈 \(p_{\text{data}}\) 有多個模式(例如人臉的膚色、姿勢、背景各有其分佈)。生成分佈 \(p_g\) 由一個受限的網路族表示。

Forward KL: \(\mathrm{KL}(p_g \| p_{\text{data}}) = \int p_g(x) \log \frac{p_g(x)}{p_{\text{data}}(x)} \mathrm{d}x\)

  • 積分的權重是 \(p_g\) ——生成器分佈所在的地方。
  • 當 \(p_g(x) > 0\) 但 \(p_{\text{data}}(x) \approx 0\) 時, \(\log \frac{p_g}{p_{\text{data}}} \to +\infty\) ,產生無限懲罰。
  • 因此 \(p_g\) 必須把自己的質量放在真實資料有支援的地方;真實資料沒有的地方, \(p_g\) 必須為 0。
  • 結果:覆蓋所有模式(mode covering)。因為漏掉一個真實模式會被無限懲罰。
  • 代價:在模式之間的區域, \(p_g\) 會被強迫放一些質量進去(否則 KL 中的 \(p_g \log p_g\) 項與覆蓋要求衝突),這些質量落在真實密度低的區域,於是產生模糊的、不太真實的樣本。

Reverse KL: \(\mathrm{KL}(p_{\text{data}} \| p_g) = \int p_{\text{data}}(x) \log \frac{p_{\text{data}}(x)}{p_g(x)} \mathrm{d}x\)

  • 積分的權重是 \(p_{\text{data}}\) ——真實資料所在的地方。
  • 當 \(p_{\text{data}}(x) > 0\) 但 \(p_g(x) \approx 0\) 時,同樣發散。因此 \(p_g\) 必須覆蓋真實資料的支撐集——看起來也要求覆蓋?
  • 但關鍵差異在於:當 \(p_{\text{data}}(x) \approx 0\) 而 \(p_g(x) > 0\) 時,reverse KL 沒有任何懲罰(因為積分權重是 \(p_{\text{data}}\) ,該處權重為 0)。所以 \(p_g\) 可以在真實資料沒有的地方任意「溢出」。
  • 結果:精準覆蓋少數模式(mode seeking)。 \(p_g\) 會把質量集中在真實密度最高的區域,代價是放棄其他模式。
  • 這正是 GAN 的 mode collapse 的數學解釋:GAN 在實務中偏向 reverse KL。

用一句話總結:

Forward KL 問「我的分佈在真實資料沒覆蓋的地方有多糟?」→ 逼你到處都去(覆蓋),代價是模糊。 Reverse KL 問「真實資料在我的分佈沒覆蓋的地方有多糟?」→ 逼你躲進最安全的高密度區(精準),代價是單調。

3.4 完整對照表:VAE vs GAN#

面向 VAE(forward KL 為主) GAN
訓練目標 ELBO(單人、有界下界) 零和 minimax(鞍點)
梯度來源 有界、可預期 對手依賴、可爆可消
密度 有明確 \(p_\theta(x)\) (可算 ELBO、可算 importance weight) 無
KL 方向 forward \(\mathrm{KL}(q_\phi \| p(z))\) (在隱變數空間) 近似對稱(JS),實務動態偏 reverse
模式行為 mode covering(覆蓋全部) mode seeking(崩坍)
樣本品質 模糊(MSE 平均化) 銳利
多樣性 高 低(易崩坍)
可驗證性 有(validation ELBO) 無(只能用 FID/IS)
取樣速度 單次前向(快) 單次前向(快)
超參敏感度 低 高
可解碼性 可做確定性重建(用 \(\mu_\phi(x)\) ) 不行(無編碼器)
主要弱點 模糊、posterior collapse 不穩定、mode collapse

這張表就是本章與下一章的橋樑。 注意 VAE 和 GAN 在「取樣速度」這一列是平手——都很快。擴散模型要解決的是品質與穩定性的問題,但它把取樣速度這個優點輸掉了(要數百步去噪)。

3.5 一個關鍵的混淆澄清:VAE 的 KL 與 GAN 的 KL 方向#

上表中的 KL 方向比較需要小心,因為 VAE 的 KL 是在隱變數空間( \(q_\phi(z|x)\) vs \(p(z)\) ),GAN 的 KL 是在資料空間( \(p_g\) vs \(p_{\text{data}}\) )。

它們的類比關係是這樣建立的:VAE 的解碼器 \(p_\theta(x|z)\) 若很窄( \(\sigma\) 很小),則把 \(q_\phi(z|x)\) 映射到 \(x\) 空間後,得到的 \(p_g(x) = \int q_\phi(z|x)p_\theta(x|z)\mathrm{d}z\) 與 \(p_{\text{data}}\) 的關係近似於 forward KL。而 GAN 的實務動態(判別器過快)把它推向 reverse KL。

這也解釋了一個實務現象:VAE 的重建很模糊,但如果把解碼器的方差 \(\sigma\) 縮小,重建會變得銳利但分佈會過度集中——這就是 VAE 的基本取捨,無法兩全。


四、VAE 的延伸#

4.1 Posterior Collapse:VAE 的隱疾#

現象:在有高度相關的資料(例如句子中相鄰詞彙、或影像中像素之間)時,ELBO 中的 KL 項會把 \(q_\phi(z|x)\) 推向先驗 \(p(z)\) ;若模型發現「直接用 \(z\) 的先驗解碼就夠了」,最優解就是讓 \(q_\phi(z|x) = p(z)\) (與 \(x\) 無關),此時 \(z\) 完全不攜帶資訊。

代價是: \(\mathrm{KL} \to 0\) (完美正則化),但重構項變差。ELBO 的權衡使得很多時候,放棄 \(z\) 的資訊反而是整體最優的(尤其當資料維度大、重構簡單時)。

診斷方法:測量每個 \(z\) 維度的 KL 值,若大部分維度的 KL 都接近 0(且總 KL 很小),就是 posterior collapse。

常見誤解:「posterior collapse 一定是壞事。」不總是。若資料本來就是低維的(例如 MNIST 數字),讓部分 \(z\) 維度 collapse 反而是正確的(那些維度本來就沒有資訊)。問題只在於「連真正有資訊的維度也 collapse」。

4.2 IWAE(Burda et al., 2014):更緊的下界#

ELBO 是用 Jensen 不等式得到的一階下界。Importance Weighting Autoencoder 用多個重要性權重得到更緊的界:

\[ \mathcal{L}_{K} = \mathbb{E}_{q_\phi(z_1,\dots,z_K \mid x)}\!\left[\log \frac{1}{K}\sum_{k=1}^{K} \frac{p_\theta(x, z_k)}{q_\phi(z_k \mid x)}\right] \]

這是 \(\log p_\theta(x)\) 的一個下界,且滿足 \(\mathcal{L}_{K} \ge \mathcal{L}_{\text{ELBO}}\) ; \(K\) 越大越緊(漸近地趨近真實對數似然)。

代價:需要 \(K\) 次前向傳播,訓練變慢。重要性權重的極端版本就是 7.3 節的擴散模型——那裡我們對 \(t\) 這個隨機變數做重要性加權,得到的界比 ELBO 緊得多,這正是 DDPM 能達到 SOTA 品質的技術原因之一。

4.3 β-VAE(Higgins et al., 2017):用 β 撬動解耦#

把 ELBO 的 KL 項乘上一個係數 \(\beta\) :

\[ \mathcal{L}_{\beta} = \mathbb{E}_{q_\phi}\!\left[\log p_\theta(x\mid z)\right] - \beta \cdot \mathrm{KL}\!\left(q_\phi(z \mid x) \| p(z)\right) \]
  • \(\beta = 1\) :標準 VAE。
  • \(\beta > 1\) :更強的先驗約束。 \(z\) 的每個維度被壓縮得更接近獨立、更有資訊量分配的味道,解耦(disentanglement)更強,代價是重建更差。
  • \(\beta < 1\) :更弱的約束,重建更好但 \(z\) 更糾纏。

4.4 Disentanglement:VAE 為何常能學到因子化的表徵#

回到第 3.3 節的語義鴻溝。VAE 常被觀察到能學到相對因子化(factorized)的表徵——不同 \(z\) 維度分別對應臉的膚色、姿勢、光照。為什麼?

機理一(容量競爭):VAE 的 decoder 是共享的,它要用同一組權重處理所有 \(z\) 。如果 \(z_1\) 和 \(z_2\) 各自冗餘地編碼了膚色,decoder 的權重會浪費在處理冗餘資訊上。最小化重構誤差會主動偏好讓每個 \(z\) 維度各自負責一個獨立的生成因子。這是「compression pressure」的機制。

機理二(KL 的各向異性懲罰):KL 項對每個維度獨立計算。讓 \(\mu_\phi(x)_j\) 變化能提供資訊,但同時被 \(\mu_j^2\) 懲罰。最佳化會把「每個維度承擔一件事」作為折衷,因為這樣 \(\mu_j\) 的變動最集中在少數維度上,整體 \(\sum \mu_j^2\) 最小。

但必須公允指出:這種解耦是不完美的、且依賴資料集與隨機種子的。Locatello et al. (2017) 的 Challenging Common Assumptions in the Unsupervised Learning of Disentangled Representations 明確指出:在沒有額外假設或 inductive bias 的無監督解耦,是不可證其存在且不可重現的。β-VAE 在實驗上有效,是因為它加入了「KL 項權重更大」這個明確的 inductive bias。

這裡與第 3.3 節的呼應:語義鴻溝不是靠更大的網路解決的,而是靠任務本身的結構(有標籤 / 有對應關係 / 有明確的因子化先驗)解決的。VAE 的解耦能力來自「 \(z\) 空間小 + decoder 共享 + KL 各向異性」這個結構,而不是來自它學到了某種語意。

4.5 下一個問題:VAE 仍然缺一塊#

VAE 有明確的密度(可以算 ELBO),但它的解碼器是手選的簡單形式(高斯 / Bernoulli / categorical)。這造成兩個限制:

  1. 表達能力受限:解碼器只能產生「某個高斯球內的樣本」,無法產生複雜的多峰後驗(例如把 \(z\) 映到一個彎曲的流形)。
  2. 取樣效率低:從先驗抽 \(z\) 映射到資料空間,需要網路「把球變形成流形」,這個映射在細節上很吃力 → 模糊。

Normalizing flow 正是為了補上這兩塊而生的:它讓任意複雜的分佈都能被精確地表達,並且取樣是精確的(不是近似的)。


五、為什麼 VAE 的影像模糊?精確的數學解釋#

5.1 逐像素機率的平均化效應#

回到 2.6.2 節:高斯解碼器的重構項是像素級 MSE。訓練過程中,decoder 學到的是 \(\mathbb{E}[x \mid z]\) ——在給定 \(z\) 下所有可能的 \(x\) 的條件期望。取期望是一個線性運算,因此產生的圖是各種可能的平均,也就是模糊。

更精確地說,若 decoder 的最優解是 \(\mu_\theta(z) = \mathbb{E}[x \mid z]\) ,且 \(z\) 只攜帶部分資訊(例如不攜帶高頻紋理細節),那麼 \(\mu_\theta(z)\) 在高頻細節上必然是「所有可能性」的模糊疊加。

5.2 這個模糊有兩個層級#

層級 原因 症狀 可否用 GAN 解決
像素級平均 逐像素高斯解碼器 + MSE 邊緣柔和、紋理糊 可以(GAN 直接對抗銳利樣本)
模式間平均 forward KL 的 mode covering 生成的樣本「位於模式之間」,不屬於任何一個真實樣本 只能部分解決

第二層級的模糊更根本:即使 decoder 能產生銳利的局部紋理,forward KL 仍會把 \(p_g\) 的質量分配到模式之間的區域。

5.3 VAE 與 GAN 的取捨總結#

  • 模糊 vs 銳利:VAE 輸。
  • 取樣速度:平手(都是單次前向)。
  • 模式覆蓋 vs 模式品質:VAE 覆蓋好但品質差;GAN 品質好但覆蓋差。
  • 穩定性:VAE 大勝。

擴散模型要做的事就清楚了:既要 VAE 的穩定與覆蓋,又要 GAN 的銳利。7.3 節將看到,它用一個非常聰明的辦法做到了:把「生成」重新表述為「去噪」,而去噪是逐像素的回歸問題——回歸問題既穩定(convex-like)又天然覆蓋所有模式,而 sharpness 則由「在多個時間步上反覆銳化」提供。


六、Normalizing Flow(Rezende & Mohamed, 2015)#

6.1 動機:VAE 缺的那塊拼圖#

VAE 的三個問題:

  1. 解碼器的分佈形式是手選的,表達能力受限;
  2. ** \(q_\phi(z|x)\) 也是手選的**(對角高斯),所以 encoder 無法學到複雜的後驗;
  3. 沒有精確密度——ELBO 是下界,實際對數似然未知。

Normalizing flow(NF,也稱 invertible network、change of variables model)的解法是:用一個可逆(bijective)神經網路把複雜分佈變換成簡單分佈。因為可逆,所以:

  • 從簡單分佈抽樣 → 經逆變換 → 得到複雜分佈的精確樣本;
  • 從複雜分佈的樣本 → 經正變換 → 算簡單分佈的密度 → 經變換公式得到精確的複雜分佈密度。

NF 提供的正是 VAE 缺乏的「明確密度」,而且是精確的、不只是下界。

6.2 變分定理:完整推導#

設 \(z \sim p_Z\) , \(x = f(z)\) ,其中 \(f : \mathbb{R}^d \to \mathbb{R}^d\) 是可微且可逆( \(g = f^{-1}\) 存在且可微)。我們要算 \(p_X(x)\) 。

第一步:寫出變換後的密度。由 \(z = g(x)\) ,

\[ p_X(x) = p_Z(g(x)) \left| \det \frac{\partial g}{\partial x} \right| \]

第二步:等價地寫成(這是標準形式)

\[ p_X(x) = p_Z(f(x)) \left| \det \frac{\partial f}{\partial x} \right| \]

(兩式等價: \(\det \frac{\partial g}{\partial x} = \left( \det \frac{\partial f}{\partial x} \right)^{-1}\) 。)

第三步:取對數(數值上必須如此,否則下溢)

\[ \log p_X(x) = \log p_Z(f(x)) + \log \left| \det \frac{\partial f}{\partial x} \right| \]

這個式子的結構極為漂亮:第一項是簡單分佈的密度(好算),第二項是一個雅可比行列式的對數(需要設計網路讓它好算)。

第四步:換成 Trace 的形式。 \(\log|\det J| = \mathrm{tr}(\log J)\) ,若 \(J = A A^{\top}\) ( \(A\) 是下三角矩陣,Cholesky 型分解),則

\[ \log|\det J| = \mathrm{tr}(\log A) + \mathrm{tr}(\log A^{\top}) = 2 \sum_i \log A_{ii} \]

這就是「為什麼 coupling layer 要設計成三角結構」的全部理由:三角矩陣的行列式就是對角線元素的乘積( \(\det A = \prod_i A_{ii}\) ),所以 \(\log|\det J|\) 的計算從 \(O(d^3)\) 降到 \(O(d)\) 。

6.3 Affine Coupling Flow(NICE, 2014;RealNVP, 2016)#

NICE(Non-Interactive Componentwise Encodings, Dinh et al., 2014) 的核心想法:把維度切成兩半 \(x = (x_a, x_b)\) ,只讓一部分維度參與變換:

\[ \begin{cases} y_a = x_a \\ y_b = x_b + f(x_a) \end{cases} \]

這是加法耦合(additive coupling)。逆變換極簡單:

\[ \begin{cases} x_a = y_a \\ x_b = y_b - f(y_a) \end{cases} \]

雅可比是分塊下三角:

\[ \frac{\partial y}{\partial x} = \begin{pmatrix} I & 0 \\ \frac{\partial f}{\partial x_a} & I \end{pmatrix} \]

行列式為 \(1\) (三角矩陣),所以 \(\log|\det| = 0\) ——完全免費。但代價是:所有變換都是平移(加法耦合不改变分佈形狀,只改變位置),表達能力受限。

RealNVP(Dinh et al., 2017 修訂版;與同期 Rezende et al. 並行) 把它升級為仿射耦合(affine coupling):

\[ \begin{cases} y_a = x_a \\ y_b = x_b \odot \exp\big(s(x_a)\big) + t(x_a) \end{cases} \]

其中 \(s, t\) 是任意函數(用神經網路參數化)。逆變換:

\[ \begin{cases} x_a = y_a \\ x_b = \big(y_b - t(y_a)\big) \odot \exp\big(-s(y_a)\big) \end{cases} \]

注意逆變換也是一樣的簡單——這是關鍵,因為取樣需要逆。

雅可比:分塊下三角

\[ \frac{\partial y}{\partial x} = \begin{pmatrix} I & 0 \\ \frac{\partial y_b}{\partial x_a} & \mathrm{diag}\big(\exp(s(x_a))\big) \end{pmatrix} \]

因此

\[ \log\left|\det \frac{\partial y}{\partial x}\right| = \sum_{j} s(x_a)_j \]

這是 O(d) 的計算。整個 coupling layer 的代價是:** \(s, t\) 網路的前向傳播 + \(O(d)\) 的對數行列式**。

為什麼「可逆性」是必要的(而非只是方便)? 因為可逆性保證:

  1. 密度唯一確定: \(p_X\) 與 \(p_Z\) 一一對應,沒有資訊遺失,所以 \(p_X\) 是明確的。
  2. 取樣無近似:抽 \(z \sim p_Z\) , \(x = f^{-1}(z)\) 就是 \(p_X\) 的精確樣本(不像 VAE 的解碼器其實是近似的)。
  3. 計算可行:我們強迫網路設計成三角結構,讓行列式可算。

代價:記憶體。 可逆性意味著網路不能丟棄資訊,所以每層的特徵維度不能減少(不像普通網路可以降維)。而且 \(f\) 與 \(f^{-1}\) 的實作必須緊密對應,工程複雜度高。

6.4 Glow(Kingma & Dhariwal, 2017)#

Glow 把 coupling flow 與 1×1 卷積結合,是 2017-2021 年影像生成的主流工具之一(PixelCNN++、BigGAN 都用到它)。

(一)仿射耦合層(ActNorm + coupling)

\[ y = x + \big(A x + b\big) \odot s(x) \]

其中 \(s(x)\) 是 coupling 網路的輸出(逐通道的 scale), \(A x + b\) 是一個 \(1\times1\) 卷積( \(A\) 為矩陣、 \(b\) 為偏置)。

(二)ActNorm:把正規化融進 flow

Glow 用 ActNorm(自適應正規化)取代 BatchNorm:每個通道有一組可學習的 scale 與 bias,並在初始化時用資料統計量設定,使得第一層輸出的每個通道均值 0、變異數 1。

ActNorm 的重要性在於它讓 flow 的數值尺度可控——這是 flow 訓練穩定的關鍵(BatchNorm 在 flow 中有問題:因為需要求 \(f^{-1}\) ,而 BN 的統計量依賴 batch,會破壞可逆性與取樣的一致性)。

(三)1×1 卷積的對數行列式

\(A x + b\) 這一項對雅可比的貢獻:

\[ \log|\det A| \]

因為 \(\mathrm{diag}\big(\mathrm{conv}(x)\big)\) 與 coupling 的 \(s(x)\) 相乘,整層的對數行列式為

\[ \log|\det| = \sum_i \log \big|s(x)_i\big| + \log|\det A| \]

Glow 貢獻的三個關鍵設計:

  1. 1×1 卷積提供通道間的混合(coupling 只在通道內做縮放,不能混合通道)。
  2. ActNorm 提供尺度控制。
  3. 把耦合層的結構簡化( \(s\) 只依賴前一半通道),實作更快。

6.5 FFJORD(Grin et al., 2019):連續時間版本#

FFJORD 把 flow 放進常微分方程的框架:

\[ \frac{\mathrm{d}z}{\mathrm{d}t} = f(z, t; \theta), \qquad z(0) = x, \quad z(T) = z_T \]

用 ODE solver(如 RK4)求解,並用連鎖法則的 ODE 版本計算對數密度增量:

\[ \frac{\mathrm{d}\log p(z(t))}{\mathrm{d}t} = - \nabla \cdot f(z(t), t; \theta) \]

優點:可以用任意連續時間的 ODE solver(包括高階、自適應步長),表達能力不受離散層數限制。 缺點:需要多次 ODE 求解,訓練與取樣都慢;密度估計有數值誤差。

6.6 主要 Normalizing Flow 的對比表#

NICE (2014) RealNVP (2016/17) Glow (2017) VAE + Flow (2016) FFJORD (2019)
核心機制 加法耦合 仿射耦合 仿射耦合 + 1×1 卷積 + ActNorm Flow 作為 encoder 連續時間 ODE
耦合類型 加法 仿射 仿射 視架構而定 ODE
\(\log\|\det\|\) 計算 0(三角 det=1) \(O(d)\) \(O(d)\) \(O(d)\) 數值(ODE + divergence)
表達能力 弱(只有平移) 中 中高 高(flow 能表達複雜後驗) 最高(連續時間)
影像生成用途 少 有(RealNVP-VAE) 主流(BigGAN、PixelCNN++) 有(VAE 改進) 少
取樣速度 快 快 快 快 慢(多次 ODE 求解)
訓練難度 低 低 中(ActNorm 尺度敏感) 中 高

6.7 Flow 對 VAE 的改進:這才是關鍵#

Flow 最大的實用價值不在於「獨立做生成模型」,而在於把 flow 塞進 VAE 的 encoder 或 decoder,解決 5.2 節的模糊問題。

(a)Flow 作為 decoder(iVAE / "Normalizing Flows for Variational Auto-Encoders", Kingma et al., 2020)

把解碼器從「高斯」升級為「高斯經可逆 flow 變換」:

\[ p_\theta(x \mid z) = \mathcal{N}\!\left(f_\psi(x);\, \mu_\theta(z),\, \sigma I\right) \]

其中 \(f_\psi\) 可逆。這讓解碼器能表達彎曲、多峰的條件分佈(例如「給定 \(z\) ,臉可以是任何膚色,但只能是膚色」),從而大幅減少模糊。代價是推理(重建)需要多次 flow 逆運算。

(b)Flow 作為 encoder(NF-VAE)

讓 encoder \(q(z|x)\) 是一個可逆變換的結果: \(z = f_\psi(x)\) ,其中 \(f_\psi\) 與先驗的密度差用 flow 建模。這樣可以精確表達複雜後驗,且能算精確的邊際似然(不只是 ELBO)。

(c)分層 VAE(LVS-VAE)與 VAE + Glow 的組合

在 VAE 的分層架構中,各層用 flow 做條件分佈,實驗顯示能顯著改善生成樣本的銳利度。

為什麼 flow 改善了模糊? 因為模糊的根源是解碼器的分佈形式太受限(一維高斯球)。flow 讓解碼器的輸出分佈可以任意複雜,同時保持精確密度與可抽樣性。這是 VAE 內部能達到的最好結果——但注意,它仍受制於 forward KL 的 mode covering 傾向,因此仍會有「模式間平均」的模糊,只是像素級的模糊被解決了。


七、實作#

7.1 VAE 的完整 PyTorch 實作#

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Normal, Bernoulli
from torchvision import datasets, transforms
from torch.utils.data import DataLoader


class Encoder(nn.Module):
    """把 x 映射到 (mu, logvar)。注意 logvar 而非 sigma:數值穩定且 KL 有閉式解。"""

    def __init__(self, in_dim=784, hidden=400, latent=20):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, hidden),
            nn.ReLU(),
        )
        self.fc_mu = nn.Linear(hidden, latent)
        self.fc_logvar = nn.Linear(hidden, latent)

    def forward(self, x):
        h = self.net(x)
        return self.fc_mu(h), self.fc_logvar(h)


class Decoder(nn.Module):
    """解碼器輸出 mu(各像素的均值)與 logvar(各像素的方差)。"""

    def __init__(self, latent=20, hidden=400, out_dim=784):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(latent, hidden),
            nn.ReLU(),
            nn.Linear(hidden, hidden),
            nn.ReLU(),
            nn.Linear(hidden, out_dim),
        )

    def forward(self, z):
        out = self.net(z)
        # 把 784 個 logit 轉成 (mu, logvar) 兩組
        return out[:, : out.shape[1] // 2], out[:, out.shape[1] // 2:]


def reparameterize(mu, logvar):
    """重參數化技巧:z = mu + sigma * eps,eps ~ N(0, I)。"""
    std = torch.exp(0.5 * logvar)
    eps = torch.randn_like(std)
    return mu + std * eps


def gaussian_kl(mu, logvar):
    """對角高斯 q=N(mu, diag(exp(logvar))) 與先驗 N(0, I) 之間的 KL(閉式解)。

    KL = 0.5 * sum( mu^2 + exp(logvar) - 1 - logvar )
    """
    return 0.5 * torch.sum(mu.pow(2) + logvar.exp() - 1.0 - logvar)


def elbo_loss(x, recon_mu, recon_logvar, mu, logvar, beta=1.0):
    """回傳 (loss, recon_nll, kl)。loss 是最小化形式。"""
    # 高斯解碼器的負對數似然(含常數項)
    recon_nll = 0.5 * torch.sum(
        (x - recon_mu).pow(2) * torch.exp(-recon_logvar)
        + recon_logvar
    ) / x.size(0)
    kl = gaussian_kl(mu, logvar) / x.size(0)
    return recon_nll + beta * kl, recon_nll, kl


class VAE(nn.Module):
    def __init__(self, in_dim=784, hidden=400, latent=20):
        super().__init__()
        self.encoder = Encoder(in_dim, hidden, latent)
        self.decoder = Decoder(latent, hidden, in_dim)
        self.latent = latent

    def forward(self, x):
        mu, logvar = self.encoder(x)
        z = reparameterize(mu, logvar)
        recon_mu, recon_logvar = self.decoder(z)
        return recon_mu, recon_logvar, mu, logvar

    @torch.no_grad()
    def sample(self, n, device):
        """從先驗採樣並解碼。注意:這裡沒有 mode collapse,樣本多樣但模糊。"""
        z = torch.randn(n, self.latent, device=device)
        recon_mu, _ = self.decoder(z)
        return torch.sigmoid(recon_mu)   # Bernoulli 解碼器的取樣近似


def train_vae(model, loader, device, epochs=10, beta=1.0, lr=1e-3):
    opt = torch.optim.Adam(model.parameters(), lr=lr)
    for epoch in range(epochs):
        total = 0.0
        for batch, _ in loader:
            x = batch.view(batch.size(0), -1).to(device)
            recon_mu, recon_logvar, mu, logvar = model(x)
            loss, recon, kl = elbo_loss(x, recon_mu, recon_logvar, mu, logvar, beta)
            opt.zero_grad(set_to_none=True)
            loss.backward()
            opt.step()
            total += loss.item() * x.size(0)
        print(f"epoch {epoch}: loss={total/len(loader.dataset):.3f}")
    return model

資料準備:

# MNIST
tf = transforms.ToTensor()
ds = datasets.MNIST("./data", train=True, transform=tf)
loader = DataLoader(ds, batch_size=128, shuffle=True)

# CelebA(影像版本)
# tf = transforms.Compose([
#     transforms.Resize(64),
#     transforms.CenterCrop(64),
#     transforms.ToTensor(),
#     transforms.Normalize((0.5,) * 3, (0.5,) * 3),
# ])

CelebA 版本的注意事項:灰階 MNIST 用 784 維,彩色 64×64 影像需要 \(64\times64\times3 = 12288\) 維的輸入與輸出,全連接層會非常巨大( \(12288 \times 400\) = 490 萬參數)。實務上影像 VAE 應該用卷積 encoder/decoder:

class ConvEncoder(nn.Module):
    def __init__(self, in_ch=3, latent=64, base=64):
        super().__init__()
        self.body = nn.Sequential(
            nn.Conv2d(in_ch, base, 4, 2, 1),   # 64 -> 32
            nn.GroupNorm(8, base), nn.SiLU(),
            nn.Conv2d(base, base * 2, 4, 2, 1),  # 32 -> 16
            nn.GroupNorm(8, base * 2), nn.SiLU(),
            nn.Conv2d(base * 2, base * 4, 4, 2, 1),  # 16 -> 8
            nn.GroupNorm(8, base * 4), nn.SiLU(),
            nn.Conv2d(base * 4, base * 4, 4, 2, 1),  # 8 -> 4
            nn.GroupNorm(8, base * 4), nn.SiLU(),
        )
        self.fc_mu = nn.Linear(base * 4 * 4 * 4, latent)
        self.fc_logvar = nn.Linear(base * 4 * 4 * 4, latent)

    def forward(self, x):
        h = self.body(x).flatten(1)
        return self.fc_mu(h), self.fc_logvar(h)

這裡用 GroupNorm 而非 BatchNorm:BatchNorm 在 VAE 中會讓 encoder 的輸出統計量依賴 batch,與 ELBO 的逐樣本解釋衝突;GroupNorm 是逐樣本的,語義更乾淨。

診斷 posterior collapse 的實用技巧:

@torch.no_grad()
def per_dim_kl(model, loader, device, max_batches=50):
    """印出每個 z 維度的平均 KL。若大量維度接近 0,就是 posterior collapse。"""
    sums = torch.zeros(model.latent, device=device)
    n = 0
    for i, (batch, _) in enumerate(loader):
        if i >= max_batches:
            break
        x = batch.view(batch.size(0), -1).to(device)
        mu, logvar = model.encoder(x)
        kl = 0.5 * (mu.pow(2) + logvar.exp() - 1 - logvar)   # (B, latent)
        sums += kl.sum(0)
        n += x.size(0)
    return (sums / n).cpu().numpy()

判讀:如果 20 維中有 18 維的 KL < 0.01,而總重構誤差也很低,就是典型的 posterior collapse(模型放棄了大部分 \(z\) )。

7.2 Affine Coupling Layer 的實作#

import torch
import torch.nn as nn
import numpy as np


class AffineCouplingLayer(nn.Module):
    """RealNVP 風格的仿射耦合層。

    前向: y_a = x_a
           y_b = x_b * exp(s(x_a)) + t(x_a)
    逆向: x_a = y_a
           x_b = (y_b - t(y_a)) * exp(-s(y_a))
    對數行列式: sum( s(x_a) )
    """

    def __init__(self, dim, hidden=64, scale=2.0):
        super().__init__()
        self.dim = dim
        self.net = nn.Sequential(
            nn.Linear(dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, hidden),
            nn.ReLU(),
            nn.Linear(hidden, 2 * dim),   # 前一半是 s,後一半是 t
        )
        # 初始化為接近單位變換,讓訓練初期容易穩定
        nn.init.zeros_(self.net[-1].weight)
        nn.init.zeros_(self.net[-1].bias)

    def _s_t(self, x):
        out = self.net(x)
        return out[:, : self.dim], out[:, self.dim:]

    def forward(self, x):
        """回傳 (y, logdet)。"""
        xa, xb = x[:, : self.dim], x[:, self.dim:]
        s, t = self._s_t(xa)
        s = torch.tanh(s) * 2.0        # 限制 scale,避免 exp 溢出
        yb = xb * torch.exp(s) + t
        y = torch.cat([xa, yb], dim=1)
        logdet = s.sum(dim=1)
        return y, logdet

    def inverse(self, y):
        """精確的逆變換(取樣時用)。"""
        ya, yb = y[:, : self.dim], y[:, self.dim:]
        s, t = self._s_t(ya)
        s = torch.tanh(s) * 2.0
        xb = (yb - t) * torch.exp(-s)
        return torch.cat([ya, xb], dim=1)


class NormalizingFlow(nn.Module):
    """堆疊多個 coupling layer 的 flow。"""

    def __init__(self, dim, n_layers=8, hidden=64):
        super().__init__()
        self.layers = nn.ModuleList(
            [AffineCouplingLayer(dim, hidden) for _ in range(n_layers)]
        )

    def forward(self, x):
        """x -> z,回傳 (z, total_logdet)。"""
        logdet_total = torch.zeros(x.size(0), device=x.device)
        z = x
        for layer in self.layers:
            z, logdet = layer(z)
            logdet_total = logdet_total + logdet
        return z, logdet_total

    def inverse(self, z):
        """z -> x。必須按相反順序反向遍歷。"""
        x = z
        for layer in reversed(self.layers):
            x = layer.inverse(x)
        return x

    def log_prob(self, x, base=None):
        """log p_X(x) = log p_Z(f(x)) + log|det df/dx|。"""
        z, logdet = self.forward(x)
        if base is None:
            base = torch.distributions.Normal(
                torch.zeros(z.size(1), device=z.device),
                torch.ones(z.size(1), device=z.device),
            )
        return base.log_prob(z).sum(dim=1) + logdet

    @torch.no_grad()
    def sample(self, n, device):
        """從標準高斯抽樣,經逆變換得到複雜分佈的樣本。"""
        z = torch.randn(n, self.layers[0].dim, device=device)
        return self.inverse(z)

驗證 flow 的正確性(這是必做的單元測試):

def test_flow_roundtrip():
    torch.manual_seed(0)
    dim = 8
    flow = NormalizingFlow(dim, n_layers=4, hidden=32)

    x = torch.randn(16, dim)
    z, logdet = flow(x)
    x_rec = flow.inverse(z)

    # 1. 可逆性:invert(forward(x)) == x
    assert torch.allclose(x, x_rec, atol=1e-5), "逆變換沒有正確還原"

    # 2. 對數行列式與自動微分的 autodet 對照
    z_ = x.clone().requires_grad_(True)
    y = flow.layers[0](z_)[0]
    autodet = torch.autograd.functional.det(y[0])   # 對第一個樣本
    assert torch.allclose(
        torch.log(autodet.abs()), flow.layers[0](x)[1][0], atol=1e-4
    ), "對數行列式計算錯誤"

    # 3. 變分定理的一致性:log p_X(x) 應等於以數值積分檢查的結果
    #    (省略,實務上用 change of variables theorem 已足夠)

    print("flow 驗證通過")

對數行列式的自動微分驗證是特別值得寫的一段:它能直接檢查你對 flow 數學的理解是否正確。torch.autograd.functional.det 對小維度可行,大維度則應改用對數版本的 torch.slogdet。

7.3 用 Flow 改善 VAE 的模糊:實作要點#

class FlowDecoderVAE(nn.Module):
    """把 VAE 的解碼器從高斯升級為「高斯 + flow」,減少像素級模糊。"""

    def __init__(self, latent=64, hidden=256, in_dim=12288, n_flow=8):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(latent, hidden), nn.SiLU(),
            nn.Linear(hidden, hidden), nn.SiLU(),
            nn.Linear(hidden, in_dim * 2),   # mu 與 logvar
        )
        self.in_dim = in_dim
        self.flow = NormalizingFlow(in_dim, n_layers=n_flow, hidden=64)

    def forward(self, z):
        out = self.net(z)
        mu, logvar = out[:, : self.in_dim], out[:, self.in_dim:]
        # 以 mu 為中心建立高斯,再用 flow 扭曲它
        base = torch.distributions.Normal(mu, torch.exp(0.5 * logvar))
        # 簡化版:對採樣的 x 施加 flow,並用變分定理修正密度
        eps = torch.randn_like(mu)
        x = mu + torch.exp(0.5 * logvar) * eps
        x_flowed, logdet = self.flow(x)
        return x_flowed, logdet

這個簡化版本展示了核心思想,但不是完整正確的 iVAE:完整版本需要處理「對 \(z\) 的期望」以及 flow 在 encoder 端的對偶。實務上建議直接引用 Kingma et al. (2020) 的實作。


八、本節小結#

  1. VAE 的核心是 ELBO: \(\log p(x) \ge \mathbb{E}_q[\log p(x,z) - \log q(z|x)]\) ,分解為重構項與 KL 項。這給了生成模型一個單一、有界、有絕對意義的訓練目標——正是對 7.1 節「問題一」的正面回答。
  2. 重參數化技巧 \(z = \mu_\phi(x) + \sigma_\phi(x)\odot\epsilon\) 把隨機性隔離到固定噪音中,讓梯度可以流通。這個手法是所有連續隱變數模型的基礎。
  3. 高斯 KL 的閉式解讓訓練完全免積分;它同時扮演「把 \(z\) 拉回先驗」的正則化器。
  4. 高斯解碼器 + MSE = 逐像素平均化 = 模糊。這是 VAE 影像模糊的精確來源,不是實作問題。
  5. forward KL vs reverse KL 是全章最重要的概念對比:forward KL(VAE)→ mode covering、代價模糊;reverse KL(GAN 的實務動態)→ mode seeking、代價 mode collapse。兩者無法同時最佳化。
  6. VAE 的兩副面孔:變分推論面(可驗證的 ELBO)與生成面(模糊的樣本)。這兩者之間的落差是 VAE 最大的結構性問題。
  7. Normalizing flow 用變分定理 \(p_X(x) = p_Z(f(x))|\det \partial f/\partial x|\) 補上了 VAE 缺的三塊:精確密度、精確取樣、複雜分佈的表達能力。Affine coupling 的三角雅可比讓 \(\log|\det|\) 的計算降到 \(O(d)\) 。
  8. Glow(1×1 卷積 + ActNorm + 仿射耦合)成為 2017-2021 影像生成的支柱;flow 塞進 VAE 的 encoder/decoder(iVAE)能消除像素級模糊,但無法消除 mode covering 造成的「模式間平均」。
  9. 下一節的擴散模型要同時解決 VAE 與 GAN 的兩個弱點:既要 VAE 的穩定與覆蓋,又要 GAN 的銳利。它的答案是把生成重新表述為去噪,把訓練變成一個簡單的逐像素回歸。

下一節:從非平衡熱力學(2015)出發,經 DDPM(2021)、DDIM(2020)、classifier-free guidance(2021)到 score-based SDE(2021),我們會看到擴散模型如何在數學上同時取得 VAE 的穩定與 GAN 的品質——並且解釋為什麼它最終在 2021 年正式超越 BigGAN 成為 ImageNet 上的品質標準。