2. t-SNE 基本原理:核心思想、概率分布与 KL 散度
好,我们正式开始聊 t-SNE 的核心。我第一次接触 t-SNE 时,觉得它的名字挺唬人——t-分布随机邻域嵌入,每个词都像在劝退。但真正理解之后,你会发现它的核心逻辑其实特别朴素。
说白了,t-SNE 就干了三件事:保持邻居关系、在高维算距离、在低维画地图。它不像 PCA 那样追求全局方差最大化,它只在乎一件事——原始空间中离得近的点,降维后还得靠在一起。至于离得远的点?嗯,随便,反正不影响局部结构。
2.1 t-SNE 的核心思想:从“邻居”说起
想象一下。你有一群朋友站在一个高维房间里,你只能看到他们在二维地图上的投影。你希望这个投影能反映真实关系——好朋友站一起,陌生人站远点。t-SNE 就是这个画地图的人。
我习惯把 t-SNE 理解成一个“翻译官”:它先把高维空间里的点对之间的相似度,翻译成概率分布;再把低维空间里的点对相似度,也翻译成概率分布。然后它不断调整低维点的位置,让这两个分布尽量一致。
这个“尽量一致”怎么衡量?就是依靠我们接下来要讲的 KL 散度。
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)的数据集 |
你可能会问,为什么不用对称的联合概率 $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 的所有计算量,基本都花在反复算这个分母上了。
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}$ 很大(低维里很近),那么惩罚也会存在,但幅度要小得多。
我曾经做过一个实验:把 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)
别被它吓到。这个梯度告诉我们三件事:
- 如果 $p_{ij} > q_{ij}$(高维更近),梯度会推着 y_i 和 y_j 相互靠近。
- 如果 $p_{ij} < q_{ij}$(高维更远),梯度会推着它们相互远离。
- 力的强度由距离本身调节,远距离的调整幅度自然变小。
你想想看,这不就是一个弹簧系统吗?该近的拉近,该远的推远,最后达到平衡。算法就是这么直观。
2.6 本章知识结构总览
下面这张图把整个 t-SNE 的核心流程连起来了,你对照着看,能更清晰:
2.7 避坑指南:KL 散度的那些坑
我遇到过不少同学,t-SNE 跑完一看图,发现簇内点特别分散、没有紧凑感。原因往往出在 KL 散度的非对称性上。
KL 散度的非对称性意味着:t-SNE 不在意远处点被拉近,但非常在意近处点被推远。这导致了一个有趣的现象——如果你的数据集包含一个很大的簇,t-SNE 可能会把它拆成几个小碎片,因为这样能更好地保持每个小碎片内部的紧致性,而牺牲了大簇的连续性。
另一个常见问题是:同一数据集跑两次,结果长得不一样。这是因为 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