降維:設計一組真的抓得到錯的驗證
這一關你要叫 agent 手刻 PCA,然後證明它對不對。聽起來很簡單。 但你會發現,一組看起來很完整、跑起來全部 PASS 的驗證, 可能對某一類錯誤完全免疫。
先轉一轉:把平面上的點投影到一條線
假設每個點代表一筆只有兩個特徵的資料。現在只准用一個數字描述它:把點垂直投影到藍線,記下沿線的位置。你會把線轉向哪裡,讓投影後的點盡量分散?
- 投影變異
- 平均重建平方距離
拖曳滑桿,也可用鍵盤方向鍵調整。先找出你認為最好的方向,再按「轉到主成分方向」比較。
主成分方向保留最多變異,也讓重建的平方距離總和最小。這裡的「重建」是把線上的位置放回平面:它仍在投影線上,通常不會回到原來的點。
把剛才看到的圖寫成公式
這組固定點雲的平均為零。令單位方向 $u=(\cos\theta,\sin\theta)^\top$,第 $i$ 點投影的位置是 $z_i=x_i^\top u$,重建為 $\hat x_i=z_i u$。
上方分別顯示樣本變異 $\sum_i z_i^2/(n-1)$ 與平均平方距離 $\sum_i\|x_i-\hat x_i\|^2/n$。兩者分母不同,不能直接相加;方向最佳化的結論不受這兩個固定分母影響。
把方向反過來,$u$ 換成 $-u$,投影座標會變號,但重建位置與這兩個指標都不變。稍後比對兩種 PCA 實作時,這點很有用。
從投影接到 PCA
剛才是二維變一維;接下來每張手寫數字有 64 個像素,我們要保留 10 個方向。先讀懂「減去平均、找方向、投影」這三步,再讓 agent 寫程式。還沒學過特徵向量也可以先跟著做,下方會補上意義。
PCA 在做什麼?為什麼要降維?
每張手寫數字圖是 64 維向量。但這 64 個像素並不獨立。 左上角那個像素幾乎永遠是 0,中間幾個像素則高度相關(筆畫是連著的)。 換句話說,資料實際上只「住」在一個維度低得多的子空間附近。
PCA 要做的就是找出那個子空間:在 $\mathbb{R}^{64}$ 裡找 $K$ 個互相正交的方向, 讓資料投影上去之後保留最多的變異。
這裡會用到哪些線性代數概念
向量代表一筆資料,矩陣把許多筆資料排在一起。若還沒學到特徵向量,可以先把它理解為:矩陣作用後,只會縮放、不會改變所在直線的方向;縮放倍數就是特徵值。
| PCA 用到的 | 對應的線代概念 |
|---|---|
| 共變異數矩陣 $C$ | 對稱矩陣 |
| 主成分 | $C$ 的特徵向量 |
| 各主成分的重要性 | 對應的特徵值 |
| 主成分彼此垂直 | 對稱矩陣的特徵向量可取為正交 |
| 投影與重建 | 正交投影 $\mathbf{x}\mapsto W^\top W\mathbf{x}$ |
這些概念不必一次記熟。先掌握 PCA 要保留資料變化最大的方向,再練習「怎麼驗證別人寫的實作」。特徵分解的細節可以配合線性代數課程回來閱讀。
共變異數矩陣是什麼?為什麼要先中心化?
先把每一行(每個像素)減掉它自己的平均,得到中心化的 $X_c$。 然後 $C=\frac{X_c^\top X_c}{n-1}$ 是一個 $64\times 64$ 的矩陣,其中
$$C_{jk}=\frac{1}{n-1}\sum_{i=1}^{n}(x_{ij}-\bar x_j)(x_{ik}-\bar x_k)$$- 對角線 $C_{jj}$:第 $j$ 個像素自己的變異數,也就是它「變化得多劇烈」
- 非對角線 $C_{jk}$:第 $j$ 與第 $k$ 個像素一起變動的程度。 正的代表「一個亮另一個也傾向亮」,負的代表相反,接近 0 代表線性共同變動較弱,仍可能有非線性關係
為什麼一定要先中心化
不減平均的話,$X^\top X$ 量到的是「離原點多遠」, 而不是「離資料中心多分散」。資料平均的位置也會影響求出的方向, 這時找出的就不一定是「相對資料中心,變異最大的方向」。這一步漏掉是手刻 PCA 最常見的錯誤之一。
「解釋變異比例」是什麼意思?
把 $C$ 的特徵值由大到小排成 $\lambda_1\ge\lambda_2\ge\cdots$。 每個 $\lambda_j$ 就是資料在第 $j$ 個主成分方向上的變異數。
對稱矩陣的跡等於特徵值總和,而這個跡剛好是所有像素的變異數總和,所以
$$\text{第 }j\text{ 個主成分的解釋變異比例}=\frac{\lambda_j}{\sum_{m}\lambda_m}$$意思是「整體變異裡,有百分之幾是由這個方向貢獻的」。 在這份資料中,第一個主成分約占 14.89%,前十個加起來約 73%。 換句話說,用 10 個數字就能保留原本 64 個數字約七成三的變異。
注意這是個比例。待會會看到,正因為它是比例, 分母上的常數會被約掉。那個驗證盲點就是這麼來的。
會看到的 numpy 寫法
| 寫法 | 意思 |
|---|---|
A @ B | 矩陣乘法(不是逐元素相乘)。A * B 才是逐元素 |
A.T | 轉置 $A^\top$ |
X - mu | X 是 (1797,64)、mu 是 (64,),numpy 會自動把 mu 對每一列廣播相減(broadcasting) |
np.linalg.eigh(C) | 對稱矩陣專用的特徵分解。回傳的特徵值是由小到大,所以要自己反轉 |
np.argsort(v)[::-1] | 取得「由大到小」的索引順序 |
W[:K] | 取前 $K$ 列 |
用 eigh 而不是 eig 是有理由的:共變異數矩陣是對稱的,
eigh 利用這個性質,數值更穩、而且保證回傳實數特徵值。
用 eig 可能會拿到帶極小虛部的複數,然後你會花半小時懷疑人生。
PCA 的定義:對中心化後的資料 $X_c$,計算共變異數矩陣
取 $\Lambda$ 中最大的 $K$ 個特徵值對應的特徵向量,就是前 $K$ 個主成分。
一個「正確的 PCA 實作」至少要做對這幾件事:中心化對、分母對、特徵分解對、由大到小排序對、取前 $K$ 個對。 你的任務是設計檢查,盡可能抓出這些步驟中的錯誤,並留意每項檢查仍可能漏掉什麼。
動手
要求 agent 手刻
手刻一個 PCA,取前 10 個主成分,資料用 sklearn 內建的 digits。
要求:
- 只能用 numpy:中心化、算共變異數矩陣、np.linalg.eigh、排序、取前 10 個
- 不可以呼叫 sklearn.decomposition.PCA 來算(它只能當對照組)
- 寫成 lab2_pca.py,用 uv run python 執行
寫完之後,請你自己設計驗證,證明你的實作跟 sklearn 的 PCA 一致,
並且把每一項驗證的實際數值印出來。
注意最後一句:驗證方法是叫 agent 自己想的。 這是刻意的:先看看它會想出什麼。
第一個坑:它可能會直接相減
如果 agent 直接逐項比對 components_,可能得到下面的結果。你的模型回覆與末位數字不必完全相同:
如果 agent 因此直接修改 PCA 程式,先停下來檢查比較方式。打斷它(按 Esc 或 Ctrl+C),
然後問它一個問題:
「$v$ 是 $C$ 的特徵向量,那 $-v$ 是不是?如果是,你的比較方法有什麼問題?」
關鍵在於 $Cv=\lambda v \Rightarrow C(-v)=\lambda(-v)$。 特徵向量的正負號是任意的,兩個實作各自挑了不同的號,逐項相減當然差很大。
換成正確的比法
兩邊的主成分都已正規化,所以比它們的夾角餘弦絕對值:
剛才那個比較方法是錯的,因為特徵向量的正負號任意。
請改成下面四項驗證,每一項都印出實際數值與 PASS/FAIL:
1. 解釋變異比例:evals[:10]/evals.sum() vs explained_variance_ratio_
2. 主成分方向:|cos| 應該都接近 1,並列出哪幾個方向相反
3. 重建誤差:(Xc @ W.T) @ W + mu 的 MSE vs sklearn inverse_transform 的 MSE
4. 正交性:W @ W.T 應該接近單位矩陣
四關全過。看起來可以收工了。
但是這組驗證有一個洞
現在做一件事:故意把程式寫錯,把共變異數的分母從 $n-1$ 改成 $n$,然後重跑那四關。
請做一個實驗:把共變異數矩陣的分母從 (n-1) 改成 n,
其他都不動,重新跑那四項驗證,把結果印出來。
這不是巧合,是數學上的必然。
設 $C'=cC$,其中 $c=\frac{n-1}{n}$ 是一個正常數。那麼:
- 特徵向量完全不變 → 關卡 2(方向)、關卡 4(正交)必然通過
- 特徵值同乘 $c$,但比例 $\dfrac{c\lambda_i}{\sum_j c\lambda_j}=\dfrac{\lambda_i}{\sum_j\lambda_j}$ → 關卡 1 中 $c$ 被約掉
- 重建 $X_cW^\top W+\mu$ 只用到特徵向量,跟特徵值大小無關 → 關卡 3 通過
四道關卡沒有任何一道會去看特徵值本身的數值, 而分母錯誤剛好只影響特徵值的尺度。所以這組驗證對這個錯誤完全免疫。
補上第五關
再加一個關卡 5:直接比對特徵值本身,
evals[:10] 應該等於 sklearn 的 pca.explained_variance_。
兩個版本(n-1 和 n)都跑一次。
「驗證全部通過」只代表「這組驗證抓不到錯」,不代表程式正確。
設計驗證的時候,該問的不是「我檢查了幾項」, 而是「什麼樣的錯誤會從我的檢查縫隙中溜過去」。 以後檢查作業或自己的數學程式,也可以用同樣的方式思考。
兩個必須講清楚的但書
但書一:$|\cos|$ 不是永遠有效
$|\cos|$ 比對成立的前提是特徵值單純(沒有重根)且間隔夠大。 如果 $\lambda_i=\lambda_{i+1}$,那個二維特徵空間裡任何一組正交基底都是合法的特徵向量。 兩個都正確的實作,逐向量 $|\cos|$ 甚至可以是 0。
請印出前 11 個特徵值,並計算相鄰特徵值的最小間隔,
告訴我這份資料有沒有接近重根的問題。
digits 剛好安全。但如果你之後換一份資料,務必先檢查這件事。 若要處理重複特徵值,應把對應的整個區塊一起比較,例如比較投影矩陣 $W^\top W$。還要確認保留的第 $K$ 與第 $K+1$ 個特徵值有間隔;若剛好從重根區塊中間切開,兩個都最佳的 $K$ 維子空間也可能不同,不能要求投影矩陣相等。
但書二:關卡 4 只是必要條件
$WW^\top\approx I$ 確實不需要對照組,但它只證明「這 $K$ 個向量彼此正交且長度為 1」。 隨便一組正交基底都會通過,包括「不小心取到最小的那 10 個特徵向量」這種錯誤。 要完整驗證還得檢查特徵方程殘差 $CW^\top-W^\top\Lambda$、特徵值大小與取用順序。
檢查清單
如果還有時間
- 問 agent:
pca._fit_svd_solver是什麼?你會發現預設auto在這份資料上選的是covariance_eigh,跟手刻是同一種演算法。所以這根本不算「兩種演算法交叉驗證」。要真的交叉驗證必須明寫svd_solver='full'。 - 設計一個「取錯順序」的錯誤(取最小的 10 個特徵值),看看五道關卡各自會不會抓到。
- 把前 10 個主成分 reshape 回 8×8 畫出來,看看 PCA 學到了什麼樣的筆畫結構。