一小步的雜訊
在第一篇導讀中,我們看到擴散模型做了一件近乎魔法的事:從一整片純粹的雜訊開始,一步一步耐心地,把它雕琢成一張清晰的圖片。當時我們是先選擇相信它能行。現在,我們要掀開引擎蓋。模型在學會移除雜訊之前,我們必須先精確定義雜訊一開始是如何被加入的——而且出乎意料地,這個「加入」的步驟才是簡單的那一半。它是固定的、由人手設計的,完全不含任何學習。這就是前向擴散過程,把它定義得恰到好處,正是讓整套方法能夠被訓練的關鍵。
底下每一行都會用到一個材料,所以先用一句話把它釘牢:高斯分佈(又稱常態分佈)不過就是一堆隨機數值聚集在某個中心平均值附近,而它們散開的程度由變異數決定——想像那條經典的鐘形曲線,中間又高又密,往兩側平滑地變稀。變異數小,鐘形就窄而尖(數值緊貼平均值);變異數大,鐘形就寬而平(數值四處遊走)。當我們對某個像素「加入高斯雜訊」,就是依照這樣一條鐘形曲線抽出一個隨機量,去輕推它的數值。
一排五張影像方塊,由左至右從清晰的色塊逐漸變成散亂的顆粒點,上方箭頭加入雜訊,下方箭頭移除雜訊。
擴散是「逐步地」加入雜訊,分成許多極小的步驟,而不是一次加完。把乾淨影像記為 x_0,把完全變成雜訊的終點記為 x_T(T 常取大約 1000)。每一步拿目前的影像 x_{t-1},產生一張稍微更吵雜的 x_t。單一步驟的規則寫成一個條件高斯分佈——讀作「在已知前一張影像的條件下,下一張是從這條鐘形曲線抽出來的」:
把每個零件都點名。q(\cdot) 是前向(加噪)分佈——小寫的 q 標示它是固定、由人手設計的,不是要學的模型。x_t 是第 t 步的影像;x_{t-1} 是它前一步、稍微乾淨一點的影像。符號 \mathcal{N}(x_t;\,\mu,\,\Sigma) 的意思是「x_t 服從平均值為 \mu、共變異數為 \Sigma 的高斯分佈」。這裡平均值是 \mu = \sqrt{1-\beta_t}\,x_{t-1}——也就是前一張影像的縮小版:把每個像素都乘上 \sqrt{1-\beta_t} 這個略小於 1 的係數,畫面因此被輕輕調暗一點點。共變異數是 \Sigma = \beta_t\mathbf{I},其中 \beta_t(一個小正數,這一步的「雜訊變異數」)決定我們灑進多少新的隨機性,而 \mathbf{I} 是單位矩陣——代表雜訊是獨立地加到每一個像素上,每個像素的變異數同為 \beta_t,像素之間互不相關。所以一步=把影像略微調暗,再撒上獨立的高斯顆粒。為什麼要有縮小係數?它讓總「能量」維持穩定:存活下來的訊號保留 (1-\beta_t) 的變異數,我們再補回 \beta_t 的雜訊,於是原本變異數為 1 的像素仍停在變異數 1——影像既不會爆掉,也不會褪成空白,只是把訊號換成雜訊。實際感受一下:取典型的早期步 \beta_t = 0.0001,係數 \sqrt{1-\beta_t} \approx 0.99995,加入的雜訊只有一絲絲,所以 x_t 看起來和 x_{t-1} 幾乎一模一樣,只多了一層極淡的顆粒。最關鍵的是:這裡每個數字都是我們事先選好的,這條規則裡沒有任何東西是學來的。
雜訊排程:加多少、何時加
單一步幾乎不會改變影像——這正是重點——但要從一張照片一路走到純粹的雜訊,我們得把好幾百步串起來。所有步長 \beta_1, \beta_2, \ldots, \beta_T 組成的這份清單,就叫做雜訊排程。它是決定我們加多少雜訊、何時加的那份食譜。幾乎每次,\beta_t 都是一開始很小,隨著 t 變大而成長:早期輕輕推一下,後期大口灌入雜訊。
為什麼這份排程的「形狀」這麼要緊?想想模型在每一步要學的是什麼——剛好把這一步加進去的雜訊還原回來。如果太早倒進太多雜訊,影像的細微結構(邊緣、紋理、小細節)幾乎立刻被抹掉,到了最難、最講究細節的那些步驟,模型就沒有東西可抓。反過來,加得太少、太慢,那 T 步裡大多數都是幾乎沒雜訊的複製品——白費力氣,網路幾乎學不到東西。好的排程,會把每一步花在刀口上。
最早的去雜訊擴散機率模型用的是線性排程:\beta_t 沿一條直線上升(例如從 10^{-4} 到 0.02)。它能用,但傾向在後半段太快把訊號摧毀——當你走到通往 x_T 的三分之二處時,影像幾乎已是純雜訊,於是最後三分之一的步驟做不了多少有用的工。後來的改良——餘弦排程——把這條曲線彎一下,讓雜訊在關鍵的中段加得更溫和,只在快到終點時才陡升。打個比方就像調光開關:便宜的調光器一轉就跳到接近全黑,好的調光器則在整段行程裡平順地把光線淡下去。餘弦排程就是那個平順的調光器——它在中段保留更多訊號,而那裡正是模型在學著重建最多結構的地方,實務上也就產生明顯更好的影像。
替每一步存活下來的那部分取個自己的名字,會很划算。定義 \alpha_t = 1 - \beta_t。既然 \beta_t 是第 t 步我們花在新雜訊上的變異數比例,它的互補 \alpha_t 就是第 t 步後訊號變異數存活下來的比例。若 \beta_t = 0.01,則 \alpha_t = 0.99——原本的東西有 99% 延續下去,1% 被新雜訊取代。注意每一步的 \alpha_t 都略小於 1。這個小小的細節,正是下一節那條捷徑的種子:這一步存活 99%、再對它存活 99%、再一次……這種東西,我們可以直接連乘起來算出來。
神奇捷徑:直接跳到第 t 步
這裡有個實務上的頭痛問題。為了訓練去雜訊器,我們會在隨機的步驟拿吵雜影像給它看——有時 t=5,有時 t=700。但目前的定義是遞迴的:要得到 x_{700},每一次訓練迭代都得從 x_0 開始把單步規則搖 700 次。那會慢到讓人抓狂。我們想要一個能對任意 t 一口氣直接跳到第 t 步的辦法。令人驚訝的是,真的有。
它之所以存在,是因為高斯分佈一個漂亮的性質:把一個高斯縮放,再加上另一個獨立的高斯,結果仍然是單一個高斯——於是可以把許多小的加噪步驟摺成一步。既然前向擴散過程的每一步都是高斯,把 t 步串起來就塌縮成一個高斯,直接把乾淨影像 x_0 一路帶到 x_t。把代數推一推——其實就是反覆把單步規則代入它自己——就得到這個封閉形式:
逐項拆解。x_0 是資料集裡原始的乾淨影像。\bar\alpha_t(讀作「t 處的 alpha-bar」)是累積乘積 \alpha_1\alpha_2\cdots\alpha_t——把雜訊排程裡每一步存活的比例全部相乘,也就是原始影像一路存活到第 t 步還剩多少。因為每個 \alpha_s 都略小於 1,乘上許多個之後,\bar\alpha_t 就從接近 1(t 小)穩穩地縮向 0(t 大)。於是第 t 步的影像是兩樣東西的加權混合:\sqrt{\bar\alpha_t}\,x_0,真實影像的褪色版;加上 \sqrt{1-\bar\alpha_t}\,\epsilon,一劑雜訊。那唯一的雜訊項 \epsilon 是從標準高斯 \mathcal{N}(0,\mathbf{I}) 抽出的一個新樣本——平均值 0、每個像素變異數 1、彼此獨立——而且每次都重新抽。這兩個權重的設計讓它們的變異數相加為 1,因為 \bar\alpha_t + (1-\bar\alpha_t) = 1:影像保持單位能量,持續地把畫面換成雜訊。代點數字進去。在小步,比如 \bar\alpha_t = 0.98:\sqrt{0.98} \approx 0.99 乘在真實影像上,而雜訊只乘上 \sqrt{0.02} \approx 0.14——x_t 大致就是真實照片,帶點淡淡顆粒。在大步、\bar\alpha_t = 0.0001 時:\sqrt{\bar\alpha_t} \approx 0.01 幾乎留不住影像,而 \sqrt{1-\bar\alpha_t} \approx 1.0——於是 x_t 幾乎是純雜訊 \epsilon。換句話說,\bar\alpha_t 根本就是一個旋鈕,隨著 t 變大,把影像從「真實」一路滑向「雜訊」。
同一條五格擴散長條:左側幾格大致清晰(存活訊號高),右側幾格幾乎是顆粒(存活訊號低)。
import torch
def q_sample(x0, t, alpha_bar):
"""Jump straight to step t in ONE line -- no loop over 1..t."""
eps = torch.randn_like(x0) # one standard-normal sample, same shape as x0
a = alpha_bar[t].sqrt() # sqrt(alpha_bar_t): how much signal survives
b = (1.0 - alpha_bar[t]).sqrt() # sqrt(1 - alpha_bar_t): how much noise mixed in
x_t = a * x0 + b * eps # the noisy image at step t
return x_t, eps # return eps too -- it is the training target重參數化:為何我們預測雜訊
那麼,網路到底該算什麼?封閉形式 x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon 把三個量綁在一起:吵雜影像 x_t、乾淨影像 x_0、以及雜訊 \epsilon。關鍵觀察是:如果你手上已經有 x_t(網路永遠看得到它),那麼 x_0 和 \epsilon 其實是同一件事的兩種說法。把方程式重新整理、把 \epsilon 單獨解出來,就能證明這點:
這樣讀:在已知吵雜影像 x_t 與乾淨影像 x_0 的條件下,雜訊 \epsilon 就被完全決定了——它不過是 x_t 減去褪色的乾淨影像,再重新縮放。反過來也一樣:給定 x_t 和 \epsilon,你就能還原 x_0。它們在代數上可以互換。這就是重參數化技巧的親切版本:與其要網路直接輸出乾淨影像,我們可以要它輸出雜訊 \epsilon,而且毫無損失——任一個答案都能換出另一個。事實證明,預測 \epsilon 是更容易、更乖巧的目標:不論我們在哪一步 t,\epsilon 永遠有同樣整齊的尺度(平均值 0、變異數 1);反觀要重建的正確 x_0,早期和後期長得天差地遠。一個一致的目標,讓這個迴歸穩定許多。
於是我們造一個網路,工作就是看一張吵雜影像、猜出裡頭的雜訊。把它寫成 \epsilon_\theta(x_t, t)。下標 \theta 代表網路所有可學的權重(梯度下降會去調的那幾百萬個數)。它吃兩個輸入:x_t,吵雜影像;以及 t,時間步——餵進 t 至關重要,因為網路必須知道輸入有多吵,才能判斷該預測多少雜訊(幾乎沒顆粒的 x_5 和接近純雜訊的 x_{900} 需要極為不同的答案)。這個雜訊猜測器就是去雜訊 U-Net,一旦訓練好,它正是下一篇裡反向去雜訊過程會一步步開動、把雜訊變回圖片的那具引擎。
訓練目標:簡潔的一行式
現在我們已具備寫出訓練目標所需的一切——而它簡單得驚人。沒有兩個網路之間的對抗角力,沒有微妙的平衡把戲。只是一個迴歸:讓預測的雜訊去對上真實的雜訊。DDPM 論文真正拿來訓練的簡化損失,就一行:
把它拆開:\mathcal{L} 是我們要最小化的損失。\mathbb{E}_{x_0, t, \epsilon}[\cdot] 是期望值——一個花俏的說法,意思是「對許多次隨機抽樣取平均」——它對三個隨機選擇取平均:從訓練集抽出的乾淨影像 x_0、均勻隨機挑的時間步 t、以及雜訊樣本 \epsilon \sim \mathcal{N}(0,\mathbf{I})。由這些,我們用第 3 節的封閉形式捷徑造出吵雜影像 x_t。括號裡,\epsilon 是我們真正加進去的雜訊(目標),\epsilon_\theta(x_t, t) 是網路的猜測,而 \lVert\cdot\rVert^2 是平方 L2 範數——把每個像素上的差的平方加總起來。白話說,括號裡就是這次雜訊猜得有多錯,訓練則把這個平均的錯誤量往下壓。這是一個樸實的迴歸,完全沒有讓其他生成模型難訓練的那種對抗式不穩定——而一個訓練良好的 \epsilon_\theta,正是下一篇要接手、用來把反向鏈倒著走、從雜訊走回圖片的那把工具。
- 從資料集抓一張真實影像 x_0。
- 隨機挑一個時間步 t(在 1 到 T 之間均勻抽)。
- 抽出新雜訊 \epsilon \sim \mathcal{N}(0,\mathbf{I}),用一行造出吵雜影像:x_t = \sqrt{\bar\alpha_t}\,x_0 + \sqrt{1-\bar\alpha_t}\,\epsilon。
- 把 x_t 和 t 餵給去雜訊 U-Net,讀出它的預測 \epsilon_\theta(x_t, t)。
- 算出平方誤差 \lVert \epsilon - \epsilon_\theta(x_t,t)\rVert^2,走一步梯度下降,輕推權重 \theta,讓下次的猜測好一點點。
def training_step(model, x0, alpha_bar, T):
t = torch.randint(0, T, (x0.size(0),)) # 1-2. a random timestep per image
x_t, eps = q_sample(x0, t, alpha_bar) # 3. sample noise, build x_t in one shot
eps_pred = model(x_t, t) # 4. ask the network to guess the noise
loss = ((eps - eps_pred) ** 2).mean() # 5. mean-squared error: how wrong the guess was
loss.backward() # gradients -> nudge weights downhill
return loss一條彎曲的損失曲面,一顆球連續走出小步往下,朝最低點(極小值)前進。