无监督学习实战:K-Means聚类与PCA降维技术

4 阅读1分钟

在上一节中,我们学习了监督学习的各种模型。今天,我们将转向无监督学习,探索不需要标签数据就能发现数据内在结构的方法。我们将重点学习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算法的步骤如下:

  1. 选择簇的数量K
  2. 随机初始化K个聚类中心
  3. 将每个数据点分配给最近的聚类中心
  4. 更新聚类中心为所属点的均值
  5. 重复步骤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通过以下步骤实现降维:

  1. 标准化数据
  2. 计算协方差矩阵
  3. 计算特征值和特征向量
  4. 选择前K个最大特征值对应的特征向量
  5. 将数据投影到新的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}")

本周学习总结

今天我们深入学习了无监督学习中的两种重要技术:

  1. K-Means聚类

    • 理解了K-Means算法的工作原理
    • 学会了实现自定义K-Means算法
    • 掌握了肘部法则确定最优K值
  2. PCA降维技术

    • 学习了PCA的数学原理和实现方法
    • 理解了主成分和解释方差的概念
    • 应用了PCA进行图像数据降维和重建
  3. 结合应用

    • 将聚类和降维技术结合使用
    • 在真实数据集上进行了实践
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[图像应用]

课后练习

  1. 运行本节所有代码示例,理解聚类和降维的原理
  2. 尝试在不同的数据集上应用K-Means和PCA,观察效果
  3. 实现K-Means++初始化方法,改进聚类中心的初始化
  4. 使用t-SNE替代PCA进行降维,比较两种方法的可视化效果

下节预告

下一节我们将学习知识表示与检索技术,包括知识图谱和向量检索等现代方法,这些技术在大语言模型和信息检索中非常重要,敬请期待!


有任何疑问请在讨论区留言,我们会定期回复大家的问题。