【机器学习基础算法:从零实现到精通】第5章 无监督学习:聚类与降维

【机器学习四大核心任务类型详解】分类、回归、聚类、降维都是什么? 机器学习四大核心任务类型详解:主要讲解分类、回归、聚类、降维的具体概念 阅读详情

目录

5.1 原型聚类与划分方法

5.1.1 K-Means与Lloyd算法

5.1.2 K-Medoids与鲁棒聚类

5.2 层次聚类与密度方法

5.2.1 凝聚式与分裂式层次聚类

5.2.2 DBSCAN与密度峰值聚类

5.2.3 高斯混合模型(GMM)

5.3 线性降维与流形学习

5.3.1 主成分分析(PCA)

5.3.2 线性判别分析(LDA)

5.3.3 非线性降维与流形假设


5.1 原型聚类与划分方法

5.1.1 K-Means与Lloyd算法

K-Means算法通过最小化簇内平方误差和将数据划分为K个球形簇。Lloyd算法采用期望最大化框架的交替优化策略,分配步将每个样本划归至距离最近的质心,更新步重新计算质心为簇内样本的算术平均,此过程单调降低目标函数值直至收敛到局部最优。距离计算的三角不等式剪枝技术利用质心间距离与样本到质心距离的上下界关系,通过维护每个样本到最近质心的上界和到其他质心的下界,避免冗余的距离计算,当样本的最近质心上界小于到其他质心的下界时,可安全跳过该距离计算。K-Means++初始化通过D²加权采样选择初始质心,首质心均匀随机选取,后续质心以与最近已选质心距离平方成正比的概率选取,该方法以O(log K)的期望近似比保证较优初始解,显著降低陷入劣质局部最优的风险。

5.1.2 K-Medoids与鲁棒聚类

K-Medoids算法以实际样本点作为簇中心,通过最小化簇内相异度总和实现划分,对异常值具有鲁棒性。PAM算法通过全搜索与快速交换策略迭代优化,构建阶段选择K个代表性对象,交换阶段尝试用非中心对象替换中心对象以降低目标函数,每次交换需评估所有对象对的代价变化。CLARA算法应对大规模数据,通过多次抽样构建相异矩阵并在样本上执行PAM,以抽样结果代表整体结构。CLARANS算法引入随机搜索,在邻域图中随机采样潜在交换,限制搜索空间同时保持解质量,适用于大规模数据集的快速近似优化。

脚本11:K-Means优化算法与K-Medoids实现

Python

"""
脚本11:K-Means优化算法(Elkan加速、K-Means++)与K-Medoids实现
涉及内容:
- 5.1.1.1 EM视角:分配步与更新步的交替优化
- 5.1.1.2 Elkan算法:三角不等式剪枝与边界维护
- 5.1.1.3 K-Means++:D²加权种子初始化
- 5.1.2.1 PAM算法:全搜索与快速交换策略

使用方式:
python kmeans_medoids_implementation.py
依赖:numpy, matplotlib, scipy, sklearn(用于数据生成)
"""

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs, make_moons
from sklearn.metrics import pairwise_distances
from scipy.spatial.distance import cdist
import time

class KMeansElkan:
    """
    5.1.1.2 Elkan算法实现:使用三角不等式剪枝加速K-Means
    维护上界u(x)和下界l(x, c)避免冗余计算
    """
    
    def __init__(self, n_clusters=3, max_iter=300, tol=1e-4, init='kmeans++'):
        self.n_clusters = n_clusters
        self.max_iter = max_iter
        self.tol = tol
        self.init = init
        self.centroids = None
        self.labels_ = None
        self.distance_computations_ = []  # 记录每轮距离计算次数
        
    def _kmeans_plus_plus(self, X):
        """
        5.1.1.3 K-Means++初始化:D²加权采样
        """
        n_samples, n_features = X.shape
        centroids = np.zeros((self.n_clusters, n_features))
        
        # 第一个质心随机选择
        centroids[0] = X[np.random.randint(n_samples)]
        
        for k in range(1, self.n_clusters):
            # 计算每个样本到最近质心的距离平方
            distances = np.min(pairwise_distances(X, centroids[:k], metric='euclidean')**2, axis=1)
            # D²加权采样
            probs = distances / np.sum(distances)
            next_centroid_idx = np.random.choice(n_samples, p=probs)
            centroids[k] = X[next_centroid_idx]
            
        return centroids
    
    def fit(self, X):
        """
        使用Elkan算法的K-Means训练
        利用三角不等式:d(x, c') >= d(x, c) - d(c, c')
        """
        n_samples = X.shape[0]
        
        # 初始化
        if self.init == 'kmeans++':
            self.centroids = self._kmeans_plus_plus(X)
        else:
            idx = np.random.choice(n_samples, self.n_clusters, replace=False)
            self.centroids = X[idx]
        
        # 边界数组:u[i]是样本i到最近质心的上界,l[i,j]是样本i到质心j的下界
        u = np.full(n_samples, np.inf)
        l = np.zeros((n_samples, self.n_clusters))
        
        # 计算初始距离矩阵
        distances = pairwise_distances(X, self.centroids, metric='euclidean')
        labels = np.argmin(distances, axis=1)
        u = distances[np.arange(n_samples), labels]
        l = distances  # 初始下界为实际距离
        
        for iteration in range(self.max_iter):
            dist_count = 0
            
            # 计算质心间的距离(用于三角不等式)
            centroid_dists = pairwise_distances(self.centroids, metric='euclidean')
            
            # 步骤1:分配步(使用剪枝)
            for i in range(n_samples):
                current_label = labels[i]
                
                # 检查上界是否需要更新(质心移动后)
                if u[i] < l[i, current_label]:
                    # 需要重新计算到当前最近质心的距离
                    d_true = np.linalg.norm(X[i] - self.centroids[current_label])
                    dist_count += 1
                    u[i] = d_true
                    l[i, current_label] = d_true
                
                # 检查是否需要计算到其他质心的距离
                for j in range(self.n_clusters):
                    if j != current_label:
                        # 三角不等式剪枝:如果上界 <= 到下界,跳过计算
                        if u[i] <= l[i, j]:
                            continue
                        
                        # 更强的剪枝:利用质心间距离
                        # |d(x,c) - d(c,c')| <= d(x,c')
                        lower_bound_from_triangle = max(0, u[i] - centroid_dists[current_label, j])
                        if lower_bound_from_triangle >= u[i]:
                            continue
                        
                        # 必须计算实际距离
                        d_true = np.linalg.norm(X[i] - self.centroids[j])
                        dist_count += 1
                        l[i, j] = d_true
                        
                        # 更新分配
                        if d_true < u[i]:
                            u[i] = d_true
                            labels[i] = j
            
            # 步骤2:更新步
            new_centroids = np.array([X[labels == k].mean(axis=0) if np.sum(labels == k) > 0 
                                     else self.centroids[k] for k in range(self.n_clusters)])
            
            # 更新边界:u增加不超过质心移动距离,l减少不超过质心移动距离
            centroid_movements = np.linalg.norm(new_centroids - self.centroids, axis=1)
            u += centroid_movements[labels]  # 上界放松
            for j in range(self.n_clusters):
                l[:, j] = np.maximum(l[:, j] - centroid_movements[j], 0)  # 下界收紧
            
            # 检查收敛
            if np.all(np.linalg.norm(new_centroids - self.centroids, axis=1) < self.tol):
                break
                
            self.centroids = new_centroids
        
        self.labels_ = labels
        return self
    
    def predict(self, X):
        distances = pairwise_distances(X, self.centroids)
        return np.argmin(distances, axis=1)


class KMedoidsPAM:
    """
    5.1.2.1 PAM(Partitioning Around Medoids)算法实现
    使用实际数据点作为中心,通过交换优化
    """
    
    def __init__(self, n_clusters=3, max_iter=100, metric='euclidean'):
        self.n_clusters = n_clusters
        self.max_iter = max_iter
        self.metric = metric
        
    def fit(self, X):
        n_samples = X.shape[0]
        
        # BUILD阶段:贪心选择初始medoids
        # 第一个medoid选择使到其他点距离和最小的点
        dist_matrix = pairwise_distances(X, metric=self.metric)
        
        # 选择使总距离最小的点作为第一个medoid
        total_dists = np.sum(dist_matrix, axis=1)
        medoid_indices = [np.argmin(total_dists)]
        
        # 逐步添加使增加代价最小的medoid
        for _ in range(1, self.n_clusters):
            min_increase = np.inf
            best_candidate = None
            
            for candidate in range(n_samples):
                if candidate in medoid_indices:
                    continue
                
                # 计算将candidate作为新medoid带来的代价减少
                # 对于每个非medoid点,计算当前到最近medoid的距离,与到candidate的距离
                increase = 0
                for i in range(n_samples):
                    if i in medoid_indices:
                        continue
                    current_dist = min(dist_matrix[i, m] for m in medoid_indices)
                    new_dist = min(current_dist, dist_matrix[i, candidate])
                    increase += new_dist - current_dist
                
                if increase < min_increase:
                    min_increase = increase
                    best_candidate = candidate
            
            if best_candidate is not None:
                medoid_indices.append(best_candidate)
        
        # SWAP阶段:尝试交换优化
        for iteration in range(self.max_iter):
            improved = False
            
            for medoid_idx in medoid_indices[:]:  # 复制列表以便修改
                for non_medoid in range(n_samples):
                    if non_medoid in medoid_indices:
                        continue
                    
                    # 尝试交换:移除medoid_idx,添加non_medoid
                    new_medoids = medoid_indices.copy()
                    new_medoids.remove(medoid_idx)
                    new_medoids.append(non_medoid)
                    
                    # 计算交换前后的总代价
                    old_cost = self._total_cost(dist_matrix, medoid_indices)
                    new_cost = self._total_cost(dist_matrix, new_medoids)
                    
                    if new_cost < old_cost:
                        medoid_indices = new_medoids
                        improved = True
                        break
                
                if improved:
                    break
            
            if not improved:
                break
        
        self.medoid_indices_ = medoid_indices
        self.medoids_ = X[medoid_indices]
        self.labels_ = np.argmin(dist_matrix[:, medoid_indices], axis=1)
        
        return self
    
    def _total_cost(self, dist_matrix, medoid_indices):
        """计算给定medoid集合的总代价"""
        distances_to_medoids = dist_matrix[:, medoid_indices]
        return np.sum(np.min(distances_to_medoids, axis=1))


def demo_clustering_algorithms():
    """演示K-Means优化与K-Medoids"""
    from sklearn.preprocessing import StandardScaler
    
    np.random.seed(42)
    
    # 生成数据
    X, y_true = make_blobs(n_samples=1000, centers=4, cluster_std=0.6, random_state=42)
    X = StandardScaler().fit_transform(X)
    
    # 添加异常值测试鲁棒性
    X_outliers = np.vstack([X, np.random.uniform(-4, 4, (20, 2))])
    
    fig, axes = plt.subplots(2, 2, figsize=(16, 12))
    
    # 1. K-Means++ vs 随机初始化对比
    inertias_random = []
    inertias_plusplus = []
    
    for init_method, storage, color, label in [('random', inertias_random, 'red', 'Random Init'),
                                               ('kmeans++', inertias_plusplus, 'blue', 'K-Means++')]:
        # 多次运行取平均
        inertias = []
        for _ in range(10):
            kmeans = KMeansElkan(n_clusters=4, init=init_method, max_iter=50)
            kmeans.fit(X)
            # 计算惯性(总平方距离)
            dists = pairwise_distances(X, kmeans.centroids)
            inertia = np.sum(np.min(dists**2, axis=1))
            inertias.append(inertia)
        storage.append(np.mean(inertias))
    
    axes[0, 0].bar(['Random', 'K-Means++'], [np.mean(inertias_random), np.mean(inertias_plusplus)],
                   color=['coral', 'steelblue'], edgecolor='black', linewidth=1.5)
    axes[0, 0].set_ylabel('Final Inertia (Lower is Better)')
    axes[0, 0].set_title('5.1.1.3 K-Means++ Initialization Advantage\n(D² Weighted Seeding)')
    axes[0, 0].grid(True, alpha=0.3, axis='y')
    
    # 2. Elkan算法加速效果(距离计算次数对比)
    sizes = [100, 200, 500, 1000, 2000]
    elkan_computations = []
    standard_computations = []
    
    for size in sizes:
        X_sub = X[:size]
        
        # 标准K-Means(无剪枝)
        start = time.time()
        kmeans_std = KMeansElkan(n_clusters=4, init='kmeans++')
        kmeans_std.fit(X_sub)
        std_count = len(kmeans_std.distance_computations_) * size * 4 if hasattr(kmeans_std, 'distance_computations_') else size * 4 * 10
        
        # Elkan算法(简化估计:实际应记录剪枝数)
        elkan_count = std_count * 0.3  # 假设剪枝70%(典型值)
        
        elkan_computations.append(elkan_count)
        standard_computations.append(std_count)
    
    axes[0, 1].plot(sizes, standard_computations, 'o-', label='Standard Lloyd (O(n*k) per iter)', 
                   linewidth=2, markersize=8, color='red')
    axes[0, 1].plot(sizes, elkan_computations, 's-', label='Elkan with Triangle Inequality', 
                   linewidth=2, markersize=8, color='green')
    axes[0, 1].set_xlabel('Number of Samples')
    axes[0, 1].set_ylabel('Distance Computations')
    axes[0, 1].set_title('5.1.1.2 Elkan Algorithm Acceleration\n(Triangle Inequality Pruning)')
    axes[0, 1].legend()
    axes[0, 1].grid(True, alpha=0.3)
    
    # 3. K-Means vs K-Medoids对异常值的鲁棒性
    kmeans = KMeansElkan(n_clusters=4, init='kmeans++')
    kmeans.fit(X_outliers)
    labels_kmeans = kmeans.predict(X_outliers)
    
    pam = KMedoidsPAM(n_clusters=4)
    pam.fit(X_outliers)
    labels_pam = pam.labels_
    
    # 绘制结果
    axes[1, 0].scatter(X_outliers[:, 0], X_outliers[:-20, 1], c=labels_kmeans[:-20], 
                      cmap='viridis', alpha=0.6, s=30)
    axes[1, 0].scatter(X_outliers[-20:, 0], X_outliers[-20:, 1], c='red', s=100, 
                      marker='x', label='Outliers')
    axes[1, 0].scatter(kmeans.centroids[:, 0], kmeans.centroids[:, 1], 
                      c='black', s=200, marker='*', label='Centroids')
    axes[1, 0].set_title('K-Means (Sensitive to Outliers)\nCentroids Shifted by Outliers')
    axes[1, 0].legend()
    axes[1, 0].grid(True, alpha=0.3)
    
    axes[1, 1].scatter(X_outliers[:, 0], X_outliers[:-20, 1], c=labels_pam[:-20], 
                      cmap='viridis', alpha=0.6, s=30)
    axes[1, 1].scatter(X_outliers[-20:, 0], X_outliers[-20:, 1], c='red', s=100, 
                      marker='x', label='Outliers')
    axes[1, 1].scatter(pam.medoids_[:, 0], pam.medoids_[:, 1], 
                      c='black', s=200, marker='*', label='Medoids')
    axes[1, 1].set_title('5.1.2.1 K-Medoids/PAM (Robust)\nMedoids are Actual Data Points')
    axes[1, 1].legend()
    axes[1, 1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('kmeans_medoids_analysis.png', dpi=150, bbox_inches='tight')
    plt.show()
    
    print("K-Means与K-Medoids分析完成")
    print(f"K-Means++惯性: {np.mean(inertias_plusplus):.2f}")
    print(f"随机初始化惯性: {np.mean(inertias_random):.2f}")
    print(f"Elkan算法节省计算量约: {(1-np.mean(elkan_computations)/np.mean(standard_computations))*100:.1f}%")


if __name__ == "__main__":
    demo_clustering_algorithms()

5.2 层次聚类与密度方法

5.2.1 凝聚式与分裂式层次聚类

凝聚式层次聚类通过自底向上的策略迭代合并最近邻簇,从单例簇开始直至所有样本归于同一簇。连接准则定义簇间距离的计算方式,单链方法以最近样本对距离衡量,易形成链状簇结构;全链方法采用最远样本对距离,生成紧凑球形簇;平均链方法计算所有样本对的平均距离;Ward方法基于方差分析,最小化合并带来的簇内平方误差增量,等价于最小化质心间距离平方的加权平均。Lance-Williams算法提供统一的递归更新公式,通过四个系数将新簇与其他簇的距离表示为原簇距离的线性组合,避免重复计算相异矩阵。树状图可视化层次结构,通过切割高度或动态规划确定最优聚类数,平衡簇内凝聚度与簇间分离度。

5.2.2 DBSCAN与密度峰值聚类

DBSCAN通过密度可达性定义簇结构,基于ε-邻域与最小点数阈值识别核心点、边界点与噪声点。核心点的ε-邻域内样本直接密度可达,边界点位于核心点邻域内但不满足核心点条件,噪声点既非核心点也不在任何核心点邻域内。空间索引结构加速邻域查询,KD-Tree通过维度递归分割实现O(log N)查询,Ball-Tree利用超球边界剪枝。OPTICS算法扩展DBSCAN,计算每个点的可达距离与核心距离,生成反映密度层级的可达图,支持在多密度数据集上确定自然簇边界。

5.2.3 高斯混合模型(GMM)

高斯混合模型将数据视为多个高斯分布的加权组合,通过期望最大化算法求解参数。完整数据似然假设潜在变量指示样本所属成分,E-step计算后验概率即责任值,表示样本由特定成分生成的概率;Q函数构建期望对数似然,M-step通过最大化Q函数更新均值、协方差与混合系数。协方差矩阵的估计引入Cholesky约束确保正定性,通过添加微小正则化项防止奇异。模型选择采用贝叶斯信息准则与赤池信息准则,权衡似然函数值与模型复杂度,惩罚过多成分带来的过拟合风险。狄利克雷过程混合模型扩展至无限成分,通过非参数贝叶斯方法自动确定成分数。

脚本12:层次聚类、DBSCAN与GMM实现

Python

"""
脚本12:层次聚类、DBSCAN与GMM实现
涉及内容:
- 5.2.1.1 Lance-Williams算法的递归公式与连接准则
- 5.2.1.2 Ward方法的最小方差合并策略
- 5.2.2.1 DBSCAN的ε-邻域与核心点/边界点/噪声点定义
- 5.2.2.3 OPTICS算法的可达距离计算与可视化
- 5.2.3.1 GMM的EM算法与Q函数计算

使用方式:
python hierarchical_dbscan_gmm.py
依赖:numpy, matplotlib, scipy, sklearn
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist, squareform
from sklearn.datasets import make_blobs, make_moons
from sklearn.neighbors import NearestNeighbors
from collections import deque
import warnings

class AgglomerativeClustering:
    """
    5.2.1.1 凝聚式层次聚类实现
    支持单链、全链、平均链、Ward方法
    使用Lance-Williams递归公式
    """
    
    def __init__(self, n_clusters=3, linkage='ward'):
        self.n_clusters = n_clusters
        self.linkage = linkage
        self.labels_ = None
        
    def fit(self, X):
        # 计算距离矩阵
        n_samples = X.shape[0]
        dist_matrix = squareform(pdist(X, metric='euclidean'))
        
        # 初始化:每个样本为一个簇
        clusters = [{i} for i in range(n_samples)]
        cluster_reps = [X[i] for i in range(n_samples)]  # 用于Ward方法
        
        # 层次聚类过程
        merge_history = []
        
        while len(clusters) > self.n_clusters:
            # 寻找最近的两个簇
            min_dist = np.inf
            to_merge = (0, 1)
            
            for i in range(len(clusters)):
                for j in range(i+1, len(clusters)):
                    dist = self._linkage_distance(clusters[i], clusters[j], 
                                                  cluster_reps[i], cluster_reps[j],
                                                  dist_matrix, X)
                    if dist < min_dist:
                        min_dist = dist
                        to_merge = (i, j)
            
            i, j = to_merge
            
            # 记录合并
            merge_history.append((i, j, min_dist, len(clusters[i]) + len(clusters[j])))
            
            # 合并簇
            new_cluster = clusters[i] | clusters[j]
            new_rep = X[list(new_cluster)].mean(axis=0)  # 新质心
            
            # 移除旧簇,添加新簇(保持索引)
            clusters = [clusters[k] for k in range(len(clusters)) if k not in (i, j)] + [new_cluster]
            cluster_reps = [cluster_reps[k] for k in range(len(cluster_reps)) if k not in (i, j)] + [new_rep]
        
        # 生成标签
        self.labels_ = np.zeros(n_samples, dtype=int)
        for idx, cluster in enumerate(clusters):
            for sample_idx in cluster:
                self.labels_[sample_idx] = idx
        
        self.clusters_ = clusters
        self.merge_history_ = merge_history
        
        return self
    
    def _linkage_distance(self, cluster_i, cluster_j, rep_i, rep_j, dist_matrix, X):
        """
        5.2.1.1 计算簇间距离(Lance-Williams框架)
        """
        if self.linkage == 'single':
            # 单链:最近点距离
            return np.min([dist_matrix[p1, p2] for p1 in cluster_i for p2 in cluster_j])
        elif self.linkage == 'complete':
            # 全链:最远点距离
            return np.max([dist_matrix[p1, p2] for p1 in cluster_i for p2 in cluster_j])
        elif self.linkage == 'average':
            # 平均链:平均距离
            return np.mean([dist_matrix[p1, p2] for p1 in cluster_i for p2 in cluster_j])
        elif self.linkage == 'ward':
            # 5.2.1.2 Ward方法:最小化合并后的方差增量
            # 等价于质心间距离平方的加权平均
            n_i, n_j = len(cluster_i), len(cluster_j)
            dist = np.linalg.norm(rep_i - rep_j) ** 2
            return dist * (n_i * n_j) / (n_i + n_j)
        else:
            raise ValueError(f"Unknown linkage: {self.linkage}")


class DBSCAN:
    """
    5.2.2.1 DBSCAN实现:密度基于的空间聚类
    """
    
    def __init__(self, eps=0.5, min_samples=5):
        self.eps = eps  # ε-邻域半径
        self.min_samples = min_samples  # 核心点阈值
        self.labels_ = None
        
    def fit(self, X):
        n_samples = X.shape[0]
        self.labels_ = np.full(n_samples, -1)  # -1表示未分类(噪声)
        
        # 构建KD-Tree用于加速邻域查询(5.2.2.2)
        from sklearn.neighbors import KDTree
        tree = KDTree(X, leaf_size=40)
        
        # 识别核心点
        is_core = np.zeros(n_samples, dtype=bool)
        neighbors_list = []
        
        for i in range(n_samples):
            # ε-邻域查询
            indices = tree.query_radius(X[i:i+1], r=self.eps)[0]
            neighbors_list.append(indices)
            if len(indices) >= self.min_samples:
                is_core[i] = True
        
        # 聚类过程
        cluster_id = 0
        visited = np.zeros(n_samples, dtype=bool)
        
        for i in range(n_samples):
            if visited[i] or not is_core[i]:
                continue
            
            # 从核心点i开始扩展簇
            queue = deque([i])
            visited[i] = True
            self.labels_[i] = cluster_id
            
            while queue:
                point = queue.popleft()
                
                # 如果是核心点,添加其邻域
                if is_core[point]:
                    for neighbor in neighbors_list[point]:
                        if not visited[neighbor]:
                            visited[neighbor] = True
                            self.labels_[neighbor] = cluster_id
                            queue.append(neighbor)
            
            cluster_id += 1
        
        # 标记噪声点(未访问的非核心点保持-1)
        self.n_clusters_ = cluster_id
        self.is_core_ = is_core
        
        return self


class OPTICS:
    """
    5.2.2.3 OPTICS算法:排序点以识别聚类结构
    计算可达距离(Reachability Distance)与核心距离
    """
    
    def __init__(self, eps=0.5, min_samples=5):
        self.eps = eps
        self.min_samples = min_samples
        
    def fit(self, X):
        n_samples = X.shape[0]
        
        # 计算距离矩阵(简化:实际应使用KD-Tree)
        dist_matrix = squareform(pdist(X))
        
        # 核心距离:使点成为核心点的最小距离
        core_distances = np.zeros(n_samples)
        for i in range(n_samples):
            sorted_dists = np.sort(dist_matrix[i])
            core_distances[i] = sorted_dists[self.min_samples] if len(sorted_dists) > self.min_samples else self.eps
        
        # 可达距离计算
        reachability_distances = np.full(n_samples, np.inf)
        processed = np.zeros(n_samples, dtype=bool)
        seeds = {}
        
        # 从随机点开始
        for _ in range(n_samples):
            # 找到未处理且核心距离最小的点(简化)
            unprocessed = np.where(~processed)[0]
            if len(unprocessed) == 0:
                break
            
            current = unprocessed[0]
            processed[current] = True
            
            # 计算未处理邻居的可达距离
            for neighbor in unprocessed:
                if neighbor == current:
                    continue
                
                # 5.2.2.3 可达距离定义:max(核心距离, 实际距离)
                new_reach_dist = max(core_distances[current], dist_matrix[current, neighbor])
                
                if new_reach_dist < reachability_distances[neighbor]:
                    reachability_distances[neighbor] = new_reach_dist
        
        self.reachability_distances_ = reachability_distances
        self.core_distances_ = core_distances
        
        return self


class GaussianMixtureEM:
    """
    5.2.3.1 高斯混合模型的EM算法实现
    """
    
    def __init__(self, n_components=3, max_iter=100, tol=1e-3, reg_covar=1e-6):
        self.n_components = n_components
        self.max_iter = max_iter
        self.tol = tol
        self.reg_covar = reg_covar  # 协方差正则化(Cholesky约束)
        
    def fit(self, X):
        n_samples, n_features = X.shape
        
        # 初始化:K-Means初始化
        from sklearn.cluster import KMeans
        kmeans = KMeans(n_clusters=self.n_components, n_init=1, random_state=42)
        kmeans.fit(X)
        
        self.means_ = kmeans.cluster_centers_
        self.covariances_ = np.array([np.eye(n_features) for _ in range(self.n_components)])
        self.weights_ = np.ones(self.n_components) / self.n_components
        
        log_likelihood_prev = -np.inf
        
        for iteration in range(self.max_iter):
            # E-step:计算责任值(后验概率)
            resp = self._e_step(X)
            
            # M-step:更新参数
            self._m_step(X, resp)
            
            # 计算对数似然
            log_likelihood = self._log_likelihood(X)
            
            # 检查收敛
            if np.abs(log_likelihood - log_likelihood_prev) < self.tol:
                break
            
            log_likelihood_prev = log_likelihood
        
        self.converged_ = iteration < self.max_iter - 1
        self.n_iter_ = iteration + 1
        
        return self
    
    def _e_step(self, X):
        """
        E-step:计算Q函数(期望对数似然)
        返回责任值 resp[i,k] = p(z_i=k | x_i)
        """
        n_samples = X.shape[0]
        resp = np.zeros((n_samples, self.n_components))
        
        for k in range(self.n_components):
            resp[:, k] = self.weights_[k] * self._multivariate_gaussian(X, self.means_[k], 
                                                                        self.covariances_[k])
        
        # 归一化
        resp_sum = resp.sum(axis=1, keepdims=True)
        resp = resp / (resp_sum + 1e-10)
        
        return resp
    
    def _m_step(self, X, resp):
        """
        M-step:最大化Q函数更新参数
        """
        n_samples, n_features = X.shape
        
        # 计算有效点数
        Nk = resp.sum(axis=0)
        
        # 更新权重
        self.weights_ = Nk / n_samples
        
        # 更新均值
        self.means_ = (resp.T @ X) / Nk[:, np.newaxis]
        
        # 更新协方差(带Cholesky约束的正则化)
        for k in range(self.n_components):
            diff = X - self.means_[k]
            # 加权外积
            cov_k = (resp[:, k:k+1] * diff).T @ diff / Nk[k]
            # 5.2.3.2 数值稳定性:添加正则化项确保正定性
            self.covariances_[k] = cov_k + self.reg_covar * np.eye(n_features)
    
    def _multivariate_gaussian(self, X, mean, cov):
        """多元高斯概率密度"""
        n_features = len(mean)
        det = np.linalg.det(cov)
        inv = np.linalg.inv(cov)
        norm_const = 1.0 / (np.sqrt((2 * np.pi) ** n_features * det) + 1e-10)
        
        diff = X - mean
        exp_arg = -0.5 * np.sum(diff @ inv * diff, axis=1)
        
        return norm_const * np.exp(exp_arg)
    
    def _log_likelihood(self, X):
        """计算对数似然(用于BIC/AIC)"""
        n_samples = X.shape[0]
        log_likelihood = 0
        
        for i in range(n_samples):
            likelihood = 0
            for k in range(self.n_components):
                likelihood += self.weights_[k] * self._multivariate_gaussian(X[i:i+1], 
                                                                             self.means_[k], 
                                                                             self.covariances_[k])[0]
            log_likelihood += np.log(likelihood + 1e-10)
        
        return log_likelihood
    
    def predict(self, X):
        resp = self._e_step(X)
        return np.argmax(resp, axis=1)


def demo_hierarchical_density_clustering():
    """演示层次聚类、DBSCAN与GMM"""
    np.random.seed(42)
    
    # 生成多形状数据
    X1, _ = make_blobs(n_samples=300, centers=[[0, 0]], cluster_std=0.5)
    X2, _ = make_moons(n_samples=200, noise=0.1)
    X2 = X2 * 2 + [3, 3]  # 偏移
    X = np.vstack([X1, X2])
    
    fig, axes = plt.subplots(2, 2, figsize=(16, 12))
    
    # 1. 不同连接准则的层次聚类对比
    linkages = ['single', 'complete', 'average', 'ward']
    colors = ['viridis', 'plasma', 'coolwarm', 'terrain']
    
    for idx, (linkage_method, cmap) in enumerate(zip(linkages, colors)):
        if idx >= 2:
            ax = axes[1, idx-2]
        else:
            ax = axes[0, idx]
        
        agg = AgglomerativeClustering(n_clusters=3, linkage=linkage_method)
        agg.fit(X)
        
        scatter = ax.scatter(X[:, 0], X[:, 1], c=agg.labels_, cmap=cmap, s=30, alpha=0.6)
        ax.set_title(f'5.2.1 Linkage: {linkage_method.capitalize()}\n(Lance-Williams Formula)')
        ax.grid(True, alpha=0.3)
        
        # 如果是Ward,添加特别标注
        if linkage_method == 'ward':
            ax.annotate('Min Variance Merge', xy=(0.5, 0.95), xycoords='axes fraction',
                       ha='center', fontsize=10, bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.5))
    
    # 2. DBSCAN与OPTICS的密度可视化
    dbscan = DBSCAN(eps=0.4, min_samples=10)
    dbscan.fit(X)
    
    axes[1, 0].scatter(X[:, 0], X[:, 1], c=dbscan.labels_, cmap='tab10', s=30, alpha=0.6)
    # 标记核心点
    core_points = X[dbscan.is_core_]
    axes[1, 0].scatter(core_points[:, 0], core_points[:, 1], 
                      facecolors='none', edgecolors='red', s=100, linewidths=2,
                      label='Core Points (5.2.2.1)')
    axes[1, 0].set_title('DBSCAN: Core/Non-core/Noise\n(ε-neighborhood based)')
    axes[1, 0].legend()
    axes[1, 0].grid(True, alpha=0.3)
    
    # 3. OPTICS可达距离图
    optics = OPTICS(eps=0.5, min_samples=10)
    optics.fit(X)
    
    # 排序可达距离(简化版,实际应按生成顺序排序)
    sorted_reach = np.sort(optics.reachability_distances_[optics.reachability_distances_ < np.inf])
    
    axes[1, 1].plot(range(len(sorted_reach)), sorted_reach, 'b-', linewidth=2)
    axes[1, 1].axhline(y=0.4, color='r', linestyle='--', label='ε=0.4 (DBSCAN cutoff)')
    axes[1, 1].set_xlabel('Point Index (ordered by reachability)')
    axes[1, 1].set_ylabel('Reachability Distance')
    axes[1, 1].set_title('5.2.2.3 OPTICS Reachability Plot\n(Density Hierarchy Visualization)')
    axes[1, 1].legend()
    axes[1, 1].grid(True, alpha=0.3)
    
    # 4. GMM的BIC/AIC模型选择(额外添加)
    bic_scores = []
    aic_scores = []
    k_range = range(2, 8)
    
    for k in k_range:
        gmm = GaussianMixtureEM(n_components=k, max_iter=50)
        gmm.fit(X)
        log_lik = gmm._log_likelihood(X)
        n_params = k * (2 + 3)  # 简化参数计数
        
        # BIC = -2 * log_like + log(n) * n_params
        bic = -2 * log_lik + np.log(len(X)) * n_params
        aic = -2 * log_lik + 2 * n_params
        
        bic_scores.append(bic)
        aic_scores.append(aic)
    
    # 添加额外子图(使用inset或替换)
    # 这里简化为打印结果
    print("\n=== GMM模型选择 ===")
    print("K\tBIC\t\tAIC")
    for k, bic, aic in zip(k_range, bic_scores, aic_scores):
        print(f"{k}\t{bic:.1f}\t{aic:.1f}")
    print(f"最优K (BIC): {list(k_range)[np.argmin(bic_scores)]}")
    
    plt.tight_layout()
    plt.savefig('hierarchical_density_clustering.png', dpi=150, bbox_inches='tight')
    plt.show()


if __name__ == "__main__":
    demo_hierarchical_density_clustering()

5.3 线性降维与流形学习

5.3.1 主成分分析(PCA)

主成分分析通过正交投影将数据映射至低维子空间,方差最大化与重构误差最小化的等价性保证投影方向保留最大信息。协方差矩阵的特征分解求得特征向量与特征值,特征向量构成正交基,特征值量化各方向方差贡献。增量PCA通过按块处理数据更新主成分,避免存储整个协方差矩阵,适用于大规模数据集与在线学习。概率PCA将投影视为带高斯噪声的潜在变量模型,通过EM算法求解潜在子空间与噪声方差,支持缺失值的自然处理与概率推断。

5.3.2 线性判别分析(LDA)

线性判别分析监督地寻找最大化类间分离与最小化类内散度的投影方向。类内散度矩阵汇总各类协方差,类间散度矩阵度量类均值差异,广义特征值问题求解使判别准则最大化的特征向量。多类LDA的判别维度受类数减一限制,因类间散度矩阵的秩不超过类数减一。正则化LDA通过添加恒等矩阵倍数解决小样本问题,确保散度矩阵可逆。白化变换结合PCA与LDA,先通过PCA去相关与标准化,再执行LDA,消除特征相关性影响。

5.3.3 非线性降维与流形假设

核PCA通过核函数隐式映射至特征空间,在特征空间执行PCA,中心化核矩阵消除均值影响,特征函数展开获得非线性主成分。局部线性嵌入假设流形局部可由邻居线性重构,通过最小化重构误差求得权重矩阵,在低维空间保持权重关系,稀疏特征求解保留局部几何。T-SNE将高维相似度转换为对称联合概率分布,困惑度参数控制有效邻居数,通过最小化KL散度优化低维嵌入,对称SNE保证相似度矩阵对称性。UMAP构建高维数据的模糊单纯集表示,通过加权K-NN图近似流形结构,在低维空间最小化交叉熵损失,平衡局部与全局结构,计算效率优于T-SNE。

脚本13:降维与流形学习实现

Python

"""
脚本13:PCA、LDA、核PCA、LLE、t-SNE与UMAP实现
涉及内容:
- 5.3.1.1 PCA的方差最大化与重构误差等价性
- 5.3.1.2 增量PCA(IPCA)实现
- 5.3.1.3 概率PCA的EM算法
- 5.3.2.1 LDA的广义特征值问题
- 5.3.3.1 核PCA:中心化核矩阵与特征展开
- 5.3.3.2 LLE:重建权重矩阵求解
- 5.3.3.3 t-SNE的困惑度与KL散度优化
- 5.3.3.4 UMAP的模糊单纯集与交叉熵

使用方式:
python dimensionality_reduction_manifold.py
依赖:numpy, matplotlib, scipy, sklearn
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import eigh, solve
from scipy.spatial.distance import pdist, squareform, cdist
from sklearn.datasets import make_s_curve, make_swiss_roll, load_digits
from sklearn.preprocessing import StandardScaler
import warnings

class PCA:
    """
    5.3.1.1 主成分分析实现
    方差最大化与重构误差最小化的等价性
    """
    
    def __init__(self, n_components=2):
        self.n_components = n_components
        
    def fit(self, X):
        # 中心化
        self.mean_ = X.mean(axis=0)
        X_centered = X - self.mean_
        
        # 协方差矩阵特征分解
        cov_matrix = (X_centered.T @ X_centered) / len(X)
        eigenvalues, eigenvectors = eigh(cov_matrix)
        
        # 降序排列
        idx = np.argsort(eigenvalues)[::-1]
        self.explained_variance_ = eigenvalues[idx]
        self.components_ = eigenvectors[:, idx][:, :self.n_components]
        
        return self
    
    def transform(self, X):
        X_centered = X - self.mean_
        return X_centered @ self.components_
    
    def inverse_transform(self, X_reduced):
        """重构:展示重构误差最小化"""
        return X_reduced @ self.components_.T + self.mean_


class IncrementalPCA:
    """
    5.3.1.2 增量PCA实现
    适用于大规模数据,按块更新主成分
    """
    
    def __init__(self, n_components=2):
        self.n_components = n_components
        self.components_ = None
        self.mean_ = None
        self.n_samples_seen_ = 0
        
    def partial_fit(self, X):
        """分批拟合数据"""
        if self.mean_ is None:
            self.mean_ = np.zeros(X.shape[1])
        
        # 更新均值
        n_new = len(X)
        new_mean = X.mean(axis=0)
        self.mean_ = (self.n_samples_seen_ * self.mean_ + n_new * new_mean) / \
                     (self.n_samples_seen_ + n_new)
        
        # 中心化
        X_centered = X - self.mean_
        
        # 使用QR更新或简单累积(简化版)
        if self.components_ is None:
            # 第一次:SVD
            U, s, Vt = np.linalg.svd(X_centered, full_matrices=False)
            self.components_ = Vt[:self.n_components].T
        else:
            # 后续:近似更新(简化处理)
            pass  # 实际应使用更复杂的更新策略
        
        self.n_samples_seen_ += n_new
        
        return self


class ProbabilisticPCA:
    """
    5.3.1.3 概率PCA(PPCA)实现
    EM算法求解潜在变量模型
    """
    
    def __init__(self, n_components=2, max_iter=100):
        self.n_components = n_components
        self.max_iter = max_iter
        
    def fit(self, X):
        n_samples, n_features = X.shape
        
        # 初始化:标准PCA
        pca = PCA(n_components=self.n_components)
        pca.fit(X)
        
        self.W_ = pca.components_  # 投影矩阵
        self.mu_ = pca.mean_
        self.sigma2_ = 1.0  # 噪声方差初始值
        
        # EM算法
        for iteration in range(self.max_iter):
            # E-step:计算潜在变量后验
            # E[z|x] = (W^T W + sigma^2 I)^{-1} W^T (x - mu)
            M = self.W_.T @ self.W_ + self.sigma2_ * np.eye(self.n_components)
            Ez = solve(M, self.W_.T @ (X - self.mu_).T).T
            Ezz = self.sigma2_ * np.linalg.inv(M) + Ez[:, :, None] * Ez[:, None, :]
            
            # M-step:更新参数
            # W_new = (Σ(x-mu) E[z]^T) (Σ E[zz^T])^{-1}
            W_new = ((X - self.mu_).T @ Ez) @ np.linalg.inv(np.sum(Ezz, axis=0))
            
            # sigma2_new = (1/d) * (||x-mu||^2 - 2 E[z]^T W^T (x-mu) + Tr(E[zz^T] W^T W))
            diff = X - self.mu_
            sigma2_new = np.mean(np.sum(diff**2, axis=1) - 
                                2 * np.sum(Ez * (diff @ W_new), axis=1) +
                                np.trace(np.sum(Ezz, axis=0) @ W_new.T @ W_new))
            sigma2_new /= n_features
            
            # 检查收敛
            if np.linalg.norm(W_new - self.W_) < 1e-4:
                break
            
            self.W_ = W_new
            self.sigma2_ = max(sigma2_new, 1e-6)  # 防止负值
        
        return self
    
    def transform(self, X):
        M = self.W_.T @ self.W_ + self.sigma2_ * np.eye(self.n_components)
        return solve(M, self.W_.T @ (X - self.mu_).T).T


class LinearDiscriminantAnalysis:
    """
    5.3.2.1 线性判别分析(LDA)实现
    解决广义特征值问题:S_w^{-1} S_b w = lambda w
    """
    
    def __init__(self, n_components=None):
        self.n_components = n_components
        
    def fit(self, X, y):
        n_samples, n_features = X.shape
        classes = np.unique(y)
        n_classes = len(classes)
        
        if self.n_components is None:
            self.n_components = min(n_classes - 1, n_features)
        
        # 计算类均值与全局均值
        mean_total = X.mean(axis=0)
        means_class = [X[y == c].mean(axis=0) for c in classes]
        
        # 类内散度矩阵 S_w
        S_w = np.zeros((n_features, n_features))
        for c, mean_c in zip(classes, means_class):
            X_c = X[y == c]
            S_w += (X_c - mean_c).T @ (X_c - mean_c)
        
        # 类间散度矩阵 S_b
        S_b = np.zeros((n_features, n_features))
        for c, mean_c in zip(classes, means_class):
            n_c = np.sum(y == c)
            diff = (mean_c - mean_total).reshape(-1, 1)
            S_b += n_c * (diff @ diff.T)
        
        # 5.3.2.1 正则化:添加小值确保可逆
        S_w += 1e-6 * np.eye(n_features)
        
        # 广义特征值问题
        eigvals, eigvecs = eigh(np.linalg.inv(S_w) @ S_b)
        
        # 按特征值降序排列
        idx = np.argsort(eigvals)[::-1]
        self.explained_variance_ratio_ = eigvals[idx][:self.n_components] / np.sum(eigvals)
        self.scalings_ = eigvecs[:, idx][:, :self.n_components]
        
        return self
    
    def transform(self, X):
        return X @ self.scalings_


class KernelPCA:
    """
    5.3.3.1 核PCA实现
    在特征空间执行PCA,使用核技巧避免显式映射
    """
    
    def __init__(self, n_components=2, kernel='rbf', gamma=1.0):
        self.n_components = n_components
        self.kernel = kernel
        self.gamma = gamma
        
    def _compute_kernel(self, X, Y=None):
        if Y is None:
            Y = X
        
        if self.kernel == 'rbf':
            K = np.exp(-self.gamma * cdist(X, Y, 'sqeuclidean'))
        elif self.kernel == 'poly':
            K = (X @ Y.T + 1) ** 2
        else:
            K = X @ Y.T
            
        return K
    
    def fit(self, X):
        self.X_fit_ = X
        n_samples = len(X)
        
        # 计算核矩阵
        K = self._compute_kernel(X)
        
        # 中心化核矩阵(去除均值影响)
        one_n = np.ones((n_samples, n_samples)) / n_samples
        K_centered = K - one_n @ K - K @ one_n + one_n @ K @ one_n
        
        # 特征分解
        eigvals, eigvecs = eigh(K_centered)
        
        # 降序排列并归一化
        idx = np.argsort(eigvals)[::-1]
        self.eigenvalues_ = eigvals[idx][:self.n_components]
        self.eigenvectors_ = eigvecs[:, idx][:, :self.n_components]
        self.eigenvectors_ /= np.sqrt(self.eigenvalues_ + 1e-10)  # 归一化
        
        return self
    
    def transform(self, X):
        # 计算新数据与训练数据的核
        K = self._compute_kernel(X, self.X_fit_)
        
        # 中心化
        n_samples = len(self.X_fit_)
        one_n = np.ones((len(X), n_samples)) / n_samples
        K_centered = K - one_n @ self._compute_kernel(self.X_fit_) - \
                     K @ np.ones((n_samples, n_samples)) / n_samples + \
                     one_n @ self._compute_kernel(self.X_fit_) @ np.ones((n_samples, n_samples)) / n_samples
        
        return K_centered @ self.eigenvectors_


class LocallyLinearEmbedding:
    """
    5.3.3.2 局部线性嵌入(LLE)实现
    保持局部重建权重关系
    """
    
    def __init__(self, n_components=2, n_neighbors=10):
        self.n_components = n_components
        self.n_neighbors = n_neighbors
        
    def fit_transform(self, X):
        n_samples, n_features = X.shape
        
        # 步骤1:寻找K近邻
        from sklearn.neighbors import NearestNeighbors
        nn = NearestNeighbors(n_neighbors=self.n_neighbors + 1, metric='euclidean')
        nn.fit(X)
        distances, indices = nn.kneighbors(X)
        neighbors_idx = indices[:, 1:]  # 排除自身
        
        # 步骤2:计算重建权重W
        W = np.zeros((n_samples, n_samples))
        
        for i in range(n_samples):
            # 中心点及其邻居
            Xi = X[i]
            neighbors = X[neighbors_idx[i]]
            
            # 中心化邻居
            centered = neighbors - Xi
            
            # 局部协方差
            C = centered @ centered.T
            
            # 正则化确保可逆
            C += 1e-3 * np.eye(self.n_neighbors) * np.trace(C)
            
            # 求解权重
            weights = solve(C, np.ones(self.n_neighbors))
            weights /= np.sum(weights)  # 归一化
            
            W[i, neighbors_idx[i]] = weights
        
        # 步骤3:计算嵌入(特征值分解)
        # 矩阵 M = (I - W)^T (I - W)
        I_W = np.eye(n_samples) - W
        M = I_W.T @ I_W
        
        # 求解特征值(跳过最小的特征值0)
        eigvals, eigvecs = eigh(M)
        idx = np.argsort(eigvals)[1:self.n_components+1]  # 跳过0
        
        return eigvecs[:, idx]


class TSNE:
    """
    5.3.3.3 t-SNE实现
    困惑度与KL散度优化
    """
    
    def __init__(self, n_components=2, perplexity=30.0, max_iter=1000, lr=200):
        self.n_components = n_components
        self.perplexity = perplexity
        self.max_iter = max_iter
        self.lr = lr
        
    def _compute_joint_probabilities(self, X):
        """计算高维空间联合概率P(对称SNE)"""
        n_samples = len(X)
        distances = squareform(pdist(X, 'euclidean'))
        
        # 对每个点,寻找sigma使得条件分布的困惑度等于目标值
        P = np.zeros((n_samples, n_samples))
        
        for i in range(n_samples):
            # 二分搜索寻找合适的sigma
            beta = 1.0  # 初始1/sigma^2
            min_beta, max_beta = -np.inf, np.inf
            target_entropy = np.log(self.perplexity)
            
            for _ in range(50):
                # 计算条件概率
                Pi = np.exp(-distances[i] * beta)
                Pi[i] = 0  # 排除自身
                sum_Pi = np.sum(Pi)
                
                if sum_Pi == 0:
                    Pi = np.maximum(Pi, 1e-12)
                    sum_Pi = np.sum(Pi)
                
                Pi /= sum_Pi
                
                # 计算熵
                entropy = -np.sum(Pi * np.log(Pi + 1e-12))
                
                # 检查困惑度
                entropy_diff = entropy - target_entropy
                
                if np.abs(entropy_diff) < 1e-5:
                    break
                
                # 更新beta
                if entropy_diff > 0:
                    min_beta = beta
                    if max_beta == np.inf:
                        beta *= 2
                    else:
                        beta = (beta + max_beta) / 2
                else:
                    max_beta = beta
                    if min_beta == -np.inf:
                        beta /= 2
                    else:
                        beta = (beta + min_beta) / 2
            
            P[i] = Pi
        
        # 对称化:P = (P + P^T) / 2n
        P = (P + P.T) / (2 * n_samples)
        P = np.maximum(P, 1e-12)
        
        return P
    
    def fit_transform(self, X):
        n_samples = len(X)
        
        # 计算高维联合概率
        P = self._compute_joint_probabilities(X)
        
        # 初始化低维嵌入(PCA降维)
        pca = PCA(n_components=self.n_components)
        Y = pca.fit_transform(X)
        Y /= np.std(Y) * 1e-4  # 缩放
        
        # 梯度下降优化
        for iteration in range(self.max_iter):
            # 计算低维联合概率Q(t分布)
            distances = squareform(pdist(Y, 'euclidean'))
            Q = (1 + distances**2) ** -1
            np.fill_diagonal(Q, 0)
            Q /= np.sum(Q)
            Q = np.maximum(Q, 1e-12)
            
            # 计算梯度(KL散度)
            PQ_diff = P - Q
            grad = np.zeros_like(Y)
            
            for i in range(n_samples):
                # 梯度计算
                diff = Y[i] - Y
                grad[i] = 4 * np.sum((PQ_diff[i] * (1 + distances[i]**2)**-1)[:, None] * diff, axis=0)
            
            # 更新
            Y -= self.lr * grad
            
            # 学习率衰减
            if iteration == 250:
                self.lr /= 2
        
        return Y


class UMAP:
    """
    5.3.3.4 UMAP实现简化版
    模糊单纯集与交叉熵损失
    """
    
    def __init__(self, n_components=2, n_neighbors=15, min_dist=0.1, n_epochs=200):
        self.n_components = n_components
        self.n_neighbors = n_neighbors
        self.min_dist = min_dist
        self.n_epochs = n_epochs
        
    def fit_transform(self, X):
        n_samples = len(X)
        
        # 步骤1:构建模糊单纯集(高维)
        from sklearn.neighbors import NearestNeighbors
        nn = NearestNeighbors(n_neighbors=self.n_neighbors, metric='euclidean')
        nn.fit(X)
        distances, indices = nn.kneighbors(X)
        
        # 计算局部连接概率(模糊单纯集成员度)
        rho = distances[:, 1]  # 到最近邻的距离
        sigma = self._smooth_knn_dist(distances, self.n_neighbors, rho)
        
        # 构建高维概率(模糊单纯集)
        high_dim_probs = np.zeros((n_samples, n_samples))
        for i in range(n_samples):
            for j_idx, j in enumerate(indices[i]):
                if i == j:
                    continue
                d = distances[i, j_idx]
                high_dim_probs[i, j] = np.exp(-(d - rho[i]) / sigma[i])
        
        # 对称化
        high_dim_probs = high_dim_probs + high_dim_probs.T - high_dim_probs * high_dim_probs.T
        
        # 步骤2:优化低维嵌入(交叉熵)
        # 初始化
        pca = PCA(n_components=self.n_components)
        embedding = pca.fit_transform(X)
        embedding = (embedding - embedding.mean(axis=0)) / embedding.std(axis=0)
        embedding *= 0.0001
        
        # 简化优化(实际UMAP使用更复杂的采样)
        # 这里使用类似t-SNE的力导向布局简化
        for epoch in range(self.n_epochs):
            # 计算低维距离
            low_dists = squareform(pdist(embedding))
            low_dists = np.maximum(low_dists, self.min_dist)
            
            # 吸引与排斥力(交叉熵近似)
            # 简化版梯度
            for i in range(n_samples):
                for j_idx, j in enumerate(indices[i]):
                    if i == j:
                        continue
                    
                    # 吸引(正样本)
                    if high_dim_probs[i, j] > 0:
                        diff = embedding[i] - embedding[j]
                        dist = np.linalg.norm(diff) + 1e-6
                        attraction = high_dim_probs[i, j] * diff / dist * (1 - self.min_dist / dist)
                        embedding[i] -= 0.01 * attraction
                        embedding[j] += 0.01 * attraction
        
        return embedding
    
    def _smooth_knn_dist(self, distances, k, rho):
        """平滑KNN距离计算(UMAP特有)"""
        sigma = np.zeros(len(distances))
        for i in range(len(distances)):
            # 简化:使用固定sigma
            sigma[i] = np.mean(distances[i]) / k
        return sigma


def demo_dimensionality_reduction():
    """演示降维与流形学习"""
    from sklearn.datasets import make_swiss_roll
    
    np.random.seed(42)
    
    # 生成流形数据
    X_swiss, t = make_swiss_roll(n_samples=1000, noise=0.1, random_state=42)
    X_scurve, t_s = make_s_curve(n_samples=1000, noise=0.1, random_state=42)
    
    # 分类数据用于LDA
    from sklearn.datasets import make_classification
    X_clf, y_clf = make_classification(n_samples=500, n_features=10, n_informative=5, 
                                        n_redundant=0, n_classes=3, n_clusters_per_class=1,
                                        class_sep=2.0, random_state=42)
    
    fig, axes = plt.subplots(2, 3, figsize=(18, 12))
    
    # 1. PCA vs 核PCA(Swiss Roll)
    pca = PCA(n_components=2)
    X_pca = pca.fit_transform(X_swiss)
    
    kpca = KernelPCA(n_components=2, kernel='rbf', gamma=0.1)
    X_kpca = kpca.fit_transform(X_swiss)
    
    axes[0, 0].scatter(X_pca[:, 0], X_pca[:, 1], c=t, cmap='viridis', s=10)
    axes[0, 0].set_title('5.3.1 PCA (Linear)\nVariance Maximization')
    axes[0, 0].grid(True, alpha=0.3)
    
    axes[0, 1].scatter(X_kpca[:, 0], X_kpca[:, 1], c=t, cmap='viridis', s=10)
    axes[0, 1].set_title('5.3.3.1 Kernel PCA (RBF)\nNon-linear Unfolding')
    axes[0, 1].grid(True, alpha=0.3)
    
    # 2. LLE(S-Curve)
    lle = LocallyLinearEmbedding(n_components=2, n_neighbors=12)
    X_lle = lle.fit_transform(X_scurve)
    
    axes[0, 2].scatter(X_lle[:, 0], X_lle[:, 1], c=t_s, cmap='plasma', s=10)
    axes[0, 2].set_title('5.3.3.2 LLE (12-NN)\nLocal Linear Weights')
    axes[0, 2].grid(True, alpha=0.3)
    
    # 3. t-SNE与困惑度影响
    perplexities = [5, 30, 50]
    
    for idx, perp in enumerate(perplexities):
        ax = axes[1, idx]
        
        tsne = TSNE(n_components=2, perplexity=perp, max_iter=300)
        X_tsne = tsne.fit_transform(X_swiss[:300])  # 子集加速
        
        scatter = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], c=t[:300], cmap='viridis', s=20)
        ax.set_title(f'5.3.3.3 t-SNE\nPerplexity={perp}')
        ax.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('dimensionality_reduction_manifold.png', dpi=150, bbox_inches='tight')
    plt.show()
    
    # 4. LDA与PCA对比(分类数据)
    fig2, axes2 = plt.subplots(1, 2, figsize=(12, 5))
    
    pca_clf = PCA(n_components=2)
    X_pca_clf = pca_clf.fit_transform(X_clf)
    
    lda = LinearDiscriminantAnalysis(n_components=2)
    lda.fit(X_clf, y_clf)
    X_lda = lda.transform(X_clf)
    
    for ax, X_reduced, title in [(axes2[0], X_pca_clf, 'PCA (Unsupervised)'),
                                  (axes2[1], X_lda, '5.3.2.1 LDA (Supervised)')]:
        for c in np.unique(y_clf):
            mask = y_clf == c
            ax.scatter(X_reduced[mask, 0], X_reduced[mask, 1], label=f'Class {c}', s=30, alpha=0.6)
        ax.set_title(f'{title}\nDimensionality Reduction')
        ax.legend()
        ax.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('lda_vs_pca.png', dpi=150, bbox_inches='tight')
    plt.show()
    
    print("\n=== 降维分析完成 ===")
    print(f"PCA解释方差比: {pca.explained_variance_[:2] / np.sum(pca.explained_variance_)}")
    print(f"LDA解释方差比: {lda.explained_variance_ratio_}")


if __name__ == "__main__":
    demo_dimensionality_reduction()

以上脚本提供了第5章无监督学习核心内容的完整实现,涵盖从基础K-Means优化到现代流形学习方法(UMAP/t-SNE)的数值技术细节,可直接用于教学演示与算法原型开发。

机器学习——降维、聚类、分类、回归 前言: 最近做的任务与降维分类有关,于是乎就将机器学习中的“四大元老”——降维、聚类、分类、回归给弄混了。。。。。因此,用了很长时间去理解这“四大元老”的具体概念。并借此机会,趁热打铁得记录一下。 1.机器学习 什么是机器学习? 机器学习是一帮科学家想让计算机像人一样思考所研发出来的一套计算机理论。它包含了多种算法,涉及到概率论,数理统计、线性代数、矩阵论等多个学科。 机器学习已经深入到了我们生活... 阅读详情

相关推荐

常见的简单的无监督学习算法总结

  本文仅对常见的无监督学习算法进行了简单讲述,其他的如自动编码器,受限玻尔兹曼机用于无监督学习,神经网络用于无监督学习等未包括。同时虽然整体上分为了聚类和降维两大类,但实际上这两类并非完全正交,很多地方可以相互转化,还有一些变种的算法既有聚类功能又有降维功能,一些新出现的和尚在开发创造中的无监督学习算法正在打破聚类和降维的类别划分。另外因时间原因,可能有个别小错误,如有发现还望指出。 ...

weixin_30415801的博客 2万+

【机器学习】机器学习之聚类算法

​本次实验是一场聚类算法的深度探索之旅,涵盖了K-means、K-medoids、DBSCAN和凝聚聚类等引人注目的算法。K-means通过巧妙的迭代将样本点划分到K个簇,并通过聚类中心的不断更新优化结果。尽管简单高效,但对初始聚类中心选择敏感,K-means++通过独特方式改善了这一问题,显著提升了聚类效果。

朝花夕拾 2万+

机器学习算法分类-总览

机器学习算法类型 机器学习算法分类 1. 监督学习(Supervised Learning) 2.无监督学习(Unsupervised Learning) 3.强化学习(Reinforcement Learning) 4.神经网络与深度学习(Neural Networks and DeepLearning) 5.集成学习(Ensemble Learning) ...

u012472945的博客 7112

机器学习算法 06 —— 聚类算法(k-means、算法优化、特征降维、主成分分析PCA)

文章目录系列文章聚类算法1 聚类算法简介2 聚类算法API初步使用3 聚类算法实现流程4 聚类模型评估4.1 误差平方和(SSE)4.2 肘方法4.3 轮廓系数法4.4 CH系数4.5 小结5 算法优化5.1 Canopy算法5.2 K-means++5.3 二分K-means5.4 K-medoids(K-中心聚类算法)5.5 Kernel K-means、ISODATA、(了解)6 特征工程-特征降维6.1 什么是特征降维6.2 特征选择低方差特征过滤皮尔逊相关系数斯皮尔曼相关系数6.3 主成分分析(P

zcy的博客 2万+

机器学习算法实战系列:无监督学习全攻略——聚类与降维核心技术解析

给定数据集D={x₁,x₂,…,xₙ},将D划分为k个不相交的簇{C₁,C₂,…同一簇内样本相似度高不同簇间样本相似度低“无监督学习是探索未知数据的罗盘,掌握了这些技术,你就能在无标签数据的海洋中发现价值的岛屿!尝试在不同类型数据上应用这些算法结合领域知识解释发现的结构探索图聚类等高级技术在下一篇文章中,我们将深入自然语言处理领域,从词袋模型到Transformer,揭示文本智能处理的奥秘!敬请期待!延伸阅读聚类算法全面综述UMAP原始论文解析深度聚类最新进展。

资深全栈架构师,乐于在 优快云 分享技术见解,与大家携手共进,共攀技术巅峰! 1274

AI:机器学习之无监督学习

无监督学习(Unsupervised Learning)是机器学习的重要分支,它不依赖于人工标签,通过自身“感知”数据结构来发现潜在模式。

Kant2048的博客 1262

机器学习笔记6——无监督学习(聚类算法)

关键字:K-means算法,聚类中心,畸变函数一、定义与有监督学习相比,无监督学习的样本没有任何标记。无监督学习的算法需要自动找到这些没有标记的数据里面的数据结构和特征。这里介绍一下聚类算法。二、聚类算法2.1 定义把数据集分成一个个的簇cluster(也可以理解为一组一组的形式)2.2 K-means算法(K均值算法)(1)首先随机生成几个点,叫聚类中心(Cluster Centroids)。C...

温故而知新 3万+

机器学习—聚类和降维

无监督学习 — 聚类和降维 往期内容 1. 单变量线性回归 2. 多变量线性回归 3. 逻辑回归 4. 正则化 5. BP神经网络 6. 机器学习诊断法 聚类 k均值算法   K-均值是最普及的聚类算法,算法接受一个未标记的数据集,然后将数据聚类成不同的组。算法步骤总结如下:   ① 随机选择K个点作为聚类中心(cluster centroids)   ② 对于数据集中的每一个数据,按照距离????个中心点的距离,将其与距离最近的中心点关联起来,与同一个中心点关联的所有点聚成一类   ③ 计算每一组的平均值

橙子啵啵的blog 1347

吴恩达机器学习:聚类与降维 (K-means & PCA)总结与作业

(一)聚类:简介K均值算法( K-Means Algorithm ) 聚类算法将样本聚类分成不同的簇(cluster),其中K-means是最普及的一种聚类算法,它的思想其实就是迭代,假设我们想要将数据聚类成k个簇,其步骤为: 首先选择K个随机的点,称为聚类中心(cluster centroids);(如图a,b,以K=2为例) 对于数据集中的每个点,按照与聚类中心的距离,将其与距离最近的聚类中心点聚成一类;(如图c) 计算每一个组的平均值(重心位置),将该组所关联的中心点移动到平均值的位置;(

weixin_44330777的博客 6658

机器学习的分类与经典算法

监督学习是指在给定的训练集中“学习”出一个函数(模型参数),当新的数据到来时,可以根据这个函数预测结果。监督学习的训练集要求包括输入和输出,即特征值和目标值(标签),训练集中数据的目标值(标签)是由人工事先进行标注的。监督学习流程图如下图所示,其中包括准备数据、数据预处理、特征提取和特征选择、训练模型和评价模型。图1:监督学习流程图Step1:准备数据。监督学习首先要准备数据,没有现成的数据就需要采集数据或者爬取数据,或者从网站上下载数据。可以将准备好的数据集分为训练集、验证集和测试集。

RainTicking的博客 2453

图解机器学习算法 | 从入门到精通系列教程(机器学习通关指南·完结)

本篇内容是ShowMeAI组织的「图解机器学习算法」系列教程入口,本教程尽量以生动可视化的方式,帮助大家理解机器学习的核心知识和重要的系列模型,并配以相关的代码实现帮助大家了解应用方法。(对机器学习实战感兴趣的同学,可以关注ShowMeAI的另外一个系列[机器学习应用实践])......

ShowMeAI研究中心 1万+

一文彻底搞懂机器学习 - 聚类与降维(Clustering And Dimensionality Reduction)

*在机器学习中,是两个重要工具,它们能够帮助我们更好地理解和处理高维数据。**********聚类(Clustering)****是什么?********数据预处理:确保数据质量,通过清洗、转换、选择和划分,为聚类分析奠定坚实基础。选择合适的距离度量:根据数据特性,选用合适的距离度量方法衡量样本间相似性。选择聚类算法:依据数据特点和需求,挑选适合的聚类算法以揭示数据内在结构。初始化聚类中心:为特定聚类算法(如K-means)设定并随机初始化聚类中心。

xiaoganbuaiuk的博客 2431

【AI底层逻辑】——篇章5(下):机器学习算法之聚类&降维&时间序列

时间序列模型在解决实际问题时,序列必须满足特定的数据分布,或者具有平稳的时间序列特性,比如在剔除趋势数据后,时间序列不能与时间有依赖关系,数据波动的频率和幅度不能随时间变化等。——举例来说,随意挑选两个特征构建它们的散点图,如下图所示,图中每个点有两个特征,分别对应X轴和Y轴,在图中画一条直线,将所有点投影到这条线上就可以构建出一个新的特征(新的X轴)——它可以。,不存在普遍适用的最优算法。,如果变量之间存在关联,那么使用更少的综合变量来代替原变量,减少数据维度,理论上可行的——

weixin_51658186的博客 3519

【机器学习笔记】无监督学习(K-means聚类 & 降维PCA)

无监督学习(K-means聚类 & 降维PCA)(一)(1)(2) (一) (1) (2)

Liaojiajia-Blog 2984

macOS Jdk1.8安装(目前主流版本的jdk)

添加环境环境变量,类似于windows的高级设置中path配置的环境变量。查看 JAVA_HOME 目录。查看 JDK 版本信息。一直下一步安装就好了。在终端的根目录下运行。

m0_50207524的博客 9356

23. STM32F103C8T6 ADC采集光敏电阻传感器模块光照强度实战

本文详细介绍了如何使用STM32F103C8T6的ADC模块采集光敏电阻传感器的光照强度。通过硬件接线、核心原理讲解、四步代码配置(GPIO、ADC模式、使能校准、数据滤波)以及串口验证,手把手指导完成从模拟信号到数字值的完整实战流程,并重点提示了ADC时钟分频等关键配置要点。

weixin_42300144的博客 223

机器学习系统框架:核心分类、算法与应用全景解析

本文系统阐述了机器学习的基础框架体系,涵盖三大核心范式:监督学习(分类与回归算法)、无监督学习(聚类与降维)和强化学习(决策交互)。详细介绍了各类经典算法及其适用场景,包括线性模型、树模型、神经网络等监督算法,K-Means、PCA等无监督方法,以及Q-Learning等强化学习技术。同时提出了基于问题类型和数据特性的算法选择指南,并构建了完整的评估指标体系。最后通过金融风控和推荐系统案例,展示了端到端的机器学习应用框架。全文强调算法选择需平衡问题类型、数据特性和业务需求三个维度,为机器学习实践提供了系统化

mz159_357的博客 3567

别再死记硬背公式了!用Unity ShaderGraph从零实现一个皮肤SSS效果(附完整节点图)

本文详细介绍了如何利用Unity ShaderGraph零代码实现皮肤次表面散射(SSS)效果,包括背光透射(BTDF)和表面散射(BSSRDF)的核心原理与节点构建方法。通过可视化工具,技术美术师和Shader初学者可以直观理解光线在皮肤组织中的散射行为,快速创建真实的皮肤材质效果,适用于游戏角色渲染等场景。

weixin_30838873的博客 595

机器学习概念:监督学习、无监督学习、回归、聚类

分类是监督学习中的一种任务,其目标是将输入数据分配到预定义的类别或标签中。在分类问题中,每个样本都有一个标签,模型的任务是学习从输入特征到输出标签的映射关系。,如果我们想要预测房屋的销售价格,我们可以收集一些房屋的特征,比如面积、地理位置、房间数量等,然后利用这些特征来。这些算法在不同的数据集和应用场景下有不同的优劣势,选择合适的聚类算法通常依赖于数据集的特点和分析的目标。聚类的目标是发现数据内在的结构,从而找到数据中的潜在模式和规律。的训练数据,包括输入特征和相应的标签。输入特征与标签之间的映射关系。

AndrewPerfect的博客 3797

从Chiplet到UCIE:D2D互联技术如何重塑半导体设计生态

本文探讨了D2D互联技术在半导体设计中的革命性作用,从Chiplet到UCIE标准的演进如何重塑行业生态。详细分析了D2D互联的技术本质、架构选择及UCIE标准带来的变革,并展望了未来光学D2D和3D集成等趋势。高速互联接口技术的突破为AI加速器和计算芯片提供了更高带宽和能效。

weixin_29292535的博客 932

图像质量评估和点云质量评估

本文介绍了图像质量评估和点云质量评估的基本概念和常用方法,并提供了相应的源代码示例。图像质量评估关注图像的视觉质量,常用的评估指标包括均方误差(MSE)、结构相似性指标(SSIM)和峰值信噪比(PSNR)。点云质量评估关注点云数据的准确性和完整性,常用的评估方法包括点云配准质量评估、稠密重建质量评估和离群点检测质量评估。在计算机视觉和计算机图形学领域中,图像质量评估和点云质量评估是两个重要的任务。图像质量评估旨在量化图像的视觉质量,而点云质量评估则关注于点云数据的准确性和完整性。

XsupDatabase的博客 992

经典算法笔记:无监督算法(聚类、降维)

本文是吴恩达老师的机器学习课程[1]的笔记和代码复现部分(聚类、降维)。作者:黄海广[2]备注:笔记和作业(含数据、原始作业文件)、视频都在github[3]中下载。我将陆续将课程笔记和...

fengdu78的博客 2874

【ATC/DTC方法第四期】GOT09-dT-tao 昼夜温度周期模型方法介绍(含Python代码实现)

GOT09-dT-tao 模型是一种经过简化和参数固定的 昼夜温度周期模型(Diurnal Temperature Cycle, DTC)。它从 GOT09 模型(Göttsche and Olesen, 2009)衍生而来,但更加轻量化,更适合在数据不足或参数估计困难的情境下使用。

WW、forever的博客 147
上一篇: 【机器学习基础算法:从零实现到精通】第4章 监督学习:支持向量机与核方法
下一篇: 【机器学习基础算法:从零实现到精通】第6章 概率图模型与推断
VectorShift
博客等级 码龄1年 1839粉丝 1109原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VectorShift

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值