JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

學會去雜訊:U-Net 與取樣器

既然網路能辨認雜訊,就來看它如何把滿天雪花一路走回一張清晰影像——認識 U-Net 去雜訊器、反向步驟、分數基礎觀點,以及 DDIM 加速法。

重述去雜訊器的任務

讓我們從第 2 篇結束的地方接著講。我們訓練了一個神經網路,寫成 \epsilon_\theta,它的工作謙卑卻強大:給它一張帶雜訊的影像 \mathbf{x}_t,再加上一個數字 t 告訴它這張影像有多吵,它就會預測藏在裡面的雜訊。它預測的不是乾淨的圖片,而是被混進去的那層雜訊本身。下標 \theta 是「所有可訓練的權重」的簡寫,所以 \epsilon_\theta 字面意思就是「由我們訓練好的權重所產生的雜訊猜測」。

但是「找出雜訊」只是魔術的一半。擴散模型真正的目標是生成一張全新的圖片,而起點只是純粹的雜訊 \mathbf{x}_T——一片完全沒有影像的隨機雪花。所以本篇要回答的問題是:既然我們已經擁有一個好的雜訊預測器,要怎麼真正運用它,一步一步地把那片雜訊的天空,走成一張清晰、前所未見的影像?

答案分成兩部分,我們會依序來看。第一部分是讓 \epsilon_\theta 一開始就能成為預測器的架構——一種特定形狀的網路,叫做去雜訊 U-Net第二部分是把一連串預測變成成品影像的食譜——反向去雜訊過程,也叫取樣器(sampler)。先談架構,再談取樣:先有一顆好腦袋,再有一套善用它的流程。

走進去雜訊 U-Net 內部

為什麼非得用一種特別的形狀?因為去除雜訊需要兩種互相拉扯的能力。要知道這張圖應該是什麼——一張臉、一隻貓、一片風景——網路必須一次看見整張影像,對龐大、粗略的結構進行推理。但要補回銳利的邊緣與細緻的紋理,它又得在完整解析度下、一個像素一個像素地工作。去雜訊 U-Net 就是一種同時做到這兩件事的優雅形狀。想像一位畫家:先退到離畫布很遠的地方規劃整體構圖,再湊近用細筆把細節補回來。U-Net 的兩半,正是這個「退後」與「湊近」。

U-Net:編碼器把影像逐層縮小成一個小而抽象的摘要,解碼器再把它重建回完整尺寸,而跳接(圖中水平的橋)則把細節直接橫向搬運過去。

一個 U 字形圖示:左側是逐層降採樣的編碼器路徑,右側是逐層升採樣的解碼器路徑,中間有水平的跳接箭頭連起對應的層級。

左半邊是編碼器(也就是「退後」)。它反覆把影像縮小——把寬與高各砍一半,同時讓通道數成長——於是經過幾個階段後,一張 256×256 的圖會變成一個很小的格子,比方說 16×16 的特徵向量。縮小迫使網路丟掉精確的像素位置,改去摘要出現了什麼以及大致在哪:這是一隻貓、身體在這、頭在左上。這種粗略、全域的理解,正是判斷去雜訊後影像該長什麼樣所需要的。

右半邊是解碼器(也就是「湊近」)。它做相反的事,把那個極小的摘要逐層升採樣回完整的 256×256 解析度。但光靠它自己,從 16×16 的格子放大只會得到模糊的東西——細節在縮小的路上早就被丟掉了。解法就是 U-Net 的招牌絕活:跳接(skip connections)。在每一層,解碼器都會直接拿到對應編碼器特徵圖的一份副本(圖中那些水平的橋)。於是解碼器兩邊的好處都拿到了——從底部重建出來的粗略「是什麼、在哪裡」,加上從編碼器直接接過來的銳利高解析邊緣。這就是為什麼 U-Net 的輸出能保住俐落線條,而不會糊成一團。

還有一個關鍵輸入:時間步 t。同一個網路在高雜訊時(該下大膽、粗略的筆觸)和低雜訊時(該做細微、謹慎的修飾)必須表現得非常不同。我們用時間嵌入(time embedding)告訴它目前處在哪個狀態:把整數 t 轉成一個向量(用正弦函數,很像位置編碼),再把這個向量注入網路的每一個區塊。具體來說,它會對特徵做縮放與平移,於是同一組權重就能扮演 1000 個略有不同的去雜訊器,每個雜訊等級一個——而我們不必訓練 1000 個獨立的網路。

最後,現代的 U-Net 會在低解析度的階段(格子夠小、負擔得起的地方)灑上幾層自注意力(self-attention)。注意力讓影像中相距很遠的部分彼此對話、達成一致——於是一張臉的兩隻眼睛最終會互相匹配,或地平線的左右兩端對得齊。請記住這個細節,因為它就是第 4 篇的鷹架:同一個骨幹之後會搭載交叉注意力(cross-attention),那時網路關注的不是其他影像區塊,而是文字提示裡的詞。現在就把這個心智模型建起來——編碼器、解碼器、跳接、時間嵌入、注意力——之後文字生成圖像就能無縫接上。

反向步驟:一階一階往下走

現在輪到取樣器。生成是一道往下爬的梯子:我們從最頂端的純雜訊 \mathbf{x}_T 出發,一階一階往下走,\mathbf{x}_T \to \mathbf{x}_{T-1} \to \dots \to \mathbf{x}_0,每一步都乾淨一點點。反向去雜訊過程就是「往下走一階」的規則:給定目前的帶雜訊影像 \mathbf{x}_t,我們要怎麼算出乾淨一點點的 \mathbf{x}_{t-1}?這就是 DDPM 的祖先取樣(ancestral sampling)更新式,完整寫在下面。

\mathbf{x}_{t-1} = \frac{1}{\sqrt{\alpha_t}}\left(\mathbf{x}_t - \frac{1-\alpha_t}{\sqrt{1-\bar{\alpha}_t}}\,\epsilon_\theta(\mathbf{x}_t,t)\right) + \sigma_t\,\mathbf{z}, \qquad \mathbf{z}\sim\mathcal{N}(0,\mathbf{I})

一次 DDPM 反向步驟:先還原縮小,扣掉預測出的雜訊,再加上一小撮受控的新隨機性。

我們逐項拆解——並回想第 2 篇的雜訊排程\alpha_t 是第 t 步的「保留比例」,而 \bar{\alpha}_t = \alpha_1\alpha_2\cdots\alpha_t 是從一開始到現在的累積保留比例。(1) \epsilon_\theta(\mathbf{x}_t,t) 是我們的去雜訊 U-Net\mathbf{x}_t 內雜訊的猜測。(2) 因子 \tfrac{1-\alpha_t}{\sqrt{1-\bar{\alpha}_t}} 是那份猜測的權重:它把預測雜訊縮放到剛好適合這一步要去除的份量,而不是一次去掉全部雜訊。(3) 我們把這份縮放後的猜測從 \mathbf{x}_t 扣掉,朝乾淨的影像邁進一步。(4) 接著整個括號再乘上 \tfrac{1}{\sqrt{\alpha_t}},把影像「還原放大」——記得前向步驟乘了 \sqrt{\alpha_t}<1 來縮小訊號,所以這裡除以它,就把比例還原回正確大小。(5) 最後 \sigma_t\,\mathbf{z} 加上一小撮新的隨機性,其中 \mathbf{z} 是一份全新抽樣的標準高斯雜訊(每個像素均值 0、變異數 1),而 \sigma_t 是排程設定的一個小步長。

我們明明想去除雜訊,為什麼還要把它加回去?直覺是這樣的。U-Net 的雜訊猜測只是一個平均——在高雜訊時,有許多看似合理的乾淨影像都與同一片雪花相符。如果我們貪心地一口氣扣掉所有雜訊、完全不加回去,就會直直走向那個唯一「最安全的平均」答案,而許多張臉的平均看起來會像一張模糊、不成臉的東西。我們改成估計雜訊、去掉大部分以朝乾淨邁進,再加回一點點隨機性,好讓我們持續探索真實影像的空間,而不是塌縮到一個糊糊的均值。\sigma_t\,\mathbf{z} 這一項就是那個探索性的輕推——而在最後一步(t=1)我們令 \mathbf{z}=0,因為我們要最終輸出是清晰的,不要再撒上新的雜點。令人安心的是,這一切都不是憑空捏造:每一個係數都是把第 2 篇你已經見過的前向方程式做代數反推後自然掉出來的。

用圖看反向鏈:從右邊的純雜訊出發,每一步把影像往左推向乾淨的樣本,把上面的更新式重複數百次。

一條由左到右的影像帶,從隨機雜訊開始,經過許多去雜訊步驟逐漸清晰成一張明確的圖片。

import torch

@torch.no_grad()
def ddpm_sample(model, shape, T, alpha, alpha_bar, sigma):
    # Start from pure Gaussian static x_T
    x = torch.randn(shape)
    # Walk DOWN the ladder: t = T, T-1, ..., 1
    for t in reversed(range(1, T + 1)):
        # Network's guess of the noise inside x at this noise level
        eps = model(x, t)
        # Weight that removes just this step's worth of noise
        coef = (1 - alpha[t]) / (1 - alpha_bar[t]).sqrt()
        # Step toward cleaner, then un-shrink by 1/sqrt(alpha_t)
        mean = (x - coef * eps) / alpha[t].sqrt()
        # Fresh randomness on every step EXCEPT the last (t == 1)
        z = torch.randn(shape) if t > 1 else torch.zeros(shape)
        x = mean + sigma[t] * z
    return x  # x_0: a finished image
完整的 DDPM 取樣迴圈:每一階呼叫一次網路,總共數百階,從雜訊走到圖片。

分數基礎觀點:朝真實影像攀升

接下來是把一切串起來的更深層想法——雖然我們會講得直覺,但它值得那枚大師級徽章。想像所有可能的影像都是一個巨大空間裡的點(對一張百萬像素的圖來說,就是百萬維的空間)。真實的照片不是散落各處的;它們聚集在一個薄薄的、有結構的區域——那片「真實影像居住的土地」。在這個空間的任何一點,我們都可以問:我該往哪個方向推這張影像,才能讓它像真實資料?這個在每一點都有定義的方向,就叫做分數(score)。把它想成插在每個位置的一支箭,所有箭都指向「上坡」、朝向那些可信影像所在的地方。

\nabla_{\mathbf{x}}\log p(\mathbf{x}_t) \;\approx\; -\,\frac{\epsilon_\theta(\mathbf{x}_t,t)}{\sqrt{1-\bar{\alpha}_t}}

關鍵的橋樑:雜訊預測器其實就是分數估計器,差別只在一個已知的縮放與一個變號。

讓我們仔細讀。左邊的 p(\mathbf{x}_t) 是第 t 等級帶雜訊影像的機率密度——在(帶雜訊的)真實影像聚居處很大,在不合理的區域則極小。符號 \nabla_{\mathbf{x}} 的意思是「對影像取梯度」,也就是指向最陡上升方向的那個向量。所以 \nabla_{\mathbf{x}}\log p(\mathbf{x}_t) 正是那支「上坡的箭」——在影像空間中,最快增加「像真實資料」之(對數)可能性的方向。這條式子說:這支箭在差一個已知常數的意義下,就是 U-Net 雜訊預測的負值。那個負號是核心:雜訊指向遠離乾淨資料的方向,所以要真實影像攀升,你就往預測雜訊的相反方向走。分母 \sqrt{1-\bar{\alpha}_t} 是到第 t 步為止所加總雜訊的標準差(直接來自第 2 篇的前向方程式),它把原始的雜訊猜測重新縮放成一個真正的梯度——在高雜訊時分母很大,所以同樣的雜訊猜測對應到比較平緩的坡度。

一旦你把分數看成指南針,反向去雜訊過程就化成一個乾淨的想法:在資料密度上做梯度上升。每一步把影像往上坡推一點點,朝向真實影像居住的地方,反覆進行,直到你抵達那片可信圖片的土地。確切地做這件事——反覆沿著分數邁步,再加上一點雜訊以免卡住——是一個經典配方,叫做 Langevin 動力學,也是分數基礎生成模型的引擎。重點是:DDPM(預測並扣除雜訊)和分數基礎模型(攀爬對數密度)並不是對手——它們是同一個過程的兩種描述,這也是為什麼這個領域把它們當成一回事。

用 DDIM 加速取樣

第 3 節的 DDPM 取樣器有個痛點:它通常用大約 1000 階,而每一階都是對 U-Net 的一次完整呼叫。那是每張影像約 1000 次大型網路的前向傳遞——對一個你希望一兩秒就出圖的互動工具來說,實在太慢。DDIM 取樣就是解法。它把反向過程重新表述為確定性的(完全丟掉隨機項 \sigma_t\,\mathbf{z}),而且關鍵在於它允許你跳過時間步——可能用 20 到 50 步取代 1000 步,而品質損失少得驚人。

關鍵想法,用白話說:在每一步,DDIM 不只是把影像往乾淨推一小格,而是先用雜訊預測一口氣到對完全乾淨影像的猜測 \hat{\mathbf{x}}_0,再把這個猜測重新加噪到我們選定要造訪的下一個時間步。下面就是這個乾淨影像的猜測——注意它其實只是把第 2 篇的前向方程式解出乾淨影像而已:

\hat{\mathbf{x}}_0 = \frac{\mathbf{x}_t - \sqrt{1-\bar{\alpha}_t}\,\epsilon_\theta(\mathbf{x}_t,t)}{\sqrt{\bar{\alpha}_t}}

網路目前對乾淨影像的最佳猜測:把預測雜訊從 x_t 中剝掉後還原出來。

怎麼讀:第 2 篇的前向規則說,帶雜訊影像是 \mathbf{x}_t = \sqrt{\bar{\alpha}_t}\,\mathbf{x}_0 + \sqrt{1-\bar{\alpha}_t}\,\epsilon——訊號被 \sqrt{\bar{\alpha}_t} 縮小,加上被 \sqrt{1-\bar{\alpha}_t} 縮放的雜訊。我們不知道真正的雜訊 \epsilon,但 U-Net 給了我們它的估計 \epsilon_\theta(\mathbf{x}_t,t)。於是我們把這份估計的雜訊從 \mathbf{x}_t 扣掉,再除以 \sqrt{\bar{\alpha}_t} 來抵銷縮小——\hat{\mathbf{x}}_0 就跳出來了,那是網路從我們此刻所站位置看出去對成品圖片的最佳猜測。一開始這個猜測粗糙、像夢境;到接近尾聲時就變得銳利。做事的還是同一個去雜訊器,只是被重新排列成指向終點,而不是指向下一階。

\mathbf{x}_{t-1} = \sqrt{\bar{\alpha}_{t-1}}\,\hat{\mathbf{x}}_0 \;+\; \sqrt{1-\bar{\alpha}_{t-1}}\,\epsilon_\theta(\mathbf{x}_t,t)

DDIM 的確定性步驟:拿乾淨影像的猜測,精確地重新加噪到下一個選定的時間步——沒有隨機項。

這個更新式的形狀美得很簡單——它又是同一條前向公式:拿乾淨影像的猜測 \hat{\mathbf{x}}_0,用 \sqrt{\bar{\alpha}_{t-1}} 縮小,再加回被 \sqrt{1-\bar{\alpha}_{t-1}} 縮放的雜訊——只不過用的是同一個預測雜訊方向 \epsilon_\theta(\mathbf{x}_t,t),而不是全新的隨機雜訊。因為沒有隨機項 \mathbf{z},這條路是一條從雜訊到影像的平滑、確定性曲線,而我們可以落在任何選定的時間步 t-1,不必只走到緊鄰的下一階。這正是我們能跳步的原因:終點 \hat{\mathbf{x}}_0 在每一步都重新估計,所以就算大步一跳(比方 1000 → 950 → … 每次跳 50),也仍停在一條連貫的軌跡上。從 1000 步降到 20 步就是 50 倍的加速——這是「等半分鐘」和「現在就拿到圖」之間的差別。