K-means算法改进(Python版)

系列文章目录

文章目录

文章摘要:本文针对传统 K-Means 聚类算法对初始聚类中心敏感、易陷入局部最优的问题,系统介绍了两种改进方法。方法一基于密度权值,通过计算全局平均距离、局部密度、特征权重与密度权值,选取位于数据密集区且相互远离的点作为初始质心;方法二利用 DBSCAN 预筛选高密度核心点,以各簇核心点均值作为 K-Means 初始质心,规避离群点干扰。两种方法均附有完整 Python 实现与可视化对比,可有效提升聚类稳定性与效果。

一、K-means算法流程

输入

样本数据集,预先指定聚类数量 k,最大迭代次数、收敛阈值。

步骤:

(1)初始化质心:从数据集里挑选 k 个样本,作为初始聚类中心。

(2)样本分配:遍历每一个样本点,计算该点到 k 个质心的欧氏距离;把样本划分给距离最近 的那个质心,形成 k 个簇。

(3)更新质心(M 步):对每个簇,计算簇内所有样本的平均值,均值作为新的聚类中心。

(4)判断是否收敛:收敛条件①:新旧质心位置几乎不变(变化小于阈值),停止迭代。收敛条件②:达到设定最大迭代次数,强制停止。

输出

每个样本所属类别标签 + k 个最终聚类中心。

python 复制代码
输入:数据集X,k,max_iter
1. 随机选取k个样本作为初始质心centroids
for i in range(max_iter):
    簇集合C = 空
    # 分配样本
    for x in X:
        计算x到所有centroids距离
        把x划入最近质心对应的簇
    # 更新质心
    new_centroids = 每个簇样本求均值
    if new_centroids 和 centroids几乎一样:
        break
    centroids = new_centroids
输出:簇划分C、质心centroids

关键优缺点

✅ 优点:简单、计算快,适合大数据

❌ 缺点:

• 需要提前确定 k 值

• 对初始中心点敏感,容易得到局部最优

• 对异常值、非球形簇效果差

• 必须用连续数值特征,需要先标准化

python 复制代码
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

# 生成测试数据
X = np.random.randn(200,2)

# 构建kmeans模型,k=3
kmeans = KMeans(n_clusters=3, max_iter=300)
kmeans.fit(X)

labels = kmeans.labels_         # 每个样本类别

centers = kmeans.cluster_centers_# 聚类中心


# 绘图
plt.scatter(X[:,0], X[:,1], c=labels, s=20)
plt.scatter(centers[:,0], centers[:,1], c='red', marker='*', s=200)
plt.show()

二、改进方法1:基于密度权值对初始聚类中心的改进

 在 K-Means 聚类算法中,初始聚类中心的选择对聚类效果有着至关重要的影响。传统 K-Means 算法通过计算每个数据点到聚类中心的距离来划分数据点的归属。然而,若初始聚类中心恰好选中噪声点或离群点,将导致聚类过程中计算出的中心点发生偏移,进而影响后续的聚类结果。这种偏移可能导致聚类算法提前收敛于一个不理想的局部最优解,无法有效捕捉数据的实际分布特征,尤其在处理含有异常数据或复杂模式的数据集时,这一问题更为突出。

 为解决上述问题,本文提出了一种基于密度权值的初始聚类中心选择策略,以 改进传统 K-Means 算法的聚类效果。该方法通过计算数据点的密度值来确定初始聚类中心,从而避免了随机选取初始聚类中心所带来的误差。具体而言,密度值的计算基于每个数据点与其余数据点的距离关系,通过计算每个点到其他点的平均距离来衡量其在数据集中的密度。密度值较大的数据点通常位于数据集的核心区域,代表了较为密集的分布区域。选择这些密度较大的点作为初始聚类中心,能够确保聚类过程更加稳健,减少离群点或噪声点对聚类效果的负面影响。该策略的核心在于通过多维空间密度评估与加权距离度量,实现对数据特征的精准刻画。

 内部方法1:计算全局平均距离 AVDis:

式子中:n为样本数,为样本i与样本j之间的距离。两两测距得到nn的距离矩阵;(因为(i, j) 和 (j, i) 是同一个对,所以距离有n(n-1)个.

 内部方法2:计算每个样本的局部密度 ρ_i。

局部密度为为以该样本点为中心、平均距离AVDis为半径超球体内包含的样本数(需减去该点自己)。

内部方法3:计算特征权重 w_h。

(如假如你的数据是24维度,如光伏,在中午时刻大,早晚小,晚上为0。如果直接算欧氏距离,数值大的时刻会主导距离计算,数值小的时刻几乎没影响。这显然不合理------每个时刻都同样重要。给每个时刻(每个特征维度)一个权重 w_h,让数值大的维度不要"压死"数值小的维度。)

式子中:n为样本数;m为特征数;xih第 i 个样本、第 h 个特征的值;wh第 h 个特征的权重。分子:第 h 个特征所有样本的平方和;分母:所有样本、所有特征平方和的总平均。如果第 h 个特征的平方和大于总平均 → wh>1(权重大,重要)。如果第 h 个特征的平方和小于总平均 → wh<1(权重小,不重要)。如果等于总平均 → wh=1(中性)

内部方法4:计算密度权值 w_i 详解。

为什么需要"密度权值"?

问题背景

我们想从1000个样本里,挑出 K=5 个"最像聚类中心"的点作为初始中心。

什么样的点适合当聚类中心?

理想情况下,一个好的聚类中心应该:

(1)位于数据密集区(周围邻居多,代表性强)

(2)离其他中心远(不和别的中心挤在一起)

评价指标:簇内平方和

python 复制代码
import numpy as np
from scipy.spatial.distance import cdist


class ImprovedKMeans:
    """
    基于密度权值的改进K-means聚类器

    使用示例:
        model = ImprovedKMeans(n_clusters=3)
        labels, centers = model.fit(X)
    """

    def __init__(self, n_clusters=3, max_iter=300, tol=1e-6, verbose=False):
        """
        构造函数

        参数:
            n_clusters : int, 聚类数K
            max_iter   : int, 最大迭代次数
            tol        : float, 收敛阈值(中心移动小于此值则停止)
            verbose    : bool, 是否打印中间过程
        """
        self.n_clusters = n_clusters          # 聚类数
        self.max_iter = max_iter              # 最大迭代次数
        self.tol = tol                        # 收敛阈值
        self.verbose = verbose                # 是否打印日志

        # 以下属性在fit后赋值
        self.labels_ = None                   # 聚类标签
        self.cluster_centers_ = None          # 聚类中心
        self.inertia_ = None                  # 簇内平方和(越小越好)

    # ============================================================
    # 内部方法1:计算全局平均距离 AVDis
    # ============================================================
    def _compute_avg_distance(self, dist_matrix):
        """
        计算全局平均距离

        公式:AVDis(D) = 2/(n(n-1)) * ΣΣ d_ij

        参数:
            dist_matrix : (n, n) 距离矩阵
        返回:
            AVDis : float 全局平均距离
        """
        n = dist_matrix.shape[0]              # 样本数
        # 所有元素求和(对角线为0,不影响结果)
        total = np.sum(dist_matrix)
        # 除以 n*(n-1),因为共有n*(n-1)个非对角元素(两个方向)
        AVDis = total / (n * (n - 1))
        return AVDis

    # ============================================================
    # 内部方法2:计算局部密度 ρ_i
    # ============================================================
    def _compute_density(self, dist_matrix, AVDis):
        """
        计算每个样本的局部密度

        公式:ρ_i = Σ_j f(d_ij ≤ AVDis)  - 1

        参数:
            dist_matrix : (n, n) 距离矩阵
            AVDis       : float 全局平均距离
        返回:
            rho : (n,) 局部密度数组
        """
        n = dist_matrix.shape[0]
        rho = np.zeros(n)
        for i in range(n):
            # 统计第i行中距离 ≤ AVDis 的个数,再减1(去掉自己)
            rho[i] = np.sum(dist_matrix[i, :] <= AVDis) - 1
        return rho

    # ============================================================
    # 内部方法3:计算特征权重 w_h
    # ============================================================
    def _compute_feature_weights(self, X):
        """
        计算每个特征维度的权重

        公式:w_h = Σ_i x_ih² / ((1/n) Σ_i Σ_k x_ik²)

        参数:
            X : (n, m) 数据矩阵
        返回:
            w : (m,) 特征权重数组
        """
        numerator = np.sum(X ** 2, axis=0)            # 每列平方和
        denominator = np.mean(np.sum(X ** 2, axis=1)) # 每行平方和的均值
        if denominator > 0:
            w = numerator / denominator
        else:
            w = np.ones(X.shape[1])                    # 异常保护
        return w

    # ============================================================
    # 内部方法4:计算密度权值 w_i
    # ============================================================
    def _compute_density_weight(self, dist_weighted, rho):
        """
        计算每个样本的密度权值

        公式:w_i = ρ_i * s_i / m_i

        参数:
            dist_weighted : (n, n) 加权距离矩阵
            rho           : (n,) 局部密度
        返回:
            weight_i : (n,) 密度权值
        """
        n = dist_weighted.shape[0]
        weight_i = np.zeros(n)
        for i in range(n):
            # 把第i行距离升序排列
            sorted_d = np.sort(dist_weighted[i, :])
            # s_i:最近邻距离(第0个是自己,距离为0)
            s_i = sorted_d[1] if len(sorted_d) > 1 else 1e-6
            # m_i:到其他点的平均距离
            m_i = np.mean(sorted_d[1:]) if len(sorted_d) > 1 else 1e-6
            # 密度权值
            weight_i[i] = rho[i] * s_i / m_i if m_i > 0 else 0
        return weight_i

    # ============================================================
    # 内部方法5:选择初始聚类中心
    # ============================================================
    def _select_initial_centers(self, X, dist_matrix, AVDis, weight_i):
        """
        基于密度权值选择K个初始聚类中心

        策略:
            1. 选权值最大的点作为第1个中心
            2. 依次选权值次大的点,要求与已选中心距离 ≥ AVDis
            3. 若不足K个,从剩余点中补充

        参数:
            X           : (n, m) 数据矩阵
            dist_matrix : (n, n) 距离矩阵
            AVDis       : float 全局平均距离
            weight_i    : (n,) 密度权值
        返回:
            centers : (K, m) 初始聚类中心
        """
        n = X.shape[0]
        K = self.n_clusters

        # 按权值降序排列
        sorted_idx = np.argsort(-weight_i)

        # 第1个中心:权值最大
        centers_idx = [sorted_idx[0]]

        # 依次选择后续中心
        for idx in sorted_idx[1:]:
            if len(centers_idx) >= K:
                break
            # 检查是否与所有已选中心距离 ≥ AVDis
            is_valid = True
            for c_idx in centers_idx:
                if dist_matrix[idx, c_idx] < AVDis:
                    is_valid = False
                    break
            if is_valid:
                centers_idx.append(idx)

        # 如果不够K个,从剩余点补充
        if len(centers_idx) < K:
            for idx in sorted_idx:
                if idx not in centers_idx:
                    centers_idx.append(idx)
                if len(centers_idx) >= K:
                    break

        # 从原始数据取出中心
        centers = X[centers_idx, :].copy()
        return centers

    # ============================================================
    # 主方法:fit ------ 执行完整聚类流程
    # ============================================================
    def fit(self, X):
        """
        执行聚类

        参数:
            X : (n, m) 数据矩阵,n个样本,每个样本m维
        返回:
            self : 返回自身,支持链式调用
        """
        n_samples, n_features = X.shape

        if self.verbose:
            print(f"[改进K-means] 开始聚类:样本数={n_samples}, 特征数={n_features}, K={self.n_clusters}")

        # ---------- 步骤1:计算原始距离矩阵 ----------
        dist_matrix = cdist(X, X, metric='euclidean')

        # ---------- 步骤2:计算全局平均距离 ----------
        AVDis = self._compute_avg_distance(dist_matrix)
        if self.verbose:
            print(f"[改进K-means] 全局平均距离 AVDis = {AVDis:.4f}")

        # ---------- 步骤3:计算局部密度 ----------
        rho = self._compute_density(dist_matrix, AVDis)

        # ---------- 步骤4:计算特征权重 ----------
        w = self._compute_feature_weights(X)

        # ---------- 步骤5:计算加权距离矩阵 ----------
        X_weighted = X * np.sqrt(w)
        dist_weighted = cdist(X_weighted, X_weighted, metric='euclidean')

        # ---------- 步骤6:计算密度权值 ----------
        weight_i = self._compute_density_weight(dist_weighted, rho)

        # ---------- 步骤7:选择初始聚类中心 ----------
        centers = self._select_initial_centers(X, dist_matrix, AVDis, weight_i)
        if self.verbose:
            print(f"[改进K-means] 初始聚类中心已选:{centers.shape}")

        # ---------- 步骤8:迭代优化 ----------
        labels = np.zeros(n_samples, dtype=int)

        for iteration in range(self.max_iter):
            # 8.1 分配:每个样本分到最近的中心
            dist_to_centers = cdist(X, centers, metric='euclidean')
            new_labels = np.argmin(dist_to_centers, axis=1)

            # 8.2 更新:重新计算每个簇的中心
            new_centers = centers.copy()
            for k in range(self.n_clusters):
                members = X[new_labels == k]
                if len(members) > 0:
                    new_centers[k, :] = np.mean(members, axis=0)

            # 8.3 检查收敛:中心移动量是否小于阈值
            shift = np.linalg.norm(new_centers - centers)
            labels = new_labels
            centers = new_centers

            if self.verbose and (iteration % 10 == 0 or shift < self.tol):
                print(f"[改进K-means] 第{iteration}次迭代:中心移动={shift:.6f}")

            if shift < self.tol:
                if self.verbose:
                    print(f"[改进K-means] 已收敛,迭代次数={iteration+1}")
                break

        # ---------- 步骤9:计算簇内平方和(评价指标) ----------
        inertia = 0.0
        for k in range(self.n_clusters):
            members = X[labels == k]
            if len(members) > 0:
                inertia += np.sum((members - centers[k]) ** 2)

        # ---------- 保存结果 ----------
        self.labels_ = labels
        self.cluster_centers_ = centers
        self.inertia_ = inertia

        return self

    # ============================================================
    # 辅助方法:预测新样本的簇标签
    # ============================================================
    def predict(self, X):
        """
        对新样本预测所属簇

        参数:
            X : (n, m) 新样本
        返回:
            labels : (n,) 簇标签
        """
        if self.cluster_centers_ is None:
            raise ValueError("请先调用 fit() 训练模型")
        dist = cdist(X, self.cluster_centers_, metric='euclidean')
        return np.argmin(dist, axis=1)

    # ============================================================
    # 辅助方法:fit_predict
    # ============================================================
    def fit_predict(self, X):
        """训练并返回标签"""
        self.fit(X)
        return self.labels_


#===============算例1===========
import numpy as np
import matplotlib.pyplot as plt

# ---------- 1. 生成测试数据 ----------
np.random.seed(0)

# 3簇,每簇10个点,围绕中心(0,0)、(10,0)、(0,10)
cluster1 = np.random.randn(10, 2) + [0, 0]
cluster2 = np.random.randn(10, 2) + [10, 0]
cluster3 = np.random.randn(10, 2) + [0, 10]

X = np.vstack([cluster1, cluster2, cluster3])  # (30, 2)
print("数据形状:", X.shape)  # (30, 2)

# ---------- 2. 用改进K-means聚类 ----------
model = ImprovedKMeans(n_clusters=3, verbose=True)
model.fit(X)

print("\n聚类标签:", model.labels_)
print("聚类中心:\n", model.cluster_centers_)
print("簇内平方和:", model.inertia_)

# ---------- 3. 可视化结果 ----------
plt.figure(figsize=(8, 6))
colors = ['red', 'green', 'blue']
for k in range(3):
    members = X[model.labels_ == k]
    plt.scatter(members[:, 0], members[:, 1], c=colors[k], label=f'簇{k+1}', s=80)
    plt.scatter(model.cluster_centers_[k, 0], model.cluster_centers_[k, 1],
                c=colors[k], marker='*', s=400, edgecolors='black')

plt.title('改进K-means聚类结果')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

三、改进方法2:DBSCAN 预筛选高密度点作为 Kmeans 初始质心

思路:先用 DBSCAN 找到数据高密度区域,从高密度核心点里挑选初始质心,规避离群点选为初始中心

DBSCAN 原理讲解

DBSCAN(Density-Based Spatial Clustering of Applications with Noise,基于密度的聚类算法),核心思想:簇是高密度连通区域,低密度区域是噪声,不需要预先指定聚类数量 。

两个核心超参数

  1. eps(ε,邻域半径):以某个点为圆心,半径 eps 内的区域就是该点的邻域
  2. min_samples:一个点的 eps 邻域内至少要有 min_samples 个样本,才叫核心点
    点的三类划分
    • ✅ 核心点(Core point):邻域内样本数 ≥ min_samples,是高密度区域的基石
    • ✅ 边界点(Border point):本身不是核心点,但落在某个核心点的 eps 邻域内
    • ❌ 噪声点(Noise point):既不是核心点,也不属于任何核心点的邻域
    聚类流程
  3. 随机选一个未访问的点;
  4. 判断是不是核心点:
    ◦ 如果是核心点:收集它邻域内所有点,不断扩张(把邻域里其他核心点的邻域也合并进来),形成一个连通簇;
    ◦ 如果不是核心点,标记为噪声点;
  5. 重复直到所有点都被访问;
  6. 噪声点最后不归属任何簇。
    特点:
    • 自动发现簇数量,可以发现任意形状的簇
    • 对异常值鲁棒
    • 缺点:对eps和min_samples很敏感;高维数据距离度量容易失效

改进思路:DBSCAN 筛选高密度点作为 K-Means 初始质心

原生 K-Means 缺陷:随机初始化质心,容易收敛到局部最优,结果不稳定。

改进方案步骤:

  1. 对数据集运行 DBSCAN;
  2. DBSCAN 得到各个簇,取每个簇的核心点(高密度点);
  3. 从每个簇的核心点集合里选一个代表点(比如簇内核心点均值,或者直接簇中心),作为 K-Means 的初始质心;
  4. 把初始质心传给 KMeans 的 init= 参数,替代随机初始化;

注意:DBSCAN 得到簇的数量,就是 K-Means 的 k;DBSCAN 的噪声点直接不参与质心选取。

对比:原生 kmeans 是init='k-means++',我们这里用 DBSCAN 高密度簇中心作为 init。

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体
mpl.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题
from sklearn.datasets import make_blobs
from sklearn.cluster import DBSCAN, KMeans
from sklearn.preprocessing import StandardScaler

# ---------------------- 1. 生成模拟数据 + 标准化 ----------------------
X, y_true = make_blobs(n_samples=500, centers=4, cluster_std=0.6, random_state=42)
X = StandardScaler().fit_transform(X)


# ---------------------- 2. DBSCAN聚类,提取高密度核心点 ----------------------
eps = 0.5
min_samples = 5
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
db_labels = dbscan.fit_predict(X)

# DBSCAN标签:-1 代表噪声点
unique_labels = set(db_labels)
n_clusters_db = len([l for l in unique_labels if l != -1])
print(f"DBSCAN 识别出簇数量:{n_clusters_db}") #4
print(f"DBSCAN 标签集合:{unique_labels}")#{0, 1, 2, 3}


# 收集每个簇的核心点,计算每个簇核心点的均值作为初始质心
core_sample_mask = dbscan.core_sample_indices_  # 核心点索引
core_X = X[core_sample_mask]
core_labels = db_labels[core_sample_mask]

init_centers = []
for label in unique_labels:
    if label == -1:
        continue
    # 当前簇的所有核心点
    cluster_core_points = core_X[core_labels == label]
    center = np.mean(cluster_core_points, axis=0)
    init_centers.append(center)
init_centers = np.array(init_centers)
print("===== DBSCAN高密度核心点生成的初始质心 =====")
print(init_centers)#shape(4,2)



# ---------------------- 3. Kmeans 使用DBSCAN得到的质心初始化 ----------------------
# n_clusters 和DBSCAN找到的簇数保持一致
kmeans_improved = KMeans(
    n_clusters=n_clusters_db,
    init=init_centers,  # 关键:用DBSCAN高密度点中心初始化
    n_init=1,  # 给定初始质心时n_init必须设为1
    random_state=42
)
y_pred_improved = kmeans_improved.fit_predict(X)

# 原生Kmeans(kmeans++初始化,用来对比)
kmeans_origin = KMeans(n_clusters=n_clusters_db, init="k-means++", random_state=42)
y_pred_origin = kmeans_origin.fit_predict(X)

# ---------------------- 4. 绘图对比 ----------------------
fig, axes = plt.subplots(1, 3, figsize=(16, 4))

# 子图1:DBSCAN结果
axes[0].scatter(X[:,0], X[:,1], c=db_labels, cmap="tab10", s=30)
axes[0].set_title("DBSCAN 聚类结果(深色为噪声)")
axes[0].scatter(core_X[:,0], core_X[:,1], c="red", marker="x", s=50, label="核心点")
axes[0].legend()

# 子图2:改进Kmeans(DBSCAN初始化)
axes[1].scatter(X[:,0], X[:,1], c=y_pred_improved, cmap="tab10", s=30)
axes[1].scatter(kmeans_improved.cluster_centers_[:,0],
                kmeans_improved.cluster_centers_[:,1],
                c="black", marker="*", s=200, label="最终质心")
axes[1].set_title("Kmeans(DBSCAN高密度点初始化)")
axes[1].legend()

# 子图3:原生Kmeans++
axes[2].scatter(X[:,0], X[:,1], c=y_pred_origin, cmap="tab10", s=30)
axes[2].scatter(kmeans_origin.cluster_centers_[:,0],
                kmeans_origin.cluster_centers_[:,1],
                c="black", marker="*", s=200, label="最终质心")
axes[2].set_title("原生Kmeans(k-means++初始化)")
axes[2].legend()

plt.tight_layout()
plt.show()

# ---------------------- 5. 评估指标(可选) ----------------------
from sklearn.metrics import silhouette_score
sil_dbscan = silhouette_score(X, db_labels)
sil_improved = silhouette_score(X, y_pred_improved)
sil_origin = silhouette_score(X, y_pred_origin)
print(f"\n轮廓系数:")
print(f"DBSCAN: {sil_dbscan:.4f}")
print(f"改进Kmeans(DBSCAN init): {sil_improved:.4f}")
print(f"原生Kmeans++: {sil_origin:.4f}")
相关推荐
CaptainT1 小时前
第 2 卷:函数、模块与推导式
python
Shockang1 小时前
项目驱动的 Python 入门
python
灯澜忆梦1 小时前
【面向对象编程c++】| 指针与自定义数据类型
开发语言·c++·算法
xxwl5851 小时前
区域赛算法模版第八章---贪心(Greedy)
c++·笔记·学习·算法
余槐i2 小时前
transformers 升级后加载旧权重报错:三处不兼容如何逐一验证
python·大模型·transformers·模型量化·模型推理
CCblandy2 小时前
向量检索 ANN 参数实测:换一种数据分布,recall 掉了 51 个百分点
人工智能·python·算法·自然语言处理·性能优化
CS创新实验室2 小时前
一棵树怎么做到“自平衡还不太费力“?——红黑树的旋转与染色
数据结构·算法·红黑树·408·计算机考研
Logic1012 小时前
C语言/数据结构位运算题解:异或XOR找出被挑选一次的果子编号——只出现一次的数字
c语言·数据结构·算法·数组·位运算·时间复杂度·空间复杂度
深圳市快瞳科技有限公司2 小时前
面向端侧部署的多帧宠物身份识别:完整性评分机制与工程约束
人工智能·算法·计算机视觉·端侧ai·宠物识别·宠物ai·宠物身份识别