§01學習重點
- 說出這個模型為什麼「生成容易、算分難」:兩步抽樣就能生成,但概似是一個沒有閉式解的積分
- 逐行重建從 Jensen 不等式到證據下界的推導,並解釋「乘一除一塞進一個 q」那一步為什麼合法
- 分辨下界兩種改寫裡的兩個 KL 散度:哪一個量下界的鬆緊、哪一個真的進了損失、為什麼前者永遠算不出來
- 說出編碼器的真實身分——訓練期的鷹架——並解釋為什麼生成新樣本時完全用不到它
- 解釋為什麼不能直接對「抽樣」這個動作求導,並親手驗證重參數化前後梯度估計的天壤之別
- 寫出單樣本近似後的完整損失,指出重建項與 KL 閉式各自從哪一條假設推出來
- 親手驗證對角常態 KL 散度的閉式公式與取樣估計收斂到同一個數字
§02課程內容
一、生成很容易:模型本體與邊際化
第 14 章量生成模型的那把尺——測試集概似——有一個前提:模型得算得出機率。那一章也預告過,有些做法算得出的不是精確值,而是一個下界或近似值。第 15 章的生成對抗網路則站在更遠的一端:抽得出樣本、算不出密度,而且那是它架構上的選擇。接下來三章各自把「算得出機率」補回來,路線不同:第 16 章走可逆變換與精確概似那條路,本章走另一條——不追求精確的概似,改追求一個可以計算的下界。這正是章名「用一個下界換來的」的意思:尺還是有,但刻度讀作「至少這麼多」,不是「就是這麼多」。這個退讓換到了什麼、又付出了什麼,是全章的主線。
先把模型本體搭起來。變分自編碼器(variational autoencoder,常縮寫 VAE) 的生成端只有三個零件:頭兩個是第 1 章鋪過的老朋友,第三個是第 5 章那個「輸出服從常態」的老假設換了個崗位。第一個零件是潛在變數 \(\mathbf{z}\) 的分布:取標準多變量常態,也就是每一維平均為 0、變異數為 1、彼此獨立的那個鐘形分布。因為它描述的是「在看任何資料之前,我們預設 \(\mathbf{z}\) 長什麼樣」,習慣上叫它先驗(prior)。第二個零件是一個深度網路,負責把低維的 \(\mathbf{z}\) 攤開成資料空間裡的一點,我們把它寫成 \(d[\mathbf{z}, \boldsymbol\phi_d]\),稱作解碼器(decoder)——下標 \(d\) 是本章內的助憶字母,指 decoder。要提醒一句:第 15 章也用過 \(d[\cdot]\) 這個寫法,那裡指的是判別器,是另一個網路;兩個 \(d\) 各自服務自己的章,不是同一個東西。第三個零件是觀測雜訊:解碼器的輸出不直接當成資料,而是當成一個常態分布的中心,資料在它周圍散開。合起來寫,先約定一個縮寫——本課用 \(\mathcal{N}(\cdot\,;\,\boldsymbol\mu, \boldsymbol\Sigma)\) 表示「以 \(\boldsymbol\mu\) 為平均、\(\boldsymbol\Sigma\) 為共變異矩陣的常態分布密度」,往後不再重複說明——模型就是:
逐項拆解:\(\mathbf{z}\) 是潛在變數,維度記作 \(D_z\),通常遠低於資料維度;\(\mathbf{I}\) 是單位矩陣,對角線全是 1、其餘全是 0,所以 \(\sigma^2\mathbf{I}\) 的意思是「每一維各自帶著同樣大小的變異數 \(\sigma^2\)、彼此獨立」;\(d[\mathbf{z}, \boldsymbol\phi_d]\) 是解碼器網路的輸出,\(\boldsymbol\phi_d\) 是它的參數。\(\sigma^2\) 是一個固定的超參數,它的職務是收容資料裡沒被解碼器抓到的那部分變化——沒有哪個網路能把資料的每一絲細節都塞進 \(\mathbf{z}\) 再攤回來,塞不下的零頭總得有個去處,這個常態雜訊就是去處。
模型真正關心的是資料本身的分布 \(Pr(\mathbf{x}\mid\boldsymbol\phi_d)\)。上面兩條式子給的是 \(\mathbf{z}\) 的分布和「給定 \(\mathbf{z}\) 之後 \(\mathbf{x}\) 的分布」,兩者相乘是聯合分布 \(Pr(\mathbf{x}, \mathbf{z}\mid\boldsymbol\phi_d)\);要拿到只含 \(\mathbf{x}\) 的分布,得把 \(\mathbf{z}\) 的所有可能值累加掉——連續變數的累加就是積分。這個動作叫邊際化(marginalization),名字來自一個老習慣:離散機率表把某個方向加總後,結果寫在表格的邊緣。寫出來是:
逐項拆解:積分符號寫成一個 \(\int\),但 \(\mathbf{z}\) 是向量,所以這其實是對 \(\mathbf{z}\) 的每一個維度各積一次的多重積分,寫成一個只是簡寫;被積的東西是聯合分布,也就是「\(\mathbf{z}\) 出現的機率密度,乘上在這個 \(\mathbf{z}\) 底下看到 \(\mathbf{x}\) 的機率密度」;積完之後 \(\mathbf{z}\) 消失,只剩 \(\mathbf{x}\) 的函數。
這條式子有一個很值得停下來品味的讀法。對每一個固定的 \(\mathbf{z}\) 值,\(Pr(\mathbf{x}\mid\mathbf{z},\boldsymbol\phi_d)\) 是一座以 \(d[\mathbf{z},\boldsymbol\phi_d]\) 為中心的鐘形;積分把無限多座這樣的鐘形加權疊起來,權重由先驗給。所以 \(Pr(\mathbf{x}\mid\boldsymbol\phi_d)\) 是由無限多座常態鐘形加權疊成的混合體。單獨一座鐘形很單純,但中心的位置由一個深度網路決定,\(\mathbf{z}\) 掃過整個潛在空間時,中心就沿著一個彎彎曲曲的低維曲面移動——疊出來的總體分布可以要多複雜有多複雜。簡單零件、複雜總和,這是整個設計的第一個巧思。
生成新樣本更是簡單得過分。照著分布定義的先後次序抽兩次:先從先驗抽一個 \(\mathbf{z}^\ast\),把它餵進解碼器,再以 \(d[\mathbf{z}^\ast,\boldsymbol\phi_d]\) 為中心抽一次觀測雜訊。兩步抽的都是常態分布,毫無計算困難。這種「沿著分布的因果次序,先抽上游、再抽下游」的流程有個正式名稱,叫祖先取樣(ancestral sampling),也有中文材料譯作祖先抽樣。請注意一件此刻看起來理所當然、之後會變成伏筆的事:這整套生成流程裡沒有出現任何叫「編碼器」的東西。 本章標題裡的「自編碼器」三個字,到目前為止完全沒有登場的理由——它為什麼會出現,是第五節的事。
比喻: 本章的例子都住在同一個場景:演講廳側邊的口譯廂,一位同步口譯員來不及逐字記下講者的話,只能壓成一頁速記符號,再據以復述。「生成」對應的是這個場景裡最奇特的一種操作:口譯員不聽任何講者,闔上耳機,按照通用速記系統的規律隨手落一組符號,然後把它展開成一段通順的新話。落符號對應「從先驗抽 \(\mathbf{z}^\ast\)」,展開對應「過解碼器」;整個過程確實不需要聽任何人說話——正如生成不需要編碼器。這個比喻有一處失準:速記符號是離散的,一個符號就是一個符號;潛在向量是連續的,兩組 \(\mathbf{z}\) 可以只差 0.01,而中間每一個沒被抽過的值也都對應到一段合理的話。連續性是這個模型能在樣本之間平滑過渡的原因,速記系統做不到這件事。
二、訓練卡關:一個算不出來的積分
模型搭好了,接著要訓練。訓練準則沿用第 5 章與第 14 章建立的口徑:最大化訓練集的平均對數概似,等價地說,最小化負的平均對數概似:
逐項拆解:\(I\) 是訓練資料筆數;\(\mathbf{x}_i\) 是第 \(i\) 筆資料;\(Pr(\mathbf{x}_i\mid\boldsymbol\phi_d)\) 就是上一節那個積分在 \(\mathbf{x}_i\) 這一點的值;取對數、加負號、取平均,都是老規矩。
麻煩在於:那個積分算不出來。 不是我們懶,是它真的沒有閉式解——被積的式子裡嵌著一個深度網路 \(d[\cdot]\),積分變數 \(\mathbf{z}\) 從網路的輸入端進去,你沒有任何代數工具可以把「常態密度套在網路輸出上」這種東西沿 \(\mathbf{z}\) 積出一條公式。損失寫得出來、算不出來,梯度就更不用說了。
算不出精確值,那就數值近似。有兩條看起來很自然的路,都走不通,但兩條死路各自留下了有用的線索。
死路一:打格點。 把 \(\mathbf{z}\) 的空間切成細格,逐格算被積函數再加總。一維時完全可行,但格點總數隨維度指數成長:每維取 100 個格點,\(D_z = 10\) 就是 \(100^{10}\) 格。第 14 章講密度估計時遇過同型的論證——需要的樣本量隨維度快速膨脹,六個量測還撐得住,六百個就不行——這裡只是同一堵牆換了個地方擋路。
死路二:從先驗亂抽。 積分的形式是「對先驗的期望值」,那就從先驗抽 \(N\) 個 \(\mathbf{z}\),拿 \(Pr(\mathbf{x}\mid\mathbf{z}_n,\boldsymbol\phi_d)\) 的平均當估計。這個做法在平均意義上是對的,實際上卻慘不忍睹,原因一句話就能講完:從先驗抽出來的絕大多數 \(\mathbf{z}\),解碼出來跟眼前這筆 \(\mathbf{x}\) 毫無關係,它們貢獻的概似趨近於零;整個估計實際上被極少數碰巧解碼到 \(\mathbf{x}\) 附近的幸運樣本扛著,有效的樣本數少得可憐。維度越高,幸運越稀有,估計越不可靠。
跑給你看。設想一個玩具模型:把一段發言的內容壓成 \(D\) 維的座標 \(\mathbf{z}\)(一種極端簡化的速記),解碼器是一條固定的非線性曲線,逐維把座標展開成可量測的量 \(\mathbf{x}\)(比如譯稿各段落的長度,以百字為單位)。因為解碼是逐維獨立的,真值可以用逐維的細格點積分算出來(一維才辦得到的奢侈),拿來跟「從先驗抽十萬個樣本」的估計對比。順帶定義一個診斷量:有效樣本數(effective sample size),它衡量「這批樣本的權重集中在多少個樣本身上」,全部權重平均分攤時它等於 \(N\),權重被一個樣本獨吞時它等於 1。
import numpy as np
rng = np.random.default_rng(0)
# 玩具模型(假設):每一維的解碼曲線都是同一條固定的非線性函數
def d(z):
return 2.0 * np.sin(3.0 * z) + 0.5 * z
sigma = 0.15 # 觀測雜訊標準差(假設值)
def log_lik_dim(x, z): # 單一維度上 log Pr(x|z)
return -(x - d(z))**2 / (2*sigma**2) - 0.5*np.log(2*np.pi*sigma**2)
# 先從模型本身抽一筆「典型的」觀測 x0(維度 D)
def draw_x(D):
z = rng.standard_normal(D)
return d(z) + sigma * rng.standard_normal(D)
# 真值:解碼逐維獨立,Pr(x) 是各維一維積分的乘積 → 用細格點算
zg = np.linspace(-6.0, 6.0, 120001)
log_prior_g = -zg**2/2 - 0.5*np.log(2*np.pi)
def log_truth(x0):
total = 0.0
for xj in x0:
total += np.log(np.trapz(np.exp(log_lik_dim(xj, zg) + log_prior_g), zg))
return total
N = 100000 # 每次估計抽這麼多 z
for D in (1, 2, 5, 10):
x0 = draw_x(D)
t = log_truth(x0)
z = rng.standard_normal((N, D))
logw = log_lik_dim(x0, z).sum(axis=1) # log Pr(x0|z_n)
m = logw.max()
est = m + np.log(np.mean(np.exp(logw - m))) # log-平均(防溢位)
ess = np.exp(logw - m); ess = ess.sum()**2 / (ess**2).sum()
print(f"D={D:>2} 真值 {t:8.3f} 估計 {est:8.3f} 有效樣本 {ess/N*100:7.3f}%")實跑輸出:\(D=1\) 時真值 -1.665、估計 -1.677,有效樣本還有 10.023%,勉強能用;\(D=2\) 時有效樣本掉到 0.170%;\(D=5\) 時只剩 0.001%——十萬個樣本實際上等於一個;到 \(D=10\),估計是 -53.849,真值是 -19.278,錯了三十多個對數單位,已經不是誤差,是災難。注意這還只是十維;真實應用的潛在空間動輒幾十幾百維。
死路二留下的線索比死路一值錢。問題不在「抽樣取平均」這個框架,在於抽的分布不看 \(\mathbf{x}\):先驗對眼前這筆資料一無所知,抽出來的 \(\mathbf{z}\) 自然絕大多數不相干。想有效率地估這個積分,抽 \(\mathbf{z}\) 的分布必須知道 \(\mathbf{x}\) 長什麼樣,專挑「有可能生出這筆 \(\mathbf{x}\)」的 \(\mathbf{z}\) 來抽。這個「哪些 \(\mathbf{z}\) 值有可能生出眼前這筆 \(\mathbf{x}\)」的分布是有名字的:潛在變數的後驗分布(posterior distribution),寫作 \(Pr(\mathbf{z}\mid\mathbf{x},\boldsymbol\phi_d)\)。先驗是「看資料之前」的信念,後驗是「看了這筆資料之後」的修正——每一筆資料的後驗都不同。
這裡要就地切開一個同名不同物的地雷。第 9 章談過「最大後驗」與「貝葉斯取徑」,那裡的後驗是參數的後驗——整個訓練集看完之後,對 \(\boldsymbol\phi\) 的信念。本章的後驗是潛在變數的後驗——固定參數、看了單獨一筆 \(\mathbf{x}\) 之後,對這筆資料背後那個 \(\mathbf{z}\) 的信念。兩者共用「後驗」這個詞,因為都是「看了證據之後的修正信念」,但對象一個是全域的參數、一個是逐筆的潛在變數,別混。
那就把後驗算出來吧。貝葉斯規則給出:
逐項拆解:分子的兩項都好算——一項是常態密度套在解碼器輸出上,一項是標準常態密度;分母是把分子對 \(\mathbf{z}\) 積分的結果,保證整條式子積起來等於一。看清楚分母是誰了嗎?它正是我們一開始算不出來的那個積分。 卡點自我複製了:想算概似需要後驗幫忙抽樣,想算後驗又需要概似當分母。正面強攻到此全部失敗,下一節換一種完全不同的思路——不算了,改為它找一個算得出來的下界。
三、退而求其次:Jensen 不等式與證據下界
整條退路建立在一個關於對數的初等事實上,先把它講透。
一個函數叫凹函數(concave function),意思是任取曲線上兩點連一條弦,弦整段落在曲線下方;等價地說,它的二階導數處處不大於零。對數正是凹函數:\(\log\) 的二階導數是 \(-1/y^2\),恆為負。凹函數有一條非常好用的性質,叫 Jensen 不等式(Jensen's inequality):凹函數作用在期望值上,大於等於先作用再取期望。用 \(\mathbb{E}[\cdot]\) 表示期望值(也就是「按機率加權的平均」,第 8 章與第 14 章都用過這個記號),一般形與對數特例是:
逐項拆解:左邊是「先把 \(y\) 平均起來,再取對數」;右邊是「先對每個 \(y\) 取對數,再平均」。兩邊差在哪,是大二學生最常卡住的地方,值得用方向感講一次:對數壓縮大值、放過小值——把 100 壓成 4.6,把 1 留在 0 附近。先平均的話,大的 \(y\) 值還來得及把平均拉高,然後才被對數壓;先取對數的話,大值在進場前就先被壓扁了,拉不動平均。所以「先平均再壓」總是不小於「先壓再平均」。兩邊相等只有一種情形:\(y\) 根本不散開、是個定值,兩種順序就沒有差別(對數是嚴格凹的,所以這是唯一的等號條件)。
數字檢查一次,順便看差距跟分散程度的關係:
import numpy as np
rng = np.random.default_rng(0)
# 最小的兩點例:y 只取 1 或 9,各一半機率
y2 = np.array([1.0, 9.0])
print(f"兩點例:log(平均)={np.log(y2.mean()):.4f} 平均(log)={np.log(y2).mean():.4f}")
# 連續版:三種分散程度的正值隨機變數(對數常態)
for s in (0.1, 0.5, 1.5):
y = np.exp(rng.normal(0.0, s, 1_000_000))
a = np.log(y.mean()) # 先平均,再取對數
b = np.log(y).mean() # 先取對數,再平均
print(f"分散程度 s={s}: log(平均)={a:.4f} 平均(log)={b:.4f} 差距={a-b:.4f}")
# 退化情形:y 是常數時,兩邊相等
y = np.full(1000, 4.2)
print(f"常數 y:log(平均)={np.log(y.mean()):.4f} 平均(log)={np.log(y).mean():.4f}")實跑輸出:兩點例是 log(平均)=1.6094、平均(log)=1.0986,左邊確實大;三種分散程度下差距依序是 0.0050、0.1248、1.1225——越分散差距越大;常數情形兩邊同為 1.4351,等號成立。記住「差距隨分散程度變大、集中到定值時歸零」這件事,第四節它會變成主角。
現在把這把刀對準我們的積分。從對數概似出發,逐行走:
一行一行交代。第一行:概似本來就是聯合分布對 \(\mathbf{z}\) 邊際化的結果,這是第一節那條式子換個寫法。第二行:挑任何一個處處為正的機率分布 \(q(\mathbf{z})\),在被積函數裡同時乘它、除它——乘一除一,值一分不變,這一步在代數上完全免費。第三行才是重點:乘上 \(q(\mathbf{z})\) 再積分,整個式子的形狀變成了「某個量對 \(q\) 這個分布取期望值」——\(\int q(\mathbf{z})\,(\cdot)\,\mathrm{d}\mathbf{z}\) 就是 \(\mathbb{E}_{q}[\cdot]\)。於是第二行是 \(\log \mathbb{E}_q[\cdot]\),套 Jensen 不等式,換成 \(\mathbb{E}_q[\log(\cdot)]\),方向是變小或不變。
第二行那個憑空冒出來的 \(q\) 通常會讓初學者非常不安:它是誰?哪來的?為什麼可以隨便塞一個分布進去?不安是對的,但方向要擺正——正因為推導對任何 \(q\) 都成立,我們反而拿到了一份自由:挑 \(q\) 的自由。挑得好不好,決定這個不等式鬆還是緊,這是下一節的主題;此刻只需要確認每一步的合法性。
最後一行那個量有名字:證據下界(evidence lower bound,通稱 ELBO)。「證據」是統計學對 \(\log Pr(\mathbf{x})\) 這個量的稱呼,來歷屬支線,本課不展開,原書對照節有頁碼指路。有兩件事現在就要看清楚。第一,這個下界算得出來——被積函數裡只有解碼器的前向計算、常態密度、和 \(q\) 自己,沒有那個要命的積分(期望值本身還要用抽樣近似,第七節處理,但那是可控的近似,不是無解的積分)。第二,下界同時吃兩組東西:\(q\) 這一側的選擇,和解碼器參數 \(\boldsymbol\phi_d\)。訓練策略就此定案:放棄最大化算不出來的對數概似,改為同時對 \(q\) 與 \(\boldsymbol\phi_d\) 最大化這個算得出來的下界。
還有一個順手的收穫。KL 散度——第 5 章從它導出交叉熵、第 14 章拿它量兩個分布的距離——它的非負性就是 Jensen 的直接推論。對任意兩個分布 \(q\) 與 \(p\):\(D_{\mathrm{KL}}\left(q \,\|\, p\right) = \mathbb{E}_q\!\left[\log \frac{q}{p}\right] = -\mathbb{E}_q\!\left[\log \frac{p}{q}\right] \ge -\log \mathbb{E}_q\!\left[\frac{p}{q}\right] = -\log \int p(\mathbf{z})\,\mathrm{d}\mathbf{z} = -\log 1 = 0\)。同一把刀用兩次。這個非負性馬上就要當理由用。
四、同一個下界的兩種改寫:鬆與緊
下界推出來了,但兩個問題懸著:它離真的對數概似有多遠?實際訓練時到底要算什麼?答案藏在同一個下界的兩種改寫裡——聯合分布 \(Pr(\mathbf{x},\mathbf{z}\mid\boldsymbol\phi_d)\) 有兩種拆法,各拆一次,各回答一個問題。這是本章最重的一段推導,也是兩個 KL 散度最容易張冠李戴的地方,我們放慢速度。
改寫一:按「後驗 × 證據」拆。 條件機率的定義給出 \(Pr(\mathbf{x},\mathbf{z}\mid\boldsymbol\phi_d) = Pr(\mathbf{z}\mid\mathbf{x},\boldsymbol\phi_d)\,Pr(\mathbf{x}\mid\boldsymbol\phi_d)\),代進下界:
中間那步把對數裡的乘積拆成兩項相加。第三行用了兩個理由,都要明說。第一項:\(\log Pr(\mathbf{x}\mid\boldsymbol\phi_d)\) 完全不含 \(\mathbf{z}\),可以提出積分外,剩下 \(\int q(\mathbf{z})\,\mathrm{d}\mathbf{z}\),而 \(q\) 是機率分布,積起來等於一,於是第一項就是對數概似本人。第二項:對照 KL 散度的定義 \(D_{\mathrm{KL}}\left(q\,\|\,p\right)=\int q \log(q/p)\),我們手上的是 \(\int q\log(p/q)\),分子分母顛倒,差一個負號——所以收攏成負的 KL 散度,號向別搞反。
這條改寫是「鬆緊判準」。由 KL 非負,立刻讀出:下界 ≤ 對數概似,恆成立,跟你怎麼挑 \(q\) 無關——這確認了「下界」名副其實。更有價值的是縫隙的身分:下界與真概似之間差的,不多不少,正是 \(q\) 到真後驗的 KL 散度。\(q\) 挑得越貼近真後驗,縫隙越小;\(q\) 恰好等於真後驗時,縫隙歸零,下界貼死概似。第二節的線索在這裡兌現了:那時說「抽 \(\mathbf{z}\) 的分布必須知道 \(\mathbf{x}\) 長什麼樣」,現在有了定量版本——理想的 \(q\) 就是後驗,而後驗因 \(\mathbf{x}\) 而異,所以理想的 \(q\) 也必須逐筆不同。給每筆資料各自最佳化一個 \(q\) 太奢侈,更聰明的做法是讓 \(q\) 直接成為 \(\mathbf{x}\) 的函數——訓練一個網路統一代勞。這個網路是誰,下一節揭曉。
改寫二:按「概似 × 先驗」拆。 同一個聯合分布也可以拆成 \(Pr(\mathbf{x}\mid\mathbf{z},\boldsymbol\phi_d)\,Pr(\mathbf{z})\),代進下界:
拆項的手法跟改寫一完全相同:對數裡的乘積拆成兩項,第二項按定義收成 KL 散度(這次分子分母的方向剛好讓它帶負號站在外面)。兩項各有職務。期望重建項問的是:從 \(q\) 抽出來的 \(\mathbf{z}\) 解碼回去,能多準地押中眼前這筆 \(\mathbf{x}\)?押得越準,\(\log Pr(\mathbf{x}\mid\mathbf{z},\boldsymbol\phi_d)\) 越大。先驗拉力項把 \(q\) 往標準常態拽:\(q\) 離先驗越遠,罰得越重。它在守護一個生成模型的命脈——生成時是從先驗抽 \(\mathbf{z}\) 的,如果訓練時各筆資料的 \(q\) 各自跑到潛在空間的偏僻角落,先驗覆蓋的區域反而沒人負責解碼,抽出來的樣本就會落在解碼器從沒練過的地方。把每筆資料的 \(q\) 都拽向先驗,等於強迫「先驗抽得到的每一點都解得回像樣的資料」。
兩種改寫是同一個量的兩種拆法,不是兩個量。改寫一回答「這個下界離真概似多遠」——理論保證。改寫二回答「網路實際要算什麼」——工程藍圖。連兩個 KL 散度也各司其職,並排放好:改寫一的 KL 量的是 \(q\) 到真後驗的距離,它永遠算不出來(真後驗正是那個算不出來的東西),只負責告訴你縫隙存在、以及縫隙由什麼決定;改寫二的 KL 量的是 \(q\) 到先驗的距離,兩邊都是常態分布,有閉式解(第七節寫給你),真的進了損失函數。一個管理論、一個管實作,混淆這兩個 KL 是本章的頭號地雷。
比喻: 口譯員的筆記同時被兩股力量拉扯。一股是重建的忠實度:憑這頁速記符號,能把講者的內容還原得多完整?另一股是系統的通用性:筆記必須寫成通用速記系統的樣子——換一位受過同樣訓練的譯者拿到這頁筆記,也展得開。自創暗號可以把眼前這場演講記得再貼身,也不合格,因為「生成」那種操作是不聽講者、直接按通用系統落符號再展開的;暗號體系裡隨手落下的符號,誰也展不開。前者對應期望重建項,後者對應先驗拉力項,兩股力量的平衡就是下界的最大化。這個比喻有一處失準:口譯員是有意識地在忠實與通用之間做取捨,網路沒有意圖——它只是沿著一條把兩項加在一起的損失的梯度走,平衡是最佳化的結果,不是誰的決定。
最後談鬆緊如何影響學到的東西。等一下我們會把 \(q\) 限制在一個很小的家族裡——單峰的常態分布。如果某筆資料的真後驗是多峰的(第二節的玩具模型就是這樣:解碼曲線是正弦,好幾段相距很遠的 \(\mathbf{z}\) 都能解碼到同一個 \(\mathbf{x}\) 附近,後驗就有好幾座峰),單峰的 \(q\) 怎麼擺都貼不緊,縫隙壓不到零。這時會發生一件微妙的事:訓練最大化的是下界,而下界=概似減縫隙,所以解碼器參數有兩條路可以把下界推高——把概似做大,或把縫隙做小。後一條路意味著解碼器會被推著「遷就」變分家族:往讓自己的後驗好被單峰常態近似的方向變形,而不是純粹往概似最大的方向走。要說清楚:這是領域內普遍接受的定性理解,不是一條定理——它描述訓練壓力的方向,不保證每次訓練都以可觀測的幅度發生。但它提醒你一件重要的事:挑了多簡單的 \(q\),就等於對模型下了多強的隱形約束。
五、架構成形:變分近似與編碼器
上一節留下兩個待辦:\(q\) 要在哪個範圍裡挑、以及「讓 \(q\) 成為 \(\mathbf{x}\) 的函數」怎麼實作。兩個一起解決,模型的完整架構就成形了。
第一個待辦的答案叫變分近似(variational approximation):不在「所有可能的分布」這個大到沒邊的集合裡找 \(q\),只在一個參數化的家族裡找。本章選多變量常態,平均為 \(\boldsymbol\mu\)、共變異為對角矩陣 \(\boldsymbol\Sigma\)——對角的意思是各維獨立、每維各有一個變異數,參數量只有 \(2D_z\) 個,找起來不費力。順帶交代「變分」這個詞的血統:數學裡對「函數」而不是對「數」做最佳化的那門學問叫變分法(calculus of variations),我們在分布的空間裡挑 \(q\),挑的對象是一個函數,名字由此而來。
第二個待辦:最佳的 \(q\) 逐筆不同,那就讓第二個網路吃 \(\mathbf{x}\)、吐出這筆資料專屬的 \(\boldsymbol\mu\) 與 \(\boldsymbol\Sigma\):
逐項拆解:\(e[\cdot]\) 是這個新網路,參數記作 \(\boldsymbol\phi_e\),下標 \(e\) 同樣是章內助憶字母;它輸出 \(2D_z\) 個數字——\(D_z\) 個平均、\(D_z\) 個對角變異數。這個網路叫編碼器(encoder)——第一節埋的伏筆到這裡回收一半:它把資料壓回潛在空間,方向跟解碼器相反。整條訓練管線於是接通:\(\mathbf{x}\) 進編碼器、得到 \(q\) 的參數,從 \(q\) 抽一個 \(\mathbf{z}\),過解碼器,得到重建。輸入自己、壓縮、再還原自己——這個結構叫自編碼器(autoencoder),章名的後半就是這麼來的。前面冠上「變分」,指的是中間那一站不是一個點、是一個分布:編碼器交出的不是「這筆資料對應哪個 \(\mathbf{z}\)」,而是「這筆資料可能對應哪些 \(\mathbf{z}\)」的不確定範圍。
比喻: 老練的口譯員看著自己筆記上的每個速記符號,心裡有的不是一句固定的話,而是一個範圍——「這個符號在當時的語境下,可能指這幾種說法」。編碼器交出 \(\boldsymbol\mu\) 與 \(\boldsymbol\Sigma\) 做的正是這件事:\(\boldsymbol\mu\) 是最可能的位置,\(\boldsymbol\Sigma\) 是把握程度,把握越低、範圍越寬。壓縮必然丟資訊,誠實的壓縮者交代自己丟了多少——這就是編碼器輸出一個分布而不是一個點的理由。這個比喻有一處失準:譯者筆記心裡的範圍可以裝下兩種截然不同的解讀(這個符號要嘛指甲案、要嘛指乙案,中間沒有過渡),但編碼器交出的常態分布永遠只有一座峰——它表達不了「兩種都有可能、中間不可能」。這個表達力的天花板,正是上一節縫隙壓不到零的那種情形。
有了兩個網路,符號也要跟著到位。兩個網路的參數都用 \(\boldsymbol\phi\) 加助憶下標:編碼器 \(\boldsymbol\phi_e\)、解碼器 \(\boldsymbol\phi_d\)。有些文獻讓兩個網路各用一個希臘字母,本課不採用——\(\boldsymbol\phi\) 從第 1 章起就是「參數全集」,多網路時加下標分家,跨章讀起來才不會以為出現了新東西。下界從此寫成兩組參數的函數:對 \(\boldsymbol\phi_e\) 最大化是在壓縫隙(把 \(q\) 貼向真後驗),對 \(\boldsymbol\phi_d\) 最大化是在推概似兼壓縫隙(上一節末那個微妙的雙重身分)。另外三個容易撞在一起的符號也就地排開:\(f[\mathbf{x},\boldsymbol\phi]\) 從第 1 章起指「模型本身」,本章不拿它指解碼器;\(\sigma^2\) 是第一節那個固定的觀測雜訊超參數,住在解碼器那一側;\(\boldsymbol\Sigma\) 是編碼器吐出的後驗共變異,逐筆隨 \(\mathbf{x}\) 變。\(\sigma\) 與 \(\boldsymbol\Sigma\) 長得像、身分完全不同,第一次同框就把話說清楚。
還有一個名字的錯位,重要到值得單獨一段。等訓練結束、成品交付時,真正上場生成的只有先驗加解碼器——第一節就演示過,生成的兩步抽樣裡沒有編碼器的位置。編碼器、變分近似、乃至整個「自編碼器」的迴路,全是訓練期的鷹架:為了讓那個算不出的積分有一個算得出的下界,我們才蓋了這一整套。成品交付時,鷹架拆掉。所以「變分自編碼器」這個名字描述的其實是訓練時的樣子,不是成品的樣子——成品更貼切的描述,就是第一節那個「先驗+解碼器」的潛在變數模型。初學者常把「用 VAE 生成」想成「先編碼再解碼」,錯就錯在把鷹架當成了建築。
兩件小事收尾。其一:讓一個網路一次學會「對任何 \(\mathbf{x}\) 都能立刻報出近似後驗」,而不是每筆資料各自跑一輪最佳化,這個手法叫攤提式推論(amortized inference)——「攤提」取自會計用語,一次訓練的成本攤到之後的每一次查詢上;也有文獻譯作攤還式推論,同一件事。這一手不是免費的:單筆資料各自最佳化能把 \(q\) 逼得更貼,網路統一代勞則多一層「網路本身不夠準」的誤差,換到的是速度。其二:「編碼器/解碼器」這對名字在 Transformer 那一族語言模型(第 12 章的題材)的文獻裡也大量出現,指的是把序列讀入與生成的兩半架構。同名近義——都在「壓進去、展出來」——但那裡服務的是序列到序列的監督式任務,這裡服務的是後驗的變分近似,讀文獻時看清楚脈絡即可。
六、最後一關:對抽樣求導與重參數化
架構接通了,訓練卻還踩不下油門。攤開管線:\(\mathbf{x}\) → 編碼器 → 從 \(q\) 抽一個 \(\mathbf{z}\) → 解碼器 → 重建。中間加粗那一站是個隨機動作,不是普通函數——同樣的 \(\boldsymbol\mu\)、\(\boldsymbol\Sigma\) 進去,每次跑出不同的 \(\mathbf{z}\)。麻煩就出在這裡。
先說為什麼梯度非過去不可。期望重建項的訊號要回答的問題是:「編碼器該把 \(\boldsymbol\mu\) 挪去哪、把 \(\boldsymbol\Sigma\) 收多緊,重建才會更準?」這個訊號從損失出發,反向經過解碼器、經過那個抽樣步驟,才到得了 \(\boldsymbol\phi_e\)。抽樣站在必經之路的正中央,繞不開。
再說為什麼梯度過不去。求導數要問的是:「\(\boldsymbol\mu\) 微微挪動一點,輸出跟著動多少?」但抽樣的輸出是隨機的——\(\boldsymbol\mu\) 動了 0.001,這次抽出的 \(\mathbf{z}\) 跟上次的差異裡,有多少是 \(\boldsymbol\mu\) 挪動造成的、多少是這次運氣不同造成的?拆不開,「輸出對參數的導數」就沒有定義良好的值。有一個角度能把這件事講得很白:如果把亂數種子固定住,抽樣就退化成普通的確定性函數,求導毫無問題;沒定義的不是「這條計算路徑」,而是「參數與隨機性糾纏在同一站」這件事。
這個角度直接指出解法:把隨機性從主幹搬到旁支。這一手叫重參數化技巧(reparameterization trick)。不要「從 \(\mathcal{N}(\boldsymbol\mu, \boldsymbol\Sigma)\) 抽 \(\mathbf{z}\)」,改成兩步:先從標準常態抽一個跟參數毫無關係的雜訊 \(\boldsymbol\epsilon^\ast\),再用一個確定性的變換把它加工成目標樣本:
逐項拆解:\(\boldsymbol\Sigma^{1/2}\) 讀作「共變異矩陣的平方根」,在對角情形下毫不神祕——就是把各維的變異數 \(\Sigma_{jj}\) 各自開根號(得到各維的標準差)排成的對角矩陣;整條式子落到每一維就是 \(z_j^\ast = \mu_j + \sqrt{\Sigma_{jj}}\,\epsilon_j^\ast\),「平均加上標準差倍的標準雜訊」,跟一維常態的直覺完全一致。
這樣造出來的 \(\mathbf{z}^\ast\) 跟直接從 \(q\) 抽,分布一模一樣。一維情形兩行就能驗證:\(\mathbb{E}[z^\ast] = \mu + \sqrt{\Sigma}\,\mathbb{E}[\epsilon] = \mu\)(標準常態的平均是零);變異數 \(= \Sigma \cdot \mathrm{Var}[\epsilon] = \Sigma\)(標準常態的變異數是一,平移不改變變異數)。再加上「常態分布經過這種一次式變換仍是常態」這個標準性質,平均、變異數、形狀三者全對上,分布相同。
關鍵是梯度現在通了。看新的計算圖:主幹上從 \(\boldsymbol\mu\)、\(\boldsymbol\Sigma\) 到 \(\mathbf{z}^\ast\) 只有乘法和加法,全是確定性運算,導數處處有定義;隨機源 \(\boldsymbol\epsilon^\ast\) 被隔離在一條旁支上,它是計算圖的葉節點,沒有人需要對它求導。隨機性沒有消失——每一步訓練仍然要抽一次 \(\boldsymbol\epsilon^\ast\)——它只是搬了家,搬到梯度不必經過的地方。
差別有多大,做個實驗。設想口譯訓練班的一個極簡練習:一維的 \(q\),平均 0.7、標準差 0.9(假設值),玩具損失是「抽出的 \(z\) 離目標 2.0 的平方距離」——期望損失對 \(\mu\) 的真梯度可以手算出來,是 \(2(\mu - 2)= -2.6\)。兩種做法對照:做法 A 不重參數化,直接用有限差分硬估——\(\mu \pm h\) 各自「新抽」一批樣本算平均損失再相除;做法 B 重參數化——固定同一組 \(\boldsymbol\epsilon\) 給兩邊用,隨機性與參數脫鉤。各重複兩百次看平均與抖動:
import numpy as np
rng = np.random.default_rng(0)
mu, sd = 0.7, 0.9 # 假設的一維 q:平均 0.7、標準差 0.9
def loss(z): # 玩具重建損失:目標還原到 2.0
return (z - 2.0)**2
true_g = 2 * (mu - 2.0) # 解析真梯度 d/dμ E[(z-2)^2] = 2(μ-2)
N, R = 2000, 200 # 每次估計抽 N 個樣本,重複 R 次看抖動
print(f"解析真梯度 = {true_g:.4f}")
for h in (1e-1, 1e-2, 1e-3):
naive, rep = [], []
for _ in range(R):
# 做法A:不重參數化——μ±h 各自「新抽」一批 z,再做有限差分
zp = mu + h + sd * rng.standard_normal(N)
zm = mu - h + sd * rng.standard_normal(N)
naive.append((loss(zp).mean() - loss(zm).mean()) / (2*h))
# 做法B:重參數化——固定同一組 ε,隨機性與參數脫鉤
eps = rng.standard_normal(N)
zp = mu + h + sd * eps
zm = mu - h + sd * eps
rep.append((loss(zp).mean() - loss(zm).mean()) / (2*h))
naive, rep = np.array(naive), np.array(rep)
print(f"h={h:g}: A 平均 {naive.mean():8.3f} 抖動 {naive.std():7.3f}"
f" B 平均 {rep.mean():7.4f} 抖動 {rep.std():.4f}")實跑輸出:真梯度 -2.6000。做法 A 在 \(h=0.1\) 時抖動 0.398,\(h=0.01\) 時 4.030,\(h=0.001\) 時 40.570——\(h\) 每縮小十倍,抖動放大十倍。這不是實作粗糙,是結構性的死局:有限差分要 \(h\) 夠小才逼近導數,但兩邊各自新抽的隨機差異除以 \(2h\) 之後隨 \(h\) 縮小而爆炸,兩頭夾死,這個估計不收斂。做法 B 三種 \(h\) 下的平均都貼著 -2.60,抖動穩定在 0.039 上下,跟 \(h\) 無關——因為固定 \(\boldsymbol\epsilon\) 之後,兩次計算之間唯一的差異就是 \(\mu\) 挪動的那一點,隨機性被完全對消。重參數化在真實訓練裡配的是反向傳播而不是有限差分,但道理同一條:讓梯度只看見確定性的路。
比喻: 口譯員複盤自己的筆記規則時,最怕的是「臨場的隨機發揮」跟「規則本身」攪在一起——同一頁筆記,今天展開成這樣、明天展開成那樣,你根本無從判斷是規則改了還是心情不同,規則也就無從修訂。重參數化等於把臨場發揮制度化地抽出來:先擲骰子決定這次的隨機成分,再按固定規則落筆;想評估規則的一個小改動,就用同一把骰子的結果跑兩遍,差異全數歸因於規則。這個比喻有一處失準:譯者的臨場發揮源自無法複製的直覺,抽出來就不是原來的東西了;網路的隨機成分本來就只是一個亂數,存檔重放不損失任何東西——這正是機器版本能做而人做不到的一步。
七、把損失組起來:訓練與使用
零件到齊,組裝。從第四節改寫二出發,兩項各自落地。
期望重建項是一個對 \(q\) 的期望值,仍然是積分,但跟第二節那個無解積分有本質差別:我們可以輕鬆從 \(q\) 抽樣(重參數化之後尤其如此),所以用蒙地卡羅估計(Monte Carlo estimate)——抽幾個樣本取平均——就能近似。實務上粗暴到底:只抽一個樣本。單樣本估計抖動很大,但沿用第 6 章對隨機梯度的論點——單步隨機、期望值對(各批次的期望梯度就是全體梯度),多步平均下來方向不歪——這裡的抽樣雜訊跟批次雜訊疊在一起,由同一套邏輯吸收。先驗拉力項則完全不必抽樣:兩個常態分布之間的 KL 散度有閉式解,直接寫成公式。
於是單筆資料的損失(負的下界,取負是因為習慣上最小化損失)是:
逐項拆解:\(\mathbf{z}^\ast\) 是用重參數化從這筆資料的 \(q\) 抽出的那一個樣本;第一項是拿它重建的負對數概似;第二項是這筆資料的 \(q\) 到先驗的 KL。整批訓練的損失 \(\mathcal{L}\) 取各筆 \(\ell\) 的平均,口徑與全課一致。
兩項再各自化簡一步。第一項:觀測模型是常態,把常態密度取負對數,跟第 5 章「常態假設推出平方誤差」的原班推導一模一樣,得到重建的平方誤差除以 \(2\sigma^2\)、再加一個不含參數的常數(可以丟)。順帶看出 \(\sigma\) 的第二個身分:它是重建項與 KL 項之間的權重旋鈕——\(\sigma\) 越小,平方誤差被除得越大,重建的話語權越重;\(\sigma\) 越大,先驗拉力相對越強。第二項:對角常態到標準常態的 KL 閉式是:
逐項拆解:\(D_z\) 是潛在空間維度;\(\Sigma_{jj}\) 與 \(\mu_j\) 是編碼器對第 \(j\) 維吐出的變異數與平均。推導路徑建議逐維走:先算一維常態 \(\mathcal{N}(\mu_j, \Sigma_{jj})\) 對標準常態的 KL(作業一會帶你完整推一遍),再利用各維獨立把 \(D_z\) 個一維結果直接相加。文獻裡也常見用跡與行列式寫的矩陣形式,那是同一條式子的另一種穿著,對角情形下逐維寫法直白得多。讀一下每一項的行為,能看出這個閉式在「罰什麼」:\(\mu_j^2\) 罰平均偏離零;\(\Sigma_{jj} - 1 - \log \Sigma_{jj}\) 在 \(\Sigma_{jj}=1\) 時剛好是零、往兩邊都變大——變異數太小(自以為太篤定)或太大(散得離譜)都要罰。整條式子在 \(\boldsymbol\mu=\mathbf{0}\)、\(\boldsymbol\Sigma=\mathbf{I}\)(\(q\) 恰好等於先驗)時歸零,與 KL 的非負性相符。
閉式是不是真的等於那個 KL?抽樣核對一次——從 \(q\) 抽大量樣本,直接按定義平均 \(\log q - \log p\),看它收不收斂到閉式:
import numpy as np
rng = np.random.default_rng(0)
mu = np.array([0.8, -0.4, 1.1]) # 假設的編碼器輸出:3 維平均
var = np.array([0.5, 1.6, 0.9]) # 對角共變異的三個對角元(變異數)
# 閉式:逐維公式加總
closed = 0.5 * np.sum(var + mu**2 - 1.0 - np.log(var))
print(f"閉式 KL = {closed:.4f}")
# 取樣估計:從 q 抽 z,平均 log q(z) - log p(z)
for N in (100, 10_000, 1_000_000):
z = mu + np.sqrt(var) * rng.standard_normal((N, 3))
log_q = -0.5 * np.sum(np.log(2*np.pi*var) + (z - mu)**2 / var, axis=1)
log_p = -0.5 * np.sum(np.log(2*np.pi) + z**2, axis=1)
print(f"N={N:>7} 取樣估計 = {(log_q - log_p).mean():.4f}")實跑輸出:閉式是 1.1693;取樣估計在 \(N=100\) 時 1.0588,\(N=10000\) 時 1.1674,\(N=1000000\) 時 1.1680——一路收向閉式值。這也順便演示了「閉式」的價值:同一個數字,右邊要抽一百萬個樣本才逼近,左邊一行公式直接寫出來,還能求導。
訓練迴圈至此毫無新花樣:抽一個小批次,每筆各走一遍「編碼 → 重參數化抽樣 → 解碼」,算 \(\mathcal{L}\),反向傳播同時取得對 \(\boldsymbol\phi_e\) 與 \(\boldsymbol\phi_d\) 的梯度,隨機梯度下降一步——全是第 6、7 章的原班工具。新花樣全部集中在一件事上:目標函數是一個下界。訓練時盯著的那條損失曲線,量的從來不是對數概似本身,而是「至少這麼多」的那把尺;下界升高,可能是概似真的變好,也可能只是縫隙變小(第四節的雙路徑),兩者從曲線上讀不出來。這是用一個下界換來生成模型時,簽下的最後一行條款。
訓練完成,回收第一節的伏筆:把編碼器整個拆下來收好,交付的成品只有先驗加解碼器。生成新樣本照第一節的兩步祖先取樣走。編碼器也不是全無下場——它在幾個延伸用途裡還有戲份,下一節見。
八、走出去:應用、病症與隔壁的路
主線到上一節收攏完畢。這一節是支線概覽——每一條只給你「它是什麼、為什麼可行」的骨架,細節由原書對照節的頁碼指路。
把尺的刻度磨細。 本章開頭說這個模型的尺讀作「至少這麼多」,其實刻度可以磨得更準。訓練完之後想評估「這筆資料的機率到底是多少」,可以拿編碼器當提議分布做重要性取樣(importance sampling):從 \(q(\mathbf{z}\mid\mathbf{x})\) 抽樣(它專挑跟 \(\mathbf{x}\) 相干的區域,正是第二節死路二缺的那塊拼圖),再用權重修正回對先驗的積分。抽得夠多,估計可以做得比下界準得多。第 14 章「測試集概似那把尺」的話頭,到這裡才算完整接上——這個模型不只給下界,還附一件把密度估準的工具。
樣本偏糊。 陽春版本生成的樣本常有一種霧面感,細節模糊。定性的成因可以追到一個建模決定:這一路上處處選常態——觀測雜訊是常態、變分家族是常態——常態的本性是把機率質量鋪在平均值周圍,逼著模型在「幾種都有可能的細節」之間輸出折衷的平均,而平均的細節就是糊的。改良路線多半從「換掉某個常態」下手,細節指路原書。
後驗塌縮(posterior collapse)。 一種訓練病症:編碼器擺爛,對任何 \(\mathbf{x}\) 都交出幾乎等於先驗的 \(q\),潛在變數形同虛設。成因用第四節的兩股拉力就能講:當解碼器強到不太需要 \(\mathbf{z}\) 也能重建個大概時,KL 項的拉力壓過重建項給 \(\mathbf{z}\) 的用處,把 \(q\) 一路拽到先驗上動彈不得——KL 歸零,代價是編碼器什麼資訊都沒傳。
潛在空間的語意編輯。 編碼器的延伸戲份:把一筆真實樣本投影進潛在空間(取 \(q\) 的平均),沿某個方向平移,再解碼回來——如果那個方向恰好對應某個語意因素,就得到「同一筆資料、只改一個屬性」的編輯效果。
解纏結(disentanglement)。 更進一步的期望:讓潛在座標每一維各管一個真實世界的變化因素。陽春版本不保證這件事,常見做法是加重或增添正則項,把「各維各司其職」的壓力寫進損失。變體名單見原書對照。
隔壁的路。 把單層的先驗疊成多層——潛在變數自己又由更上游的潛在變數生成——是這個家族自然的延伸方向,而沿這條思路走到底,會接上第 18 章那條「把雜訊一步步還原成資料」的路。兩章看似不同門派,血緣其實很近,這裡按下不表。
§03原書對照
原書第 17 章的鋪陳從一般的潛在變數模型開始。pp.327–328 給出「資料分布=聯合分布對潛在變數的邊際化」這條路線,並以一維高斯混合當入門例,示範離散潛在變數如何靠加權求和完成邊際化;p.327 的開場同時提醒了一件容易被忽略的事:這個架構是用來幫忙學模型的工具,訓練完成後真正拿去生成的那個模型,更貼切的名字其實是非線性潛在變數模型。pp.328–330 進入連續版本:先驗取標準常態、概似取以網路輸出為中心的球形常態,並以「無限多個常態分布的加權混合」這個觀點解釋其表達力;p.329 交代了由祖先取樣產生新樣本的流程,p.330 的兩張圖分別畫出二維密度的組裝過程與取樣流程。
訓練的困難與下界的建立橫跨 pp.331–333。p.331 指出最大概似的積分沒有閉式解;接著原書用整整兩頁鋪陳 Jensen 不等式,p.331 的圖用離散點的凸組合呈現、p.332 的圖用連續分布呈現,對「期望值的對數為何大於等於對數的期望值」給出兩種互補的幾何直覺。p.333 完成下界推導、交代這個下界名字的來歷,並用一張多條曲線的圖呈現下界與對數概似之間的關係。pp.334–335 是性質分析:p.334 用五行等式把下界改寫成對數概似減去一個 KL 散度,鬆緊判準由此而來;p.335 以貝葉斯規則圖解潛在變數的後驗分布,同頁推出重建誤差減先驗距離的第三種寫法。變分近似的選擇在 p.336;單樣本蒙地卡羅近似與對角常態 KL 散度的閉式都在 p.337,同頁另有一張兩格對照圖,畫出變分近似貼得住與貼不住真後驗的兩種情形——後驗多峰時,常態家族怎麼選都貼不緊;p.338 給出完整架構圖與逐步演算法,並解釋「變分」與「自編碼器」兩個名字各自從哪裡來。重參數化技巧在 p.339,配有改動前後的架構對照圖。原書隨章附了三個線上筆記本,分別對應潛在變數模型、重參數化與重要性取樣三個主題,連結散在對應小節的頁邊。
應用的部分在 pp.340–344:以重要性取樣近似新樣本機率的完整推導在 pp.340–341;名人臉孔資料集上的取樣品質對照圖在 p.341;把真實影像投影回潛在空間、沿語意方向平移再解碼的再合成在 pp.342–343,含球面插值的說明;解纏結的正則化路線在 pp.342–344,含兩條變體的式子與一組椅子資料集的圖。p.342 並指出擴散模型可以視為帶分層先驗的變分自編碼器。章末摘要在 pp.343–344,把全章的主線用三段話收攏。註記 pp.344–347 是一份文獻地圖:各應用領域的文獻在 pp.344–345,與傳統自編碼器及主成分分析的關係在 p.345,架構變體與更複雜的概似模型橫跨 pp.345–346,與對抗式訓練的多種混種、後驗塌縮、重建偏糊的成因分析都在 p.346,重參數化的替代做法、解纏結的後續文獻、以及此下界與另一個經典交替演算法的關係在 pp.346–347。習題七題在 pp.347–348,其中兩題帶星號、屬較重的推導題。想逐行看懂下界的兩種改寫,pp.334–335 是全章最值得慢讀的兩頁。原書第 17 章對應印刷頁 pp.327–348。
§04作業和解答
作業一:親手推一維的 KL 閉式
設 \(q(z) = \mathcal{N}(z\,;\,\mu, s)\)(\(s\) 為變異數)、\(p(z) = \mathcal{N}(z\,;\,0, 1)\)。(a)從定義 \(D_{\mathrm{KL}}\left(q\,\|\,p\right) = \mathbb{E}_q[\log q(z) - \log p(z)]\) 出發,推出閉式 \(\tfrac{1}{2}(s + \mu^2 - 1 - \log s)\),每一步寫明用了哪個事實。(b)代入 \(\mu = 1.2\)、\(s = 0.49\) 算出數值。(c)說明為什麼第七節那條 \(D_z\) 維的公式就是把(a)的結果逐維相加。
解答 SOLUTION
(a)寫出兩個對數密度:\(\log q(z) = -\tfrac{1}{2}\log(2\pi s) - \tfrac{(z-\mu)^2}{2s}\);\(\log p(z) = -\tfrac{1}{2}\log(2\pi) - \tfrac{z^2}{2}\)。逐項取對 \(q\) 的期望。第一項:\(\mathbb{E}_q[(z-\mu)^2] = s\)(這就是變異數的定義),所以 \(\mathbb{E}_q[\log q] = -\tfrac{1}{2}\log(2\pi s) - \tfrac{1}{2}\)。第二項:\(\mathbb{E}_q[z^2] = s + \mu^2\)(變異數加平均的平方,由 \(\mathrm{Var}[z] = \mathbb{E}[z^2] - \mathbb{E}[z]^2\) 移項而得),所以 \(\mathbb{E}_q[\log p] = -\tfrac{1}{2}\log(2\pi) - \tfrac{s + \mu^2}{2}\)。兩者相減,\(-\tfrac{1}{2}\log(2\pi)\) 對消,剩 \(-\tfrac{1}{2}\log s - \tfrac{1}{2} + \tfrac{s+\mu^2}{2} = \tfrac{1}{2}(s + \mu^2 - 1 - \log s)\)。
(b)\(\tfrac{1}{2}(0.49 + 1.44 - 1 - \log 0.49) = \tfrac{1}{2}(0.93 + 0.7134) = 0.8217\)。(我另用取樣估計核對:從 \(q\) 抽一百萬個樣本按定義平均,得 0.8222,與閉式一致;本題數值以 numpy 重算核對過。)
(c)\(q\) 與 \(p\) 都是各維獨立的分布,密度是各維密度的乘積,取對數變成各維相加,期望值再逐項分開——所以整體 KL 就是 \(D_z\) 個一維 KL 的和,每一維套(a)的結果,把 \(\mu, s\) 換成 \(\mu_j, \Sigma_{jj}\) 即得第七節的公式。
作業二:下界升高,概似動了沒有
訓練中追蹤某一筆資料,假設在三個時刻量到(本題數字皆為假設):時刻一,對數概似 \(-3.0\)、\(q\) 到真後驗的 KL 為 \(1.4\);時刻二,對數概似 \(-3.0\)、該 KL 為 \(0.3\);時刻三,對數概似 \(-2.5\)、該 KL 為 \(0.3\)。(a)用第四節改寫一算出三個時刻的下界。(b)時刻一到二,下界升了多少?這段進步是誰的功勞——模型變好了,還是量尺變準了?(c)由此說明:為什麼「下界升高」不能直接讀成「概似升高」,以及要再加上哪個條件才能這樣讀。
解答 SOLUTION
(a)改寫一說下界=對數概似減縫隙。時刻一:\(-3.0 - 1.4 = -4.4\);時刻二:\(-3.0 - 0.3 = -3.3\);時刻三:\(-2.5 - 0.3 = -2.8\)。(本題數值以 numpy 重算核對過。)
(b)升了 \(1.1\)。但對數概似一動未動——這 \(1.1\) 全部來自縫隙從 \(1.4\) 壓到 \(0.3\),也就是 \(q\) 貼近了真後驗。模型對資料的解釋力沒有變,是量尺變準了。這通常對應 \(\boldsymbol\phi_e\) 那一側的進步(把 \(q\) 挑得更好),而時刻二到三那 \(0.5\) 才是概似本身的進步。
(c)下界=概似減縫隙,所以下界升高的原因可以是概似升、可以是縫隙縮,也可以兩者混合——單看下界分不出來。只有再加上「縫隙不變(或已知其變化量)」這個條件,下界的變化才能直接讀成概似的變化。實務上真後驗算不出來、縫隙也就量不到,這正是「用下界訓練」要一直放在心上的閱讀守則:損失曲線在動的,未必是你以為在動的那個量。
作業三:驗證重參數化沒有偷換分布
設一維情形 \(z^\ast = \mu + \sqrt{s}\,\epsilon\),\(\epsilon \sim \mathcal{N}(0, 1)\),\(\mu = -0.6\)、\(s = 1.69\)(即標準差 \(1.3\))。(a)不用抽樣,算出 \(z^\ast\) 的平均與變異數,說明每一步用了期望值或變異數的哪條性質。(b)說明為什麼光對上平均與變異數還不夠、還差哪一塊拼圖才能斷言「分布一模一樣」。(c)我用 numpy 抽了一百萬個 \(\epsilon\) 做這個變換,樣本平均是 -0.5987、樣本標準差是 1.3009。這個結果驗證到了(a)與(b)中的哪一部分?
解答 SOLUTION
(a)平均:\(\mathbb{E}[z^\ast] = \mathbb{E}[\mu + \sqrt{s}\,\epsilon] = \mu + \sqrt{s}\,\mathbb{E}[\epsilon] = \mu = -0.6\)——用了「期望值對加法與常數倍拆開」以及 \(\mathbb{E}[\epsilon]=0\)。變異數:\(\mathrm{Var}[z^\ast] = \mathrm{Var}[\sqrt{s}\,\epsilon] = s\,\mathrm{Var}[\epsilon] = s = 1.69\)——用了「平移不改變變異數」「常數倍使變異數乘上平方」以及 \(\mathrm{Var}[\epsilon]=1\)。
(b)平均與變異數相同的兩個分布未必是同一個分布(一個常態與一個同平均同變異數的雙峰分布就是反例)。補上的拼圖是形狀:常態隨機變數經過「乘常數再加常數」的一次式變換後仍是常態——這是常態分布的標準性質(完整證明可由密度換元寫出,超出本課範圍,結論正確)。形狀是常態、平均是 \(\mu\)、變異數是 \(s\),三者合起來才鎖定它就是 \(\mathcal{N}(\mu, s)\)。
(c)樣本平均 -0.5987 貼近 \(-0.6\)、樣本標準差 1.3009 貼近 \(1.3\),驗證的是(a)算出的兩個數字;至於(b)的「形狀」,光看這兩個統計量驗證不到——要看整個直方圖或分位數的吻合程度才算數。分清楚「驗證了什麼、沒驗證什麼」,正是蒙地卡羅實驗的基本紀律。(本題數值以 numpy 重算核對過,seed 為 0。)
§05參考資料
- An Introduction to Variational Autoencoders(Kingma & Welling, 2019) — 原始作者親筆的長篇導論,本章的下界、重參數化與攤提式推論在這裡都有第一手的完整版
- Tutorial on Variational Autoencoders(Doersch) — 以「為什麼每一步非這樣不可」的口吻重走一遍推導,適合讀完本章後對照第二種講法
- Variational Inference: A Review for Statisticians(Blei 等) — 把變分近似放回統計推論的大脈絡,看清楚本章只是這門手藝在深度學習的一個切片
- From Autoencoder to Beta-VAE(Lilian Weng) — 從傳統自編碼器一路梳理到各種變體的圖解筆記,第八節每一條支線在這裡都有展開
- Wikipedia:Evidence lower bound — ELBO 的多種等價寫法與推導的百科整理,方便快速查對符號約定不同的文獻
- Wikipedia:Jensen's inequality — 第三節那把刀的完整數學條目,含幾何直覺與等號條件的一般敘述
- Understanding Deep Learning(MIT Press) — 本課課綱主題所本的原書出版頁(ISBN 9780262048644,2023-12 出版)
- udlbook 官方網站(作者釋出的 PDF、投影片與習題) — 原書作者維護的免費資源站(udlbook.com 會轉址到此)