跳至内容
无监督学习:聚类与降维

无监督学习:聚类与降维

没有标签怎么学?无监督学习的回答是:从数据本身的结构中学习。聚类找出自然分组,降维压缩信息保留精华,异常检测揪出偏离正常的样本。这三类技术在用户分群、特征压缩、欺诈检测中无处不在。

聚类:发现数据的自然分组

K-Means:最经典的聚类算法

算法流程

随机初始化 K 个聚类中心 把每个样本分配给**距离最近**的聚类中心 重新计算每个簇的**质心**(所有样本的均值) 重复步骤 2–3,直到质心不再移动
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler

# 生成 3 个簇的示例数据
X, y_true = make_blobs(n_samples=400, centers=3, cluster_std=0.8, random_state=42)
X = StandardScaler().fit_transform(X)  # K-Means 依赖距离,必须标准化

kmeans = KMeans(n_clusters=3, n_init=10, random_state=42)
labels = kmeans.fit_predict(X)

# 可视化
plt.scatter(X[:,0], X[:,1], c=labels, cmap="viridis", alpha=0.6)
plt.scatter(kmeans.cluster_centers_[:,0], kmeans.cluster_centers_[:,1],
            marker="X", s=200, c="red", label="质心")
plt.legend()
plt.title("K-Means 聚类结果")
plt.savefig("kmeans.png", dpi=120)

如何选 K:肘部法则

K-Means 需要预先指定 K,实践中用肘部法则找合适的 K:

inertias = []  # 簇内平方和(SSE),越小越好,但 K 越大 SSE 越小
for k in range(1, 11):
    km = KMeans(n_clusters=k, n_init=10, random_state=42)
    km.fit(X)
    inertias.append(km.inertia_)

plt.plot(range(1, 11), inertias, "bo-")
plt.xlabel("K(簇数量)")
plt.ylabel("SSE(簇内平方和)")
plt.title("肘部法则:曲线弯折处是最优 K")
plt.savefig("elbow.png", dpi=120)

曲线弯折最明显的地方(像手肘的弯折)就是推荐的 K 值。

DBSCAN:无需指定 K,能发现任意形状

K-Means 只能发现凸形状的簇,遇到环形、月牙形数据就失效。DBSCAN(基于密度的聚类)无需预先指定簇数量,靠密度定义簇:

from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons

X_moon, _ = make_moons(n_samples=300, noise=0.05, random_state=42)

# eps:邻域半径;min_samples:核心点至少有多少邻居
dbscan = DBSCAN(eps=0.2, min_samples=5)
labels = dbscan.fit_predict(X_moon)

# label=-1 表示噪声点
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise    = (labels == -1).sum()
print(f"发现 {n_clusters} 个簇,{n_noise} 个噪声点")
DBSCAN 参数怎么选eps 可以用 k-近邻距离图确定(画出每个点到第 k 个邻居的距离,排序后找弯折点);min_samples 通常设为特征维数的 2 倍或 5。

聚类质量评估

没有标签时,用内部评估指标衡量聚类质量:

from sklearn.metrics import silhouette_score, davies_bouldin_score

# 轮廓系数:-1~1,越接近 1 表示簇内紧、簇间散
sil = silhouette_score(X, labels)
print(f"轮廓系数: {sil:.3f}")

# Davies-Bouldin 指数:越小越好
db = davies_bouldin_score(X, labels)
print(f"Davies-Bouldin: {db:.3f}")
方法需预设 K簇形状对噪声适用场景
K-Means球形/凸形敏感大规模数据分群
DBSCAN任意形状鲁棒不规则形状、异常检测
层次聚类否(可调)灵活较敏感小数据集、需树状结构

降维:压缩高维数据

高维数据(几百个特征)面临「维度诅咒」:数据稀疏、计算慢、可视化困难。降维把数据压缩到低维,保留最核心的信息。

PCA:主成分分析

PCA 找出数据中方差最大的方向(主成分),在这些方向上投影,实现降维。

from sklearn.decomposition import PCA
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt

X, y = load_digits(return_X_y=True)  # 64 维手写数字图像
print(f"原始维度: {X.shape}")  # (1797, 64)

pca = PCA(n_components=2)
X_2d = pca.fit_transform(X)
print(f"降维后:   {X_2d.shape}")  # (1797, 2)

# 可视化(二维散点图)
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_2d[:,0], X_2d[:,1], c=y, cmap="tab10", alpha=0.6, s=15)
plt.colorbar(scatter)
plt.title("PCA 将 64 维降至 2 维")
plt.savefig("pca_digits.png", dpi=120)

选多少个主成分? 看累计解释方差比:

pca_full = PCA().fit(X)
cumvar = np.cumsum(pca_full.explained_variance_ratio_)

plt.plot(cumvar)
plt.axhline(0.95, color="r", linestyle="--", label="95% 方差")
plt.xlabel("主成分数量")
plt.ylabel("累计解释方差比")
plt.legend()

# 找到解释 95% 方差所需的最少主成分数
n_components_95 = np.argmax(cumvar >= 0.95) + 1
print(f"解释 95% 方差需要 {n_components_95} 个主成分")

t-SNE:用于可视化

PCA 是线性降维,对非线性结构无能为力。t-SNE 能保留局部结构,专门用于把高维数据可视化到 2D/3D,但不适合用于降维后再训练模型(结果随机性大,不稳定):

from sklearn.manifold import TSNE

# 注意:t-SNE 很慢,大数据集先用 PCA 降到 50 维再跑 t-SNE
X_50d = PCA(n_components=50).fit_transform(X)

tsne = TSNE(n_components=2, perplexity=30, random_state=42, n_iter=1000)
X_tsne = tsne.fit_transform(X_50d)

plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_tsne[:,0], X_tsne[:,1], c=y, cmap="tab10", s=15, alpha=0.7)
plt.colorbar(scatter)
plt.title("t-SNE 可视化手写数字(每个颜色是一个数字 0-9)")
plt.savefig("tsne_digits.png", dpi=120)

UMAP:速度更快的现代降维

UMAP(2018 年提出)在保留全局结构和速度上都优于 t-SNE,已逐渐成为可视化的首选:

import umap

reducer = umap.UMAP(n_components=2, n_neighbors=15, min_dist=0.1, random_state=42)
X_umap = reducer.fit_transform(X)

异常检测:找出离群点

很多场景需要找「不正常」的样本:欺诈交易、设备故障、网络入侵。无标签时,异常检测用无监督方法完成。

Isolation Forest

核心思路:异常点更容易被「隔离」——随机切分特征空间,正常点需要更多次切分才能被单独隔离,异常点很快就被隔离。

from sklearn.ensemble import IsolationForest
import numpy as np

# 生成含异常点的数据
np.random.seed(42)
X_normal  = np.random.randn(300, 2)
X_outlier = np.random.uniform(-4, 4, (20, 2))  # 20 个随机异常点
X_all = np.vstack([X_normal, X_outlier])

iso = IsolationForest(contamination=0.05, random_state=42)  # contamination=异常比例
labels = iso.fit_predict(X_all)  # -1=异常,1=正常

print(f"检测到 {(labels == -1).sum()} 个异常点")

# 查看异常分数(越负越异常)
scores = iso.score_samples(X_all)

Local Outlier Factor(LOF)

基于局部密度:如果一个点比它的邻居密度低得多,它就是异常点。

from sklearn.neighbors import LocalOutlierFactor

lof = LocalOutlierFactor(n_neighbors=20, contamination=0.05)
labels = lof.fit_predict(X_all)  # -1=异常,1=正常
print(f"LOF 检测到 {(labels == -1).sum()} 个异常点")
方法适用场景特点
Isolation Forest高维数据,大数据集速度快,工程首选
LOF密度不均匀的场景对局部密度变化敏感
One-Class SVM小数据集,高维精度高但慢
Autoencoder图像/序列异常深度学习方法

一句话小结

无监督学习:K-Means / DBSCAN 做分群,PCA / UMAP 做降维可视化,Isolation Forest 做异常检测。三者都不需要标签,从数据结构本身提取信息,是探索性分析和数据理解的利器。

最后更新于