目录
K-Means算法是无监督的聚类算法,它实现起来比较简单,聚类效果也不错,因此应用很广泛。
K-Means算法有大量的变体,本文就从最传统的K-Means算法讲起,在其基础上讲述K-Means的优化变体方法。包括初始化优化K-Means++, 距离计算优化elkan K-Means算法和大数据情况下的优化Mini Batch K-Means算法。
1. K-Means原理初探
K-Means算法的思想很简单,对于给定的样本集,按照样本之间的距离大小,将样本集划分为K个簇。让簇内的点尽量紧密的连在一起,而让簇间的距离尽量的大。
如果用数据表达式表示,假设簇划分为( C 1 C_1 C1, C 2 C_2 C2,… C k C_k Ck),则我们的目标是最小化平方误差 E E E:
E = ∑ i = 1 k ∑ x ∈ C i ∥ x − μ i ∥ 2 2 E = \sum_{i=1}^{k}\sum_{x\in C_i}\parallel x-\mu_i\parallel_{2}^{2} E=i=1∑kx∈Ci∑∥x−μi∥22
其中
μ
i
μ_i
μi是簇
C
i
C_i
Ci的均值向量,有时也称为质心,表达式为:
μ
i
=
1
∣
C
i
∣
∑
x
∈
C
i
x
\mu_i=\frac{1}{\mid C_i\mid}\sum_{x\in C_i}x
μi=∣Ci∣1x∈Ci∑x
如果我们想直接求上式的最小值并不容易,这是一个NP难的问题,因此只能采用启发式的迭代方法。
K-Means采用的启发式方式很简单,用下面一组图就可以形象的描述。
上图 a a a 表达了初始的数据集,假设 k = 2 k=2 k=2 。在图 b b b 中,我们随机选择了两个 k k k 类所对应的类别质心,即图中的 红色质心 {\color{Red} 红色质心} 红色质心和 蓝色质心 {\color{blue}蓝色质心} 蓝色质心,然后分别求样本中所有点到这两个质心的距离,并标记每个样本的类别为和该样本距离最小的质心的类别,如图 c c c 所示,经过计算样本和 红色质心 {\color{Red} 红色质心} 红色质心和 蓝色质心 {\color{blue}蓝色质心} 蓝色质心的距离,我们得到了所有样本点的第一轮迭代后的类别。此时我们对我们当前标记为 红色 {\color{Red} 红色} 红色和 蓝色 {\color{blue}蓝色} 蓝色的点分别求其新的质心,如图 4 4 4 所示,新的 红色质心 {\color{Red} 红色质心} 红色质心和 蓝色质心 {\color{blue}蓝色质心} 蓝色质心的位置已经发生了变动。图 e e e 和图 f f f 重复了我们在图 c c c 和图 d d d 的过程,即将所有点的类别标记为距离最近的质心的类别并求新的质心。最终我们得到的两个类别如图 f f f 。
当然在实际 K-Means 算法中,我们一般会多次运行图 c c c 和图 d d d ,才能达到最终的比较优的类别。
2. 传统K-Means算法流程
在上一节我们对K-Means的原理做了初步的探讨,这里我们对K-Means的算法做一个总结。
首先我们看看K-Means算法的一些要点。
1)对于K-Means算法,首先要注意的是k值的选择,一般来说,我们会根据对数据的先验经验选择一个合适的k值,如果没有什么先验知识,则可以通过交叉验证选择一个合适的k值。
2)在确定了k的个数后,我们需要选择k个初始化的质心,就像上图b中的随机质心。由于我们是启发式方法,k个初始化的质心的位置选择对最后的聚类结果和运行时间都有很大的影响,因此需要选择合适的k个质心,最好这些质心不能太近。
好了,现在我们来总结下传统的K-Means算法流程。
输入是样本集 D = x 1 , x 2 , . . . x m D={x_1,x_2,...x_m} D=x1,x2,...xm ,聚类的簇树k,最大迭代次数N
输出是簇划分
C
=
C
1
,
C
2
,
.
.
.
C
k
C={C_1,C_2,...C_k}
C=C1,C2,...Ck
1) 从数据集
D
D
D 中随机选择k个样本作为初始的
k
k
k 个质心向量:
{
μ
1
,
μ
2
,
.
.
.
,
μ
k
}
\{μ_1,μ_2,...,μ_k\}
{μ1,μ2,...,μk}
2)对于
n
=
1
,
2
,
.
.
.
,
N
n=1,2,...,N
n=1,2,...,N
a) 将簇划分
C
C
C 初始化为
C
t
=
∅
C_t=\emptyset
Ct=∅
t
=
1
,
2...
k
t=1,2...k
t=1,2...k
b) 对于
i
=
1
,
2...
m
i=1,2...m
i=1,2...m ,计算样本
x
i
x_i
xi 和各个质心向量
μ
j
(
j
=
1
,
2
,
.
.
.
k
)
μ_j(j=1,2,...k)
μj(j=1,2,...k) 的距离:
d
i
j
=
∣
∣
x
i
−
μ
j
∣
∣
2
2
d_{ij}=||x_i−μ_j||_{2}^{2}
dij=∣∣xi−μj∣∣22 ,将
x
i
x_i
xi 标记最小的为
d
i
j
d_{ij}
dij 所对应的类别
λ
i
λ_i
λi 。此时更新
C
λ
i
=
C
λ
i
∪
x
i
Cλ_i=Cλ_i∪{x_i}
Cλi=Cλi∪xi
c) 对于
j
=
1
,
2
,
.
.
.
,
k
j=1,2,...,k
j=1,2,...,k ,对
C
j
C_j
Cj 中所有的样本点重新计算新的质心
μ
j
=
1
∣
C
j
∣
∑
x
∈
C
j
x
μ_j=\frac{1}{\mid C_j\mid}∑_{x∈_{C_j}}x
μj=∣Cj∣1∑x∈Cjx
d) 如果所有的k个质心向量都没有发生变化,则转到步骤3)
3) 输出簇划分 C = C 1 , C 2 , . . . C k C={C_1,C_2,...C_k} C=C1,C2,...Ck
3. K-Means初始化优化K-Means++
在上节我们提到,k个初始化的质心的位置选择对最后的聚类结果和运行时间都有很大的影响,因此需要选择合适的k个质心。如果仅仅是完全随机的选择,有可能导致算法收敛很慢。K-Means++算法就是对K-Means随机初始化质心的方法的优化。
K-Means++的对于初始化质心的优化策略也很简单,如下:
a) 从输入的数据点集合中随机选择一个点作为第一个聚类中心
μ
1
μ_1
μ1
b) 对于数据集中的每一个点
x
i
x_i
xi,计算它与已选择的聚类中心中最近聚类中心的距离
D
(
x
i
)
=
a
r
g
m
i
n
∣
∣
x
i
−
μ
r
∣
∣
2
2
,
r
=
1
,
2
,
.
.
.
k
s
e
l
e
c
t
e
d
D(x_i)=argmin||x_i−μ_r||_{2}^{2} , r=1,2,...k_{selected}
D(xi)=argmin∣∣xi−μr∣∣22,r=1,2,...kselected
c) 选择一个新的数据点作为新的聚类中心,选择的原则是:
D
(
x
)
D(x)
D(x)较大的点,被选取作为聚类中心的概率较大
d) 重复
b
b
b 和
c
c
c 直到选择出
k
k
k 个聚类质心
e) 利用这
k
k
k 个质心来作为初始化质心去运行标准的
K
−
M
e
a
n
s
K-Means
K−Means 算法
4. K-Means距离计算优化elkan K-Means
在传统的K-Means算法中,我们在每轮迭代时,要计算所有的样本点到所有的质心的距离,这样会比较的耗时。那么,对于距离的计算有没有能够简化的地方呢?elkan K-Means算法就是从这块入手加以改进。它的目标是减少不必要的距离的计算。那么哪些距离不需要计算呢?
elkan K-Means利用了两边之和大于等于第三边,以及两边之差小于第三边的三角形性质,来减少距离的计算。
第一种规律是对于一个样本点x和两个质心 μ j 1 \mu_{j_1} μj1, μ j 2 \mu_{j_2} μj2。如果我们预先计算出了这两个质心之间的距离 D ( j 1 , j 2 ) D(j_1,j_2) D(j1,j2),则如果计算发现 2 D ( x , j 1 ) ≤ D ( j 1 , j 2 ) 2D(x,j_1)≤D(j_1,j_2) 2D(x,j1)≤D(j1,j2),我们立即就可以知道 D ( x , j 1 ) ≤ D ( x , j 2 ) D(x,j_1)≤D(x,j_2) D(x,j1)≤D(x,j2)。此时我们不需要再计算 D ( x , j 2 ) D(x,j_2) D(x,j2),也就是说省了一步距离计算。
第二种规律是对于一个样本点x和两个质心 μ j 1 \mu_{j_1} μj1, μ j 2 \mu_{j_2} μj2。我们可以得到 D ( x , j 2 ) ≥ m a x { 0 , D ( x , j 1 ) − D ( j 1 , j 2 ) D(x,j_2)≥max\{{0,D(x,j_1)−D(j_1,j_2)} D(x,j2)≥max{0,D(x,j1)−D(j1,j2)。这个从三角形的性质也很容易得到。
利用上边的两个规律,elkan K-Means比起传统的K-Means迭代速度有很大的提高。但是如果我们的样本的特征是稀疏的,有缺失值的话,这个方法就不使用了,此时某些距离无法计算,则不能使用该算法。
5. 大样本优化Mini Batch K-Means
在统的K-Means算法中,要计算所有的样本点到所有的质心的距离。如果样本量非常大,比如达到10万以上,特征有100以上,此时用传统的K-Means算法非常的耗时,就算加上elkan K-Means优化也依旧。在大数据时代,这样的场景越来越多。此时Mini Batch K-Means应运而生。
顾名思义,Mini Batch,也就是用样本集中的一部分的样本来做传统的K-Means,这样可以避免样本量太大时的计算难题,算法收敛速度大大加快。当然此时的代价就是我们的聚类的精确度也会有一些降低。一般来说这个降低的幅度在可以接受的范围之内。
在Mini Batch K-Means中,我们会选择一个合适的批样本大小batch size,我们仅仅用batch size个样本来做K-Means聚类。那么这batch size个样本怎么来的?一般是通过无放回的随机采样得到的。
为了增加算法的准确性,我们一般会多跑几次Mini Batch K-Means算法,用得到不同的随机采样集来得到聚类簇,选择其中最优的聚类簇。
6. K-Means与KNN
初学者很容易把K-Means和KNN搞混,两者其实差别还是很大的。
K-Means是无监督学习的聚类算法,没有样本输出;而KNN是监督学习的分类算法,有对应的类别输出。KNN基本不需要训练,对测试集里面的点,只需要找到在训练集中最近的k个点,用这最近的k个点的类别来决定测试点的类别。而K-Means则有明显的训练过程,找到k个类别的最佳质心,从而决定样本的簇类别。
当然,两者也有一些相似点,两个算法都包含一个过程,即找出和某一个点最近的点。两者都利用了最近邻(nearest neighbors)的思想。
7. K-Means小结
K-Means是个简单实用的聚类算法,这里对K-Means的优缺点做一个总结。
K-Means的主要优点有:
1)原理比较简单,实现也是很容易,收敛速度快。
2)聚类效果较优。
3)算法的可解释度比较强。
4)主要需要调参的参数仅仅是簇数k。
K-Means的主要缺点有:
1)K值的选取不好把握(改进:(1)多选择几个k训练模型,选择聚类结果最好解释的k;(2)主成分分析(PCA)等降维方法将数据将降维投影到二维平面上,通过人工观察确定划分数;(3)K-Means的思想,每个簇内间距尽可能小,我们尝试使用不同划分数K进行K-Means聚类,看看不同划分的簇内间距变化情况;(4)各种K值算出的SSE做比较,取最小的SSE的K值);
2)对于不是凸的数据集比较难收敛 (改进:基于密度的聚类算法更加适合,比如DESCAN算法)
3)如果各隐含类别的数据不平衡,比如各隐含类别的数据量严重失衡,或者各隐含类别的方差不同,则聚类效果不佳。
4) 采用迭代方法,得到的结果只是局部最优。
5) 对噪音和异常点比较的敏感。(改进1:离群点检测的LOF算法,通过去除离群点后再聚类,可以减少离群点和孤立点对于聚类效果的影响;改进2:改成求点的中位数,这种聚类方式即K-Mediods聚类(K中值))。
8.小批量处理的K-Means聚类算法
K-Means聚类算法的时间复杂度随着样本数的增加而增大,若样本量达到上万时,K-Means聚类非常耗时,因此对该数据集进行无放回随机抽样得到何时的小批量样本数据集,sklearn.cluster包提供了相应的实现方法MiniBatchKMeans。
import time
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import MiniBatchKMeans, KMeans
from sklearn.metrics.pairwise import pairwise_distances_argmin
from sklearn.datasets.samples_generator import make_blobs
# Generate sample data
np.random.seed(0)
# minibatch随机抽样100例样本进行训练
batch_size = 100
centers = [[1, 1], [-1, -1], [1, -1]]
n_clusters = len(centers)
# 产生3个簇类的30000个样本数据
X, labels_true = make_blobs(n_samples=30000, centers=centers, cluster_std=0.7)
# k-means++算法
k_means = KMeans(init='k-means++', n_clusters=3, n_init=10)
t0 = time.time()
k_means.fit(X)
t_batch = time.time() - t0
# MiniBatchKMeans算法
mbk = MiniBatchKMeans(init='k-means++', n_clusters=3, batch_size=batch_size,
n_init=10, max_no_improvement=10, verbose=0)
t0 = time.time()
mbk.fit(X)
t_mini_batch = time.time() - t0
# 打印k-means++运行时间和性能度量
print("k-means++_runtime= ",t_batch)
print("k_means++_metics= ",k_means.inertia_)
# 打印minibatch_k_means++运行时间和性能度量值
print("MiniBatch_k_means++_runtime= ",t_mini_batch)
print("k_means_metics= ",mbk.inertia_)
#>
k-means++_runtime= 0.36002039909362793
k_means++_metics= 25164.97821695812
MiniBatch_k_means++_runtime= 0.15800929069519043
k_means_metics= 25178.611517320118
9. python实现
# -*- coding:utf-8 -*-
import numpy as np
from matplotlib import pyplot
class K_Means(object):
# k是分组数;tolerance‘中心点误差’;max_iter是迭代次数
def __init__(self, k=2, tolerance=0.0001, max_iter=300):
self.k_ = k
self.tolerance_ = tolerance
self.max_iter_ = max_iter
def fit(self, data):
self.centers_ = {}
for i in range(self.k_):
self.centers_[i] = data[i]
for i in range(self.max_iter_):
self.clf_ = {}
for i in range(self.k_):
self.clf_[i] = []
# print("质点:",self.centers_)
for feature in data:
# distances = [np.linalg.norm(feature-self.centers[center]) for center in self.centers]
distances = []
for center in self.centers_:
# 欧拉距离
# np.sqrt(np.sum((features-self.centers_[center])**2))
distances.append(np.linalg.norm(feature - self.centers_[center]))
classification = distances.index(min(distances))
self.clf_[classification].append(feature)
# print("分组情况:",self.clf_)
prev_centers = dict(self.centers_)
for c in self.clf_:
self.centers_[c] = np.average(self.clf_[c], axis=0)
# '中心点'是否在误差范围
optimized = True
for center in self.centers_:
org_centers = prev_centers[center]
cur_centers = self.centers_[center]
if np.sum((cur_centers - org_centers) / org_centers * 100.0) > self.tolerance_:
optimized = False
if optimized:
break
def predict(self, p_data):
distances = [np.linalg.norm(p_data - self.centers_[center]) for center in self.centers_]
index = distances.index(min(distances))
return index
if __name__ == '__main__':
x = np.array([[1, 2], [1.5, 1.8], [5, 8], [8, 8], [1, 0.6], [9, 11]])
k_means = K_Means(k=2)
k_means.fit(x)
print(k_means.centers_)
for center in k_means.centers_:
pyplot.scatter(k_means.centers_[center][0], k_means.centers_[center][1], marker='*', s=150)
for cat in k_means.clf_:
for point in k_means.clf_[cat]:
pyplot.scatter(point[0], point[1], c=('r' if cat == 0 else 'b'))
predict = [[2, 1], [6, 9]]
for feature in predict:
cat = k_means.predict(predict)
pyplot.scatter(feature[0], feature[1], c=('r' if cat == 0 else 'b'), marker='x')
pyplot.show()
声明:转载自刘建平Pinard…
参考文献:
[1] K-Means算法原理、缺点及改进,遇到异常值怎么办?评估算法的指标有哪些? 2022.6
[2] [549]python实现K-Means算法 2019.3