无监督学习:聚类与降维
没有标签怎么学?无监督学习的回答是:从数据本身的结构中学习。聚类找出自然分组,降维压缩信息保留精华,异常检测揪出偏离正常的样本。这三类技术在用户分群、特征压缩、欺诈检测中无处不在。
聚类:发现数据的自然分组
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 做异常检测。三者都不需要标签,从数据结构本身提取信息,是探索性分析和数据理解的利器。
最后更新于