在上一节中,我们学习了监督学习的各种模型。今天,我们将转向无监督学习,探索不需要标签数据就能发现数据内在结构的方法。我们将重点学习K-Means聚类和PCA降维技术,这两种方法在数据分析和机器学习中应用广泛。
无监督学习概览
无监督学习是机器学习的一个重要分支,它处理没有标签的数据,目标是发现数据中的潜在结构、模式或规律。
graph TD
A[机器学习] --> B[监督学习]
A --> C[无监督学习]
A --> D[强化学习]
C --> E[聚类]
C --> F[降维]
C --> G[关联规则]
C --> H[异常检测]
E --> I[K-Means]
E --> J[层次聚类]
F --> K[PCA]
F --> L[t-SNE]
聚类分析基础
聚类是一种将相似的数据点分组的技术,目标是使同一组内的数据点尽可能相似,不同组之间的数据点尽可能不同。
K-Means聚类算法
K-Means是最常用的聚类算法之一,它通过迭代优化将数据分为K个簇。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
import seaborn as sns
# 生成示例数据
X_blobs, _ = make_blobs(n_samples=300, centers=4, cluster_std=0.60, random_state=0)
# 可视化原始数据
plt.figure(figsize=(10, 6))
plt.scatter(X_blobs[:, 0], X_blobs[:, 1], s=50)
plt.title('原始数据分布')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.grid(True, alpha=0.3)
plt.show()
print(f"数据形状: {X_blobs.shape}")
print("这是一个包含4个自然簇的人造数据集")
K-Means算法原理
K-Means算法的步骤如下:
- 选择簇的数量K
- 随机初始化K个聚类中心
- 将每个数据点分配给最近的聚类中心
- 更新聚类中心为所属点的均值
- 重复步骤3-4直到收敛
class SimpleKMeans:
"""简单K-Means实现"""
def __init__(self, k=3, max_iters=100, random_state=None):
self.k = k
self.max_iters = max_iters
self.random_state = random_state
def fit(self, X):
"""训练模型"""
if self.random_state:
np.random.seed(self.random_state)
# 初始化聚类中心
self.centroids = X[np.random.choice(X.shape[0], self.k, replace=False)]
for _ in range(self.max_iters):
# 计算每个点到聚类中心的距离
distances = np.sqrt(((X - self.centroids[:, np.newaxis])**2).sum(axis=2))
# 分配每个点到最近的聚类中心
labels = np.argmin(distances, axis=0)
# 更新聚类中心
new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(self.k)])
# 检查收敛
if np.all(self.centroids == new_centroids):
break
self.centroids = new_centroids
self.labels_ = labels
return self
def predict(self, X):
"""预测新数据点的簇"""
distances = np.sqrt(((X - self.centroids[:, np.newaxis])**2).sum(axis=2))
return np.argmin(distances, axis=0)
# 使用自定义K-Means
simple_kmeans = SimpleKMeans(k=4, random_state=42)
simple_kmeans.fit(X_blobs)
simple_labels = simple_kmeans.labels_
# 使用sklearn K-Means
sklearn_kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
sklearn_labels = sklearn_kmeans.fit_predict(X_blobs)
# 比较结果
plt.figure(figsize=(15, 6))
plt.subplot(1, 3, 1)
plt.scatter(X_blobs[:, 0], X_blobs[:, 1], s=50)
plt.title('原始数据')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.grid(True, alpha=0.3)
plt.subplot(1, 3, 2)
plt.scatter(X_blobs[:, 0], X_blobs[:, 1], c=simple_labels, cmap='viridis', s=50)
plt.scatter(simple_kmeans.centroids[:, 0], simple_kmeans.centroids[:, 1],
c='red', marker='x', s=200, linewidths=3, label='聚类中心')
plt.title('自定义K-Means聚类结果')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.legend()
plt.grid(True, alpha=0.3)
plt.subplot(1, 3, 3)
plt.scatter(X_blobs[:, 0], X_blobs[:, 1], c=sklearn_labels, cmap='viridis', s=50)
plt.scatter(sklearn_kmeans.cluster_centers_[:, 0], sklearn_kmeans.cluster_centers_[:, 1],
c='red', marker='x', s=200, linewidths=3, label='聚类中心')
plt.title('Sklearn K-Means聚类结果')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("聚类中心比较:")
print("自定义K-Means聚类中心:")
for i, centroid in enumerate(simple_kmeans.centroids):
print(f" 簇 {i}: ({centroid[0]:.2f}, {centroid[1]:.2f})")
print("\nSklearn K-Means聚类中心:")
for i, centroid in enumerate(sklearn_kmeans.cluster_centers_):
print(f" 簇 {i}: ({centroid[0]:.2f}, {centroid[1]:.2f})")
确定最优K值
选择合适的K值对K-Means算法至关重要,常用的方法是肘部法则。
# 肘部法则确定最优K值
def elbow_method(X, max_k=10):
"""使用肘部法则确定最优K值"""
inertias = []
K_range = range(1, max_k + 1)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X)
inertias.append(kmeans.inertia_) # inertia_是簇内平方和
return K_range, inertias
# 计算不同K值的惯性
K_range, inertias = elbow_method(X_blobs, max_k=10)
# 绘制肘部图
plt.figure(figsize=(10, 6))
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('簇数量 (K)')
plt.ylabel('簇内平方和 (Inertia)')
plt.title('肘部法则确定最优K值')
plt.grid(True, alpha=0.3)
plt.show()
print("不同K值对应的簇内平方和:")
for k, inertia in zip(K_range, inertias):
print(f"K={k}: {inertia:.2f}")
降维技术:PCA
主成分分析(PCA)是一种常用的线性降维技术,它通过找到数据中方差最大的方向来实现降维。
PCA原理
PCA通过以下步骤实现降维:
- 标准化数据
- 计算协方差矩阵
- 计算特征值和特征向量
- 选择前K个最大特征值对应的特征向量
- 将数据投影到新的K维空间
class SimplePCA:
"""简单PCA实现"""
def __init__(self, n_components=2):
self.n_components = n_components
self.components_ = None
self.mean_ = None
def fit(self, X):
"""训练PCA模型"""
# 中心化数据
self.mean_ = np.mean(X, axis=0)
X_centered = X - self.mean_
# 计算协方差矩阵
cov_matrix = np.cov(X_centered, rowvar=False)
# 计算特征值和特征向量
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
# 按特征值降序排列
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 选择前n_components个主成分
self.components_ = eigenvectors[:, :self.n_components]
self.explained_variance_ = eigenvalues[:self.n_components]
self.explained_variance_ratio_ = eigenvalues[:self.n_components] / np.sum(eigenvalues)
return self
def transform(self, X):
"""将数据投影到主成分空间"""
X_centered = X - self.mean_
return np.dot(X_centered, self.components_)
def fit_transform(self, X):
"""训练并转换数据"""
self.fit(X)
return self.transform(X)
# 创建高维数据进行PCA演示
np.random.seed(42)
X_high_dim = np.random.randn(100, 5)
# 添加一些相关性
X_high_dim[:, 1] = X_high_dim[:, 0] * 0.8 + np.random.randn(100) * 0.2
X_high_dim[:, 2] = X_high_dim[:, 0] * 0.5 + X_high_dim[:, 1] * 0.3 + np.random.randn(100) * 0.3
print(f"原始高维数据形状: {X_high_dim.shape}")
# 使用自定义PCA
simple_pca = SimplePCA(n_components=2)
X_pca_simple = simple_pca.fit_transform(X_high_dim)
# 使用sklearn PCA
sklearn_pca = PCA(n_components=2)
X_pca_sklearn = sklearn_pca.fit_transform(X_high_dim)
# 比较结果
plt.figure(figsize=(15, 6))
plt.subplot(1, 3, 1)
plt.scatter(X_high_dim[:, 0], X_high_dim[:, 1], alpha=0.7)
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.title('原始高维数据 (前两维)')
plt.grid(True, alpha=0.3)
plt.subplot(1, 3, 2)
plt.scatter(X_pca_simple[:, 0], X_pca_simple[:, 1], alpha=0.7)
plt.xlabel('第一主成分')
plt.ylabel('第二主成分')
plt.title('自定义PCA降维结果')
plt.grid(True, alpha=0.3)
plt.subplot(1, 3, 3)
plt.scatter(X_pca_sklearn[:, 0], X_pca_sklearn[:, 1], alpha=0.7)
plt.xlabel('第一主成分')
plt.ylabel('第二主成分')
plt.title('Sklearn PCA降维结果')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("主成分解释方差比例:")
print("自定义PCA:")
for i, ratio in enumerate(simple_pca.explained_variance_ratio_):
print(f" 主成分 {i+1}: {ratio:.4f} ({ratio*100:.2f}%)")
print("\nSklearn PCA:")
for i, ratio in enumerate(sklearn_pca.explained_variance_ratio_):
print(f" 主成分 {i+1}: {ratio:.4f} ({ratio*100:.2f}%)")
PCA在图像处理中的应用
# 使用手写数字数据集演示PCA
from sklearn.datasets import load_digits
# 加载手写数字数据
digits = load_digits()
X_digits, y_digits = digits.data, digits.target
print(f"手写数字数据形状: {X_digits.shape}")
print(f"每个图像大小: 8x8 像素")
print(f"类别数量: {len(np.unique(y_digits))}")
# 可视化一些原始图像
fig, axes = plt.subplots(2, 5, figsize=(12, 6))
for i, ax in enumerate(axes.flat):
ax.imshow(digits.images[i], cmap='gray')
ax.set_title(f'数字: {digits.target[i]}')
ax.axis('off')
plt.suptitle('原始手写数字图像')
plt.tight_layout()
plt.show()
# 使用PCA降维
pca_digits = PCA(n_components=2)
X_digits_pca = pca_digits.fit_transform(X_digits)
# 可视化降维结果
plt.figure(figsize=(12, 8))
scatter = plt.scatter(X_digits_pca[:, 0], X_digits_pca[:, 1], c=y_digits, cmap='tab10', alpha=0.7)
plt.xlabel('第一主成分')
plt.ylabel('第二主成分')
plt.title('手写数字PCA降维结果 (2D)')
plt.colorbar(scatter, label='数字类别')
plt.grid(True, alpha=0.3)
plt.show()
# 查看解释方差比例
print("前10个主成分解释的方差比例:")
for i in range(10):
print(f" 主成分 {i+1}: {pca_digits.explained_variance_ratio_[i]:.4f}")
# 累积解释方差
cumsum_ratio = np.cumsum(pca_digits.explained_variance_ratio_)
plt.figure(figsize=(10, 6))
plt.plot(range(1, len(cumsum_ratio)+1), cumsum_ratio, 'bo-')
plt.xlabel('主成分数量')
plt.ylabel('累积解释方差比例')
plt.title('PCA累积解释方差')
plt.grid(True, alpha=0.3)
plt.show()
print(f"前2个主成分解释了 {cumsum_ratio[1]*100:.2f}% 的方差")
# 使用更多主成分重建图像
def reconstruct_images(pca, X, n_components, n_images=5):
"""使用不同数量的主成分重建图像"""
# 重新训练PCA
pca_full = PCA(n_components=n_components)
X_pca = pca_full.fit_transform(X)
X_reconstructed = pca_full.inverse_transform(X_pca)
return X_reconstructed[:n_images]
# 重建图像
fig, axes = plt.subplots(3, 5, figsize=(15, 10))
# 原始图像
for i in range(5):
axes[0, i].imshow(digits.images[i], cmap='gray')
axes[0, i].set_title(f'原始图像 {i}')
axes[0, i].axis('off')
# 使用10个主成分重建
reconstructed_10 = reconstruct_images(PCA(n_components=10), X_digits, 10)
for i in range(5):
axes[1, i].imshow(reconstructed_10[i].reshape(8, 8), cmap='gray')
axes[1, i].set_title(f'10个主成分重建 {i}')
axes[1, i].axis('off')
# 使用30个主成分重建
reconstructed_30 = reconstruct_images(PCA(n_components=30), X_digits, 30)
for i in range(5):
axes[2, i].imshow(reconstructed_30[i].reshape(8, 8), cmap='gray')
axes[2, i].set_title(f'30个主成分重建 {i}')
axes[2, i].axis('off')
plt.suptitle('PCA图像重建效果')
plt.tight_layout()
plt.show()
print("PCA图像重建说明:")
print("- 使用10个主成分可以捕捉基本形状")
print("- 使用30个主成分可以保留更多细节")
print("- 原始图像有64个特征(8x8像素)")
print("- 通过PCA可以有效压缩数据")
聚类与降维结合应用
将聚类和降维技术结合使用可以更好地理解和分析复杂数据。
# 在手写数字数据上应用聚类
from sklearn.metrics import adjusted_rand_score
# 使用PCA降维后再进行聚类
pca_cluster = PCA(n_components=20)
X_digits_reduced = pca_cluster.fit_transform(X_digits)
# K-Means聚类
kmeans_digits = KMeans(n_clusters=10, random_state=42, n_init=10)
cluster_labels = kmeans_digits.fit_predict(X_digits_reduced)
# 评估聚类效果
ari_score = adjusted_rand_score(y_digits, cluster_labels)
print(f"聚类与真实标签的调整兰德指数: {ari_score:.4f}")
# 可视化聚类结果
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
scatter1 = plt.scatter(X_digits_pca[:, 0], X_digits_pca[:, 1], c=y_digits, cmap='tab10', alpha=0.7)
plt.xlabel('第一主成分')
plt.ylabel('第二主成分')
plt.title('真实标签')
plt.colorbar(scatter1, label='数字类别')
plt.subplot(1, 2, 2)
scatter2 = plt.scatter(X_digits_pca[:, 0], X_digits_pca[:, 1], c=cluster_labels, cmap='tab10', alpha=0.7)
plt.xlabel('第一主成分')
plt.ylabel('第二主成分')
plt.title('K-Means聚类结果')
plt.colorbar(scatter2, label='聚类标签')
plt.tight_layout()
plt.show()
# 分析每个聚类的数字分布
plt.figure(figsize=(12, 8))
for i in range(10):
plt.subplot(2, 5, i+1)
# 找到属于聚类i的样本
cluster_samples = np.where(cluster_labels == i)[0]
# 统计真实标签分布
unique, counts = np.unique(y_digits[cluster_samples], return_counts=True)
plt.bar(unique, counts)
plt.title(f'聚类 {i} 的数字分布')
plt.xlabel('数字')
plt.ylabel('数量')
plt.tight_layout()
plt.show()
print("聚类分析结果:")
for i in range(10):
cluster_samples = np.where(cluster_labels == i)[0]
unique, counts = np.unique(y_digits[cluster_samples], return_counts=True)
dominant_digit = unique[np.argmax(counts)]
purity = np.max(counts) / len(cluster_samples)
print(f"聚类 {i}: 主要数字={dominant_digit}, 纯度={purity:.3f}")
本周学习总结
今天我们深入学习了无监督学习中的两种重要技术:
-
K-Means聚类
- 理解了K-Means算法的工作原理
- 学会了实现自定义K-Means算法
- 掌握了肘部法则确定最优K值
-
PCA降维技术
- 学习了PCA的数学原理和实现方法
- 理解了主成分和解释方差的概念
- 应用了PCA进行图像数据降维和重建
-
结合应用
- 将聚类和降维技术结合使用
- 在真实数据集上进行了实践
graph TD
A[无监督学习] --> B[聚类]
A --> C[降维]
B --> D[K-Means]
B --> E[评估方法]
C --> F[PCA]
C --> G[应用案例]
D --> H[算法原理]
D --> I[实现方法]
F --> J[数学原理]
F --> K[图像应用]
课后练习
- 运行本节所有代码示例,理解聚类和降维的原理
- 尝试在不同的数据集上应用K-Means和PCA,观察效果
- 实现K-Means++初始化方法,改进聚类中心的初始化
- 使用t-SNE替代PCA进行降维,比较两种方法的可视化效果
下节预告
下一节我们将学习知识表示与检索技术,包括知识图谱和向量检索等现代方法,这些技术在大语言模型和信息检索中非常重要,敬请期待!
有任何疑问请在讨论区留言,我们会定期回复大家的问题。