原始博文地址:https://mp.weixin.qq.com/s/nlw7nN7Hiel2O8BoCRKbgw
UMAP,全称,Uniform Manifold Approximation and Projection。
它的目标很朴素:把高维数据折叠到2D/3D里,同时尽量不破坏谁和谁更像的局部邻里关系。
首先,为什么要降维,为什么常用 UMAP?
高维数据人眼看不见,难以直观理解样本之间像不像;降维到2D/3D是探索式数据分析(EDA)最常用的入口。
PCA 适合线性结构,但遇到月牙形、瑞士卷这类非线性结构就容易压坏;t-SNE 的可视化很漂亮,但全局结构常常不可靠、参数难调、扩展到超大数据有点慢。
UMAP 结合了速度快结构保真度高可扩展参数直觉更强等优点,已经成为数据科学家在可视化和表征学习里的首发阵容。
01UMAP 核心逻辑
把 UMAP 想成三个步骤:交朋友 -> 拼地图 -> 折到纸上。
先交朋友:
-
对每个点,找出它最要好的 个邻居(参数 n_neighbors,比如15)。
-
朋友不是非黑即白,而是亲密度打分:越近越铁,越远越普通朋友。
再拼地图:
-
每个人手里有一张小范围邻里地图(谁和谁亲密)。
-
把全体人的小地图糊成一本网络地图;这时候你得到的是一个模糊的、带权重的邻接图。
最后折到纸上:
-
在2D/3D的纸上摆点,让铁哥们更靠近;不太熟的,尽量别挤在一起。
-
用一种交叉熵的目标函数,让高维的亲密度与低维的亲密度尽量一致。
-
有个关键参数 min_dist,控制铁哥们在2D里能靠多近,避免团成死疙瘩。
就是说,UMAP 就是在做把朋友网络画在纸上,既保留谁和谁是一伙的(局部结构),也尽可能让不同团体之间的相对远近有点道理(某种全局结构)。
02一个通俗例子
就好比说,教室里有一群小朋友,每两个人的亲密度不同:一起玩耍多的更亲、很少一起玩的不太熟。
第一步:老师让每个小朋友写下最要好的前15位朋友名单,并给每位朋友打分(比如共同游戏次数越多,分越高)。
第二步:老师把所有人的好朋友名单合在一起,做成一张友谊网。
第三步:要在地板上摆位置:
-
给每一对要好的小朋友之间拴一根橡皮筋,越要好橡皮筋越紧(想把他们拽近一点)。
-
同时准备一些回形针,对不太熟的一些人对进行轻微推开(不让他们全都挤在一团)。
-
放手之后,让橡皮筋和回形针在地上拉扯一会儿,大家就形成了比较舒服的坐姿,这就是UMAP的2D坐标。
min_dist 就像防挤条款:再怎么要好,也要留一点缝,不许坐在同一个小板凳上。
03核心原理
1) 邻域距离与亲密度(条件隶属度)
对于样本 ,通过 近邻搜索得到它的邻居集合,并计算距离 。
UMAP会估计一个本地连通阈值 和
缩放 ,将距离转为亲密度(条件概率/隶属度):

2) 模糊集合的对称化(像或运算)
把两个方向的亲密度合成一个无向的边权:![]()
3) 低维空间的相似度模型
在2D/3D中,UMAP用一条重尾曲线建模两点距离
的亲密度:
其中 由 min_dist 参数通过拟合确定(保证距离很近时曲线陡、远时尾巴长)。
4) 目标函数(交叉熵)
在低维中,希望 跟高维的
尽量一致,优化的目标是:
![]()
实现上通过负采样高效近似第二项,从而有很强的扩展性。
5) 参数直观
-
n_neighbors:控制朋友圈多大;小一点更强调局部结构,大一点更看全局形状。
-
min_dist:控制在2D里的最小拥挤度;越小团越紧,越大团越松。
-
metric:距离度量(欧氏、余弦等);文本/推荐等可考虑余弦。
04完整案例
我们构造一个多形状、非线性的数据集:瑞士卷、双月牙、S曲线、同心圆,各取若干样本,然后升维到50维,添加非线性特征与噪声,再用 UMAP 还原到2D。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_swiss_roll, make_moons, make_s_curve, make_circles
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
from sklearn.manifold import trustworthiness, TSNE
import umap
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)
np.random.seed(42)
# 1) 数据集:4种形状,各自添加非线性特征与噪声,升维到50维
def make_virtual_dataset(n_each=1000, noise_dim=44, random_state=42):
rs = np.random.RandomState(random_state)
# 瑞士卷(3D)
X_sw, t_sw = make_swiss_roll(n_samples=n_each, noise=0.05, random_state=rs)
y_sw = np.array(["SwissRoll"]*n_each)
# 双月牙(2D);补一个零维拼成3D再处理
X_mo, y_dummy = make_moons(n_samples=n_each, noise=0.05, random_state=rs)
X_mo = np.c_[X_mo, np.zeros(n_each)]
y_mo = np.array(["Moons"]*n_each)
# S曲线(3D)
X_sc, t_sc = make_s_curve(n_samples=n_each, noise=0.05, random_state=rs)
y_sc = np.array(["SCurve"]*n_each)
# 同心圆(2D);补一个零维拼成3D
X_ci, y_dummy2 = make_circles(n_samples=n_each, noise=0.05, factor=0.5, random_state=rs)
X_ci = np.c_[X_ci, np.zeros(n_each)]
y_ci = np.array(["Circles"]*n_each)
X3 = np.vstack([X_sw, X_mo, X_sc, X_ci]) # 基础3维
labels = np.concatenate([y_sw, y_mo, y_sc, y_ci])
# 构造非线性高维特征:sin/cos 的随机投影 + 高斯噪声
W1 = rs.normal(size=(3, noise_dim//2))
W2 = rs.normal(size=(3, noise_dim - noise_dim//2))
nonlin1 = np.sin(X3 @ W1)
nonlin2 = np.cos(X3 @ W2)
noise = 0.2 * rs.normal(size=(X3.shape[0], 3))
X_high = np.c_[X3 + noise, nonlin1, nonlin2] # 3 + noise_dim ≈ 47 ~ 50维
scaler = StandardScaler()
X_high = scaler.fit_transform(X_high)
return X_high, labels
X, y = make_virtual_dataset(n_each=1000, noise_dim=47, random_state=42)
print("数据集维度:", X.shape, "类别分布:", pd.Series(y).value_counts().to_dict())
palette = {
"SwissRoll": "#e41a1c",
"Moons": "#377eb8",
"SCurve": "#4daf4a",
"Circles": "#984ea3"
}
colors = pd.Series(y).map(palette).values
# 2) 图1:PCA三维投影,观察高维数据的一个窗口
pca3 = PCA(n_components=3, random_state=42)
X_pca3 = pca3.fit_transform(X)
fig = plt.figure(figsize=(9, 7))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X_pca3[:, 0], X_pca3[:, 1], X_pca3[:, 2], c=colors, s=6, alpha=0.8)
ax.set_title("图1:高维数据的PCA三维投影", fontsize=14)
ax.set_xlabel("PC1"); ax.set_ylabel("PC2"); ax.set_zlabel("PC3")
plt.tight_layout()
plt.show()
# 3) 训练UMAP
umap_model = umap.UMAP(
n_neighbors=15,
min_dist=0.05,
n_components=2,
metric="euclidean",
random_state=42
)
X_umap = umap_model.fit_transform(X)
plt.figure(figsize=(8, 7))
plt.scatter(X_umap[:, 0], X_umap[:, 1], c=colors, s=6, alpha=0.85)
plt.title("图2:UMAP二维嵌入(n_neighbors=15, min_dist=0.05)", fontsize=14)
plt.xlabel("UMAP-1"); plt.ylabel("UMAP-2")
plt.tight_layout()
plt.show()
# 4) 参数网格对比:不同n_neighbors与min_dist组合的效果
def plot_umap_grid(X, y, palette, nn_list=(5, 15, 50), md_list=(0.0, 0.3, 0.8), random_state=42):
fig, axes = plt.subplots(len(nn_list), len(md_list), figsize=(14, 12))
for i, nn in enumerate(nn_list):
for j, md in enumerate(md_list):
um = umap.UMAP(n_neighbors=nn, min_dist=md, n_components=2, random_state=random_state)
emb = um.fit_transform(X)
ax = axes[i, j]
cols = pd.Series(y).map(palette).values
ax.scatter(emb[:, 0], emb[:, 1], c=cols, s=6, alpha=0.85)
ax.set_xticks([]); ax.set_yticks([])
ax.set_title(f"n_neighbors={nn}, min_dist={md}")
plt.suptitle("图3:UMAP参数网格可视化", fontsize=16, y=1.02)
plt.tight_layout()
plt.show()
plot_umap_grid(X, y, palette)
# 5) 信任度(trustworthiness)对比:UMAP vs PCA vs t-SNE
def compute_embeddings(X, random_state=42):
pca2 = PCA(n_components=2, random_state=random_state).fit_transform(X)
# 为了运行时间,t-SNE迭代步数不设太大
tsne2 = TSNE(n_components=2, perplexity=30, n_iter=600, init='pca', random_state=random_state).fit_transform(X)
umap2 = umap.UMAP(n_neighbors=15, min_dist=0.05, n_components=2, random_state=random_state).fit_transform(X)
return pca2, tsne2, umap2
pca2, tsne2, umap2 = compute_embeddings(X, random_state=42)
k_list = [5, 10, 15, 30, 50, 100]
tw_pca = [trustworthiness(X, pca2, n_neighbors=k) for k in k_list]
tw_tsne = [trustworthiness(X, tsne2, n_neighbors=k) for k in k_list]
tw_umap = [trustworthiness(X, umap2, n_neighbors=k) for k in k_list]
plt.figure(figsize=(8, 6))
plt.plot(k_list, tw_pca, marker='o', color="#1f77b4", label="PCA-2D")
plt.plot(k_list, tw_tsne, marker='o', color="#ff7f0e", label="t-SNE-2D")
plt.plot(k_list, tw_umap, marker='o', color="#2ca02c", label="UMAP-2D")
plt.xlabel("k(评估近邻数)")
plt.ylabel("Trustworthiness")
plt.title("图4:信任度对比", fontsize=14)
plt.legend()
plt.tight_layout()
plt.show()
# 6) 轮廓系数对比:量化类内紧凑和类间分离
def algo_silhouette(X_orig, y, random_state=42):
res = {}
algo = {
"PCA-2D": PCA(n_components=2, random_state=random_state).fit_transform(X_orig),
"t-SNE-2D": TSNE(n_components=2, perplexity=30, n_iter=600, init='pca', random_state=random_state).fit_transform(X_orig),
"UMAP-2D": umap.UMAP(n_neighbors=15, min_dist=0.05, n_components=2, random_state=random_state).fit_transform(X_orig)
}
# 将类别转为整数标签计算轮廓系数
label_to_id = {c: i for i, c in enumerate(np.unique(y))}
y_id = np.array([label_to_id[c] for c in y])
for name, emb in algo.items():
res[name] = silhouette_score(emb, y_id, metric="euclidean")
return res
sil = algo_silhouette(X, y, random_state=42)
plt.figure(figsize=(7, 5))
method_names = list(sil.keys())
scores = [sil[m] for m in method_names]
bar_colors = ["#1f77b4", "#ff7f0e", "#2ca02c"]
plt.bar(method_names, scores, color=bar_colors)
plt.ylabel("Silhouette Score(越高越好)")
plt.title("图5:聚类轮廓系数对比(PCA vs t-SNE vs UMAP)", fontsize=14)
for i, v in enumerate(scores):
plt.text(i, v + 0.01, f"{v:.3f}", ha='center', va='bottom')
plt.tight_layout()
plt.show()
PCA三维投影:

这是把50维高维数据压到3维后的一个窗口,颜色代表四类不同形状的来源。你会看到不同颜色的大致簇,但它们之间仍交织,说明线性投影难以完全展开复杂的非线性结构。
UMAP二维嵌入主图:
这是 UMAP 的成品地图。同一来源形状的点大多聚到一起,不同颜色之间的相对位置也更有地理意义。

这张图很适合:
-
初探数据:有几类?是否有子团?
-
发现离群点:落在团之外的奇怪点
-
观察局部结构:同一类内部是否有分层(比如S曲线中的不同段)
参数网格:n_neighbors × min_dist:

行方向(n_neighbors从5到50):越小越强调各自抱团,越大越地理统一(全局结构更连续)。
列方向(min_dist从0.0到0.8):越小团越紧密,越大更松散、透气。
Trustworthiness 对比:

Trustworthiness 衡量低维结果保留了多少高维的近邻关系,取值越高表明局部结构保真度越好。UMAP 通常在一系列 k 上都能保持较高的 trustworthiness,说明它对邻居关系的还原普遍更稳健。
Silhouette 轮廓系数对比:

衡量类内紧凑、类间分离的综合指标。UMAP 的分数通常较高,说明它在2D里把同类拉近、异类拉开做得不错(当然具体结果与数据分布相关)
核心处理逻辑
第0步:数据准备
合成四种非线性形状,升维到50维,添加随机非线性特征与噪声,模拟真实世界高维+非线性+噪声的复杂场景。
标准化(StandardScaler)让不同特征不至于因量纲差异而主导距离。
第1步:先来个PCA-3D探路
低成本看个轮廓,辅助理解数据是否适合更复杂的非线性降维。
第2步:核心 UMAP 降维(2D)
关键参数:n_neighbors=15, min_dist=0.05;平衡局部与全局、适度紧凑。
结果:二维可视化,可肉眼判断簇结构、离群点、子结构。
第3步:参数网格扫描
选几个典型组合,观察形状随参数的演变,确定符合你业务解释的形态(比如需要更紧的团还是更平滑的地貌)。
第4步:定量指标评估
Trustworthiness(局部结构保真),Silhouette(类内紧凑 vs 类间分离)
可以加上KNN在嵌入空间的分类精度、邻域召回等,作为辅助指标(本文示例已足够)。
UMAP vs t-SNE
t-SNE:特别擅长把团块可视化得很清楚,但不同团块之间的远近不要太认真。参数 perplexity 影响很大;大数据上速度一般,需要近似或子采样。
UMAP:把数据当作流形上的点,先构造模糊的邻接图,再在低维用目标函数拟合,既看重局部,也努力给全局一点秩序。近似KNN和负采样优化让 UMAP 更快、更可扩展。
总结
UMAP 的直觉其实很朴素:把谁和谁更像的信息装进一个友谊网,再在平面上尽量画出这个网的结构。理解交朋友 + 拼地图 + 折到纸上这三个关键步骤,你就抓住了 UMAP 的灵魂。
大家在实战中,UMAP 不仅是可视化工具,还是下游机器学习的表征引擎,比如把高维特征压到一小撮维度喂给KNN/聚类/检索。

1501

被折叠的 条评论
为什么被折叠?



