t-SNE 原理:SNE、对称 SNE、t 分布

在深入 t-SNE 之前,得先弄明白它爹——SNE(随机邻域嵌入)。
说白了,SNE 想干一件事:把高维空间的“邻居关系”搬到低维空间里。
我最初看论文时觉得这思路挺朴素,但实际操作起来坑不少。

1. SNE——最初的思路

SNE 的核心是用条件概率衡量两个点之间的相似度。
在高维空间,它给每个点 i 定义了一个高斯分布,中心在 i 上,
然后算 j 出现在 i 周围的概率——记作 pj|i。
同理,在低维嵌入里也算一个 qj|i。
目标是让这两个概率分布尽可能接近,用的损失函数是 KL 散度。

这里有个问题:KL 散度是不对称的。
我在项目中遇到过:如果高维里两个点远,低维里却很近,惩罚很小;
但反过来,高维很近、低维很远,惩罚极大。
这就导致 SNE 很“珍惜”局部邻居,却不在乎远处点被拉近。

SNE 的损失函数:
KL(p||q) = Σi Σj pj|i log(pj|i / qj|i)
特性SNE
相似度度量条件概率 pj|i(高斯核)
对称性不对称,pj|i ≠ pi|j
优化难度梯度爆炸,超参数敏感
可视化效果容易拥挤中心,尾巴乱飘

2. 对称 SNE——对称化的改进

对称 SNE 把条件概率换成了联合概率 pij 和 qij。
为什么要对称?因为 KL 散度对称之后,梯度更容易算,
而且不会出现“低维里一对点分开很远但高维里很近”这种极端惩罚不均。

不过对称 SNE 还有个麻烦:pij 怎么定义?
论文里用了 pij = (pi|j + pj|i) / 2n,
强制对称之后,数值稳定性好了很多。

我个人习惯在实验对称 SNE 时,会把困惑度(perplexity)调大一点。
因为对称之后全局结构更容易显现,perplexity 太小反而挤在一起。

技巧: 如果你用 sklearn 的 TSNE,其实底层已经用了对称 SNE 的思想,
但注意它的 method='exact' 和 method='barnes_hut' 在梯度计算上有差异。
小数据集(<5000 点)推荐 exact,大数据集用 barnes_hut。

3. 为什么是 t 分布?——自由度问题

对称 SNE 用高斯核做低维映射,结果呢?
高维里密密麻麻的点,在低维里会被挤成一团——这叫“拥挤问题”(Crowding Problem)。
为什么会这样?因为高斯核的尾巴太薄了。
高维空间里的点,在低维里如果距离中等,高斯概率几乎为零,
于是优化器只好把它们堆在一起。

t 分布登场。它的尾巴更厚,也就是距离较远的点也能得到非零的概率。
这样就能“推开”那些原本不该挤在一起的簇。
t-SNE 的名字就是这么来的——在低维用了自由度 1 的 t 分布(柯西分布)。

我曾经因为没搞懂这个,跑 t-SNE 时 perplexity 调到了 5,结果图变成一坨。
嗯,后来发现 t 分布让每个点都有“排斥力”,
perplexity 太小意味着局部邻居太少,全局排斥又太弱,
最后所有点都缩到原点附近。

避坑: t 分布的“厚尾”不是万能药。
如果你的数据本身就有很多离群点,t-SNE 可能会把那些点推得更远,
导致离群点被孤立成单独的小簇,甚至看起来像发现新类别(其实是假象)。
我建议配合 DBSCAN 或 HDBSCAN 先做一次噪声过滤。

4. t-SNE 的完整流程

把上面三步串起来就是 t-SNE:

  1. 在高维空间用高斯核计算条件概率 pj|i,再对称化为联合概率 pij。
  2. 在低维空间用 t 分布(自由度 1)计算联合概率 qij。
  3. 用 KL 散度衡量 p 和 q 的差异,梯度下降最小化。
  4. 用动量(momentum)加速收敛,早期有“早期压缩”技巧防止陷入局部最优。

下面这张图是我做课件时画的流程图,可以帮你把关系理清楚:

t-SNE 原理演化 高维空间 (原始数据) SNE 条件概率 + KL散度 对称 SNE 联合概率 + 对称梯度 t-SNE 低维 t 分布 低维嵌入 (2D/3D 可视化) 困惑度设定 (perplexity) 影响局部邻居数 注意:不对称→对称→t分布 三个演进关键

5. 几种方法的对比

方法高维核低维核损失函数对称性常见问题
SNE高斯高斯KL(pj|i||qj|i)不对称梯度爆炸,局部拥挤
对称SNE高斯(对称化)高斯KL(pij||qij)对称仍有拥挤问题
t-SNE高斯(对称化)t分布(df=1)KL(pij||qij)对称超参数敏感,计算慢

6. 实际代码中的感知

调 sklearn 的 TSNE 时,我一般会先试这几个参数:

from sklearn.manifold import TSNE

tsne = TSNE(
    n_components=2,        # 降维维度
    perplexity=30.0,       # 困惑度,默认 30
    early_exaggeration=12.0, # 早期压缩,默认 12
    learning_rate=200.0,   # 学习率
    n_iter=1000,           # 迭代次数
    method='barnes_hut',   # 或 'exact'
    random_state=42        # 复现
)
X_embedded = tsne.fit_transform(X)

这里 perplexity 对应的是 SNE 里的高斯核带宽选择。
我曾经过早地把 early_exaggeration 调成 4,结果每个簇之间空隙特别大,
后来才明白早期压缩其实是模拟 t 分布“推开”的效果——值太小等于关掉了这个功能。

经验: t-SNE 的梯度计算在早期非常敏感。
如果你发现结果每次跑都不一样,试试固定 random_state,
或者增大 n_iter 到 2000 以上。
还有,先用 PCA 降维到 30-50 维再丢给 t-SNE,能提速 50% 以上。

7. 小结这章的核心

  • SNE 用条件概率,不对称,训练难。
  • 对称 SNE 用对称联合概率,梯度稳定,但仍有拥挤问题。
  • t-SNE 用 t 分布代替高斯核,厚尾特性解决拥挤,
    但引入了新的超参数(perplexity, learning_rate, early_exaggeration)。
  • 三者是一脉相承的演进,理解每个改动背后的动机,
    比死记参数值更有用。

我个人建议初学者先拿对称 SNE 和 t-SNE 对比跑一次,
看看同一份数据在两个方法下的低维分布差异。
那种“簇被挤在一起 vs 簇被撑开”的视觉冲击,比任何论文都直观。