§01學習重點
- 說出擴散模型的兩段式構圖,並指出可學參數全部集中在哪一半
- 解釋前向單步更新為什麼要「先縮再混、係數平方和為一」,並親手用程式驗證只混不縮的下場
- 寫下跳步公式,說明它為什麼是訓練效率的鑰匙,以及「握住起點」為什麼讓反向條件分布變成可算的常態
- 說出反向過程為什麼必須拆成一長串小步——「步子小」與常態近似之間的因果關係
- 解釋概似為什麼積不出來、下界怎麼換到手,以及把下界收緊的責任為什麼全部落在反向那一半
- 從跳步公式反解出起點,說明「認出雜訊=認出訊號」,並解釋損失最後為什麼塌成純平方誤差
- 親手用 numpy 跑完「訓練+生成」完整一圈,檢查生成樣本的平均與共變異數貼不貼資料
- 說出取樣時每一步為什麼要補一小份新雜訊、收尾那一步為什麼不補,並用實驗指出不補的下場
§02課程內容
一、化開是固定的:前向過程零參數
第 15 章末尾把話說明白了:「抽得出樣本、算不出密度」是那一章那個做法自己的選擇,不是生成模型的通性,接下來的路線會用各自的方式把這件事補回來。本章走的是其中最出人意料的一條——先把資料一步步徹底毀掉,再學會毀掉的每一小步怎麼退回去。 這就是擴散模型(diffusion model)。
整套做法由兩段組成。第一段叫前向過程(forward process):從一筆真實資料出發,一小步一小步往裡面混入隨機雜訊,直到原始內容被徹底洗掉、只剩純雜訊。第二段叫反向過程(reverse process):從純雜訊出發,一小步一小步把雜訊退掉,最後收出一筆新資料。先把方向釘死:本章的「前向」一律指從資料走向雜訊,「反向」一律指從雜訊走回資料。 順帶提醒一句,不同模型家族對這兩個詞的方向約定並不一致,讀其他文獻時,先翻到那本書定義方向的地方確認一次再往下讀。
真正該先站穩的是一個結構事實:前向那一半是一條寫死的流程,零參數、不用訓練。 你沒看錯——這個模型家族有一半的流程完全沒有東西可學。本章把資料變成雜訊的那一半不含任何參數,所有要學的東西都集中在反向那一半。為什麼可以這樣設計、這個設計又換到了什麼,是貫穿本章的第一條主線。
動手之前把記號備齊。資料記作 \(\mathbf{x}\)。前向每走一步產生一個中間結果,整串記作 \(\mathbf{z}_1, \mathbf{z}_2, \dots, \mathbf{z}_T\),其中 \(t\) 是時間步、\(T\) 是總步數。兩件事先說清楚,免得記號絆倒你。第一,這裡的下標是時間步,不是層——第 7 章反向傳播裡的 \(\mathbf{z}_k\) 指第 \(k\) 層的預活化,跟本章只是撞了同一個字母,兩者毫無關係。第二,這串 \(\mathbf{z}\) 沿用第 1 章「潛在變數」的名字,但第 1 章當時把潛在變數介紹成一個維度很低的向量,那個印象在本章不成立:這裡每一個 \(\mathbf{z}_t\) 都跟資料同維度,一張圖片加噪之後還是同尺寸的一張圖片。「潛在」在本章只剩一個意思——訓練資料裡觀測不到(它們是流程自己造出來的中間產物),跟維度高低無關。你如果一直想找「壓縮」發生在哪裡,可以停了:本章沒有壓縮。
前向的單步規則只有一行。先準備一組介於 0 與 1 之間的數 \(\rho_1,\dots,\rho_T\),本章叫每一步的加噪比例,整組合稱加噪時程(noise schedule)——它是超參數,訓練前就寫死、全程不動:
逐項拆解:為了讓式子從 \(t=1\) 起就長得一樣,這裡把 \(\mathbf{z}_0\) 當成 \(\mathbf{x}\) 的別名,它就是那筆資料本身。\(\boldsymbol\epsilon_t\) 是新抽的標準常態雜訊——每個分量獨立、平均 0、變異數 1,而且每一步都重抽一份。\(\rho_t\) 控制這一步混多兇:整條式子是線性混合,舊內容佔 \(\sqrt{1-\rho_t}\) 的比重、新雜訊佔 \(\sqrt{\rho_t}\) 的比重。用分布的語言說,\(q(\mathbf{z}_t \mid \mathbf{z}_{t-1})\) 是平均 \(\sqrt{1-\rho_t}\,\mathbf{z}_{t-1}\)、共變異數 \(\rho_t\mathbf{I}\) 的常態分布,\(\mathbf{I}\) 是單位矩陣(對角線是 1、其餘是 0)。本章固定用 \(q(\cdot)\) 記這條寫死的前向流程,把 \(Pr(\cdot)\) 留給待會要學的模型,兩邊不會混。
注意那兩個係數:平方和剛好是 \((1-\rho_t) + \rho_t = 1\)。這不是巧合,而是整個設計裡最容易被看漏的一手。直覺上「加噪」就是把雜訊疊上去,但這條式子做了兩件事——先把既有內容按 \(\sqrt{1-\rho_t}\) 往零縮,再混入雜訊。為什麼要縮?算一次變異數就明白。兩個獨立的量相加,變異數相加;係數則平方地作用在變異數上。若 \(\mathbf{z}_{t-1}\) 的變異數已是 1,則 \(\mathbf{z}_t\) 的變異數是 \((1-\rho_t)\cdot 1 + \rho_t\cdot 1 = 1\):每一步都停在同一個尺度上。要是只疊不縮,變異數每步淨增 \(\rho_t\),走越久膨脹越大,終點就不會落在我們想要的標準常態上。直接把兩種版本都跑給你看:
import numpy as np
rng = np.random.default_rng(0)
# 一杯水裡的 400 個墨點粒子:每個粒子一組二維座標(水平位置、深度)
n = 400
x = rng.normal([0.9, -0.6], [0.5, 0.35], (n, 2)) # 墨滴剛落下時的粒子雲
rho = 0.05 # 這段示範先用固定的加噪比例
za = x.copy() # 正版更新:先縮一點、再混一點
zb = x.copy() # 錯版更新:只混不縮
for t in range(1, 41):
za = np.sqrt(1 - rho) * za + np.sqrt(rho) * rng.normal(0, 1, (n, 2))
zb = zb + np.sqrt(rho) * rng.normal(0, 1, (n, 2))
if t % 10 == 0:
print(f"t={t:2d} 正版 std={za.std():.3f} 錯版 std={zb.std():.3f}")實跑輸出:正版的整體標準差四十步下來穩穩貼著 1——依序是 0.947、0.943、0.958、0.974(起點的粒子雲比標準常態窄,所以從下方逼近);錯版則一路膨脹:1.101、1.301、1.521、1.649,而且會繼續漲。「縮一點、混一點、係數平方和為一」這個設計換到的就是變異數守恆:不管走多少步,尺度不跑掉。
比喻: 一滴墨落進一杯清水。你不需要對它做任何事,物理自己接手:墨慢慢化開,水色一步步變勻,最後整杯是再也看不出落點的淡灰。把整個清水化開的過程拍成影片、一幀一幀存下來——第 \(t\) 幀就是 \(\mathbf{z}_t\),最後一幀就是那杯勻開的水。前向過程正是這卷影片:化開的規則寫死在物理定律裡,沒有旋鈕可調、沒有東西要學,這正對應「前向零參數」。這個比喻有一處明確失準:真實的墨水擴散只會「散」,沒有哪條物理定律會把杯裡既有的濃淡按比例往零壓;而前向過程每一步都先縮再混——少了縮,終點就不是標準常態,上面錯版更新的下場就是這個失準之處的數學版。
再看兩個結構性質。第一,單步規則裡 \(\mathbf{z}_t\) 只由 \(\mathbf{z}_{t-1}\) 與新雜訊決定,跟更早的歷史無關——這叫馬可夫性(Markov property),整條鏈是一條馬可夫鏈(Markov chain)。第二,走得夠久之後,「留下來的比例」連乘會把起點的比重壓到趨近零(下一節會把這個連乘算出來),留在杯裡的幾乎全是一路混進來的常態雜訊;又因為變異數守恆一路撐住尺度,終點分布就趨近標準常態——跟起點是哪筆資料無關。這句話值得停一秒:不管墨滴落在哪、濃淡如何,走到最後都是同一杯統計上無法分辨的灰水。起點的資訊被徹底洗光,這是「化勻的水看不出墨滴落點」的數學版本。
於是全景如下:\(\mathbf{x}\) 沿上排一路走到近似標準常態的 \(\mathbf{z}_T\),整段沒有任何可學參數;要學的是下排那條反著走的路——從 \(\mathbf{z}_T\) 一步步退回 \(\mathbf{x}\),每一小步靠同一個網路。可調的旋鈕,全部集中在下排。
二、跳步公式:一步到位,也是目標的來源
先解決一個訓練上的實際問題。照單步規則,想拿到第 40 步的加噪樣本就得真的走 40 步;訓練時每筆資料、每個時間步都這樣走,成本吃不消。幸好這條鏈是線性的、雜訊是常態的,兩件事加起來給出一條捷徑。把單步規則代入它自己:\(\mathbf{z}_2 = \sqrt{1-\rho_2}\bigl(\sqrt{1-\rho_1}\,\mathbf{z}_0 + \sqrt{\rho_1}\,\boldsymbol\epsilon_1\bigr) + \sqrt{\rho_2}\,\boldsymbol\epsilon_2\)。兩份互相獨立的常態雜訊線性組合起來仍是常態、變異數相加,所以兩步可以合成等價的一步(作業一請你親手推一遍並用程式核對)。一路合下去,得到跳步公式,這個領域也叫它擴散核(diffusion kernel):
逐項拆解:\(\prod\) 是連乘符號,跟 \(\sum\) 是連加同一個道理。\(\kappa_t\) 把每一步「留下來的比例」全部乘起來,本章叫它累積保真比例——白話就是「走到第 \(t\) 步,原始訊號還剩幾分比重」。每個 \((1-\rho_u)\) 都小於 1,所以 \(\kappa_t\) 隨 \(t\) 單調下降:從趨近 1(幾乎全是墨)一路掉到趨近 0(幾乎全是水)。\(\boldsymbol\epsilon\) 是單獨一份標準常態雜訊——沿途混進來的所有雜訊在數學上合併成了這一份。整條式子說:想要第 \(t\) 步的樣本,不必逐步模擬,抽一份雜訊、按比例跟資料混一下,一步到位。這直接推翻一個常見誤解——訓練擴散模型不需要把整條鏈跑完,一步就能造出任何時間步的訓練樣本。先驗證它真的成立:
import numpy as np
rng = np.random.default_rng(0)
T = 60
rho = np.linspace(0.008, 0.16, T) # 加噪時程:越到後段混得越兇
kappa = np.cumprod(1 - rho) # 累積保真比例
print("kappa:", " ".join(f"t={t}:{kappa[t-1]:.4f}" for t in (1, 10, 20, 30, 45, 60)))
n = 5000
x = rng.normal([0.9, -0.6], [0.5, 0.35], (n, 2)) # 同一批墨點粒子
tc = 30
z = x.copy() # (a) 照單步規則一步一步走到第 30 步
for t in range(tc):
z = np.sqrt(1 - rho[t]) * z + np.sqrt(rho[t]) * rng.normal(0, 1, (n, 2))
zj = (np.sqrt(kappa[tc-1]) * x # (b) 跳步公式一步到位
+ np.sqrt(1 - kappa[tc-1]) * rng.normal(0, 1, (n, 2)))
print("逐步模擬 mean =", np.round(z.mean(0), 3), " std =", np.round(z.std(0), 3))
print("跳步公式 mean =", np.round(zj.mean(0), 3), " std =", np.round(zj.std(0), 3))實跑輸出兩件事。第一,\(\kappa_t\) 的長相:t=1:0.9920、t=10:0.8202、t=20:0.5155、t=30:0.2464、t=45:0.0479、t=60:0.0048——第 20 步上下墨水各半,到第 60 步墨只剩千分之五的比重,整杯幾乎是純雜訊。第二,同一批五千個墨點粒子,(a) 逐步模擬 30 步的樣本平均是 [0.467 -0.309]、標準差 [0.913 0.894];(b) 跳步公式一步抽出的樣本平均是 [0.447 -0.286]、標準差 [0.918 0.894]——兩組統計上一致,差異是抽樣自身的抖動,不是系統性偏差。
有了跳步公式,你可能期待反向也有類似的捷徑——直接把 \(q(\mathbf{z}_{t-1}\mid\mathbf{z}_t)\)(看到第 \(t\) 步、猜上一步)的閉式算出來不就好了?一試就卡住。用貝葉斯規則(Bayes' rule)把條件翻過來:\(q(\mathbf{z}_{t-1}\mid\mathbf{z}_t) = q(\mathbf{z}_t\mid\mathbf{z}_{t-1})\,q(\mathbf{z}_{t-1})\,/\,q(\mathbf{z}_t)\)。右邊第一項是單步規則、有閉式;但 \(q(\mathbf{z}_{t-1})\) 與 \(q(\mathbf{z}_t)\) 這兩個邊際分布,得把跳步公式對整個資料分布做平均——而資料分布正是我們從頭到尾不知道的東西。邊際沒有閉式,這條路走不通。這不是計算技巧不夠,是結構性的死路:反向之難,難在這裡。
死路旁邊有一扇門:多握住一個條件——起點 \(\mathbf{x}\)——局面整個翻轉。 \(q(\mathbf{z}_{t-1}\mid\mathbf{z}_t,\mathbf{x})\) 問的是「我知道墨滴當初落在哪,也看到了第 \(t\) 幀,那上一幀長什麼樣」。這時貝葉斯規則裡每一項都是已知的常態(單步規則供一項、跳步公式供兩項),常態相乘配平方之後還是常態,完全可以算:它是平均 \(\mathbf{m}_t\)、共變異數 \(s_t^2\,\mathbf{I}\) 的常態分布,其中
逐項拆解:先看結構再看細節。\(\mathbf{m}_t\) 是 \(\mathbf{z}_t\) 與 \(\mathbf{x}\) 的線性組合——對上一幀的最佳猜測,一部分來自「現在看到的樣子」、一部分來自「當初的落點」,而兩個係數都只由時程裡的 \(\rho\) 與 \(\kappa\) 決定,跟資料內容無關。\(s_t^2\) 是這個猜測殘餘的不確定寬度,同樣只由時程決定,且 \(\rho_t\) 越小它越小——步子越小,上一幀越接近被鎖定。用墨水的話說:光看一幀化到一半的水,你猜不準上一幀,因為很多種上一幀都能化成這個樣子;但若知道墨滴當初落在哪,上一幀就幾乎確定,只剩一點抖動空間。中間用到的兩條高斯恆等式本課不推,原書對照節會指出它們在哪幾頁。這個閉式常態是全章的樞紐:它就是反向網路每一步要追的目標。
三、學倒帶:反向過程與下界
現在正面處理要學的那一半。真正的 \(q(\mathbf{z}_{t-1}\mid\mathbf{z}_t)\)——沒握住起點的那個——長得很醜:它跟著未知的資料分布走,可能歪,可能同時隆起好幾個峰。同一幀化到一半的水,可能來自左邊落的墨滴,也可能來自右邊落的,兩種來歷各自成峰,想用一個簡單分布去套,一般套不住。但步子夠小時套得住。 這是「逐步」二字的全部理由:\(\rho_t\) 很小時,一步之間變化極少,上一幀跟這一幀離得很近,那個醜分布在這麼小的鄰域裡看起來就近似一個常態。一步想跨太大,常態就罩不住了。所以「步子小」與「步數多」不是兩個獨立的選擇,是同一個決定的兩面。
反向模型於是長成這樣:每一步用一個常態去近似——\(Pr(\mathbf{z}_{t-1}\mid\mathbf{z}_t,\boldsymbol\phi)\) 是平均 \(\boldsymbol\mu_t[\mathbf{z}_t,\boldsymbol\phi]\)、共變異數 \(\sigma_t^2\,\mathbf{I}\) 的常態分布。\(\boldsymbol\mu_t[\cdot]\) 由網路計算:吃進第 \(t\) 步的樣子,吐出對上一步的猜測;\(\boldsymbol\phi\) 照全站慣例是參數全集。\(\sigma_t^2\) 是每一步預先固定的寬度——注意它是我們選定的超參數,不是從資料算出來的統計量;常見選法之一是直接取 \(\sigma_t^2=\rho_t\)(本章的程式就用它),也有做法連寬度一起學,本章不展開。生成的流程叫祖先取樣(ancestral sampling):從標準常態抽一個 \(\mathbf{z}_T\),用 \(t=T\) 那一步的常態抽出 \(\mathbf{z}_{T-1}\),一路抽到底,最後一步收出 \(\mathbf{x}\)。
為什麼這一長串「每步一個常態」學得起來?因為訓練時我們手上有起點。給定訓練資料 \(\mathbf{x}\),上一節那個閉式常態 \(q(\mathbf{z}_{t-1}\mid\mathbf{z}_t,\mathbf{x})\) 就是第 \(t\) 步的「標準答案分布」,網路要做的只是把自己的平均 \(\boldsymbol\mu_t\) 往答案的平均 \(\mathbf{m}_t\) 推。每一步都有自己的答案可對——這比「一口氣把純雜訊變成資料」好學太多了。
不過「往答案推」需要一條正式的訓練目標,而目標應該從概似來——這是第 5 章立的規矩:想讓模型給資料高機率,就最大化對數概似。麻煩在於,本章的模型要給 \(\mathbf{x}\) 一個機率,得把所有中間變數都積掉。模型的聯合機率由反向鏈一步步乘出來:\(Pr(\mathbf{x},\mathbf{z}_1,\dots,\mathbf{z}_T\mid\boldsymbol\phi)=Pr(\mathbf{z}_T)\,Pr(\mathbf{z}_{T-1}\mid\mathbf{z}_T,\boldsymbol\phi)\cdots Pr(\mathbf{x}\mid\mathbf{z}_1,\boldsymbol\phi)\);而概似 \(Pr(\mathbf{x}\mid\boldsymbol\phi)\) 要對 \(\mathbf{z}_1,\dots,\mathbf{z}_T\) 全部積分——\(T\) 份跟資料同維度的變數,這個積分毫無指望。
出路是退一步:算不出它,就找一個算得出的下界,改成把下界推高。第一步把積分改寫成期望:對任何抽得出樣本的分布 \(q\),都有 \(Pr(\mathbf{x}\mid\boldsymbol\phi)=\mathbb{E}_{q}\bigl[\,Pr(\mathbf{x},\mathbf{z}_{1},\dots,\mathbf{z}_T\mid\boldsymbol\phi)\,/\,q(\mathbf{z}_{1},\dots,\mathbf{z}_T\mid\mathbf{x})\,\bigr]\)。這裡 \(\mathbb{E}_q[\cdot]\) 讀作「照 \(q\) 抽樣時,方括號裡那個量的平均」;等式成立是因為把 \(q\) 乘進去又除回來,什麼都沒改變。第二步用 log 的凹性:log 曲線向下彎,弦永遠壓在曲線下方,所以「先平均再取 log」不會小於「先各自取 log 再平均」。兩步合起來:
逐項拆解:左邊是我們真正想推高、卻算不出的對數概似。右邊是下界:分子是模型的聯合機率——反向鏈,每一項是常態、有閉式;分母是前向流程給這串中間變數的機率——同樣逐項有閉式;期望對 \(q\) 取,而 \(q\) 正是我們自己的前向過程,跳步公式讓我們想抽多少樣本就抽多少。也就是說,右邊每一個零件都拿得到,這個下界算得出來,可以拿去最大化。文獻管這種「概似算不出、退而求其次推高下界」的量叫證據下界(evidence lower bound),常縮寫成 ELBO。
這個下界有一個本章獨有的性質,值得單獨點出來。下界跟真值之間一般有縫隙,縫隙大小與 \(q\) 的選擇有關——第 17 章的章題說它是「用一個下界換來的生成模型」,走的也是這類路線。本章的特別之處在:\(q\) 是寫死的前向流程,一個參數都沒有,你動不了它。 把下界往上推的責任,於是百分之百落在反向那一半的 \(\boldsymbol\phi\) 上。「前向零參數」這個第一節立下的設計,在訓練目標上的迴響就是這裡。
接下來是一段純代數化簡,思路一句話講完:把分子分母都按時間步拆開、逐步配對,再用貝葉斯規則把每一對整理成「握住起點的閉式常態」對上「模型的第 \(t\) 步常態」。逐行代數原書寫得極細,本課不搬(章末原書對照節告訴你在哪幾頁),只給終點的形狀:下界拆成一項「最後一步把 \(\mathbf{z}_1\) 落回資料」的重建項,加上一串項,每一項各管一個時間步,度量模型第 \(t\) 步與 \(q(\mathbf{z}_{t-1}\mid\mathbf{z}_t,\mathbf{x})\) 之間的分布距離。這種分布間距離叫 KL 散度(Kullback-Leibler divergence),本課只用它的一個現成結果:兩個同寬度常態之間的 KL 散度有閉式,而其中跟 \(\boldsymbol\phi\) 有關的只剩「平均值差的平方」\(\lVert\mathbf{m}_t-\boldsymbol\mu_t[\mathbf{z}_t,\boldsymbol\phi]\rVert^2\),乘上一個只由時程決定的係數。
停下來看看發生了什麼:負對數概似、下界、散度,一路聽起來都是重機械,落地卻是第 5 章那種再熟悉不過的平方差。整個訓練塌成一串迴歸問題——每個時間步一題,題目是「把網路的平均推向那個算得出的目標平均」,而且各題互相獨立:第 37 步的損失不牽動第 12 步,抽到哪步練哪步。這是本章第一個反直覺的收穫,但還不是最大的那個。
比喻: 一位學徒看過幾百萬杯墨化開的影片。他學的不是「一口氣從勻開的灰水倒回一滴墨」——那是不可能的課題;他練的是小得多的一題:「給我影片裡任何一幀,我往回猜一小步。」而且訓練時師傅手上有答案——每卷影片都記錄了墨滴當初落在哪,所以每一幀「往回一小步該長什麼樣」都有一個算得出的標準答案(就是那個閉式常態),學徒只要把自己的猜測往標準答案推。這個比喻有一處明確失準:影片倒帶是把記錄下來的畫面照原樣一幀幀倒放,每一幀確定不變;反向過程不是播放,是抽樣——每退一步都帶著不確定性,同一幀往回可以退出不一樣的上一幀。下一節的取樣迴圈會把這個差別變成看得見的一行程式。
四、認出雜訊:重參數化把損失塌成平方誤差
上一節收尾時,網路的職務還是「猜上一幀的平均」。本節做一個換元,換完之後職務會變成一句更奇怪、也更深刻的話:認出這一幀裡哪些是雜訊。 這一步叫重參數化(reparameterization)——同一個目標,換一組變數來寫。
出發點是跳步公式自己。它說 \(\mathbf{z}_t=\sqrt{\kappa_t}\,\mathbf{x}+\sqrt{1-\kappa_t}\,\boldsymbol\epsilon\):第 \(t\) 幀由兩個成分線性合成——按比例縮過的訊號、按比例放大的雜訊。這條式子可以反著解:
逐項拆解:站在 \(\mathbf{z}_t\) 上,\(\kappa_t\) 是已知的時程量,所以這條式子說的是——知道混進來的雜訊 \(\boldsymbol\epsilon\),就等於知道起點 \(\mathbf{x}\);把式子反過來解一次,知道 \(\mathbf{x}\) 也就等於知道 \(\boldsymbol\epsilon\)。兩個成分被一條線性關係鎖在一起,認出其中一個,另一個自動現身。「預測雜訊是不是在繞遠路」這個常見疑問到這裡有了答案:不是繞路,是同一件事的另一個名字。
把這條反解代進上一節的目標平均 \(\mathbf{m}_t\)(中途用一次 \(\kappa_t=(1-\rho_t)\,\kappa_{t-1}\),兩行就收乾淨),\(\mathbf{x}\) 消失,\(\mathbf{m}_t\) 變成:
逐項拆解:目標平均原本寫成「\(\mathbf{z}_t\) 與 \(\mathbf{x}\) 的線性組合」,換元之後變成「\(\mathbf{z}_t\) 與當初那份雜訊的線性組合」,兩個係數照樣只由時程決定。妙處在這裡:\(\mathbf{z}_t\) 本來就是網路的輸入,網路看得到;整個目標裡網路唯一不知道的只剩 \(\boldsymbol\epsilon\)。那何必讓它猜整個平均?讓它專心認出那份雜訊就好。
於是網路的職務正式改寫。本章的去噪網路記作 \(\hat{\boldsymbol\epsilon}_t[\mathbf{z}_t,\boldsymbol\phi]\)——雜訊預測網路,帽子表示估計,函數名直接說出職務:認出 \(\mathbf{z}_t\) 裡混進來的那份雜訊。模型的平均照上式的樣子組裝:\(\boldsymbol\mu_t[\mathbf{z}_t,\boldsymbol\phi]=\bigl(\mathbf{z}_t-\tfrac{\rho_t}{\sqrt{1-\kappa_t}}\,\hat{\boldsymbol\epsilon}_t[\mathbf{z}_t,\boldsymbol\phi]\bigr)/\sqrt{1-\rho_t}\),跟目標平均一模一樣、只把真雜訊換成預測。兩式相減,共同的 \(\mathbf{z}_t\) 項對消,上一節的「平均值差平方」就變成 \(\boldsymbol\epsilon-\hat{\boldsymbol\epsilon}_t\) 的平方,乘上一個只由時程決定的係數。要強調:猜平均與認雜訊在數學上等價——隔著一條係數已知的線性換算而已;但實務上發現,讓網路認雜訊的訓練效果更好。這是經驗性的發現,沒有定理保證,別把它記成數學結論。
最後一刀:每個時間步前面那個由時程決定的權重係數,實務上直接丟掉,所有步一律等權。這也不是偷懶,是刻意的經驗選擇——丟掉之後效果反而更好(帶權的精確形式在原書對照節指的頁碼裡)。丟完,整章的推導塌成一條乾淨得不可思議的損失:
逐項拆解:\(\ell\) 是單筆損失——網路認出來的雜訊跟實際混進去的雜訊,逐分量相減、平方、加總,\(\lVert\cdot\rVert^2\) 就是這個「差的平方長度」。\(\mathcal{L}\) 照全站慣例取批次平均;第 \(i\) 筆資料隨機抽自己的時間步 \(t_i\) 與雜訊 \(\boldsymbol\epsilon_i\)。訓練迴圈於是短得像一句話:抽一筆資料、抽一個時間步、抽一份雜訊,用跳步公式一步造出加噪樣本,要求網路把那份雜訊認出來。 沒有博弈、沒有內外兩層目標,就是第 5 章那種平方誤差迴歸。附帶一個免費的紅利:同一筆資料可以配上取之不盡的(時間步、雜訊)組合,每種組合都是一題新練習——訓練資料等於被無限放大。
比喻: 回到那位學徒。他真正的本事說穿了是「認滲紋」:給他一幀化到一半的水,他指得出哪些暈紋是後來滲進來的、哪些濃淡是墨滴本來的樣子。認出滲進來的部分,往回退一步就等於把它退掉一點。輪到生成時,師傅遞來一杯全新的、完全勻開的灰水,讓他一步步往回收——最後收出來的那滴墨,不是哪卷舊影片的還原,是一滴新的墨。這個比喻最要緊的失準之處也在這裡:「影片倒帶」聽起來像在播放一卷已經存在的影片,但生成時根本沒有影片可倒——每一步是「猜一小步,再補一點隨機」,同一杯灰水可以倒出無數滴不同的墨。
取樣迴圈把「猜一小步,再補一點隨機」寫成式子。每一步先用雜訊預測換算出上一步的估計,再補一小份新的雜訊:
逐項拆解:括號那一坨就是網路版的平均 \(\boldsymbol\mu_t\),拿它當上一步的估計;\(\boldsymbol\epsilon'\) 是現抽的一份新標準常態雜訊,跟訓練時的 \(\boldsymbol\epsilon\) 毫無關係;\(\sigma_t\) 是上一節那個預先固定的寬度。為什麼還要補雜訊?因為反向的每一步本來就是一個機率分布,不是一條定軌——只取平均,等於把每一步的分布硬壓成一個點,抽出來的東西會失真(下面的程式跑完,你可以把補雜訊那行拿掉重跑一次,親眼看失真有多嚴重)。唯一的例外是收尾:走到 \(t=1\) 這一步時輸出就是最終的 \(\mathbf{x}\),只取估計、不再補雜訊——收墨滴的最後一收,手要穩。
最後把整章串成完整一圈。資料取一團二維的墨點粒子雲,而且刻意取成常態分布——這個選擇有私心:資料是常態時,每一步的最佳雜訊預測恰好是 \(\mathbf{z}_t\) 的線性函數,用閉式最小平方就能「訓練」,不必動用梯度下降,四十行以內走完「訓練+生成」:
import numpy as np
rng = np.random.default_rng(0)
T = 60
rho = np.linspace(0.008, 0.16, T)
kappa = np.cumprod(1 - rho)
# 資料:一團墨點粒子(二維常態,兩個座標刻意帶相關性)
n = 6000
L = np.array([[0.46, 0.0], [0.17, 0.31]])
x = rng.normal(0, 1, (n, 2)) @ L.T + np.array([0.9, -0.6])
# 「訓練」:每個時間步各解一個線性雜訊預測器(閉式最小平方)
W = []
for t in range(T):
eps = rng.normal(0, 1, (n, 2))
zt = np.sqrt(kappa[t]) * x + np.sqrt(1 - kappa[t]) * eps
A = np.hstack([zt, np.ones((n, 1))]) # 特徵=z 的兩個座標+常數項
W.append(np.linalg.lstsq(A, eps, rcond=None)[0])
# 取樣:從純雜訊出發,每一步先估、再補一小份新雜訊
m = 4000
z = rng.normal(0, 1, (m, 2))
for t in range(T - 1, -1, -1):
eps_hat = np.hstack([z, np.ones((m, 1))]) @ W[t]
z = (z - rho[t] / np.sqrt(1 - kappa[t]) * eps_hat) / np.sqrt(1 - rho[t])
if t > 0: # 收尾那一步不補
z = z + np.sqrt(rho[t]) * rng.normal(0, 1, (m, 2))
print("資料 mean =", np.round(x.mean(0), 3), " 生成 mean =", np.round(z.mean(0), 3))
print("資料 cov:", np.round(np.cov(x.T), 3).tolist())
print("生成 cov:", np.round(np.cov(z.T), 3).tolist())實跑輸出:生成樣本的平均 [0.898 -0.595] 對資料的 [0.903 -0.598];共變異數矩陣逐格對照——資料是 [[0.212, 0.075], [0.075, 0.122]],生成是 [[0.208, 0.071], [0.071, 0.118]],每一格的差距都在 0.005 以內。從純雜訊出發,靠六十個「認雜訊」的線性小步,收回了一團平均、散布、傾斜方向都對的墨點雲;而這一圈裡沒有任何一步是神祕的,每個零件你都在前三節親手摸過。補噪那行的對照實驗我也替你跑了一次:拿掉 if t > 0 那兩行再跑,生成樣本的共變異數塌到 [[0.0, 0.0], [0.0, 0.0]]——所有樣本縮成幾乎同一個點。反向的每一步是分布不是定軌,這個實驗是最直接的證據。
五、實作與條件生成:概覽
把玩具換成真實影像時,機制一個字不用改,要換的只有網路的形狀。去噪網路吃進一張加噪影像、吐出同尺寸的雜訊估計,主流選擇是一種「影像進、影像出」、能同時照顧粗尺度與細尺度的架構,領域慣稱 U-Net(無通行中譯);它的組成沒有新零件——第 11 章的殘差區塊、第 12 章的自注意力,拼裝起來而已。另一個工程要點:\(T\) 個時間步不是 \(T\) 個網路。實務上用一個網路吃所有時間步,把「現在是第幾步」編成一個向量跟輸入一起餵進去——與第 12 章位置編碼同款思路,只是編的不是位置、是時間步。
代價也要說清楚。取樣慢是結構性的:常態近似逼著步子小,步子小逼著步數多,而每退一步都要過一次網路前傳,這條因果鏈沒有一環能白白省掉。不過,同一條「認雜訊」的訓練目標其實相容於一整族前向流程,各種加速取樣的做法就從這個自由度長出來——本課不展開,原書對照節有路線圖。
條件生成(conditional generation)——第 1 章介紹過這個需求:指定條件、只生成符合條件的樣本。擴散模型上有兩類做法。一類外接一個現成的分類器,取樣時用分類器的梯度在每一步把去噪的方向往指定類別推一把,稱作分類器引導(classifier guidance)。另一類更常用:把條件直接餵給去噪網路一起訓練,且訓練時隨機把條件遮掉一部分批次,讓同一個網路同時學會「有條件」與「沒條件」兩種去噪;使用時把兩種預測的差距按倍數放大,就得到一個可調的「聽條件的程度」,稱作無分類器引導(classifier-free guidance)。這個倍數轉過頭有一個為人熟知的副作用:單張品質變高、整體輸出變刻板——聽話聽得太徹底,多樣性就被犧牲。品質與多樣,是一組要自己拿捏的取捨。
高解析度通常不一步生到位,而是先在低解析度生一張小圖,再以小圖為條件逐級放大,稱作級聯(cascade)。把這些零件組合起來——文字編碼當條件、無分類器引導、級聯放大——就是文字生成影像系統的基本骨架,沒有額外的魔法。
六、這條路線換到了什麼
最後盤點這條路線的帳。拿到的:訓練是一條純平方誤差的迴歸,沒有兩個網路互相牽制的動態,穩定好調;同一筆資料配上無窮的(時間步、雜訊)組合,資料利用率極高;生成品質站上了公認的高標。付出的:取樣要一長串網路前傳,慢是結構性的;而在「算不算得出機率」這把尺上——第 14 章就用這把尺劃分過各家生成模型——本章的基本形式交出的是概似的下界,不是精確值。第 15 章末尾說過,「抽得出樣本、算不出密度」是那一章做法的選擇;本章補回了一半:你能給每筆資料算出一個有保證的下界。想要精確的機率,得走第 16 章那條用可逆變換換取精確概似的路線——各有各的代價,沒有免費的。
一句話收束整章。一滴墨的化開不用學,物理替你寫好了;本事全在倒帶——而倒帶的本事,說到底是認出每一幀裡哪些是後來滲進來的雜訊。前向固定、反向逐步、損失塌成認雜訊:三件事扣在一起,就是擴散模型。
§03原書對照
原書第 18 章是全書代數密度最高的章節之一:正文把每一步化簡都攤開來寫,頁邊標了對應的附錄小節與習題。以下按主題指路。
開章 pp.349–350 先把這一族模型放進生成模型的版圖:與同部其他路線各自共享什麼、又差在哪裡,一段講完;p.350 的腳註提醒「前向」一詞在不同模型家族裡指的方向相反,讀跨章文獻前值得先記住。前向過程的定義與機率形式在 pp.350–351;pp.351–352 用一個一維數值例把整條鏈的分布演化畫成圖組,樣本軌跡、條件分布、邊際分布各有一張,是全章直覺的地基。跳步公式(原書稱擴散核)的推導在 p.353,從兩步代換起頭、歸納到任意步,並給出機率形式。p.354 說明邊際分布為何沒有閉式、直接反轉單步分布為何行不通;pp.354–356 推導「握住起點之後反向分布變成可算常態」的完整式子,途中用到的兩條高斯恆等式列在 p.356,並指向附錄的對應小節,也配了證明用的習題。
反向模型的常態近似與取樣流程在 pp.356–357,同一段也交代了這個近似成立的前提條件寫在哪裡。訓練目標的推導佔 pp.357–360:p.357 用不等式立出下界,並就「誰負責把界收緊」這一點對照了它與前一章那族模型的差別;pp.358–359 做逐項化簡——代換、連乘消去、化成一串散度項,再把每一項化成平均值差的平方——p.360 收成最終損失,附一維例的擬合結果。想逐行跟代數的人,這三頁半每個等號都有交代,頁邊還標了附錄裡散度定義與閉式的位置。
重參數化在 pp.361–363,分成目標的改寫與網路職務的改寫兩段,各有一長串化簡,關鍵恆等式都配了習題;p.363 得到最後那條極簡損失,並註明實務上各步的權重被直接丟掉。訓練與取樣兩個演算法用框線列在 p.364,一目了然——只想抓骨架的人,把開章的 pp.349–350 與這兩個框合起來讀就夠。
實作在 pp.364–365:影像上選什麼架構、時間步怎麼編成向量餵進網路、步數與取樣速度的關係。pp.364–366 說明同一個損失相容於一整族前向流程,以及由此而來的加速做法,配圖比較了不同流程的軌跡與樣本。條件生成在 pp.365–367:外接分類器的引導式與把條件併入網路的免分類器式各有一段,後者連同「條件加權過頭會怎樣」一起交代。級聯生成高解析度的流程在 p.367,成果展示在 pp.368–369。p.368 有一節簡短的本章總結,把整條推導線收攏成幾段話,複習時值得回頭讀。
pp.368–372 的註記是一張文獻路線圖:這條路線的起源與里程碑、影像之外各種資料型態的應用、把加噪換成其他退化方式的變體、與其他數學觀點的正式連結、提速與提質的各條戰線,各有一段並附出處。其中提速那幾段把「為什麼慢」拆解得比正文更細,想往工程端走的人值得細讀;p.370 也誠實列出了這一族模型的主要缺點。章末 pp.372–373 有十二道習題,多數是正文推導的補全,適合想把代數親手走一遍的讀者。原書第 18 章對應印刷頁 pp.349–373。
§04作業和解答
作業一:兩步合成一步
用本站符號寫下連續兩個單步更新:\(\mathbf{z}_1=\sqrt{1-\rho_1}\,\mathbf{z}_0+\sqrt{\rho_1}\,\boldsymbol\epsilon_1\)、\(\mathbf{z}_2=\sqrt{1-\rho_2}\,\mathbf{z}_1+\sqrt{\rho_2}\,\boldsymbol\epsilon_2\),其中 \(\mathbf{z}_0\) 是資料、兩份雜訊互相獨立。(a)把第一式代入第二式,證明 \(\mathbf{z}_2\) 可以寫成 \(c\,\mathbf{z}_0+\sqrt{1-c^2}\,\boldsymbol\epsilon\) 的形式(\(\boldsymbol\epsilon\) 是單獨一份標準常態雜訊),求出 \(c\),並指出它跟跳步公式的關係。(b)設 \(\rho_1=0.03\)、\(\rho_2=0.08\),用 numpy 造 20 萬個一維樣本,分別跑「真的走兩步」與「合併成一步」,比較兩組的平均與標準差。
解答 SOLUTION
(a)代入後展開:\(\mathbf{z}_2=\sqrt{(1-\rho_1)(1-\rho_2)}\,\mathbf{z}_0+\sqrt{(1-\rho_2)\rho_1}\,\boldsymbol\epsilon_1+\sqrt{\rho_2}\,\boldsymbol\epsilon_2\)。後兩項是兩份獨立常態雜訊的線性組合,仍是常態且變異數相加:\((1-\rho_2)\rho_1+\rho_2 = 1-(1-\rho_1)(1-\rho_2)\)。所以 \(c=\sqrt{(1-\rho_1)(1-\rho_2)}\),合併後的雜訊變異數恰是 \(1-c^2\)——這正是跳步公式在 \(t=2\) 的情形:\(c=\sqrt{\kappa_2}\)。同樣的合併一路做下去,就得到 \(\kappa_t\) 的連乘定義。
(b)程式與實跑輸出:
import numpy as np
rng = np.random.default_rng(0)
rho1, rho2 = 0.03, 0.08
c = np.sqrt((1 - rho1) * (1 - rho2))
print("合併係數 =", round(c, 4), " 合併雜訊 std =", round(np.sqrt(1 - c**2), 4))
n = 200000
z0 = rng.normal(1.4, 0.6, n) # 自創的一維起點分布
z2 = (np.sqrt(1 - rho2) * (np.sqrt(1 - rho1) * z0
+ np.sqrt(rho1) * rng.normal(0, 1, n))
+ np.sqrt(rho2) * rng.normal(0, 1, n))
zm = c * z0 + np.sqrt(1 - c**2) * rng.normal(0, 1, n)
print("兩步:mean =", round(z2.mean(), 4), " std =", round(z2.std(), 4))
print("合併:mean =", round(zm.mean(), 4), " std =", round(zm.std(), 4))合併係數 \(c=0.9447\)、合併雜訊標準差 0.328;起點取平均 1.4、標準差 0.6 的一維常態,走兩步的樣本平均 1.3238、標準差 0.6543,合併一步的樣本平均 1.3225、標準差 0.6552——兩組在抽樣抖動範圍內一致,合併式成立。
作業二:認雜訊=認訊號
從跳步公式 \(\mathbf{z}_t=\sqrt{\kappa_t}\,\mathbf{x}+\sqrt{1-\kappa_t}\,\boldsymbol\epsilon\) 出發:(a)解出用 \(\mathbf{z}_t\) 與 \(\boldsymbol\epsilon\) 表示 \(\mathbf{x}\) 的式子;(b)假設你手上有一個訓練好的雜訊預測網路 \(\hat{\boldsymbol\epsilon}_t[\mathbf{z}_t,\boldsymbol\phi]\),寫出由它換算「起點估計」\(\hat{\mathbf{x}}\) 的公式;(c)用兩三句白話說明,為什麼「猜訊號的網路」與「認雜訊的網路」是同一種能力的兩種寫法,以及「等價」這個詞在這裡涵蓋什麼、不涵蓋什麼。
解答 SOLUTION
(a)移項再除以係數:\(\mathbf{x}=\bigl(\mathbf{z}_t-\sqrt{1-\kappa_t}\,\boldsymbol\epsilon\bigr)/\sqrt{\kappa_t}\)。
(b)把真雜訊換成網路的預測:\(\hat{\mathbf{x}}=\bigl(\mathbf{z}_t-\sqrt{1-\kappa_t}\,\hat{\boldsymbol\epsilon}_t[\mathbf{z}_t,\boldsymbol\phi]\bigr)/\sqrt{\kappa_t}\)。
(c)\(\mathbf{z}_t\) 是訊號與雜訊按已知比例的線性合成;站在 \(\mathbf{z}_t\) 上,兩個成分只有一個是自由的——認出雜訊,剩下的就是訊號;認出訊號,剩下的就是雜訊。兩種網路之間隔著一條係數全部已知的線性換算,不多不少。「等價」涵蓋的是數學上的互換:任何一個雜訊預測都唯一對應一個訊號預測。它不涵蓋訓練難易——哪一種目標訓練起來效果好是另一回事,實務上認雜訊那一版勝出,而那是經驗結論、不是定理。
作業三:設計一組加噪時程
總步數 \(T=40\),要求走完時累積保真比例 \(\kappa_{40}<0.01\)(墨剩不到百分之一)。(a)設計一組線性遞增的 \(\rho_t\),用程式算出 \(\kappa_{40}\) 驗證達標。(b)把你的 \(\rho_t\) 全部乘以 2 之後,\(\kappa_{40}\) 變成多少?這樣做最先弄壞的是哪個環節?(提示:回想反向模型用常態近似的前提。)
解答 SOLUTION
(a)取 \(\rho_t\) 從 0.02 線性升到 0.21。程式與實跑輸出:
import numpy as np
T = 40
rho = np.linspace(0.02, 0.21, T)
kappa = np.cumprod(1 - rho)
print("設計時程 kappa_40 =", round(kappa[-1], 5))
rho2 = 2 * rho
print("加倍後 rho 最大值 =", round(rho2.max(), 3))
print("加倍後 kappa_40 =", f"{np.cumprod(1 - rho2)[-1]:.2e}")輸出 kappa_40 = 0.00696,小於 0.01,達標。
(b)實跑輸出:加倍後 \(\rho_t\) 最大到 0.42,\(\kappa_{40}\) 掉到約 1.86e-05——「墨洗得夠乾淨」這個指標反而更漂亮了,但最先壞掉的不是 \(\kappa\),是常態近似的前提。反向模型每一步用一個常態去近似真正的反向條件分布,撐住這個近似的是「步子小」;單步就混掉四成的內容,上一幀與這一幀相距太遠,真正的反向分布會歪、會出多峰,常態罩不住——網路再準,追的也是一個形狀就錯了的目標。時程設計的真正約束從來是兩頭的:\(\kappa_T\) 要夠小(終點夠接近純雜訊),每一步的 \(\rho_t\) 又要夠小(常態近似站得住);總步數 \(T\) 就是在這兩個要求之間換出來的。
§05參考資料
- What are Diffusion Models?(Lilian Weng) — 密度最高的一篇綜述筆記,本章的每條公式都能在裡面找到對應段落與出處
- Denoising Diffusion Probabilistic Models(arXiv) — 「損失塌成認雜訊」那條路線的原始論文,等權簡化與實驗證據都在這裡
- Deep Unsupervised Learning using Nonequilibrium Thermodynamics(arXiv) — 這一族模型最早的源頭,「前向固定、反向學習」的構圖第一次被完整寫下
- Understanding Diffusion Models: A Unified Perspective(arXiv) — 把本章刻意略過的三段大化簡逐行攤開的教學型綜述,想跟代數的人看這篇
- The Annotated Diffusion Model(Hugging Face) — 逐行帶你把訓練與取樣迴圈寫成能跑的程式,是本章玩具版的工業版
- Generative Modeling by Estimating Gradients of the Data Distribution(Yang Song) — 同一套機制的另一種數學視角,讀完本章再看會有「原來是同一件事」的時刻
- Understanding Deep Learning(MIT Press) — 本課課綱主題所本的原書出版頁(ISBN 9780262048644,2023-12 出版)
- udlbook 官方網站(作者釋出的 PDF、投影片與習題) — 原書作者維護的免費資源站(udlbook.com 會轉址到此)