【Python机器学习实战】聚类算法(1)——K-Means聚类

实战部分主要针对某一具体算法对其原理进行较为详细的介绍,然后进行简单地实现(可能对算法性能考虑欠缺),这一部分主要介绍一些常见的一些聚类算法。


K-means聚类算法

0.聚类算法算法简介

  聚类算法算是机器学习中最为常见的一类算法,在无监督学习中,可以说聚类算法有着举足轻重的地位。

  提到无监督学习,不同于前面介绍的有监督学习,无监督学习的数据没有对应的数据标签,我们只能从输入X中去进行一些知识发现或者预处理。

  过去在有监督学习中,我们(让机器)通过X去预测Y,而到了无监督学习中,我们(让机器)只能从X去发现什么,或者X中哪些输入对我们是有用的,因此:

  无监督学习中包括了两大方面:聚类和降维

  在无监督学习中,我们通过X可以发现什么。聚类就是主要回答这一类问题。而对于一个具有很多维的数据,那些维度对于我们想要知道的事情的影响比较大,这就是降维要做的事情。

  聚类算法,顾名思义,就是一种能将属于同类别的数据聚集在一起的算法,称之为“物以类聚”。聚类的目的就是将相似的对象归为同一簇中,不相似的对象归到不同簇中。

  聚类算法比较常用的有K-means、层次聚类算法、DBSCAN等,这些后面会一一介绍,本节主要是K-means算法。

  具体聚类算法有很多应用场景,如客户群体分析、社交网络分析等,还有很多间接的应用,如数据预处理、半监督学习等。

1.K-means聚类原理

  K-means聚类就是给定一组数据,以及一个k值,然后把这些数据分为k个类别的算法。其中k是事先需要给定的参数。每一个簇(类别)通过这个簇的中心(质心)进行描述。大概就是下面这样子:

【Python机器学习实战】聚类算法(1)——K-Means聚类

  K-means算法是聚类算法中较为简单的一种,原理简单,易于实现。其原理大致是:首先给定k个中心(质心),然后将数据分别划分到k个簇中去,也就是说把每个数据分到距离其最近的那个中心所在的簇中。

  然后重新计算每个簇的中心,即属于这个簇的样本的均值即为新的簇的中心。

  具体算法流程如下:

    1.初始化k个点作为聚类中心(通常随机选择)

    2.计算每个样本距离k个聚类中心的距离,然后将每个样本分到距离其最近的中心所属的簇中;

    3.重新计算k个簇的中心,中心为簇中所包含数据点的均值,;

    4.重复2~3,知道k个聚类中心不再移动。

【Python机器学习实战】聚类算法(1)——K-Means聚类

  图片来源于网络,这里稍微整理了一下。

    上面就是K-means算法的过程,下面我们先对上面的过程做一个简单的实现:

from numpy import *

# 定义距离计算
def cal_dist(vect_a, vect_b):
return sqrt(sum(power(vect_a, vect_b, 2))) # 随机选取聚类中心
def rand_center(data, k):
n = shape(data)[1]
centroids = mat(zeros((k, n)))
for j in range(n):
min_j = min(data[:, j])
range_j = float(max(data[:, j]) - min(data[:, j]))
# np.random.rand(k, 1) 生成size为(k,1)的0~1的随机array
centroids[:, j] = min_j + range_j * random.rand(k, 1)
return centroids def Kmeans(data, k, dis_meas=cal_dist, create_center=rand_center):
m = shape(data)[0]
# 用于保存每个样本所属类别的矩阵,第0维为所属类别,第一维为样本距离该类别的距离
clusterAssment = mat(zeros((m, 2)))
# 初始化聚类中心
centroids = create_center(data, k)
cluster_changed = True
while cluster_changed:
cluster_changed = False
for i in range(m):
min_dist = inf
min_index = -1
for j in range(k):
dist_ij = dis_meas(data[i, :], centroids[j, :])
if dist_ij < min_dist:
min_dist = dist_ij
min_index = j
# 如果样本的类别发生了变化,则继续迭代
if clusterAssment[i, 0] != min_index:
cluster_changed = True
# 第i个样本距离最近的中心j存入
clusterAssment[i, :] = min_index, min_dist ** 2
print(centroids)
# 重新计算聚类中心
for cent in range(k):
# 找出数据集中属于第k类的样本的所有数据,nonzero返回索引值
points_in_cluster = data[nonzero(clusterAssment[:, 0].A == cent)[0]]
centroids[cent, :] = mean(points_in_cluster, axis=0)
return centroids, clusterAssment

  然后定义一个读取数据的函数,使用testdata进行测试:

def loadData(filename):
data_mat = []
fr = open(filename)
for line in fr.readlines():
cur_line = line.strip().split('\t')
flt_line = [float(example) for example in cur_line]
data_mat.append(flt_line)
return mat(data_mat)
data = loadData('.\testSet.txt')
centroids, cluster_ass = Kmeans(data, 4, dis_meas=cal_dist, create_center=rand_center) import matplotlib.pyplot as plt data_0 = data[nonzero(cluster_ass[:, 0].A == 0)[0]]
data_1 = data[nonzero(cluster_ass[:, 0].A == 1)[0]]
data_2 = data[nonzero(cluster_ass[:, 0].A == 2)[0]]
data_3 = data[nonzero(cluster_ass[:, 0].A == 3)[0]]
plt.scatter(data_0[:, 0].A[:, 0], data_0[:, 1].A[:, 0])
plt.plot(centroids[0, 0], centroids[0, 1], '*', markersize=20)
plt.scatter(data_1[:, 0].A[:, 0], data_1[:, 1].A[:, 0])
plt.plot(centroids[1, 0], centroids[1, 1], '*', markersize=20)
plt.scatter(data_2[:, 0].A[:, 0], data_2[:, 1].A[:, 0])
plt.plot(centroids[2, 0], centroids[2, 1], '*', markersize=20)
plt.scatter(data_3[:, 0].A[:, 0], data_3[:, 1].A[:, 0])
plt.plot(centroids[3, 0], centroids[3, 1], '*', markersize=20)

【Python机器学习实战】聚类算法(1)——K-Means聚类

2.关于K-means算法的问题和改进

  K-means的损失函数为数据点与数据点所在的聚类中心之间的距离的平方和,也就是:

【Python机器学习实战】聚类算法(1)——K-Means聚类

  其中μ为数据点所在的类别的聚类中心,我们期望最小化损失,从而找到最佳的聚类中心和数据所属的类别。

2.1 陷入局部最小值问题及改进

  然而,上面说到,在K-means算法的第一步是随机选取k个位置作为聚类中心,这可能就会导致,不同的初始位置,对最终的聚类结果有着很大的影响,比如当把k设置为2时:

【Python机器学习实战】聚类算法(1)——K-Means聚类

  第一次随机选取的两个聚类中心为左边这张图的位置,那么结果可能分为上下两个簇,当第二次选取的为右边那张图片的位置时,可能最终的聚类结果为左右两个簇。

  因此,聚类中心的初始位置,对于我们最终的结果影响很大。再比如:

【Python机器学习实战】聚类算法(1)——K-Means聚类

  上面通过K-means将数据聚为3类,但由于聚类中心的问题,导致效果不好,“+”为最终聚类中心位置,此时聚类中心已不再更新。

  这是因为K-means算法收敛到了局部最小值,而非全局最小值。

  因此,我们需要一定的处理方法,来处理这样的问题:

  一种最直观的做法是,我们通过多次初始化聚类中心,运行K-means聚类,得到多个结果,然后比较最后的损失(上面损失函数计算方法),选择其中最小的那一个结果。

  然而这种对于k取值比较的少的时候可以这么做,但是如果k值过大,这样做也不会得到较好的改善。

  另一种做法是对生成的簇进行后处理,通过将具有最大的损失的那个簇,再分成两个簇,也就是对于损失最大的那个簇再运用一次K-means算法,将k值设为2

  但为了保证簇的总数不变,再将某两个簇进行合并,比如上面图中,将下面圆圈和方块进行合并,但是如果是一个多维数据,无法进行可视化时,我们无法查看应该去合并哪几个簇

  因此,可以有两种方法去衡量:(1)将最近的聚类中心对应的类进行合并;(2)合并两个使总的损失增加幅度最小的簇。

  第一种方法是计算聚类中心(这里已经将最大的簇又分为两个簇)之间的距离,将最近的两个簇进行合并;

  第二种方法需要计算合并两个簇之后的损失的大小,找出最佳的合并结果。

下面介绍一种利用上面划分簇的技术所改善的K-means算法。

二分K-Means算法

  二分K-means算法是一种能够解决算法收敛到局部最小值的算法,算法思想是:首先将所有的点作为一个簇,然后分成2个,接下来,在其中选择一个簇进行划分,具体选择哪个,

  要根据选择划分的簇能够使总损失降低程度最大的那个,此时簇被分为3个,然后重复上述过程,直到达到所指定的k个簇即停止。

  具体算法过程如下:

    1.将所有数据看做为一个簇;

    2.当簇的总数目小于k时:

      对于每一个簇:

        (1)计算此时的损失;

        (2)在此簇上采用K-means聚类(k=2)

        (3)计算划分后的损失;

      选择使划分后损失最小的簇,将其进行划分

  上述过程中也可以选择损失最大的那个簇进行划分。

下面是具体实现过程:

def bi_kmeans(data, k, dist_measure=cal_dist):
m = shape(data)[0]
cluster_ass = mat(zeros((m, 2)))
# 初始化聚类中心,此时聚类中心只有一个,因此对数据取平均
centroid0 = mean(data, axis=0).tolist()[0]
# 存储每个簇的聚类中心的列表
centList = [centroid0]
for j in range(m):
cluster_ass[j, 1] = dist_measure(mat(centroid0), data[j, :]) ** 2 while (len(centList)) < k:
lowestSSE = inf
for i in range(len(centList)):
# 在当前簇中的样本点
point_in_current_cluster = data[nonzero(cluster_ass[:, 0].A == i)[0], :]
# 在当前簇运用kmeans算法,分为两个簇,返回簇的聚类中心和每个样本点距离其所属簇的中心的距离
centroid_mat, split_cluster_ass = Kmeans(point_in_current_cluster, 2, dist_measure)
# 计算被划分的簇,划分后的损失
sse_split = sum(split_cluster_ass[:, 1])
# 计算没有被划分的其它簇的损失
sse_not_split = sum(cluster_ass[nonzero(cluster_ass[:, 0].A != i)[0], 1])
# 选择最小的损失的簇,对其进行划分
if sse_split + sse_not_split < lowestSSE:
# 第i个簇被划分
best_cent_to_split = i
# 第i个簇被划分后的聚类中心
best_new_centers = centroid_mat
# 第i个簇的样本,距离划分后所属的类别(只有0和1)以及距离聚类中心的距离
best_cluster_ass = split_cluster_ass
lowestSSE = sse_split + sse_not_split
# 把新划分出来的簇,属于1类的簇重新进行编号,编号为原先的总类别数目,比如原先有两类,选择一个进行划分后,又分成两类,等于1的那一类编号为2
best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 1)[0], 0] = len(centList)
# 同理,属于第0类的重新编号,编号为所选的那一类的编号
best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 0)[0], 0] = best_cent_to_split
# 将原来的聚类中心进行替换
centList[best_cent_to_split] = best_new_centers[0, :]
# 并加入新的1类的聚类中心
centList.append(best_new_centers[1, :])
# 将之前被选到划分的那一类的结果全部替换成被划分后的结果
cluster_ass[nonzero(cluster_ass[:, 0].A == best_cent_to_split[0]), :] = best_cluster_ass return mat(centList), cluster_ass

【Python机器学习实战】聚类算法(1)——K-Means聚类

2.2 K值的影响及其改进

  K-means算法的K值需要人为给出所需要聚类的类别数目k,那么k的选择对于结果影响很大,通常k值的选择通常需要根据实际进行手动选择,比如给定一组身高和体重的数据,我们可以聚成三类(S、M、L)来指导生产。

  还有一种方法用于选择k值的方法,成为“手肘法则”(elbow method),其原理很简单,就是通过设定不同的k值,然后画出每一个k值对应的损失,如图所示:

【Python机器学习实战】聚类算法(1)——K-Means聚类

  然后找出“手肘”的位置,就是最佳的聚类数目k。

  这里有个问题,损失不应该是越小越好吗?理论上是这样的,但当我们把n个数据点聚成n个类别,此时损失为0,然而这并没有什么意义(类似于过拟合)。

  因此我们选择“手肘”的位置,此时损失下降相较于后面下降速度较大,即k=3。

  具体的实现这里不再说了,就是计算不同k值,然后计算Loss损失就可以了。这里补充一个关于能够自动选择k值的库:yellowbrick,代码很简单(参考https://www.zhihu.com/question/279825061/answer/1686762604):

from sklearn.cluster import KMeans
from yellowbrick.cluster.elbow import kelbow_visualizer
from yellowbrick.datasets.loaders import load_nfl x, y = load_nfl() kelbow_visualizer(KMeans(random_state=4), x, k=(2, 10))

【Python机器学习实战】聚类算法(1)——K-Means聚类

  可以自动选出k值(k=4),而且画出的图也很好看。

  上面就是K-means算法的基本内容,由于算法比较简单,内容不多,而且sklearn也带有Kmeans的工具包(上面那个例子里就是)。总的来说虽然K-means算法比较简单,但是用途还是比较广泛的。


最近事情比较多,学习进度有点慢~,后面会继续针对聚类算法做总结和学习,下一次主要对层次聚类和DBSCAN进行一个回顾和总结。

上一篇:Linux下用来获取各种系统信息的C++类


下一篇:Apache POI