t-SNE 原理:SNE、对称 SNE、t 分布
在深入 t-SNE 之前,得先弄明白它爹——SNE(随机邻域嵌入)。
说白了,SNE 想干一件事:把高维空间的“邻居关系”搬到低维空间里。
我最初看论文时觉得这思路挺朴素,但实际操作起来坑不少。
1. SNE——最初的思路
SNE 的核心是用条件概率衡量两个点之间的相似度。
在高维空间,它给每个点 i 定义了一个高斯分布,中心在 i 上,
然后算 j 出现在 i 周围的概率——记作 pj|i。
同理,在低维嵌入里也算一个 qj|i。
目标是让这两个概率分布尽可能接近,用的损失函数是 KL 散度。
这里有个问题:KL 散度是不对称的。
我在项目中遇到过:如果高维里两个点远,低维里却很近,惩罚很小;
但反过来,高维很近、低维很远,惩罚极大。
这就导致 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 太小反而挤在一起。
但注意它的
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-SNE 可能会把那些点推得更远,
导致离群点被孤立成单独的小簇,甚至看起来像发现新类别(其实是假象)。
我建议配合 DBSCAN 或 HDBSCAN 先做一次噪声过滤。
4. t-SNE 的完整流程
把上面三步串起来就是 t-SNE:
- 在高维空间用高斯核计算条件概率 pj|i,再对称化为联合概率 pij。
- 在低维空间用 t 分布(自由度 1)计算联合概率 qij。
- 用 KL 散度衡量 p 和 q 的差异,梯度下降最小化。
- 用动量(momentum)加速收敛,早期有“早期压缩”技巧防止陷入局部最优。
下面这张图是我做课件时画的流程图,可以帮你把关系理清楚:
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 分布“推开”的效果——值太小等于关掉了这个功能。
如果你发现结果每次跑都不一样,试试固定
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 簇被撑开”的视觉冲击,比任何论文都直观。