Skip to content

Commit d3619b4

Browse files
committed
更新 KMeans 聚类算法文档
1 parent 01a6442 commit d3619b4

6 files changed

Lines changed: 317 additions & 31 deletions

File tree

docs/10.k-means聚类.md

Lines changed: 194 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,195 @@
11

2-
# 10) k-means聚类
3-
* 聚类介绍
4-
* 聚类是一种无监督的学习,它将相似的对象归到一个簇中。
5-
* 聚类分析试图将相似对象归入同一簇,将不相似对象归到不同簇。相似这一概念取决于所选择的相似度计算方法。
6-
* K-均值聚类算法
7-
* 优点 : 容易实现
8-
* 缺点 : 可能收敛到局部最小值,在大规模数据集上收敛较慢。
9-
* 使用数据类型 : 数值型数据。
10-
* K-均值算法工作流程
11-
* 首先,随机确定 k 个初始点作为质心。
12-
* 然后,将数据集中的每个点分配到一个簇中。(为每个点找到距其最近的质心,并将其分配给该质心所对应的簇)
13-
* 最后,每个簇的质心更新为该簇所有点的平均值。
14-
* K-均值算法伪代码如下
15-
* 创建 k 个点作为起始质心(通常是随机选择)
16-
* 当任意一个点的簇分配结果发生改变时
17-
* 对数据集中的每个数据点
18-
* 对每个质心
19-
* 计算质心与数据点之间的距离
20-
* 将数据点分配到距其最近的簇
21-
* 对每一个簇,计算簇中所有点的均值并将均值作为质心
2+
# 第 10 章 K-Means(K-均值)聚类算法
3+
4+
## K-Means 算法
5+
聚类是一种无监督的学习, 它将相似的对象归到一个簇中, 将不相似对象归到不同簇中.
6+
相似这一概念取决于所选择的相似度计算方法.
7+
K-Means 是发现给定数据集的 K 个簇的聚类算法, 之所以称之为 `K-均值` 是因为它可以发现 K 个不同的簇, 且每个簇的中心采用簇中所含值的均值计算而成.
8+
簇个数 K 是用户指定的, 每一个簇通过其质心(centroid), 即簇中所有点的中心来描述.
9+
聚类与分类算法的最大区别在于, 分类的目标类别已知, 而聚类的目标类别是未知的.
10+
11+
```
12+
优点: 容易实现
13+
缺点:可能收敛到局部最小值, 在大规模数据集上收敛较慢
14+
使用数据类型 : 数值型数据
15+
```
16+
17+
### K-Means 场景
18+
主要用来聚类, 但是类别是未知的.
19+
例如: 对地图上的点进行聚类.
20+
21+
### K-Means 术语
22+
23+
* 簇: 簇中的对象是相似的.
24+
* 质心: 簇中所有点的中心(计算所有点的均值而来).
25+
* SSE: Sum of Sqared Error(平方误差和), SSE 值越小,表示越接近它们的质心. 由于对误差取了平方,因此更加注重那么远离中心的点.
26+
27+
有关 ```质心` 术语更形象的介绍, 请参考下图:
28+
29+
![K-Means 术语图](../images/10.KMeans/apachecn-k-means-term-1.jpg)
30+
31+
### K-Means 工作流程
32+
1. 首先, 随机确定 K 个初始点作为质心.
33+
2. 然后将数据集中的每个点分配到一个簇中, 具体来讲, 就是为每个点找到距其最近的质心, 并将其分配该质心所对应的簇. 这一步完成之后, 每个簇的质心更新为该簇说有点的平均值.
34+
35+
上述过程的 `伪代码` 如下:
36+
37+
* 创建 k 个点作为起始质心(通常是随机选择)
38+
* 当任意一个点的簇分配结果发生改变时
39+
* 对数据集中的每个数据点
40+
* 对每个质心
41+
* 计算质心与数据点之间的距离
42+
* 将数据点分配到距其最近的簇
43+
* 对每一个簇, 计算簇中所有点的均值并将均值作为质心
44+
45+
### K-Means 开发流程
46+
47+
```
48+
收集数据:使用任意方法
49+
准备数据:需要数值型数据类计算距离, 也可以将标称型数据映射为二值型数据再用于距离计算
50+
分析数据:使用任意方法
51+
训练算法:此步骤不适用于 K-Means 算法
52+
测试算法:应用聚类算法、观察结果.可以使用量化的误差指标如误差平方和(后面会介绍)来评价算法的结果.
53+
使用算法:可以用于所希望的任何应用.通常情况下, 簇质心可以代表整个簇的数据来做出决策.
54+
```
55+
56+
### K-Means 聚类算法函数
57+
58+
#### 从文件加载数据集
59+
60+
```python
61+
# 从文本中构建矩阵,加载文本文件,然后处理
62+
def loadDataSet(fileName): # 通用函数,用来解析以 tab 键分隔的 floats(浮点数),例如: 1.658985 4.285136
63+
dataMat = []
64+
fr = open(fileName)
65+
for line in fr.readlines():
66+
curLine = line.strip().split('\t')
67+
fltLine = map(float,curLine) # 映射所有的元素为 float(浮点数)类型
68+
dataMat.append(fltLine)
69+
return dataMat
70+
```
71+
72+
#### 计算两个向量的欧氏距离
73+
74+
```python
75+
# 计算两个向量的欧式距离(可根据场景选择)
76+
def distEclud(vecA, vecB):
77+
return sqrt(sum(power(vecA - vecB, 2))) # la.norm(vecA-vecB)
78+
```
79+
80+
#### 构建一个包含 K 个随机质心的集合
81+
82+
```python
83+
# 为给定数据集构建一个包含 k 个随机质心的集合。随机质心必须要在整个数据集的边界之内,这可以通过找到数据集每一维的最小和最大值来完成。然后生成 0~1.0 之间的随机数并通过取值范围和最小值,以便确保随机点在数据的边界之内。
84+
def randCent(dataSet, k):
85+
n = shape(dataSet)[1] # 列的数量
86+
centroids = mat(zeros((k,n))) # 创建k个质心矩阵
87+
for j in range(n): # 创建随机簇质心,并且在每一维的边界内
88+
minJ = min(dataSet[:,j]) # 最小值
89+
rangeJ = float(max(dataSet[:,j]) - minJ) # 范围 = 最大值 - 最小值
90+
centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1)) # 随机生成
91+
return centroids
92+
```
93+
94+
#### K-Means 聚类算法
95+
96+
```python
97+
# k-means 聚类算法
98+
# 该算法会创建k个质心,然后将每个点分配到最近的质心,再重新计算质心。
99+
# 这个过程重复数次,知道数据点的簇分配结果不再改变位置。
100+
# 运行结果(多次运行结果可能会不一样,可以试试,原因为随机质心的影响,但总的结果是对的, 因为数据足够相似,也可能会陷入局部最小值)
101+
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
102+
m = shape(dataSet)[0] # 行数
103+
clusterAssment = mat(zeros((m, 2))) # 创建一个与 dataSet 行数一样,但是有两列的矩阵,用来保存簇分配结果
104+
centroids = createCent(dataSet, k) # 创建质心,随机k个质心
105+
clusterChanged = True
106+
while clusterChanged:
107+
clusterChanged = False
108+
for i in range(m): # 循环每一个数据点并分配到最近的质心中去
109+
minDist = inf; minIndex = -1
110+
for j in range(k):
111+
distJI = distMeas(centroids[j,:],dataSet[i,:]) # 计算数据点到质心的距离
112+
if distJI < minDist: # 如果距离比 minDist(最小距离)还小,更新 minDist(最小距离)和最小质心的 index(索引)
113+
minDist = distJI; minIndex = j
114+
if clusterAssment[i, 0] != minIndex: # 簇分配结果改变
115+
clusterChanged = True # 簇改变
116+
clusterAssment[i, :] = minIndex,minDist**2 # 更新簇分配结果为最小质心的 index(索引),minDist(最小距离)的平方
117+
print centroids
118+
for cent in range(k): # 更新质心
119+
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A==cent)[0]] # 获取该簇中的所有点
120+
centroids[cent,:] = mean(ptsInClust, axis=0) # 将质心修改为簇中所有点的平均值,mean 就是求平均值的
121+
return centroids, clusterAssment
122+
```
123+
124+
#### 测试函数
125+
1. 测试一下以上的基础函数是否可以如预期运行, 请看: <https://github.com/apachecn/MachineLearning/blob/master/src/python/10.kmeans/kMeans.py>
126+
2. 测试一下 kMeans 函数是否可以如预期运行, 请看: <https://github.com/apachecn/MachineLearning/blob/master/src/python/10.kmeans/kMeans.py>
127+
128+
参考运行结果如下:
129+
![K-Means 运行结果1](../images/10.KMeans/apachecn-k-means-run-result-1.jpg)
130+
131+
> 在 kMeans 的函数测试中,可能偶尔会陷入局部最小值(局部最优的结果,但不是全局最优的结果).
132+
133+
### K-Means 聚类算法的缺陷
134+
在 kMeans 的函数测试中,可能偶尔会陷入局部最小值(局部最优的结果,但不是全局最优的结果).
135+
所以为了客户 KMeans 算法收敛于局部最小值的问题,有更厉害的大佬提出了另一个称为二分K-均值(bisecting K-Means)的算法.
136+
137+
### 二分 K-Means 聚类算法
138+
该算法首先将说有点作为一个簇,然后将该簇一分为二。
139+
之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分时候可以最大程度降低 SSE(平方和误差)的值。
140+
上述基于 SSE 的划分过程不断重复,直到得到用户指定的簇数目为止。
141+
142+
#### 二分 K-Means 聚类算法伪代码
143+
* 将所有点看成一个簇
144+
* 当簇数目小雨 k 时
145+
* 对于每一个簇
146+
* 计算总误差
147+
* 在给定的簇上面进行 KMeans 聚类(k=2)
148+
* 计算将该簇一分为二之后的总误差
149+
* 选择使得误差最小的那个簇进行划分操作
150+
151+
另一种做法是选择 SSE 最大的簇进行划分,直到簇数目达到用户指定的数目位置。
152+
接下来主要介绍该做法。
153+
154+
#### 二分 K-Means 聚类算法代码
155+
156+
```python
157+
# 二分 KMeans 聚类算法, 基于 kMeans 基础之上的优化,以避免陷入局部最小值
158+
def biKMeans(dataSet, k, distMeas=distEclud):
159+
m = shape(dataSet)[0]
160+
clusterAssment = mat(zeros((m,2))) # 保存每个数据点的簇分配结果和平方误差
161+
centroid0 = mean(dataSet, axis=0).tolist()[0] # 质心初始化为所有数据点的均值
162+
centList =[centroid0] # 初始化只有 1 个质心的 list
163+
for j in range(m): # 计算所有数据点到初始质心的距离平方误差
164+
clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
165+
while (len(centList) < k): # 当质心数量小于 k 时
166+
lowestSSE = inf
167+
for i in range(len(centList)): # 对每一个质心
168+
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] # 获取当前簇 i 下的所有数据点
169+
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 将当前簇 i 进行二分 kMeans 处理
170+
sseSplit = sum(splitClustAss[:,1]) # 将二分 kMeans 结果中的平方和的距离进行求和
171+
sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1]) # 将未参与二分 kMeans 分配结果中的平方和的距离进行求和
172+
print "sseSplit, and notSplit: ",sseSplit,sseNotSplit
173+
if (sseSplit + sseNotSplit) < lowestSSE:
174+
bestCentToSplit = i
175+
bestNewCents = centroidMat
176+
bestClustAss = splitClustAss.copy()
177+
lowestSSE = sseSplit + sseNotSplit
178+
# 找出最好的簇分配结果
179+
bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) # 调用二分 kMeans 的结果,默认簇是 0,1. 当然也可以改成其它的数字
180+
bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit # 更新为最佳质心
181+
print 'the bestCentToSplit is: ',bestCentToSplit
182+
print 'the len of bestClustAss is: ', len(bestClustAss)
183+
# 更新质心列表
184+
centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0] # 更新原质心 list 中的第 i 个质心为使用二分 kMeans 后 bestNewCents 的第一个质心
185+
centList.append(bestNewCents[1,:].tolist()[0]) # 添加 bestNewCents 的第二个质心
186+
clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss # 重新分配最好簇下的数据(质心)以及SSE
187+
return mat(centList), clusterAssment
188+
```
189+
190+
#### 测试二分 KMeans 聚类算法
191+
* 测试一下二分 KMeans 聚类算法,请看: <https://github.com/apachecn/MachineLearning/blob/master/src/python/10.kmeans/kMeans.py>
192+
193+
上述函数可以运行多次,聚类会收敛到全局最小值,而原始的 kMeans() 函数偶尔会陷入局部最小值。
194+
运行参考结果如下:
195+
![二分 K-Means 运行结果1](../images/10.KMeans/apachecn-bikmeans-run-result-1.jpg)
49.7 KB
Loading
51.3 KB
Loading
74.2 KB
Loading

input/10.KMeans/testSet2.txt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
3.275154 2.957587
2+
-3.344465 2.603513
3+
0.355083 -3.376585
4+
1.852435 3.547351
5+
-2.078973 2.552013
6+
-0.993756 -0.884433
7+
2.682252 4.007573
8+
-3.087776 2.878713
9+
-1.565978 -1.256985
10+
2.441611 0.444826
11+
-0.659487 3.111284
12+
-0.459601 -2.618005
13+
2.177680 2.387793
14+
-2.920969 2.917485
15+
-0.028814 -4.168078
16+
3.625746 2.119041
17+
-3.912363 1.325108
18+
-0.551694 -2.814223
19+
2.855808 3.483301
20+
-3.594448 2.856651
21+
0.421993 -2.372646
22+
1.650821 3.407572
23+
-2.082902 3.384412
24+
-0.718809 -2.492514
25+
4.513623 3.841029
26+
-4.822011 4.607049
27+
-0.656297 -1.449872
28+
1.919901 4.439368
29+
-3.287749 3.918836
30+
-1.576936 -2.977622
31+
3.598143 1.975970
32+
-3.977329 4.900932
33+
-1.791080 -2.184517
34+
3.914654 3.559303
35+
-1.910108 4.166946
36+
-1.226597 -3.317889
37+
1.148946 3.345138
38+
-2.113864 3.548172
39+
0.845762 -3.589788
40+
2.629062 3.535831
41+
-1.640717 2.990517
42+
-1.881012 -2.485405
43+
4.606999 3.510312
44+
-4.366462 4.023316
45+
0.765015 -3.001270
46+
3.121904 2.173988
47+
-4.025139 4.652310
48+
-0.559558 -3.840539
49+
4.376754 4.863579
50+
-1.874308 4.032237
51+
-0.089337 -3.026809
52+
3.997787 2.518662
53+
-3.082978 2.884822
54+
0.845235 -3.454465
55+
1.327224 3.358778
56+
-2.889949 3.596178
57+
-0.966018 -2.839827
58+
2.960769 3.079555
59+
-3.275518 1.577068
60+
0.639276 -3.412840

src/python/10.kmeans/kMeans.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
# 从文本中构建矩阵,加载文本文件,然后处理
1414
def loadDataSet(fileName): # 通用函数,用来解析以 tab 键分隔的 floats(浮点数)
15-
dataMat = [] # 假设最后一列是目标变量
15+
dataMat = []
1616
fr = open(fileName)
1717
for line in fr.readlines():
1818
curLine = line.strip().split('\t')
@@ -28,7 +28,7 @@ def distEclud(vecA, vecB):
2828

2929
# 为给定数据集构建一个包含 k 个随机质心的集合。随机质心必须要在整个数据集的边界之内,这可以通过找到数据集每一维的最小和最大值来完成。然后生成 0~1.0 之间的随机数并通过取值范围和最小值,以便确保随机点在数据的边界之内。
3030
def randCent(dataSet, k):
31-
n = shape(dataSet)[1] # 列的数俩
31+
n = shape(dataSet)[1] # 列的数量
3232
centroids = mat(zeros((k,n))) # 创建k个质心矩阵
3333
for j in range(n): # 创建随机簇质心,并且在每一维的边界内
3434
minJ = min(dataSet[:,j]) # 最小值
@@ -51,21 +51,52 @@ def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
5151
for i in range(m): # 循环每一个数据点并分配到最近的质心中去
5252
minDist = inf; minIndex = -1
5353
for j in range(k):
54-
distJI = distMeas(centroids[j,:],dataSet[i,:]) # 计算距离
54+
distJI = distMeas(centroids[j,:],dataSet[i,:]) # 计算数据点到质心的距离
5555
if distJI < minDist: # 如果距离比 minDist(最小距离)还小,更新 minDist(最小距离)和最小质心的 index(索引)
5656
minDist = distJI; minIndex = j
57-
if clusterAssment[i, 0] != minIndex:
58-
clusterChanged = True
57+
if clusterAssment[i, 0] != minIndex: # 簇分配结果改变
58+
clusterChanged = True # 簇改变
5959
clusterAssment[i, :] = minIndex,minDist**2 # 更新簇分配结果为最小质心的 index(索引),minDist(最小距离)的平方
6060
print centroids
6161
for cent in range(k): # 更新质心
6262
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A==cent)[0]] # 获取该簇中的所有点
6363
centroids[cent,:] = mean(ptsInClust, axis=0) # 将质心修改为簇中所有点的平均值,mean 就是求平均值的
6464
return centroids, clusterAssment
6565

66-
67-
if __name__ == "__main__":
68-
66+
# 二分 KMeans 聚类算法, 基于 kMeans 基础之上的优化,以避免陷入局部最小值
67+
def biKMeans(dataSet, k, distMeas=distEclud):
68+
m = shape(dataSet)[0]
69+
clusterAssment = mat(zeros((m,2))) # 保存每个数据点的簇分配结果和平方误差
70+
centroid0 = mean(dataSet, axis=0).tolist()[0] # 质心初始化为所有数据点的均值
71+
centList =[centroid0] # 初始化只有 1 个质心的 list
72+
for j in range(m): # 计算所有数据点到初始质心的距离平方误差
73+
clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
74+
while (len(centList) < k): # 当质心数量小于 k 时
75+
lowestSSE = inf
76+
for i in range(len(centList)): # 对每一个质心
77+
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] # 获取当前簇 i 下的所有数据点
78+
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 将当前簇 i 进行二分 kMeans 处理
79+
sseSplit = sum(splitClustAss[:,1]) # 将二分 kMeans 结果中的平方和的距离进行求和
80+
sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1]) # 将未参与二分 kMeans 分配结果中的平方和的距离进行求和
81+
print "sseSplit, and notSplit: ",sseSplit,sseNotSplit
82+
if (sseSplit + sseNotSplit) < lowestSSE:
83+
bestCentToSplit = i
84+
bestNewCents = centroidMat
85+
bestClustAss = splitClustAss.copy()
86+
lowestSSE = sseSplit + sseNotSplit
87+
# 找出最好的簇分配结果
88+
bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) # 调用二分 kMeans 的结果,默认簇是 0,1. 当然也可以改成其它的数字
89+
bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit # 更新为最佳质心
90+
print 'the bestCentToSplit is: ',bestCentToSplit
91+
print 'the len of bestClustAss is: ', len(bestClustAss)
92+
# 更新质心列表
93+
centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0] # 更新原质心 list 中的第 i 个质心为使用二分 kMeans 后 bestNewCents 的第一个质心
94+
centList.append(bestNewCents[1,:].tolist()[0]) # 添加 bestNewCents 的第二个质心
95+
clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss # 重新分配最好簇下的数据(质心)以及SSE
96+
return mat(centList), clusterAssment
97+
98+
def testBasicFunc():
99+
# 加载测试数据集
69100
datMat = mat(loadDataSet('input/10.KMeans/testSet.txt'))
70101

71102
# 测试 randCent() 函数是否正常运行。
@@ -76,14 +107,35 @@ def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
76107
print 'max(datMat[:, 0])=', max(datMat[:, 0])
77108

78109
# 然后看看 randCent() 函数能否生成 min 到 max 之间的值
79-
# print 'randCent(datMat, 2)=', randCent(datMat, 2)
110+
print 'randCent(datMat, 2)=', randCent(datMat, 2)
80111

81112
# 最后测试一下距离计算方法
82-
# print ' distEclud(datMat[0], datMat[1])=', distEclud(datMat[0], datMat[1])
113+
print ' distEclud(datMat[0], datMat[1])=', distEclud(datMat[0], datMat[1])
114+
115+
def testKMeans():
116+
# 加载测试数据集
117+
datMat = mat(loadDataSet('input/10.KMeans/testSet.txt'))
83118

84119
# 该算法会创建k个质心,然后将每个点分配到最近的质心,再重新计算质心。
85120
# 这个过程重复数次,知道数据点的簇分配结果不再改变位置。
86121
# 运行结果(多次运行结果可能会不一样,可以试试,原因为随机质心的影响,但总的结果是对的, 因为数据足够相似)
87122
myCentroids, clustAssing = kMeans(datMat, 4)
88123

89-
# print 'centroids=', myCentroids
124+
print 'centroids=', myCentroids
125+
126+
def testBiKMeans():
127+
# 加载测试数据集
128+
datMat = mat(loadDataSet('input/10.KMeans/testSet2.txt'))
129+
130+
centList, myNewAssments = biKMeans(datMat, 3)
131+
132+
if __name__ == "__main__":
133+
134+
# 测试基础的函数
135+
# testBasicFunc()
136+
137+
# 测试 kMeans 函数
138+
# testKMeans()
139+
140+
# 测试二分 biKMeans 函数
141+
testBiKMeans()

0 commit comments

Comments
 (0)