机器学习-聚类算法K-Means
机器学习-聚类算法K-Means
在机器学习领域中,通过输入数据有无标签可以划分为有监督学习和无监督学习。举个例子,给一张猫的照片同时配一句话“这是一个猫”,这就是有标签,是有监督学习。
如果我们没有现成的标签,该如何让机器理解数据?答案就是聚类,聚类算法用一句话来形容就是“簇内相似度高、簇间相似度低”,而K-Means作为最经典的聚类算法,也是本贴的重点。
1.聚类算法概述
聚类算法可以大致分为以下四大类,详见下表。
| 类别 | 代表算法 | 核心思想 | 适用场景 |
|---|---|---|---|
| 基于划分 | K-Means | 预设 K K K 个中心,通过迭代优化距离。 | 数据集大小中等,簇呈球形。 |
| 基于层次 | AGNES / DIANA | 像树状图一样层层合并或拆分点。 | 需要查看不同层级的聚类关系。 |
| 基于密度 | DBSCAN | 只要点足够“挤”就划分为一类,能识别噪音。 | 形状不规则(如环形)的数据。 |
| 基于概率模型 | GMM (高斯混合模型) | 假设数据是由多个高斯分布组合而成。 | 数据之间存在重叠,需要软分类。 |
2.K-Means算法流程
K-Means 的算法流程可以概括为以下五个步骤:
S1. Determine the Number of Clusters(K).首先确定数据中的聚类数量K。
S2. Initinalize K Centroids.初始化K个聚类中心。
S3. Reassign Points.将数据中的每一个点分配到距离(一般为欧式距离)它最近的聚类中心。
S4. Update Centroids.将每个聚类中心的位置重新计算为分配给该聚类的所有点的均值。
S5. Repeat Until Convergence.重复步骤3和步骤4,直到聚类中心不再移动。
此链接https://www.naftaliharris.com/blog/visualizing-k-means-clustering/是可视化展示K-means算法的,我做一个案例展示:
S1. 初始化聚类中心的方式选择随机初始化,点击Randomly。

S2. 数据集选择Packed Circles,点击即可。

S3. 我们划分簇类设置K=4,点击”Add Centroid“按钮四次,再点击"go"。(簇类中心是随机的,所以我们的不一定一样)


S4. 接着更新簇类中心,点击Update Centroids按钮,再点击”Reassign Points“。


S5. 反复点击“Update Centroids”和”Reassign Points“,直到不再发生变化。如下图可以看到我的这个还是划分的比较均匀的。点击Restart可以尝试其余数据集或者初始化划分K的方法或者K的个数。

3.K-Means常见问题
Q1:K-Means算法最后聚类结果唯一吗?
- 不唯一,取决于初始质心的位置。上面的案例选择的是随机初始化(Randomly)。初始化质心的策略还有最远点启发式算法(Farthest Point)。
Q2:K-Means算法优缺点是什么?
- 优缺点各列了三个在下表。
| 优点 | 缺点 |
|---|---|
| 简单易懂:算法逻辑直观,容易实现。 | 对异常值敏感:一个极端的离群点会大幅拉动质心。 |
| 计算效率高:处理大规模数据集时速度较快。 | 局部最优:初始点选得不好,可能导致结果不理想。 |
| 解释性强:聚类结果非常容易可视化。 | 无法处理非球形簇:对于形状怪异(如环形)的数据表现不佳。 |
Q3:K-Means算法的应用场景有哪些?
- 商业领域用于将用户分为不同群体,图像处理用于图像分割与压缩,搜索引擎用于新闻或文档自动分类。
Q4:K-Means 结果不稳定,每次运行都不一样怎么办?
- 原因: 原始算法随机初始化质心,如果运气不好选到了聚集在一起的点,结果就会陷入“局部最优”。
- 解决方案: 首先使用 K-Means++ 初始化技术(这是 Scikit-learn 的默认选项)。 接着增加 n_init 参数,让算法多跑几次,选误差最小的那次。
Q5:数据里有明显的离群点(Outliers),会对聚类有影响吗?
- 影响: 非常大!因为 K-Means 是基于“均值”更新质心的,一个极远处的异常值会像拔河一样把质心强行拉过去。
- 解决方案: 首先在聚类前进行异常值检测并剔除。 接着考虑使用 K-Medoids 算法,它用中位数点替代均值点,鲁棒性更强。
Q6:如果我的数据特征维度(单位)不一样可以吗?
- 警告: 不可以!K-Means 依赖欧几里得距离。如果特征 A 的范围是 1-10(如年龄),特征 B 是 10000-100000(如年薪),算法会完全忽略年龄的影响。
- 解决方案: 在聚类前必须进行数据标准化(Standardization),如使用 StandardScaler让所有特征回到同一数量级。
4.K-means实战案例
我们将通过模拟300个点,划分为4簇来演示 K-Means 如何自动识别数据簇。
python代码如下:
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import os
# 确保输出目录存在
output_dir = "output_images"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# --- 数据准备,选择了300个点,划分为4类---
X, y = make_blobs(n_samples=300, centers=4, cluster_std=0.60, random_state=0)
# --- 图 1: 原始数据 ---
plt.figure(figsize=(8, 6))
plt.scatter(X[:, 0], X[:, 1], s=50, color='gray')
plt.title("Original unclassified data")
# 保存图片,dpi=300 保证清晰度,bbox_inches='tight' 防止文字被切断
plt.savefig(f"{output_dir}/1_raw_data.png", dpi=300, bbox_inches='tight')
print(f"图片已保存: {output_dir}/1_raw_data.png")
plt.close() # 关闭当前图像,释放内存,防止下一张图重叠
# --- K-Means 聚类 ---
kmeans = KMeans(n_clusters=4, init='k-means++', max_iter=300, n_init=10, random_state=0)
y_kmeans = kmeans.fit_predict(X)
centroids = kmeans.cluster_centers_
# --- 图 2: 聚类结果 ---
plt.figure(figsize=(8, 6))
# 绘制不同聚类的点
plt.scatter(X[y_kmeans == 0, 0], X[y_kmeans == 0, 1], s=50, c='lightgreen', label='Cluster 1')
plt.scatter(X[y_kmeans == 1, 0], X[y_kmeans == 1, 1], s=50, c='orange', label='Cluster 2')
plt.scatter(X[y_kmeans == 2, 0], X[y_kmeans == 2, 1], s=50, c='lightblue', label='Cluster 3')
plt.scatter(X[y_kmeans == 3, 0], X[y_kmeans == 3, 1], s=50, c='magenta', label='Cluster 4')
# 绘制质心
plt.scatter(centroids[:, 0], centroids[:, 1], s=200, marker='x', c='red', label='Centroids')
plt.title("K-Means Clustering Results")
plt.legend()
plt.grid(True)
# 保存图片
plt.savefig(f"{output_dir}/2_kmeans_result.png", dpi=300, bbox_inches='tight')
print(f"图片已保存: {output_dir}/2_kmeans_result.png")
plt.close()
print("所有图片生成完毕!")
展示的两张图片如下:


进一步思考:我怎么知道数据应该分几类?其实我们可以运行一遍算法,看看不同 K K K 值下的簇内误差平方和(Inertia/SSE)。就像折手肘一样,下降最剧烈的那个拐点,就是性价比最高的结果。
代码和效果如下
import os
import urllib.request
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
# 字体文件路径
font_path = 'SimHei.ttf'
# 如果字体不存在,则下载
if not os.path.exists(font_path):
print("正在下载 SimHei 字体以解决中文乱码问题...")
url = "https://github.com/StellarCN/scp_zh/raw/master/fonts/SimHei.ttf"
try:
urllib.request.urlretrieve(url, font_path)
print("字体下载完成。")
except Exception as e:
print(f"字体下载失败: {e}")
# 配置 Matplotlib 使用该字体
fm.fontManager.addfont(font_path)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 1. 生成一组带干扰的数据(设想我们不知道它有几个簇)
X, _ = make_blobs(n_samples=300, centers=4, cluster_std=0.7, random_state=0)
# 2. 计算不同 K 值的 Inertia(簇内误差平方和)
inertia = []
k_range = range(1, 11)
for k in k_range:
kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=0)
kmeans.fit(X)
inertia.append(kmeans.inertia_) # inertia_ 是 scikit-learn 自动计算的误差和
# 3. 绘制手肘图
plt.figure(figsize=(12, 5))
# 子图1:手肘曲线
plt.subplot(1, 2, 1)
plt.plot(k_range, inertia, 'bo-')
plt.xlabel('K值 (聚类数量)')
plt.ylabel('Inertia (簇内误差平方和)')
plt.title('手肘法确定最优 K 值')
plt.annotate('手肘拐点', xy=(4, inertia[3]), xytext=(5, inertia[3]+2000),
arrowprops=dict(facecolor='black', shrink=0.05))
# 4. 根据手肘图选择 K=4 进行最终聚类
plt.subplot(1, 2, 2)
final_model = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=0)
y_pred = final_model.fit_predict(X)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, s=30, cmap='viridis')
plt.scatter(final_model.cluster_centers_[:, 0], final_model.cluster_centers_[:, 1],
s=150, c='red', marker='X', label='质心')
plt.title('最终聚类结果 (K=4)')
plt.legend()
plt.tight_layout()
plt.savefig('kmeans_elbow_method.png',bbox_inches='tight') # 保存为图片

纸上得来终觉浅,绝知此事要躬行。K-Means 算法的原理并不复杂,亲手运行一遍代码印象更深。欢迎评论区留言,我们一起探讨。
5.扩展:西瓜书是如何讲解聚类算法
我不建议一开始学聚类算法一行行啃书,有点费时费力。其实只要看几篇博客加上看点讲解视频知道大致意思,跑通一个实战案例就可以了。下面思维导图只能算是我的一个阅读笔记。

更多推荐

所有评论(0)