L4.5 16 分鐘閱讀 2026年9月

L4.5 同一台機器要依指令做不同的事,指令從哪裡餵進去?

本篇重用M0.2導航的三十秒:Taylor 展開與「假設直線」·M1.1從鞋子猜身高:Conditional Expectation

起點:同一台機器,不同的指令

到目前為止每一個模型都只做一件事:給 xx,回答 yy

但實務上更常見的是一台可以被指揮的機器:同一個模型,依照一個額外的指令做不同的事。

  • 「把這張圖修成夜景」——條件是一個類別。
  • 「照這段文字生成」——條件是一串 token。
  • 「現在是流程的第 300 步,請做這一步該做的事」——條件是一個純量。

最後一種特別值得注意,因為它不是使用者給的指令,而是演算法自己的狀態。同一組權重要在很多個不同的階段各做一件不同的事,於是「現在是第幾步」必須被餵進去。

接法看起來很自由:接在輸入、接在中間某一層、或讓網路自己去查。但這幾種接法不是同一件事的幾種寫法——它們對不同形狀的條件有非常不同的成本。

指令要從哪裡餵進去?
接在輸入的一個維度上就好了嗎?
如果那個指令是一個純量,會不會特別吃虧?

課堂提問Q1

把「依指令做不同的事」翻成數學:條件模型和無條件模型差在哪裡?把一個純量條件接成輸入的第二個維度,網路必須自己長出什麼東西——那件事對它來說容易嗎?

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

寫下來。 無條件模型學的是一個函數 fθ(x)f_\theta(x);條件模型學的是一族函數,用 cc 索引:

fθ(x,c)  E[YX=x,C=c].f_\theta(x,c)\ \approx\ \mathbb E[Y\mid X=x,\,C=c].

從鞋子猜身高:Conditional Expectation 的語言:多了一個條件,桶子被切得更細了。理想上這讓每個桶子裡的散佈更小(預測更準);代價是同一組參數要同時服務所有的 cc

接成輸入維度之後,網路要自己長出什麼。ffcc 的依賴是高頻的——例如流程的第 3 步和第 4 步該做的事差很多——那麼把 cc 當成一個普通的輸入座標,就是要求網路在那個座標上表現出高頻的變化。

什麼都能擬合,那為什麼還需要別的架構? 的頻率實驗量過這件事:同一個 64×6464\times64 的網路,目標從 1 個週期換到 32 個週期,訓練 MSE 從 0.00000.0000 變成 0.38750.3875MLP 對高頻很不友善,而這個不友善不分輸入座標——xx 這一維吃虧,cc 這一維一樣吃虧。

兩條出路,方向不同。

  • 把頻率直接給它:不要接 cc,接 (sin(2πfjc),cos(2πfjc))j\big(\sin(2\pi f_j c),\cos(2\pi f_j c)\big)_j 這一組不同頻率的特徵。網路要高頻時,它已經在輸入裡了,只需要線性組合。
  • 不要讓 cc 走同一條路:讓它去改變每一層的行為(尺度與偏移),而不是當成一個和 xx 平起平坐的座標。

本篇會把第一條量出來(嵌入比加寬便宜也有效),並說清楚第二條在什麼形狀的條件上才划算。

三種注入方式,三種「改變什麼」

把接法按照「它改變了網路的哪一部分」排開:

  • concat(接在輸入)f([x,c])f([x,c])。最簡單,而且在條件和輸入是同一種東西時很自然(例如都是影像的像素)。缺點是條件的影響必須一路經過所有層才擴散開,而且它和 xx 共用同一條路。
  • FiLM(每一層的尺度與偏移):由 cc 算出一組 (γ,β)(\gamma_\ell,\beta_\ell),在第 \ell 層做 hγh+βh\leftarrow\gamma_\ell\odot h+\beta_\ell。條件不再是一個座標,而是直接調整每一層的行為。這對「全域性、影響整張圖」的條件特別合適——類別、時間步、風格。
  • cross-attention(查表):條件是一串長度可變的東西(一段文字、一組 token),讓每個位置自己去問「我該看這串裡的哪幾個」。它處理的是 concat 與 FiLM 都處理不了的形狀:條件的長度不固定,而且不同位置需要看不同的部分

三者不互斥,實務上常常同時出現(時間用 FiLM、文字用 cross-attention、低階的輔助輸入用 concat)。挑的依據是條件的形狀

條件長什麼樣適合的注入方式
一個純量(時間、強度)嵌入 → FiLM(或 concat 嵌入)
一個類別(有限選項)查一個 embedding 表 → FiLM
一張同尺寸的圖(遮罩、邊緣圖)concat 在通道上
一串長度可變的 tokencross-attention

為什麼純量條件要先做嵌入

正弦嵌入把一個純量 c[0,1]c\in[0,1] 換成一組不同頻率的三角函數:

emb(c)=(sin(2πfjc), cos(2πfjc))j=0J1,fj=2j.\mathrm{emb}(c)=\big(\sin(2\pi f_jc),\ \cos(2\pi f_jc)\big)_{j=0}^{J-1}, \qquad f_j=2^{\,j}.

它在做的事可以用一句話說清楚:把「網路要自己長出高頻」換成「高頻已經在輸入裡,只需要把它們線性組合起來」。

為什麼這會有效,可以從 導航的三十秒:Taylor 展開與「假設直線」 的角度看。一個網路對輸入的局部行為,是由它在那一點的一階(與高階)變化決定的;要在 [0,1][0,1] 上做出 kk 個週期,第一層的權重必須大到 O(k)O(k),而那會同時把激發函數推進飽和區(同一張地圖從不同地方出發:損失曲面與初始化 的 gain 掃描量過:太大的尺度讓訓練變差且不穩)。嵌入把這個張力拆開——頻率由固定的、不需要學的 fjf_j 提供,網路只負責挑哪幾個、各要多少。

fj=2jf_j=2^j 這個幾何級數也不是隨便挑的:它讓不同尺度的變化都被覆蓋到,而且只要 JJ 個維度就涵蓋 2J2^{J} 倍的頻率範圍。

補充這和 Transformer 的位置編碼是同一個東西

Transformer 的位置編碼用的是同一條式子(sin/cos\sin/\cos 配上幾何級數的頻率),解決的也是同一個問題:位置是一個純量,而網路需要對它有多尺度的敏感度。差別只在那裡的純量是「第幾個 token」,這裡是「第幾步」或「條件的強度」。

同一個技巧在座標型網路(用網路表示一張圖或一個 3D 場景)上叫 Fourier features(參考文獻 2),那篇論文把「為什麼沒有嵌入就學不到高頻」用 neural tangent kernel 講清楚了:沒有嵌入時,網路的等效核對高頻成分的響應以指數衰減,於是那些成分在任何合理的訓練時間內都學不到。

三個名字、三個領域、同一件事——值得把它記成一個通用的手法:當一個純量需要高頻的影響力時,先把它展開成多個頻率。

回到情境:三種接法在同一個任務上

任務刻意設計成「對條件的依賴是高頻的」:

y=sin(2πx+2π8t),x,tU(0,1), n=800.y=\sin\big(2\pi x+2\pi\cdot 8\cdot t\big),\qquad x,t\sim U(0,1),\ n=800 .

xx 的依賴是 1 個週期(很容易),對 tt 的依賴是 8 個週期(照 什麼都能擬合,那為什麼還需要別的架構? 的頻率表,這是要花點力氣的區域)。三個設定跑同樣的 6000 步、同樣的 Adam η=0.01\eta=0.01

  concat 純量 t            輸入維度   2  h= 64  訓練 MSE 0.0018
  concat 正弦嵌入 t         輸入維度  13  h= 64  訓練 MSE 0.0004
  concat 純量 t(寬兩倍)    輸入維度   2  h=128  訓練 MSE 0.0010

嵌入把誤差降到四分之一(0.00180.00040.0018\to0.0004),而它只多了 11 個輸入維度。 對照組更有說服力:把網路加寬兩倍(參數變成約四倍)只從 0.00180.0018 改善到 0.00100.0010——四倍的參數買到的,還不如 11 個不需要學的輸入維度。

嵌入不是在增加容量,是在把網路不擅長的那件事移出去。

這也解釋了為什麼幾乎每一個需要「時間步」的模型都有一個正弦嵌入層,而且那一層通常沒有參數:它不是模型的一部分,是輸入的一種座標選擇——和 霧中下山,一步該走多大:梯度下降的一頁 裡「標準化改的是碗的形狀」是同一類的動作。

回到情境:「沒有條件」也必須被訓練過

條件模型有一個很容易被忽略的需求:有時候你要它不要依照條件做事。

  • 想知道模型在「什麼都不指定」時的預設行為。
  • 想比較「有條件」與「無條件」的輸出差在哪裡(很多引導式的取樣做法都需要這兩個)。
  • 使用者就是沒有給條件。

直覺的做法是留一個 null token:一個代表「沒有條件」的特殊輸入(例如把 one-hot 全部設成零)。問題是——如果訓練時條件永遠都在,那個 null token 從來沒有出現在訓練資料裡。

把它量出來。任務是 y=sin(2πx)+0.8cy=\sin(2\pi x)+0.8cc{0,1}c\in\{0,1\},條件用 one-hot 接進輸入;「沒有條件」就是把 one-hot 設成全零。真正的無條件答案是兩類的平均 sin(2πx)+0.4\sin(2\pi x)+0.4。訓練時用機率 pp 把條件隨機丟掉(換成全零):

def feats(x, c, mask):                      # mask=0 表示這一筆的條件被丟掉
    oh = np.zeros((len(x), 2)); oh[np.arange(len(x)), c] = 1.0
    return np.c_[x, oh*mask[:,None]]

mask = (rng.random(N) > p).astype(float)    # p 是條件 dropout 的機率
  條件 dropout p=0.0   用 null token 預測「兩類平均」的 MSE 中位數 1.3908
  條件 dropout p=0.1   用 null token 預測「兩類平均」的 MSE 中位數 0.0660
  條件 dropout p=0.3   用 null token 預測「兩類平均」的 MSE 中位數 0.0225
  (對照:直接輸出 0 的 MSE = 0.6575)

p=0p=0 那一列比「什麼都不算、直接輸出零」還差兩倍多1.391.390.660.66)。這不是「精度差一點」——是模型在一個從未見過的輸入上任意外推,而外推的方向沒有任何理由是對的(這正是 台北的房仲去台中:風險與經驗風險 那個 81138113 的小型版本:訓練分佈之外沒有定義)。

只要 10%10\% 的條件 dropout,誤差就掉到 0.0660.066——二十一倍。 p=0.3p=0.3 再降到 0.02250.0225,但代價是三成的訓練樣本沒有用到條件資訊,條件本身的品質會開始下降。實務上 10%10\% 左右是常見的折衷。

「沒有條件」不是一個預設狀態,它是一個要被訓練的輸入。

刻意違反:把 null token 換成一個「沒用過的」向量。 如果不是用全零,而是隨便挑一個訓練時沒出現過的向量(例如 one-hot 的第三格),結果和 p=0p=0 那一列一樣糟——問題不在「用哪個向量代表沒有條件」,而在「那個向量有沒有被訓練過」。 這也是為什麼實務上一律用「訓練時真的餵過的那個」當 null,而不是任何看起來比較中性的東西。

這一切依賴什麼。 一,上面的數字是在一個純量/類別條件、小網路上量的;條件是長序列時 dropout 的做法不同(丟整串、還是丟其中幾個 token,會影響模型學到的東西)。二,pp 是一個要調的超參數,而它同時影響「無條件有多準」與「有條件有多準」——兩者用同一組權重。三,這裡量的是「無條件輸出對不對」,不是「有條件輸出有沒有變差」;完整的評估要兩個都看。

三種接法、一個頻率旋鈕、一個 dropout 旋鈕

互動 demo:把條件頻率 k 拉大,「純量」那條訓練曲線整條抬高而「嵌入」那條幾乎不動。

先消化一下

想一想

一個模型需要知道「現在是流程的第幾步」,團隊把步數除以總步數變成一個 [0,1][0,1] 的純量接在輸入上,發現模型在相鄰步數之間的行為幾乎一樣。最直接的處方是:

想一想

一個條件模型在訓練時每一筆資料都有條件。上線後有人問「不給條件時模型會輸出什麼」,團隊把條件向量設成全零去問。根據本篇的實測,最可能的結果是:

想一想

關於 FiLM(用條件算出每一層的 γ,β\gamma_\ell,\beta_\ell)與 concat,下列哪一個判斷比較合理?

想一想

下列哪一句不對

參考文獻

  1. Perez, E. et al. FiLM: Visual Reasoning with a General Conditioning Layer. AAAI 2018.(把「條件改變每一層的尺度與偏移」抽象成一個通用層,並比較它與 concat 的差別。)
  2. Tancik, M. et al. Fourier Features Let Networks Learn High Frequency Functions in Low Dimensional Domains. NeurIPS 2020.(用 NTK 說明沒有嵌入時高頻為什麼學不到——本篇 Remark 的來源。)
  3. Ho, J., Salimans, T. Classifier-Free Diffusion Guidance. 2022.(條件 dropout 與 null token 的標準做法;本篇最後一段實驗的實務對應。)