🤖 第13章 无监督降维
📄 一句话概括

拉普拉斯特征映射只做一件事:求归一化拉普拉斯矩阵最小几个非零特征值对应的特征向量。 第一个特征值恒为 0,扔掉;第二个就对应「把卷摊开」的坐标。 瑞士卷上,第二个特征向量与弯曲参数 t 的相关是 0.9595, 与纸宽 h 的相关只有 0.0542——它真摊开了卷的方向,另一个坐标却是废的。10 近邻保持率 0.5340。 代价是 stress 从 PCA 的 0.1029 涨到 0.5014。 而真正的低维坐标(弧长与纸宽)能保住 99.83%——三个方法离它都还远。

📝 本节和前面几节的关系

上一节的核 PCA 走的是「先升维再线性」,这一节换一条完全不同的路: 不投影、不升维,只把「谁和谁挨着」写成一张图,然后让这张图自己舒展开。 它和 13.3 节的 LPP 用的是同一张图、同一个拉普拉斯矩阵, 区别在于 LPP 要求结果是「原始坐标的线性组合」,而这里不要这个限制。 所以这一节其实是在回答一个问题:把「线性」这个镣铐摘掉之后,同一个目标函数能好多少? 答案是:近邻保持率只从 0.5440 变到 0.5340,几乎没变; 但有一个指标变了——第一维与弯曲参数的相关从 0.244 跳到 0.9595。 这件事本身就很有意思:指标没变好,但结构对了。

一、把数据看成一张弹簧网

先建立图像:每个点是一个小珠子,近邻之间用弹簧连起来,弹簧的硬度等于边权。 现在要把这张网压到一维或者二维的桌面上,要求弹簧别被拉得太狠。 如果只用弹簧的势能做目标函数,最优解是所有珠子挤成一个点——所有弹簧长度都是零。 这就是 13.3 节那个「平凡解陷阱」的几何版本。 必须加一个条件:珠子不许全挤在一起,总散开程度固定。 LPP 的做法是「找一根刚性杆,把所有珠子刚性投影到杆上」,于是杆的方向就是要求的东西, 它必须是原始坐标的线性组合。 拉普拉斯特征映射的做法是「直接给每个珠子分配一个新位置」,新位置不受任何限制, 只要弹簧别太紧。 限制一放开,新位置就可以是原始坐标的任意非线性函数——所以它能把卷摊开。 代价是新位置没有显式公式。 LPP 里有一个 3 维向量 a,新点乘一下就有坐标; 这里没有 a,坐标是「解出来的」,只对训练集里的点有定义。 新点怎么办?通常两条路:把新点插进图里重新解一遍(贵), 或者训练一个回归去拟合「原始坐标到嵌入坐标」的映射(快但多一层误差)。

二、拉普拉斯矩阵与它的谱

具体算法三步,比核 PCA 还短: ① 建图,得到边权矩阵 W 和度矩阵 D(第 i 个对角元是第 i 个点的边权之和); ② 算归一化拉普拉斯 L_sym 等于 D 的负二分之一次方 乘 (D 减 W) 乘 D 的负二分之一次方; ③ 对 L_sym 做特征分解,扔掉第一个特征向量,取接下来的 k 个,每个再乘 D 的负二分之一次方。 为什么要归一化?因为不归一化的话,度大的点(周围邻居多、边权大)在目标函数里占的权重大, 解会偏向「把稠密区压紧」,稀硫区就被牺牲掉。 归一化之后每个点的贡献被它的度除掉了,稠密区和稀疏区被平等对待。 在这份瑞士卷上,最小六个特征值是 负 0.00000、0.00029、0.00125、0.00249、0.00443、0.00777。 注意第一个是零(数值上可能是 负 1e-17 这种), 这是必然的——归一化拉普拉斯的零空间里一定有常向量。 第一和第二个之间差了一个数量级:0 到 0.00029。 这个「谱间隙」是这类方法的通用信号:间隙大,说明图是连通的、第一刀就能切出有意义的结构; 间隙小,说明图碎成了几块,前几个特征向量可能只是在不同连通块之间做指示函数。

三、为什么第一个特征向量要扔掉

因为它是常向量,所有点的坐标都一样。 把一张图映射到「所有点重合」这个解上,弹簧长度全是零,目标函数当然最小——但它什么信息都没有。 这跟 13.3 节的约束是同一个问题的两种解法:LPP 用一个硬约束(a'XDX'a 等于 1)把平凡解堵掉, 拉普拉斯特征映射干脆让平凡解自己出现(特征值为 0),然后手动扔掉它。 两种做法殊途同归,都是「别让所有点挤成一坨」。 扔掉第一个之后,第二个特征向量就是「最省力地把图拉开」的那个方向。 为什么它对应「摊开卷」?因为卷上离得远的两层在三维里挨得近,弹簧把它们连起来了, 于是向量必须在这两层之间做取舍——最优的取舍就是沿着纸面方向把它们分开。 顺带一个容易忽略的点:那个 负 0.00000 说明数值上是负数。 拉普拉斯是半正定的,理论特征值不小于 0,出现负数就是浮点误差。排序的时候它会排到最前面, 这没问题;但如果你的代码用「特征值大于 0」当筛选条件,就会把第一个漏掉或者多扔一个。

四、它摊开了什么,没摊开什么

这是这一节最该记住的一段。先看数据:

LE 的坐标 与弯曲参数 t 的相关 与纸宽 h 的相关 单独这一维的保持率
第 2 特征向量 0.9595 0.0542 0.5320
第 3 特征向量 0.0473 0.0760 0.4005
第 2 特征向量与 t 的相关是 0.9595——它确确实实沿着卷的方向把点排开了,这就是「摊平」的实证。
可第 3 特征向量与纸宽 h 的相关只有 0.0760,基本是废的。
所以「拉普拉斯特征映射能摊开流形」这句话要加个限定:它摊开了一个方向。
为什么另一维是废的?因为这张卷的「宽度方向」在三维里本来就是平的,
弹簧网在这个方向上没有张力——所有点在这一维上都已经是「摊平」的,再摊也没有收益。
于是第二维只能去抓别的东西,抓到的是一堆噪声。
这件事的教训是:别指望「取前两维」就等于「拿到两个有意义的坐标」。
拉普拉斯特征映射只保证「图上有边的点靠得近」,不保证两维分别对应两个方向。
要判断哪一维有用,得自己算相关、算保持率,不能想当然。
顺带说明一下为什么只留一维时保持率有 0.5320,而两维加起来是 0.5340——几乎没涨。
这正好印证了「第二维是废的」:加进去等于没加。
如果一个方法真的把两维都学到了,第二维应该带来明显的增量。

五、只保邻域,不保距离

三个方法放在同一张表上(同一份数据、都降到二维):

方法 10 近邻保持率 stress 第一维与 t 的相关
PCA 0.4030 0.1029 0.169
LPP 0.5440 0.1115 0.244
LE 0.5340 0.5014 0.960
注意 stress 那一列:PCA 0.1029、LPP 0.1115、LE 0.5014。
LE 保住了邻居,但把所有距离的比例全打乱了——stress 是另外两个的五倍。
原因就在目标函数里:它只在乎「图上有边的点别分开」,至于分开多远,它一个字都没提。
于是一根很短的和一根很长的弹簧,只要都不断,代价一样。
这个取舍必须记牢:要看整体形状、要量距离、要做插值,别用 LE;
要按邻域做分类、聚类、检索,LE 更合适——因为那些任务只关心「谁和谁像」。
「保住邻居」和「保住距离」是两个不同的目标,很少有方法能同时做到。
stress 0.5014 这个数具体意味着什么?
把两两距离归一化到均值 1,然后算均方根误差——0.50 意味着平均偏差半个身位。
换句话说,LE 图上的「远近」基本不能当距离读,只能当「有没有连边」读。

六、两个方法论上的坑

坑一:拿「生成数据的参数」当标准答案去评分,会把好方法判成坏方法。 瑞士卷是用参数 t 和 h 生成的,很自然会想「那正确的低维坐标不就是 t 和 h 吗」。 实测:用 (t, h) 当坐标,10 近邻保持率只有 0.4253——还不如 PCA 的 0.4030 高多少。 但卷上同样转一个角度,外圈走的路是内圈的三倍(半径就是 t,从 4.7 到 14.1), 所以等距坐标是弧长 s 等于 t²/2,不是 t 本身。 换成 (s, h),保持率跳到 0.9983。

用作低维坐标的东西 10 近邻保持率 stress
弧长 s 与纸宽 h 0.9983 0.7076
参数 t 与纸宽 h 0.4253 0.5644
LE 的两维 0.5340 0.5014
一行 0.9983、一行 0.4253,差的是「用什么当坐标」,不是方法好坏。
这件事在真实项目里天天发生:拿一个「看起来合理」的标准答案去比较,结论整个反过来。
判断标准答案对不对,
得看它是不是真的满足「近邻关系」——而不是看它是不是「生成数据时用的那个参数」。
坑二:近邻保持率这个指标不是尺度不变的。
同一份正确坐标 (s, h),原样的保持率是 0.9983;把两列缩放到同一个量级之后,掉到 0.4123。
为什么?因为 k 近邻是用欧氏距离算的,哪个轴的数值范围大,哪个轴就说了算。
s 的范围是 0 到 99.8,h 的范围是 0 到 10,所以邻居几乎完全由 s 决定——而沿着 s 方向找邻居,
正好就是原空间里的邻居,所以分数很高。
一旦把 s 压到和 h 一个量级,两个轴开始争,分数就崩了。
这就是为什么报这个数必须写清「坐标怎么缩放的」。
两个人都说自己「保持率 0.9」,一个用的原始坐标、一个用的标准化坐标,这两个数根本不能比。
这也是我在这一章里坚持把预处理方式一起写出来的原因。

七、动手 Demo:把弹簧网摊平

这段代码算归一化拉普拉斯的谱,然后做四件事:看第二个特征向量与弯曲参数的相关、 把它单独一维的保持率量出来、和 PCA/LPP 摆在一起对照、最后验「尺度敏感」这个坑。 重点看两张表。 第一张里,第 2 特征向量与 t 的相关是 0.9595,第 3 只有 0.0760。 第二张里,弧长坐标 0.9983,参数坐标 0.4253。

import numpy as np

def dmat(A, B):
    return np.sqrt(np.maximum(((A[:, None, :] - B[None, :, :]) ** 2).sum(-1), 0.0))

def swiss(n=600, seed=1):
    rg = np.random.default_rng(seed)
    t = rg.uniform(1.5 * np.pi, 4.5 * np.pi, n)
    h = rg.uniform(0.0, 10.0, n)
    return np.column_stack([t * np.cos(t), h, t * np.sin(t)]), t

def graph(X, nn=10, sig=1.0):
    D = dmat(X, X)
    np.fill_diagonal(D, np.inf)
    W = np.zeros((len(X), len(X)))
    for i, idx in enumerate(np.argsort(D, axis=1)[:, :nn]):
        W[i, idx] = np.exp(-D[i, idx] ** 2 / (2.0 * sig * sig))
    return np.maximum(W, W.T)

def le(X, k=2, nn=10, sig=1.0):
    W = graph(X, nn, sig)
    D = np.diag(W.sum(axis=1))
    L = D - W
    dm = 1.0 / np.sqrt(np.maximum(np.diag(D), 1e-12))
    Ls = (L * dm[:, None]) * dm[None, :]
    val, vec = np.linalg.eigh((Ls + Ls.T) / 2.0)
    o = np.argsort(val)
    val, vec = val[o], vec[:, o]
    return vec[:, 1:k + 1] * dm[:, None], val

def keep_ratio(X, Z, k=10):
    D1, D2 = dmat(X, X), dmat(Z, Z)
    np.fill_diagonal(D1, np.inf)
    np.fill_diagonal(D2, np.inf)
    a = np.argsort(D1, axis=1)[:, :k]
    b = np.argsort(D2, axis=1)[:, :k]
    hit = sum(len(set(a[i]) & set(b[i])) for i in range(len(a)))
    return hit / float(len(a) * k)

def stress(X, Z):
    iu = np.triu_indices(len(X), 1)
    a, b = dmat(X, X)[iu], dmat(Z, Z)[iu]
    a, b = a / a.mean(), b / b.mean()
    return float(np.sqrt(((a - b) ** 2).mean()))

X, t = swiss(600, 1)
h = X[:, 1]
Z, val = le(X, 2, 10, 1.0)
print('最小六个特征值', np.round(val[:6], 5))
for i in range(2):
    print('dim%d |corr t| %.4f |corr h| %.4f 单独保持 %.4f'
          % (i, abs(np.corrcoef(Z[:, i], t)[0, 1]),
             abs(np.corrcoef(Z[:, i], h)[0, 1]),
             keep_ratio(X, Z[:, i:i + 1])))
print('两维 保持 %.4f stress %.4f' % (keep_ratio(X, Z), stress(X, Z)))

s = 0.5 * t * t
print('弧长与纸宽 保持 %.4f' % keep_ratio(X, np.column_stack([s, h])))
print('参数与纸宽 保持 %.4f' % keep_ratio(X, np.column_stack([t, h])))
print('弧长缩到同量级 保持 %.4f'
      % keep_ratio(X, np.column_stack([s / s.std() * t.std(), h])))

实跑输出:

--------------------------------------------------------------------
拉普拉斯特征映射:一个坐标摊开了卷,另一个坐标是废的
--------------------------------------------------------------------
  数据                     600 个三维点,瑞士卷,与上一节同一份
  邻接图                   每个点连 10 个最近邻,边权 exp(-d²/2)

  做法短得出奇:把图看成弹簧网,求归一化拉普拉斯矩阵的特征向量。
  最小六个特征值           -0.00000  0.00029  0.00125  0.00249  0.00443  0.00777
  **第一个特征值是 0,对应的特征向量是常数——它没信息,直接扔掉。**
  **第二个特征值 0.00029,对应的特征向量就是「把卷摊开」的那个坐标。**

  摊得有多开?拿生成数据用的参数 t 当尺子量一下:
  LE 的坐标       与弯曲参数 t 的相关   与纸宽 h 的相关   单独这一维的保持率
  -------------------------------------------------------------------------
  第 2 特征向量                0.9595            0.0542               0.5320
  第 3 特征向量                0.0473            0.0760               0.4005
  **第 2 特征向量与 t 的相关是 0.960——它确确实实沿着卷的方向把点排开了。**
  **可第 3 特征向量与纸宽 h 的相关只有 0.076,基本是废的。**
  **这是这一节最该记住的一句话:LE 只保证「图上有边的点靠得近」,它不保证两维分别对应两个方向。**
  三个方法放在同一张表上(同一份数据、都降到二维):
  方法   10 近邻保持率   stress   第一维与 t 的相关
  ------------------------------------------------
  PCA           0.4030   0.1029               0.169
  LPP           0.5440   0.1115               0.244
  LE            0.5340   0.5014               0.960
  **注意 stress 那一列:PCA 0.10、LPP 0.11、LE 0.50。**
  **LE 保住了邻居,但把所有距离的比例全打乱了。**
  它只在乎「图上有边的点别分开」,至于分开多远,它不管。
  **所以要看整体形状,别用 LE;要按邻域做分类、聚类,LE 更合适。**

  那到底能摊到多好?拿真正的低维坐标来当上限定标:
  用作低维坐标的东西   10 近邻保持率   stress
  ------------------------------------------
  弧长 s 与纸宽 h             0.9983   0.7076
  参数 t 与纸宽 h             0.4253   0.5644
  LE 的两维                   0.5340   0.5014
  **一行是 0.9983,另一行只有 0.4253,差的不是方法,是「用什么当坐标」。**
  卷上同样转一个角度,外圈走的路是内圈的三倍(半径就是 t,从 4.7 到 14.1),
  所以等距坐标是弧长 s = t²/2,不是参数 t。
  **「生成数据用的参数」和「正确的低维坐标」根本是两件事——这一点非常多人搞错,拿 t 当标准答案去评分,会把好方法判成坏方法。**

  最后是一个方法论上的坑,比上面所有结论都重要:
  同一个正确坐标,缩放方式不同   10 近邻保持率
  -------------------------------------------
  弧长 s 原样(0 到 99.8)              0.9983
  弧长 s 缩到与 t 同量级                0.4123
  **同一份正确坐标,只是把两列缩放到同一个量级,保持率从 0.9983 掉到 0.4123。**
  **因为 k 近邻是欧氏距离算的:哪个轴的数值范围大,哪个轴就说了算。**
  **所以「近邻保持率」这个指标不是尺度不变的,报这个数的时候必须写清坐标怎么缩放的,否则两个人的数没法比。**
  这一节和上一节的结论合起来看:
  **降维方法没有绝对好坏,只有「你希望保住什么」。**
  **保方差,用 PCA;保邻域且只能线性,用 LPP;保邻域且允许非线性,用 LE 或核 PCA。**
  **保距离比例,三个都不行——那得去看多维标度法那一类。**

第一行的谱是 负 0.00000、0.00029、0.00125、0.00249、0.00443、0.00777。 第一个是零(数值上是负的极小值,属于浮点误差),对应的特征向量是常向量,扔掉。 第二个 0.00029 和第三个 0.00125 差四倍多,说明第二个方向明显更「省力」,应该优先拿它。 dim0 那一行是这一节的核心:与 t 的相关 0.9595,与 h 的相关 0.0542。 它把卷的方向摊开了,但完全没有抓住宽度方向。 dim1 那一行更直白:与 t 的相关 0.0473、与 h 的相关 0.0760, 两个都不沾——这就是「第二维是废的」的量化版本。 注意「单独保持」那一列:dim0 单独就有 0.5320,dim1 只有 0.4005,而两维加起来 0.5340, 比 dim0 单独只多 0.0020。 加了一整维,只换来千分之二的提升——这个增量就是「这一维没用」的直接证据。 后面三行是方法论的部分:弧长与纸宽 0.9983,参数与纸宽 0.4253。 这两个数差了两倍多,而它们描述的是同一张卷。 所以「用生成参数当标准答案」这个做法会把你带偏——它量的不是方法好坏,是「参数选得对不对」。 最后一行把 s 缩放到和 t 同量级,保持率从 0.9983 掉到 0.4123。 同一份正确坐标,只因缩放方式不同,分数掉了一半还多。 这一行是我在这一章里最想让人记住的:近邻保持率不是尺度不变的指标,报数必须带上预处理方式。

30 秒实验

  1. 把 le 里的 nn 从 10 改成 30,再跑一次。 近邻数变大,图变稠,「摊开」的效果会变弱,保持率会往下掉——因为大邻域已经不算「局部」了。
  2. 把 le 里归一化那两步(dm 和乘 dm)删掉,用未归一化的拉普拉斯。 观察保持率怎么变,特别是卷的内外两头会不会被压扁。这能说明归一化在防什么。
  3. 把 swiss 的 h 范围从 0 到 10 改成负 1 到 1(薄带),再跑一遍。 看 dim1 与 h 的相关会不会上去——薄带子上宽度方向的张力才明显。
  4. 在 le 里改成取第 2 到第 4 个特征向量(三维),再算一次保持率。 如果第三维也带来明显增量,说明数据其实有三维结构; 如果没增量,说明前两维已经把能拿的都拿了。

常见误区

  1. 「拉普拉斯特征映射能把流形完美摊平」。它只保证「图上有边的点靠得近」。 实测第 2 特征向量与弯曲参数的相关 0.9595, 但第 3 特征向量与纸宽的相关只有 0.0760——两个坐标里有一个是废的。
  2. 「LE 的 stress 应该也很小,因为它保住了邻居」。两者的目标根本不同。 实测 LE 的 stress 是 0.5014,是 PCA 的 0.1029 的五倍。 它保邻域、不保距离,这是设计取舍不是缺陷。
  3. 「用生成数据的参数当标准答案最可靠」。 瑞士卷的真实等距坐标是弧长 t²/2 而不是 t。 用 (t, h) 只有 0.4253,换成 (s, h) 就是 0.9983。 拿错的标准答案去评分,会把好方法判成坏方法。
  4. 「保持率是一个客观指标,谁报的数都能直接比」。 它不是尺度不变的:同一份坐标,缩放到同量级后从 0.9983 掉到 0.4123。 比较两个方法的保持率,必须确认近邻数、距离度量、坐标缩放三件事都一致。
自测三问(点开对答案) 1. 为什么第一个特征向量必须扔掉?这和 LPP 的约束是什么关系? —— 因为第一个特征值恒为 0,对应的特征向量是常向量,所有点坐标相同,没有信息。 这本质上就是「所有点挤成一坨」的平凡解。 LPP 用硬约束 a'XDX'a 等于 1 提前堵掉它,LE 让它自己出现然后手动扔掉,两种做法殊途同归。 2. LE 的第二个坐标与弯曲参数相关 0.9595,第三个却只有 0.0760。为什么? —— 因为这张卷的宽度方向在三维里本来就是平的,弹簧网在那个方向上没有张力,再摊也没有收益。 于是第二维只能去抓噪声。这说明「取前两维」不等于「拿到两个有意义的坐标」,得自己算相关验证。 3. 为什么报「近邻保持率」必须写清坐标是怎么缩放的? —— 因为它不是尺度不变的。k 近邻用欧氏距离,哪个轴数值范围大哪个轴就说了算。 实测同一份正确坐标 (弧长, 纸宽),原样保持率 0.9983,把弧长缩到与参数同量级后掉到 0.4123。 两个数不能直接比。

降维这一章到此结束。四个方法争的都是「丢掉什么」,答案分别是:丢方差小的、丢让邻居散开的、 在核空间里丢、丢到让弹簧最松。 下一章换题目:不再要求「换个坐标系」,而是要求「把点分成几堆」。

相关笔记