kmeans背景原理以及工作流程介绍:
源代码:
https://github.com/apachecn/AiLearning/blob/master/src/py3.x/ml/10.kmeans/kMeans.py
样本数据:
https://github.com/apachecn/AiLearning/tree/master/db/10.KMeans
代码解析:(比上面的更详细、细致)
上述是资料和代码来源,这里说一下重点的内容:
k-means特征:
聚类、无监督学习、无需划分样本数据和实验数据、分类算法
k-means的缺陷和解决办法:
kMeans 可能偶尔会陷入局部最小值(局部最优的结果,但不是全局最优的结果),即并不能准确的将数据划分成k个簇,出现这种情况的原因有很多,例如:距离算法选的不好、初始质心选的不好、数据本身存在较多异常数据等等。
解决方法:直接的思路就是将sse较大的簇进行再切分、然后对所有簇的数目进行把控,思路简单、但是实现起来比较麻烦,有一种可行的方法是利用二分k-means法,该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分时候可以最大程度降低 SSE(平方和误差)的值。上述基于 SSE 的划分过程不断重复,直到得到用户指定的簇数目为止。
下面对二分法k-means的代码进行详细的解释:
# 二分 KMeans 聚类算法, 基于 kMeans 基础之上的优化,以避免陷入局部最小值
def biKMeans(dataMat, k, distMeas=distEclud):
m = shape(dataMat)[0] #获取数据矩阵的行数、每一行即一个样本、每一列即一个样本特征(维度)
clusterAssment = mat(zeros((m, 2))) # 保存每个数据点的簇分配结果和平方误差
centroid0 = mean(dataMat, axis=0).tolist()[0] # 质心初始化为所有数据点的均值、按列取
centList = [centroid0] # 初始化只有 1 个质心的 list
for j in range(m): # 计算所有数据点到初始质心的距离平方误差,质心索引均为0.0
clusterAssment[j, 1] = distMeas(mat(centroid0), dataMat[j, :])**2
while (len(centList) < k): # 当质心数量小于 k 时
lowestSSE = inf #无穷大
for i in range(len(centList)): # 对每一个质心(第一次只有一个质心0.0)
ptsInCurrCluster = dataMat[nonzero(clusterAssment[:, 0].A == i)[0], :] # 获取当前簇 i 下的所有数据点
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 将当前簇 i 进行二分 kMeans 处理
sseSplit = sum(splitClustAss[:, 1]) # 将二分 kMeans 结果中的平方和的距离进行求和
# 将未参与二分 kMeans 分配结果中的平方和的距离进行求和,第一次的话这部分数据为空
sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:, 0].A != i)[0], 1])
print("sseSplit, and notSplit: ", sseSplit, sseNotSplit)
if (sseSplit + sseNotSplit) < lowestSSE:
bestCentToSplit = i # 当前的簇索引为最优的划分簇索引
bestNewCents = centroidMat # 当前的簇质心集合为最优的簇质心集合
bestClustAss = splitClustAss.copy() # 当前的簇分配结果为最优的簇分配结果
lowestSSE = sseSplit + sseNotSplit # 本次划分后的误差为当前最小的误差
# 使用kMeans()函数并指定簇数目为2来划分当前簇, 会得到两个编号分别为0和1的结果簇.
# 需要将这些簇编号修改为划分前簇的编号和新加簇的编号
# bestClustAss[:, 0].A == 1: 最优的簇分配结果中的簇质心索引值等于1返回True即非0, 不等于1返回False即0
# np.nonzero(bestClustAss[:, 0].A == 1): 取得最优的簇分配结果中簇质心索引值为1的全部数据在数据集中的索引值
# bestClustAss[np.nonzero(bestClustAss[:, 0].A == 1)[0], 0]: 取得最优的簇分配结果中簇质心索引值为1的簇质心索引值
# 将最优的簇分配结果中值为1的簇质心索引值修改为簇质心列表的长度即当前簇质心列表中最大的索引值+1作为新划分出来的数据集的簇质心索引值,
# 也就是划分后新的数据集的簇质心索引值更新为原簇质心列表中没有的新的索引值
bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList) # 调用二分 kMeans 的结果,默认簇是 0,1. 当然也可以改成其它的数字
bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit # 二分后属于质心0的的数据更新为最佳质心
print('the bestCentToSplit is: ', bestCentToSplit)
print('the len of bestClustAss is: ', len(bestClustAss))
# 更新质心列表
centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0] # 更新原质心 list 中的第 i 个质心为使用二分 kMeans 后 bestNewCents 的第一个质心
centList.append(bestNewCents[1, :].tolist()[0]) # 添加 bestNewCents 的第二个质心
clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss # 重新分配最好簇下的数据(质心)以及SSE
return mat(centList), clusterAssment
为了便于理解、附上日志:
E:\software\anaconda\envs\ml_learn\python.exe D:/Code/ml_learn/k-means/kMeans.py
sseSplit, and notSplit: 432.3044040256054 0.0
the bestCentToSplit is: 0
the len of bestClustAss is: 60
bestNewCents:
[[-0.45965615 -2.7782156 ]
[-0.00675605 3.22710297]]
clusterAssment:
[[ 1. 14.42964713]
[ 1. 8.92361885]
[ 0. 0. ]
[ 1. 5.48163004]
[ 1. 3.38753576]
[ 0. 0. ]
[ 1. 10.33066673]
[ 1. 7.11789729]
[ 0. 0. ]
[ 1. 14.44898501]
[ 1. 0.16326315]
[ 0. 0. ]
[ 1. 8.2912542 ]
[ 1. 6.23343833]
[ 0. 0. ]
[ 1. 18.76543237]
[ 1. 16.35850981]
[ 0. 0. ]
[ 1. 11.17973661]
[ 1. 10.03617426]
[ 0. 0. ]
[ 1. 4.57851637]
[ 1. 2.55294053]
[ 0. 0. ]
[ 1. 25.14668637]
[ 1. 20.0623013 ]
[ 0. 0. ]
[ 1. 6.72306501]
[ 1. 8.03490212]
[ 0. 0. ]
[ 1. 18.94815516]
[ 1. 14.20426601]
[ 0. 0. ]
[ 1. 19.38834427]
[ 1. 2.49892168]
[ 0. 0. ]
[ 1. 2.69633036]
[ 1. 2.64915048]
[ 0. 0. ]
[ 1. 9.718268 ]
[ 1. 1.56593924]
[ 0. 0. ]
[ 1. 25.95805735]
[ 1. 15.34244344]
[ 0. 0. ]
[ 1. 14.72791916]
[ 1. 13.89401767]
[ 0. 0. ]
[ 1. 25.58631958]
[ 1. 2.2306424 ]
[ 0. 0. ]
[ 1. 21.03897846]
[ 1. 7.08569773]
[ 0. 0. ]
[ 1. 3.30829156]
[ 1. 5.78452309]
[ 0. 0. ]
[ 1. 12.05061416]
[ 1. 11.38132298]
[ 0. 0. ]]
centList: [[-0.45965614999999993, -2.7782156000000002], [-0.006756050000000002, 3.2271029749999998]]
sseSplit, and notSplit: 15.75493440555293 432.3044040256054
sseSplit, and notSplit: 81.19422535738047 0.0
the bestCentToSplit is: 1
the len of bestClustAss is: 40
bestNewCents:
[[ 2.93386365 3.12782785]
[-2.94737575 3.3263781 ]]
clusterAssment:
[[ 1. 0. ]
[ 2. 4.42569457]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 6.52625666]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 3.46783235]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 10.35334989]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 3.47814388]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 11.62824683]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 3.43334781]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 3.62516593]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 1.87786715]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 0.64501952]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 0.31135728]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 2.67583528]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 3.13566793]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 6.22921318]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 1.29433823]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 0.32763308]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 2.95066813]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 3.44911224]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 1.54742427]
[ 0. 0. ]
[ 1. 0. ]
[ 2. 9.81205113]
[ 0. 0. ]]
centList:
[[-0.45965614999999993, -2.7782156000000002], [2.93386365, 3.12782785], [-2.94737575, 3.3263781000000003]]
centList=
[[-0.45965615 -2.7782156 ]
[ 2.93386365 3.12782785]
[-2.94737575 3.3263781 ]]
Process finished with exit code 0
文中提供的kmeans方法感觉有点困惑,为啥有不少点的质心距离数据为0:
def kMeans(dataMat, k, distMeas=distEclud, createCent=randCent):
m = shape(dataMat)[0] # 行数
clusterAssment = mat(zeros((m, 2))) # 创建一个与 dataMat 行数一样,但是有两列的矩阵,用来保存簇分配结果
centroids = createCent(dataMat, k) # 创建质心,随机k个质心
clusterChanged = True
while clusterChanged:
clusterChanged = False
for i in range(m): # 循环每一个数据点并分配到最近的质心中去
minDist = inf
minIndex = -1
for j in range(k):
distJI = distMeas(centroids[j, :], dataMat[i, :]) # 计算数据点到质心的距离
if distJI < minDist: # 如果距离比 minDist(最小距离)还小,更新 minDist(最小距离)和最小质心的 index(索引)
minDist = distJI
minIndex = j
if clusterAssment[i, 0] != minIndex: # 簇分配结果改变
clusterChanged = True # 簇改变
clusterAssment[i, :] = minIndex, minDist**2 # 更新簇分配结果为最小质心的 index(索引),minDist(最小距离)的平方
#print(centroids)
for cent in range(k): # 更新质心
ptsInClust = dataMat[nonzero(clusterAssment[:, 0].A == cent)[0]] # 获取该簇中的所有点
centroids[cent, :] = mean(ptsInClust, axis=0) # 将质心修改为簇中所有点的平均值,mean 就是求平均值的
return centroids, clusterAssment
clusterAssment 在刚开始肯定是样本行数mX2(质心索引、距离)的0矩阵,这一点没有疑问,请注意下面这个代码块:
while clusterChanged:
clusterChanged = False
for i in range(m): # 循环每一个数据点并分配到最近的质心中去
minDist = inf
minIndex = -1
for j in range(k):
distJI = distMeas(centroids[j, :], dataMat[i, :]) # 计算数据点到质心的距离
if distJI < minDist: # 如果距离比 minDist(最小距离)还小,更新 minDist(最小距离)和最小质心的 index(索引)
minDist = distJI
minIndex = j
if clusterAssment[i, 0] != minIndex: # 簇分配结果改变
clusterChanged = True # 簇改变
clusterAssment[i, :] = minIndex, minDist**2 # 更新簇分配结果为最小质心的 index(索引),minDist(最小距离)的平方
最初的minIndex=-1,minDist= inf,在第一次计算的时候,算出来的distJI肯定小于minDist,因此minDist = distJI 、minIndex = j这二者都被更新了,也没问题,但是当j=0时,if clusterAssment[i, 0] != minIndex: # 簇分配结果改变,明显clusterAssment[i, 0] = minIndex这二者相等,那么在index=0时,距离就不会更新,所以在上面的结果中会出现很多[0, 0]的答案,同理有[1, 0]
说明这些点从一开始 就划给了 第一个质心、也是最优的质心,才导致结果数据里面有不少质心距离为0