无监督学习
2864 字约 10 分钟
2026-05-20
无监督学习处理的是没有标签的数据。算法需要自己发现数据中隐藏的结构、模式和规律。
在现实世界中,有标签的数据往往很稀缺(需要大量人工标注),无标签的数据则到处都是。无监督学习让我们能充分利用这些未标注数据。
无监督学习的难点在于评估:没有标签,如何判断结果好不好?往往需要结合领域知识和可视化。
1. 聚类(Clustering)
1.1 什么是聚类
将数据分成若干组(簇),使得组内样本相似,组间样本差异。本质是在数据中发现自然的分组结构。
应用场景:
- 用户分群(电商客户分层、用户画像)
- 文档主题发现
- 图像分割
- 异常检测(不属于任何簇的点)
- 基因表达分析
1.2 K-Means 算法
最经典的聚类算法,简单高效。
算法流程:
- 随机初始化 K 个聚类中心(centroids)μ₁, μ₂, ..., μₖ
- 分配步骤:将每个样本分配到最近的聚类中心
c(i)=argkmin∣∣x(i)−μk∣∣2
- 更新步骤:重新计算每个簇的中心(簇内所有点的均值)
μk=∣Ck∣1i∈Ck∑x(i)
- 重复步骤 2-3,直到聚类中心不再变化(收敛)
代价函数(畸变函数):
J=m1i=1∑m∣∣x(i)−μc(i)∣∣2
K-Means 实际上是在最小化这个函数,交替优化 c(固定 μ 优化 c)和 μ(固定 c 优化 μ)。
from sklearn.cluster import KMeans
import numpy as np
import matplotlib.pyplot as plt
# 基本使用
kmeans = KMeans(
n_clusters=3,
init='k-means++', # 更好的初始化方法
n_init=10, # 运行10次取最优(避免局部最优)
max_iter=300,
random_state=42
)
kmeans.fit(X)
labels = kmeans.labels_ # 每个样本的簇编号
centers = kmeans.cluster_centers_ # 聚类中心
inertia = kmeans.inertia_ # 代价函数值(簇内距离之和)
# 可视化
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.7)
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='X', s=200)
plt.title('K-Means 聚类结果')
plt.show()K-Means 的问题:
- 需要预先指定 K
- 对初始中心点敏感,可能陷入局部最优 → 用
k-means++初始化缓解 - 假设簇是球形的,对非球形簇效果差
- 对异常值敏感
如何选择 K?肘部法则(Elbow Method):
inertias = []
K_range = range(1, 11)
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(X)
inertias.append(km.inertia_)
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('K')
plt.ylabel('Inertia(簇内误差)')
plt.title('肘部法则选择最优 K')
# 找"肘部"——误差下降速度明显变缓的位置1.3 DBSCAN(基于密度的聚类)
不需要预先指定 K,能发现任意形状的簇,能识别噪声点。
核心概念:
- 核心点(Core Point):邻域(半径 ε)内至少有
min_samples个点 - 边界点(Border Point):在核心点邻域内,但自身不是核心点
- 噪声点(Noise Point):既不是核心点也不是边界点,被标记为 -1
算法流程:
- 找出所有核心点
- 将相互距离 ≤ ε 的核心点放入同一簇
- 将边界点加入相邻核心点的簇
- 剩余的是噪声点
from sklearn.cluster import DBSCAN
dbscan = DBSCAN(
eps=0.5, # 邻域半径(关键参数)
min_samples=5, # 核心点的最少邻居数
metric='euclidean'
)
labels = dbscan.fit_predict(X)
# 噪声点的标签为 -1
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = list(labels).count(-1)
print(f'簇数量: {n_clusters}, 噪声点: {n_noise}')如何选择 ε? 对每个点找最近邻距离,画排序后的距离曲线,找曲线的"肘部"。
DBSCAN vs K-Means:
| K-Means | DBSCAN | |
|---|---|---|
| 簇的形状 | 只适合球形 | 任意形状 |
| 需要指定 K | 是 | 否 |
| 对噪声 | 敏感 | 能识别噪声 |
| 计算复杂度 | O(nKt) | O(n log n)(用 kd-tree) |
| 密度不均匀的簇 | 差 | 也较差(HDBSCAN 更好) |
1.4 层次聚类(Hierarchical Clustering)
自底向上(凝聚式):
- 每个点作为一个独立的簇
- 反复合并最相似的两个簇
- 直到所有点合并为一个大簇
结果是一棵树状图(Dendrogram),可以在任意层次切割,得到不同数量的簇。
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
# 画树状图
linkage_matrix = linkage(X, method='ward')
dendrogram(linkage_matrix)
plt.title('层次聚类树状图')
# 选定 K 进行切割
hc = AgglomerativeClustering(n_clusters=3, linkage='ward')
labels = hc.fit_predict(X)2. 降维(Dimensionality Reduction)
2.1 为什么需要降维
- 维度诅咒:高维空间中,数据变得极其稀疏,距离计算失去意义
- 可视化:高维数据无法直接可视化,降到2/3维才能观察
- 去除冗余:多个高度相关的特征可以压缩成少数几个
- 加速训练:减少特征数量,降低模型复杂度
2.2 PCA(主成分分析)
核心思想:找到数据方差最大的方向(主成分),将数据投影到这些方向构成的低维空间。
直觉理解:把一个三维物体的影子投影到二维平面,选择最能反映原始形状信息的投影角度。
PCA 步骤:
- 对数据中心化(减去均值)
- 计算协方差矩阵 Σ = (1/m) XᵀX
- 对协方差矩阵做特征值分解(或 SVD)
- 取前 k 个最大特征值对应的特征向量作为主成分
- 将原始数据投影到主成分上
z=UreduceTx
from sklearn.decomposition import PCA
import numpy as np
# 降到2维
pca = PCA(n_components=2)
X_reduced = pca.fit_transform(X)
# 查看每个主成分解释的方差比例
print(pca.explained_variance_ratio_)
# 累计解释方差
print(np.cumsum(pca.explained_variance_ratio_))
# 选择保留95%方差的维度数
pca_auto = PCA(n_components=0.95)
X_95 = pca_auto.fit_transform(X)
print(f"保留95%方差需要 {pca_auto.n_components_} 维")
# 数据重建(解压)
X_reconstructed = pca.inverse_transform(X_reduced)
reconstruction_error = np.mean((X - X_reconstructed) ** 2)注意:PCA 用于降维时,必须先在训练集上 fit,再用同样的变换应用到测试集。绝对不能在整个数据集上 fit(数据泄露)。
PCA 的局限:
- 只能捕捉线性关系
- 对异常值敏感(因为基于方差)
- 丢失可解释性(主成分是原始特征的线性组合)
2.3 t-SNE(非线性降维可视化)
专门用于可视化高维数据,能保留局部结构(相似的点在低维中仍然相近)。
原理:
- 在高维空间中,用高斯分布建模点对的相似度
- 在低维空间中,用 t 分布建模点对的相似度(t 分布尾部更重,避免"拥挤问题")
- 最小化两个分布之间的 KL 散度
from sklearn.manifold import TSNE
tsne = TSNE(
n_components=2,
perplexity=30, # 关键参数,控制局部/全局结构的权衡(通常5-50)
learning_rate=200,
n_iter=1000,
random_state=42
)
X_tsne = tsne.fit_transform(X)
# 用颜色标注类别
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='tab10', alpha=0.7)
plt.colorbar()
plt.title('t-SNE 可视化')t-SNE 的注意事项:
- 计算量大,不适合大数据集(先用 PCA 降到 50 维再用 t-SNE)
- 结果有随机性(不同 random_state 结果不同)
- 不能用来做特征工程(不可逆,没有 inverse_transform)
- 轴没有意义,只有点的相对位置有意义
2.4 UMAP(均匀流形近似与投影)
t-SNE 的改进版,越来越流行:
| t-SNE | UMAP | |
|---|---|---|
| 速度 | 慢 | 快很多 |
| 全局结构保留 | 较差 | 更好 |
| 参数敏感性 | 较高 | 较低 |
| 可否新数据投影 | 否 | 是 |
import umap
reducer = umap.UMAP(n_components=2, n_neighbors=15, min_dist=0.1, random_state=42)
X_umap = reducer.fit_transform(X)3. 异常检测
3.1 什么是异常检测
发现数据中与绝大多数样本显著不同的点。
应用场景:信用卡欺诈、网络入侵、工厂设备故障预警、医疗异常指标。
注意:和分类不同,异常检测通常是单类学习——只有正常样本的训练数据,没有(或极少)异常样本标签。
3.2 基于统计的方法
假设正常数据服从高斯分布:
p(x)=2πσ21exp(−2σ2(x−μ)2)
对每个特征计算均值和方差,如果 p(x) < ε,则判断为异常。
多元高斯分布(考虑特征间相关性):
p(x;μ,Σ)=(2π)n/2∣Σ∣1/21exp(−21(x−μ)TΣ−1(x−μ))
3.3 Isolation Forest(孤立森林)
核心思想:异常点在随机分割树中需要更少的步骤就能被"孤立"。
正常点需要很多次分割才能与其他点区分(因为它们聚集在一起),而异常点由于处于稀疏区域,很快就能被孤立。
from sklearn.ensemble import IsolationForest
iso_forest = IsolationForest(
n_estimators=100,
contamination=0.1, # 预期的异常点比例
random_state=42
)
predictions = iso_forest.fit_predict(X)
# 1 = 正常, -1 = 异常
anomaly_scores = iso_forest.score_samples(X) # 分数越低越异常3.4 Autoencoder 异常检测
用神经网络重建正常数据,重建误差大的样本视为异常:
import torch
import torch.nn as nn
class Autoencoder(nn.Module):
def __init__(self, input_dim, latent_dim):
super().__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Linear(64, latent_dim)
)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 64),
nn.ReLU(),
nn.Linear(64, input_dim)
)
def forward(self, x):
latent = self.encoder(x)
reconstructed = self.decoder(latent)
return reconstructed
# 重建误差作为异常分数
def anomaly_score(model, x):
with torch.no_grad():
reconstructed = model(x)
return torch.mean((x - reconstructed) ** 2, dim=1)4. 关联规则学习
4.1 概念
发现数据中项目之间的有趣关联:
支持度(Support):项集 {A, B} 在数据集中出现的比例
Support(A→B)=∣D∣∣{A∪B}∣
置信度(Confidence):A 出现时 B 也出现的概率
Confidence(A→B)=Support(A)Support(A∪B)
提升度(Lift):A 对 B 出现概率的提升倍数
Lift(A→B)=Support(B)Confidence(A→B)
- Lift > 1:A 和 B 正相关
- Lift = 1:A 和 B 独立
- Lift < 1:A 和 B 负相关
4.2 Apriori 算法
关键原理:频繁项集的任何子集也是频繁的(向下封闭性)。据此可以剪枝,避免遍历所有可能项集。
from mlxtend.frequent_patterns import apriori, association_rules
import pandas as pd
# 购物篮数据(one-hot 编码)
basket = pd.DataFrame({
'牛奶': [1, 0, 1, 1, 0],
'面包': [1, 1, 1, 0, 1],
'黄油': [0, 1, 1, 0, 1],
'啤酒': [0, 0, 0, 1, 0],
'尿布': [0, 0, 0, 1, 1],
}, dtype=bool)
# 挖掘频繁项集
frequent_items = apriori(basket, min_support=0.4, use_colnames=True)
# 生成关联规则
rules = association_rules(frequent_items, metric='lift', min_threshold=1.0)
rules.sort_values('lift', ascending=False).head(10)5. 高斯混合模型(GMM)
GMM 是比 K-Means 更灵活的聚类方法——K-Means 假设簇是球形的,GMM 假设数据由多个高斯分布混合而成,每个高斯分布对应一个簇。
p(x)=k=1∑KπkN(x∣μk,Σk)
- 软分配:每个点属于每个簇的概率(K-Means 是硬分配)
- 可以处理椭圆形簇
- 用 EM 算法训练:E步(计算属于各簇的概率)→ M步(更新参数)
from sklearn.mixture import GaussianMixture
gmm = GaussianMixture(n_components=3, covariance_type='full', random_state=42)
gmm.fit(X)
labels = gmm.predict(X) # 硬分配
probs = gmm.predict_proba(X) # 软分配(每个点属于各簇的概率)6. 无监督学习的评估
没有标签,如何评估聚类效果?
内部评估指标(不需要真实标签):
| 指标 | 含义 | 越好 |
|---|---|---|
| 轮廓系数(Silhouette Score) | 簇内紧密度 vs 簇间距离 | 越接近1越好 |
| Calinski-Harabasz 指数 | 簇间离散度 / 簇内离散度 | 越大越好 |
| Davies-Bouldin 指数 | 每对簇的平均相似度 | 越小越好 |
from sklearn.metrics import silhouette_score, calinski_harabasz_score
sil = silhouette_score(X, labels)
ch = calinski_harabasz_score(X, labels)
print(f"轮廓系数: {sil:.3f}")
print(f"Calinski-Harabasz: {ch:.1f}")外部评估指标(需要真实标签,用于验证聚类是否对应真实分组):
- 调整兰德指数(Adjusted Rand Index, ARI)
- 调整互信息(Adjusted Mutual Information, AMI)
- 同质性(Homogeneity)、完整性(Completeness)