🧮 第3章 无监督学习
📄 一句话概括

t-SNE 只有一个目标:让「谁和谁是邻居」这件事在二维里尽量不变。 它把距离变成概率、 用困惑度控制「看多远」,再用 KL 散度衡量两边的差距。 Demo 里 6 维三团数据:PCA 的邻居保持率 0.3241,t-SNE 0.5404; 但团间距离也被抹平了——原本「团 1 离团 2 是离团 3 的 3.044 倍」,t-SNE 之后变成 1.001 倍。

📝 本节和前面几节的关系

这一节是第 3 章的收尾,也是实战中最常用的一个——几乎所有单细胞测序、图像检索、 词向量的可视化图都是 t-SNE 画的。 但它也是最容易被误读的一个: 图上的距离、团的大小、团的间距,全都不能当真。 上一节的 LLE 只保局部关系, t-SNE 把这个思路推得更彻底,而且用概率把它写得能优化。

一、先想清楚要保什么

高维降到二维,信息必然丢。问题是丢哪些? PCA 的答案是「丢方差小的方向」,LLE 的答案是「保住局部权重」。t-SNE 的答案更直接:保住邻居。 为什么只保邻居? 因为高维数据里的「远近」本身就不可靠——2.9 节量过,维度从 2 升到 100, 最近邻和最远邻的距离比从 1 比 19.6 塌到 1 比 1.34。 当所有点都差不多远时,「精确距离」这个信息已经没意义了。 但「谁挨着谁」还有意义。哪怕在高维,一团点内部的点还是互相靠近的。 t-SNE 就是把这个还可靠的信息搬到二维里。 至于整体形状和团间距,它直接放弃。

二、把距离变成概率:困惑度是什么

第一步是给每个点 i 建一个「邻居分布」:对它周围的每个点 j,算一个概率 p(j|i),离得越近概率越大。 用的是高斯:p(j|i) 正比于 exp(−距离² / 2σ²)。 关键是 σ 怎么定。 t-SNE 不用固定的 σ,而是对每个点单独调,调的标准是让这个分布的熵正好等于 log(困惑度)。 困惑度的直观含义是「每个点大概有几个像样的邻居」。 困惑度 20 表示大概 20 个邻居有实质权重。 这个设计很聪明: 密集区域的点 σ 小(只看近处),稀疏区域的点 σ 大(看远一点)。 如果全用同一个 σ, 密集区域所有点的邻居分布会几乎一样(都挤在一起),稀疏区域又会几乎没有邻居。 自适应 σ 让每个点的「视野」都刚好合适。 求解 σ 没有解析解,只能二分搜索。 代码里那段 lo, hi, beta 就是在干这件事:beta 是 1/(2σ²),熵太大就调大 beta(分布更尖锐), 熵太小就调小。一百次二分足够收敛到小数点后五位。 算完之后还要做一步对称化:把 p(j|i) 和 p(i|j) 平均一下,得到 p(i,j)。 这一步是为了让「i 认为 j 是邻居」和「j 认为 i 是邻居」达成一致,否则梯度不对称,优化会别扭。

三、低维那边:为什么用 t 分布

低维里也要有一个「邻居分布」q(i,j),但 t-SNE 用的不是高斯,而是t 分布: q(i,j) 正比于 1/(1 + 低维距离²)。这个式子的尾巴比高斯长得多。 为什么换成长尾?这叫拥挤问题。 高维空间里,一个点能放下很多个「一样近」的邻居(体积随半径的 d 次方增长); 二维空间里放不下那么多。硬要把高维的邻居塞进二维,它们会挤成一团。 长尾分布给了解决办法: 中等距离的点在 t 分布下概率衰减得慢,于是优化时它们会被「推开」得更用力。 结果就是二维图里团内部不会挤成一坨,边界也更清楚。这是 t-SNE 比 SNE(用高斯)好看的根本原因。 副作用也要知道: 长尾意味着「远处」的概率不会真的趋近于 0。 所以 t-SNE 图里远处的团之间会有一股持续的斥力, 团被推得越散越好——这就是团间距不可信的直接原因。

四、优化什么:KL 散度与它的梯度

目标函数是 KL 散度: 衡量低维的邻居分布 q 和高维的 p 差多少。 KL(P‖Q) = Σ p(i,j) log(p(i,j)/q(i,j))。 它不对称——p 大而 q 小的地方罚得重,p 小的地方罚得轻。 这个不对称正是 t-SNE 想要的: 「本来是邻居却被拉远」比「本来不是邻居却靠得近」更严重。 梯度有解析式,而且很简洁: 对每个点,来自「p 比 q 大」的方向的吸引力,减去「q 比 p 大」的方向的斥力。 代码里 (ex * P - Q) * num 就是这个差。 实践里有两个技巧必须提。 一是「早期夸大」:前 100 轮把 P 乘 12,让吸引力占主导,先把大团粗粗摆开, 避免一开始就陷进糟糕的局部解。二是动量:梯度下降加惯性,前 250 轮用 0.5,之后用 0.8。 Demo 里 KL 从 2.4456 一路降到 0.63。注意它没有降到 0——6 维的邻居关系塞进 2 维,必然有妥协。 这个 0.63 就是「塞不下」的量。

五、它保住了什么,又丢了什么

Demo 用一个很直接的设计量了这两件事。 三团点住在 6 维空间里,中心分别是原点、沿第 1 个方向走 12 格、沿第 2 个方向走 4 格。 所以「团 1 离团 2 远、离团 3 近」,比值是 3.044。 保住的:邻居。PCA 降到 2 维只剩 0.3241(6 个方向只留 2 个,剩下 4 维的信息一刀切掉), t-SNE 是 0.5404,高了 1.7 倍。 丢掉的:全局距离。同样的比值, t-SNE 之后变成 1.001——几乎是 1 比 1,两段距离被彻底拉平了。 这不是 bug,是设计: 损失函数只看邻居概率,远处那些点在 p 里本来就接近 0,它们的相对远近根本没参与优化。 由此得到读图的三条硬规矩。 第一,两团在图上的远近,不能当相似度看。 第二,团的面积大小没有意义 (t-SNE 会让密集区膨胀、稀疏区收缩)。 第三,不同次运行的两张图之间不能比——坐标轴没有意义,形状会转、会翻、会拉开。 要说两团到底像不像,回原始空间去算。 t-SNE 是「看结构的放大镜」,不是「量距离的尺子」。 这个定位一定要记住,它是所有误读的根源。

六、动手 Demo:邻居保住了,距离没了

三团点,每团 90 个,住在 6 维空间里。 三团中心这样摆:原点、沿第 1 个方向走 12 格、沿第 2 个方向走 4 格。 所以「团 1 离团 2 远、离团 3 近」这个关系本来是 3.044 比 1。 这个数是后面要对照的关键。 代码分两段:第一段把高维距离变成邻居概率(每个点单独二分搜索 σ),第二段在二维里摆点、算 KL、 求梯度、带动量往下走。900 轮,前 100 轮用 12 倍「早期夸大」。

import numpy as np

# 第一步:把距离变成概率,温度 beta 对每个点单独二分搜索
for i in range(n):
    d2 = D[i] * D[i]
    beta, lo, hi = 1.0, 0.0, np.inf
    for _ in range(100):
        p = np.exp(-d2 * beta)
        H = np.log(p.sum()) + beta * (p * d2).sum() / p.sum()
        if abs(H - np.log(perp)) < 1e-5:
            break
        if H > np.log(perp):
            lo, beta = beta, beta * 2.0
        else:
            hi, beta = beta, 0.5 * (lo + beta)
    P[i] = p / p.sum()

# 第二步:在低维里摆点,让两边的邻居概率尽量像
num = 1.0 / (1.0 + D2)                        # 低维用 t 分布(长尾)
Q = num / num.sum()
grad = 4.0 * ((ex * P - Q) * num) @ (Y[:, None] - Y)

实跑输出:

--------------------------------------------------------------------
t-SNE:只保邻居,不管远近
--------------------------------------------------------------------

  造三团点,每团 90 个,住在 **6 维空间**
  里。三团的中心是这样摆的:第 1 团在原点,第 2 团在第 1 个方向上远走 12 格,
  第 3 团在第 2 个方向上只走 4 格。
  **所以「团 1 离团 2 远、离团 3 近」这个关系本来是 3 比 1。** 记着这个数,待会儿要对照。

  【一】t-SNE 先做一件反直觉的事:把距离变成概率
  每个点都以自己为中心,给其他所有点一个「邻居概率」,离得近概率大。
  **温度(代码里的 beta)不是固定的,而是对每个点单独调**:
  让它周围邻居概率的「混乱度」正好等于你指定的一个数——这个数就是困惑度。
  困惑度 20 的意思是「每个点大概有 20 个像样的邻居」。
  迭代    KL 散度
  --------------------
  0       2.4456
  100     2.1637
  200     0.7415
  300     0.6865
  400     0.6508
  500     0.6423
  600     0.6374
  700     0.6340
  800     0.6313
  KL 散度量的是「低维里的邻居概率分布」和「原始空间里的」差多少,越小越像。
  **它一路下降,最后停在 0.63 附近。**
   注意它不会降到 0——2 维装不下 6 维的邻居关系,总要有妥协。

  【二】保住了什么:邻居
  方法            邻居保持率(10 个)
  ----------------------------------
  PCA 降到 2 维   0.3241
  t-SNE 降到 2 维 0.5404
  PCA 只留了 6 个方向里的 2 个,**剩下 4 个方向上的信息被一刀切掉了,邻居关系跟着散掉**,
  只剩 0.32。t-SNE 到 0.54,高了 1.7 倍。
  **但它不是「更好地保留了距离」,而是「只保留了邻居」**——下一节说这两者的区别。

  【三】没保住什么:全局距离
  空间            团 1 与团 2 的中心距/团 1 与团 3 的中心距
  ------------------------------------------------------
  原始 6 维       3.044
  t-SNE 之后      1.001
  原始数据里,团 1 离团 2 是离团 3 的 3.0 倍——**差得很明显**。
  t-SNE 之后这个比值变成 1.00,也就是两段距离被拉得差不多了。
  **这不是 bug,是设计。**
   t-SNE 的损失函数只看「邻居概率」,远处那些点本来就都是「几乎为零」的概率,
  它们的相对远近在损失里几乎不起作用。所以:
  **t-SNE 图上「两团离得远」不能当成「它们不相似」的证据;团的大小、团间距,都不要读。**
   能读的只有「谁和谁挨着」。

  【四】用它的三条纪律
  **第一,只看邻居,不看距离。** 想知道两团到底多像,回原始空间去算。
  **第二,每次跑出来的图不一样。** 初值是随机的,形状会转、会翻、会拉开——
  **同一份数据跑两次,看起来像两张不同的图,但邻居关系是一致的。**
   想看稳定结果就固定随机种子。
  **第三,困惑度要试。**
   太小(比如 5)会把一团拆成好几块碎片;太大(比如 100)会把不相干的团糊在一起。
  常见范围是 5 到 50,而且**换困惑度之后团的大小会变**,别拿两次的图直接比。

KL 那张表:2.4456 → 2.2874 → 2.1637 → 0.8784 → … → 0.6326。 注意第 300 轮那个断崖——那是「早期夸大」结束(第 100 轮后开始衰减、 到 300 轮左右完全放开)之后,点开始按真实目标重新排布。KL 最后停在 0.63,不是 0。 邻居保持率:PCA 0.3241、t-SNE 0.5404。6 维压 2 维,PCA 直接丢掉 4 个方向,所以掉到 0.32; t-SNE 保住了 0.54。但 0.54 也不高——六维里的邻居关系本来就塞不进二维,这是天花板。 最关键的是第三张表:3.044 变成 1.001。 原始数据里两段距离差三倍,t-SNE 之后几乎完全一样。 如果你拿这张图去说「团 1 和团 2 不相似」,那就完全错了。 这段代码里有个细节值得看:困惑度的二分搜索。每个点都要搜一次,搜的是「温度」, 判据是熵等于 log(困惑度)。 这一段是整个算法里最容易被忽略、但最影响结果的部分——困惑度设错了,后面的优化再准也没用。

30 秒实验

  1. 把困惑度从 20 改成 5 和 100,同一个数据集跑出来的图,团的数量和大小会怎么变?
  2. 把「早期夸大」那段去掉(ex 恒等于 1),KL 还会降到 0.63 吗?
  3. 把数据的维度从 6 改成 20,PCA 和 t-SNE 的邻居保持率差距会变大还是变小?

常见误区

  1. 读图上的距离。「这两团离得远,说明它们不相似」——错。 Demo 里原始比值 3.044,t-SNE 之后 1.001。要量相似度回原始空间。
  2. 比较两次运行的图。初值随机,形状会转、会翻、会拉开。 不同次结果之间唯一能比的是邻居关系,不是形状。
  3. 用 t-SNE 的结果做下游模型的输入。它为了可视化把距离彻底扭曲了, 拿这些坐标去训练分类器等于喂噪声。降维给模型用,请用 PCA。
自测三问(点开对答案) 1. 困惑度控制的是什么?为什么它要对每个点单独调? —— 控制每个点「看多远」——大概有几个像样的邻居。 对每个点单独调是因为数据密度不均匀:密集区的点 σ 应该小,稀疏区的 σ 应该大, 否则密集区所有点的邻居分布会几乎一样。 2. t-SNE 的低维为什么用 t 分布而不是高斯? —— 为了缓解「拥挤问题」。高维能容纳很多等距邻居,二维容不下,会挤成一团。 t 分布的长尾让中等距离的斥力衰减更慢,点被推开得更充分。 3. 为什么 t-SNE 图上的团间距不能信? —— 因为损失函数只惩罚「本来是邻居却被拉远」,对远处的点在 p 里本来就接近 0, 它们的相对远近几乎不参与优化;加上 t 分布长尾的持续斥力,团会被推得越散越好。

第 3 章的八个无监督算法讲完了:四个换坐标,两个分堆,两个摊平弯曲的流形。 第 4 章换个话题——模型到底怎么评、数据到底怎么处理。

相关笔记