高维邻居挤进二维后为何成团?t-SNE 的困惑度与重尾相似度
从流形方法难以突出局部簇出发,推导 t-SNE 的邻域概率、困惑度、Student-t 相似度与 KL 目标,并解释图形误读和调试方法。
上一篇的局部线性嵌入(Locally Linear Embedding,LLE)保存每个点的局部重构配方,再用谱分解求全局坐标。它适合研究流形参数化,却不一定能在二维图上突出“谁真正像谁”:高维邻居被迫挤进只有两个方向的平面,许多本来不近的点也可能占据相近位置。
t 分布随机邻域嵌入(t-distributed Stochastic Neighbor Embedding,t-SNE)改变了问题:**不要求低维坐标复现距离本身,而要求高维与低维空间对“哪些点互为邻居”给出相似概率。**本文只讲透三件紧密相连的事:高维邻域概率、困惑度(Perplexity),以及用重尾分布缓解拥挤。
01 为什么保存距离仍不等于看清邻居?#
假设高维点 位于多个弯曲、密度不同的局部群体。Isomap 关心全局测地距离,LLE 关心局部重构;而探索性可视化常问的是:
与样本 最相似的少数样本,在二维图中是否仍围绕它?
从 维压到 2 维时,球壳可用面积急剧减少,远近关系不能全部满足:
高维局部邻域 二维直接使用高斯相似度
· · ··
· xᵢ · 压缩 ·xᵢ·
· · ──► ··
大量“中等远”样本 外围没有足够空间
结果:本来只算普通邻居的点也被推到中心附近,局部结构发生拥挤text这叫拥挤问题(Crowding Problem)。t-SNE 的关键不是凭空制造更多二维面积,而是让低维相似度的尾部下降得更慢,使中等距离的点仍能被放得更远。
02 完整数据流与张量形状#
设输入 ,目标坐标 ,可视化通常取 :
X [N,D]
│ 两两平方距离
▼
Δˣ [N,N]
│ 每行选择 σᵢ,使困惑度达到目标
▼
P_cond [N,N]:p(j|i),对角为 0,每行和为 1
│ 对称化并除以 2N
▼
P [N,N]:高维联合邻域概率,总和为 1
Y [N,d](随机或主成分分析 Principal Component Analysis/PCA 初始化)
│ Student-t 相似度并全局归一化
▼
Q [N,N]:低维联合邻域概率,总和为 1
│ 最小化 KL(P || Q)
▼
更新 Y [N,d]text是由原数据固定下来的训练目标; 随低维坐标改变。标准 t-SNE 不学习从 到 的显式函数,它直接优化这 个坐标。
03 高维距离怎样变成“以 i 为中心”的概率?#
对中心点 ,先用带宽为 的高斯核定义条件概率:
- :第 个高维样本;
- :只属于中心点 的局部带宽;
- :从 看过去,选择 作为邻居的概率;
- 每一行 的和为 1。
带宽小,概率集中在最近的几个点;带宽大,较远点也获得质量。每个点使用自己的 ,因此稠密区和稀疏区可以采用不同的距离尺度。
条件概率有方向: 不必等于 。t-SNE 将它对称化为联合概率:
于是 对称,且 。
04 困惑度究竟控制多少邻居?#
用户不直接指定 ,而指定每行概率分布的困惑度:
若概率恰好均匀落在 个邻居上,熵为 ,困惑度就是 。所以它可粗略理解为“有效邻居数”,但不是硬性的 K 近邻数量。
算法对每个 用二分搜索寻找 :
目标困惑度太小于当前值 → 分布太平,缩小 σᵢ
目标困惑度太大于当前值 → 分布太尖,增大 σᵢ
直到 |当前困惑度 - 目标| 足够小text05 三个一维点怎样手算一行概率?#
取 ,关注中间点 ,暂定 。它到另外两点的平方距离分别为 1 和 4:
归一化后:
这一行的熵约为:
所以困惑度约为 。若目标困惑度是 2,当前分布太集中,应增大 ,让远处的 获得更多概率。
这个例子也揭示了一个边界:困惑度相同不代表物理半径相同。稠密区域的 可能很小,稀疏区域的 可能很大。
06 为什么低维空间改用 Student-t 重尾分布?#
给定低维坐标 ,t-SNE 使用自由度为 1 的 Student-t 核,也就是柯西核:
对比尾部下降速度:
| 低维距离 | 高斯核 | Student-t 核 |
|---|---|---|
| 1 | 0.368 | 0.500 |
| 2 | 0.018 | 0.200 |
| 4 | 0.059 |
重尾意味着:一对点被分开后,仍保留可观的低维相似度和排斥作用;普通高斯在中等距离处已经近似为零,难以继续把不相似点推出拥挤区域。
低维作用力
近邻: pᵢⱼ > qᵢⱼ → 吸引,缩短 yᵢ 与 yⱼ
伪邻: pᵢⱼ < qᵢⱼ → 排斥,拉开 yᵢ 与 yⱼ
重尾: 中等距离仍能“感到”排斥text07 KL 目标为何更在意“丢失真邻居”?#
t-SNE 最小化 Kullback-Leibler 散度(Kullback-Leibler Divergence,KL):
若高维认为一对点是邻居, 大,但二维把它们分得很远、 小,这一项惩罚很大。反过来,若 极小,即使二维把它们画得较近,单项权重也较小。
因此 KL 的方向不对称:t-SNE 优先避免漏掉高维邻居,而不是同等精确地保存所有远点关系。这正适合局部可视化,也意味着簇间距离、方向和面积不能按原空间尺度解释。
目标对坐标的梯度为:
求和中的每一项都是一对点的吸引或排斥。训练常配合动量、学习率和早期夸大(Early Exaggeration):开始阶段临时放大 ,先拉紧可靠邻域并拉开群体,之后再优化真实目标。
08 不调用 TSNE,写出可检查的概率本体#
下面实现高维联合概率和低维 KL;它使用 内存,只用于小数据教学与单元测试:
import numpy as np
def _conditional_row(dist2, target_perplexity, max_steps=60):
lo, hi = -20.0, 20.0 # 搜索 log(sigma)
for _ in range(max_steps):
sigma = np.exp((lo + hi) / 2)
weights = np.exp(-dist2 / (2 * sigma**2))
probs = weights / weights.sum()
entropy = -(probs * np.log2(np.maximum(probs, 1e-300))).sum()
perplexity = 2**entropy
if perplexity > target_perplexity:
hi = np.log(sigma) # 分布太平,缩小 sigma
else:
lo = np.log(sigma) # 分布太尖,增大 sigma
return probs, sigma
def joint_probabilities(X, perplexity=5.0):
X = np.asarray(X, dtype=float) # [N,D]
n = len(X)
if not 1 <= perplexity < n:
raise ValueError('perplexity 必须位于 [1, N)')
dist2 = ((X[:, None] - X[None, :]) ** 2).sum(axis=2) # [N,N]
conditional = np.zeros((n, n))
sigmas = np.zeros(n)
for i in range(n):
mask = np.arange(n) != i
conditional[i, mask], sigmas[i] = _conditional_row(
dist2[i, mask], perplexity
)
P = (conditional + conditional.T) / (2 * n)
np.fill_diagonal(P, 0.0)
return P, sigmas
def low_dimensional_probabilities(Y):
dist2 = ((Y[:, None] - Y[None, :]) ** 2).sum(axis=2) # [N,N]
numerator = 1.0 / (1.0 + dist2)
np.fill_diagonal(numerator, 0.0)
return numerator / numerator.sum()
def kl_loss(P, Y):
Q = low_dimensional_probabilities(Y)
mask = P > 0
return np.sum(P[mask] * np.log(P[mask] / np.maximum(Q[mask], 1e-300)))python可先断言 P、Q 对称、对角为 0、总和为 1,再实现梯度。生产实现还需要近似近邻、Barnes-Hut 或 FFT 加速、稳定优化和停止条件,不应直接扩展这段稠密代码。
09 用 scikit-learn 1.9 正确落地#
当前官方 TSNE API ↗ 使用 max_iter,默认 learning_rate='auto' 与 init='pca':
import numpy as np
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE, trustworthiness
from sklearn.preprocessing import StandardScaler
X_scaled = StandardScaler().fit_transform(X) # [N,D]
# 高维稠密数据先降到约 50 维,可抑制噪声并减少距离计算成本
n_pre = min(50, X_scaled.shape[1], X_scaled.shape[0] - 1)
X_pre = PCA(n_components=n_pre, random_state=42).fit_transform(X_scaled)
tsne = TSNE(
n_components=2,
perplexity=30.0, # 必须小于 N,应扫描多个尺度
early_exaggeration=12.0,
learning_rate='auto',
max_iter=1000,
init='pca',
method='barnes_hut',
angle=0.5,
random_state=42,
verbose=1,
)
Y = tsne.fit_transform(X_pre) # [N,2]
assert Y.shape == (len(X), 2)
assert np.isfinite(Y).all()
print('final KL:', tsne.kl_divergence_)
print('iterations:', tsne.n_iter_)
print('trustworthiness:', trustworthiness(X_pre, Y, n_neighbors=10))python稀疏输入应考虑 TruncatedSVD,而不是强行中心化后做稠密 PCA。官方建议在特征极多时先压到合理维数,例如 50,再运行 t-SNE。
method='barnes_hut' 将排斥项近似到约 ,但只支持较低输出维数;angle 越小通常越精确也越慢。method='exact' 约为 ,适合小数据核对,不是大样本默认选择。
10 为什么它没有自然的 transform?#
scikit-learn 的 TSNE 只有 fit_transform,没有把新样本映射到旧图的 transform。原因不是接口遗漏,而是标准目标把所有训练坐标联合优化:加入新点会改变归一化、吸引和排斥关系,旧点的最佳位置也可能移动。
若必须处理新样本,应明确改变了算法语义:
- 用支持插值/参数化映射的实现,并在独立数据上验证;
- 训练监督模型从原特征预测已有 t-SNE 坐标,但它只是近似器;
- 改用有训练外变换的 UMAP、PCA 或自编码器;
- 为可复现报告固定训练样本集合,不把每批新数据悄悄拼入旧图。
二维 t-SNE 坐标通常用于探索和展示,不应未经验证直接作为稳定线上特征。
11 困惑度、学习率和初始化怎样诊断?#
- 扫描多个困惑度:例如 5、15、30、50;只相信跨合理范围反复出现的局部关系。
- 固定样本与预处理:每次图变化只能归因于参数或种子,而不是数据过滤。
- 比较多个随机种子:目标非凸;局部群体若不断拆合,结论不稳定。
- 查看 KL 轨迹:早期夸大阶段若代价持续上升,夸大因子或学习率可能过高。
- 识别典型形状:学习率过高常出现近似等距的“球”;过低常挤成密云并伴随少量离群点。
- 同时看邻域指标:信任度(Trustworthiness)检查二维近邻中有多少是原空间伪邻居,但仍不能验证全局距离。
12 最常见的图形误读与调试路径#
- 把簇间空白当真实距离:t-SNE 没有保存全局尺度;对候选簇回到原特征计算距离、分类可分性与稳定性。
- 把点团面积当样本方差:每点自适应带宽会部分平衡密度,二维团块面积不等于原空间密度。
- 看到分团就宣布类别存在:随机连续流形也可能呈现岛状;用已知模拟数据、重采样和外部标签盲测。
- 用标签参与调图再声称发现标签:颜色与参数选择会引入确认偏差;先固定无标签流程。
- 忽略尺度与度量:标准化、余弦距离或领域距离会改写高维邻居;先验证近邻检索本身。
- 把重复点画成一个点:记录重复行计数与透明度;重叠不代表只有一个样本。
- 只保存图片:同时保存数据版本、样本顺序、预处理器、全部参数、库版本、种子与坐标数组。
最短调试顺序是:检查非有限值和重复点 → 检查距离与近邻 → 扫困惑度和种子 → 观察 KL 与信任度 → 回到原空间验证视觉假设。
13 失败场景与相近方法边界#
| 方法 | 主要保存对象 | 新样本映射 | 适合回答的问题 |
|---|---|---|---|
| PCA | 全局线性方差/重构 | 有确定 transform | 大方向、压缩、稳定下游特征 |
| Isomap | 近邻图上的全局测地距离 | 有近似 transform | 单一流形的全局展开 |
| LLE | 局部线性重构权重 | 有局部重构式变换 | 局部仿射结构是否可保持 |
| t-SNE | 高低维邻域联合概率 | 标准形式没有 | 哪些样本在多个局部尺度上互为邻居 |
| UMAP | 模糊近邻图与低维边权 | 有近似 transform | 更可扩展的局部拓扑可视化与表示 |
t-SNE 在样本极少、噪声邻居不可靠、距离度量不合语义或数据持续变化时尤其容易误导。它也不会自动完成聚类:先做 t-SNE 再对二维坐标运行 K 均值,得到的更多是可视化目标下的分组,而非原空间聚类保证。
14 今天真正需要记住什么?#
t-SNE 先为每个高维样本选择满足目标困惑度的局部带宽,构造对称联合概率 ;再用 Student-t 重尾核从低维坐标构造 ,最小化 。它擅长保存局部邻居,却故意放弃可直接解释的全局距离、面积和方向;可靠使用依赖多尺度、多种子、原空间复核和完整实验记录。
15 思考题与小练习#
- 对 的手算例,把 从 1 增到 2,重新计算 、 与困惑度。为什么困惑度会上升?
- 构造两条连续相连但密度不同的曲线,扫描
perplexity={5,30,80}。哪些岛状结构跨尺度稳定?哪些只是参数造成的断裂? - 在同一数据上比较
init='pca'与init='random'的五个种子,先做普罗克拉斯特对齐(Procrustes Alignment),再比较 10 近邻重合率。坐标旋转与邻域改变应怎样区分?
相关工作#
- van der Maaten & Hinton (2008), Visualizing Data using t-SNE ↗:t-SNE 的高低维概率、重尾核与优化方法原始论文。
- van der Maaten (2014), Accelerating t-SNE using Tree-Based Algorithms ↗:Barnes-Hut 近似如何加速排斥项。
- Linderman et al. (2019), Fast Interpolation-based t-SNE ↗:用插值和 FFT 扩展到更大样本的代表工作。
- Kobak & Berens (2019), The Art of Using t-SNE for Single-Cell Transcriptomics ↗:多尺度参数、初始化和生物数据解释的系统实践。
- scikit-learn 1.9: TSNE ↗:当前参数、属性、复杂度与官方实践提示。
16 下一篇预告#
t-SNE 用概率邻域做出了清晰的局部图,却没有自然的新样本映射,且全局结构很难解释。下一篇将研究 UMAP:它怎样把每个点的近邻半径变成模糊图边权,再用交叉熵同时吸引真邻居、采样排斥非邻居,并由 n_neighbors 与 min_dist 控制局部—全局和团块紧密度。