K-均值算法的原理与实战

article/2025/8/31 16:01:28

K-均值(K-means)算法是一种常用的聚类算法。

  当我们需要对未标记的数据划分类别时,往往用到的算法是聚类(clustering)。聚类是一种无监督的学习,它试图将数据集中的样本划分为若干个通常是不相交的子集,每个子集称为一个“簇”(cluster);相似的样本归到一个簇中,不相似的样本分到不同的簇中。
  K-均值(K-means)算法是一种聚类算法,k 是用户指定的簇个数,每个簇通过其质心(centroid),即簇中所有点的中心来描述。

算法原理

  给定样本集 D D D = { x 1 , x 2 , . . . , x m x_1,x_2,...,x_m x1,x2,...,xm}, k k k 均值算法将样本集 D D D划分为 k k k个互不相交的簇 C C C = { C 1 , C 2 , . . . , C k C_1,C_2,...,C_k C1,C2,...,Ck},划分的依据是样本的相似度,相似的样本会归入同一簇,不相似的样本会归入不同簇。
  相似度的计算方法有多种,常见的有曼哈顿距离,欧几里得距离,余弦相似度等。 k k k 均值算法使用的是欧几里得距离(Euclidean Distance),即两点间的真实距离。给定样本 x i = ( x i 1 , x i 2 , . . . , x i n ) x_i = (x_{i1},x_{i2},...,x_{in}) xi=(xi1,xi2,...,xin) x j = ( x j 1 , x j 2 , . . . , x j n ) x_j = (x_{j1},x_{j2},...,x_{jn}) xj=(xj1,xj2,...,xjn),计算样本间的欧几里得距离的公式如下:
d i s t e d ( x i , x j ) = ∣ ∣ x i − x j ∣ ∣ 2 = ∑ u = 1 n ∣ x i u − x j u ∣ 2 dist_{ed}(x_i,x_j) = ||x_i-x_j||_2 = \sqrt{\sum_{u=1}^{n}{|x_{iu}-x_{ju}|}^2} disted(xi,xj)=xixj2=u=1nxiuxju2

   k k k 均值算法的流程是这样的。第一步,从数据集中随机选择 k k k个点作为簇质心。第二步,将样本集中的每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的簇。第三步,每个簇的质心更新为该簇所有点的平均值。重复上述步骤二与步骤三,直到所有的簇不再发生变化。该过程的伪代码表示如下:


输入: 样本集 D D D = { x 1 , x 2 , . . . , x n x_1,x_2,...,x_n x1,x2,...,xn};
       聚类簇数 k k k
过程:

  1. D D D中随机选择 k k k个样本作为初始均值向量{ u 1 , u 2 , . . . , u k u_1,u_2,...,u_k u1,u2,...,uk}
  2. repeat
  3.   令 C i = ∅ ( 1 ≤ i ≤ k ) C_i = \emptyset (1 \leq i \leq k) Ci=(1ik)
  4.   for j = 1 , 2 , . . . , m j = 1,2,...,m j=1,2,...,m do
  5.     计算样本 x i x_i xi与各均值向量 u i u_i ui( 1 ≤ i ≤ k ) 1 \leq i \leq k) 1ik)的距离: d j i = ∣ ∣ x j − u i ∣ ∣ 2 d_{ji}=||x_j-u_i||_2 dji=xjui2
  6.     根据距离最近的均值向量确定 x i x_i xi的簇标记: λ i = a r g m i n i ∈ { 1 , 2 , . . . , k } d j i \lambda_i = argmin_{i \in {\{1,2,...,k\}}} d_{ji} λi=argmini{1,2,...,k}dji;
  7.     将样本 x j x_j xj划入相应的簇: C λ j ⋃ { x j } C_{\lambda_j}\bigcup\{x_j\} Cλj{xj}
  8.   end for
  9.   for i = 1 , 2 , . . . , k i = 1,2,...,k i=1,2,...,k do
  10.     计算新均值向量: u i ′ = 1 ∣ C i ∣ ∑ x ∈ C i x u_i^{'} = \frac{1}{|C_i|}\sum_{x \in C_i}x ui=Ci1xCix
  11.     if u i ′ ≠ u i u_i^{'} \neq u_i ui=ui then
  12.       将当前均值向量 u i u_i ui更新为 u i ′ u_i^{'} ui
  13.     else
  14.       保持当前均值向量不变
  15.     end if
  16.   end for
  17. until 所有样本的簇标记均未更新

输出: 簇划分 C = { C 1 , C 2 , . . . , C k } C = \{C_1,C_2,...,C_k\} C={C1,C2,...,Ck}


  簇划分完成后,如何评价聚类效果的好与坏呢?一种用于度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和),SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。
S S E = ∑ i = 1 k ∑ x ∈ C i ∣ ∣ x − u i ∣ ∣ 2 2 SSE = \sum_{i=1}^k\sum_{x\in C_i}||x-u_i||_2^2 SSE=i=1kxCixui22

其中 u i = 1 ∣ C i ∣ ∑ x ∈ C i x u_i = \frac{1}{|C_i|}\sum_{x \in C_i}x ui=Ci1xCix 是簇 C i C_i Ci的均值向量。该式在一定程度上刻画了簇内样本围绕簇均值向量的紧密程度, S S E SSE SSE值越小则簇内样本相似度越高。

编程实战

  首先从百度网盘下载测试数据集,下载链接:https://pan.baidu.com/s/1wShFlsq36Fez5-Qh10Jpog ,提取码:ltaw。然后在Jupyter Notebook中运行以下代码。

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
#文本文件导入矩阵
def loadDataSet(fileName):dataMat = []fr = open(fileName)for line in fr.readlines():curLine = line.strip().split('\t') #当前行切割为列表fltLine = list(map(float,curLine)) #转换元素的数据类型为floatdataMat.append(fltLine)return np.mat(dataMat)#计算两个向量的欧氏距离
def distED(vecA, vecB):return np.sqrt(np.sum(np.power(vecA - vecB, 2))) #构建k个初始簇质心
def randCent(dataSet, k):m = np.shape(dataSet)[0] #总样本数centIdx = set()   #随机样本索引while len(centIdx) < k:randj = np.random.randint(m)centIdx.add(randj)centroids = dataSet[list(centIdx)] #初始簇质心return centroids#K-均值聚类方法
def kMeans(dataSet, k, distMeas=distED, createCent=randCent):m = np.shape(dataSet)[0]  #样本总数clusterAssment=np.mat(np.zeros((m,2))) #簇分配结果矩阵centroids=createCent(dataSet,k) #构建k个初始随机质心clusterChanged=True  #簇变化标志while clusterChanged:  #直到簇不再变化clusterChanged=Falsefor i in range(m): #遍历所有样本点minDist=np.inf  #样本点离最近质心的距离minIndex=-1  #样本的簇标记for j in range(k): #遍历所有的质心distJI = distMeas(centroids[j,:],dataSet[i,:]) #样本点I到质心J的距离if distJI < minDist:minDist=distJIminIndex=j if clusterAssment[i,0] != minIndex: clusterChanged = True  #样本点的簇标记发生变化clusterAssment[i,:]= minIndex,minDist**2  #簇分配结果矩阵的两列分别为簇标记和误差for cent in range(k): #更新质心的位置ptsInClust = dataSet[np.nonzero(clusterAssment[:,0].A == cent)[0]] #给定簇下的所有样本点centroids[cent,:] = np.mean(ptsInClust, axis=0) #axis=0表示沿列方向计算均值return centroids,clusterAssment#绘制所有样本点与质心
def drawPic(dataMat,centroids,clusterAssment):type1_x=[];type1_y=[];type2_x=[];type2_y=[];type3_x=[];type3_y=[];type4_x=[];type4_y=[]for i in range(dataMat.shape[0]):if clusterAssment[i,0]==0:type1_x.append(dataMat[i,0])type1_y.append(dataMat[i,1])elif clusterAssment[i,0]==1:type2_x.append(dataMat[i,0])type2_y.append(dataMat[i,1])elif clusterAssment[i,0]==2:type3_x.append(dataMat[i,0])type3_y.append(dataMat[i,1])elif clusterAssment[i,0]==3:type4_x.append(dataMat[i,0])type4_y.append(dataMat[i,1])cent_x=centroids[:,0].T.tolist()[0]cent_y=centroids[:,1].T.tolist()[0]p1 = plt.scatter(type1_x,type1_y,s=30,marker='x')p2 = plt.scatter(type2_x,type2_y,s=30,marker='x')p3 = plt.scatter(type3_x,type3_y,s=30,marker='x')p4 = plt.scatter(type4_x,type4_y,s=30,marker='x')cent = plt.scatter(cent_x,cent_y,s=200,marker='o')plt.legend(["p1","p2","p3",'p4','cent'],bbox_to_anchor=(1, 1))plt.show()#加载数据集
dataMat=loadDataSet('kMeansTestSet.txt') 
#生成聚类结果(簇质心和簇分配结果)
myCentroids, myClusterAssment = kMeans(dataMat, 4) 
#绘制聚类结果图
drawPic(dataMat, myCentroids, myClusterAssment) 

  最终的聚类结果图如下,其中p1-p4为划分为4种类别的簇,cent为簇质心。
在这里插入图片描述

结语

  聚类是一种无监督学习方法。所谓无监督学习是指事先并不知道要寻找的内容,即没有目标变量。聚类试图做到“物以类聚”,即将相似数据点归于同一簇,而不相似数据点归于不同簇。聚类中相似的概念取决于所选择的相似度计算方法。
  一种广泛使用的聚类算法是K-均值算法,其中k是用户指定的要创建的簇的数目。K-均值算法以 k k k个随机质心开始。算法会计算每个点到质心的距离,每个点会被分配到距其最近的簇质心。然后基于新分配到簇的点更新簇质心。以上过程重复数次,直到簇质心不再改变。
  K-均值算法非常简单有效,但是也容易受到初始簇质心的影响。算法只是收敛到局部最小值,而非全局最小值。一种聚类效果的评估指标是SSE,值越小表示数据点越接近质心,聚类效果也越好。其中一种比较笨的解决办法是,通过多次执行该算法,选择SSE值最小的那一个。
  为了克服K-均值算法只收敛到局部最小值的问题,下篇文章将会讲解聚类效果更好的二分K-均值(bisecting K-means)算法。


http://chatgpt.dhexx.cn/article/NW0LMbFW.shtml

相关文章

十分钟快速讲明白K均值聚类算法

K均值聚类算法 无监督学习 在一个典型的监督学习中&#xff0c;我们有一个有标签的训练集&#xff0c;我们的目标是找到能够区分正样本和负样本的决策边界&#xff0c;在这里的监督学习中&#xff0c;我们有一系列标签&#xff0c;我们需要据此拟合一个假设函数。 与此不同的…

深入浅出聚类算法之k-means算法

k-means是一个十分简单的聚类算法&#xff0c;它的思路非常简明清晰&#xff0c;所以经常拿来当做教学。下面就来讲述一下这个模型的细节操作。 内容 模型原理模型收敛过程模型聚类个数模型局限 1. 模型原理 将某一些数据分为不同的类别&#xff0c;在相同的类别中数据之间的…

c++函数调用

一个函数就是完成一项任务的独立代码块&#xff0c;它以名字作为整个代码块的代表 函数完成任务后&#xff0c;如果有数值需要返回&#xff0c;则在声明函数时&#xff0c;在函数名前写好返回值类型 如果不需要返回任何值&#xff0c;则在函数名前写 void 有时&#xff0c;一个…

c语言中函数调用的过程

一.程序在内存中的占用。 要学习C语言中函数调用的过程&#xff0c;必须要知道程序在内存中各个区域的分布。 C语言的函数调用的过程主要分布在栈中&#xff0c;所以我们今天主要研究栈。 二.几个基本的汇编指令。 call&#xff1a;1.将当前指令的下一条指令的地址保存到栈中…

函数定义和调用

<1>定义函数 定义函数的格式如下&#xff1a; def 函数名(): 代码 demo: 定义一个函数&#xff0c;能够完成打印信息的功能 def printInfo(): print(’------------------------------------’) print(’ 人生苦短&#xff0c;我用Python’) print(’----------------…

Shell函数调用

文章目录 一.函数基本格式二.函数调用2.1函数中调用2.2函数调用函数2.3外部调用2.4案例 三.总结 在shell脚本中&#xff0c;有些命令或者某些操作需要频繁的使用时&#xff0c;每次都重新写太过繁琐&#xff0c;这时我们就可以使用函数&#xff0c;当需要使用时&#xff0c;直接…

类的函数调用

父类和子类的函数调用 1.用指针&#xff08;引用&#xff09;调用函数的时候&#xff0c;被调用的函数取决于指针&#xff08;引用&#xff09;的类型&#xff1b; 2.涉及多态性时&#xff0c;采用虚函数和动态绑定&#xff0c;函数调用在运行时绑定&#xff0c;而非在编译时…

函数调用栈分析

进程在虚拟地址空间的布局 操作系统把磁盘上可执行文件加载到内存运行之前, 需要做很多工作, 其中很重要的一件事就是把可执行文件中的代码, 数据存放到内存 中合适的位置, 并分配和初始化程序运行过程中必须的堆栈, 所有准备工作完成之后操作系统才会调度程序起来运行. 进程…

C语言的函数调用过程

C语言的函数调用过程 先上一段代码 #include<stdio.h> int Add(int x, int y) {int z 0;z x y;return z; } #include <stdio.h> int main() {int a 10;int b 20;int c Add(a, b);return 0; } 123456789101112131415 这个函数计算两个数的值。接下来我们通…

C--函数调用

C函数的调用约定 编译器实现函数调用时所遵循的一系列规则称为函数的“调用约定&#xff08;Calling Convention&#xff09;” 对于C语言来说&#xff0c;运行在X86-64平台上的编译器基本都会根据操作系统的不同选择使用几种常见的调用约定。例如&#xff0c;在wiindows下通常…

C语言 函数调用的过程

例题&#xff1a;求两个整数中的较小者&#xff0c;用函数调用实现。 【代码实现】 int Min(int x, int y) {if (x < y){return x;}elsereturn y; } int main() {int Min(int x, int y);int a, b, c;printf("输入两个要比较的整数&#xff1a;\n");scanf("%…

函数调用的过程分析

一、函数调用机制 局部变量占用的内存是在程序执行过程中”动态”地建立和释放的。这种”动态”是通过栈由系统自动管理进行的。当任何一个函数调用发生时,系统都要作以下工作: 1)建立栈帧空间;2)保护现场: 主调函数运行状态和返回地址入栈&#xff1b;3)为被调函数传递数据(…

C/C++ 函数调用是如何实现的?

一、写在前面的话 C/C 函数调用方式与栈原理是 C/C 开发必须要掌握的基础知识&#xff0c;也是高级技术岗位面试中高频题。我真的真的真的建议无论是使用 C/C 的学生还是广大 C/C 开发者&#xff0c;都该掌握此回答中所介绍的知识。 如果你看不懂接下来第二部分在说什么&#…

函数调用过程

今天突然看到有人私信我说一直没写函数调用过程&#xff08;栈帧的形成和销毁过程&#xff09;这篇博文&#xff0c;赶紧补上。 刚看的栈帧内容时&#xff0c;我很迷惑&#xff0c;我觉得栈帧创建和销毁很麻烦&#xff0c;几句话根本说不完&#xff0c;而且我好像描述不清楚他…

浅谈函数调用!

导语 | 在任意一门编程语言中&#xff0c;函数调用基本上都是非常常见的操作&#xff1b;我们都知道&#xff0c;函数是由调用栈实现的&#xff0c;不同的函数调用会切换上下文&#xff1b;但是&#xff0c;你是否好奇&#xff0c;对于一个函数调用而言&#xff0c;其底层到底…

函数调用流程

函数调用模型 1. 函数调用流程函数调用流程分析函数参数调用代码分析自右向左入栈顺序的优点 2. 调用惯例函数参数的传递顺序和方式栈的维护方式调用管理表 3. 函数变量传递分析分析图 1. 函数调用流程 栈(stack)是现代计算机程序里最为重要的概念之一&#xff0c;几乎每一个程…

函数调用栈

函数调用栈 我们在编程中写的函数&#xff0c;会被编译器编译为机器指令&#xff0c;写入可执行文件&#xff0c;程序执行的时候&#xff0c;会把这个可执行文件加载到内存&#xff0c;在虚拟地址空间中的代码段存放。 如果在一个函数中调用另一个函数&#xff0c;编译器就会…

函数调用和使用

1.函数是什么 函数&#xff08;Function&#xff09;能实现的功能从简单到复杂&#xff0c;各式各样&#xff0c;但其本质是相通的&#xff1a;“喂”给函数一些数据&#xff0c;它就能内部消化&#xff0c;给你“吐”出你想要的东西。 2.定义和调用函数 2.1 定义函数 #…

C语言——函数的调用

函数的调用 传值调用 函数的形参和实参分别占有不同的内存块&#xff0c;对形参的修改不会影响实参。 传址调用 1.传址调用是把函数外部创建的变量的内存地址传递给函数参数的一种调用函数的方式 2.这种传参方式可以让函数和函数外边的变量建立起真正的联系&#xff0c;也就…

C语言之函数调用

C语言之函数调用 “温故而知新&#xff0c;可以为师矣”&#xff01; 让我们开启函数的道路吧&#xff01; 今天主要讲函数的调用方式&#xff01; 在讲之前&#xff0c;先回顾一下实际参数和形式参数的区别&#xff1b; 1.在定义函数时函数名后面括号中的变量名称为“形式参数…