三维 S 曲线、600 个点。线性降维只有一个动作:挑一个方向丢掉,把点正交投影到剩下的平面上。 丢掉方向 u 之后的重建误差,精确等于 u 这个方向上的方差——实测 0.07288847 对 0.07288847, 一位不差。 随机撒 400 个方向:平均要丢掉 0.56208,正好是总方差的三分之一; 撒两万个方向,最好的一个离 PCA 的解只差 0.62 度,一个更低的都没撞出来。
第 1 到第 12 章,每一个算法都有明确的答案要对齐:回归对数值、分类对类别、 异常检测对「像不像」。从这一章开始,题目里连「正确答案」这个词都消失了——只有一堆数据, 目标是「换一个更省地方的描述方式」。 这一节先把「降维」这个词的物理含义钉死:它就是一个正交投影,没有别的花样。 弄清了代价怎么算,后面四节才有共同的尺子。 PCA、LPP、核 PCA、拉普拉斯特征映射,四个方法争的其实是同一件事:到底该丢掉哪个方向。
一、降维只有一个动作:投影
先把最容易含糊的地方说明白。所谓「把三维降到二维」,干的事就是找一张过原点的平面, 把每个点垂直压上去;降到一维,就是找一条过原点的直线,把每个点垂直压上去。 点的个数一个没少,坐标从三个数变成两个数,这就是全部。 那为什么不干脆只留第一个坐标、扔掉后两个?因为那等于硬性规定「平面就是 xy 平面」。 数据摆在哪儿是不知道的,真正的问题是:在所有可能的平面里,哪一张压下去损失最小。 所以线性降维的完整表述是:在所有过原点的低维子空间里,挑一个让「压扁的损失」最小的。 「线性」这两个字,说的就是「只能压、不能弯」——这一条限制后面会反复咬人。 这段话说起来简单,但它有一个立刻能验证的推论: 损失只跟「丢掉哪个方向」有关,跟「保留的平面怎么摆」无关。因为三维丢一维, 丢掉的那一个方向定了,剩下的平面就定了。于是问题从「挑平面」缩成「挑一个单位向量」。
二、代价怎么量:重建误差
损失得有个数。定义很朴素:把点压到平面上,再按原路抬回去,看它离原来的位置有多远。 这个距离叫重建误差。 写成式子:原始点 x,单位方向 u,压到正交补之后再抬回来,抬回来的位置是 x 减去它在 u 上的投影。 所以残差就是 (u'x) 乘以 u,长度是 |u'x|。 把所有点的这个长度平方加起来取平均,就是我们要最小化的量。 这一步有个容易搞混的地方: 「降到二维」丢掉的是一个方向,不是两个。「降到一维」丢掉的才是两个。三维降到二维、 二维降到一维,丢的方向数分别是 1 和 2,所以重建误差也是两个不同的量级。 这一点在后面的阶梯表里会看得非常清楚。 还要记住一个前提:先减均值。 不减均值的话,所有点整体偏在一边,投影误差里就混进了「离原点多远」这个和降维无关的量。 PCA 的标准做法是先中心化,这一节所有实验也都先中心化了。
三、把代价算成一个数:被丢方向的方差
这一节最硬的一句话:丢掉方向 u 的重建误差,等于数据在 u 这个方向上的方差。 不是近似,不是「大概」,是恒等。推导只有三行:把中心化后的点写成三个正交方向上的分量之和, 投影把 u 那一项整个抹掉,剩下的残差就只剩 u 那一项,平方求和取平均,正好是「投影到 u 上的方差」。 空口无凭,用代码验:拿 PCA 解出来的最小特征方向当 u, 实测重建误差 0.07288847,这个方向上的方差 0.07288847, 协方差矩阵的最小特征值也是 0.07288847——三个数一位不差。 这句话把整个降维问题翻译成了一个纯代数问题:要最小化 u'Cu,其中 C 是协方差矩阵、 u 是单位向量。 这个形式的好处是它不再提「点」,只提一个三阶方阵,于是可以用特征分解直接给出答案, 一个循环都不用写。 反过来看也一样好用:你想保住多少方差,就等于「少丢几个方向」。降到二维只丢一个方向, 只丢那一份方差;降到一维要丢两个方向,代价立刻翻几倍。
四、随机挑方向有多亏
在解出来之前,先量一下「不做功课」的代价,心里才有底。 在三维单位球面上随便撒 400 个方向,每个都算一遍重建误差:
| 统计量 | 重建误差 |
|---|---|
| 最小 | 0.07786 |
| 中位数 | 0.49628 |
| 平均 | 0.56208 |
| 最大 | 1.29443 |
| 总方差除以 3 | 0.56951 |
| 最后两行值得盯一会儿:随机方向的平均重建误差 0.56208, | |
| 和「总方差除以 3」的 0.56951 几乎相等。这不是巧合。 | |
| 各向同性地随机挑方向,u'Cu 的期望就是迹的三分之一。 | |
| 换个说法:闭着眼睛挑一个方向丢,平均要丢掉 33.3% 的方差。 | |
| 这个数就是「不做降维、直接扔掉一列坐标」的期望表现——任何人都能做到的水平。 | |
| 最小 0.07786 和最大 1.29443 差了 16 倍,也说明挑方向这件事非常值钱:挑得好和挑得差, | |
| 代价能差一个数量级。 |
五、最优平面有闭式解
那最好的方向会不会藏在某个很怪的地方,得靠搜索才找得到? 撒两万个随机方向试一下:最好的一个是 0.07293,和 PCA 解出来的最小特征方向夹角只有 0.62 度。 两万个方向里,没有一个低于 PCA 的 0.07289。 而且随着方向数增加,最好的那个会越来越贴近 PCA 的解, 但永远只能从上方逼近——因为它就是下界本身。 道理在特征分解里:C 是实对称矩阵,它一定可以写成 三个正交方向乘以三个非负特征值。 要最小化 u'Cu,把 u 按这三个方向展开,答案就是「把全部权重压在最小特征值那个方向上」。 于是最优解是协方差矩阵最小特征值对应的特征向量,最优值是那个最小特征值。 这就是「有闭式解」的含义:不用搜、不用迭代、不用调学习率,一次特征分解就到底。 对比一下第 5 章那些要靠坐标下降一轮一轮磨的算法,这里可以说是白送。 顺带一个副产品:把 C 的三个特征向量全算出来,就得到了三个互相正交的方向, 按特征值从大到小排好——这三个方向就是下一节的主成分。 所以 PCA 不是另一个算法,它就是这一节的问题解到底之后的名字。
六、降维阶梯与有效维数
把每一级的代价排成一张表,降维这件事就彻底透明了:
| 降到几维 | 丢掉几个方向 | 最低重建误差 | 占总方差 |
|---|---|---|---|
| 3 维 | 0 | 0.00000 | 0.00% |
| 2 维 | 1 | 0.07289 | 4.27% |
| 1 维 | 2 | 0.41158 | 24.09% |
| 0 维 | 3 | 1.70853 | 100.00% |
| 「占总方差」那一列是这张表的重点:降到二维只丢 4.27%,降到一维要丢 24.09%。 | |||
| 这说明这份数据「差不多是二维的」——第三个方向几乎没有信息,丢掉它几乎不心疼。反过来说, | |||
| 从二维再往一维降,就得扔一个真正有内容的维度,代价一下跳到 24%。 | |||
| 「有效维数」这个词就是从这张表来的:累计解释比到 95% 就够了,再往后加维度, | |||
| 付的代价和拿到的信息不成比例。 | |||
| 但请注意,95% 这个门槛是人定的,不是数据定的; | |||
| 下一节会看到,「方差保住了」离「该保的都保住了」还差得远。 | |||
| 还有一件事得提前说清:这一节的「有效维数」是按方差算的,而方差只跟「离中心多远」有关, | |||
| 跟「谁挨着谁」无关。 | |||
| 如果数据的用途是找近邻、做聚类、看局部结构,那这个阶梯量的根本不是你要的东西。 | |||
| 第 13.3 和 13.5 两节,就是为了修这个错位。 |
七、动手 Demo:量出「丢掉一个方向要付多少代价」
下面这段代码只做四件事:造一份三维数据、验证「重建误差等于被丢方向的方差」、撒随机方向看平均代价、 再把两万个方向里最好的一个和 PCA 的解对上。 重点看第二段:三个数(实测重建误差、方向方差、最小特征值)打出来是一模一样的。 这一节所有结论都挂在这个恒等式上。
import numpy as np
def dmat(A, B):
return np.sqrt(np.maximum(((A[:, None, :] - B[None, :, :]) ** 2).sum(-1), 0.0))
def scurve(n=600, seed=1, span=0.75):
rg = np.random.default_rng(seed)
t = rg.uniform(-span * np.pi, span * np.pi, n)
h = rg.uniform(-1.0, 1.0, n)
X = np.column_stack([np.sin(t), h, np.sign(t) * (np.cos(t) - 1.0)])
return X, t
def recon_var(X, u): # 丢掉方向 u 后的重建误差
Xc = X - X.mean(axis=0)
flat = Xc - np.outer(Xc @ u, u)
return float(((Xc - flat) ** 2).sum(1).mean())
def dir_var(X, u): # 数据在方向 u 上的方差
Xc = X - X.mean(axis=0)
p = Xc @ u
return float(p @ p / len(X))
def pca(X, k=3):
Xc = X - X.mean(axis=0)
C = Xc.T @ Xc / len(X)
w, V = np.linalg.eigh(C)
o = np.argsort(w)[::-1]
return w[o], V[:, o]
X, t = scurve(600, 1)
C = np.cov(X.T, bias=True)
w, V = pca(X)
print('迹', np.trace(C))
print('最小特征方向', V[:, 2])
print('实测重建误差', recon_var(X, V[:, 2]))
print('该方向方差 ', dir_var(X, V[:, 2]))
print('最小特征值 ', w[2])
rg = np.random.default_rng(7)
U = rg.normal(0.0, 1.0, (400, 3))
U /= np.sqrt((U ** 2).sum(1))[:, None]
rv = np.array([recon_var(X, u) for u in U])
print('随机方向 最小 %.5f 中位 %.5f 平均 %.5f 最大 %.5f'
% (rv.min(), np.median(rv), rv.mean(), rv.max()))
print('迹除以3', np.trace(C) / 3.0)
U2 = rg.normal(0.0, 1.0, (20000, 3))
U2 /= np.sqrt((U2 ** 2).sum(1))[:, None]
rv2 = np.array([recon_var(X, u) for u in U2])
b = int(np.argmin(rv2))
print('两万个里最好 %.5f' % rv2[b])
print('与PCA最小方向夹角 %.2f度'
% np.degrees(np.arccos(min(1.0, abs(U2[b] @ V[:, 2])))))
for k in (3, 2, 1, 0):
print('降到 %d 维 最低重建误差 %.5f 占总方差 %.2f%%'
% (k, w[k:].sum(), w[k:].sum() / np.trace(C) * 100.0))
实跑输出:
--------------------------------------------------------------------
线性降维丢掉的那部分,正好是被丢方向上的方差
--------------------------------------------------------------------
数据 600 个三维点,S 曲线,t 在正负 0.75π 之间对折
协方差矩阵(三个方向两两之间的相关强度):
行 列 x y z
-------------------------------------
x 0.60465 0.01229 -0.60645
y 0.01229 0.33920 -0.02069
z -0.60645 -0.02069 0.76468
迹 1.70853 <- 三个方向的方差加起来
这一节只有一句话要记住:
**线性降维=找一个低维平面把点压上去;压不掉的那部分(重建误差),正好等于「被丢掉的那个方向上的方差」。**
先拿一个具体方向验一遍,别信我,看数:
方向 PCA 解出来的最小特征方向 -0.75162 -0.01655 -0.65939
实测重建误差 0.07288847
该方向的方差 0.07288847
最小特征值 0.07288847
三个数一模一样。这不是巧合:把 X = sum(z_i u_i) 代进重建误差,
正交方向上剩下的只有 u 那一项,平方和就是 u' C u。
那随便挑个方向会怎样?撒 400 个随机方向:
统计量 重建误差
----------------
最小 0.07786
中位数 0.49628
平均 0.56208
最大 1.29443
迹 / 3 0.56951
**最后两行值得盯一会儿:随机方向的平均重建误差,正好等于总方差的三分之一。**
也就是说,什么都不想、闭着眼挑一个方向丢,平均要丢掉 33.3% 的方差。
再撒两万个,看看瞎撞能撞到什么程度:
两万个里最好 0.07293
与 PCA 最小方向夹角 0.62 度
**两万个方向里没有一个低于 PCA 的 0.07289,最好的那个离 PCA 的解只差 0.62 度。**
**这就是「有闭式解」的含义:最优平面不用搜,它就是协方差矩阵最小特征向量的正交补。**
把降维当成一个阶梯来看,每一级的代价一目了然:
降到几维 丢掉几个方向 最低重建误差 占总方差
------------------------------------------------
3 维 0 0.00000 0.00%
2 维 1 0.07289 4.27%
1 维 2 0.41158 24.09%
0 维 3 1.70853 100.00%
**注意「占总方差」那一列:降到二维只丢 4.27%,降到一维要丢 24.09%。**
**换句话说,这份数据「差不多是二维的」——第三维几乎没有信息。**
这也解释了下一节的主成分分析为什么好用:它只做一件事,就是把这个阶梯算出来。
输出第一段就把这一节的骨架打出来了:实测重建误差、该方向方差、最小特征值, 三个 0.07288847 排在一起。 这不是数值巧合——它就是把残差式子展开之后必然得到的结果。 随机 400 个方向那一行,平均 0.56208 对着迹除以 3 的 0.56951,差的那一点是抽样误差。 随机挑方向平均丢掉三分之一的方差,这句听起来像经验的话, 其实是「各向同性随机向量的二次型期望等于迹除以维数」的直接推论。 两万个方向那一行,最好的 0.07293 比 PCA 的 0.07289 大一点点,夹角 0.62 度。 方向数越多,这两个数就越贴近,但永远不会更好——因为 PCA 的解就是下界。 最后那四行阶梯里,降到二维只占 4.27%、降到一维要占 24.09%,这个跳变说明第三个方向基本是空的, 第二个方向才开始有真东西。
30 秒实验
- 把
scurve(600, 1)的 span 从 0.75 改成 1.5,再跑一遍。 看协方差矩阵的第三特征值从 0.07289 涨到多少—— 折得越狠,第三维越有内容,「差不多是二维的」这句话就不成立了。 - 把
recon_var里的Xc = X - X.mean(axis=0)改成Xc = X,不中心化。 再跑一遍,三个数就对不上了。想清楚:多出来的那一块误差是什么? - 把随机方向的个数从 400 加到 4000,看平均值和迹除以 3 的差是不是变小了。 如果变小,说明那个三分之一是数学期望不是巧合。
常见误区
- 「降维就是把不重要的列删掉」。删列是降维的一个特例(丢掉的正好是坐标轴方向), 而最优方向几乎总是斜的。 这份数据的 PC1 是 [-0.752, -0.017, -0.659],三列都占——删哪一列都到不了这个效果。
- 「重建误差越小,说明降维效果越好」。重建误差只量「点离平面多远」, 不量「点之间的相对位置对不对」。 一份数据压到二维重建误差很小,也可能把两团分开的东西搅成一团。
- 「先标准化还是先降维,随便,反正都要做」。顺序有讲究:标准化必须在降维之前。 13.2 节会看到,PCA 对坐标单位极其敏感,先降维再标准化会把主成分方向弄歪。
- 「95% 的方差就够了」。这个门槛是从「保住波动」的角度定的。 如果你的用途是找近邻、做聚类, 保住了 91.6% 的方差也可能只保住 40.3% 的邻居(13.2 节实测)。
自测三问(点开对答案)
1. 三维数据降到二维,要丢掉几个方向?重建误差等于什么? —— 丢一个方向。重建误差精确等于「被丢掉的那个方向上的方差」,也就是 u'Cu,其中 C 是协方差矩阵。 这道题的关键是别把「丢一个方向」和「保留两个方向」混成两件事——三维到二维只丢一个。 2. 为什么随机挑方向平均要丢掉三分之一的方差? —— 因为各向同性地随机取单位向量 u,u'Cu 的期望等于迹除以维数。三维就是迹的三分之一。 这不是这份数据的特性,是任何数据都成立的恒等式。 3. 「最优方向有闭式解」在实际工程里意味着什么? —— 意味着不需要搜索、不需要迭代、不需要调学习率,一次特征分解就得到全局最优。 代价是 O(d³) 的特征分解,d 是原始维数。 撒两万个方向只能逼近它、不能超过它,这一点在 Demo 里已经量过了。这一节只解决了「怎么挑一个方向」,而且只用了方差这一个标准。 下一节把三个方向一起解出来,看看按方差排出来的坐标到底长什么样,再看它在什么情况下会骗人。
- ☐ (本节完,下一节 13.2 13.2)