U2.5 實作:同一個 toy,只換訓練目標
本篇重用M2.3導航每 30 秒更新一次:Euler 法與它的誤差
設定:只動兩行
沿用 U1.5 的 toy.py——同一組月牙資料、同一個 MLP、同一組 random seed。唯一的改動是訓練目標與取樣器,這樣圖上的差異才歸因得回框架,而不是「這次剛好訓得比較好」。
先確認符號沒有搞反:這個單元 x0 是噪聲、x1 是資料、t 從 0 走到 1,跟 U1 的 notebook 剛好對調。
步驟 1:訓練
for step in range(n_steps):
x1 = sample_data(batch) # 資料
x0 = torch.randn_like(x1) # 噪聲
t = torch.rand(batch, 1)
xt = t * x1 + (1 - t) * x0
loss = ((model(xt, t) - (x1 - x0)) ** 2).mean()
loss.backward(); opt.step(); opt.zero_grad()
和 U1.5 的訓練迴圈逐行對照,真的只有 xt 與目標那兩行不同。
換框架在程式裡只是換兩行;換掉的是那兩行背後的設計自由度。
會看到的事情是:loss 一樣不會掉到 0,而且停住的數字比上一個單元大。這不是壞事——U2.2 說過 比 多一個常數,就是條件速度繞著邊際速度的變異數。真正要看的是它停在哪裡不動之後還會不會慢慢往下。
步驟 2:取樣與軌跡圖(圖 a)
def euler_sample(model, x0, n_steps):
x, h, traj = x0, 1.0 / n_steps, [x0]
for i in range(n_steps):
t = torch.full((x.shape[0], 1), i * h)
x = x + h * model(x, t)
traj.append(x)
return torch.stack(traj)
固定和 U1.5 相同的 32 個起點(注意 FM 的起點是 的噪聲,對應 DDPM 的 ),步數取 ,把上一個單元的 DDIM 軌跡和這裡的 FM 軌跡並排。
會看到的事情是:兩邊的軌跡形狀明顯不同,而且在兩個月牙中間那塊區域都特別彎——那裡正是條件直線交叉最密的地方(U2.3 Q1)。
那到底哪一條路徑的軌跡比較好走?
這件事能不能量出來,而不是看圖猜?
步驟 3:步數對誤差(圖 b)
對步數 各取 2000 個樣本,算終點與資料的 (用 POT 的精確 OT),DDIM 與 FM 各一條線,畫在 log-log 座標上。
這張圖有兩個獨立的訊息:
- 斜率=數值方法的階數。Euler 是一階,斜率應該接近 ;換成 Heun(二階)會變成接近 。
- 高度=軌跡有多彎。同樣是 Euler,兩條線平行但高低不同,高的那條就是彎的那條。
先寫下你的猜測(哪條高?),再跑出來對。
互動 demo:步數對誤差的 log-log 圖。 這是圖 b 的「標準答案版」——速度場用資料精確算,所以看到的完全是離散化誤差,沒有網路的逼近誤差混進來。切「比方法」量到 Euler 、Heun ;切「比路徑」兩條線斜率一樣,但線性路徑高出 VP 兩到三倍。demo 只積到 :最後那一小段 太陡,會把階數的訊號蓋掉。
補充為什麼要用中位數,而不是平均
少數幾條軌跡會在兩個 mode 之間的分岔點上「選錯邊」——步數少的時候走去左邊的月牙,步數多的時候走去右邊。這種樣本的誤差是 ,跟步數幾乎無關,平均起來會把整條曲線壓平,斜率就讀不出來了。
量收斂階數之前,先確認你量的是離散化誤差,不是分岔選錯邊。
取中位數就避開這件事。這不是把不好看的資料藏起來:分岔造成的誤差確實存在,但它和「離散化誤差隨步數怎麼縮」是兩個不同的現象,要分開量。跑自己的實驗時,值得把兩者都畫出來。
步驟 4:不重訓的轉換(圖 c)
載入 U1 的 。它是在 VP 路徑上訓的,所以轉換時要用 VP 的 ,其中 是把 FM 的 映回 DDPM 步數的那個對應。注意它是反向的:FM 的 是資料那一端,對到的是 DDPM 的 ();FM 的 是噪聲端,對到 。寫 to_ddpm_step 的時候先把這個邊界條件對一次,方向弄反了整張圖都會是錯的。
def eps_to_velocity(eps_model, x, t):
a, s, da, ds = vp_alpha(t), vp_sigma(t), vp_alpha_dot(t), vp_sigma_dot(t)
eps = eps_model(x, to_ddpm_step(t))
return (da / a) * (x - s * eps) + ds * eps
拿 euler_sample 去解這個速度場,和 DDIM 50 步的終點比對。會看到的事情是:兩邊幾乎重合,剩下的差來自兩個取樣器的時間格點對不齊,不是模型不同。這張圖就是 U2.4「同一個物件、兩套座標」的數值實證;那一篇的那個動手推導已經把代數那一半做完了——代 VP 進轉換式,兩個係數都化成 ,正好是 PF-ODE。
課堂提問Q1
把步驟 4 跑出來之後,很自然會想:既然 轉一下就能當速度場,那 U1 那個模型是不是也可以直接拿去用線性路徑的 FM 取樣器取樣?
先想一想,再展開看整理後的答案
不行,而且這個坑很容易踩。
學到的是 ,其中 ——它只在VP 路徑的那一族中間分布上見過資料。線性路徑的 給出的是不同的 :同樣的 ,兩邊的訊噪比不一樣,點的分布也不一樣。
具體一點:VP 在 上走,所以 大致固定;線性路徑的 ,在中間會明顯縮小。把線性路徑的 餵進 VP 訓的網路,等於問它一個沒見過的分布上的問題,答案沒有理由是對的。
所以步驟 4 的轉換式裡, 必須是模型當初訓練時用的那一條路徑:「不用重訓」只換得了取樣器,換不了路徑。路徑是訓練時就寫進模型裡的東西。
這也順便解釋了 U2.4 為什麼要把四個旋鈕分開講:能事後換的(取樣器、座標)和不能事後換的(起點、路徑、配對、加權)不是同一類事情。
作業
- 非 Gaussian 起點。 把
x0換成 上的均勻分布,重訓 FM,畫軌跡。說明 U1 的框架為什麼做不到這件事——對應 U2.0 Q1 的哪一個旋鈕? - 加權對照。 訓兩個線性 FM 模型:一個均勻抽 ,一個從 logit-normal 抽 。比較圖 b 的兩條曲線:斜率變了還是高度變了?先猜再跑,U3.5 會給答案。
- 路徑對照(把一個軸單獨隔離出來)。 保持獨立配對、保持 FM 目標,只把 換成 VP 的 重訓。它的軌跡比線性 FM 直還是彎?和上面 demo 量到的方向一致嗎?如果不一致,先懷疑「網路有沒有訓好」而不是「理論錯了」。
- 分岔樣本。 把步驟 3 的誤差改成平均而不是中位數,看曲線怎麼變形;再把「終點跑到另一個月牙」的樣本單獨挑出來畫。你會得到兩條性質完全不同的曲線。
- (選)Likelihood。 用 Hutchinson 估計散度、沿 ODE 積分算測試點的 ,和月牙混合 Gaussian 的精確 log-density 比對。
消化一下
參考文獻
- Lipman, Y., Chen, R. T. Q., Ben-Hamu, H., Nickel, M., Le, M. Flow Matching for Generative Modeling. ICLR 2023. (步驟 1 的訓練目標。)
- Karras, T., Aittala, M., Aila, T., Laine, S. Elucidating the Design Space of Diffusion-Based Generative Models. NeurIPS 2022. (把路徑、加權、取樣器當成可分開調的設計軸,並且是二階取樣器的標準參考。)
- Esser, P., et al. Scaling Rectified Flow Transformers for High-Resolution Image Synthesis. ICML 2024. (作業 2 的 logit-normal 時間取樣出自這裡。)