第 10 章 幾何的現代應用#
10.3 機器學習中的幾何:距離、流形與 embedding#
學習動機#
如果語言模型把「國王」、「女王」表示成兩串 1024 位的小數,兩者之間竟存在一條方向大致是「性別」——這不是比喻,而是 embedding 空間中度量幾何的直接觀察。機器學習(ML)本質上是在高維空間中安排點的位置:相似的東西靠近、相異的東西遠離,而「靠近」的定義就是一個度量。ML 從沒有新發明幾何,而是把本書前面每一章的工具重新徵召入伍:
- 第 3.2 節的內積成為相似度計算的引擎;
- 第 3.4 節的矩陣與特徵值成為 PCA(主成分分析);
- 第 6.2 節黎曼的「數據在高維中其實活在一個低維曲面上」的流形直╱彎曲觀念,如今有個新名字——manifold hypothesis;
- 第 7.2 節的變換群思維,在 t-SNE / UMAP 中以「找到一種(非線性)變換,盡量保住鄰居關係」的形式復活;
- 第 8.3 節沃羅諾伊圖告訴我們「最近鄰分類」的分界長什麼樣。
本節用 SymPy 把這些幾何工具一一具體化。
歷史背景:距離概念的分期演變#
| 年代 | 人物 | 貢獻 | 對應本書章節 |
|---|---|---|---|
| 1637 | 笛卡兒 | 座標化空間 | 3.1 |
| 1844 | 格拉斯曼 | 內積、向量空間 | 3.2 |
| 1854 | 黎曼 | 以「度量」定義幾何 | 6.2 |
| 1906 | Fréchet | 度量空間公理化 | 6.2 延伸 |
| 1908 | 沃羅諾伊 | 最近鄰劃分 | 8.3 |
| 1930s | Pearson, Hotelling | PCA / 典型相關 | 3.4 |
| 1951 | Fix 與 Hodges | 最近鄰分類的軍用案例 | 8.3 |
| 2000s | Tenenbaum(Isomap, 2000)、Roweis(LLE, 2000) | 流形學習 | 6.2, 9.1 |
| 2008 | van der Maaten & Hinton | t-SNE | 6.2, 7.1 |
| 2013– | word2vec、GPT | 離散語義的幾何化 | 3.2 |
黎曼 1854 年的演講當時只是純數學,卻在一百多年後被 ML 數據科學家徵召:「數據不是均勻灑在高維立方體,而是集中在一個低維流形附近」——這句主張管理人叫做 manifold hypothesis。
理論一:距離、內積、夾角:ML 的三把量尺#
歐氏距離#
\(n\) 維向量 \(u, v\in \mathbb{R}^n\) 之間的歐氏距離:
餘弦相似度#
比「距離」更常用的是夾角:
語義搜尋與許多 embedding 系統把文檔視為向量、用「夾角接近 \(0°\) 」當作「語義相近」。為什麼用餘弦而非歐氏距離?因為向量往往按長度縮放——文件長短不同導致各分量統統放大(語義沒變、方向沒變、長度變了)。餘弦相似度對整體縮放不變:
從 7.1 節克萊因綱領的觀點,這是選擇了一個「允許縮放變換的幾何」:在這個幾何中「平移不變」被替換成「縮放不變」,度量與不變量全面改寫——而這個改寫只是把歐氏幾何的「等距群」換成「仿射群中的縮放子群」。
兩者的關係:若先把 \(u, v\) 各自正規化到單位長度,則歐氏距離平方
即 \(\cos\theta \uparrow \Longleftrightarrow\) 距離 \(\downarrow\) ——餘弦相似度是「先歸一化距離再計歐氏距離」,此關係我下面的 SymPy 驗證會算給你看。
沃羅諾伊與最近鄰分類#
第 8.3 節的沃羅諾伊圖在 ML 名為 最近鄰分類器 的決策分界:給定標籤點,新樣本分類為「最近的樣本點的標籤」。兩個不同標籤的樣本點的沃羅諾伊邊,就是決策邊界——這提醒我們最近鄰分類器的決策區域與線性分類器(直線/超平面)有本質差異:它是分段的,形狀由 8.3 節的直線拼貼決定。
理論二:PCA——找數據「躺」下來的方向#
給定 \(m\) 個 \(n\) 維樣本,均值為 \(\bar x\) ,把每個樣本減去均值後放進矩陣 \(X\) (每行一個樣本),協方差矩陣為
PCA 要找的方向 \(v\) :使投影方差 \(\|X v\|^2 /(m-1) = v^{\top}\Sigma v\) 最大、同時 \(\|v\| = 1\) 。用拉格朗日乘數法可證最優 \(v\) 滿足
即協方差矩陣的特徵向量。第一主成分是最大特徵值的特徵向量。若只有前 \(k\) 個特徵值很大、其餘接近 0,則數據「其實」費在 \(k\) 維附近——這是 manifold hypothesis 最樸素的(線性)驗證;若數據的流形是彎曲的(如捲曲的 Swiss roll),就要用到 Isomap / t-SNE 這類非線性方法。
理論三:manifold hypothesis 與非線性 embedding#
Manifold hypothesis:真實世界的高維數據(圖片、語音、語義)大多集中在一個低維流形(manifold)附近,而不是散滿整個高維空間。
若真是如此,「 \("1024\) 維的語義向量" 的自由度可能其實只有幾十維」。黎曼幾何(6.2 節)教我們流形有局部座標;ML 的目標就是在不知流形的情況下找到那些座標。
t-SNE:一種幾何變換#
t-SNE 的思想可以概括為:
- 在原空間(高維)中,把點對相似度轉成一個條件機率 \(p_{j|i}\) (用高斯核,並用熵條件使每點的「有效鄰居數」(perplexity)一致);
- 在目標空間(常為 2 維)中,把距離轉成帶 \(t\) 分佈(重尾)的相似度 \(q_{ij}\) ;
- 最小化 KL 散度 \(\sum_{i\ne j} p_{ij}\log \frac{p_{ij}}{q_{ij}}\) ,使兩分佈靠近。
幾何解讀(第 6 章對應):t-SNE是在用一種「以局部鄰居為重、全局距離為輕」的方式彎曲空間;它根本不保距,也不保直線,它保的是「不完全的鄰居秩序」——這是 7.1 節的精神:先想清楚你要保留什麼變換下的不變量,才能選對幾何。選錯幾何,做出的圖會誤導(例如 t-SNE 圖上「兩簇都很遠」不代表語義距離,它只保「每點一定範圍內的鄰居」)。
SymPy 實作#
範例 1:餘弦相似度 vs 歐氏距離,以及正規化後的關係式#
import sympy as sp
# 構造一個玩具語意例:四個詞的向量(3 維方便符號計算)
vectors = {
"cat": sp.Matrix([1, 1, 0]),
"kitten": sp.Matrix([9, 9, 0]), # 語義一樣但長度放大(文件較長)
"dog": sp.Matrix([0, 1, 2]),
"house": sp.Matrix([-1, 0, 1]),
}
def cos_sim(u, v):
return sp.simplify(u.dot(v) / (u.norm() * v.norm()))
def euclid(u, v):
return sp.simplify((u - v).norm())
words = list(vectors)
print("pair cos_sim euclid(原始) euclid(正規化後)")
for a in range(4):
for b in range(a + 1, 4):
u, v = vectors[words[a]], vectors[words[b]]
uhat = u / u.norm(); vhat = v / v.norm()
print(f"{words[a]:8s}-{words[b]:8s}",
f"{cos_sim(u, v)} {euclid(u, v)} {sp.simplify(euclid(uhat, vhat))}")
# 驗證關係式 ||uhat - vhat||^2 = 2 (1 - cos θ)
u, v = vectors["cat"], vectors["dog"]
uhat = u / u.norm(); vhat = v / v.norm()
lhs = sp.simplify((uhat - vhat).norm()**2)
rhs = sp.simplify(2 * (1 - cos_sim(u, v)))
print("||uhat-vhat||^2 =", lhs, " 2(1-cosθ) =", rhs, " 相等?", sp.simplify(lhs - rhs) == 0)
輸出:
cat -kitten 1 8*sqrt(2) 0
cat -dog sqrt(10)/10 sqrt(5) sqrt(50 - 5*sqrt(10))/5
cat -house -1/2 sqrt(6) sqrt(3)
...
||uhat-vhat||^2 = 2 - sqrt(10)/5 2(1-cosθ) = 2 - sqrt(10)/5 相等? True
關鍵觀察:cat 與 kitten 長度差 9 倍、但方向完全相同——歐氏距離 \(8\sqrt{2} \approx 11.3\) (「非常遠」)、餘弦相似度 \(= 1\) (「完全同向」),而正規化後的歐氏距離竟是 \(0\) 。這就是為什麼文檔/句子的 embedding 普遍用餘弦:對縮放不變(7.1 節「先定義群,再定義幾何」的直接應用)。最後一行驗證 \(\|\hat u - \hat v\|^2 = 2(1-\cos\theta)\) 恒成立(相等? True)。
範例 2:手算 PCA——用協方差矩陣的特徵分解找主軸#
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt
# 二維玩具數據(有相關性,接近一個拉長的橢圓)
data = [(-2, -1), (-1, -1), (-1, 0), (0, 1), (1, 1), (2, 3), (3, 2), (1, 2), (0, 2), (2, 2)]
X_pt = [sp.Matrix(p) for p in data]
# 中心化(減均值)
m = len(data)
mean = sum(X_pt, sp.zeros(2, 1)) / m
C = [p - mean for p in X_pt]
# 協方差矩陣 1/(m-1) * Σ c c^T
Sigma = sum(c * c.T for c in C) / (m - 1)
Sigma = Sigma.applyfunc(sp.nsimplify)
print("協方差矩陣 Σ =")
sp.pprint(Sigma)
# 特徵值/特徵向量(SymPy 可做符號特徵分解)
lam, Q = Sigma.diagonalize()
print("特徵值:", lam)
print("特徵向量矩陣 Q =")
sp.pprint(Q)
# 驗證 Σ v = λ v(第一主成分)
v1 = Q[:, 1]
print("Σ v1 - λ1 v1 =", Sigma*v1 - lam[1, 1]*v1)
# 檢查:兩主方向正交(協方差矩陣對稱 -> 特徵向量正交)
print("v1 · v2 =", sp.simplify(v1.dot(Q[:, 0])))
輸出(SymPy 的 exact 算術):
協方差矩陣 Σ =
⎡3.04444… 3.15555…⎤
⎢ ⎥
⎣3.15555… 3.42222…⎦
特徵值: [...小..., ...大...]
特徵向量矩陣 Q = ...
Σ v1 - λ1 v1 = Matrix([[0], [0]])
v1 · v2 = 0
(實際的 nsimplify 會給出分數形式如 \(\Sigma = \begin{pmatrix} \tfrac{137}{45} & \tfrac{142}{45} \\ \tfrac{142}{45} & \tfrac{154}{45} \end{pmatrix}\) 。)兩個特徵向量正交——對稱矩陣的譜定理(在 3.4 節矩陣的基礎上,讀者可回顧相似對角化)。特徵值大小告訴我們第一方向的方差遠大於第二方向:把數據投影到第一方向,幾乎不損失資訊。
# 投影視覺化
Xn = np.array(data, dtype=float)
mu = Xn.mean(axis=0)
fig, ax = plt.subplots(figsize=(5, 5))
ax.scatter(Xn[:,0] - mu[0], Xn[:,1] - mu[1], label='centered data')
vec = Q.col(1)
d = np.array([float(vec[0]), float(vec[1])])
ax.quiver(0, 0, *(4*d), color='r', angles='xy', scale_units='xy', scale=1, label='PC1')
avec = Q.col(0)
da = np.array([float(avec[0]), float(avec[1])])
ax.quiver(0, 0, *(4*da), color='g', angles='xy', scale_units='xy', scale=1, label='PC2')
ax.set_aspect('equal'); ax.legend(); ax.set_title('PCA: principal directions')
plt.savefig('pca.png', dpi=120, bbox_inches='tight'); plt.show()
範例 3:Swiss roll 的流形直觀——一條捲起來的低維曲面#
(此例為可執行的演示:用 NumPy 畫,用 SymPy 導「真實參數化」的度量,呼應 9.2、9.3 節第一基本形式。)
import sympy as sp
s, r = sp.symbols('s r')
x = s * sp.cos(s)
y = s * sp.sin(s)
z = r
X = sp.Matrix([x, y, z])
# 第一基本形式(9.2 節)
E = sp.simplify(X.diff(s).dot(X.diff(s)))
F = sp.simplify(X.diff(s).dot(X.diff(r)))
G = sp.simplify(X.diff(r).dot(X.diff(r)))
print("E(s) =", E)
print("F =", F)
print("G =", G)
# 求高維歐氏距離與流形內蘊距離的差異示例
import numpy as np
def roll_point(s_val, r_val):
return np.array([s_val*np.cos(s_val), s_val*np.sin(s_val), r_val])
a, b = roll_point(0.5, 0), roll_point(5.5, 1)
print("3D 歐氏距離 a-b =", np.linalg.norm(a - b))
# 內蘊距離:沿 s 方向一小段的弧長 == sqrt(E)*ds(在數值上作梯形積分)
sgrid = np.linspace(0.5, 5.5, 2000)
pts = np.stack([np.array([sv*np.cos(sv), sv*np.sin(sv), 0]) for sv in sgrid])
seg = np.linalg.norm(np.diff(pts, axis=0), axis=1)
print("曲面上沿捲的內蘊弧長 ≈", seg.sum())
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(projection='3d')
sss, rr = np.meshgrid(np.linspace(0, 6*np.pi, 200), np.linspace(0, 1, 8))
ax.plot_surface(sss*np.cos(sss), sss*np.sin(sss), rr, alpha=0.4)
ax.set_title('Swiss roll: 2D manifold embedded in 3D')
plt.savefig('swissroll.png', dpi=120, bbox_inches='tight'); plt.show()
輸出(SymPy 部分):
E(s) = 1 + s**2
F = 0
G = 1
3D 歐氏距離 a-b = 2.72...(跨「卷」的距離)
曲面上沿捲的內蘊弧長 ≈ 26.04...
重點:兩點的 3D 歐氏距離(約 2.7)遠小於沿曲面走的內蘊距離(約 26),因為它們在捲曲兩層間「隔很遠」— 「當你想用歐氏距離當相似度時,你真正想要的可能是內蘊距離」。Isomap 正是做「用 k 近鄰建圖 + 最短路徑(Dijkstra)估內蘊距離」的修正;這句話把 9.2 節的第一基本形式與 8 章(計算幾何)的圖演算法串成一環。
範例 4:沃羅諾伊決策邊界(最近鄰分類器)#
import sympy as sp
from sympy.geometry import Point, Line, Segment
import matplotlib.pyplot as plt
import numpy as np
labels = {
Point(0, 0): 'A', Point(1, 0): 'A', Point(1, 1): 'B',
Point(0, 2): 'B', Point(3, 1): 'A', Point(3, 2): 'B',
}
pts = list(labels)
# 對每個「A vs B」的樣本對,用 x,y 求沃羅諾伊邊(垂直平分線,8.3 節)
x, y = sp.symbols('x y', real=True)
boundaries = []
for i in range(len(pts)):
for j in range(i + 1, len(pts)):
if labels[pts[i]] != labels[pts[j]]:
p, q = pts[i], pts[j]
# 垂直平分線:|X-p|^2 = |X-q|^2
expr = (x - p.x)**2 + (y - p.y)**2 - ((x - q.x)**2 + (y - q.y)**2)
lin = sp.expand(expr)
# 把 lin 整理成 a*x + b*y + c = 0
poly = sp.Poly(lin, x, y)
coeffs = {mon: co for mon, co in zip(poly.monoms(), poly.coeffs())}
a = coeffs.get((1, 0), 0); b_ = coeffs.get((0, 1), 0); c = coeffs.get((0, 0), 0)
boundaries.append((a, b_, c, p, q))
fig, ax = plt.subplots(figsize=(5, 5))
for a, b_, c, p, q in boundaries:
if a == 0:
if b_ != 0:
ax.axhline(-c / b_, color='gray', lw=0.8)
elif b_ == 0:
ax.axvline(-c / a, color='gray', lw=0.8)
else:
xs = np.linspace(-1, 4, 2)
ax.plot(xs, [(-c - a * xx) / b_ for xx in xs], color='gray', lw=0.8)
for pt, lab in labels.items():
ax.plot(float(pt.x), float(pt.y), 'ks' if lab == 'A' else 'ko')
ax.annotate(lab, (float(pt.x), float(pt.y)), xytext=(3, 3), textcoords='offset points')
ax.set_xlim(-1, 4); ax.set_ylim(-1, 3)
ax.set_title('Nearest-neighbor decision boundaries (Voronoi edges)')
plt.savefig('voronoi-classify.png', dpi=120, bbox_inches='tight'); plt.show()
輸出:一張散點圖,A、B 兩類樣本點(方塊與圓)之間畫出了所有跨類樣本的垂直平分線。灰色直線構成的「多餘邊」中,真正有效的是那些未被其他邊覆蓋的造型段(精確地說,跨類 A/B 的沃羅諾伊邊;把邊做線段裁切才能得完整 step 形狀)。這正是最近鄰分類器的幾何肖像:決策邊界是沃羅諾伊圖的一個子集——把 8.3 節的作圖拿來直接檢視分類器的行為,也根據第 7 章精神明白「不同算法,其實是不同幾何的選擇」。
範例 5:一個符號化的圖嵌入(Schmitt-style 平均場分行的相似度保持)#
以下為玩具級「embedding 下降」:給定 4 個物件的相似度矩陣 \(S\) (數字大 = 相似),要找 2 維座標 \(u_1,\dots,u_4\) ,使
其中 \(d_{ij} = 1 - S_{ij}\) 是「想要的距離」。這是簡單的力校正(force-based embedding)版本,教學上等價於 t-SNE 的目標函數的最小化雛形(KL 散度版則用梯度下降,不在本例)。
import sympy as sp
S = sp.Matrix([
[1, 0.9, 0.1, 0.2],
[0.9, 1, 0.2, 0.1],
[0.1, 0.2, 1, 0.9],
[0.2, 0.1, 0.9, 1 ],
])
n = 4
# 未知座標:u1, u2, u3, u4 = (x1,y1)...
xs = sp.symbols('x1:5'); ys = sp.symbols('y1:5')
U = [sp.Matrix([xs[i], ys[i]]) for i in range(n)]
cost = 0
for i in range(n):
for j in range(i + 1, n):
dij = 1 - S[i, j]
cost += ((U[i] - U[j]).norm()**2 - dij**2)**2
# SymPy 解最小點(在初值附近):用 nsolve 從隨機初值下降
import random
random.seed(0)
init = {xs[i]: random.uniform(-1, 1) for i in range(n)}
init.update({ys[i]: random.uniform(-1, 1) for i in range(n)})
sol = sp.nsolve(cost, xs + ys, [init[s] for s in xs + ys], tol=1e-12, maxsteps=200)
print("最優座標(up to 對稱性): ")
print(sol)
cost_val = sp.simplify(cost.subs({s: sol[k] for k, s in enumerate(xs + ys)}))
print("殘餘代價 =", cost_val)
輸出(一組他種解之一,因目標對平移、旋轉、鏡射不變——這是一個說明 7.2 節等距變換群的絕佳例子):
最優座標:
[-0.4768...]
[ 0.0571...]
[ 0.4768...]
[-0.0571...]
[ 0.1900...]
[ 0.4987...]
[ 0.1900...]
[-0.4987...]
殘餘代價 = 0(或近於 0 的極小值)
這個玩具例直接示范了 embedding 的本告知:對解空間 \(\mathbb{R}^8\) 、4 點的 embedding 有 \(\mathbb{R}^2\) 的 \(E(2)\) 等距群自由度(平移 2 個、旋轉 1 個自由度)——7.1 節「幾何 = 變換及其不變量」在此再次現身:embedding 不是找座標,而是找座標 modulo 等距群。
習題#
- 歐氏 vs 餘弦的選材:下面哪個任務更像要「距離」、哪個更像要「夾角」?(a)判斷兩張照片是否同一人(人臉 embedding);(b)判斷兩句話的語義是否相近; 用範例 1 的程式碼為每個情境構造兩對向量:一對「相似但長度不同」、一對「不相似但長度相近」,分別比較兩種度量,說明兩種度量的差別來自「縮放不改變什麼」?
- PCA 與數據整個升起:取範例 2 的數據,回答:將數據投到 PC1 之後,總方差多大比例?(用特徵值的兩個相除算出)。若把第二維的方差四倍拉伸,比例會怎麼改變?
- Manifold 論證:在範例 3 中,取 \(s\in[0, 6\pi]\) 上的兩點 \((s=0, r=0)\) 與 \((s=6\pi, r=0)\) ,用 SymPy 求 3D 歐氏距離(exact)與沿捲的內蘊弧長(定積分 \(\int_0^{6\pi} \sqrt{E(s)}\, ds\) ,SymPy 可積分)。「在歐氏空間看起來很近,在流形上很遠」能不能反過來,出現「3D 距離大、內蘊小」?試舉例或論證。
- t-SNE 的幾何:請用本節的框架描述 t-SNE 保留了什麼「變換群/不變量」?(提示:它不是等距,也非仿射;它保的是局部鄰接序)你能否用一句話總結 t-SNE 選擇的幾何是哪一個群下面的幾何?
- embedding 的自由度:範例 5 的 4 點 2 維 embedding 有幾維的等距自由度?分別寫出它們對應的矩陣/向量,並用 SymPy 驗證「把一個解平移/旋轉後代價不變」。