[无监督学习] 深入解析t-SNE:从原理到实战应用

张开发
2026/4/7 14:38:43 15 分钟阅读

分享文章

[无监督学习] 深入解析t-SNE:从原理到实战应用
1. 从“一团乱麻”到“一目了然”为什么我们需要t-SNE如果你处理过高维数据比如成千上万个特征的表格或者几百个通道的图像特征你肯定有过这种体验数据就像一团纠缠在一起的毛线你根本看不清它的内在结构。传统的图表比如散点图最多只能展示三个维度面对成百上千维的数据我们几乎是“睁眼瞎”。这时候无监督学习中的降维技术就成了我们的“眼睛”而t-SNE无疑是这双眼睛中最锐利、最擅长发现局部结构的那一只。我刚开始接触高维数据可视化时试过主成分分析PCA。PCA确实快能把数据的主要变化方向找出来但它有个大问题它只关心数据的全局方差。简单来说PCA就像是从很远的地方看一座城市你能看到城市的轮廓和几个最高的地标但看不清街区里的小巷和邻里关系。而很多机器学习问题的关键恰恰就藏在这些“邻里关系”里——哪些数据点本质上是相似的应该聚在一起。t-SNE的出现就是为了解决这个问题。它的全称是t-分布随机邻域嵌入。这个名字听起来有点唬人但它的目标很单纯把高维空间中数据点之间的“远近亲疏”关系尽可能原汁原味地映射到二维或三维平面上。它不关心数据的全局坐标只关心“谁和谁挨得近”。经过t-SNE处理后的数据图如果两个点离得近那它们在原始高维空间里也大概率是“邻居”如果离得远那它们原本就没什么关系。想象一下你有一堆混合了猫、狗、汽车、飞机的图片特征向量。用PCA降维后你可能看到动物和交通工具大致分开了但猫和狗可能还混在一起。而用t-SNE你很有希望看到四个清晰的簇猫聚一堆狗聚一堆汽车和飞机也各自成团。这种能力使得t-SNE在探索性数据分析、论文图表绘制、甚至深度神经网络中间层特征的可视化中都成了不可或缺的神器。它特别适合帮你回答一个问题“我的数据里到底藏着几类不同的东西”2. 庖丁解牛t-SNE的工作原理其实很“人性化”很多教程一上来就堆公式容易把人吓跑。咱们换个方式我用一个“派对社交”的比喻来拆解t-SNE的核心思想你会发现它其实非常直观。假设你举办了一个大型派对来宾都是高维空间里的数据点。在派对上你不会拿尺子去量每个人之间的精确距离而是通过观察“谁和谁在频繁聊天”来判断他们的亲疏关系。t-SNE的工作就分两步走第一步在高维派对原始空间里绘制“社交关系图”。对于每一个数据点比如来宾Alicet-SNE会以她为中心画一个高斯分布可以想象成一个柔和的、影响力随距离衰减的“社交圈”。然后计算其他所有来宾数据点落入Alice这个“社交圈”的概率。这个概率就是相似度。离Alice越近的点这个概率越高越远的点概率越低。但关键来了这个概率不是对称的Bob对Alice的“关注度”和Alice对Bob的“关注度”可能不同。t-SNE会把这两个条件概率对称化得到一个联合概率分布p_ij。这个p_ij就精准刻画了高维空间中每对数据点之间“被认为是邻居”的强度。它捕捉了数据的局部结构——谁和谁是一个小圈子。第二步在二维海报低维空间上重新安排来宾位置。现在我们需要把这场复杂的派对画在一张二维海报上进行宣传。我们随机地把所有来宾的名字对应的低维点y_i撒在海报上。显然一开始的位置是乱七八糟的反映不出真实的社交关系。在海报这个二维世界里我们换一种方式来定义“邻居”关系使用自由度为1的t分布也叫柯西分布。为什么换因为t分布比高斯分布的“尾巴”更厚、更长。这意味着在低维空间里对于中等距离的点t分布会给它们赋予一个相对更高的“相似度”概率q_ij。第三步让海报反映真实的派对。现在我们有了两个关系网一个是真实派对的高维关系网p_ij一个是海报上瞎画的低维关系网q_ij。t-SNE的核心优化目标就是调整海报上每个点的位置让q_ij变得和p_ij尽可能像。它使用一个叫KL散度的指标来衡量两个分布的差异并通过梯度下降法不断移动低维点y_i来最小化这个差异。这里就体现了t分布的妙用因为t分布尾巴长它对“中等距离”的点比较“宽容”会倾向于给它们一个还不错的q_ij值。而在优化过程中为了匹配高维的p_ij算法会努力把那些在高维是邻居的点p_ij大在低维拉得更近让q_ij也变大同时把那些在高维不相关的点p_ij非常小在低维推得更远因为即使推远了在t分布下q_ij也不会变得极小优化压力不大。这个“拉近”和“推远”的效应同时作用最终使得在低维平面上不仅局部簇结构清晰而且不同簇之间会自然拉开空隙视觉效果非常棒。所以你可以把t-SNE理解为一个有强迫症的派对记录员他一定要把高维空间里复杂的社交网络用一种能突出小团体、隔离陌生人的方式完美地誊写到二维平面上。3. 实战用Python和sklearn玩转t-SNE可视化原理懂了手就痒了。咱们直接上代码用最经典的sklearn库里的手写数字数据集来实战一遍。这个数据集有1797张8x8像素的手写数字图片每张图片展开就是一个64维的向量。我们的目标就是把这64维的数据降到2维看看数字0到9能不能自然地分开。3.1 基础版五分钟出图首先我们把必要的工具包和数据集准备好。import matplotlib.pyplot as plt import numpy as np from sklearn.datasets import load_digits from sklearn.manifold import TSNE import seaborn as sns # 用seaborn让图更好看 # 加载数据 digits load_digits() X digits.data # 这是我们的高维数据形状是(1797, 64) y digits.target # 这是标签0-9我们用来上色 target_names digits.target_names print(f数据形状{X.shape}) print(f标签类别{np.unique(y)})接下来创建t-SNE模型并拟合数据。这里会遇到几个关键参数咱们先用一个常用的配置。# 创建t-SNE模型 tsne TSNE( n_components2, # 降到2维 perplexity30, # 最重要的参数之一通常取值在5到50之间 n_iter1000, # 优化迭代次数通常1000以上 random_state42 # 随机种子固定它可以让结果可重复 ) # 执行降维拟合这可能需要几秒钟到几分钟取决于数据量 print(开始t-SNE降维请稍候...) X_tsne tsne.fit_transform(X) print(降维完成) print(f降维后数据形状{X_tsne.shape})最后我们把降维后的结果画出来。用标签y来给点涂上颜色这样就能直观看到同类数字是否聚在一起了。# 设置图形风格 plt.style.use(seaborn-v0_8-darkgrid) fig, ax plt.subplots(figsize(10, 8)) # 为每个类别画散点图 scatter ax.scatter(X_tsne[:, 0], X_tsne[:, 1], cy, cmapSpectral, s15, alpha0.8) # 添加图例和标题 legend ax.legend(scatter.legend_elements()[0], target_names, title数字类别, locbest, fontsize9) ax.add_artist(legend) ax.set_title(t-SNE可视化手写数字数据集 (perplexity30), fontsize14) ax.set_xlabel(t-SNE 第一维) ax.set_ylabel(t-SNE 第二维) plt.tight_layout() plt.show()运行这段代码你应该能得到一张非常漂亮的图。图上会有10个颜色各异的“云团”每个云团基本对应一个数字。你会发现数字1、0这些形状独特的可能聚得特别紧而数字3、5、8这些形状容易混淆的它们的云团边界可能有些模糊甚至有小部分点混在一起。这正是t-SNE能力的体现——它揭示了数据内在的相似性结构甚至能反映出类别之间的混淆程度。3.2 参数调优像老手一样控制你的t-SNE上面我们用了默认参数但要想得到最佳效果或者解决一些奇怪的问题你必须理解这几个核心参数perplexity困惑度这是t-SNE最重要的参数没有之一。你可以把它理解为每个点考虑多少个邻居。值越小模型越关注局部结构可能形成很多小簇值越大模型越关注全局结构簇会变大但可能模糊局部细节。对于手写数字这种有明确簇的数据30是个不错的起点。如果你的数据簇大小差异很大可能需要调整。一个经验法则是perplexity值应该小于你的数据点数。我通常会在5、30、50这几个值上都试一下看看可视化结果的稳定性。n_iter迭代次数优化过程的迭代次数。太少了可能没收敛图看起来乱糟糟的太多了浪费时间。一般1000是足够的。你可以观察模型的kl_divergence_属性如果它在最后迭代轮次中基本不变了就说明收敛了。在实际操作中我经常先设1000跑一次如果发现图上还有明显的“拉丝”状结构点连成线说明还没完全稳定就增加到2000或3000再跑。learning_rate学习率梯度下降的步长。默认是200。如果学习率太大点可能会在图上“爆炸式”散开形成一些空洞的大圈如果太小优化会太慢点挤成一团打不开。如果你发现图形很奇怪可以尝试调低到50或调高到1000试试。sklearn现在有个auto模式通常效果不错。random_state务必设置t-SNE的优化起点是随机的不同的随机种子会导致每次降维后点的绝对位置不同但相对聚类关系应该稳定。设置一个固定的random_state可以确保你的实验是可重复的这在写报告或论文时至关重要。让我们做个对比实验看看perplexity的影响# 对比不同perplexity perplexities [5, 30, 100] fig, axes plt.subplots(1, 3, figsize(18, 5)) for i, perp in enumerate(perplexities): tsne TSNE(n_components2, perplexityperp, n_iter1000, random_state42) X_embedded tsne.fit_transform(X) ax axes[i] scatter ax.scatter(X_embedded[:, 0], X_embedded[:, 1], cy, cmapSpectral, s10, alpha0.7) ax.set_title(fPerplexity {perp}, fontsize13) ax.set_xlabel(t-SNE 1) ax.set_ylabel(t-SNE 2) plt.tight_layout() plt.show()运行后你会看到perplexity5时每个数字大类内部可能会分裂成很多小簇结构非常细碎perplexity100时全局结构更清晰但一些局部细节比如数字7的不同写法可能被平滑掉了。perplexity30则是一个很好的折中。4. 避坑指南t-SNE的这些“坑”我帮你踩过了t-SNE用起来爽但坑也不少。我结合自己多年的经验总结了几条最重要的注意事项能帮你省下大量调试时间。第一t-SNE的结果不能用于除了可视化以外的任何下游任务这是最重要的一条。t-SNE输出的低维坐标是高度非线性的并且每次运行结果都可能因为初始化而整体旋转、平移。你不能把t-SNE降维后的二维数据拿去训练一个分类器然后指望它在新的测试数据上还有效。因为对于新来的一个高维数据点你无法用训练好的t-SNE模型将其稳定地映射到之前那个二维空间的对应位置transform方法存在但通常不用于新数据。它的唯一目的就是给人看帮助人理解数据。第二t-SNE不保留全局距离只保留局部排名。在t-SNE图上两个簇之间的距离是没有明确意义的。一个簇在左边另一个在右边并不代表它们在原始空间里就一定有那么远的欧氏距离。它只保证“同一个簇里的点离得近”这个局部关系可靠。所以千万不要从t-SNE图上测量簇间距离来做定量分析。第三簇的大小在图上占据的面积可能具有误导性。t-SNE倾向于在低维空间中将密集的簇散开将稀疏的簇压缩。所以一个在图上看起来很大的簇可能在原始高维空间里并不一定数据量最多或方差最大它可能只是原本的分布比较稀疏被算法拉开了而已。第四超参数敏感需要多次尝试。正如我们前面看到的perplexity对结果影响巨大。对于全新的数据集没有银弹参数。我的标准流程是先用perplexity30跑一次得到一个基准图。然后围绕这个值向下如5, 10, 20和向上如40, 50, 100各试几个值。如果不同perplexity下图的主要簇结构是稳定的那这个结构就比较可靠。如果变化剧烈说明数据本身可能没有特别清晰的簇状结构或者你需要结合领域知识进一步分析。第五计算成本高大数据集慎用。t-SNE的时间复杂度大概是O(N^2)对于超过1万个样本的数据集计算就会变得非常慢。这时候有几种策略1.先做PCA预降维这是最常用的技巧。先用PCA把数据降到50或100维去除噪声和冗余然后再喂给t-SNE。这能大幅加速且有时效果更好。2.使用近似算法sklearn的TSNE默认使用了Barnes-Hut近似算法将复杂度降到O(N log N)能处理上万级别的数据。对于更大的数据可能需要采样或使用其他专门的大规模可视化工具。第六解读时需要结合标签或领域知识。t-SNE是无监督的它只是把相似的点放一起。图上出现的簇具体代表什么需要你用数据的真实标签如果有的话去着色验证或者依靠你对业务的理解去解释。有时候它揭示出的子簇可能对应着某个你从未考虑过的数据子类这才是它最大的价值所在。5. 不只是对比t-SNE在真实项目中的高级玩法掌握了基础咱们再拔高一点看看t-SNE在一些更复杂、更贴近实际项目的场景中怎么用。5.1 可视化深度神经网络的特征这是我个人最常用的场景之一。训练一个图像分类网络我们常常想知道倒数第二层也就是分类头之前的那层输出的特征到底学得好不好。这时候t-SNE就是照妖镜。# 假设我们已经有一个训练好的模型model和一批测试数据X_test, y_test # 1. 提取特征 from torchvision import models, transforms import torch # 以ResNet为例移除最后的全连接层获取特征提取器 feature_extractor models.resnet18(pretrainedTrue) feature_extractor.fc torch.nn.Identity() # 去掉分类头 feature_extractor.eval() # 对测试集图片提取特征 features_list [] labels_list [] with torch.no_grad(): for images, labels in test_loader: # 假设你有一个DataLoader features feature_extractor(images) features_list.append(features.numpy()) labels_list.append(labels.numpy()) all_features np.vstack(features_list) all_labels np.hstack(labels_list) # 2. 使用PCA进行预降维可选但推荐 from sklearn.decomposition import PCA pca PCA(n_components50) features_pca pca.fit_transform(all_features) # 3. t-SNE可视化 tsne TSNE(n_components2, perplexity40, random_state42) features_tsne tsne.fit_transform(features_pca) # 4. 绘图 plt.figure(figsize(10,8)) scatter plt.scatter(features_tsne[:,0], features_tsne[:,1], call_labels, cmaptab20, s10, alpha0.7) plt.colorbar(scatter) plt.title(t-SNE Visualization of Deep Features from ResNet) plt.show()如果网络训练得好不同类别的特征在t-SNE图上应该会形成界限分明的簇。如果不同类别的点混作一团那可能意味着网络没有学到判别性特征或者你的任务本身类别界限就很模糊。这对于模型诊断和调优有巨大帮助。5.2 文本数据的探索词向量的世界地图对于自然语言处理词向量如Word2Vec, GloVe是高维空间中的点。用t-SNE可以把这些词向量降到二维绘制一张“语义地图”。from gensim.models import KeyedVectors # 假设我们加载了一个预训练的词向量模型 # model KeyedVectors.load_word2vec_format(GoogleNews-vectors-negative300.bin, binaryTrue) # 选取一些有关系的词 words [king, queen, man, woman, paris, france, london, england, car, bus, train, plane, apple, banana, orange, fruit] # 获取它们的向量 word_vectors np.array([model[w] for w in words if w in model]) # t-SNE降维 tsne TSNE(n_components2, random_state42, perplexity5) # 词数量少perplexity调小 word_vecs_2d tsne.fit_transform(word_vectors) # 绘图并标注 plt.figure(figsize(12,10)) plt.scatter(word_vecs_2d[:,0], word_vecs_2d[:,1]) for i, word in enumerate([w for w in words if w in model]): plt.annotate(word, xy(word_vecs_2d[i,0], word_vecs_2d[i,1]), fontsize12) plt.title(t-SNE of Word Embeddings) plt.show()在这张图上你大概率会看到“king”和“queen”离得很近“paris”和“france”挨在一起而水果类的词聚在另一区域。这种直观展示对于向非技术人员解释词向量的语义特性非常有说服力。5.3 与UMAP的对比我该选哪个近年来另一个降维算法UMAP风头正劲。它和t-SNE目标相似但数学基础不同。简单对比一下特性t-SNEUMAP核心目标保持局部邻居概率分布一致保持局部流形结构假设数据均匀分布在拓扑空间全局结构几乎不保留能更好地保留全局结构簇间关系更可信速度较慢尤其大数据集通常更快尤其大数据集优势明显超参数对perplexity敏感对n_neighbors类似perplexity和min_dist敏感新数据映射困难通常需要重新训练可以通过transform映射新数据近似可视化效果簇内更紧密簇间空隙大艺术感强结构更“清晰”更像传统的散点图怎么选呢我的经验是如果你纯粹为了得到一张最具视觉冲击力、最能凸显局部聚类的探索性图表并且数据量不大比如几千条t-SNE依然是首选它的图往往更“漂亮”簇更分离。如果你需要兼顾一些全局结构信息或者数据量很大几万以上或者需要将降维模型用于后续近似转换新数据那么UMAP是更好的选择。在实际项目中我经常两个都跑一下对比着看从不同角度理解数据。安装UMAP也很简单pip install umap-learn。用法和sklearn的t-SNE几乎一样你可以轻松地替换上去试试效果。t-SNE就像一把精密的雕刻刀它能把你数据中最细微的局部结构纹理都清晰地展现出来。虽然它有自己的脾气比如参数敏感、结果不可用于建模但只要你理解了它的工作原理和这些注意事项它就能成为你数据探索工具箱里最得心应手的利器之一。下次当你面对一堆高维数据感到无从下手时别犹豫跑一个t-SNE看看说不定惊喜就藏在那些突然显现的彩色云团里。

更多文章