当散点图不够用时:用 t-SNE 可视化多维数据
从散点图的局限说起
在数据分析的工作中,我们经常会遇到这样一个问题:如何把数据的“样子”展示给别人看?
如果数据只有两个维度,事情很简单——画一张散点图就好了。
横轴一个变量,纵轴一个变量,每个点代表一条数据,分布一目了然。
如果数据有三个维度呢?也还行——散点图加上颜色,用不同颜色代表第三个维度的取值。
比如用散点图展示城市的人口、GDP和面积,点的颜色深浅表示面积大小。
如果数据有四个维度呢?可以再加一个点的大小(半径),用点的大小表示第四个维度。
比如散点图展示学生的学习时间、考试成绩、出勤率和作业完成度——横轴是学习时间,纵轴是考试成绩,颜色表示出勤率,点的大小表示作业完成度。
那五个维度呢?六个呢?十个呢?
散点图到此为止了。当数据的维度超过4个,传统的散点图就再也无能为力了。我们无法在二维平面上用有限的视觉通道(位置、颜色、大小、形状)去表达十几个甚至上百个维度的信息。
但现实世界中的数据,往往就是高维的。一份用户画像可能有几十个特征,一篇文档的向量表示可能有几百维,一张图片经过神经网络提取特征后可能有上千维。这些数据里藏着丰富的结构和模式,但我们看不见。
直到我看到了t-SNE。
什么是 t-SNE?
t-SNE(t-Distributed Stochastic Neighbor Embedding,t-分布随机邻域嵌入)是一种非线性降维技术,它的目标是把高维空间中的数据点映射到二维或三维空间,同时尽可能保留高维空间中数据点之间的局部邻域关系。
说人话就是:在高维空间中离得近的点,在 t-SNE 图上也会离得近;在高维空间中离得远的点,在 t-SNE 图上也会离得远。
t-SNE的核心思想是概率的——它把数据点之间的相似度转化为概率分布,然后在低维空间中寻找一个分布,使得这两个分布尽可能接近。
它不需要预先指定簇的数量,能够自动揭示数据中潜在的聚类结构。
t-SNE 的优点
- 擅长展示聚类效果:t-SNE 能够很好地将数据中的自然分组在二维平面上呈现出来。
- 保留局部结构:在高维空间中相近的数据点在低维空间中仍然会靠在一起。
- 无需预先定义簇数量:不像 K-Means 等聚类算法需要指定分成几类。
t-SNE 的缺点(⚠️ 重要!)
- 计算成本高:尤其是在处理大型数据集时,t-SNE 的运行时间可能很长。
- 结果不稳定:不同次运行可能得到不同的结果。
- 坐标轴不可解释:t-SNE 的两个轴不代表任何原始特征的逻辑组合,不像 PCA 的主成分那样可以解释含义。
- 参数敏感:困惑度(perplexity)等参数的选择会显著影响结果。
- 可能产生误导:t-SNE 有时会夸大簇的大小差异,或者把本不相关的点凑到一起。
尤其需要注意的是,t-SNE 的全局距离(不同簇之间的距离)是没有意义的。
它擅长展示“谁和谁是一伙的”,但不擅长展示“这两个群体之间到底有多远”。
什么时候用 t-SNE?
t-SNE最适合的场景是:你想探索高维数据中是否存在自然的聚类结构。
典型的使用场景包括:
- 文本数据的可视化:将文档的向量表示(Embedding)降维到二维,观察不同主题的文档是否自然聚在一起。
- 图像数据的可视化:将图片的特征向量降维,观察不同类别的图片在空间中的分布。
- 用户行为分析:将用户的多维行为特征降维,识别不同的用户群体。
- 模型输出的解释:可视化深度学习模型中间层的特征表示,帮助理解模型学到了什么。
动手实践:两个生活案例
光说不练假把式。接下来我们用两个贴近生活的例子,看看t-SNE到底怎么用。
案例一:葡萄酒品鉴数据可视化
假设你是一个葡萄酒爱好者,收集了 200 款红葡萄酒的 13 个特征数据(酒精含量、苹果酸含量、灰分含量、镁含量、总酚含量等)。
你想看看这些葡萄酒是否可以按照产地自然地分成几类。
这个数据集是scikit-learn这个机器学习库中自带的。
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_wine from sklearn.manifold import TSNE from sklearn.preprocessing import StandardScaler # 加载葡萄酒数据集(13个特征,3个产地类别) wine = load_wine() X = wine.data # 13个特征 y = wine.target # 产地标签(0, 1, 2) # 标准化数据(t-SNE 对尺度敏感) X_scaled = StandardScaler().fit_transform(X) # 使用 t-SNE 降维到 2 维 tsne = TSNE(n_components=2, perplexity=30, random_state=42, max_iter=1000) X_tsne = tsne.fit_transform(X_scaled) # 可视化 plt.figure(figsize=(10, 8)) colors = ['red', 'green', 'blue'] for i, label in enumerate(np.unique(y)): mask = y == label plt.scatter(X_tsne[mask, 0], X_tsne[mask, 1], c=colors[i], label=f'产地 {i}', alpha=0.7, s=60) plt.title('t-SNE 可视化:葡萄酒按产地聚类', fontsize=14) plt.xlabel('t-SNE 维度 1') plt.ylabel('t-SNE 维度 2') plt.legend() plt.show()
运行这段代码,你会看到不同产地的葡萄酒在二维平面上自然地分成了几个簇。
原本 13 个维度的数据,现在可以直观地看到聚类结构——这就是t-SNE的魔力。
案例二:电影推荐系统中的用户画像
假设你有一个电影推荐系统,收集了 500 个用户的观影偏好数据。每个用户有 20 个特征:对不同类型电影(动作、喜剧、科幻、爱情、恐怖、纪录片等)的偏好程度。
你想看看这些用户是否可以自然地分成不同的“观影人群”,以便做更精准的推荐。
import numpy as np import matplotlib.pyplot as plt from sklearn.manifold import TSNE from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 模拟数据:500个用户,20个电影类型偏好特征 np.random.seed(42) n_users = 500 n_features = 20 # 生成模拟数据:假设存在4种不同的用户类型 X = np.random.randn(n_users, n_features) # 为不同用户类型添加不同的偏好模式 # 类型0:喜欢动作和科幻 X[:125, 0:5] += 2.0 # 类型1:喜欢爱情和喜剧 X[125:250, 5:10] += 2.0 # 类型2:喜欢恐怖和悬疑 X[250:375, 10:15] += 2.0 # 类型3:喜欢纪录片和历史 X[375:500, 15:20] += 2.0 # 标准化 X_scaled = StandardScaler().fit_transform(X) # 先用 K-Means 聚类(这里只是为了给点打标签做可视化) kmeans = KMeans(n_clusters=4, random_state=42) labels = kmeans.fit_predict(X_scaled) # t-SNE 降维 tsne = TSNE(n_components=2, perplexity=30, random_state=42, max_iter=1000) X_tsne = tsne.fit_transform(X_scaled) # 可视化 plt.figure(figsize=(10, 8)) scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=labels, cmap='viridis', alpha=0.6, s=50) plt.colorbar(scatter, label='用户类型') plt.title('t-SNE 可视化:电影用户聚类', fontsize=14) plt.xlabel('t-SNE 维度 1') plt.ylabel('t-SNE 维度 2') plt.show()
在这个例子中,t-SNE 帮助我们直观地看到了 500 个用户在偏好空间中的分布——哪些用户是“动作片爱好者”,哪些是“爱情片爱好者”,一目了然。
一个完整的代码模板
如果你想把 t-SNE 应用到自己的数据上,下面这个模板可以直接套用:
import numpy as np import matplotlib.pyplot as plt from sklearn.manifold import TSNE from sklearn.preprocessing import StandardScaler # 1. 准备数据 # X 是你的高维数据,形状为 (n_samples, n_features) # labels 是每个样本的类别标签(可选,用于着色) # X = ... # 2. 标准化(重要!) X_scaled = StandardScaler().fit_transform(X) # 3. t-SNE 降维 tsne = TSNE( n_components=2, # 降维到2维 perplexity=30, # 困惑度,通常 5-50 random_state=42, # 固定随机种子,保证结果可复现 max_iter=1000 # 迭代次数,确保收敛 ) X_tsne = tsne.fit_transform(X_scaled) # 4. 可视化 plt.figure(figsize=(10, 8)) if labels is not None: scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=labels, cmap='tab10', alpha=0.6, s=50) plt.colorbar(scatter) else: plt.scatter(X_tsne[:, 0], X_tsne[:, 1], alpha=0.6, s=50) plt.title('t-SNE Visualization') plt.xlabel('t-SNE dimension 1') plt.ylabel('t-SNE dimension 2') plt.show()
