2. t-SNE 基本原理:核心思想、概率分布与 KL 散度

好,我们正式开始聊 t-SNE 的核心。我第一次接触 t-SNE 时,觉得它的名字挺唬人——t-分布随机邻域嵌入,每个词都像在劝退。但真正理解之后,你会发现它的核心逻辑其实特别朴素。

说白了,t-SNE 就干了三件事:保持邻居关系、在高维算距离、在低维画地图。它不像 PCA 那样追求全局方差最大化,它只在乎一件事——原始空间中离得近的点,降维后还得靠在一起。至于离得远的点?嗯,随便,反正不影响局部结构。

2.1 t-SNE 的核心思想:从“邻居”说起

想象一下。你有一群朋友站在一个高维房间里,你只能看到他们在二维地图上的投影。你希望这个投影能反映真实关系——好朋友站一起,陌生人站远点。t-SNE 就是这个画地图的人。

我习惯把 t-SNE 理解成一个“翻译官”:它先把高维空间里的点对之间的相似度,翻译成概率分布;再把低维空间里的点对相似度,也翻译成概率分布。然后它不断调整低维点的位置,让这两个分布尽量一致。

这个“尽量一致”怎么衡量?就是依靠我们接下来要讲的 KL 散度。

一句话概括: t-SNE 是在高维空间里定义了一个“谁跟谁关系好”的规则,然后在低维空间里模仿这个规则。模仿得越像,可视化效果越好。

2.2 高维空间的概率分布:条件概率与困惑度

t-SNE 怎么定义“关系好”?它用了一个条件概率的概念。

给定一个高维数据点 $x_i$,它把其他点 $x_j$ 选作自己邻居的概率定义为:

p_{j|i} = exp(-||x_i - x_j||² / 2σ_i²) / Σ_{k≠i} exp(-||x_i - x_k||² / 2σ_i²)

这个公式看着复杂,其实意思很简单:两个点越近,指数部分越大,概率越高;越远,指数衰减越快。这里的 σ_i 是个关键参数——它决定了每个点的“有效感知半径”。

σ_i 由用户指定的一个全局参数控制——困惑度(Perplexity)。我在项目中遇到过很多次,困惑度选不好,降维结果天差地别。

困惑度取值 对结果的影响 我建议的使用场景
5 - 10 局部结构突出,全局关系混乱 探索小集群或离群点检测
30 - 50 局部与全局平衡较好 大多数数据集(默认值)
> 100 全局结构优先,局部细节丢失 样本量极大(>10000)的数据集
我的实践建议: 刚开始做可视化时,我建议固定前几次实验用 30。如果结果出现很多“毛球”状簇,适当减小;如果簇太大太散,适当增大。不要纠结于找一个“最优”困惑度——没有一个参数能完美适配所有数据。

你可能会问,为什么不用对称的联合概率 $p_{ij}$,而是用条件概率 $p_{j|i}$?其实原版 t-SNE 最后确实会对称化处理——把 $p_{j|i}$ 和 $p_{i|j}$ 取平均,得到对称的联合概率 $p_{ij}$。这么做的目的是让所有点对的概率加起来等于 1,方便后续计算。

2.3 低维空间的概率分布:t-分布的妙用

好了,高维那边定义完了。那低维空间里我们怎么定义相似度呢?

t-SNE 在这里做了一个非常聪明的选择——它没有用高斯分布,而是用了自由度为 1 的 t-分布(也就是柯西分布)。为什么?

你可以想想看:高维空间里,点之间的距离普遍偏大,用高斯分布会让远距离点之间的概率几乎为零。但低维空间(通常是二维)里,所有点都被压缩在一个小画布上,如果还用高斯分布,就会导致“拥挤问题”——所有点都挤在一起,看不清结构。

t-分布的“尾巴”比高斯分布厚得多。这意味着在低维空间里,稍微远一点的点对,其概率值仍然是非零的,不会迅速衰减到零。这给了低维点更多的“活动空间”,有效缓解了拥挤问题。

低维空间的联合分布公式:
q_ij = (1 + ||y_i - y_j||²)^(-1) / Σ_{k≠l} (1 + ||y_k - y_l||²)^(-1)

这里的 y_i 和 y_j 就是低维空间里我们正在优化的点。t-SNE 的所有计算量,基本都花在反复算这个分母上了。

计算复杂度警告: 上面的公式需要对所有点对(i, j)求和,复杂度是 O(N²)。当 N = 10000 时,需要计算约 5000 万对距离。这是我当年做第一个 t-SNE 项目时踩过的坑——直接算,电脑风扇就起飞了。后面我们会讲到 Barnes-Hut 近似,可以把复杂度降到 O(N log N),但那是优化技巧,不是原理。

2.4 KL 散度:衡量分布差异的“标尺”

好,现在我们有了一高一低两套概率分布:

  • 高维空间的联合分布 $p_{ij}$
  • 低维空间的联合分布 $q_{ij}$

我们的目标就是让这两个分布尽可能一致。用什么来度量“不一致”呢?用 KL 散度(Kullback-Leibler Divergence)。

KL(P || Q) = Σ_i Σ_j p_ij * log(p_ij / q_ij)

这个公式的直观含义是:对于每一对点,如果 $p_{ij}$ 很大(高维里距离很近),而 $q_{ij}$ 很小(低维里距离很远),那么这一项就会贡献一个很大的值——t-SNE 会非常“惩罚”这种错误。反之,如果 $p_{ij}$ 很小(高维里很远),$q_{ij}$ 很大(低维里很近),那么惩罚也会存在,但幅度要小得多。

记住了——KL 散度是非对称的,它更重视保持近距离点的关系,牺牲远距离点的准确性。这就是 t-SNE 为什么擅长展示局部结构,但全局位置关系可能失真的根本原因。

我曾经做过一个实验:把 MNIST 手写数字用 t-SNE 降维到二维,结果 0、6、8、9 这四个数字的簇挨得很近,因为它们在图像空间里有相似的像素模式;而 1 被孤零零地甩到一边。这个全局位置关系反映的是局部邻居的累积效应,并不代表数字 1 在原始空间中真的远离其他数字。嗯,这一点在做结论时一定要小心。

2.5 优化过程:梯度下降与学习率

有了目标函数(KL 散度),剩下的就是优化了。t-SNE 用梯度下降来逐步调整低维点的位置。梯度公式长这样:

∂C / ∂y_i = 4 Σ_j (p_ij - q_ij)(y_i - y_j)(1 + ||y_i - y_j||²)^(-1)

别被它吓到。这个梯度告诉我们三件事:

  1. 如果 $p_{ij} > q_{ij}$(高维更近),梯度会推着 y_i 和 y_j 相互靠近。
  2. 如果 $p_{ij} < q_{ij}$(高维更远),梯度会推着它们相互远离。
  3. 力的强度由距离本身调节,远距离的调整幅度自然变小。

你想想看,这不就是一个弹簧系统吗?该近的拉近,该远的推远,最后达到平衡。算法就是这么直观。

学习率怎么设? 我个人一般用 200 起步,如果损失曲线振动幅度太大,降到 100;如果收敛太慢,升到 500。但别超过 1000,否则你会发现低维点在画布上疯狂震荡,永远不收敛。

2.6 本章知识结构总览

下面这张图把整个 t-SNE 的核心流程连起来了,你对照着看,能更清晰:

t-SNE 核心流程总览 高维数据点 X = {x₁, x₂, ..., xₙ} 高维联合分布 P p_ij = (p_j|i + p_i|j) / 2N 高斯核 + 困惑度调节 低维映射点 Y = {y₁, y₂, ..., yₙ} 低维联合分布 Q q_ij = (1+||y_i-y_j||²)⁻¹ / Z t-分布(自由度=1) KL(P || Q) 最小化 目标函数梯度下降 梯度计算与更新 ∂C/∂y_i = 4 Σ(p_ij - q_ij)(y_i - y_j)(1+||...||²)⁻¹ 学习率 η + 动量 α 二维可视化结果 Y ∈ ℝ²,可直接绘制散点图 反馈循环:更新 Y 后重新计算 Q

2.7 避坑指南:KL 散度的那些坑

我遇到过不少同学,t-SNE 跑完一看图,发现簇内点特别分散、没有紧凑感。原因往往出在 KL 散度的非对称性上。

KL 散度的非对称性意味着:t-SNE 不在意远处点被拉近,但非常在意近处点被推远。这导致了一个有趣的现象——如果你的数据集包含一个很大的簇,t-SNE 可能会把它拆成几个小碎片,因为这样能更好地保持每个小碎片内部的紧致性,而牺牲了大簇的连续性。

怎么判断结果是“掰碎”还是“真实结构”? 我的经验是:先用 PCA 看一眼全局。如果 PCA 能看出一个连续的大簇,但 t-SNE 却把它撕碎了,那往往是参数设置的问题。如果 PCA 也看不出连续性,说明数据本身就可能是分块的。

另一个常见问题是:同一数据集跑两次,结果长得不一样。这是因为 t-SNE 的初始化是随机的,不同随机种子会收敛到不同的局部最优解。但结构层次(比如簇的数量、相对排列)应该保持一致。如果两次结果的结构都不一样,说明困惑度设得太小了。

# 一个简单检查方法:固定随机种子
from sklearn.manifold import TSNE
import numpy as np

# 第一次
tsne1 = TSNE(n_components=2, random_state=42)
Y1 = tsne1.fit_transform(X)

# 第二次(同一随机种子)
tsne2 = TSNE(n_components=2, random_state=42)
Y2 = tsne2.fit_transform(X)

# Y1 和 Y2 应该完全一致
print(np.allclose(Y1, Y2))  # True
最后提醒一句: 不要仅凭 t-SNE 的可视化结果来判断数据的“类别距离远近”。两个簇看起来隔得很远,不代表它们在原始空间里真的离得远——可能是 t-SNE 为了让簇内更紧凑,人为把簇间距离拉大了。