3. t-SNE核心参数:perplexity
perplexity 这个词,第一次接触时容易被唬住。说白了,它就是 t-SNE 里控制每个点到底看多远的一个旋钮。我个人认为,在所有参数中 perplexity 对结果的影响最大,也是最需要花时间去调的。
3.1 什么是 perplexity?
简单讲,perplexity 决定了每个点在计算相似度时,要考虑多少个邻居。数值越大,每个点的"视野"就越宽,全局结构会更明显;数值越小,每个点只看身边的几个点,局部细节会更突出。
从数学上看,perplexity 和高斯分布的带宽 σ 直接挂钩。t-SNE 会为每个点自动调整 σ,使得条件概率分布的熵恰好等于你指定的 perplexity 值。我刚开始做降维时,总觉得这个逻辑有点绕,后来自己推了一遍公式才真正理解——它本质上是在控制概率分布的"陡峭程度"。
核心理解:perplexity ≈ 每个点的"有效邻居数"。它平衡了局部结构和全局结构之间的权重。
3.2 典型取值范围:5~50 的经验法则
几乎所有教程都会告诉你,perplexity 取 5 到 50 之间。这个区间是怎么来的?我自己的体会是:小于 5 时,每个点几乎只看自己,聚类会碎成很多小团;大于 50 时,每个点看到的范围太大,局部细节被抹平,不同簇容易黏在一起。
下表整理了不同 perplexity 区间下的表现特征,方便你快速对照:
| perplexity 值 | 特点 | 典型场景 |
|---|---|---|
| 5 ~ 10 | 局部结构非常细致,簇内模式清晰;全局位置可能失真 | 小数据集(<500 样本),或需要观察细粒度子结构 |
| 20 ~ 40 | 局部与全局相对平衡,大多数情况下效果稳定 | 中等规模数据(500~10000 样本)的通用选择 |
| 45 ~ 50+ | 全局布局更规整,簇间距离更真实;局部细节会损失 | 大数据集(>10000 样本),或关注宏观聚类结构 |
实用技巧:可以先从 30 开始,观察结果是否合理,再向两侧调整。如果嵌入结果对 perplexity 非常敏感,说明数据本身可能没有清晰的聚类结构——这本身就是一个重要发现。
为什么会有这样的规律?你想想看,当 perplexity 很小时,高斯分布带宽很窄,每个点只和极少数邻居产生有效交互,t-SNE 的梯度更新基本由局部决定。当 perplexity 很大时,带宽变宽,远处的点也会贡献梯度,全局形状就站住了。
3.3 如何选择 perplexity?
没有万能答案,但有几个靠谱的实操策略。我在项目中通常按下面的流程来:
- 先按样本量估算:N < 1000 时从 5~15 起步;N 在 1000~10000 时从 20~40 起步;N > 10000 时从 30~50 起步。
- 跑 3~5 个不同的值:比如 [5, 15, 30, 45, 50],观察嵌入结果是否稳定。如果每个值的结果都差不多,那恭喜你,数据本身很鲁棒。
- 看簇的形态是否合理:如果某个 perplexity 下出现大量均匀分布的点云,大概率是值选大了;如果出现很多细碎的小簇,可能是值选小了。
注意:不要只用一次 t-SNE 就下结论。同一个 perplexity 下,不同随机种子也可能带来差异。建议固定 perplexity 跑 3 次,观察哪些簇是稳定的。
3.4 代码示例:用不同 perplexity 观察结果
我习惯用 sklearn 的 TSNE 快速做对比实验。下面这段代码跑三个不同的 perplexity 值,直接看效果:
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
perplexities = [5, 30, 50]
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for i, perp in enumerate(perplexities):
tsne = TSNE(perplexity=perp, random_state=42)
X_emb = tsne.fit_transform(X)
axes[i].scatter(X_emb[:, 0], X_emb[:, 1], c=y, cmap='viridis', s=5)
axes[i].set_title(f'perplexity = {perp}')
axes[i].set_xticks([])
axes[i].set_yticks([])
plt.tight_layout()
plt.show()
跑完之后,重点看两个东西:簇的分离度和簇内紧凑程度。perplexity=5 时,如果看到很多细碎的离群小团,说明局部噪声被放大了;perplexity=50 时,如果本应分开的簇黏在了一起,那就是全局压力过大,把细节压没了。
我的习惯:先跑一个中等 perplexity(比如 30)作为基准,再分别向小和大各试一个值。三步就能摸清数据的"脾性"。
3.5 避坑指南
先说一个我犯过的错。曾经在一个基因表达数据集上,我直接用了默认的 perplexity=30,出来的图看起来有四个清晰的簇。后来换到 perplexity=15,发现其中一个簇其实是两个独立的子群。嗯,默认值有时候会"隐藏"细节。
还有几个常见问题:
- perplexity 大于样本数:t-SNE 会报错,因为每个点找不到那么多邻居。最大值不能超过 N-1。
- perplexity 设得太小:损失函数容易陷入局部最优,每次运行结果差异很大。遇到这种情况,试着把值翻倍看看。
- 忽略数据规模:100 个样本用 perplexity=50 和 10000 个样本用 perplexity=5 一样不合理——一个抹平了结构,一个碎成了渣。
重要提醒:perplexity 不是越大越好,也不是越小越好。它的意义在于帮你找到数据中"真实的"结构层次。如果你发现嵌入结果随 perplexity 剧烈变化,先别急着调参,回头检查一下数据预处理和距离度量是否合理。
3.6 写在最后
perplexity 是 t-SNE 里最值得花心思去理解的参数。它直接控制着"每个点看多远",而这个距离感决定了你是看到树林还是看到树木。我遇到过不少初学者把 perplexity 当成一个固定值来用,结果错过了数据里重要的子结构。记住:没有"最佳"的 perplexity,只有"最合适你问题"的 perplexity。