
- 个人首页: 永远都不秃头的程序员(互关)
- C语言专栏:从零开始学习C语言
- C++专栏:C++的学习之路
- 本文章所属专栏:K-Means深度探索系列
文章目录
-
-
- 引言:揭开混沌中的秩序之美
- [K-Means 核心思想:迭代的"物以类聚"哲学](#K-Means 核心思想:迭代的“物以类聚”哲学)
- [手把手代码实现:搭建你的 K-Means 核心模块](#手把手代码实现:搭建你的 K-Means 核心模块)
- [深度实践:运行与解读你的 K-Means 算法 📊](#深度实践:运行与解读你的 K-Means 算法 📊)
- [小结与展望:数据炼金之旅的开端 📖](#小结与展望:数据炼金之旅的开端 📖)
-
引言:揭开混沌中的秩序之美
亲爱的读者朋友们,你是否曾被大数据中的汪洋大海所淹没?海量信息扑面而来,如何才能像炼金术士一样,从中提炼出有价值的"黄金",发现隐藏的规律和结构?今天,我们就将迈出"数据炼金术"的第一步,探索一个在机器学习领域赫赫有名、应用极其广泛的无监督学习算法------K-Means 聚类!
K-Means 就像一位经验丰富的"数据侦探",它能在我们对数据一无所知(没有标签)的情况下,凭借数据点之间的相似性,自动将它们分门别类,形成若干个有意义的"簇"。无论是用户画像分析、文档主题发现,还是图像像素聚类,K-Means 都能大显身手。
而今天,我们的目标不仅仅是学会如何调用 sklearn 库中现成的 KMeans 类,更要从零开始,亲手敲下每一行代码,彻底理解 K-Means 的运作机制。这不仅能让你对算法有更深刻的认识,更能培养你独立解决问题的能力。相信我,当你看到自己写出的代码能将杂乱无章的数据点清晰地聚类时,那种成就感是无与伦比的! 准备好了吗?让我们一起深入 K-Means 的核心,开始我们的"手撕"之旅!
K-Means 核心思想:迭代的"物以类聚"哲学
K-Means 的思想朴素而强大:"物以类聚,人以群分" 。它尝试将数据集中的数据点分配到 K 个不同的簇中,使得同一个簇内的数据点尽可能相似,而不同簇之间的数据点则尽可能不同。这个过程是一个不断优化迭代的过程,直至达到一个稳定的状态。
算法的"三步走"迭代循环:
-
确定 K 值: 在算法开始前,我们需要人为地决定将数据分成多少个簇(
K)。这是 K-Means 中最关键且最具挑战性的一步,我们会在后续的文章中深入探讨其选择策略。 -
初始化质心: 随机地从数据集中选择
K个数据点作为每个簇的初始中心点,我们称之为"质心 (Centroids)"。你可以想象它们是 K 个小小的"磁铁",准备吸引周围的数据点。 -
迭代优化 (核心): 算法的核心就在于这个不断重复直到收敛的循环过程,它包含两个交替的子步骤:
- 分配阶段 (Assignment Step / E-step): 对于数据集中的每一个数据点,计算它到所有
K个质心的距离(通常采用欧氏距离)。然后,将该数据点分配给距离它最近的那个质心所代表的簇。每个点都找到了自己的"归属"。 - 更新阶段 (Update Step / M-step): 在所有数据点都被分配到簇之后,算法会重新计算每个簇的质心。新的质心是该簇内所有数据点坐标的平均值(即该簇的几何中心)。这就像磁铁移动到了它所吸引到的所有点的中心位置。
- 分配阶段 (Assignment Step / E-step): 对于数据集中的每一个数据点,计算它到所有
-
收敛判断: 重复分配和更新阶段,直到质心的位置不再发生显著变化(即质心移动的距离小于某个阈值),或者达到预设的最大迭代次数。此时,算法认为已经找到了一个相对最优的聚类结果。
通过这个迭代过程,数据点和质心会互相"吸引"和"调整",最终形成紧密的簇。
手把手代码实现:搭建你的 K-Means 核心模块
理解了理论,是时候将它变为现实了!我们将使用 Python 从零开始构建一个简易的 K-Means 算法。为了让代码量更少、结构更清晰,我们将其分解为几个核心函数。
首先,我们需要一些模拟数据来测试我们的算法。sklearn 的 make_blobs 函数是生成聚类数据的绝佳工具。
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
# 1. 数据准备:生成一些示例数据
# n_samples: 样本数量,这里是 300 个点
# centers: 簇的数量,我们希望它分成 4 个簇
# cluster_std: 每个簇的标准差,影响簇的紧密程度
# random_state: 随机种子,保证每次运行结果一致
X, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.60, random_state=0)
# 可视化原始数据点分布,看看它们长什么样
plt.figure(figsize=(8, 6))
plt.scatter(X[:, 0], X[:, 1], s=50, alpha=0.8)
plt.title("原始数据点分布")
plt.xlabel("特征1")
plt.ylabel("特征2")
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()
运行上述代码,你将看到一个由四个松散聚类组成的二维散点图。这就是我们的"混沌数据",等待被 K-Means 算法"炼化"!接下来,我们将一步步实现 K-Means 的核心逻辑。
python
# 2. 核心函数实现:K-Means的"心脏"
def euclidean_distance(point1, point2):
"""
功能:计算两个点(Numpy数组)之间的欧氏距离。
用途:衡量数据点与质心之间的"远近"。
"""
return np.sqrt(np.sum((point1 - point2)**2))
def initialize_centroids(data, k):
"""
功能:从数据集中随机选择K个点作为初始质心。
用途:K-Means算法的第一步,为迭代过程提供起点。
"""
indices = np.random.choice(data.shape[0], k, replace=False) # 随机选择K个不重复的索引
return data[indices]
def assign_to_clusters(data, centroids):
"""
功能:根据当前质心,将每个数据点分配到最近的簇。
用途:实现K-Means的"分配阶段"。
"""
assignments = np.zeros(data.shape[0], dtype=int) # 存储每个数据点所属的簇索引
for i in range(data.shape[0]):
distances = [euclidean_distance(data[i], centroid) for centroid in centroids]
assignments[i] = np.argmin(distances) # 找到距离最近的质心索引
return assignments
def update_centroids(data, assignments, k):
"""
功能:根据新的簇分配结果,重新计算每个簇的质心。
用途:实现K-Means的"更新阶段"。
"""
new_centroids = np.zeros((k, data.shape[1]))
for i in range(k):
# 找到属于当前簇i的所有数据点
points_in_cluster = data[assignments == i]
if len(points_in_cluster) > 0: # 避免空簇导致计算NaN
new_centroids[i] = np.mean(points_in_cluster, axis=0) # 计算平均值作为新质心
else:
# 如果出现空簇,可以重新随机选择一个点作为质心,这里简化为保持原位
new_centroids[i] = centroids[i] # 或者从数据集中随机选择一个点
return new_centroids
def run_kmeans(data, k, max_iterations=100, tolerance=1e-4):
"""
功能:K-Means算法的主循环函数。
用途:整合所有步骤,执行聚类过程直到收敛。
"""
# 步骤1: 初始化质心
centroids = initialize_centroids(data, k)
for iteration in range(max_iterations):
# 步骤2: 分配数据点到最近的簇
assignments = assign_to_clusters(data, centroids)
# 步骤3: 更新质心
new_centroids = update_centroids(data, assignments, k)
# 步骤4: 检查收敛性 (质心是否还在移动)
# 计算新旧质心之间的最大距离
max_centroid_shift = np.max([euclidean_distance(centroids[i], new_centroids[i]) for i in range(k)])
centroids = new_centroids # 更新质心
# 如果质心移动距离小于容忍度,则认为收敛
if max_centroid_shift < tolerance:
print(f"K-Means在第 {iteration + 1} 次迭代后收敛。")
break
else:
print(f"K-Means达到最大迭代次数 {max_iterations} 仍未完全收敛。")
return assignments, centroids
深度实践:运行与解读你的 K-Means 算法 📊
现在,我们已经具备了 K-Means 的所有核心模块。让我们用之前准备好的数据来运行它,并观察结果!我们将 K 设定为 4,这与我们生成数据时的簇数量一致,看看我们的算法能否准确地找到它们。
python
# 3. 运行 K-Means 并可视化结果
k_clusters = 4 # 我们知道数据有4个簇
final_assignments, final_centroids = run_kmeans(X, k=k_clusters)
# 可视化聚类结果
plt.figure(figsize=(10, 8))
# 使用不同的颜色来区分不同的簇
colors = ['red', 'green', 'blue', 'purple', 'orange', 'brown', 'pink', 'gray']
for i in range(k_clusters):
# 绘制属于当前簇的数据点
plt.scatter(X[final_assignments == i, 0], X[final_assignments == i, 1],
s=50, label=f'Cluster {i+1}', color=colors[i % len(colors)], alpha=0.7)
# 绘制当前簇的质心
plt.scatter(final_centroids[i, 0], final_centroids[i, 1],
s=200, marker='X', color='black', edgecolor='white', linewidth=2, label=f'Centroid {i+1}')
plt.title(f"K-Means 聚类结果 (K={k_clusters})")
plt.xlabel("特征1")
plt.ylabel("特征2")
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()
运行上述代码后,你会看到原始杂乱的数据点被清晰地划分成四个不同的颜色区域,并且每个区域中心都有一个黑色的"X"标记,那就是我们算法找到的质心!是不是很有趣?🎉 这说明我们的"手撕"K-Means算法成功地从数据中发现了隐藏的结构!
实践的深度思考:
- 初始化的影响: 仔细观察
initialize_centroids函数,我们是随机选择 K 个点。这种随机性意味着每次运行 K-Means,初始质心可能不同,从而可能导致最终的聚类结果(特别是质心的确切位置和簇的形成)略有差异,甚至可能陷入局部最优。这就是 K-Means 的一个"玄学"之处,也是我们后续文章中将要探索的优化方向,比如 K-Means++。 - 收敛的意义: 我们的
run_kmeans函数包含了tolerance参数来判断收敛。当质心不再显著移动时,说明算法已经找到了一个相对稳定的聚类结构。理解这一点对于避免不必要的迭代和评估算法效率至关重要。 - 为何"物以类聚": 核心在于"距离"的计算。欧氏距离确保了相似的点被分配到一起,而质心的更新则是不断调整簇的"中心",使其能更好地代表其成员。
小结与展望:数据炼金之旅的开端 📖
恭喜你!🎉 通过今天的实践,你不仅理解了 K-Means 聚类算法的核心原理,更重要的是,你亲手实现了一个 K-Means 的完整版本!这种从零开始的体验,让你对每一个步骤都有了真切的感受,不再仅仅是"调包侠"。
这仅仅是K-Means深度探索系列的第一步。在接下来的文章中,我们将继续深入,探讨 K 值选择的艺术与科学、K-Means++等更智能的初始化策略、MiniBatch K-Means 在大数据场景下的应用,以及 K-Means 的局限性与实际案例分析。