K-Means聚类算法理论与代码实践

简介

K均值聚类,也叫做K-Means Clustering,是一种著名的用于分类问题的无监督机器学习聚类算法。聚类是针对给定的样本, 依靠它们特征的相似度或者距离,将其归到若干个‘类’或者’簇‘的数据分析问题。一个类就是给定样本集合的一个子集。所谓”人以类聚,物以群分“,常理来看,越相似的人更容易聚集在一起,在这里也是一样,相似的样本聚集在相同的类里面,不相似的样本分布在不同的类里。聚类属于无监督学习,因为只是根据样本特征之间的相似度或者距离来进行分类,而类或者簇事先并不知道,即这些类或者簇的个数以及对应的概念语义需要使用者来把握和命名。下图是对K-Means聚类算法执行过程的一个动图展示,图片来自维基百科

K-Means算法流程

聚类的基本概念

1. 相似度或距离度量方法

聚类的对象是观测数据或者样本集合。上面我们讲到相似的样本更大可能会聚集在相同的类或者簇里,但是如何来定义相似性呢?如果是判断两个人是否相似的话,我们一般会观察这2个人的身高,长相,性格,习惯等特征,以此来判断是否相似。同理,如果我们有n个样本,每个样本具有m个属性,假设我们能够计算出任意两个样本的每个属性之间的相似度(similariity)或者距离(distance),那么我们就可以根据这个值来判断这两个样本是否相似。因此,聚类的核心概念是相似度和距离,有很多相似度和距离的计算方式。因为相似度和距离直接影响到聚类的结果,所以选择适当的相似度和距离度量方法,是聚类的根本问题。
如果我们将样本集合看成是空间中的点的集合,以该空间的距离表示样本之间的相似度,那么常用的距离有闵可夫斯基距离,闵可夫斯基距离越大表示相似度越小,距离越小则相似度越大。

我们常用的欧氏距离,其实就是闵可夫斯基距离的一种。

给定样本集合X, Xm实数向量空间R^m中点的集合,其中x_i, x_j \in X, x_i = (x_{1i}, x_{2i},...,x_{mi})^T, x_j = (x_{1j}, x_{2j},...,x_{mj})^T, 那么样本x_i和样本x_j之间的闵可夫斯基距离定义为:
d_{ij} = (\sum_{k=1}^m |x_{ki} - x_{kj} |^p)^{ \frac {1}{p} }, 其中 p \ge 1
p = 2时,称为欧氏距离,即:
d_{ij} = (\sum_{k=1}^m |x_{ki} - x_{kj} |^2 ) ^ { \frac {1}{2} }
p = 1时,称为曼哈顿距离,即:
d_{ij} = \sum_{k=1}^m |x_{ki} - x_{kj}|
p = \inf时,称为切比雪夫距离,即取各个坐标数值差的绝对值的最大值:
d_{ij} = max_k(|x_{ki} - x_{kj}|)

最常用也是大家最熟悉的,莫过于欧氏距离。除了上述距离度量方式以外,还有马哈拉诺比斯距离相关系数夹角余弦等等。感兴趣的读者可以自行查阅资料。

2. 类或者簇的定义

通过上节讲的相似度或者距离的度量方式,我们现在可以计算两个样本之间的距离了,然后通过聚类可以得到类或者簇,本质都是样本的子集。如果一个聚类方法假定一个样本只能属于一个类,或者类的交集为空集,那么该方法称为硬聚类方法。否则,如果一个样本属于多个类,或者类的交集不为空集,那么该方法称为软聚类方法。用G表示类或者簇,用x_i, x_j表示类中的样本,用n_G表示G中样本的个数,用d_{ij}表示样本x_i, x_j之间的距离。
T为给定的正数,若集合G中任意两个样本x_i, x_j, 若d_{ij}满足:
d_{i,j} \leq T
那么称G为一个类或者簇。
得到了一个类或者簇,也就是得到了一个子集,那么如何来描述这个类呢,主要有以下几种方式:

  • 使用类中所有样本的均值\overline x_G,即类的中心
    \overline x_G = \frac {1}{n_G} \sum_{i=1}^{n_G} x_i
  • 使用类中任意两个样本之间的最大距离D_G,即
    D_G = max d_{ij}
  • 使用类的样本散布矩阵A_G和样本协方差矩阵S_G
    A_G = \sum_{i=1}^{n_G} (x_i - \overline x_G)(x_i - \overline x_G)^T \\ S_G = \frac {1}{m-1} \sum_{i=1}^{n_G} (x_i - \overline x_G)(x_i - \overline x_G)^T \\
    其中m为样本的维度(样本属性的个数)。

3. 类间距离的度量

如果我们现在已经有两个类G_p、G_q,分别包含n_p、n_q个样本,分别用\overline x_p、 \overline x_q来代表它们的均值,即类中心。那么我们如何衡量这两个类之间的距离呢?这里面也包含好几种定义,如下。

  • 最短距离或单连接
    定义类G_p的样本与类G_q的样本之间的最短距离为两类之间的距离
    D_{pq} = min(d_{ij} | x_i \in G_p, x_j \in G_q)
  • 最大距离或完全连接
    定义类G_p的样本与类G_q的样本之间的最长距离为两类之间的距离
    D_{pq} = max(d_{ij} | x_i \in G_p, x_j \in G_q)
  • 中心距离
    定义类G_p的类中心与类G_q的类中心之间的距离
    D_{pq} = d_{\overline x_p \overline x_q}

中心距离是我们比较常用的。

K-Means算法流程

在了解了聚类算法的相关基本概念之后,现在我们就可以正式介绍K-Means算法了。K-Means Clustering算法译为K均值聚类算法,它是基于样本集合划分的聚类算法。k均值聚类将样本集合划分为k个子集,构成k个类,将n个样本划分到k个类中,每个样本到其所属的类中心距离最短。并且每个样本只能属于一个类,故k均值聚类是硬聚类算法。K-均值算法归结为对样本集合X的划分,或者说从样本到类的函数的选择问题。K-均值聚类的策略是通过损失函数的最小化选取最优的划分或者函数。

1. 策略

我们首先统一一下相似度或距离的度量方式,在下文中统一采用欧氏距离作为样本之间的距离度量d(x_i, x_j)
d(x_i, x_j) = \sum_{k=1}^m (x_{ki} - x_{kj})^2 = ||x_i - x_j ||^ 2
接着,定义样本与其所属的类中心之间的距离的总和为损失函数,即:
W(C) = \sum_{l=1}^k \sum_{C(i)=l} || x_i - \overline x_l||^2
其中\overline x_l是第l个类的均值或者聚类中心,C(i)=l是指示函数,判断第i个样本是否属于第l个类或簇。因此K均值算法就变成了一个最优化问题:
C^* = argmin \sum_{l=1}^k \sum_{C(i)=l} || x_i - \overline x_l||^2
相似的样本总是被聚到同类中,此时的损失函数值最小,这个目标函数的最优化能达到聚类的目的。但是这是个组合问题,所有可能的分法是指数级别的,实际操作时,我们一般采用迭代的方法逐渐逼近问题的解。

算法流程

k-均值聚类算法是一个迭代的过程,每次迭代包括两个步骤。

  1. 首先选择k个类的中心,将样本逐渐指派到与其最相近的中心的类中,得到一个聚类结果。
  2. 接着计算每个类中的样本均值,将其作为新的聚类中心。

不断重复以上两个步骤,直到聚类中心不再发生改变。
下面给出算法具体步骤:
输入为n个样本集合X, 输出为样本的聚类C
(1)初始化时,令t=0,从样本集合中随机选择k个样本作为初始聚类中心m^{(0)} = (m_1^{(0)},m_2^{(0)},...,m_k^{(0)} )
(2)对样本进行聚类。对固定的类中心m^{(t)} = (m_1^{(t)},m_2^{(t)},...,m_k^{(t)} ),其中m_l^{(t)}G_l的中心,计算每个样本到所有类中心的距离,将每个样本指派到与其最近的类中心去,构成聚类结果C^{(t)}
(3)计算新的类中心。对每一轮的聚类结果C^{(t)},计算当前各个类中的样本的均值,作为新的类中心m^{(t)} = (m_1^{(t+1)},m_2^{(t+1)},...,m_k^{(t+1)} )
(4)如果步骤(3)中计算出来的聚类中心结果与步骤(2)一样,即聚类中心不再发生改变,那么算法停止,输出结果。否则t=t+1,返回步骤(2)继续执行。

k-均值聚类算法的复杂度为O(mnk),其中m是样本的维度,n是样本的个数,k是类或者簇的个数。

实例演示

假如我们有以下5个样本点(为了可视化,这里选择的是二维平面上的坐标点)的集合,我们需要通过K-均值聚类算法将其聚集到2个类中。原始数据点如下。

import matplotlib.pyplot as plt

x_point = [0,0,1,5,5]
y_point = [2,0,0,0,2]
label_y = ['k','k','k','k','k']

plt.scatter(x_point, y_point, c=label_y)
# 设置坐标轴范围
plt.xlim(-2,6)
plt.ylim(-2,6)

plt.grid()
plt.show()

原始数据点
接着,我们从样本点中随机选择两个点作为初始的聚类中心点m_1,m_2,这里假设我们选择的是x_1(0,2)和x_2(0,0)点,我们在图中将其标注出来,如下:
初始2个聚类中心
然后以m_1, m_2为聚类中心,形成两个类G_1, G_2。计算每个点到聚类中心的距离,有:
对于x_3而言,有d(x_3, m_1) = 2.23, d(x_3, m_2) = 1,故x_3被分到G_2
对于x_4而言,有d(x_4, m_1) = 5.38, d(x_4, m_2) = 5,故x_3被分到G_2
对于x_5而言,有d(x_5, m_1) = 5, d(x_5, m_2) = 5.38,故x_3被分到G_1
经过这一轮划分之后,得到了新的类G_1 = \{ x_1, x_5 \}, G_2 = \{ x_2, x_3, x_4 \} \。此时需要重新计算新的聚类中心m_1, m_2,有:
m_1 = (2.5, 2.0), m_2=(2, 0)
下面是新的样本分布图:
一次迭代之后
上图中画圈的是新的聚类中心,注意新计算的聚类中心不一定就在样本上了。
重复上述步骤,直到聚类中心不再改变,具体就不再赘述了。

sklearn库算法包

sklearn库中已经有包装好的K-Means聚类算法,下面来展示一下其用法。我们使用sklearn的聚类数据集生成器,生成一些聚类数据,然后对其使用K-Means算法。如下:

import matplotlib.pyplot as plt
import seaborn as sns; sns.set()  # for plot styling
import numpy as np

from sklearn.datasets.samples_generator import make_blobs
X, y_true = make_blobs(n_samples=100, centers=4,
                       cluster_std=0.60, random_state=0)
plt.scatter(X[:, 0], X[:, 1], s=50);
聚类数据

肉眼观察可以看到这实际上可以聚为4类,接下来我们使用K-Means算法来自动进行分类。

from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=4)
kmeans.fit(X)
y_kmeans = kmeans.predict(X)
y_kmeans

可视化一下:

plt.scatter(X[:, 0], X[:, 1], c=y_kmeans, s=50, cmap='viridis')
# 取出K-Means算法得到的聚类中心
centers = kmeans.cluster_centers_
print(centers)
plt.scatter(centers[:, 0], centers[:, 1], c='r', marker='*', s=200);
K-Means算法结果

可以看到K-Means算法得到的结果跟我们直观看上去的很接近,很完美的完成了聚类任务。

手写K-Means算法

鉴于K-Means算法比较经典,同时也比较简单,因此在很多大厂面试的时候,都要求能够手写K-Means算法,本文这里也给出一个简单实现,如下:

import numpy as np

def euclidean(X, Y):
    return sum([(a - b)**2 for (a,b) in zip(X,Y)])**0.5

def dispatch_samples(x, centers):
    # 将样本x指派到距离最近的聚类中心
    res = []
    for i in range(x.shape[0]):
        idx, dis = 0, float('inf')
        for j in range(centers.shape[0]):
            d = euclidean(x[i], centers[j])
            if d < dis:
                idx, dis = j, d
        res.append(idx) # 将当前样本指派的类别放入数组中
    return np.array(res)

def Simple_KMeans(X, n_clusters, rseed=2):
    # 1. 首先随机选择n_clusters个样本作为原始聚类中心
    # 生成一个伪随机数生成器,每次都通过它来生成随机数,可以保证每次运行生成的随机数是一样
    rng = np.random.RandomState(rseed)
    # 取一个排列,然后取前n_clusters个作为聚类中心
    i = rng.permutation(X.shape[0])[:n_clusters]
    centers = X[i]
    while True:
        # 1. 对每个样本都计算其到所有聚类中心的距离,将样本指派到距离其最近的聚类中心
        labels = dispatch_samples(X, centers)
        # 2. 重新计算当前聚类的聚类中心
        new_centers = np.array([X[labels == i].mean(0) for i in range(n_clusters)])
        
        # 如果新的聚类中心与之前一致,则退出算法
        if np.all(centers == new_centers): break
        # 使用新的聚类中心
        centers = new_centers
    # 返回聚类中心和标签
    return centers, labels

centers, labels = Simple_KMeans(X, 4)
plt.scatter(X[:, 0], X[:, 1], c=labels, s=50, cmap='viridis');
plt.scatter(centers[:, 0], centers[:, 1], c='r', marker='*', s=200);

生成的结果与调用sklearn库是一样的,如下:
手写K-Means算法执行结果

K-Means优缺点

K-Means算法是一个经典的,实用的聚类算法,值得我们去掌握它,它具有以下优点 :

  • 算法原理简单,实现容易,收敛较快
  • 聚类效果较优
  • 算法可解释性较强
  • 参数较少,仅仅需要类别数K

当然K-Means算法也有很多缺点,下面逐一说明。

  • K-Means算法得到的结果是局部最优,并不总能得到全局最优解。还是以上面的例子,如果我们使用另外一个随机种子,那么得到的结果可能是这样的。
centers, labels = find_clusters(X, 4, rseed=80)
plt.scatter(X[:, 0], X[:, 1], c=labels,
            s=50, cmap='viridis');
错误的聚类结果

可以看到,修改了随机种子之后,说明初始聚类中心也改变了,那么得到的聚类结果就可能发生很大的改变,甚至不是我们想要的结果。

  • 类的个数K必须手动提前设置
    我们必须提前告诉算法要聚成多少类,这个数据算法无法自己从数据中学到。但是有时候我们也无法确定应该设置成多少。算法只会乖乖地将数据聚合成我们设置给它的K类。在上面的例子中,如果我们将k设置成6,那么可能会得到以下的结果。
centers, labels = find_clusters(X, 6)
plt.scatter(X[:, 0], X[:, 1], c=labels,
            s=50, cmap='viridis');
聚成6类

算法确实收敛了,将样本聚成了6类,但是这个结果对我们来说有意义嘛?这个不太好回答。因为实际应用中的最优K值往往是不知道的。解决这个问题的一个思路是,尝试多个不同的K值,然后检查不同K值下的聚类结果的质量。衡量聚类结果的质量的方式可以是计算算是函数值,也可以是使用类的平均直径,即所有类的直径取平均。一般来说,类别数k变小时,平均直径会变大;类别说变大是,平均直径会逐渐减小,直到某个阈值之后就不再减小。而这个值一般是最优的k值。

  • K-Means聚类受限于线性聚类边界
    K-Means模型假设一个样本点应该是比其他点更加靠近自己的聚类中心,但是对于一些有复杂边界的样本集合,这个假设就不成立了,比如如下这种情况。
from sklearn.datasets import make_moons
X, y = make_moons(200, noise=.05, random_state=0)

labels = KMeans(2, random_state=0).fit_predict(X)
plt.scatter(X[:, 0], X[:, 1], c=labels,
            s=50, cmap='viridis');
复杂边界样本的聚类

实际上K-Means类之间的边界应该总是线性的,当面对这种具有复杂边界的情况是,K-Means算法并不能取得好的结果。

  • 计算量巨大
    同KNN算法一样,K-Means算法在每次迭代的时候,每一个点都需要跟所有的聚类中心进行距离计算,当样本的维度较高时,这个计算量是非常大的。

K-Means++算法

K-Means算法自1967年由MacQueen提出后,历经很多次改进,比较有名的有K-Means++算法。K-Means++算法主要是解决前面提及的K-Means算法的初始化聚类中心随机选择导致的问题。K-Means++不再采用K-Means算法随机确定初始聚类中心的做法,而是令最初选择的聚类中心尽可能的远。这也符合我们的直觉,因为每个类之间就应该尽可能的远。K-Means++算法的流程如下:
(1)随机选取一个样本作为第一个聚类中心 c_1
(2)计算每个样本与当前已有类聚中心最短距离(即与最近一个聚类中心的距离),用 D(x)表示;这个值越大,表示被选取作为聚类中心的概率较大;最后,用轮盘法选出下一个聚类中心;
(3):重复(2),直到选出 k 个聚类中心。

选出初始点后,其余步骤就与标准K-means 算法一致了。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容