K-means以及K-means++原理以及实现方式
K-means与K-means++
k-means++与k-means相比,主要在质心的初始选择上,其余的步骤基本差不多。k-means在选择质心时是直接随机选取k个质心(在选择时不能重复选取),而k-means++在选择初始质心时是根据轮盘法选择距离位置远的数据点作为下一个质心。具体实现如下。
k-means
1、k-means原理
Kmeans 算法是一种常用的聚类算法,它是基于划分方法聚类的。它的原理是将数据划分为k个簇,每个簇由距离中心最近的数据点组成,基于计算样本与中心点的距离归纳各簇类下的所属样本,迭代实现样本与其归属的簇类中心的距离为最小的目标。
简单来说,Kmeans 算法就是通过不断地调整簇的中心点,并将数据点指派到距离它最近的中心点所在的簇,来逐步将数据划分成若干个簇。
2、实现步骤
- 选取K个点做为初始聚集的质心(也即每一簇的簇心);
- 分别计算每个样本点到 K个簇核心的距离(这里的距离一般取欧氏距离),找到离该点最近的簇核心,将它归属到对应的簇;
- 所有点都归属到簇之后, 所有点就分为了 K个簇。
- 判断是否满足终止条件(可以是每一个质心之间的距离小于某一个临界值,一般可设为1e-5)更新质心,计算每一个簇所有样本点的均值作为新的簇心”;
- 反复迭代 2 - 3 步骤,直到达到某个中止条件。
【注意】常用的中止条件有迭代次数、最小平方误差MSE、簇中心点变化率
3、实现(python3)
此处使用的数据集是sklearn.datasets中的鸢尾花数据集(iris)
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import time
from sklearn import datasets
from sklearn.metrics import f1_score, accuracy_score, rand_score, adjusted_rand_score
from sklearn.decomposition import PCA
iris = datasets.load_iris()
dataset = pd.DataFrame(iris.data, columns=iris.feature_names)
dataset["class"] = iris.target
labels_name = list(iris.target_names)
labels_dict = {index: ele for index, ele in enumerate(labels_name)}
data1 = dataset.copy()
data1["class"] = data1["class"].map(labels_dict)
data2 = data1.iloc[:, :len(data1.columns)-1]
# 方法1
# 1.初始化质心(随机选择k个质心)
def init_centroids(data, k):
centroids = data[np.random.choice(data.shape[0], k, replace=False)]
return centroids
# 2.计算数据样本点中除质心外其余点与各个质心之间的距离
def get_cluster(data, centroids):
distance = np.linalg.norm(data[:, np.newaxis] - centroids, axis=2)
cluster_labels = np.argmin(distance, axis=1)
return cluster_labels
# 3.更新质心,直至满足某个迭代条件则终止
def upgrade_centroids(data, cluster_labels, k):
new_centroids = np.array([data[cluster_labels == i].mean(axis=0) for i in range(k)])
return new_centroids
# 4.经典k-means方法
def k_means(data, k ,iters, epsilon):
start = time.time()
t = 0 # 初始化迭代次数
centroids = init_centroids(data, k)
while t < iters:
cluster_labels = get_cluster(data, centroids)
new_centroids = upgrade_centroids(data, cluster_labels, k)
# 判断终止条件(此处使用各个质心之间距离变化小于某个阈值作为条件)
if np.linalg.norm(new_centroids - centroids) < epsilon:
break
centroids = new_centroids
t += 1
print(f"第{t}次迭代.")
print(f"迭代所需时间:{(time.time()-start)}")
return cluster_labels, centroids
# 5.评价指标
def evalue_indecators(labels_true, labels_pred):
F1_score = f1_score(labels_true, labels_pred, average='macro')
Acc = accuracy_score(labels_true, labels_pred)
Rs = rand_score(labels_true, labels_pred)
Ars = adjusted_rand_score(labels_true, labels_pred)
return F1_score, Acc, Rs, Ars
# 6.绘制聚类后的图
def draw_centroids(data, cluster_labels, centroids, k, attributes):
cluster_labels = np.arange(cluster_labels)
if attributes > 2: # 这个为数据的维度(使用处理过的数据data2,此处atrributes为4)
data = PCA(n_components=2).fit_transform(data) # 对data进行降维
centroids = PCA(n_components=2).fit_transform(centroids)
else:
data = np.array(data)
centroids = np.array(centroids)
plt.scatter(data[:, 0], data[:, 1], c='black', s=7, marker='0') # 绘制原图(初始化k个簇前的图)
plt.show()
colors = np.array(
["#FF0000", "#0000FF", "#00FF00", "#FFFF00", "#00FFFF", "#FF00FF", "#800000", "#008000", "#000080", "#808000",
"#800080", "#008080", "#444444", "#FFD700", "#008080"])
for i in range(k):
plt.scatter(data[cluster_labels == i, 0], data[cluster_labels == i, 1] c=color[i], s=7, marker='o')
plt.scatter(centroids[:, 0], centroids[:, 1], c='black', s=21, marker='o')
plt.show()
if __name__ == '__main__':
k = 3 # 此处为已知鸢尾花种类分为3类(不知k的情况下需要选择合适的k值)
iters = 100 # 设置最大迭代次数
epsilon = 1e-5 # 各个质心之间的距离最小阈值
data = np.array(data2)
attributes = len(data.columns)-1
labels, centroids = k_means(data, k, iters, epsilon)
print(f"labels:\n{labels}")
print(f"centroids:\n{centroids}")
F1_score, Acc, Rs, Ars = evalue_indecators(data, labels)
print(f"F1_score:{F1_score}, \nAcc:{Acc}, \nRs:{Rs}, \nArs:{Ars}")
draw_centroids(data, labels, centroids, k, attributes)
4、优缺点
优点
- 易理解,聚类效果可以接受,虽然是局部最优, 但往往局部最优就够了;
- 算法复杂低,也适合大数据集使用,但是可能耗时;
- 当簇近似高斯分布的时候,效果非常不错;
- 调参的参数仅仅是簇数k。
缺点
- K值的选取不好把握(改进1:k-means++;改进2:二分K-means);
- 对于不是凸的数据集比较难收敛(改进:基于密度的聚类算法更加适合,比如DESCAN算法);
- 如果各隐含类别的数据不平衡,比如各隐含类别的数据量严重失衡,或者各隐含类别的方差不同,则聚类效果不佳;
- 结果仅是局部最优;
- 对噪音和异常点比较的敏感;(改进1:离群点检测的LOF算法,通过去除离群点后再聚类,可以减少离群点和孤立点对于聚类效果的影响;改进2:改成求点的中位数,这种聚类方式即K-Mediods聚类(K中值))。
k-means++
1、k-means++原理
原始Kmeans算法最开始随机选取数据集中k个点作为聚类中心,k个初始化的质心的位置选择对最后的聚类结果和运行时间都有很大的影响,因此需要选择合适的k个质心。如果仅仅是完全随机的选择,有可能导致算法收敛很慢。Kmeans++算法主要对对K-Means初始值选取的方法的优化。也就是说,Kmeans++算法与Kmeans算法最本质的区别是在k个聚类中心的初始化过程。
2、算法步骤
- 随机选取一个样本作为第一个聚类中心 c 1 c_1 c1;
- 计算每个样本与当前已有类聚中心最短距离(即与最近一个聚类中心的距离),用 D ( x ) D(x) D(x)表示;这个值越大,表示被选取作为聚类中心的概率较大;
- 用轮盘法选出下一个聚类中心;
- 重复步骤
2
∼
3
2\sim3
2∼3 ,直到选出 k 个聚类中心。
选出k个初始质心后,继续使用标准的 k-means 算法(即两个算法只有在质心的选择上不一样,其余差不多)。
轮盘法
- 基本原理:各个个体被选中的概率与其适应度大小成正比;
- 实现步骤:
a. 计算出数据样本的适应度 f ( i ) , i ∈ [ 1 , n ] ,其中 n 为数据样本大小 ( 该处的适应度可理解为是到质心的距离 ) f(i), i∈[1, n],其中n为数据样本大小(该处的适应度可理解为是到质心的距离) f(i),i∈[1,n],其中n为数据样本大小(该处的适应度可理解为是到质心的距离)
b. 计算出每个个体概率 P ( i ) = f ( x i ) ∑ j = 1 N f ( x i ) P(i) = \frac{f(x_i)}{\sum_{j=1}^\N f(x_i)} P(i)=∑j=1Nf(xi)f(xi)
c. 计算出每个个体的累计概率 C u m u l a t i v e P r o b a b i l i t y ( i ) = ∑ j = 1 P ( x j ) CumulativeProbability(i) = \sum_{j=1} P(x_j) CumulativeProbability(i)=j=1∑P(xj)
d. 在 [ 0 , 1 ] [0,1] [0,1] 区间内产生一个均匀分布的伪随机数 r r r ;
e. 若 r ≤ q 1 r ≤ q_1 r≤q1, 选择样本1;否则选择样本k,使得: q k − 1 < r ≤ q k q_k-1<r ≤ q_k qk−1<r≤qk 成立;
f. 重复步骤 d 、 e d、e d、e 共计 n n n 次。
累加概率举例如下:
注意:
- 个体排序顺序会影响“轮盘赌”法;
- 当适应性值相差不大的情况,该方法与随机选没有什么区别,此时难于控制进化方向和速度;
- 该方法是属于有放回的的选择,不适合不放回的情况;
3、实现(python3)
# 方法2:k-means++
def centroids_choose(data, k):
centroids = [list(data[np.random.randint(data.shape[0])])]
for _ in range(1, k):
distance = np.linalg.norm(data[:, np.newaxis] - centroids, axis=2)
min_distance = np.min(distance, axis=1)
properbility = min_distance / min_distance.sum()
cump = properbility.cumsum()
i = 0
r = np.random.random()
for j, p in enumerate(cump):
if r < p:
i = j
break
centroids.append(data[i])
return np.array(centroids)
def k_means_plus_plus(data, k, iters, epsilon):
t = 0
centroids = centroids_choose(data, k)
while t < iters:
cluster_labels = np.array(np.argmin(np.linalg.norm(data[:, np.newaxis] - centroids, axis=2), axis=1))
new_centroids = np.array([data[cluster_labels == i].mean(axis=0) for i in range(k)])
if np.linalg.norm(new_centroids - centroids) < epsilon:
break
centroids = new_centroids
t += 1
print(f"第{t}次数迭代.")
return cluster_labels, centroids
def draw_cluster(data, centroids, labels):
plt.scatter(dataset[:, 0], dataset[:, 1], c=labels, cmap='viridis', s=7)
plt.scatter(centroids[:, 0], centroids[:, 1], marker='x', color='red', s=50)
plt.show()
if __name__ == '__main__':
k = 3
iters = 100
epsilon = 100
data = np.array(data2)
labels, centroids = k_means_plus_plus(data, k, iters, epsilon)
print(f"labels:\n{labels}")
print(f"centroids:\n{centroids}")
draw_cluster(data, centroids, labels)
4、优缺点
优点
- 初始质心的选择上降低了陷入局部最优解的风险,并通常导致更好的聚类效果和更快的收敛速度;
- 在目标检测应用中,Kmeans++通过改变初始聚类中心的生成方式,增大初始锚框之间差距,使锚框更适应整体据分布,得到了匹配度更好的多尺度锚框,进而提升了模型的检测性能。
缺点
- 初始化步骤比标准 KMeans 算法更复杂,可能会导致略微增加的计算成本;
- 由于聚类中心点选择过程中的内在有序性,在扩展方面存在着性能方面的问题(第k个聚类中心点的选择依赖前k-1个聚类中心点的值)。