机器学习, 法→原理, 算法实现

随机搜索的大规模数据聚类CLARANS

钱魏Way · · 5 次浏览

CLARANS简介

CLARANSClustering Large Applications based on RANdomized Search,基于随机搜索的大规模应用聚类)是一种经典的聚类算法,由Raymond T. Ng和Jiawei Han于1994年提出。它旨在解决当时主流聚类算法(如K-means)在处理大规模数据集和发现任意形状簇方面的两大核心挑战。

本质上,CLARANS是 K-medoids(或PAM)算法 的一个高效、随机化的改进版本,特别适用于空间数据挖掘

核心思想与设计动机

CLARANS的核心思想是:将寻找最优聚类中心点(Medoids)的过程,视作在一个巨大的“邻居图”上进行一次受限制的、随机化的局部搜索,通过多次重启来逼近全局最优解。

为了更透彻地理解,我们将其拆解为以下几个层次:

根本动机:解决前辈算法的致命缺陷

CLARANS并非凭空诞生,它的设计直接针对当时两个主流划分算法 PAMCLARA 的痛点:

  • 对 PAM 的改进:
    • PAM(K-medoids)的问题: 采用贪心交换策略,质量高,能准确找到局部最优解。但其计算复杂度为 O(k(n-k)²),这意味着它对数据量 n极其敏感,几乎无法用于大规模数据集。
    • CLARANS的洞察:“我们真的需要检查每一个可能的交换吗?”答案是否定的。很多交换方向是低效或无效的。因此,CLARANS放弃穷举检查,改用随机抽样检查。
  • 对 CLARA 的改进:
    • CLARA 的问题:为了处理大数据,它对数据本身进行采样,然后对样本使用PAM。这带来了一个根本性问题:如果采样不能代表原始数据的整体结构(尤其是当簇的大小、密度不均时),那么在样本上找到的“最优”中心点,对于全体数据来说往往是次优的。
    • CLARANS的洞察:“与其对固定不变的数据进行采样,不如对‘解的空间’进行动态采样。”它始终在整个数据集上评估解的质量,但只在解的搜索空间中随机游走,从而避免了CLARA的“代表性偏差”。

核心思想一:将解空间构建为“邻居图

这是CLARANS最精妙的概念建模。

  • 什么是“节点”?每个节点代表一个可能的k-medoids集合,即一组k个实际数据点作为中心点的方案。这是一个候选解。
  • 什么是“边”?如果两个节点(两个k-medoids集合)可以通过交换一个中心点(例如,用集合外的某个点替换集合内的一个点)来相互转换,那么它们之间就有一条边相连。这两个节点互为“邻居”。
  • 这个图有什么特点?
    • 巨大无比: 节点数量是组合数 C(n, k),对于大规模数据,这是一个天文数字。
    • 每个节点的度(邻居数)很高: 每个节点有 k * (n-k)个邻居。

这个建模的意义在于: 寻找最佳聚类方案的问题,被转化成了在这个巨大图上寻找成本最低(即目标函数值最小)的节点的优化问题。

核心思想二:受限的随机化局部搜索

面对这个巨大且复杂的图,如何进行高效搜索?CLARANS采用了两种关键策略:

  • 随机化:
    • 起点随机: 每次搜索从一个随机节点(随机选择的k个中心点)开始。
    • 探索随机: 在当前节点,随机选择一个邻居进行考察,而不是按固定顺序检查。
    • 作用: 这赋予了算法跳出局部最优的能力,并通过多次重启增加找到更好解的概率。
  • 受限搜索(最核心的控制参数 maxNeighbor):
    • 关键问题: 在图的某个节点(山顶或山谷),如果盲目地一直随机尝试邻居,可能会浪费大量时间在“坏方向”上。
    • CLARANS的策略: 它为搜索设置了一个耐心限制 maxNeighbor。在当前节点,算法会连续随机尝试最多 maxNeighbor个邻居:
      • 如果发现一个更好(成本更低)的邻居,就立即“走”过去,并重置计数器,开始新的探索(这相当于下山)。
      • 如果连续尝试了 maxNeighbor个邻居都没有改善,算法就认为当前节点是一个局部最优解(已经身处一个“洼地”),并停止本次搜索。
    • 哲学: maxNeighbor平衡了 “探索深度” 和 “搜索效率”。值太小,容易在平缓区域过早放弃;值太大,则可能在不值得的坏节点上浪费过多时间。

核心思想三:多次重启,择优录用

  • 为什么要多次重启(参数 numLocal)?由于搜索是局部且随机的,单次搜索很可能终止于一个普通的局部最优解,而非全局最优解。
  • CLARANS的策略:它独立地执行 numLocal次上述的“随机化局部搜索”。每次从一个全新的随机起点开始,找到一个新的局部最优解。最后,从这 numLocal个局部最优解中,选出成本最低的那个作为最终答案。
  • 本质: 这是一种朴素而有效的集成思想。通过增加对解空间的采样次数,来提高捕获全局最优解或接近全局最优解的概率。

设计动机总结

CLARANS的设计动机,源于在 “聚类质量”、“计算效率” 和“对大数据及复杂形状的适应性” 三者之间寻求一个实用的工程平衡点。

它继承了PAM基于Medoid的鲁棒性和任意度量能力,摒弃了其穷举搜索的低效性。

它借鉴了CLARA的采样思想,但将其从数据采样升维到解空间采样,保证了搜索的全局视野。

最终,CLARANS通过“邻居图”的抽象、 “受限随机游走”的搜索机制、以及“多次重启择优”的集成策略,成功地将一个NP难组合优化问题,转化为一个可通过参数控制、在实践中高效可解的启发式算法。 这正是它经典且巧妙之处。

CLARANS的优缺点

优点

  • 对噪声和异常值鲁棒性强
    • 核心原因: 使用实际数据点作为簇中心点。一个异常值最多影响它所在的一个簇,而不会像K-means的“均值”那样将整个簇的中心拖偏。这使得聚类结果更稳定。
  • 不依赖于数据的几何形状或均值概念
    • 核心原因: 只需定义任意两个对象之间的相异性度量(如编辑距离、语义相似度、交通时间),无需在向量空间中计算均值。因此,它能处理K-means无法处理的复杂数据类型和任意形状的簇。
  • 搜索效率比PAM有质的提升
    • 核心原因: 通过引入 maxNeighbor参数,避免了PAM算法的穷举交换测试,将计算复杂度从 O(k(n-k)²)降低到可控水平,使其能够应用于大规模数据集。
  • 比CLARA的采样策略更可靠
    • 核心原因: CLARANS始终在整个数据集上评估解的质量,避免了CLARA因数据采样偏差导致的次优解。它的随机化是对解空间的采样,而非对数据的采样。
  • 原理清晰,易于理解和实现
    • 核心原因: 基于图搜索和局部优化的思想非常直观,代码实现比许多复杂模型(如深度聚类)更简单。

缺点

  • 计算复杂度依然较高
    • 核心原因: 虽然比PAM快,但每次评估成本都需要计算所有点到k个中心点的距离,其时间复杂度通常被认为是 O(n²) 级别。对于超大规模数据集(如千万级以上),运行时间仍可能不可接受。
  • 对参数 (maxNeighbor, numLocal) 敏感
    • 核心原因: 算法性能高度依赖这两个参数。maxNeighbor太小会导致搜索不充分,陷入较差局部最优;太大会显著增加计算量。numLocal同样需要权衡效果与时间。调参需要经验和实验。
  • 需要预先指定簇的数量 k
    • 核心原因: 与K-means一样,k值的选择是一个难题,需要领域知识或使用肘部法则、轮廓系数等方法辅助确定,增加了使用成本。
  • 可能无法找到全局最优解
    • 核心原因: 作为一种启发式随机搜索算法,它只能保证找到高质量的局部最优解,无法保证是全局最优。虽然多次重启提高了概率,但并非绝对。
  • 对初始随机种子敏感
    • 核心原因: 随机化的起点意味着多次运行的结果可能有细微差异,这在需要完全确定性的场景中是一个缺点。

CLARANS的典型应用场景

CLARANS特别适用于同时满足以下 “三重特性” 的问题:

  • 数据存在噪声或异常值。
  • 数据间的“距离”不能用简单的欧氏距离衡量,或簇的形状复杂。
  • 数据规模处于“中等至大型”,而非“超大规模”。

具体领域示例:

  • 空间数据挖掘与地理信息系统
    • 场景: 根据地理位置对商店、加油站、故障设备进行聚类,以规划物流或部署维护团队。
    • 为何适用: 使用实际坐标点(经纬度)作为中心点很合理;距离通常采用球面距离(如Haversine公式),而非欧氏距离;城市中的店铺分布可能形成不规则形状的簇。
  • 生物信息学与计算生物学
    • 场景: 基于基因序列的相似性(如编辑距离、BLAST得分)对基因或蛋白质进行聚类,以发现功能相似的家族。
    • 为何适用: 序列数据无法计算均值;相似性度量复杂;数据可能存在测序错误(噪声)。
  • 图像分析与计算机视觉
    • 场景: 在颜色、纹理等特征空间中对图像区域进行分割。
    • 为何适用: 可以使用各种特征距离度量;图像中物体区域可能是不规则形状;对分割边界的噪声点不敏感。
  • 社交网络与图数据分析
    • 场景: 基于节点间的结构距离(如最短路径长度、Jaccard系数)发现社区结构。
    • 为何适用: 图数据无法定义均值;社区结构复杂;离群节点(与所有社区联系都弱)应被妥善处理。
  • 市场研究与客户细分
    • 场景: 基于多维度行为数据(购买记录、浏览时长等)对客户进行分群,其中使用自定义的混合距离函数。
    • 为何适用: 客户行为数据常包含异常值(如“羊毛党”);细分群体可能具有非球形的复杂特征分布。

总结与选型建议

CLARANS是“稳健性”和“通用性”的折衷之选。

  • 当你需要比K-means更鲁棒、更通用的聚类能力,且数据量尚未达到PB级别时,CLARANS是一个强有力的候选。
  • 当计算资源极其有限,或数据规模达到海量级别时,应考虑更现代、更高效的算法,如:
    • 追求速度与规模:改进的K-means变种(如Mini-Batch K-means)、或基于密度的算法如 HDBSCAN(自带噪声识别,无需指定k)。
    • 追求任意形状和自动确定簇数:DBSCAN 或 HDBSCAN。
    • 超大规模数据:需要依赖于分布式计算框架(如Spark MLlib)中的聚类实现。

核心定位: CLARANS在算法演进史上,是一座连接经典精确算法与现代高效启发式算法的桥梁。它解决了其前辈(PAM, CLARA)的关键痛点,为2000年代前后的空间数据挖掘提供了可靠的解决方案。在今天,它依然是一个在特定约束下(中等数据、需鲁棒性、复杂度量)值得考虑的经典工具。

CLARANS算法实现

CLARANS算法详细步骤

算法输入参数

  • k:要聚类的簇数量
  • numlocal:尝试的局部最优解数量
  • maxneighbor:每个节点探索的邻居数量上限
  • data:包含n个数据点的数据集
  • distance_func:计算两点间距离的函数

步骤1:初始化

  • 设置 i = 1(当前局部搜索次数)
  • 初始化 best_solution = None和 best_cost = float(‘inf’)

步骤2:外循环 – 多次局部搜索

对于 i从 1 到 numlocal执行:

  • 随机选择初始中心点:从n个数据点中随机选择k个点作为初始中心点集合,称为 current_node
  • 计算当前代价:current_cost = 计算所有点到最近中心点的总距离
  • 设置邻居计数器:j = 1

步骤3:内循环 – 局部优化

当 j <= maxneighbor时执行:

  • 生成随机邻居:
    • 随机选择一个非中心点 P_random
    • 随机选择一个当前中心点 P_medoid
    • 用 P_random替换 P_medoid得到新中心点集合 new_node
  • 计算新代价:new_cost = 使用new_node时所有点到最近中心点的总距离
  • 判断与移动:
    • 如果 new_cost < current_cost:
      • 接受移动:current_node = new_node, current_cost = new_cost
      • 重置计数器:j = 1
    • 否则:
      • 拒绝移动:j = j + 1

步骤4:记录局部最优解

  • 当内循环结束后,current_node是局部最优解
  • 如果 current_cost < best_cost,则更新:best_cost = current_cost, best_solution = current_node

步骤5:最终分配

  • 使用最终的中心点集合 best_solution
  • 将每个数据点分配到距离最近的中心点所在的簇

Python实现代码

import numpy as np
import random
from sklearn.datasets import make_blobs
from sklearn.metrics import pairwise_distances
import matplotlib.pyplot as plt
from typing import List, Tuple, Callable, Any
import time

class CLARANS:
    """
    CLARANS聚类算法实现
    
    参数:
    -----------
    n_clusters : int, default=3
        聚类数量
        
    numlocal : int, default=5
        局部最优解尝试次数
        
    maxneighbor : int or float, default=0.025
        每个节点探索的邻居数量上限
        如果是浮点数,表示总可能邻居数的百分比
        
    distance_metric : str, default='euclidean'
        距离度量,可选: 'euclidean', 'manhattan', 'cosine'
        
    random_state : int, default=42
        随机种子
    """
    
    def __init__(self, n_clusters=3, numlocal=5, maxneighbor=0.025, 
                 distance_metric='euclidean', random_state=42):
        self.n_clusters = n_clusters
        self.numlocal = numlocal
        self.maxneighbor = maxneighbor
        self.distance_metric = distance_metric
        self.random_state = random_state
        self.medoids = None
        self.labels_ = None
        self.cost_ = None
        
        np.random.seed(random_state)
        random.seed(random_state)
    
    def _compute_distance_matrix(self, X: np.ndarray) -> np.ndarray:
        """计算距离矩阵"""
        return pairwise_distances(X, metric=self.distance_metric)
    
    def _compute_cost(self, distance_matrix: np.ndarray, medoids: List[int]) -> float:
        """计算给定中心点集合的代价(总距离和)"""
        # 选择每个点到最近中心点的距离
        medoid_distances = distance_matrix[:, medoids]
        min_distances = np.min(medoid_distances, axis=1)
        return np.sum(min_distances)
    
    def _get_random_neighbor(self, medoids: List[int], n_points: int) -> Tuple[List[int], int, int]:
        """随机生成一个邻居:替换一个中心点"""
        # 深拷贝当前中心点列表
        new_medoids = medoids.copy()
        
        # 当前非中心点的索引
        non_medoids = [i for i in range(n_points) if i not in medoids]
        
        if not non_medoids:
            return medoids, -1, -1
        
        # 随机选择一个非中心点和一个中心点进行替换
        random_non_medoid = random.choice(non_medoids)
        random_medoid = random.choice(range(len(medoids)))
        
        # 执行替换
        replaced_medoid = new_medoids[random_medoid]
        new_medoids[random_medoid] = random_non_medoid
        
        return new_medoids, replaced_medoid, random_non_medoid
    
    def _find_closest_medoid(self, distance_matrix: np.ndarray, medoids: List[int]) -> np.ndarray:
        """为每个点找到最近的中心点索引"""
        medoid_distances = distance_matrix[:, medoids]
        closest_medoid_indices = np.argmin(medoid_distances, axis=1)
        return closest_medoid_indices
    
    def _assign_clusters(self, distance_matrix: np.ndarray, medoids: List[int]) -> np.ndarray:
        """分配每个点到最近的中心点对应的簇"""
        closest_medoid_indices = self._find_closest_medoid(distance_matrix, medoids)
        
        # 将中心点索引映射到簇标签
        cluster_labels = np.zeros(distance_matrix.shape[0], dtype=int)
        for i, medoid_idx in enumerate(medoids):
            # 找到以这个中心点为最近中心点的所有点
            mask = closest_medoid_indices == i
            cluster_labels[mask] = i
            
        return cluster_labels
    
    def fit(self, X: np.ndarray) -> 'CLARANS':
        """
        在数据X上拟合CLARANS模型
        
        参数:
        ----------
        X : array-like, shape (n_samples, n_features)
            输入数据
            
        返回:
        -------
        self : CLARANS
            拟合后的模型
        """
        n_samples = X.shape[0]
        
        # 计算距离矩阵
        distance_matrix = self._compute_distance_matrix(X)
        
        # 如果maxneighbor是浮点数,则计算为百分比
        if isinstance(self.maxneighbor, float):
            maxneighbor = int(self.maxneighbor * self.n_clusters * (n_samples - self.n_clusters))
        else:
            maxneighbor = self.maxneighbor
        
        # 确保maxneighbor至少为1
        maxneighbor = max(1, maxneighbor)
        
        best_cost = float('inf')
        best_medoids = None
        
        # 记录每次迭代的成本用于可视化
        self.cost_history_ = []
        
        # 外循环:尝试多个局部最优解
        for local_idx in range(self.numlocal):
            # 步骤A: 随机选择初始中心点
            all_indices = list(range(n_samples))
            current_medoids = random.sample(all_indices, self.n_clusters)
            
            # 计算当前代价
            current_cost = self._compute_cost(distance_matrix, current_medoids)
            
            # 内循环:局部搜索
            neighbor_count = 0
            
            while neighbor_count < maxneighbor:
                # 步骤B: 生成随机邻居
                new_medoids, old_medoid, new_medoid = self._get_random_neighbor(
                    current_medoids, n_samples
                )
                
                # 步骤C: 计算新代价
                new_cost = self._compute_cost(distance_matrix, new_medoids)
                
                # 步骤D: 判断与移动
                if new_cost < current_cost:
                    # 接受移动
                    current_medoids = new_medoids
                    current_cost = new_cost
                    neighbor_count = 0  # 重置计数器
                else:
                    neighbor_count += 1
                
                # 记录成本历史
                self.cost_history_.append(current_cost)
            
            # 记录当前局部最优解
            if current_cost < best_cost:
                best_cost = current_cost
                best_medoids = current_medoids.copy()
            
            print(f"局部搜索 {local_idx+1}/{self.numlocal} 完成,当前最佳成本: {best_cost:.4f}")
        
        # 保存结果
        self.medoids = best_medoids
        self.cost_ = best_cost
        self.labels_ = self._assign_clusters(distance_matrix, best_medoids)
        self.cluster_centers_ = X[best_medoids]
        
        return self
    
    def predict(self, X: np.ndarray) -> np.ndarray:
        """
        预测X中每个样本的簇标签
        
        参数:
        ----------
        X : array-like, shape (n_samples, n_features)
            要预测的数据
            
        返回:
        -------
        labels : array, shape (n_samples,)
            每个样本的簇标签
        """
        if self.medoids is None:
            raise ValueError("模型尚未拟合,请先调用fit方法")
        
        distance_matrix = self._compute_distance_matrix(X)
        
        # 计算每个点到各个中心点的距离
        medoid_distances = distance_matrix[:, self.medoids]
        
        # 为每个点找到最近的中心点
        closest_medoid_indices = np.argmin(medoid_distances, axis=1)
        
        return closest_medoid_indices
    
    def fit_predict(self, X: np.ndarray) -> np.ndarray:
        """拟合模型并返回簇标签"""
        self.fit(X)
        return self.labels_


# 三、使用示例和可视化

def generate_sample_data(n_samples=300, centers=4, random_state=42):
    """生成示例数据"""
    X, y_true = make_blobs(
        n_samples=n_samples, 
        centers=centers, 
        cluster_std=0.7,
        random_state=random_state
    )
    return X, y_true

def plot_clustering_results(X, y_pred, medoids, title="CLARANS聚类结果"):
    """可视化聚类结果"""
    plt.figure(figsize=(12, 5))
    
    # 绘制原始数据
    plt.subplot(1, 2, 1)
    plt.scatter(X[:, 0], X[:, 1], c='gray', alpha=0.6, s=30)
    plt.title("原始数据")
    plt.xlabel("特征1")
    plt.ylabel("特征2")
    
    # 绘制聚类结果
    plt.subplot(1, 2, 2)
    scatter = plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap='tab20', s=30, alpha=0.7)
    
    # 标记中心点
    plt.scatter(
        X[medoids, 0], X[medoids, 1], 
        c='red', marker='X', s=200, 
        edgecolors='black', linewidth=2,
        label='中心点'
    )
    
    plt.title(title)
    plt.xlabel("特征1")
    plt.ylabel("特征2")
    plt.legend()
    plt.colorbar(scatter, label='簇标签')
    plt.tight_layout()
    plt.show()

def plot_cost_history(cost_history, title="CLARANS代价变化历史"):
    """可视化代价变化历史"""
    plt.figure(figsize=(10, 5))
    plt.plot(cost_history, linewidth=1)
    plt.title(title)
    plt.xlabel("迭代次数")
    plt.ylabel("总代价(距离和)")
    plt.grid(True, alpha=0.3)
    plt.show()

def evaluate_clustering(X, y_true, y_pred):
    """评估聚类结果"""
    from sklearn.metrics import adjusted_rand_score, silhouette_score
    
    ari = adjusted_rand_score(y_true, y_pred)
    silhouette = silhouette_score(X, y_pred)
    
    print(f"调整兰德指数 (ARI): {ari:.4f}")
    print(f"轮廓系数: {silhouette:.4f}")
    
    return ari, silhouette

# 四、完整示例

def main():
    """CLARANS算法完整示例"""
    
    # 1. 生成示例数据
    print("生成示例数据...")
    X, y_true = generate_sample_data(n_samples=300, centers=4)
    print(f"数据形状: {X.shape}")
    
    # 2. 创建并训练CLARANS模型
    print("\n训练CLARANS模型...")
    start_time = time.time()
    
    # 创建模型
    clarans = CLARANS(
        n_clusters=4,        # 簇数量
        numlocal=5,          # 尝试5个不同的局部最优解
        maxneighbor=0.025,   # 探索每个节点2.5%的邻居
        distance_metric='euclidean',
        random_state=42
    )
    
    # 训练模型
    y_pred = clarans.fit_predict(X)
    
    end_time = time.time()
    print(f"训练时间: {end_time - start_time:.2f}秒")
    print(f"最终代价: {clarans.cost_:.4f}")
    print(f"中心点索引: {clarans.medoids}")
    
    # 3. 可视化结果
    plot_clustering_results(X, y_pred, clarans.medoids, "CLARANS聚类结果")
    plot_cost_history(clarans.cost_history_, "代价变化历史")
    
    # 4. 评估聚类结果
    print("\n聚类评估:")
    evaluate_clustering(X, y_true, y_pred)
    
    # 5. 对比K-means
    print("\n与K-means对比:")
    from sklearn.cluster import KMeans
    kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
    y_kmeans = kmeans.fit_predict(X)
    
    # 可视化K-means结果
    plt.figure(figsize=(6, 5))
    plt.scatter(X[:, 0], X[:, 1], c=y_kmeans, cmap='tab20', s=30, alpha=0.7)
    plt.scatter(
        kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
        c='red', marker='X', s=200, edgecolors='black', linewidth=2,
        label='K-means中心'
    )
    plt.title("K-means聚类结果")
    plt.xlabel("特征1")
    plt.ylabel("特征2")
    plt.legend()
    plt.colorbar(label='簇标签')
    plt.tight_layout()
    plt.show()
    
    # 评估K-means
    evaluate_clustering(X, y_true, y_kmeans)

# 五、优化版本:增量计算代价
class OptimizedCLARANS(CLARANS):
    """优化版CLARANS,使用增量计算提高效率"""
    
    def _compute_cost_incremental(self, distance_matrix: np.ndarray, 
                                 medoids: List[int], 
                                 old_medoid: int, 
                                 new_medoid: int) -> float:
        """
        增量计算替换中心点后的代价变化
        这比重新计算整个数据集的代价要快得多
        """
        # 获取当前中心点到所有点的距离
        medoid_distances = distance_matrix[:, medoids]
        current_min_distances = np.min(medoid_distances, axis=1)
        
        # 计算新中心点的影响
        new_distances = distance_matrix[:, new_medoid]
        
        # 找到受影响的点(新距离比当前最小距离小的点)
        affected_mask = new_distances < current_min_distances
        
        if not np.any(affected_mask):
            return np.sum(current_min_distances)
        
        # 计算新代价
        new_min_distances = current_min_distances.copy()
        new_min_distances[affected_mask] = new_distances[affected_mask]
        
        return np.sum(new_min_distances)
    
    def fit(self, X: np.ndarray) -> 'OptimizedCLARANS':
        """优化的fit方法"""
        n_samples = X.shape[0]
        
        # 计算距离矩阵
        distance_matrix = self._compute_distance_matrix(X)
        
        # 设置maxneighbor
        if isinstance(self.maxneighbor, float):
            maxneighbor = int(self.maxneighbor * self.n_clusters * (n_samples - self.n_clusters))
        else:
            maxneighbor = self.maxneighbor
        
        maxneighbor = max(1, maxneighbor)
        
        best_cost = float('inf')
        best_medoids = None
        
        self.cost_history_ = []
        
        # 外循环
        for local_idx in range(self.numlocal):
            # 随机选择初始中心点
            all_indices = list(range(n_samples))
            current_medoids = random.sample(all_indices, self.n_clusters)
            current_cost = self._compute_cost(distance_matrix, current_medoids)
            
            neighbor_count = 0
            
            # 内循环
            while neighbor_count < maxneighbor:
                # 生成随机邻居
                new_medoids, old_medoid_idx, new_medoid = self._get_random_neighbor(
                    current_medoids, n_samples
                )
                
                if old_medoid_idx == -1:  # 没有有效的邻居
                    break
                
                # 增量计算新代价
                new_cost = self._compute_cost_incremental(
                    distance_matrix, new_medoids, 
                    current_medoids[old_medoid_idx], new_medoid
                )
                
                # 判断是否接受移动
                if new_cost < current_cost:
                    current_medoids = new_medoids
                    current_cost = new_cost
                    neighbor_count = 0
                else:
                    neighbor_count += 1
                
                self.cost_history_.append(current_cost)
            
            # 更新最佳解
            if current_cost < best_cost:
                best_cost = current_cost
                best_medoids = current_medoids.copy()
            
            print(f"局部搜索 {local_idx+1}/{self.numlocal} 完成,当前最佳成本: {best_cost:.4f}")
        
        # 保存结果
        self.medoids = best_medoids
        self.cost_ = best_cost
        self.labels_ = self._assign_clusters(distance_matrix, best_medoids)
        self.cluster_centers_ = X[best_medoids]
        
        return self


# 六、性能对比
def compare_performance():
    """对比原始版本和优化版本的性能"""
    print("性能对比测试...")
    X, _ = generate_sample_data(n_samples=1000, centers=5)
    
    # 测试原始版本
    print("\n1. 原始CLARANS:")
    start_time = time.time()
    clarans_original = CLARANS(n_clusters=5, numlocal=3, maxneighbor=0.01)
    clarans_original.fit(X)
    original_time = time.time() - start_time
    print(f"   时间: {original_time:.2f}秒")
    print(f"   最终代价: {clarans_original.cost_:.4f}")
    
    # 测试优化版本
    print("\n2. 优化版CLARANS:")
    start_time = time.time()
    clarans_optimized = OptimizedCLARANS(n_clusters=5, numlocal=3, maxneighbor=0.01)
    clarans_optimized.fit(X)
    optimized_time = time.time() - start_time
    print(f"   时间: {optimized_time:.2f}秒")
    print(f"   最终代价: {clarans_optimized.cost_:.4f}")
    
    print(f"\n优化版本加速: {original_time/optimized_time:.2f}倍")
    
    # 可视化优化版本的结果
    plot_clustering_results(
        X, clarans_optimized.labels_, clarans_optimized.medoids,
        "优化版CLARANS聚类结果"
    )


# 执行主程序
if __name__ == "__main__":
    print("=" * 50)
    print("CLARANS算法实现与示例")
    print("=" * 50)
    
    # 运行主示例
    main()
    
    # 运行性能对比
    compare_performance()
    
    print("\n" + "=" * 50)
    print("程序执行完毕")
    print("=" * 50)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注