Python K-Means聚类算法原理与实战详解

小林子
小林子
发布时间:2026-10-13| 5人浏览过
原创

本文深入解析Python中K-Means聚类算法的核心原理。从无监督学习的概念入手,详细阐述如何通过随机初始化质心、计算欧式距离及迭代更新中心点来划分数据簇。同时介绍利用误差平方和确定最佳K值的方法,并提供完整的Python代码实现与不同K值下的可视化结果对比,帮助读者掌握聚类分析的关键技术。

K-Means算法概述

一、概述

首先需要介绍无监督学习的概念。所谓无监督学习,是指训练样本中没有标记信息,其目标是通过学习无标记的训练样本来揭示数据的内在性质及规律。通俗地说,就是根据数据的内在性质,挖掘其隐含的规律。在这一类算法中,应用最为广泛的是“聚类”算法。

聚类算法的主要作用包括数据归约,即在尽可能保证数据完整性的前提下减少数据量级,以便后续处理;此外,也可以直接对聚类结果进行应用或分析。

K-Means算法是聚类算法中较为基础且常见的一种。

K-Means算法原理示例

二、从样例开始

假设我们在二维平面上有一组数据点。


   x         y
1.658985    4.285136
-3.453687    3.424321
4.838138    -1.151539
-5.379713    -3.362104
0.972564    2.924086
-3.567919    1.531611
0.450614    -3.302219
-3.487105    -1.724432
2.668759    1.594842
-3.156485    3.191137
3.165506    -3.999838
-2.786837    -3.099354
4.208187    2.984927
-2.123337    2.943366
0.704199    -0.479481
-0.392370    -3.963704
2.831667    1.574018
-0.790153    3.343144
2.943496    -3.357075
...

这些点在二维平面上的分布大致如下:

观察这些点,可以隐约看出它们分成了4个“簇”。因此,我们可以假定目标是将数据分成4个簇(尽管实际上也可能分为其他数量,如5个)。这里的“4个簇”是基于视觉判断得出的,而在实际应用中,这一数值应由算法计算得出,下文将详细介绍。

确定4个“簇”后,下一步是找出每个簇的中心。虽然人类可以“看出”大概的中心位置,但计算机无法直接感知。计算机通过向量距离(即向量相似性)来确定中心。相似性的计算方法有多种,例如欧氏距离、曼哈顿距离、切比雪夫距离等。

本文采用欧氏距离,它反映的是空间中两点之间的直线距离。

基于上述原理,我们可以让计算机计算出4个“簇”。

主要步骤如下:首先随机生成4个点,假设这4个点即为4个簇的中心。接着计算平面中每个点到这4个中心点的距离,并将每个点分配给距离最近的那个中心点。

此时,我们完成了第一步,即将平面中的所有点分成了4个簇。然而,由于初始中心点是随机生成的,这样划分出的4个簇显然不是最优结果。解决方法如下:

现有4个簇,根据每个簇中所有点的坐标计算出每个簇的新中心点。这个新中心点比旧的初始中心点更优,因为它更接近簇内所有点的几何中心。随后,使用新中心点重复第一步的操作,即重新计算所有点到新中心点的距离,并将点重新分配到4个新簇中。通过不断迭代,直到误差收敛到较小范围。

以上就是K-Means算法的基本流程。

关键知识点解析

三、知识点浅析

3.1 确定“簇”的个数

前文提到的分成4个簇,其中的“4”即为K-Means算法中的参数K。使用K-Means算法时,首先必须选择一个K值作为聚类的簇数。在上例中,K=4是我们主观“看”出来的,但在多数实际场景中,我们无法直观判断应该分为多少个簇。那么该如何确定呢?

我们可以从较小的K值开始尝试。使用简单的K-Means算法(即较少迭代次数,不追求最佳结果,只求快速)计算结果。然后计算每个点到其所属簇中心点的距离,并求和,该总和即为误差值。

接着增加K值,再次计算误差值。例如在上例中,我们可以从K=2开始,计算K值从2到7的K-Means算法误差值。 这样会得到类似如下的图表:

图中的“Error”代表K-Means的误差。当分成的“簇”越多时,误差通常会越来越小。

但是,“簇”越多越好吗?答案是否定的。有时“簇”过多反而不利于得到预期结果或进行后续操作。

因此,我们通常选择误差减小速度变得平缓的那个临界点,例如上图中的4。

可以发现,在分成4个簇之后,再增加簇的数量,误差并没有显著减少。而取4个簇也与我们的视觉观察相符。

3.2 欧氏距离

欧氏距离是常用的距离定义,指在m维空间中两个点之间的真实距离,计算公式如下:

而本例是在二维空间中,因此本例的计算公式简化为:

Python代码实现与结果分析

四、代码和结果

加载数据的代码使用了numpy库,先将数据加载为matrix类型。


import numpy as np

def loadDataSet(fileName):
  '''
  加载数据集
  :param fileName:
  :return:
  '''
  # 初始化一个空列表
  dataSet = []
  # 读取文件
  fr = open(fileName)
  # 循环遍历文件所有行
  for line in fr.readlines():
    # 切割每一行的数据
    curLine = line.strip().split('\t')
    # 将数据转换为浮点类型,便于后面的计算
    # fltLine = [float(x) for x in curLine]
    # 将数据追加到dataMat
    fltLine = list(map(float,curLine))  # 映射所有的元素为 float(浮点数)类型
    dataSet.append(fltLine)
  # 返回dataMat
  return np.matrix(dataSet)

接下来需要生成K个初始质点(即中心点)。这里采用随机生成的方法生成K个“簇”的初始中心。


def randCent(dataMat, k):
  '''
  为给定数据集构建一个包含K个随机质心的集合,
  随机质心必须要在整个数据集的边界之内,这可以通过找到数据集每一维的最小和最大值来完成
  然后生成0到1.0之间的随机数并通过取值范围和最小值,以便确保随机点在数据的边界之内
  :param dataMat:
  :param k:
  :return:
  '''
  # 获取样本数与特征值
  m, n = np.shape(dataMat)
  # 初始化质心,创建(k,n)个以零填充的矩阵
  centroids = np.mat(np.zeros((k, n)))
  # 循环遍历特征值
  for j in range(n):
    # 计算每一列的最小值
    minJ = min(dataMat[:, j])
    # 计算每一列的范围值
    rangeJ = float(max(dataMat[:, j]) - minJ)
    # 计算每一列的质心,并将值赋给centroids
    centroids[:, j] = np.mat(minJ + rangeJ * np.random.rand(k, 1))
  # 返回质心
  return centroids

欧氏距离计算函数。


def distEclud(vecA, vecB):
  '''
  欧氏距离计算函数
  :param vecA:
  :param vecB:
  :return:
  '''
  return np.sqrt(sum(np.power(vecA - vecB, 2)))

cost方法执行一个简化的K-Means,即较少次数的迭代,计算出其中的误差(即当前点到簇质心的距离,后续将使用该误差来评价聚类效果)。


def cost(dataMat, k, distMeas=distEclud, createCent=randCent,iterNum=300):
  '''
  计算误差的多少,通过这个方法来确定 k 为多少比较合适,这个其实就是一个简化版的 kMeans
  :param dataMat: 数据集
  :param k: 簇的数目
  :param distMeans: 计算距离
  :param createCent: 创建初始质心
  :param iterNum:默认迭代次数
  :return:
  '''
  # 获取样本数和特征数
  m, n = np.shape(dataMat)
  # 初始化一个矩阵来存储每个点的簇分配结果
  # clusterAssment包含两个列:一列记录簇索引值,第二列存储误差(误差是指当前点到簇质心的距离,后面会使用该误差来评价聚类的效果)
  clusterAssment = np.mat(np.zeros((m, 2)))
  # 创建质心,随机K个质心
  centroids = createCent(dataMat, k)
  clusterChanged = True
  while iterNum > 0:
    clusterChanged = False
    # 遍历所有数据找到距离每个点最近的质心,
    # 可以通过对每个点遍历所有质心并计算点到每个质心的距离来完成
    for i in range(m):
      minDist = np.inf
      minIndex = -1
      for j in range(k):
        # 计算数据点到质心的距离
        # 计算距离是使用distMeas参数给出的距离公式,默认距离函数是distEclud
        distJI = distMeas(centroids[j, :], dataMat[i, :])
        # print(distJI)
        # 如果距离比minDist(最小距离)还小,更新minDist(最小距离)和最小质心的index(索引)
        if distJI < minDist:
          minDist = distJI
          minIndex = j
      # 更新簇分配结果为最小质心的index(索引),minDist(最小距离)的平方
      clusterAssment[i, :] = minIndex, minDist ** 2
      iterNum -= 1;
    # print(centroids)
    # 遍历所有质心并更新它们的取值
    for cent in range(k):
      # 通过数据过滤来获得给定簇的所有点
      ptsInClust = dataMat[np.nonzero(clusterAssment[:, 0].A == cent)[0]]
      # 计算所有点的均值,axis=0表示沿矩阵的列方向进行均值计算
      centroids[cent, :] = np.mean(ptsInClust, axis=0)
  # 返回给定迭代次数后误差的值
  return np.mat(clusterAssment[:,1].sum(0))[0,0]

最后可以调用K-Means算法进行正式计算。


def kMeans(dataMat, k, distMeas=distEclud, createCent=randCent):
  '''
  创建K个质心,然后将每个店分配到最近的质心,再重新计算质心。
  这个过程重复数次,直到数据点的簇分配结果不再改变为止
  :param dataMat: 数据集
  :param k: 簇的数目
  :param distMeans: 计算距离
  :param createCent: 创建初始质心
  :return:
  '''
  # 获取样本数和特征数
  m, n = np.shape(dataMat)
  # 初始化一个矩阵来存储每个点的簇分配结果
  # clusterAssment包含两个列:一列记录簇索引值,第二列存储误差(误差是指当前点到簇质心的距离,后面会使用该误差来评价聚类的效果)
  clusterAssment = np.mat(np.zeros((m, 2)))
  # 创建质心,随机K个质心
  centroids = createCent(dataMat, k)
  # 初始化标志变量,用于判断迭代是否继续,如果True,则继续迭代
  clusterChanged = True
  while clusterChanged:
    clusterChanged = False
    # 遍历所有数据找到距离每个点最近的质心,
    # 可以通过对每个点遍历所有质心并计算点到每个质心的距离来完成
    for i in range(m):
      minDist = np.inf
      minIndex = -1
      for j in range(k):
        # 计算数据点到质心的距离
        # 计算距离是使用distMeas参数给出的距离公式,默认距离函数是distEclud
        distJI = distMeas(centroids[j, :], dataMat[i, :])
        # 如果距离比minDist(最小距离)还小,更新minDist(最小距离)和最小质心的index(索引)
        if distJI < minDist:
          minDist = distJI
          minIndex = j
      # 如果任一点的簇分配结果发生改变,则更新clusterChanged标志
      if clusterAssment[i, 0] != minIndex: clusterChanged = True
      # 更新簇分配结果为最小质心的index(索引),minDist(最小距离)的平方
      clusterAssment[i, :] = minIndex, minDist ** 2
    # print(centroids)
    # 遍历所有质心并更新它们的取值
    for cent in range(k):
      # 通过数据过滤来获得给定簇的所有点
      ptsInClust = dataMat[np.nonzero(clusterAssment[:, 0].A == cent)[0]]
      # 计算所有点的均值,axis=0表示沿矩阵的列方向进行均值计算
      centroids[cent, :] = np.mean(ptsInClust, axis=0)
  # 返回所有的类质心与点分配结果
  return centroids, clusterAssment

选取不同的K值对结果影响有多大?我们通过以下K值为2到6的效果来观察。 图中红色方块代表“簇”的中心点,每个“簇”所属的点用不同颜色表示。

K = 2

K = 3

K = 4

K = 5

K = 6

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。