AI: 无监督学习
最后更新:2026-08-26
数据没有标签怎么办?当你面对一堆用户行为记录、基因序列或传感器数据,却没有任何"正确答案"时,无监督学习就是你发现隐藏结构的利器。本章从聚类和降维两大方向出发,带你理解 K-Means 和 PCA 的原理与实战。
1. 你将学到
- 无监督学习 vs 监督学习的本质区别
- K-Means 聚类算法的原理与实现
- 肘部法则(Elbow Method)选择最优 k 值
- PCA 降维的直觉与数学直觉
- 聚类与降维在实际业务中的应用
2. 故事:100K Users,谁来帮他们分组?
(1) 痛点:海量用户无从下手
Charlie 的营销团队有 100K user data——消费金额、购买频率、注册时长、最近活跃时间……但没人知道用户能分成几类。市场部凭着经验把用户分为"新用户"和"老用户"两组推送优惠券,结果转化率只有 2.3%。CEO 要求提升到 5%,团队一筹莫展。
(2) AI 的解法:用聚类自动发现用户群
Alice 接手后,用 K-Means 把用户聚成了 4 群:
| 群编号 | 特征 | 占比 | 命名 |
|---|---|---|---|
| 群 0 | 高消费低频 | 15% | 大额买家 |
| 群 1 | 低消费高频 | 35% | 活跃羊毛党 |
| 群 2 | 高消费高频 | 10% | 核心VIP |
| 群 3 | 低消费低频 | 40% | 沉默用户 |
每个群推送不同的优惠券:给大额买家推荐新品、给活跃羊毛党发满减券、给核心 VIP 发专属权益、给沉默用户发唤醒红包。转化率直接从 2.3% 提升到 3.1%,提升了 35%。
(3) 收益:从"拍脑袋"到"数据驱动"
Charlie 发现:无监督学习的威力不在于预测,而在于发现你不知道的存在。没有标签、没有先验知识,算法依然能从数据中挖掘出有意义的分组——这就是无监督学习的价值。
3. 无监督学习 vs 监督学习
(1) 核心区别
| 维度 | 监督学习 | 无监督学习 |
|---|---|---|
| 训练数据 | 有标签(X → y) | 无标签(只有 X) |
| 目标 | 预测已知类别/数值 | 发现数据内在结构 |
| 典型任务 | 分类、回归 | 聚类、降维、关联规则 |
| 评估方式 | 准确率、F1、RMSE 等 | 轮廓系数、惯性、可视化 |
| 类比 | 老师教学生认动物 | 孩子自己把玩具按颜色分类 |
| 代表算法 | 决策树、SVM、线性回归 | K-Means、PCA、DBSCAN |
(2) 为什么需要无监督学习
- 标签获取成本高:医学影像标注需要专家,每张 $50-$200
- 标签不存在:用户分群、异常检测——根本没有"标准答案"
- 探索性分析:先聚类看数据长什么样,再决定用监督学习做什么
4. K-Means 聚类
(1) 什么是聚类
聚类(Clustering)是把相似的数据点分到同一组、不相似的分到不同组的过程。关键问题:什么叫"相似"?——通常用距离度量,最常见的是欧氏距离。
(2) K-Means 算法流程
K-Means 是最经典、最实用的聚类算法。核心思想:把数据分成 k 个组,让每个点到自己组中心的距离之和最小。
flowchart TD
A[Initialize k centroids randomly] --> B[Assign each point to nearest centroid]
B --> C[Recalculate centroids as mean of assigned points]
C --> D{Centroids changed?}
D -- Yes --> B
D -- No --> E[Converged! Output clusters]
style A fill:#e1f5fe
style E fill:#e8f5e9
style D fill:#fff3e0
算法步骤:
- 初始化:随机选择 k 个点作为初始质心(centroid)
- 分配:将每个数据点分配到最近的质心所在的簇
- 更新:重新计算每个簇的质心(取簇内所有点的均值)
- 迭代:重复步骤 2-3,直到质心不再变化或达到最大迭代次数
▶ 示例:K-Means 聚类客户数据(难度⭐)
from sklearn.cluster import KMeans
import numpy as np
np.random.seed(42)
group_a = np.random.normal(loc=[20, 80], scale=5, size=(30, 2))
group_b = np.random.normal(loc=[80, 20], scale=5, size=(30, 2))
group_c = np.random.normal(loc=[50, 50], scale=5, size=(30, 2))
X = np.vstack([group_a, group_b, group_c])
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X)
print(f"Cluster centers:\n{kmeans.cluster_centers_}")
print(f"Inertia (sum of squared distances): {kmeans.inertia_:.2f}")
print(f"First 10 labels: {kmeans.labels_[:10]}")
Cluster centers:
[[50.14 49.60]
[20.38 79.63]
[80.27 19.44]]
Inertia (sum of squared distances): 1310.56
First 10 labels: [1 1 1 1 1 1 1 1 1 1]
(3) 三种聚类算法对比
| 维度 | K-Means | 层次聚类 | DBSCAN |
|---|---|---|---|
| 需要指定 k | 是 | 否 | 否(需指定 eps/min_samples) |
| 簇形状 | 球形 | 任意 | 任意 |
| 处理噪声 | 差 | 一般 | 好(标记为噪声点) |
| 时间复杂度 | O(nkt) | O(n²) ~ O(n³) | O(n log n) |
| 可扩展性 | 大数据集友好 | 大数据集慢 | 中等 |
| 适用场景 | 客户分群、图像压缩 | 小数据集、树状结构 | 异常检测、空间数据 |
5. 如何选择 k 值
K-Means 最大的问题:k 需要你提前指定。选错了 k,聚类结果就没意义。两种常用方法帮你找到最优 k。
(1) 肘部法则(Elbow Method)
绘制不同 k 值对应的惯性(inertia,即簇内平方距离和)曲线,选择"拐弯处"的 k——就像手臂的肘部,之后增加 k 收益递减。
▶ 示例:肘部法则画图选 k(难度⭐)
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
import numpy as np
np.random.seed(42)
X = np.vstack([
np.random.normal(loc=[20, 80], scale=5, size=(50, 2)),
np.random.normal(loc=[80, 20], scale=5, size=(50, 2)),
np.random.normal(loc=[50, 50], scale=5, size=(50, 2)),
np.random.normal(loc=[80, 80], scale=5, size=(50, 2)),
])
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.figure(figsize=(8, 5))
plt.plot(K_range, inertias, 'bo-', linewidth=2, markersize=8)
plt.xlabel('Number of clusters (k)')
plt.ylabel('Inertia')
plt.title('Elbow Method for Optimal k')
plt.axvline(x=4, color='red', linestyle='--', label='Elbow at k=4')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('elbow_method.png', dpi=150)
plt.show()
(2) Silhouette 评分
Silhouette(轮廓系数)衡量每个点与自己簇的相似度 vs 与最近其他簇的相似度,取值 [-1, 1],越大越好。
▶ 示例:Silhouette 评分选择 k(难度⭐⭐)
from sklearn.metrics import silhouette_score
from sklearn.cluster import KMeans
import numpy as np
np.random.seed(42)
X = np.vstack([
np.random.normal(loc=[20, 80], scale=5, size=(50, 2)),
np.random.normal(loc=[80, 20], scale=5, size=(50, 2)),
np.random.normal(loc=[50, 50], scale=5, size=(50, 2)),
np.random.normal(loc=[80, 80], scale=5, size=(50, 2)),
])
print("k | Silhouette Score")
print("--|------------------")
for k in range(2, 8):
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(X)
score = silhouette_score(X, labels)
print(f"{k} | {score:.4f}")
k | Silhouette Score
--|------------------
2 | 0.5812
3 | 0.6234
4 | 0.6891
5 | 0.5543
6 | 0.4672
7 | 0.3891
(3) 聚类评估方法对比
| 方法 | 衡量内容 | 取值范围 | 优点 | 缺点 |
|---|---|---|---|---|
| Inertia | 簇内紧密度 | ≥0,越小越好 | 计算快,K-Means 自带 | 随 k 增大单调下降,不能单独用 |
| Silhouette | 紧密度 + 分离度 | [-1, 1],越大越好 | 可比较不同 k | 计算慢,O(n²) |
| Calinski-Harabasz | 簇间/簇内方差比 | ≥0,越大越好 | 计算快 | 对凸形簇偏好 |
| Davies-Bouldin | 簇内散度/簇间距离比 | ≥0,越小越好 | 直观 | 对凸形簇偏好 |
6. PCA 降维
(1) 为什么需要降维
- 维度灾难:特征太多,距离失去意义,模型性能下降
- 可视化:人类只能看 2D/3D,高维数据必须降维才能画图
- 去噪:主要信息集中在前几个主成分,后面的可能是噪声
- 加速:降维后训练更快、内存更省
(2) PCA 直觉
PCA(主成分分析)的核心思想:找到数据方差最大的方向,投影到这些方向上。方差大 = 信息多,投影后尽量保留原始信息。
想象一堆 3D 数据点呈"扁平"的椭球状——PCA 会找到最长的轴(方差最大),把数据投影到这个轴上,3D 就变成了 1D,同时尽量不丢失信息。
▶ 示例:PCA 降维到 2D 并可视化(难度⭐⭐)
from sklearn.decomposition import PCA
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
import numpy as np
iris = load_iris()
X = iris.data
y = iris.target
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
print(f"Original shape: {X.shape}")
print(f"After PCA: {X_pca.shape}")
print(f"Explained variance ratio: {pca.explained_variance_ratio_}")
print(f"Total variance explained: {pca.explained_variance_ratio_.sum():.4f}")
plt.figure(figsize=(8, 6))
for target, color, label in zip([0, 1, 2], ['red', 'blue', 'green'], iris.target_names):
plt.scatter(X_pca[y == target, 0], X_pca[y == target, 1],
c=color, label=label, alpha=0.7, edgecolors='k', s=60)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%} variance)')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%} variance)')
plt.title('PCA: Iris Dataset Reduced to 2D')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('pca_iris.png', dpi=150)
plt.show()
Original shape: (150, 4)
After PCA: (150, 2)
Explained variance ratio: [0.9246 0.0530]
Total variance explained: 0.9776
(3) 降维方法对比
| 方法 | 类型 | 核心思想 | 保持全局结构 | 保持局部结构 | 可解释性 | 适用场景 |
|---|---|---|---|---|---|---|
| PCA | 线性 | 最大方差方向 | 好 | 差 | 高(特征权重可解释) | 快速降维、去噪 |
| t-SNE | 非线性 | 保持邻域概率 | 差 | 好 | 低 | 可视化(2D/3D) |
| UMAP | 非线性 | 拓扑结构保持 | 较好 | 好 | 低 | 可视化、大规模数据 |
| Autoencoder | 非线性 | 神经网络压缩 | 视结构 | 视结构 | 中 | 图像/文本降维 |
7. t-SNE 可视化简介
PCA 是线性降维,擅长保持全局结构但会"压扁"非线性关系。t-SNE(t-Distributed Stochastic Neighbor Embedding)专门为可视化设计,擅长保持局部邻域结构——相似的点在 2D 中仍然靠近。
▶ 示例:t-SNE 可视化高维数据(难度⭐⭐)
from sklearn.manifold import TSNE
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
import numpy as np
digits = load_digits()
X = digits.data
y = digits.target
print(f"Original shape: {X.shape}") # 64-dimensional handwritten digits
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X)
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='tab10',
alpha=0.7, edgecolors='k', s=30)
plt.colorbar(scatter, label='Digit Class')
plt.title('t-SNE: 64D Handwritten Digits → 2D')
plt.xlabel('t-SNE Dimension 1')
plt.ylabel('t-SNE Dimension 2')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('tsne_digits.png', dpi=150)
plt.show()
8. 聚类与降维的实际应用
(1) 客户分群
电商/金融/游戏行业用聚类将用户分为不同群体,制定差异化策略:
| 行业 | 聚类特征 | 分群结果 | 业务动作 |
|---|---|---|---|
| 电商 | 消费金额、频率、品类偏好 | 高价值/低价值/流失风险 | 差异化优惠券 |
| 金融 | 收入、负债、信用记录 | 低风险/中等/高风险 | 差异化利率 |
| 游戏 | 在线时长、付费、社交活跃 | 鲸鱼/海豚/白嫖 | 差异化运营 |
(2) 图像压缩
K-Means 可用于图像颜色量化——把 millions of colors 聚成 k 种,大幅减小文件体积。
(3) 异常检测
不属于任何簇的点可能是异常值。DBSCAN 标记的噪声点、离质心很远的点都值得关注。
(4) 特征工程
PCA 降维后的主成分可作为新特征输入监督学习模型,去除噪声、减少共线性、加速训练。
9. 聚类结果与原始标签对比
有时你有标签但想看看无监督聚类能否"自己发现"这些类别——这是检验聚类质量的绝佳方式。
▶ 示例:聚类结果与原始标签对比(难度⭐⭐)
from sklearn.cluster import KMeans
from sklearn.datasets import load_iris
from sklearn.metrics import confusion_matrix, accuracy_score
from scipy.stats import mode
import numpy as np
iris = load_iris()
X = iris.data
y_true = iris.target
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
y_pred = kmeans.fit_predict(X)
# Cluster labels are arbitrary, so we align them with true labels
aligned_pred = np.zeros_like(y_pred)
for i in range(3):
mask = y_pred == i
aligned_pred[mask] = mode(y_true[mask], keepdims=True).mode[0]
print("Confusion Matrix (cluster vs true label):")
print(confusion_matrix(y_true, aligned_pred))
print(f"\nAligned accuracy: {accuracy_score(y_true, aligned_pred):.4f}")
Confusion Matrix (cluster vs true label):
[[50 0 0]
[ 0 48 2]
[ 0 14 36]]
Aligned accuracy: 0.8933
10. 无监督学习的局限
| 局限 | 说明 |
|---|---|
| 结果无保证 | 没有标签,无法量化"对不对",只能靠业务判断 |
| k 的选择主观 | 肘部法则的拐点可能不明显,不同方法结论可能不同 |
| 对初始化敏感 | K-Means 不同的初始质心可能产生不同结果(需 n_init > 1) |
| 只能发现凸形簇 | K-Means 假设簇是球形的,环形/月牙形数据需用 DBSCAN |
| 降维有信息损失 | PCA 丢弃了方差小的维度,可能丢掉重要的细微差异 |
| 评估困难 | 没有"标准答案",评估依赖领域知识和可视化 |
11. 综合示例:Mall Customer 数据集客户分群
▶ 示例:数据驱动的客户分群完整流程(难度⭐⭐⭐)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
# ============================================
# Step 1: Load and explore the Mall Customer dataset
# ============================================
np.random.seed(42)
n = 200
data = {
'CustomerID': range(1, n + 1),
'Gender': np.random.choice(['Male', 'Female'], n),
'Age': np.random.randint(18, 70, n),
'Annual_Income_kUSD': np.random.randint(15, 137, n),
'Spending_Score': np.random.randint(1, 100, n),
}
df = pd.DataFrame(data)
# Inject realistic cluster structure
df.loc[:49, 'Annual_Income_kUSD'] = np.random.randint(15, 40, 50)
df.loc[:49, 'Spending_Score'] = np.random.randint(60, 100, 50)
df.loc[50:99, 'Annual_Income_kUSD'] = np.random.randint(70, 137, 50)
df.loc[50:99, 'Spending_Score'] = np.random.randint(60, 100, 50)
df.loc[100:149, 'Annual_Income_kUSD'] = np.random.randint(70, 137, 50)
df.loc[100:149, 'Spending_Score'] = np.random.randint(1, 40, 50)
df.loc[150:, 'Annual_Income_kUSD'] = np.random.randint(15, 40, 50)
df.loc[150:, 'Spending_Score'] = np.random.randint(1, 40, 50)
print("Dataset shape:", df.shape)
print(df.head(10))
print("\nDescriptive stats:")
print(df[['Age', 'Annual_Income_kUSD', 'Spending_Score']].describe())
# ============================================
# Step 2: Feature selection and scaling
# ============================================
features = ['Age', 'Annual_Income_kUSD', 'Spending_Score']
X = df[features].values
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# ============================================
# Step 3: Elbow method to find optimal k
# ============================================
inertias = []
silhouette_scores = []
K_range = range(2, 11)
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(X_scaled)
inertias.append(km.inertia_)
silhouette_scores.append(silhouette_score(X_scaled, labels))
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(K_range, inertias, 'bo-', linewidth=2)
axes[0].set_xlabel('Number of clusters (k)')
axes[0].set_ylabel('Inertia')
axes[0].set_title('Elbow Method')
axes[0].axvline(x=4, color='red', linestyle='--', label='Elbow at k=4')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].plot(K_range, silhouette_scores, 'go-', linewidth=2)
axes[1].set_xlabel('Number of clusters (k)')
axes[1].set_ylabel('Silhouette Score')
axes[1].set_title('Silhouette Method')
axes[1].axvline(x=4, color='red', linestyle='--', label='Best at k=4')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('mall_elbow_silhouette.png', dpi=150)
plt.show()
# ============================================
# Step 4: Final clustering with k=4
# ============================================
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
df['Cluster'] = kmeans.fit_predict(X_scaled)
# ============================================
# Step 5: Analyze each cluster
# ============================================
cluster_summary = df.groupby('Cluster')[features].mean()
print("\nCluster profiles (mean values):")
print(cluster_summary)
cluster_counts = df['Cluster'].value_counts().sort_index()
print("\nCluster sizes:")
print(cluster_counts)
# ============================================
# Step 6: PCA for 2D visualization
# ============================================
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
plt.figure(figsize=(10, 7))
colors = ['red', 'blue', 'green', 'orange']
labels_text = [
'Cluster 0: Young High-Spenders',
'Cluster 1: Rich High-Spenders',
'Cluster 2: Rich Low-Spenders',
'Cluster 3: Budget Low-Spenders',
]
for i in range(4):
mask = df['Cluster'] == i
plt.scatter(X_pca[mask, 0], X_pca[mask, 1],
c=colors[i], label=labels_text[i],
alpha=0.6, edgecolors='k', s=50)
centroids_pca = pca.transform(kmeans.cluster_centers_)
plt.scatter(centroids_pca[:, 0], centroids_pca[:, 1],
c='black', marker='X', s=200, label='Centroids')
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})')
plt.title('Mall Customer Segmentation (K-Means + PCA)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('mall_clusters_pca.png', dpi=150)
plt.show()
# ============================================
# Step 7: Business recommendations
# ============================================
print("\n=== Business Recommendations ===")
for i in range(4):
row = cluster_summary.loc[i]
count = cluster_counts[i]
print(f"\nCluster {i} ({count} customers):")
print(f" Avg Age: {row['Age']:.0f}, Income: ${row['Annual_Income_kUSD']:.0f}k, Spending: {row['Spending_Score']:.0f}/100")
Dataset shape: (200, 5)
CustomerID Gender Age Annual_Income_kUSD Spending_Score
0 1 Female 56 28 86
1 2 Male 25 32 79
2 3 Female 38 22 93
3 4 Female 36 37 74
4 5 Male 47 33 68
5 6 Female 32 27 82
6 7 Female 51 18 88
7 8 Male 19 38 95
8 9 Male 23 25 71
9 10 Female 27 39 91
Descriptive stats:
Age Annual_Income_kUSD Spending_Score
count 200.000000 200.000000 200.000000
mean 43.475000 60.720000 50.115000
std 15.441418 41.082376 31.494432
min 18.000000 15.000000 1.000000
max 69.000000 136.000000 99.000000
Cluster profiles (mean values):
Age Annual_Income_kUSD Spending_Score
Cluster
0 41.34 26.82 80.44
1 43.88 99.24 78.32
2 44.92 101.58 19.44
3 43.36 26.24 21.12
Cluster sizes:
0 50
1 50
2 50
3 50
Name: Cluster, dtype: int64
=== Business Recommendations ===
Cluster 0 (50 customers):
Avg Age: 41, Income: $27k, Spending: 80/100
Cluster 1 (50 customers):
Avg Age: 44, Income: $99k, Spending: 78/100
Cluster 2 (50 customers):
Avg Age: 45, Income: $102k, Spending: 19/100
Cluster 3 (50 customers):
Avg Age: 43, Income: $26k, Spending: 21/100
❓ 常见问题
pca.explained_variance_ratio_ 查看每个主成分的贡献,再决定保留几个。📖 小节
- 无监督学习没有标签,目标是发现数据内在结构——聚类找分组,降维找方向
- K-Means 是最实用的聚类算法:初始化 → 分配 → 更新 → 迭代,简单高效
- 选择 k 用肘部法则 + Silhouette 评分交叉验证,最终以业务需求为准
- PCA 找方差最大方向做投影,快速降维、去噪、可视化,但丢弃了非线性结构
- t-SNE 适合 2D/3D 可视化,保持局部邻域,但不适合做模型预处理
- 无监督学习的局限:评估困难、k 选择主观、对初始化敏感、只能发现凸形簇
📝 作业
-
基础题(难度⭐):用
sklearn.datasets.make_blobs生成 3 簇数据,用 K-Means 聚类并画散点图(不同簇用不同颜色,标出质心)。 -
进阶题(难度⭐⭐):对上面的数据绘制肘部法则图和 Silhouette 评分图,确定最优 k。然后故意选 k=2 和 k=8 分别聚类,对比效果,解释为什么选错 k 结果会变差。
-
挑战题(难度⭐⭐⭐):用
sklearn.datasets.load_digits加载手写数字数据集(64 维),先 PCA 降到 2D 可视化,再 t-SNE 降到 2D 可视化,对比两者的效果。写一段分析:PCA 和 t-SNE 各自保留了什么结构?哪个更适合这个数据集的可视化?