§01學習重點
- 說出「沒有標籤」與「沒有訓練訊號」不是同一件事,並指出非監督式的損失是拿什麼來打分的
- 用「模型交到你手上的是什麼」把這一類任務分成五種,並說出它們描述的其實是同一個對象
- 解釋機率密度為什麼可以大於一,以及「密度積分等於一」這條約束會逼出什麼後果
- 親手跑出「訓練集概似漂亮、測試集概似崩掉」的對比,說出概似為什麼只能在沒看過的資料上算
- 分辨潛在空間的「平滑」與「可解釋」,並各舉一個只滿足其中一邊的情況
- 用十來行 numpy 證明潛在座標旋轉之後重建誤差不變,並據此說明表示為什麼難以評價
- 說出品質與涵蓋度是兩個會各自失敗的方向,並用兩個覆蓋比例把兩種相反的失敗分開
- 列出三類評價取徑各自量不到什麼,並說明實務上為什麼不會只看一個數字
§02課程內容
一、標籤沒了,訓練訊號還剩什麼
第 1 章按「回饋訊號從哪來」把學習分成三種,非監督式那一格的定義是「沒有外部給的答案」。那是一句定義,不是一套做法。這一章要把它變成做法。
先處理幾乎每個人都會卡的第一關:沒有答案,損失函數怎麼寫?監督式學習的損失長成「預測與答案的差距」,答案一抽掉,整條式子就沒東西可以相減。
但垮掉的只是那一種寫法。損失函數需要的東西其實只有一樣:一個能對每組參數打分、而且算得出來的數。答案是打分的一種依據,不是唯一的依據。抽掉答案之後還剩下的東西是資料本身——哪些量測值常常一起出現、哪些組合從來沒被觀察到、整批數字擠在什麼形狀的區域裡。這些不需要任何人來標,它們就在資料裡。非監督式學習的損失,量的是「模型對這批資料的描述有多貼」,而不是「模型的預測跟答案差多少」。
把它放進一個具體的處境。一支考古隊在遺址挖出好幾千片陶片,沒有任何一片附著說明書寫著它是某某型罐的哪個部位——那種判定要靠專家逐片做,而專家的時間比碎片少得多。你手上有的只是每一片的量測值:唇部的厚度、斷面的弧率、每公分幾條紋飾、胎土的顆粒粗細⋯⋯假設每片量六個數。這批資料一份答案卷都沒有,可是它不是一堆亂數:厚的碎片往往紋飾疏、薄的往往紋飾密,而且厚度不是連續攤平的,它明顯集中在幾個值附近。這些規律就是可以學的東西。
學到之後要它交出什麼?這一類任務彼此差得很遠,照演算法去分很難分乾淨,換一個軸卻立刻整齊:看模型最後交到你手上的是什麼東西。
交出一個數,說這一筆在這批資料裡有多典型,這是密度估計(density estimation)。交出一組低維座標,把六個量測壓成兩三個數,這是表示學習(representation learning)。交出一個群編號,把碎片歸到幾個堆裡,這是聚類(clustering)。交出一筆新資料,合成一片訓練集裡沒有、卻像是本址出土的碎片,這是生成。交出一個是非,判斷這一片是不是外來混進來的,這是離群偵測(outlier detection)。
其中有兩類特別容易被混為一談:表示學習與聚類。差別很硬——聚類交出的是編號,編號之間沒有「差 0.3」這種事,第 2 群不會比第 1 群「多一點什麼」;表示交出的是座標,兩片碎片可以在同一條軸上相差 0.3,而且中間那些沒出現過的值也對應到合理的東西。一個是離散的歸屬,一個是連續的位置。
五種任務看起來各做各的,其實都在描述同一個對象:這批資料的分布(distribution)。有多典型,問的是分布在那一點的高低;低維座標,問的是分布集中在什麼形狀上;分成幾群,問的是分布有幾個隆起;合成新樣本,問的是能不能從分布裡再抽一個;判定外來,問的是分布在那一點是不是幾乎沒有東西。這一章接下來只展開前兩類——它們是後面四章的共同語言。
還有一條取徑值得先定位,免得你在別處讀到時對不上:自監督式學習(self-supervised learning)——把資料的一部分遮起來,拿被遮住的那部分當答案,於是無標籤的資料自己造出了成對的訓練訊號。它與本章的兩條主線是平行的兩條路,一樣不需要外部標註,只是它繞回了監督式的損失形式。本章不展開它。
最後提醒一件容易誤會的事:沒有標籤不等於沒有成本。標註的人力省下來了,另一筆成本立刻頂上——你沒有答案可以拿來檢查模型學得對不對。這一章後半三個小節,處理的都是這筆成本。
二、密度:把「像不像這批東西」算成一個數
要對「多典型」打分,得先講清楚打的是什麼分。
對連續的量而言,「厚度剛好等於 6.31 毫米」這件事的機率是零——連續變數落在任何單一點上的機率都是零。有意義的問法是「落在某一段區間裡」的機率。機率密度(probability density) 就是為了補這個缺口而定義的:把一小段區間的機率除以區間的寬度,再讓寬度縮到零,得到的那個值就是該點的密度。密度要乘上一小段寬度才會變回機率。
這帶來一個學生常踩的坑:密度值可以大於一。 假設某批碎片的厚度嚴格落在 5.0 到 5.4 毫米之間,而且在這個範圍內均勻分布,那麼密度值處處是 \(1 / 0.4 = 2.5\)。2.5 不是機率,它是「每毫米分到多少機率」。機率要等你指定一段範圍才會出現。
有了密度,訓練目標就有了。我們讓模型交出一個帶參數的密度函數 \(Pr(x \mid \boldsymbol\phi)\),再挑一組 \(\boldsymbol\phi\),讓所有訓練資料的密度值一起變大。這個準則叫最大概似(maximum likelihood)。取對數、取負號、再取平均,就得到可以直接拿去最小化的損失:
逐項拆解:\(I\) 是訓練資料的筆數;\(x_i\) 是第 \(i\) 筆資料,這裡就是第 \(i\) 片碎片的六個量測值;\(Pr(x_i \mid \boldsymbol\phi)\) 是模型在目前這組參數下給這一片打出的密度值;單筆的損失是 \(\ell_i = -\log Pr(x_i \mid \boldsymbol\phi)\),密度越高、這個數越小。取對數有兩個理由:幾千個小於一的數連乘會直接掉到浮點數表示不出來的地方,取對數把乘法變成加法;而且對數是遞增的,最大化乘積與最大化對數和挑出的是同一組參數。前面加負號是為了把「越大越好」翻成「越小越好」,好接上梯度下降。除以 \(I\) 取平均而不是加總,是為了讓不同資料量算出來的損失可以直接比大小。不除也不影響結果:\(I\) 是一個固定的正整數,把它拿掉只是讓整個數字放大 \(I\) 倍,讓損失最小的那組參數仍然是同一組。
到這裡為止還沒有出現真正有意思的東西。有意思的是密度函數必須滿足的那條約束:
逐項拆解:積分符號 \(\int\) 在這裡的意思是「把密度沿著整個資料空間掃過去、累加起來」;\(\mathrm{d}x\) 是掃描時每一小步的寬度。整條式子說的是:所有可能取值的機率加起來必須剛好等於一。密度函數不是隨便一個非負函數,它得滿足這一條才算數。
這條約束的後果比它的外表大得多:模型手上的機率質量是固定的一份,它只能重新分配,不能無中生有。 把某一處的密度堆高,別處必然被壓低。這句話一個人做了兩件事。第一,它讓「只顧訓練點」變成一種會自己付出代價的策略——你把質量全堆到已知的碎片上,就等於宣告其他所有位置幾乎不可能出東西。第二,涵蓋度不是額外加上去的要求,它是概似本身的後果:一個沒有把質量鋪到資料真的會出現的各個地方的模型,概似一定難看。
比喻: 一片剛從探坑側壁掉下來的殘片,該不該收進這個遺址的序列?你不會拿它去對照一本「標準答案圖冊」,因為沒有那種東西。你看的是它跟已經整理好的那幾百片有多相容:斷面的厚薄落在同一個範圍嗎、胎土的顆粒對得上嗎、紋飾的疏密像不像。相容的程度越高,它「屬於這個遺址」的分數越高——那個分數就是密度。這個比喻有一處會誤導你:現場的碎片是有限的幾千片,原則上可以一片片數完;資料的分布是連續的,你手上的樣本只是無窮多種可能裡的一小撮。密度描述的是整片可能性的地形,不是「我數過的那幾千片」。
下面這段程式把「機率質量守恆」的後果直接跑出來。兩個密度模型的做法一樣:在每一片訓練碎片的位置疊一個小鐘形,加起來再除以片數——這種做法叫核密度估計(kernel density estimation)。差別只在鐘形的寬度 \(h\)。
import numpy as np
rng = np.random.default_rng(0)
def dig(n):
"""從遺址挖出 n 片陶片,量它們的唇部厚度(mm)。"""
small = rng.normal(5.0, 0.6, n // 2) # 小型器
large = rng.normal(9.5, 1.1, n - n // 2) # 大型器
return np.concatenate([small, large])
train, test = dig(80), dig(80)
def logpdf(q, pts, h):
"""以 pts 每一點為中心疊高斯峰,回傳 q 各點的對數密度。"""
u = (q[:, None] - pts[None, :]) / h
dens = np.exp(-0.5 * u ** 2).mean(axis=1) / (h * np.sqrt(2 * np.pi))
return np.log(dens + 1e-300)
grid = np.linspace(0.0, 16.0, 160001) # 積分用的細格點
dx = grid[1] - grid[0]
near = np.abs(grid[:, None] - train[None, :]).min(axis=1) <= 0.05
for name, h in (("死記型 h=0.02", 0.02), ("平滑型 h=0.60", 0.60)):
tr = logpdf(train, train, h).mean()
te = logpdf(test, train, h).mean()
dens = np.exp(logpdf(grid, train, h))
print(f"{name}: 訓練集 {tr:+.3f} 測試集 {te:+.3f} "
f"全域積分 {dens.sum() * dx:.3f} "
f"訓練點 ±0.05mm 內佔 {dens[near].sum() * dx:.3f}")實跑輸出:死記型的訓練集平均對數密度是 -0.912、測試集是 -9.270、全域積分 1.000、訓練點附近佔 0.997;平滑型依序是 -1.895、-1.950、1.000、0.621。
拿訓練集那一欄看,死記型贏;換成測試集,它輸了七個單位以上。後兩個數字說明了原因:兩個模型的機率質量都恰好是一,但死記型把其中 99.7% 塞進了訓練點左右各 0.05 毫米的細縫裡,剩下的整條數線只分到 0.3%。新挖出來的碎片不會剛好落進那些細縫,於是它拿到的密度趨近於零,取對數就崩了。這不是在打比方——它就是過度擬合本身,只是這裡沒有答案可以對,過度擬合換了一種形狀:機率質量的分配過度貼著訓練樣本。
密度模型附帶兩項能力,兩項都會在後面用到。第一項是模型之間可以量化比較:同一批沒看過的資料,兩個模型各報一個平均對數密度,直接比大小就行——前提是兩個都算得出密度,這個前提第六小節會再回來。第二項是離群判定:既然每一筆都有一個分數,就可以訂一個門檻,低於門檻的判為外來。
import numpy as np
rng = np.random.default_rng(3)
# 同一個遺址、同一種量測:200 片陶片的唇部厚度(mm)
train = np.concatenate([rng.normal(5.0, 0.6, 100), rng.normal(9.5, 1.1, 100)])
def logpdf(q, pts, h=0.6):
u = (q[:, None] - pts[None, :]) / h
dens = np.exp(-0.5 * u ** 2).mean(axis=1) / (h * np.sqrt(2 * np.pi))
return np.log(dens + 1e-300)
base = logpdf(train, train)
thr = np.percentile(base, 5) # 門檻=已知碎片裡最不典型的百分之五
probe = [("典型小型器", 5.1), ("典型大型器", 9.4), ("兩群之間", 7.3),
("偏厚但沒破紀錄", 12.0), ("混進來的現代磚屑", 21.0)]
for name, v in probe:
lp = float(logpdf(np.array([v]), train)[0])
verdict = "外來" if lp < thr else "本址"
print(f"{name:<9} {v:5.1f}mm 對數密度 {lp:+7.2f} 判定:{verdict}")
print(f"門檻 {thr:+.2f};已知碎片被誤判為外來的比例 {np.mean(base < thr):.3f}")
print(f"最厚與最薄的已知碎片:{train.min():.2f}mm ~ {train.max():.2f}mm")實跑輸出:門檻是 -3.01,已知碎片被誤判為外來的比例是 0.050,已知碎片的厚度範圍是 3.30mm ~ 12.31mm。五片試探樣本的對數密度依序是 -1.47(判本址)、-1.82(本址)、-3.53(外來)、-3.49(外來)、-110.62(外來)。
最值得看的是第三片。7.3 毫米沒有破任何紀錄——已知碎片從 3.30 到 12.31 毫米都有,7.3 穩穩落在中間。一條「有沒有超出範圍」的規則會放它過去,密度模型不放,因為 7.3 毫米落在兩個器型之間的空隙裡,那裡本來就幾乎不出東西。離群不是「太大或太小」,是「落在資料本來不去的地方」。
密度這條路有一個限制要先說在前面:維度一高就很吃力。用疊鐘形的方式描述分布,需要的樣本量隨著維度快速膨脹——六個量測還撐得住,六百個就不行了。後面四章之所以不用這種數點的辦法,而改成「先選一個簡單分布、再學一個映射」,這是原因之一。
三、潛在座標:替看不見的東西編號
潛在變數(latent variable) 在第 1 章出現過一次,這一章要把它當工具用,所以先把最容易誤解的地方釘死:潛在變數不是資料裡漏記的欄位。 它不是「本來該量卻忘了量」的東西,而是模型端自己設定的一組座標。你決定要幾個,至於每一個是什麼意思——模型其實也沒有「決定」,它只是找到一組能把資料重建回來的座標,那組座標對不對應到任何講得出名字的量,完全沒有保證。這一節後半會證明這句話。
寫成式子就是第 1 章給過的那個形式:先從一個簡單、已知的分布抽一個低維向量,再讓一個學出來的映射把它攤開成高維的資料。
逐項拆解:\(Pr(z)\) 是我們自己挑的簡單分布,最常見的選擇是各分量獨立的標準常態分布;符號 \(\sim\) 讀作「服從」,所以第一步的意思是照 \(Pr(z)\) 的機率隨機抽一個座標出來,抽到的那一個記作 \(z^{*}\);\(g[\cdot]\) 是映射,通常是一個深度網路;\(\boldsymbol\phi\) 是這個網路的參數全集;\(x^{*}\) 是攤開之後得到的一筆新資料。取樣是兩步,不是一步——第一步的隨機性全部來自 \(Pr(z)\),第二步是確定的計算。這個分工是後面四章都會用到的。
這個設定裡有兩個方向,分工不同,而且不一定同時具備。由資料到座標(\(x \to z\))是壓縮與分析:給一片碎片,交出它的低維座標。由座標到資料(\(z \to x\))是合成:給一組座標,交出一片碎片。第 15 到 18 章的四種做法,有的兩個方向都給得出來,有的只給合成方向。
把 \(z\) 的維度設得比 \(x\) 低,等於在宣告一個假設:資料實際上住在一個低維的形狀上。這個假設不總是成立,而且兩邊都會出事——設得太低,模型表達不出資料真正的變化;設得太高,等於沒有壓縮,你又回到原來那個高維的問題。這個維度是要調的,而怎麼調需要一把量尺,那正是本章後半的難題。
比喻: 面對一地碎片,你真正想推斷的東西沒有一片直接寫著。碎片上量得到的是厚度、弧率、紋飾條數;你想知道的是原器物的口徑多少、器高多少、屬於哪一期的燒製工法。那幾個看不見的量才是真正在動的旋鈕——口徑一變,斷面的弧率跟著變,唇部的厚薄也跟著變。碎片是這幾個旋鈕被攤開之後的結果。這正是潛在座標與映射的關係:\(z\) 是旋鈕,\(g[\cdot]\) 是把旋鈕設定攤開成一片碎片的那套工序。這個比喻有一處嚴重失準,而那個失準點恰好是本章後半的主題:遺址底下真的存在過一件完整的器物,口徑是一個確定的數,只要哪天挖到同型的完整器,你的推斷就能被驗證。資料的「真實分布」沒有這種可以被挖出來的原件——它從來不會被驗證。
潛在空間有兩種常被要求的好性質,而學生最常把它們當成同一件事:
平滑(smooth):座標小幅移動,產出也小幅變化,而且沿途每一點都對應到一個合理的產出。可解釋(interpretable):單獨動一個座標分量,對應到一個講得出名字的屬性變化。
兩者互不蘊含,兩個方向都有反例。假設有一組座標,你沿著任何一條直線走,產出的碎片都平順地從一種形制過渡到另一種,中間沒有一步跳掉——它很平滑;但你動第一個分量的時候,口徑和器高同時在變,沒有哪一個分量單獨對應到哪一個屬性——它不可解釋。反過來,假設另一組座標的第一個分量確實只控制口徑,講得出名字;但它從 2.0 走到 2.1 的時候產出突然從一種器型跳成另一種,中間那些值對應到根本不存在的東西——它可解釋,卻不平滑。
平滑相對容易做出來,可解釋難得多。難在哪裡?難在同一個模型可以有無數組彼此等價、但意義完全不同的座標。這件事有名字:表示的不可辨識性(unidentifiability)。
拿線性的映射來看最清楚。設資料寫成 \(x = z\mathbf{G}\),其中 \(z\) 是一列潛在座標,\(\mathbf{G}\) 是把座標攤開成量測值的矩陣。任取一個可逆方陣 \(\mathbf{M}\),定義一組新的座標與一個新的映射:
逐項拆解:\(\mathbf{M}\) 是任何一個可逆的方陣,可逆的意思是它有反矩陣 \(\mathbf{M}^{-1}\),兩者相乘會得到單位矩陣;\(z'\) 是把原座標經過 \(\mathbf{M}\) 換算之後得到的新座標;\(\mathbf{G}'\) 是相應調整過的新映射。把兩者接起來算一次:
每一筆資料的重建結果一模一樣,重建誤差當然也一模一樣。但 \(z'\) 的第一個分量已經不是原來那個東西了。這裡寫成線性只是為了讓推導看得見;換成深度網路,只要能找到一個可逆的變換、再讓映射把它抵銷掉,結論不變。
import numpy as np
rng = np.random.default_rng(1)
n = 200
# 兩個真正在動的量:原器物的口徑與器高(cm),沒有任何一片碎片直接寫著它們
z = np.stack([rng.normal(24.0, 5.0, n), rng.normal(18.0, 4.0, n)], axis=1)
G = rng.normal(0, 1, (2, 6)) # 潛在座標 → 6 項碎片量測
x = z @ G + rng.normal(0, 0.05, (n, 6)) # 觀測到的碎片資料
def recon(zz, GG):
return float(np.mean((zz @ GG - x) ** 2)) # 平均平方重建誤差
t = np.deg2rad(60.0)
Q = np.array([[np.cos(t), -np.sin(t)],
[np.sin(t), np.cos(t)]]) # 旋轉 60 度
P = np.array([[0.0, 1.0], [1.0, 0.0]]) # 交換兩個座標
cases = [("原始表示", z, G),
("旋轉 60 度", z @ Q, Q.T @ G),
("交換兩軸", z @ P, P.T @ G)]
for tag, zz, GG in cases:
corr = np.corrcoef(zz[:, 0], z[:, 0])[0, 1] # 第 1 個座標還是不是「口徑」
print(f"{tag}: 重建誤差 {recon(zz, GG):.12f} "
f"第 1 座標與口徑的相關 {corr:+.3f}")實跑輸出:三組表示的重建誤差都是 0.002693051342,一路相同到小數點後十二位;而第一個座標與「口徑」的相關係數分別是 +1.000、+0.618、+0.041。
第 1 章講完潛在變數之後留了一個問題:兩套模型都把量測壓成兩個數字,還原誤差一模一樣,你憑什麼說其中一套比較好?現在可以正面回答了——你不能憑重建誤差說。不是因為還沒有人想出更好的算法,而是因為這把尺在原理上就量不到那件事:它對座標的旋轉與置換完全無感。這是「表示難以評價」的數學根因,也是下一小節整節的起點。
四、量尺的空缺:為什麼這一章沒有答案卷
監督式學習的評價有一個結構上的便利,便利到平常不會被注意:測試集裡有答案。 模型輸出一個數,答案是另一個數,兩個一減就得到誤差;整套評價方法的地基就是這件事。
非監督式學習沒有這一層。模型交出來的是一個密度函數、一組座標、或一片新的碎片,而你手上沒有「正確的密度函數」「正確的座標」「正確的新碎片」可以拿來對。
想繞過去的第一個念頭很自然:那就找一把尺來量。問題在尺要從哪裡來。要判斷模型描述的分布對不對,你得先知道真實的分布長什麼樣;而「知道真實的分布長什麼樣」正好是這個模型被派去做的事。你手上唯一有的是從真實分布抽出來的一批樣本,樣本不是分布。於是每一把可用的尺,都得從樣本或另一個模型身上長出來——找不到一把獨立於「被量的那件事」之外的尺。 這不是誰偷懶沒去做,是這件事的結構就長這樣。
比喻: 同一堆碎片交給兩組人修復。兩組都把接合面對得嚴絲合縫,沒有一處勉強,最後拼出來的器形卻不一樣——一組拼成矮胖的罐,一組拼成瘦高的壺。你憑什麼判誰對?沒有原件可以對照,兩組的內部一致性又都無可挑剔。這就是非監督式評價的處境:你有的只是碎片,沒有原件。這個比喻的失準之處在於它太樂觀了——考古現場至少還有機會挖到同型的完整器,或找到同時期別的遺址當旁證,答案原則上有浮現的一天。資料的真實分布沒有這種可能,它不是一件埋在別處等著被挖到的東西。
實務上最常用的評價方式其實是把生成的樣本印出來看。這有它的道理:人對「像不像」極為敏感,一眼就能抓到機器抓不到的違和。但它有三個硬限制。第一,只適用於人看得懂的資料型態。 影像、聲音還可以,一堆六維的量測值印出來,你看不出任何東西。第二,看不出漏掉了什麼。 你看到的永遠是模型交出來的樣本,不是它交不出來的那些;一個只做得出一種器型的模型,它做出來的每一件都可能無可挑剔。第三,兩個都「看起來不錯」的模型沒辦法比較。 眼睛給不出可以排序的數。
還有一件事得講在具體指標之前,免得誤會:任何一個公開的、可以計算的分數,都可以被針對性地推高。 一旦某個數字成為大家比較的標準,就會出現專門把那個數字做漂亮的做法,而那個做法不見得讓模型真的變好。這不是誰不老實,是把單一數字當成目標的必然結果。
最後一層最容易被忽略:量尺選錯,錯的不只是那一次評分。 你會用它來決定潛在維度設幾維、訓練跑多久、哪一版留下來。量尺量不到的那個方向,在整個開發過程中都不會有人往那邊調整——因為往那邊調不會讓數字變好看。評價在這裡不是最後一道驗收手續,它從第一天就在替你選路。
五、把「好」拆開:幾條會各自失敗的軸
評價之所以難,有一半是因為「好」根本不是一維的。在想怎麼量之前,先把它拆成幾件可以分開檢查的事。
第一組關於產出的樣本。一條軸是每一件產出單獨看夠不夠像真的;另一條軸是整批產出有沒有覆蓋到資料的各種樣態,只把最好認的那一類復原得很漂亮不算數。這兩條必須分開,因為它們是兩個可以獨立失敗的方向:一個只復原最好認的那種罐子、而且復原得無懈可擊的模型,第一條軸滿分、第二條軸慘不忍睹;一個什麼器型都碰一點、但沒有一件對得上的模型剛好相反。把兩條軸揉成一個數字,這兩種相反的失敗就會互相抵銷,看起來一樣糟或一樣好。第六小節整節的指標設計都建立在這一條上。
而且這兩條會互相拉扯:要讓每一件都無可挑剔,最省事的辦法就是只做最有把握的那一種;要什麼都涵蓋到,就得往資料稀疏的地方伸手,而那裡本來就容易做壞。
第二組關於潛在空間:平滑與可解釋,第三小節已經拆過,這裡只是把它們放回這張清單上。這一組跟第一組同樣會拉扯——一個潛在空間被整理得越規矩、越好解釋,它能容納的資料樣態通常就越受限。
第三組關於計算。產生一個樣本要花多少計算、能不能一次平行做完一整批?算一筆資料的機率值要花多少計算、算出來的是精確值還是近似值?這一組在課堂上最容易被跳過,在實務上卻經常是決定用哪個模型的那一條——一個各方面都好、但生一個樣本要跑幾百步的模型,在有些場合根本不能用。
再補三條。它們在原理的討論裡出現得少,在真的要把模型交出去的時候卻幾乎每次都會踩到。
訓練的穩定性與可重現性。 同一份設定、只換一個隨機種子,重跑一次會不會得到差很多的結果?如果會,那你在某張表格上看到的那一格數字,究竟是那個做法的實力還是那一次的運氣,就說不清楚了。這條軸不影響「最好的那一次能做多好」,但它決定了「你能不能靠它做事」。
是不是只是把訓練樣本記下來。 一個模型可以產出無可挑剔的樣本,而那些樣本其實是訓練集裡某一筆的複製品或近似複製品。這同時是兩個問題:它讓第一條軸失去意義——照抄當然像;而如果訓練資料裡有不該外流的內容,模型就等於把那些內容原樣吐了出來。要檢查這一條,光看樣本沒有用,得回頭去查每一件產出在訓練集裡有沒有一個近得可疑的鄰居。
可控性。 能不能指定條件去生成,而不是只能亂抽一個。第 1 章提過條件生成這個名字,但一個模型加不加得上這一層、加上去要付多少代價,各家做法差得很遠。
這幾條軸沒有哪個模型能同時做到頂,而且上面已經看到好幾處它們互相拉扯的地方。與其現在給你一張打勾表,不如換個讀法:接下來四章可以當成四種不同的取捨方案來讀——每一章都在某幾條軸上很強,代價落在另外幾條上。讀完之後你自己把表填出來,會比現在直接拿到一張有用得多。
六、三把尺,以及它們各自量不到的東西
先把問題擺清楚。假設兩份修復方案擺在你面前:方案 A 只復原中間那一種器型,但復原得極貼;方案 B 三種器型的區域都撒了一點,卻沒有一件對得上任何一種。哪一份比較好?
這一題沒有答案,而這正是重點——你想問的其實是兩個問題,不是一個。 任何把它們壓成一個數字的做法,都會在某處把兩種相反的失敗判成一樣。
第一把尺:兩個問題,就報兩個數。 做法是:以每一片真實碎片為中心、以它到第五個鄰居的距離為半徑畫一個球,所有球的聯集就是「真實碎片大致落在哪裡」的一個粗略近似。同樣的做法套在仿製品那一堆上,就得到「仿製品大致落在哪裡」。兩個區域畫出來之後,兩個數字自然浮現——貼合度是仿製品落進真實區域的比例,涵蓋度是真實碎片落進仿製區域的比例。
import numpy as np
rng = np.random.default_rng(2)
# 真實出土碎片:三種器型,各在(唇部厚度 mm,紋飾條數 條/cm)平面上聚成一團
cent = np.array([[5.0, 9.0], [8.0, 5.0], [11.0, 2.0]])
real = np.concatenate([c + rng.normal(0, 0.35, (60, 2)) for c in cent])
# 方案 A:只復原中間那一種器型,但復原得極貼
fakeA = cent[1] + rng.normal(0, 0.35, (180, 2))
# 方案 B:整片區域都撒一點,但沒有一件對得上任何一種器型
fakeB = real.mean(0) + rng.normal(0, 3.0, (180, 2))
def ball_radius(pts, k=5):
d = np.linalg.norm(pts[:, None, :] - pts[None, :, :], axis=2)
return np.sort(d, axis=1)[:, k] # 每點到第 k 個鄰居的距離
def inside(query, pts, r):
d = np.linalg.norm(query[:, None, :] - pts[None, :, :], axis=2)
return float(np.mean((d <= r[None, :]).any(axis=1)))
r_real = ball_radius(real)
for tag, fake in (("A", fakeA), ("B", fakeB)):
r_fake = ball_radius(fake)
fid = inside(fake, real, r_real) # 仿製品落進真實區域的比例
cov = inside(real, fake, r_fake) # 真實碎片被仿製區域蓋到的比例
gap = float(np.linalg.norm(fake.mean(0) - real.mean(0)))
print(f"方案 {tag}: 貼合度 {fid:.3f} 涵蓋度 {cov:.3f} 重心距離 {gap:.3f}")實跑輸出:方案 A 的貼合度是 0.989、涵蓋度 0.333、重心距離 0.327;方案 B 依序是 0.161、1.000、0.030。
兩個比例把兩種失敗分得乾乾淨淨:A 做出來的東西幾乎每一件都落在真實碎片堆裡(0.989),但三種器型只顧到一種(0.333);B 反過來,真實碎片全被它的區域蓋住了(1.000),可是它自己的產出只有一成六落在真實碎片附近(0.161)。
那個「單一數字」呢?重心距離把 B 排在 A 前面(0.030 對 0.327)。它不只分不出兩種失敗,還把順序排反了——B 的重心之所以對得準,只是因為它均勻地撒在正確的位置附近,而不是因為它復原出了什麼。
這裡有一個一定要點破的混淆:這兩個數在文獻裡常被叫作精確率(precision)與召回率(recall),但它們與監督式分類裡的那一對不是同一件事。分類裡的精確率與召回率是逐筆判對錯之後數出來的;這裡沒有任何一筆有對錯可判,量的是兩堆點的覆蓋關係。名字借過來了,被量的東西換掉了。
第二把尺:模型自報的機率。 如果模型算得出 \(Pr(x \mid \boldsymbol\phi)\),最直接的評價方式就是拿一批它沒看過的資料,算平均對數密度。第二小節那段程式已經示範過為什麼不能用訓練集:把密度峰全壓在訓練點上就能把訓練集的數字刷得很漂亮,而那樣的模型在新資料上是廢的。
測試集的概似同時反映了兩件事,這是它好用的原因。模型必須在沒看過的位置也給得出密度,這是泛化;而且必須把質量鋪到測試資料實際出現的各個地方,這是涵蓋度——後者是機率質量守恆的直接後果,只要有一片測試碎片落在模型認為不會出東西的位置,那一筆的對數密度就會把整個平均狠狠往下拉。
但這條路有一個前提,而且這個前提經常不成立:模型得算得出機率。 不是每一種生成模型都算得出。有些做法只保證你抽得出樣本,卻沒有辦法回答「這一筆的密度是多少」;有些算得出,但算出來的是一個下界或一個近似值,不是精確值。第 15 到 18 章的四種做法在這一點上分得很開,而這個分野正是下一把尺存在的理由。
第三把尺:借一把外部的尺。 當模型自己給不出機率,剩下的辦法是借。借法是:拿一個現成的、在大量資料上訓練過的分類模型,把真實樣本與生成樣本都丟進去,然後不比較原始的量測值,改成比較它們在那個分類模型內部引起的反應。
一種做法是看分數。看分類模型對每一件生成樣本的判斷夠不夠篤定——篤定表示這一件確實像某個東西;同時看整批樣本被判成各個類別的比例夠不夠平均——平均表示這批東西不是只有一種。這一族的代表叫 Inception 分數(Inception Score,常縮寫成 IS)——Inception 是那個借來的外部分類模型的名字,不是「起始」的意思,中文材料通常也直接沿用。它的定義是先算一個期望值、再對它取一次指數(外層的指數只是換個刻度,不改變模型之間的排名;真正做事的是裡面那個期望值):
逐項拆解:\(x\) 是一件生成樣本;\(c\) 是外部分類模型的類別編號;\(Pr(c \mid x)\) 是那個模型看到 \(x\) 之後對各個類別給出的機率向量;\(Pr(c)\) 是把所有生成樣本的這個向量平均起來得到的整批類別分布;\(D_{\mathrm{KL}}(\cdot \,\|\, \cdot)\) 是 KL 散度(Kullback-Leibler divergence),量的是兩個機率分布差多遠,兩個分布一樣時它等於零;\(\mathbb{E}_{x}[\cdot]\) 表示對所有生成樣本取平均;最外層的 \(\exp(\cdot)\) 是指數函數,它單調遞增,只負責把刻度撐開——哪個模型分數高、哪個低,仍由括號裡那個期望值決定。這個數字要大,得同時滿足兩件事:每一件的 \(Pr(c \mid x)\) 都很尖(單件夠篤定),而平均起來的 \(Pr(c)\) 卻很平(整批夠雜)。它把第五小節那兩條軸塞進了同一個數字——這是它的巧妙之處,也是它的問題:兩條軸被壓回一維,你還是看不出壞掉的是哪一邊。
另一種做法是看距離。不看分類結果,改看分類模型中間層算出來的特徵向量,把真實樣本與生成樣本的特徵各自用一個高斯分布近似,再算兩個高斯之間的距離。這一族的代表常縮寫成 FID(Frechet Inception Distance)——Inception 還是同一個外部分類模型,Fréchet 距離則是計算兩個高斯之間差多遠的那條公式。它比分數型多了一項好處:真實樣本被拉進了計算裡,而分數型從頭到尾沒看真實樣本一眼。
這一族共同的盲點只有一句話,但很致命:尺本身帶偏見。 那個外部分類模型是在某一批資料上、為了某一個任務訓練出來的,它保留下來的特徵是對那個任務有用的那些,其餘資訊在它眼裡等於不存在。被它丟掉的東西,指標就量不到。換一把尺,數字會變,模型之間的排名也可能跟著翻。
還剩最後一件事沒處理:表示要怎麼評價。 第三小節已經證明重建誤差不行。有沒有辦法直接比較兩組表示?直接比是不行的——兩組座標可以逐點對應到完全不同的數值,卻描述同一個東西。
實務上採用的是一個折衷:拿學到的表示去做一件有答案的小任務,用那件小任務的成績當代理指標。 做法是把表示凍住不動,在它上面接一個很小的模型(通常小到只是一層線性層),用少量有標註的資料訓練這個小模型,看它能做到多好。這種做法叫探測(probing)。回到考古現場:你有幾千片沒有標註的碎片,也有一小批專家花時間判定過器型的。把那一小批的座標取出來,看能不能用一層很簡單的規則就把器型分對——分得對,就說明「器型」這個資訊確實被那組座標保留了下來。
這個折衷的代價必須講清楚:你量到的是「這組表示對那件小任務有沒有用」,不是「這組表示本身好不好」。 換一件小任務,排名就可能翻過來。它是一把有刻度的尺,但它量的是側影,不是本體。到今天為止,這仍然是這個問題最好的答案——第 1 章說「還沒有公認的量尺」,指的就是這個處境。
把三把尺放在一起看,共同點很清楚:每一把都有它量不到的東西,而且量不到的部分各不相同。 兩個覆蓋比例量不到樣本的細部品質;測試集概似要求模型算得出機率;外部的尺帶著它自己的偏見;探測任務量的是代理。所以實務上不會只看一個數字——不是因為謹慎,是因為單一數字在這個題目上原理性地不夠用。
最後把接下來四章的骨架擺出來,只擺骨架。第 15 到 18 章的四種做法共用的就是第三小節那個形式:一個事先選定的簡單分布,加上一個學出來的映射。四種做法的差別全部落在兩件事上——映射怎麼學,以及機率怎麼算(或者乾脆不算)。骨架不變。讀那四章的時候帶著這一章的兩份清單就好:第五小節那幾條軸,看它強在哪、代價付在哪;第六小節那三把尺,看它算不算得出機率,因此該用哪一把尺去量它。
§03原書對照
原書第 14 章是全書篇幅最短的一章,只有七個印刷頁,功能上是替後面四章的生成模型做開場:先把「沒有標籤」這件事的界定與這一類模型的多種用途交代清楚,再給分類法、理想性質與評價指標,具體模型全部留到後續章節。
開場在 p.269。原書先用一段回顧前面十二章走過的路,再指出這一類模型共同的界定條件與彼此差異很大的目標;同一頁還有一條註腳提醒讀者,接下來四章需要比前面各章更紮實的機率基礎,並把讀者指向書末的附錄 C——那是一份從 p.449 開始的機率入門。想在讀後續四章之前把機率工具補齊的人,那個附錄比任何速成教材都貼近本書的記號習慣。
分類法在 pp.269–270。原書用一張圖把「非監督式/生成/機率式生成/潛在變數」這幾個範疇的包含關係一次畫出來,位置在 p.270 的上半;同一頁舉了一個把資料點指派到群編號的經典演算法,當作「由資料映到潛在變數」這個方向的代表,並在該頁給出以負對數概似為訓練目標的式子。順帶一提,該式在原書寫成各筆損失的總和;總和與平均只差一個正的常數倍,最小值的位置相同。p.270 的第二條註腳補了一個容易被忽略的例外:機率式的生成模型不見得都得靠潛在變數,並回指第 12 章的自迴歸寫法。
理想性質那一節跨在 pp.270–271,原書用六條項目符號一次列完;緊接著 p.272 有一張表,把四種模型對這六條逐項打勾、打叉或標問號。原書自己註明這張表的判定帶有主觀成分、細節可以爭論,只是多數實務工作者都會同意沒有哪一種模型能全部滿足。想知道作者當時怎麼給分的人,直接翻那張表最快。同一頁的上半(p.271)另有一張分成兩格的示意圖,用兩種顏色的點畫出訓練過程中的兩種樣貌:一格是生成的樣本逐步變得難以與真實樣本區分,另一格是學到的分布逐步把真實樣本涵蓋進來。
評價指標從 p.272 開始。測試集概似的討論在 p.272,包含為什麼不改用訓練集概似,以及這個量對四種模型各自算不算得動。借用預訓練影像分類器的分數型指標橫跨 pp.272–273,兩條式子與一張分成兩格的示意圖都在 p.273;把兩個分布各自用高斯近似再算距離的做法在 pp.273–274;把「像不像」與「夠不夠齊」拆成兩個數的流形重疊指標在 p.274,配圖則在 p.275,是一組六格圖,逐步畫出流形怎麼被一堆球近似出來。每一種指標,原書都在定義之後緊接著寫它失準在哪裡,那幾段對照著讀特別有用。
章末的摘要在 p.274;註記橫跨 pp.274–275,前一段列出各類生成模型的原始文獻與一份近年綜述,後一段專講評價,把每個指標的出處、後續改良與批評文獻都點了名——想按圖索驥找原始論文的人,那半頁的密度最高。附錄 C 的 pp.461–462 另有兩個分布距離的定義,是讀懂那幾條指標式子的前置。另外值得一提:全書多數章的章末都附有習題,這一章沒有。原書第 14 章對應印刷頁 pp.269–275。
§04作業和解答
作業一:把機率質量堆到零,會發生什麼事
某個遺址的陶片按紋飾分成四型:A、B、C、D。手上已整理好的十片中,A 型五片、B 型三片、C 型兩片、D 型零片。兩個候選模型給出的機率分別是:模型甲 \((0.50,\,0.30,\,0.20,\,0.00)\),模型乙 \((0.45,\,0.28,\,0.19,\,0.08)\)。
(a)分別算出兩個模型在這十片上的平均對數概似,判斷誰比較高。(b)隔週又挖出五片:A 型兩片、B 型一片、C 型一片、D 型一片。重算兩個模型在這五片上的平均對數概似。(c)模型乙的那 0.08 是從哪裡來的?用機率質量守恆解釋,並說明它換到了什麼。
解答 SOLUTION
(a)平均對數概似就是把每一片的 \(\log Pr\) 加起來除以片數。模型甲:\((5\log 0.50 + 3\log 0.30 + 2\log 0.20)/10\)。三項分別是 \(5 \times (-0.6931) = -3.4657\)、\(3 \times (-1.2040) = -3.6119\)、\(2 \times (-1.6094) = -3.2189\),合計 \(-10.2965\),除以十得 \(-1.0297\)。模型乙:\((5\log 0.45 + 3\log 0.28 + 2\log 0.19)/10 = (-3.9925 - 3.8189 - 3.3215)/10 =\) \(-1.1133\)。甲比較高。
(b)模型甲對 D 型給的機率是零,\(\log 0\) 是負無窮大,所以甲的平均對數概似是 \(-\infty\)。只要有一片落在它宣告「不可能」的地方,整個平均就毀了,其他四片再準也救不回來。模型乙:\((2\log 0.45 + \log 0.28 + \log 0.19 + \log 0.08)/5 = (-1.5970 - 1.2730 - 1.6607 - 2.5257)/5 =\) \(-1.4113\)。
(c)四個機率必須加起來等於一,所以要給 D 型 0.08,只能從 A、B、C 身上扣:甲的 0.50、0.30、0.20 在乙那裡變成 0.45、0.28、0.19,扣掉的量剛好是 0.08。這就是機率質量守恆——質量只能重新分配。乙用「在已見過的三型上稍微差一點」換到了「不對任何一型宣告絕對不可能」。(a)與(b)合起來看,這筆交易在訓練資料上是虧的、在新資料上是賺的,而且賺的幅度沒有上限,因為對手是負無窮大。(本題數值以 numpy 重算核對過。)
作業二:用兩個比例判斷哪一個「品質好但涵蓋差」
在(厚度,紋飾條數)平面上有六片真實碎片:\((0,0)\)、\((1,0)\)、\((0,1)\)、\((4,4)\)、\((5,4)\)、\((4,5)\)。兩個仿製方案各交出四件:
- 方案甲:\((0.3,0.3)\)、\((0.3,0.9)\)、\((0.9,0.3)\)、\((0.9,0.9)\)
- 方案乙:\((0,2.5)\)、\((3,0)\)、\((3,3)\)、\((5.5,4.5)\)
每個點的球半徑取「它到同一堆裡最近鄰居的距離」(正文用的是第五近鄰,這裡改成第一近鄰讓你手算得動)。(a)算出真實碎片與兩個方案各自的球半徑。(b)算出兩個方案的貼合度與涵蓋度。(c)哪一個是「品質好但涵蓋差」?
解答 SOLUTION
(a)六片真實碎片各自都有一個距離恰好為 1 的鄰居(前三片彼此構成一個直角,後三片也是),所以真實碎片的球半徑全部是 1。方案甲的四點構成邊長 0.6 的正方形,最近鄰距離都是 0.6,半徑全部是 0.6。方案乙要逐點算:\((0,2.5)\) 到 \((3,3)\) 是 \(\sqrt{9+0.25} \approx 3.041\)(比到另外兩點都近);\((3,0)\) 到 \((3,3)\) 是 3;\((3,3)\) 到 \((5.5,4.5)\) 是 \(\sqrt{6.25+2.25} \approx 2.915\);\((5.5,4.5)\) 同樣是 2.915。所以方案乙的半徑是 3.041、3.000、2.915、2.915。
(b)貼合度問的是「仿製品有沒有落進以真實碎片為心、半徑 1 的球裡」。方案甲四點到最近真實碎片的距離分別是 0.424、0.316、0.316、0.906,全部小於 1,貼合度 4/4 = 1.000。方案乙四點到最近真實碎片的距離是 1.500、2.000、1.414、0.707,只有最後一點小於 1,貼合度 1/4 = 0.250。
涵蓋度問的是「真實碎片有沒有落進仿製品的球裡」。方案甲的球半徑只有 0.6,原點附近三片分別被 \((0.3,0.3)\)、\((0.9,0.3)\)、\((0.3,0.9)\) 蓋住(距離 0.424、0.316、0.316),遠處三片離最近的仿製品都超過 4,蓋不到,涵蓋度 3/6 = 0.500。方案乙的球又大又散:\((0,0)\) 到 \((0,2.5)\) 是 2.5 < 3.041;\((1,0)\) 到 \((3,0)\) 是 2 < 3;\((0,1)\) 到 \((0,2.5)\) 是 1.5 < 3.041;\((4,4)\) 到 \((3,3)\) 是 1.414 < 2.915;\((5,4)\) 與 \((4,5)\) 到 \((5.5,4.5)\) 是 0.707 與 1.581,都小於 2.915。六片全中,涵蓋度 6/6 = 1.000。
(c)方案甲是「品質好但涵蓋差」:它交出來的每一件都落在真實碎片堆裡(1.000),但六片真實碎片只顧到一半(0.500)。方案乙相反。要注意它們的球半徑差了五倍,而這個差距本身就是資訊——半徑大表示那一堆點自己就很散。(本題數值以 numpy 重算核對過。)
作業三:證明重建誤差對座標的可逆變換無感
設潛在座標是一列向量 \(z\),映射是線性的 \(x = z\mathbf{G}\)。取 \(\mathbf{M} = \begin{bmatrix} 2 & 0 \\ 1 & 1 \end{bmatrix}\),並令 \(\mathbf{G} = \begin{bmatrix} 1 & 0 & 2 \\ 0 & 1 & -1 \end{bmatrix}\)。
(a)先做一般性的證明:對任意可逆的 \(\mathbf{M}\),令 \(z' = z\mathbf{M}\)、\(\mathbf{G}' = \mathbf{M}^{-1}\mathbf{G}\),說明為什麼每一筆資料的重建結果不變,因此重建誤差也不變。(b)用 \(z = (2, 1)\) 具體驗算一次,寫出 \(\mathbf{M}^{-1}\)、\(\mathbf{G}'\)、\(z'\)、\(z\mathbf{G}\) 與 \(z'\mathbf{G}'\)。(c)這件事對「用重建誤差挑潛在維度」這個常見做法有什麼影響?
解答 SOLUTION
(a)重建結果是 \(z'\mathbf{G}' = (z\mathbf{M})(\mathbf{M}^{-1}\mathbf{G})\)。矩陣乘法有結合律,可以重新括號成 \(z(\mathbf{M}\mathbf{M}^{-1})\mathbf{G}\);而 \(\mathbf{M}\mathbf{M}^{-1}\) 就是單位矩陣,乘上去等於什麼都沒做,所以結果是 \(z\mathbf{G}\)。這對每一筆資料都成立,逐筆的重建值一個都沒變,把它們的平方誤差平均起來當然也不變。關鍵在 \(\mathbf{M}\) 可逆——不可逆就沒有 \(\mathbf{M}^{-1}\),這條路走不通,而且不可逆本來就意味著資訊被壓掉了。
(b)\(\mathbf{M}\) 的行列式是 2,反矩陣為 \(\mathbf{M}^{-1} = \begin{bmatrix} 0.5 & 0 \\ -0.5 & 1 \end{bmatrix}\)。新映射 \(\mathbf{G}' = \mathbf{M}^{-1}\mathbf{G} = \begin{bmatrix} 0.5 & 0 & 1 \\ -0.5 & 1 & -2 \end{bmatrix}\)。新座標 \(z' = z\mathbf{M} = (2 \times 2 + 1 \times 1,\; 2 \times 0 + 1 \times 1) = (5, 1)\)。原本的重建值 \(z\mathbf{G} = (2, 1, 3)\);新的重建值 \(z'\mathbf{G}' = (5 \times 0.5 - 0.5,\; 1,\; 5 - 2) = (2, 1, 3)\)。兩者完全相同,而座標從 \((2,1)\) 變成了 \((5,1)\)。
(c)重建誤差還是可以拿來挑維度——維度不同的兩組表示不是靠可逆變換互換的,維度砍到不夠時重建誤差確實會上升。但它不能拿來比較同一個維度下的兩組表示:在同一個維度裡,任何一組座標都可以被可逆變換攪成另一組,而重建誤差全程不動。所以「這兩個模型的重建誤差差不多,所以學到的東西差不多」是錯的推論。要比同維度的兩組表示,得改用第六小節的探測做法。(本題數值以 numpy 重算核對過。)
作業四:密度不是機率
某批碎片的斷面弧率嚴格落在 \([0.30, 0.80]\) 之間(單位是每公分幾個曲率單位),且在這個區間內均勻分布。(a)寫出它的機率密度函數,並算出密度值。(b)算出弧率落在 \([0.40, 0.45]\) 的機率。(c)一片弧率 0.42 的碎片,它的對數密度是多少?是正的還是負的?這對第二小節那個「平均對數密度越大越好」的說法有什麼意涵?
解答 SOLUTION
(a)均勻分布的密度在區間內是常數、區間外是零。要讓積分等於一,常數必須是區間寬度的倒數:寬度是 \(0.80 - 0.30 = 0.5\),所以密度值是 \(1/0.5 = \mathbf{2.0}\)。這個數大於一,一點問題也沒有——它的單位是「每一個弧率單位分到多少機率」,不是機率本身。
(b)機率=密度乘寬度=\(2.0 \times 0.05 = \mathbf{0.10}\)。這才是一個機率,落在零與一之間。
(c)弧率 0.42 落在區間內,密度是 2.0,所以對數密度是 \(\log 2.0 \approx \mathbf{+0.6931}\),是正的。所以第二小節那個損失 \(-\log Pr\) 在這裡會是負數——損失可以是負的,這不是程式寫錯。 連續變數的對數密度沒有上界,只要模型把密度堆得夠尖,這個值可以無限大。這正是為什麼平均對數密度只有在同一批測試資料上比較不同模型時才有意義:它的絕對值不代表任何東西,跨資料集比更是沒有意義。(本題數值以 numpy 重算核對過。)
§05參考資料
- Wikipedia:核密度估計 — 本章第二小節那兩段程式用的就是這個方法,條目裡有頻寬怎麼選的完整討論
- Wikipedia:最大概似估計 — 想看「為什麼最大化概似是合理的訓練準則」的統計學根據,從這裡開始
- Wikipedia:可辨識性 — 第三小節那個「換一組座標、結果不變」的現象在統計學裡的一般形式
- scikit-learn:密度估計 — 把本章的一維示範換成能跑真實資料的實作,附維度上升後的行為說明
- scikit-learn:離群與新奇偵測 — 第二小節那個密度門檻做法的實務版本,另有幾種不靠密度的替代取徑
- Distill:如何正確解讀 t-SNE 圖 — 互動式地示範「用眼睛看降維結果」會怎麼騙人,正好對上第四小節那三個限制
- NumPy 官方使用手冊 — 本章四段程式只依賴 numpy,查語法的第一站
- Understanding Deep Learning(MIT Press) — 本課課綱主題所本的原書出版頁(ISBN 9780262048644,2023-12 出版)
- udlbook 官方網站(作者釋出的 PDF、投影片與習題) — 原書作者維護的免費資源站(udlbook.com 會轉址到此)