当散点图不够用时:用 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()
使用 t-SNE 的几个关键点
1. 困惑度(Perplexity)怎么选?
困惑度可以理解为“考虑多少个近邻”。这个参数对结果影响很大。
- 小数据集(几百个点) :困惑度设为 5-30
- 中等数据集(几千个点) :困惑度设为 30-50
- 大数据集(上万个点) :困惑度可以更大
一个实用的建议是:尝试多个困惑度值,观察结果是否稳定。如果不同困惑度下呈现的聚类结构一致,说明结果是可靠的。
2. 一定要标准化数据
t-SNE 基于距离计算,如果不同特征的尺度差异很大(比如一个特征范围是 0-1,另一个是 0-10000),尺度大的特征会主导距离计算。用 StandardScaler 做标准化是必要的步骤。
3. 多次运行,观察稳定性
t-SNE 的优化过程是随机的,不同次运行可能得到不同的结果。
建议多次运行,观察聚类结构是否一致。
4. 不要过度解读
t-SNE 图上的簇间距离没有定量意义。两个簇离得远,不一定意味着它们在原始空间中真的离得很远;两个簇离得近,也不一定意味着它们真的很相似。
t-SNE 主要告诉我们的是:哪些点倾向于聚在一起。
5. 迭代要够
确保 max_iter 足够大,让算法收敛到稳定状态。通常 1000 次迭代是起步。
小结
从散点图的 4 维天花板,到 t-SNE 可以处理成百上千维的数据——这确实是一个巨大的进步。
t-SNE 不是万能的,它有计算成本高、结果不稳定、坐标不可解释等缺点。
但在探索性数据分析的场景下,当我们面对一堆高维数据不知所措时,t-SNE 提供了一个非常直观的入口:把数据“画”出来,看看它长什么样。
记住 t-SNE 最核心的价值:它不告诉你“为什么”,但它告诉你“谁和谁在一起” 。至于为什么它们在一起,那就是你作为数据分析师需要进一步探究的了。
下次当你面对十几维、几十维的数据不知从何下手时,不妨试试 t-SNE——让数据自己“说话”。

浙公网安备 33010602011771号