# 04 · 无监督学习:聚类与降维 > AI生成|2026年10月26日|codebuddy **本篇目标**:没有标签怎么建模?学会用**聚类**给数据分组、用**降维**给数据压缩,并明白这两个动作在监督学习 pipeline 里同样重要(特征压缩、去噪、可视化、异常检测)。 **导航**:[栏目首页](./README.md) | 上一篇:[03 · SVM、KNN 与朴素贝叶斯](./03-SVM-KNN-朴素贝叶斯.md) | 下一篇:[05 · 神经网络与深度学习入门](./05-神经网络与深度学习入门.md) --- ## 一、核心概念 监督学习是「照着答案学」,无监督学习是「自己找结构」。常见任务有四类: | 任务 | 干什么 | 代表算法 | 典型用途 | | --- | --- | --- | --- | | **聚类** | 把相似的样本分组 | K-Means、层次聚类、DBSCAN | 用户分群、异常群组发现 | | **降维** | 用更少的特征表示同样的信息 | PCA、UMAP、t-SNE | 可视化、去噪、特征压缩、加速训练 | | **密度估计** | 刻画数据的分布形状 | 高斯混合 GMM、核密度 | 异常检测、生成模型基础 | | **关联分析** | 找「买了 A 就会买 B」的规则 | Apriori、FP-Growth | 购物篮分析、推荐 | 本篇聚焦前两类。 --- ## 二、模型原理 ### 2.1 K-Means:最经典的聚类 **目标**:把 n 个样本分成 K 簇,最小化「簇内平方和」(惯性 inertia): ```text J = Σ_{k=1..K} Σ_{x∈C_k} ‖x − μ_k‖² (μ_k 是第 k 簇的质心) ``` **Lloyd 算法**(两步交替,直到不再变化): ```text 重复: 1) 分配:每个样本归入距离最近的质心 → c(i) = argmin_k ‖xᵢ − μ_k‖² 2) 更新:质心 = 该簇所有样本的均值 → μ_k = mean(xᵢ : c(i)=k) ``` 每步都让 J 单调不增,所以**必然收敛,但只保证收敛到局部最优**——初始质心的位置很关键。scikit-learn 的默认值 `init="k-means++"`(让初始点尽量远离)+`n_init=10`(多次随机重启取最好)就是为此。 **怎么选 K**: | 方法 | 做法 | 特点 | | --- | --- | --- | | 肘部法(Elbow) | 画 inertia-K 曲线,找拐点 | 直观但常常没有明显拐点 | | 轮廓系数 | `silhouette_score` 取值越高越好 | 最常用,兼顾簇内紧密度与簇间分离度 | | Calinski-Harabasz / Davies-Bouldin | sklearn 内置 | 无需反复肉眼判断 | **轮廓系数**对单个样本的定义(`a`=簇内平均距离,`b`=到最近其他簇的平均距离): ```text s = (b − a) / max(a, b) ∈ [−1, 1],越接近 1 说明分得越好 ``` **三条硬约束**:簇是**凸的、球状、大小相近**;需要**预先给定 K**;**异常值会把质心拽偏**(想稳一点可用 `KMedoids`)。 ### 2.2 层次聚类:不需要指定 K **凝聚式(自底向上)**:一开始每个点是一个簇,每次合并距离最近的两个簇,直到只剩一个。 簇间距离怎么算?这就是 `linkage`: | linkage | 定义 | 特点 | | --- | --- | --- | | `ward` | 合并后簇内方差增量最小 | **首选**,倾向产生大小相近的球状簇 | | `average` | 两簇所有点对的平均距离 | 折中稳健 | | `complete` | 最远点对距离(最大距离) | 产生紧致簇,对噪声敏感 | | `single` | 最近点对距离 | 容易形成"链状"长条簇 | 产出是一棵**树状图(dendrogram)**,在任意高度横切就能得到任意数量的簇。代价是复杂度 O(n²)~O(n³),样本上千就吃力。 ### 2.3 DBSCAN:按密度找任意形状的簇 对三个点的定义和一个参数对: - **核心点**:半径 eps 内至少有 min_samples 个点。 - **边界点**:在某个核心点的 eps 邻域内,但自己不是核心点。 - **噪声点**:都不是(标记为 -1,DBSCAN 唯一能主动说"这个是异常"的算法)。 具体做法:从一个核心点出发,把密度可达的点都吸收进来,长成一棵簇。 ```text 优点:不需要指定 K、能发现任意形状的簇、自带异常点识别 缺点:对 eps / min_samples 敏感、密度差异大的数据难调、高维下「距离」本身失效 (升级選擇:HDBSCAN,能处理变密度簇) ``` 选 eps 的经验做法:画每个点到第 k 近邻的距离(k = min_samples)排序曲线,取"膝盖"处。 ### 2.4 PCA:最经典的降维 **目标**:找一组**正交的新坐标轴(主成分)**,使得投影后数据的方差最大——方差大 = 保留的信息多。 ```text 算法(通过 SVD 实现): X_centered = X − mean(X) (必须中心化) X_centered = U · Σ · Vᵀ (奇异值分解) 主成分方向 = V 的前 k 列 降维后的坐标 Z = X_centered · V_k ``` 几个要看的属性: | 属性 | 含义 | | --- | --- | | `components_` | 形状 (k, n_features),每个主成分在各原始特征上的权重 | | `explained_variance_ratio_` | 每个主成分解释的方差占比 | | `explained_variance_ratio_.cumsum()` | 挑 K 的依据(常取累计 ≥ 0.95) | | `inverse_transform` | 从低维重建回原空间(可用于压缩、去噪) | **三种典型用法**: 1. **可视化**:降到 2~3 维画图。 2. **去噪 / 压缩**:保留前 k 个主成分再反变换,等于把噪声方向丢掉(图像压缩的经典做法)。 3. **加速 + 去相关**:`Pipeline([("pca", PCA(0.95)), ("clf", SVC())])`,先把维度砍掉只留 95% 的方差,下游模型训练更快也更稳。 **PCA 是线性的**——它只能找到线性子空间。"瑞士卷"这种弯折的流形它压不平,这时该用 `KernelPCA`、`Isomap`、`LLE` 或 UMAP。 ### 2.5 t-SNE 与 UMAP:可视化专用 | | t-SNE | UMAP | | --- | --- | --- | | 保留什么 | **局部邻域结构**(谁是邻居) | 局部 + 部分全局结构 | | 关键参数 | `perplexity`(5~50,可理解为"看多少个邻居") | `n_neighbors`、`min_dist` | | 速度 | 慢(万级样本就吃力,建议先 PCA 降到 50 维) | 快得多 | | 能否用于建模 | ❌ 不适合,只能看 | ⚠️ 可以用于降维特征,但需谨慎 | **读图的三条纪律**:① 簇的**大小与距离没有物理意义**;② 不同 `perplexity` 会给出完全不同的图,别拿一张图下结论;③ 多跑几个随机种子,只有稳定的结构才可信。 ### 2.6 聚类怎么打分 | 类型 | 指标 | 说明 | | --- | --- | --- | | 有真实标签(外部) | ARI、NMI、V-measure、homogeneity/completeness | 评估算法好坏用,业务上拿不到 | | 无标签(内部) | silhouette、Calinski-Harabasz、Davies-Bouldin | 用于选 K、选算法 | --- ## 三、实例 ### 3.1 三种聚类正面 PK:月牙形与非球形数据 ```python import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_moons, make_blobs from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN from sklearn.metrics import silhouette_score, adjusted_rand_score def make_data(kind, n=600, random_state=0): if kind == "moons": X, y = make_moons(n_samples=n, noise=0.06, random_state=random_state) else: # 各向异性的三团 X, y = make_blobs(n_samples=n, centers=3, random_state=random_state) X = X @ np.array([[2.5, 0.6], [0.6, 0.4]]) # 拉伸+旋转,制造非球形 return StandardScaler().fit_transform(X), y def run(X, y, name, labels): mask = labels != -1 # DBSCAN 的噪声点不参与内部指标 ari = adjusted_rand_score(y, labels) sil = silhouette_score(X[mask], labels[mask]) if len(set(labels[mask])) > 1 else float("nan") print("{:22s} 簇数={:2d} ARI={:6.3f} silhouette={:6.3f}".format( name, len(set(labels)) - (1 if (labels == -1).any() else 0), ari, sil)) for kind in ["moons", "blobs"]: X, y = make_data(kind) print("\n===== {} =====".format(kind)) run(X, y, "KMeans(k=2/3)", KMeans(n_clusters=(2 if kind == "moons" else 3), n_init=10, random_state=42).fit_predict(X)) run(X, y, "Ward(k=2/3)", AgglomerativeClustering( n_clusters=(2 if kind == "moons" else 3), linkage="ward").fit_predict(X)) run(X, y, "DBSCAN(eps=.3)", DBSCAN(eps=0.3, min_samples=6).fit_predict(X)) # 用轮廓系数给 KMeans 选 K(这一步在真实业务里要反复试) ks, sils = list(range(2, 9)), [] for k in ks: lab = KMeans(n_clusters=k, n_init=10, random_state=42).fit_predict(X) sils.append(silhouette_score(X, lab)) best_k = ks[int(np.argmax(sils))] print("轮廓系数选出的最优 K = {},此时 score = {:.3f}".format(best_k, max(sils))) fig, ax = plt.subplots(1, 3, figsize=(15, 4)) for i, (nm, lab) in enumerate([ ("KMeans", KMeans(n_clusters=best_k, n_init=10, random_state=42).fit_predict(X)), ("Ward", AgglomerativeClustering(n_clusters=best_k).fit_predict(X)), ("DBSCAN", DBSCAN(eps=0.3, min_samples=6).fit_predict(X))]): ax[i].scatter(X[:, 0], X[:, 1], c=lab, s=15, cmap="tab10") ax[i].set_title(nm) plt.suptitle(kind); plt.tight_layout(); plt.show() ``` **怎么读输出**: - **月牙数据**上 KMeans 一定失败(它只能用椭圆去切),而 DBSCAN 轻松搞定——这就是「簇形状假设」的直接后果。 - **拉伸后的 blobs** 上三种方法都能分,但 ARI 会告诉你 Ward 通常比 KMeans 更贴近真实分组。 - 轮廓系数选出的 K 未必等于真实簇数:**内部指标只衡量"分得紧不紧",不等于"分得对不对"**。 ### 3.2 PCA:手写数字的压缩、可视化与加速 ```python import time import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_digits from sklearn.decomposition import PCA from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.model_selection import train_test_split digits = load_digits() X, y = digits.data, digits.target # 64 维(8x8 灰度图) # 1) 需要多少维能保住 95% 的方差? pca_full = PCA().fit(X) cumsum = np.cumsum(pca_full.explained_variance_ratio_) k95 = int(np.argmax(cumsum >= 0.95)) + 1 print("保留 95% 方差需要 {} / {} 维".format(k95, X.shape[1])) plt.figure(figsize=(6, 4)) plt.plot(cumsum, marker="o") plt.axhline(0.95, ls="--", c="r"); plt.axvline(k95, ls="--", c="r") plt.xlabel("n_components"); plt.ylabel("cumulative explained variance") plt.tight_layout(); plt.show() # 2) 降到 2 维看结构:不同数字是否天然分离 emb = PCA(n_components=2).fit_transform(X) plt.scatter(emb[:, 0], emb[:, 1], c=y, cmap="tab10", s=12) plt.colorbar(); plt.title("digits in PCA-2D"); plt.show() # 3) 压缩后再重建:看丢掉了多少视觉信息 pca = PCA(n_components=0.90).fit(X) # 直接按方差比例给维度 Z = pca.transform(X) X_rec = pca.inverse_transform(Z) fig, axes = plt.subplots(2, 8, figsize=(12, 4)) for i in range(8): axes[0, i].imshow(X[i].reshape(8, 8), cmap="gray") axes[1, i].imshow(X_rec[i].reshape(8, 8), cmap="gray") axes[0, i].axis("off"); axes[1, i].axis("off") axes[0, 0].set_title("original"); axes[1, 0].set_title("reconstructed") plt.tight_layout(); plt.show() # 4) PCA 作为 pipeline 的一环:提速且几乎不掉点 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) for tag, pipe in [("raw SVC", Pipeline([("s", StandardScaler()), ("c", SVC())])), ("PCA+SVC", Pipeline([("s", StandardScaler()), ("p", PCA(n_components=0.95)), ("c", SVC())]))]: t0 = time.perf_counter() pipe.fit(X_train, y_train) print("{:8s} train {:.3f}s test acc = {:.4f}".format( tag, time.perf_counter() - t0, pipe.score(X_test, y_test))) ``` **怎么读输出**: - 64 维通常只需 **~30 维**就能保住 95% 方差——说明相邻像素高度相关,冗余很大。 - 二维散点里相同颜色的点大致聚成团,说明 PCA 抓住了主要区分方向;但不同数字的团会重叠,这正是需要非线性方法的地方。 - 重建图像会变模糊(保留了轮廓、丢掉了细节),直观演示了「压缩 = 有损去噪」。 - `PCA+SVC` 在几乎不掉精度的同时显著更快(样本更大时差距会更夸张)。 --- ## 四、常见坑与调参要点 | 坑 | 现象 | 解决 | | --- | --- | --- | | 聚类前不标准化 | 量纲大的特征独占距离 | 一律先 `StandardScaler`(sklearn 的 pipeline 照旧适用) | | KMeans 用在非球状簇 | 月牙数据切成两半 | 换 DBSCAN / 谱聚类 / GMM;或先做特征变换 | | 高维直接聚类 | 距离失效、簇无意义 | 先 PCA 降到 10~50 维再聚类(常见最佳实践) | | 拿 t-SNE 图下结论 | 簇大小、远近都是假的 | 只用它看"有没有结构",并用 `perplexity` 做稳健性检查 | | 把 t-SNE 结果喂给下游模型 | 效果不稳定、不可复现 | 要降维用 PCA / UMAP,且放进 pipeline | | DBSCAN 调不好 eps | 全是一类或全是噪声 | 画 k 距离曲线找拐点;或直接用 `HDBSCAN` | | PCA 在 full data 上拟合 | 数据泄漏,评估虚高 | 放进 `Pipeline`,让它只在训练折里 fit | | 类别不平衡的聚类 | 指标失真 | 内部指标更不可信;改用 ARI / NMI(有标签时)或业务指标 | **速查:这一步该用什么?** ```text 要给用户分群、簇近似球状、数据量大 → KMeans(先用 PCA 降到 10~50 维) 样本小(<几千)、想要层次结构、不想指定 K → AgglomerativeClustering(linkage="ward") 簇形状不规则、需要识别异常点 → DBSCAN / HDBSCAN 想要软分配(每个点属于各簇的概率) → GMM(GaussianMixture) 要压缩/去噪/去相关、为下游模型提速 → PCA 只想画张图看看有什么结构 → UMAP / t-SNE ``` --- ## 五、小结 - **K-Means** 快但假设簇是球状的,**层次聚类**给树状结构但贵,**DBSCAN** 能找任意形状还能抓噪声。选算法本质是选「你对簇形状的先验假设」。 - **PCA** 用正交变换找方差最大的方向,是去噪、压缩、加速的默认工具;cumulative explained variance 是选维数的唯一依据。 - **t-SNE/UMAP 只负责让人看得见**,不要拿它们的距离关系去做定量结论。 - 最大的两个工程纪律:**聚类降维前必须缩放**,以及**任何预处理都要放进 Pipeline 防泄漏**。 下一篇进入深度学习:[05 · 神经网络与深度学习入门](./05-神经网络与深度学习入门.md)。