U1.5 10 分鐘閱讀 2026年9月

U1.5 實作:建立這個單元的 Toy

本篇重用L2.0民調只問一千人:小批次與噪聲·L2.1一個 η 不夠用:動量與 Adam 各買到什麼·L4.0什麼都能擬合,那為什麼還需要別的架構?·L4.5同一台機器要依指令做不同的事,指令從哪裡餵進去?·L0.3霧中下山,一步該走多大:梯度下降的一頁·L1.0背考古題的學生:過擬合與欠擬合·L1.2老師用考古題教學又出題:為什麼要切資料·L5.3一個生成模型好不好,該用哪一個數字回答?·M2.3導航每 30 秒更新一次:Euler 法與它的誤差

先在淺水區把每件事看清楚

這個單元的理論講完了,但還沒有一行程式跑過。接下來要做的,是把整個單元的東西縮成一個幾秒就能重跑的小實驗。

之後三個單元的理論都要對回同一組實驗,所以這個 toy 值得認真建。選 2D 有三個很實際的好處:

  • 有標準答案。 資料點是有限的,所以 ptp_t 就是一個混合 Gaussian——真實的 ptp_t、score、後驗均值全部可以精確算出來。網路學得好不好不用猜,可以直接比。
  • 看得見。 軌跡、向量場都能直接畫在平面上,不必靠指標想像。
  • 跑得快。 訓練幾秒鐘結束,改一行就能重跑;想試一個念頭不用等半天。

設定:資料是兩個月牙(sklearn.datasets.make_moonsn=2000n=2000,噪聲 0.05),標準化到零均值單位變異數。網路是三層 MLP、寬 128,輸入 (xt,t)(x_t,t)tt 用 sinusoidal embedding。T=1000T=1000,linear schedule β1=104βT=0.02\beta_1=10^{-4}\to\beta_T=0.02

步驟 1:forward 與訓練

def q_sample(x0, t, eps):                 # forward process 的 closed form
    ab = alpha_bar[t][:, None]
    return ab.sqrt() * x0 + (1 - ab).sqrt() * eps

for step in range(n_steps):               # denoising 回歸的五行
    x0  = sample_data(batch)
    t   = torch.randint(1, T + 1, (batch,))
    eps = torch.randn_like(x0)
    xt  = q_sample(x0, t, eps)
    loss = ((model(xt, t) - eps) ** 2).mean()
    loss.backward(); opt.step(); opt.zero_grad()

跑起來之後,loss 會在幾百步內從 ~1 掉到 0.3 附近,然後就停在那裡不動了

loss 停住、而且離 0 很遠,
是哪裡寫錯了嗎?

課堂提問Q1

一般的回歸任務,我們會期待 loss 一路往 0 掉。所以看到它停在 0.3,直覺的下一步是:把網路加寬、學習率調一調、多訓十倍步數。

這樣做 loss 會掉下去嗎?

先想一想,再展開看整理後的答案

不會,而且一點都不會——那個 0.3 不是訓練不夠,是理論算得出來的地板。

U1.3 已經把這條寫出來了:ϵθ\epsilon_\theta 的最佳解是 E[ϵxt]\mathbb E[\epsilon\mid x_t],不是訓練時抽到的那一個 ϵ\epsilon,所以即使訓到最佳,loss 也只會停在條件變異數上:

minθL=Et,xt[trVar(ϵxt)].\min_\theta \mathcal L=\mathbb E_{t,x_t}\Big[\operatorname{tr}\operatorname{Var}\big(\epsilon\mid x_t\big)\Big].

加寬網路只會讓你更快走到這條地板,不會穿過它。而且這個下界tt,兩端剛好相反:

  • tt 很小:xtx_t 幾乎就是 x0x_0,所以看到 xtx_t 就幾乎知道 ϵ\epsilon 是多少——條件變異數接近 0,loss 下界接近 0。
  • tt 很大:xtx_t 幾乎不含 x0x_0 的資訊,ϵ\epsilon 給定 xtx_t 之後幾乎還是 N(0,I)\mathcal N(0,I)——loss 下界接近 1。

把所有 tt 平均起來,就是那個停住的非零數字。

該看的不是 loss 停在多少,而是它隨 tt 長什麼形狀。

實作建議: 把 loss 依 tt 分箱畫出來(例如每 100 個 tt 一箱),你會看到一條從 ~0 爬到 ~1 的曲線。這條曲線比單一個平均 loss 有用得多——它讓你看得出模型是在哪一段 tt 沒學好。

示意圖:折線圖,橫軸 t 從 0 到 1000、縱軸 loss 從 0 到 1.1;橘色實線是理論下界(由接近 0 爬到接近 1),藍色散點是依 t 分箱的實際訓練 loss,水平虛線標「所有 t 平均起來 ≈ 0.3」;左右各一個小插圖,t 小時 x_t 幾乎等於原圖、t 大時 x_t 幾乎是純噪聲。

圖:loss 的下界隨 tt 走。 橘線是理論下界(tt 小趨近 0、tt 大趨近 1),藍點是依 tt 分箱的實測值。虛線那個 0.3\approx 0.3 就是你在 log 裡看到「停住」的那個數字——它是所有 tt 平均出來的,不是模型學不動。

步驟 2:畫 score field(圖 a)

[3,3]2[-3,3]^2 的網格上,對 t{50,300,700,950}t\in\lbrace 50,300,700,950 \rbrace 各畫一張,每張疊兩組箭頭:

  • 模型:ϵθ(x,t)/σt-\epsilon_\theta(x,t)/\sigma_t
  • 真值:exact_score(x, t)

會看到的事情:tt 大時箭頭整片指向原點,tt 小時箭頭指向月牙——跟 U1.3 的 demo 一樣。而模型與真值在資料密集的地方吻合,在遠離資料的角落偏差變大,因為那裡幾乎沒有訓練樣本落過。

這個偏差不是無害的,步驟 4 會看到它的後果。

步驟 3:三種 parametrization 真的等價嗎?(圖 b)

不重訓。取一批 (xt,t)(x_t,t),只從 ϵθ\epsilon_\theta 換算:

x^0=xtσtϵθαˉt,v^=αˉtϵθσtx^0,\hat x_0=\frac{x_t-\sigma_t\epsilon_\theta}{\sqrt{\bar\alpha_t}}, \qquad \hat v=\sqrt{\bar\alpha_t}\,\epsilon_\theta-\sigma_t\,\hat x_0 ,

先做一件一行就能做完的事:從 v^\hat vxtx_t 反解回 ϵ^\hat\epsilon,確認它和原本的 ϵθ\epsilon_\theta 只差在浮點誤差(106\lesssim10^{-6})之內。三種寫法真的是同一個東西,這一步就是 U1.2 說的「親手驗證一次」。

驗完之後再拿 x^0\hat x_0exact_posterior_mean 比對,畫出「x^0\hat x_0 的誤差對 tt」的曲線。

會看到的事情:tt 大時誤差被放大——因為換算要除以趨近 0 的 αˉt\sqrt{\bar\alpha_t}U1.2 說三種 parametrization 在數學上等價,這條曲線就是它們在數值上不等價的地方;而 vv 之所以在兩端都不退化,正是因為它不需要做這種除以趨近 0 的係數的換算。

步驟 4:DDPM 與 DDIM 軌跡(圖 c)

def ddim_step(xt, t, s, eta=0.0):
    eps = model(xt, t)
    x0_hat = (xt - sig[t] * eps) / ab[t].sqrt()
    sigma_s = eta * ((1 - ab[s]) / (1 - ab[t]) * (1 - ab[t] / ab[s])).sqrt()
    dir_xt = (1 - ab[s] - sigma_s**2).sqrt() * eps
    return ab[s].sqrt() * x0_hat + dir_xt + sigma_s * torch.randn_like(xt)

η=0\eta=0 是 DDIM,η=1\eta=1 是 DDPM——一支函數就夠,這正是 U1.4 那條一般式。固定 32 個起點 xTx_T,對步數 {10,50,1000}\lbrace 10,50,1000 \rbrace 各跑一次,把完整軌跡畫出來。

會看到的事情:DDIM 10 步的軌跡是彎的,而且終點常常落在月牙外側;1000 步時終點幾乎收在同一組位置,但路徑仍然是彎的。這張圖要留好,U2 開頭會直接用它。

補充這裡和課文 demo 的差別:現在的 score 有誤差了

U1.4 的互動 demo 用的是精確的 score,所以看不到「網路不準」造成的效果。這裡你手上是一個真的訓出來的網路——它在低密度區會不準(步驟 2 看到的),於是取樣時如果軌跡跑到那些地方,誤差會被進一步放大。

這也是為什麼作業 3 值得做:帶噪聲的取樣器(η>0\eta \gt 0)有機會把偏離的樣本拉回當下的 ptp_t,而這件事只有在 score 有誤差時才看得出好處。

用精確的 score 看不到加噪聲的好處——那個好處是拿來對付網路誤差的。

作業

  1. Parametrization 等價。 分別以 x0x_0-、ϵ\epsilon-、vv-prediction 訓練三個網路,換算成 ϵ\epsilon 之後比較 DDIM 50 步終點的 W2W_2(Wasserstein-2 距離,兩堆樣本之間的距離)。它們應該接近但不相同——說明差異來自 U1.2 的哪一節。
  2. 加權。ϵ\epsilon-prediction 訓練,但把 loss 乘上 1/SNR(t)1/\mathrm{SNR}(t)。比較 score field 在 tt 小與 tt 大時的誤差分布,跟均勻加權的版本有什麼不同。
  3. U1.4 的判準做成定量版。 對步數 {5,10,20,50,100,1000}\lbrace 5,10,20,50,100,1000 \rbrace,畫 DDPM(η=1\eta=1)與 DDIM(η=0\eta=0)終點的 W2W_2 曲線。先寫下你的猜測再跑。 這個 toy 的 score 是精確算出來的,所以照 U1.4 的判準,你應該預期確定性那一端不太會輸;如果你量到相反的結果,先去檢查是不是 score 不夠精確。
  4. (選)Anderson 反向 SDE。 讀 Anderson 1982 [3] 第 2 節,說明 reverse SDE 的 score 項是從哪一個條件期望冒出來的,並指出它與 U1.3 的 Tweedie 公式是什麼關係。

消化一下

想一想

訓練 loss 的下界隨 tt 怎麼變?

想一想

步驟 2 發現模型 score 在遠離資料的角落偏差很大。這件事會在哪裡造成問題?

想一想

為什麼同一支 ddim_step 函數可以同時當 DDPM 與 DDIM 用?

參考文獻

  1. Ho, J., Jain, A., Abbeel, P. Denoising Diffusion Probabilistic Models. NeurIPS 2020.
  2. Song, J., Meng, C., Ermon, S. Denoising Diffusion Implicit Models. ICLR 2021. (步驟 4 的 ddim_step 一般式。)
  3. Anderson, B. D. O. Reverse-time diffusion equation models. Stochastic Processes and their Applications, 1982. (作業 4。)