系列文章目录
文章目录
- 系列文章目录
- 一、K-means算法流程
- 二、改进方法1:基于密度权值对初始聚类中心的改进
- [三、改进方法2:DBSCAN 预筛选高密度点作为 Kmeans 初始质心](#三、改进方法2:DBSCAN 预筛选高密度点作为 Kmeans 初始质心)
-
- [DBSCAN 原理讲解](#DBSCAN 原理讲解)
- [改进思路:DBSCAN 筛选高密度点作为 K-Means 初始质心](#改进思路:DBSCAN 筛选高密度点作为 K-Means 初始质心)
文章摘要:本文针对传统 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,基于密度的聚类算法),核心思想:簇是高密度连通区域,低密度区域是噪声,不需要预先指定聚类数量 。
两个核心超参数
- eps(ε,邻域半径):以某个点为圆心,半径 eps 内的区域就是该点的邻域
- min_samples:一个点的 eps 邻域内至少要有 min_samples 个样本,才叫核心点
点的三类划分
• ✅ 核心点(Core point):邻域内样本数 ≥ min_samples,是高密度区域的基石
• ✅ 边界点(Border point):本身不是核心点,但落在某个核心点的 eps 邻域内
• ❌ 噪声点(Noise point):既不是核心点,也不属于任何核心点的邻域
聚类流程 - 随机选一个未访问的点;
- 判断是不是核心点:
◦ 如果是核心点:收集它邻域内所有点,不断扩张(把邻域里其他核心点的邻域也合并进来),形成一个连通簇;
◦ 如果不是核心点,标记为噪声点; - 重复直到所有点都被访问;
- 噪声点最后不归属任何簇。
特点:
• 自动发现簇数量,可以发现任意形状的簇
• 对异常值鲁棒
• 缺点:对eps和min_samples很敏感;高维数据距离度量容易失效
改进思路:DBSCAN 筛选高密度点作为 K-Means 初始质心
原生 K-Means 缺陷:随机初始化质心,容易收敛到局部最优,结果不稳定。
改进方案步骤:
- 对数据集运行 DBSCAN;
- DBSCAN 得到各个簇,取每个簇的核心点(高密度点);
- 从每个簇的核心点集合里选一个代表点(比如簇内核心点均值,或者直接簇中心),作为 K-Means 的初始质心;
- 把初始质心传给 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}")
