当前位置: 首页 > news >正文

K-means聚类算法原理与Python实现:从零到实战可视化

1. 项目概述:从数据到洞察,K-means的实战价值

如果你手头有一堆客户数据、用户行为记录或者是一组实验测量值,它们看起来杂乱无章,你可能会想:这些数据里是不是藏着一些我没发现的规律?比如,客户是不是可以分成几类来制定不同的营销策略?用户对产品的偏好是不是有几种明显的模式?这时候,聚类算法就是你需要的“数据显微镜”。而在众多聚类算法中,K-means以其原理直观、实现简单、效率较高的特点,成为了最经典、应用最广泛的入门选择。

这个项目的核心,就是使用Python亲手实现K-means聚类算法,并完成结果的可视化。这听起来像是一个纯粹的机器学习练习,但其背后的实战价值远超想象。它不仅仅是为了理解算法原理,更是为了掌握一套从原始数据中提取分组信息,并将抽象结果转化为直观图形的完整数据分析流程。在电商用户分群、市场细分、图像分割、异常检测等场景中,这套流程是数据科学家和算法工程师的日常操作。通过自己动手实现,你能深刻理解“肘部法则”如何确定最佳K值、质心迭代更新背后的数学意义,以及为什么同样的数据,不同的初始质心可能导致不同的聚类结果——这些都是在调用sklearn.cluster.KMeans一行代码时容易被忽略的细节。

2. 核心原理与算法流程拆解

2.1 K-means算法的核心思想

K-means算法的目标非常明确:将给定的数据集划分成K个簇,使得同一个簇内的数据点彼此相似,而不同簇之间的数据点尽可能不同。这里的“相似”通常用距离来衡量,最常用的是欧几里得距离。算法通过迭代优化来实现这个目标,其优化的核心是最小化每个数据点到其所属簇质心的距离平方和,这个指标被称为簇内误差平方和

你可以把它想象成一场“领地划分”游戏。假设你有K个领主(质心),他们要在平面上圈地。游戏规则是:1. 每个平民(数据点)归离他最近的那个领主管理。2. 每个领主根据归顺他的平民们的位置,重新计算自己的城堡(质心)应该建在哪里(通常就是所有属民位置的平均值)。3. 领主们移动城堡后,平民们可能会发现另一个领主更近了,于是重新选择归属。这个过程不断重复,直到领主们的城堡位置不再移动,平民们的归属也不再改变,格局就此稳定下来。

2.2 标准算法步骤详解

基于上述思想,标准的K-means算法遵循以下步骤,这也是我们待会要编码实现的核心逻辑:

  1. 初始化:从数据集中随机选择K个点作为初始的簇质心。这是关键一步,糟糕的初始化可能导致算法收敛到局部最优解,而非全局最优。因此,在实际应用中,我们常采用“K-means++”初始化策略来改善这一点,它能有效让初始质心彼此远离,提升聚类效果和速度。

  2. 分配阶段:遍历数据集中的每一个数据点,计算该点到所有K个质心的距离(通常是欧氏距离)。将该点分配给距离最近的质心所在的簇。这个过程完成了数据点的“归类”。

  3. 更新阶段:所有数据点分配完毕后,对于每一个簇,重新计算该簇的质心。新质心的坐标是该簇内所有数据点各维度坐标的算术平均值。例如,在二维空间中,新质心的(x, y)坐标就是该簇所有点x坐标的平均值和y坐标的平均值。

  4. 迭代与收敛:重复步骤2和步骤3,直到满足停止条件。常见的停止条件有:

    • 质心不再变化:新计算出的质心与上一轮的质心完全相同(或在极小的容差范围内)。
    • 数据点所属簇不再变化:所有数据点的簇标签在连续两轮迭代中保持一致。
    • 达到最大迭代次数:为了防止无限循环,设置一个迭代上限。

注意:K-means算法保证每次迭代都能降低簇内误差平方和,因此它最终一定会收敛。但收敛到的可能是一个局部最优解,而非全局最优。这也是为什么我们常常需要多次运行算法(使用不同的随机初始质心),并选择结果最好的那次。

2.3 关键参数:如何确定K值?

在运行算法之前,我们必须指定簇的数量K。但很多时候,我们并不知道数据应该分成几类。这时就需要一些方法来辅助确定K值。

  • 肘部法则:这是最常用的启发式方法。其原理是计算不同K值下的簇内误差平方和,并绘制曲线。随着K增大,每个簇更“紧凑”,误差平方和会下降。当K增加到真实簇数时,再增加K带来的误差下降幅度会骤减,曲线会出现一个明显的拐点,形状像人的肘部,拐点对应的K值就是建议值。
  • 轮廓系数:这是一个衡量聚类效果的综合指标,结合了簇内的凝聚度和簇间的分离度。轮廓系数的取值范围在[-1, 1]之间。值越接近1,说明聚类效果越好。我们可以计算不同K值下的平均轮廓系数,选择使其最大化的K。
  • 业务理解:在某些场景下,K值可能由业务需求直接决定。例如,要将客户分为高、中、低价值三类,那么K=3。

在我们的实现和后续绘图中,将重点演示如何使用“肘部法则”来确定K值。

3. 从零开始:K-means算法的Python实现

我们不依赖sklearn,而是用NumPy来手动实现算法核心,这能让你对每一个计算步骤都了然于胸。可视化部分则交给强大的Matplotlib

3.1 环境准备与数据生成

首先,确保你的Python环境安装了必要的库。在命令行中执行:

pip install numpy matplotlib

为了演示,我们使用NumPy生成一份模拟的二维数据集。这份数据包含300个样本点,它们实际上是从3个不同的高斯分布(正态分布)中采样生成的,因此天然地形成了3个簇。

import numpy as np import matplotlib.pyplot as plt # 设置随机种子,确保每次运行生成的数据一致 np.random.seed(42) # 定义三个簇的中心点 centers = np.array([[2, 2], [8, 3], [3, 7]]) # 生成数据:每个中心点周围生成100个点,并添加一些随机噪声 data = np.vstack([ np.random.randn(100, 2) * 0.8 + centers[0], np.random.randn(100, 2) * 1.2 + centers[1], np.random.randn(100, 2) * 0.9 + centers[2] ]) # 打乱数据顺序,使其更接近真实场景 np.random.shuffle(data) print(f"数据集形状: {data.shape}") # 应输出 (300, 2)

这段代码生成了我们的“战场”。centers是三个簇的真实中心,data是混合了噪声的样本点。在实际项目中,data就是你从数据库或文件中加载的原始特征矩阵。

3.2 K-means核心类实现

我们将算法封装成一个类,这样结构更清晰,也便于复用。

class KMeansManual: def __init__(self, n_clusters=3, max_iter=300, tol=1e-4, random_state=None): """ 初始化K-means聚类器。 参数: n_clusters (int): 要形成的簇的数量,也是质心的数量。 max_iter (int): 单次运行的最大迭代次数。 tol (float): 收敛阈值。如果质心的移动距离小于此值,则停止迭代。 random_state (int): 随机种子,用于复现结果。 """ self.n_clusters = n_clusters self.max_iter = max_iter self.tol = tol self.random_state = random_state self.centroids = None # 质心坐标 self.labels = None # 每个样本点所属的簇标签 self.inertia_ = None # 簇内误差平方和 def _initialize_centroids(self, X): """初始化质心。这里采用最简单的随机选择方法。""" if self.random_state is not None: np.random.seed(self.random_state) # 从数据点中随机选择K个作为初始质心 random_indices = np.random.choice(X.shape[0], self.n_clusters, replace=False) return X[random_indices] def _compute_distance(self, X, centroids): """ 计算每个数据点到所有质心的距离。 使用欧氏距离,并利用广播机制进行向量化计算,提升效率。 返回: distances: 形状为 (n_samples, n_clusters) 的距离矩阵。 """ # X: (n_samples, n_features) # centroids: (n_clusters, n_features) # 利用广播,计算样本与每个质心的差值平方和,再开方 distances = np.sqrt(((X[:, np.newaxis, :] - centroids) ** 2).sum(axis=2)) return distances def fit(self, X): """ 对输入数据X进行K-means聚类拟合。 参数: X (np.ndarray): 形状为 (n_samples, n_features) 的训练数据。 返回: self: 返回实例本身。 """ n_samples, n_features = X.shape # 1. 初始化质心 self.centroids = self._initialize_centroids(X) for iteration in range(self.max_iter): # 2. 分配阶段:计算距离,分配标签 distances = self._compute_distance(X, self.centroids) new_labels = np.argmin(distances, axis=1) # 每个点取距离最小的质心索引 # 3. 更新阶段:计算新质心 new_centroids = np.zeros((self.n_clusters, n_features)) for k in range(self.n_clusters): # 找到属于当前簇k的所有点 cluster_points = X[new_labels == k] if len(cluster_points) > 0: # 新质心是该簇所有点的均值 new_centroids[k] = cluster_points.mean(axis=0) else: # 极端情况:如果某个簇没有点,则重新随机初始化该质心 new_centroids[k] = X[np.random.randint(0, n_samples)] # 4. 检查收敛条件:质心移动是否小于阈值 centroid_shift = np.sqrt(((new_centroids - self.centroids) ** 2).sum(axis=1)).max() if centroid_shift < self.tol: print(f"迭代 {iteration + 1} 次后收敛。") break # 更新质心和标签 self.centroids = new_centroids self.labels = new_labels else: # 如果for循环正常结束(未break),说明达到了max_iter print(f"达到最大迭代次数 {self.max_iter} 后停止。") self.labels = new_labels # 计算最终的簇内误差平方和 self.inertia_ = 0 for k in range(self.n_clusters): cluster_points = X[self.labels == k] if len(cluster_points) > 0: # 计算该簇所有点到其质心的距离平方和 self.inertia_ += ((cluster_points - self.centroids[k]) ** 2).sum() return self def predict(self, X): """ 预测新数据点所属的簇。 参数: X (np.ndarray): 形状为 (n_samples, n_features) 的新数据。 返回: labels: 每个样本点预测的簇标签。 """ distances = self._compute_distance(X, self.centroids) return np.argmin(distances, axis=1)

代码要点解析

  • 向量化计算:在_compute_distance函数中,我们使用了X[:, np.newaxis, :]来增加一个维度,利用NumPy的广播机制一次性计算所有样本点到所有质心的距离,这比用for循环快几个数量级。
  • 空簇处理:在更新质心的循环中,我们检查了len(cluster_points) > 0。如果一个簇在迭代中失去了所有点(空簇),简单的均值计算会出错。这里我们采用了一种简单的处理策略:为该簇随机重新选择一个数据点作为质心。更复杂的策略包括选择距离当前质心最远的点,或者直接移除该簇。
  • 收敛判断:我们计算了新老质心之间的最大欧氏距离centroid_shift,并与容差tol比较。这是判断算法是否停止的核心。
  • inertia_属性:我们按照定义计算了簇内误差平方和,这个值将在后续确定K值时用到。

3.3 基础聚类与可视化

现在,让我们用这个类对生成的数据进行聚类,并绘制结果。

# 实例化并拟合模型 kmeans = KMeansManual(n_clusters=3, max_iter=300, random_state=42) kmeans.fit(data) # 获取结果 labels = kmeans.labels centroids = kmeans.centroids # 创建绘图 plt.figure(figsize=(10, 8)) # 1. 绘制原始数据点,按聚类结果着色 scatter = plt.scatter(data[:, 0], data[:, 1], c=labels, cmap='viridis', alpha=0.6, edgecolors='w', s=50, label='数据点') # 2. 绘制最终的质心 plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, label='质心', edgecolors='black', linewidth=2) # 3. 为每个簇绘制一个凸包或圆圈(可选,增强视觉效果) from scipy.spatial import ConvexHull for k in range(kmeans.n_clusters): cluster_points = data[labels == k] if len(cluster_points) > 2: # 凸包需要至少3个点 hull = ConvexHull(cluster_points) # 绘制凸包边界 for simplex in hull.simplices: plt.plot(cluster_points[simplex, 0], cluster_points[simplex, 1], 'k--', alpha=0.3, linewidth=1) plt.title('K-means聚类结果可视化 (K=3)', fontsize=15) plt.xlabel('特征 1') plt.ylabel('特征 2') plt.legend() plt.grid(True, alpha=0.3) plt.axis('equal') # 保证x轴和y轴比例相同,防止图形扭曲 plt.tight_layout() plt.show() print(f"簇内误差平方和 (Inertia): {kmeans.inertia_:.2f}")

这段代码执行后,你会得到一张清晰的散点图。不同颜色的点代表不同的簇,红色的“X”标记是算法找到的最终质心。虚线勾勒出了每个簇的大致范围(凸包)。通过这张图,你可以直观地评估聚类效果:簇内点是否紧密,簇间是否分离良好。同时,打印出的inertia_值是一个重要的量化指标。

4. 进阶分析与可视化技巧

4.1 确定最佳K值:肘部法则实现

面对未知数据,我们需要一个方法来推测合理的K值。下面实现肘部法则:

def plot_elbow_method(X, max_k=10): """ 绘制肘部法则图,帮助确定最佳K值。 参数: X: 输入数据。 max_k: 尝试的最大K值。 """ inertias = [] K_range = range(1, max_k + 1) for k in K_range: kmeans = KMeansManual(n_clusters=k, max_iter=300, random_state=42) kmeans.fit(X) inertias.append(kmeans.inertia_) plt.figure(figsize=(8, 5)) plt.plot(K_range, inertias, 'bo-', linewidth=2, markersize=8) plt.xlabel('簇的数量 (K)') plt.ylabel('簇内误差平方和 (Inertia)') plt.title('肘部法则 (Elbow Method)') plt.xticks(K_range) plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # 也可以计算相邻K值之间inertia的下降百分比,辅助判断“肘点” # 下降幅度突然变缓的点即为肘点 # inertias_diff_pct = [ (inertias[i-1] - inertias[i]) / inertias[i-1] for i in range(1, len(inertias)) ] # print("误差下降百分比:", inertias_diff_pct) # 对我们的数据使用肘部法则 plot_elbow_method(data, max_k=10)

运行后,你会看到一条曲线。当K从1增加到3时,误差平方和急剧下降;当K超过3后,下降趋势明显变缓。那个拐点(看起来像肘关节)对应的K=3,正是我们生成数据时使用的真实簇数。这证明了肘部法则的有效性。

4.2 质心迭代过程动画演示

理解K-means的迭代过程对于掌握其动态行为至关重要。我们可以用动画来展示质心是如何一步步移动并最终稳定的。

from matplotlib.animation import FuncAnimation from IPython.display import HTML # 为了制作动画,我们需要修改fit方法,记录每一轮的质心和标签。 # 这里我们创建一个新的类,或者修改原类的fit方法。 # 为了简洁,我们重新写一个带记录功能的拟合函数。 def fit_with_history(X, n_clusters=3, max_iter=20, random_state=42): """记录每次迭代的质心和标签历史""" np.random.seed(random_state) centroids_history = [] labels_history = [] # 初始化 centroids = X[np.random.choice(X.shape[0], n_clusters, replace=False)] centroids_history.append(centroids.copy()) for i in range(max_iter): # 分配 distances = np.sqrt(((X[:, np.newaxis, :] - centroids) ** 2).sum(axis=2)) labels = np.argmin(distances, axis=1) labels_history.append(labels.copy()) # 更新 new_centroids = np.array([X[labels == k].mean(axis=0) if len(X[labels == k])>0 else centroids[k] for k in range(n_clusters)]) # 检查收敛(简化) if np.allclose(new_centroids, centroids, atol=1e-4): print(f"在第{i+1}次迭代收敛") labels_history.append(labels.copy()) # 记录最终标签 break centroids = new_centroids centroids_history.append(centroids.copy()) else: labels_history.append(labels.copy()) # 记录最终标签 return centroids_history, labels_history # 获取历史数据 centroids_hist, labels_hist = fit_with_history(data, n_clusters=3, max_iter=15) # 创建动画 fig, ax = plt.subplots(figsize=(8, 6)) def update(frame): ax.clear() # 绘制数据点,用当前帧的标签着色 scat = ax.scatter(data[:, 0], data[:, 1], c=labels_hist[frame], cmap='viridis', alpha=0.6, s=30) # 绘制当前质心 centroids = centroids_hist[frame] ax.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=150, edgecolors='black', linewidth=2) # 绘制质心的移动轨迹(从开始到当前帧) for k in range(centroids.shape[0]): hist = np.array(centroids_hist[:frame+1]) ax.plot(hist[:, k, 0], hist[:, k, 1], 'r--', linewidth=1, alpha=0.7) ax.set_title(f'K-means迭代过程 (第 {frame+1} 步)') ax.set_xlabel('特征 1') ax.set_ylabel('特征 2') ax.grid(True, alpha=0.3) ax.axis('equal') return scat, # 创建动画对象 anim = FuncAnimation(fig, update, frames=len(labels_hist), interval=800, blit=False, repeat_delay=2000) # 在Jupyter Notebook中显示 # HTML(anim.to_jshtml()) # 如果要保存为GIF(需要安装pillow) # anim.save('kmeans_iteration.gif', writer='pillow', fps=2) plt.close() # 防止静态图也显示出来 # 由于在线环境限制,这里我们绘制最后一步的静态图作为示意 plt.figure(figsize=(8,6)) frame = -1 # 最后一步 plt.scatter(data[:, 0], data[:, 1], c=labels_hist[frame], cmap='viridis', alpha=0.6, s=30) centroids = centroids_hist[frame] plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=150, edgecolors='black', linewidth=2) # 绘制完整的质心移动轨迹 for k in range(centroids.shape[0]): hist = np.array(centroids_hist) plt.plot(hist[:, k, 0], hist[:, k, 1], 'r--', linewidth=1, alpha=0.7, label=f'质心{k+1}轨迹' if k==0 else "") plt.title('K-means迭代最终状态与质心移动轨迹') plt.xlabel('特征 1') plt.ylabel('特征 2') plt.legend() plt.grid(True, alpha=0.3) plt.axis('equal') plt.tight_layout() plt.show()

动画(或最终的轨迹图)能生动展示质心如何从随机初始位置,一步步“吸引”周围的数据点,并最终稳定在三个簇的中心。轨迹线清晰地显示了优化路径。

4.3 聚类性能的量化评估:轮廓系数

除了直观的图和肘部法则,我们还需要一个量化的指标来评估聚类质量,尤其是在比较不同K值或不同算法时。轮廓系数是一个很好的选择。

from sklearn.metrics import silhouette_score, silhouette_samples import matplotlib.cm as cm def plot_silhouette_analysis(X, cluster_labels, n_clusters): """ 绘制轮廓分析图,包括每个簇的轮廓系数分布和平均轮廓系数。 改编自scikit-learn官方示例。 """ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) # 子图1:轮廓系数分布图 ax1.set_xlim([-0.1, 1]) # (n_clusters+1)*10 是为了给每个簇的条形图留出间隔 ax1.set_ylim([0, len(X) + (n_clusters + 1) * 10]) # 计算所有样本的轮廓系数 sample_silhouette_values = silhouette_samples(X, cluster_labels) silhouette_avg = silhouette_score(X, cluster_labels) y_lower = 10 for i in range(n_clusters): # 获取第i个簇的轮廓系数,并排序 ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i] ith_cluster_silhouette_values.sort() size_cluster_i = ith_cluster_silhouette_values.shape[0] y_upper = y_lower + size_cluster_i color = cm.nipy_spectral(float(i) / n_clusters) ax1.fill_betweenx(np.arange(y_lower, y_upper), 0, ith_cluster_silhouette_values, facecolor=color, edgecolor=color, alpha=0.7) # 标记簇的编号 ax1.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i+1)) y_lower = y_upper + 10 # 为下一个簇留出10个单位的间隔 ax1.set_title("各簇轮廓系数分布图") ax1.set_xlabel("轮廓系数值") ax1.set_ylabel("簇标签") # 绘制平均轮廓系数的垂直线 ax1.axvline(x=silhouette_avg, color="red", linestyle="--") ax1.text(silhouette_avg + 0.02, y_lower * 0.9, f'平均轮廓系数: {silhouette_avg:.3f}', color='red') ax1.set_yticks([]) # 清除y轴刻度 ax1.set_xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1]) # 子图2:聚类结果散点图(按轮廓系数着色) colors = cm.nipy_spectral(cluster_labels.astype(float) / n_clusters) scatter = ax2.scatter(X[:, 0], X[:, 1], marker='.', s=30, lw=0, alpha=0.7, c=colors, edgecolor='k') # 绘制质心 # 需要计算质心位置 centers = np.array([X[cluster_labels == i].mean(axis=0) for i in range(n_clusters)]) ax2.scatter(centers[:, 0], centers[:, 1], marker='o', c="white", alpha=1, s=200, edgecolor='k') for i, c in enumerate(centers): ax2.scatter(c[0], c[1], marker='$%d$' % (i+1), alpha=1, s=50, edgecolor='k') ax2.set_title("聚类结果可视化") ax2.set_xlabel("特征 1") ax2.set_ylabel("特征 2") plt.suptitle(f"K={n_clusters}时的轮廓分析 (平均轮廓系数 = {silhouette_avg:.3f})", fontsize=14, fontweight='bold') plt.tight_layout() plt.show() return silhouette_avg # 对我们K=3的聚类结果进行分析 avg_score = plot_silhouette_analysis(data, kmeans.labels, 3) print(f"K=3时,平均轮廓系数为: {avg_score:.3f}")

轮廓系数分布图非常直观:每个簇的条形图越长、越集中向右(接近1),说明该簇的聚类效果越好。红色虚线代表所有样本的平均轮廓系数。一个良好的聚类,其平均轮廓系数应接近1,且所有簇的条形图长度应相近,没有太多负值(条形图延伸到左侧负值区域表示有些点可能被分错了簇)。

5. 实战注意事项与常见问题排查

自己实现并应用K-means时,你会遇到一些典型问题。以下是基于经验的总结:

5.1 初始质心敏感性与改进策略

K-means最大的缺点之一就是对初始质心的选择非常敏感,容易陷入局部最优。你可以用我们写的类,设置不同的random_state多次运行,观察结果的差异。

解决方案

  • 多次运行:最简单的策略是运行算法多次(例如10-50次),每次使用不同的随机种子初始化,然后选择inertia_最小的那次结果作为最终输出。sklearnKMeans默认n_init=10就是这么做的。
  • K-means++初始化:这是一种更智能的初始化方法。它选择第一个质心随机,后续质心选择时,会倾向于选择那些离已有质心较远的点。这能显著提高找到全局最优解的概率和收敛速度。其核心思想是让初始质心尽可能分散。实现起来比随机初始化稍复杂,但很多库都已内置。
  • 其他高级变种:如K-medoids(PAM算法),它选择实际存在的点作为中心点,对异常值更鲁棒。

5.2 数据预处理与特征缩放

K-means使用欧氏距离,这意味着它对特征的尺度非常敏感。如果一个特征的范围是0-1000,另一个特征的范围是0-1,那么范围大的特征将完全主导距离计算,导致聚类结果失真。

必须进行的操作特征标准化。最常用的是Z-score标准化(StandardScaler),将每个特征缩放到均值为0,标准差为1。

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() data_scaled = scaler.fit_transform(data) # 然后对 data_scaled 进行聚类

对于有明确边界的数据,也可以使用MinMaxScaler缩放到[0,1]区间。在聚类前,务必检查并处理特征的尺度问题。

5.3 非球形簇与高维数据挑战

K-means假设簇是凸形的(类似于球形或超球形),并且各向同性(在各个方向上方差相近)。对于流形、环形或不规则形状的簇,K-means效果会很差。

问题示例:两个同心圆环状分布的数据点,K-means无法正确分离。

解决方案

  • 尝试其他算法:对于非球形簇,可以考虑DBSCAN(基于密度)、谱聚类或高斯混合模型。
  • 降维:对于高维数据,距离度量可能失效(“维度灾难”)。可以先使用PCA或t-SNE等降维方法将数据降至2-3维,再进行聚类和可视化,但这会损失信息,需谨慎。

5.4 常见错误与调试

  1. 空簇错误:在更新质心时,如果某个簇没有分配到任何点,计算均值会出错。我们的代码已经做了简单处理(随机重置)。在更严谨的实现中,需要更稳定的策略。
  2. 收敛慢或不收敛:检查max_itertol参数。如果数据量巨大或特征很多,可以适当增大max_iter。如果数据有噪声或异常值,可能导致质心轻微震荡,可以适当增大tol
  3. 结果不稳定:这是初始值敏感性的体现。务必采用“多次运行取最优”的策略,并在报告结果时注明这一过程。
  4. 可视化时坐标轴比例不一致:使用plt.axis('equal')可以确保x轴和y轴的单位长度相等,否则圆形簇可能被显示成椭圆形,误导判断。

5.5 性能优化技巧

  • 向量化:如我们实现中所做,利用NumPy广播和向量化操作,避免Python层面的for循环,是提升速度的关键。
  • 三角不等式加速:成熟的K-means实现(如sklearn)会使用Elkan算法等,利用距离三角不等式避免不必要的距离计算,在数据维度较高时提速明显。
  • Mini-Batch K-means:对于海量数据,可以使用其变种Mini-Batch K-means,每次迭代只使用数据的一个子集来更新质心,牺牲少量精度换取大幅速度提升。

通过这个从零实现K-means并完成一系列可视化分析的项目,你收获的不仅仅是一个算法代码,而是一套完整的数据探索、模型构建、评估和问题诊断的方法论。下次当你面对一堆未知数据时,你知道如何用代码让它“开口说话”,揭示其内在的群组结构。

http://www.cnnetsun.cn/news/4276284.html

相关文章:

  • 智能驾驶变道控制:RL-MPC分层协同架构实战解析
  • Shell脚本工程化:模块化封装mkdir、cp、echo命令实践
  • 让 Agent 真正“记住“项目:从会话记忆到长期记忆
  • C++函数模板实现快速排序:泛型编程与算法优化实践
  • 中文短文本分类的Transformer改进实践:词感知、结构注入与领域蒸馏
  • PyTorch分布式训练实战:从数据并行原理到DDP代码实现
  • Agent Skills 实战:用 Claude Code 封装可复用技能包
  • Python线性规划实战:从数学建模到SciPy/PuLP求解
  • 深度学习在无线信道预测中的应用:从LSTM到Transformer的模型演进与实战
  • OpenCode代码智能体完全指南:从安装配置到实战项目与Skill自定义
  • 【单片机课程设计/毕业设计】基于 STM32 的 OLED 显示停车场刷卡计费系统开发 基于 STM32 的射频识别停车场语音提示控制系统设计(016505)
  • 本地大模型实测指南:从能启动到能用,一套可复现的Benchmark流程
  • BFS算法实战:从调手表问题掌握状态空间搜索与最短路径
  • 本地LLM Benchmark实战:从显存估算到量化选型全指南
  • PCA与ANOVA实战指南:从降维可视化到差异检验的完整流程
  • 蓝桥杯嵌入式实战:电压频率采集装置开发全解析
  • 用 AI 辅助代码审查:提交前检查什么
  • 【Kubernetes从入门到精通】第86篇:生产就绪检查清单——你的K8s集群真的可以上线吗
  • 【Kubernetes从入门到精通】第85篇:K8s成本优化——你的云账单一半都能省掉,老板看了想加鸡腿
  • Claude Code烧钱真相:从安装到批量任务的全流程成本治理指南
  • 慢速英语学习全流程:从标题拆解到内容制作实战
  • Matlab非稳态热传导建模:从有限差分法到工程仿真实战
  • 线性规划实战:Matlab与Lingo在数学建模中的核心应用与选型
  • 红蚂蚁检测数据集与YOLO训练实战:小目标检测全流程指南
  • PyTorch张量运算核心:形状、广播与矩阵乘法实战指南
  • GigaDevice首款Wi-Fi MCU深度解析:AIoT安全底座与开发调试实战
  • 超低功耗RF设备量产:从实验室到全球IoT的工程硬仗
  • 智能文档字段提取工作台功能需求文档
  • Claude Code安全剖析:720次攻击0成功,权限模型与防御实践
  • Spring AOP切点表达式execution实战:精准拦截与性能优化指南