终于进入无监督学习的部分了,首先介绍k-means聚类和二分k-means聚类
1. k-means聚类
k-means聚类将相似的对象归到同一个簇中,每个簇的中心采用簇中所含值的均值计算而成。
优点:容易实现
缺点:可能收敛到局部最小值,在大规模数据上收敛较慢
适用数据类型:数值型数据
伪代码:
创建k个点作为起始质心(随机选择)
当任意一个点的簇分配结果发生改变时:
对数据集中的每个数据点:
对每个质心:
计算数据点到质心的距离
将数据点分配到距其最近的簇
对每一个簇重新计算其均值作为质心
实现代码如下:
#!/usr/bin/env python # encoding: utf-8 ''' @author: shuhan Wei @software: pycharm @file: kMeans.py @time: 18-9-18 下午3:32 @desc: ''' import numpy as np import matplotlib.pyplot as plt def loadDataSet(fileName): dataMat = [] fr = open(fileName) for line in fr.readlines(): curline = line.strip().split('\t') fltLine = map(float, curline) dataMat.append(list(fltLine)) return dataMat def distEclud(vecA, vecB): """ 函数说明:计算两个向量的欧式距离 :param vecA: :param vecB: :return: """ return np.sqrt(np.sum(np.power(vecA - vecB, 2))) #la.norm(vecA-vecB) def randCent(dataSet, k): """ 函数说明:随机选取k个聚类质心 :param dataSet: 数据集 :param k: 质心个数 :return: 质心 """ n = np.shape(dataSet)[1] centroids = np.mat(np.zeros((k,n))) #创建质心向量 for j in range(n):#create random cluster centers, within bounds of each dimension minJ = np.min(dataSet[:,j]) rangeJ = float(np.max(dataSet[:,j]) - minJ) centroids[:,j] = np.mat(minJ + rangeJ * np.random.rand(k,1)) #np.random.rand(k,1)随机生成k*1的[0,1)的数 return centroids def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): """ 函数说明:k-均值聚类函数 :param dataSet: 数据集 :param k: 分为k类 :param distMeas: 计算数据到质心距离 :param createCent: 创建质心函数 :return: centroids: 质心位置 clusterAssment: 第一列是所属分类下标,第二列是点到质心距离 """ """ 伪代码: 创建k个点作为起始质心(随机选择) 当任意一个点的簇分类结果发生改变时: 对数据集中的每个数据点: 对每个质心: 计算点到质心的距离 将数据点分配到距其最近的簇 根据每个簇的均值重新计算每个质心 """ m = np.shape(dataSet)[0] clusterAssment = np.mat(np.zeros((m,2))) #第一列存放簇类index,第二列存放误差值 centroids = createCent(dataSet, k) clusterChanged = True while clusterChanged: clusterChanged = False for i in range(m): #遍历每一行数据,将数据划分到最近的质心 minDist = np.inf; minIndex = -1 for j in range(k): #计算第i个数据到每个质心的距离 distJI = distMeas(centroids[j,:],dataSet[i,:]) if distJI < minDist: minDist = distJI; minIndex = j if clusterAssment[i,0] != minIndex: clusterChanged = True clusterAssment[i,:] = minIndex, minDist**2 print(centroids) for cent in range(k): #重新计算质心 ptsInClust = dataSet[np.nonzero(clusterAssment[:,0].A==cent)[0]]#获取某个簇类的所有点 centroids[cent,:] = np.mean(ptsInClust, axis=0) #计算均值作为新的簇类中心 return centroids, clusterAssment def plot(dataSet): """ 函数说明:绘制原数据集 :param dataSet: :return: """ x = dataSet[:, 0].tolist() y = dataSet[:, 1].tolist() plt.scatter(x, y) plt.show() def plotKMeans(dataSet, clusterAssment, cenroids): """ 函数说明:绘制聚类后情况 :param dataSet: 数据集 :param clusterAssment: 聚类结果 :param cenroids: 质心坐标 :return: """ m = np.shape(dataSet)[0] x0 = dataSet[np.nonzero(clusterAssment[:, 0] == 0), 0][0].tolist() y0 = dataSet[np.nonzero(clusterAssment[:, 0] == 0), 1][0].tolist() x1 = dataSet[np.nonzero(clusterAssment[:, 0] == 1), 0][0].tolist() y1 = dataSet[np.nonzero(clusterAssment[:, 0] == 1), 1][0].tolist() x2 = dataSet[np.nonzero(clusterAssment[:, 0] == 2), 0][0].tolist() y2 = dataSet[np.nonzero(clusterAssment[:, 0] == 2), 1][0].tolist() x3 = dataSet[np.nonzero(clusterAssment[:, 0] == 3), 0][0].tolist() y3 = dataSet[np.nonzero(clusterAssment[:, 0] == 3), 1][0].tolist() plt.scatter(x0, y0, color = 'red', marker='*') plt.scatter(x1, y1, color = 'yellow', marker='o') plt.scatter(x2, y2, color = 'blue', marker='s') plt.scatter(x3, y3, color = 'green', marker='^') for i in range(np.shape(cenroids)[0]): plt.scatter(cenroids[i, 0], cenroids[i, 1], color='k', marker='+', s=200) # plt.plot(cenroids[0,0], cenroids[0,1], 'k+', cenroids[1,0], cenroids[1,1], 'k+',cenroids[2,0], # cenroids[2,1], 'k+',cenroids[3,0], cenroids[3,1], 'k+',) plt.show() if __name__ == '__main__': dataSet = loadDataSet('testSet.txt') dataMat = np.mat(dataSet) plot(dataMat) cenroids, clusterAssment = kMeans(dataMat, 4) print(cenroids, clusterAssment) plotKMeans(dataMat, clusterAssment, cenroids)
选择k=4进行聚类,结果如下:
2. 二分k-means聚类
k-means的缺点是可能收敛于局部最优解,使用二分k-means聚类算法来解决这个问题。
可以使用SSE(sum of squared error误差平方和)来度量聚类的效果,SSE值越小表示数据点接近于它的质心,聚类效果越好。因为对误差取了平方,因此更加重视那些远离质心的点。
二分k-means聚类算法的思想是先将所有数据点当作一个簇,然后将该簇一分为二。之后在现有的簇中选择一个簇进行划分,选择哪个簇取决于划分哪个簇后能使SSE值最小。不断重复上述过程,直到达到用户要求的簇的个数。
伪代码: 将所有数据点都看成一个簇 当簇的数目小于k时: 初始化lowestSSE = inf 对于每一个簇: 对该簇进行k-means聚类(k=2) 计算聚类后的总误差 如果小于lowestSSE,则保存聚类后的参数,更新lowestSEE 选择划分后使得误差值最小的那个簇进行划分
具体实现代码如下:
def biKmeans(dataSet, k, distMeas=distEclud): """ 函数说明:二分K-均值算法 :param dataSet: :param k: :param distMeas: :return: """ """ 伪代码: 将所有数据点都看成一个簇 当簇的数目小于k时: 初始化lowestSSE = inf 对于每一个簇: 对该簇进行k-means聚类(k=2) 计算聚类后的总误差 如果小于lowestSSE,则保存聚类后的参数,更新lowestSEE 选择划分后使得误差值最小的那个簇进行划分 """ m = np.shape(dataSet)[0] clusterAssment = np.mat(np.zeros((m,2))) #创建一个初始簇 centroid0 = np.mean(dataSet, axis=0).tolist()[0] centList = [centroid0] #用来保存质心的列表 for j in range(m): #初始化簇中每个点的误差值 clusterAssment[j, 1] = distMeas(np.mat(centroid0), dataSet[j,:])**2 while(len(centList) < k): lowestSSE = np.inf for i in range(len(centList)): ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:,0].A==i)[0],:] #获取属于第i个簇类的所有数据点 centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) #对属于第i个簇类的所有数据点进行k=2的聚类 sseSplit = np.sum(splitClustAss[:,1]) #计算对第i个簇类进行聚类后的sse值 sseNotSplit = np.sum(clusterAssment[np.nonzero(clusterAssment[:,0].A!=i)[0],1]) #计算不属于第i类的所有数据点的sse值 print("sseSplit, and notSplit: ",sseSplit,sseNotSplit) if (sseSplit + sseNotSplit) < lowestSSE: #将聚类后的sse值与最低sse值进行比较 bestCentToSplit = i bestNewCents = centroidMat bestClustAss = splitClustAss.copy() lowestSSE = sseSplit + sseNotSplit bestClustAss[np.nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #将1改变为新增簇的编号 bestClustAss[np.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] centList.append(bestNewCents[1,:].tolist()[0]) clusterAssment[np.nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss #用新的聚类结果替换原来的 return np.mat(centList), clusterAssment if __name__ == '__main__': dataSet2 = loadDataSet('testSet2.txt') dataMat2 = np.mat(dataSet2) cenroids2, clusterAssment2 = biKmeans(dataMat2, 3) plotKMeans(dataMat2, clusterAssment2, cenroids2) print('the error of biKmeans:', sum(clusterAssment2[:, 1]))
3. 实例——对地图上的点进行聚类
问题是:你的朋友Drew希望你带他去城里庆祝生日,他想要一个晚上去70个地方,给了你这些地方的坐标,你需要将这些地方进行聚类,之后坐车抵达簇的中心,然后步行到簇的其他地方。
实现代码如下:
#!/usr/bin/env python # encoding: utf-8 ''' @author: shuhan Wei @software: pycharm @file: placeFinder.py @time: 18-9-18 下午9:04 @desc: ''' import numpy as np from math import radians, cos, sin, asin, sqrt import kMeans import matplotlib.pyplot as plt def distSLC(vecA, vecB): """ 函数说明:计算根据经纬度计算两点之间的距离 :param vecA: 一个点坐标向量 :param vecB: 另一个点坐标向量 :return: 距离 """ a = sin(vecA[0,1] * np.pi/180) * sin(vecB[0,1] * np.pi/180) b = cos(vecA[0,1]* np.pi/180) * cos(vecB[0,1]* np.pi/180) * \ cos(np.pi * (vecB[0,0]-vecA[0,0]) /180) return np.arccos(a + b)*6371.0 def clusterClubs(numClust = 5): """ 函数说明:对地图坐标进行聚类,并在地图图片上显示聚类结果 :param numClust: 聚类数目 :return: """ clubsCoordinate = [] fr = open('places.txt') for line in fr.readlines(): lineCur = line.strip().split('\t') # lineMat = np.mat(lineCur)[0, -2:] #获取最后两列经纬度 # fltLinr = map(float, lineMat.tolist()[0]) # clubsCoordinate.append(list(fltLinr)) clubsCoordinate.append([float(lineCur[-1]), float(lineCur[-2])]) #获取最后两列经纬度 clubsCoordinateMat = np.mat(clubsCoordinate) cenroids, clusterAssment = kMeans.biKmeans(clubsCoordinateMat, numClust,distMeas=distSLC) kMeans.plotKMeans(clubsCoordinateMat, clusterAssment, cenroids) fig = plt.figure() rect = [0.1, 0.1, 0.8, 0.8] #使用矩阵来设置图片占绘制面板的位置,左下角0.1,0.1,右上角0.8,0.8 scatterMarkers = ['s', 'o', '^', '8', 'p', 'd', 'v', 'h', '>', '<'] #形状列表 axprops = dict(xticks=[], yticks=[]) ax0 = fig.add_axes(rect, label='ax0', **axprops) imgP = plt.imread('Portland.png') #基于一幅图像来创建矩阵 ax0.imshow(imgP) #绘制该矩阵 ax1 = fig.add_axes(rect, label='ax1', frameon=False) for i in range(numClust): ptsInCurrCluster = clubsCoordinateMat[np.nonzero(clusterAssment[:, 0].A == i)[0], :] markerStyle = scatterMarkers[i % len(scatterMarkers)] ax1.scatter(ptsInCurrCluster[:, 0].flatten().A[0], ptsInCurrCluster[:, 1].flatten().A[0], marker=markerStyle, s=90) #flatten()将m*n的矩阵转化为1*(m×n)的矩阵,.A[0]矩阵转化为数组后获取数组第一维数据 ax1.scatter(cenroids[:, 0].flatten().A[0], cenroids[:, 1].flatten().A[0], marker='+', s=300) print(sum(clusterAssment[:,1])) plt.show() if __name__ == '__main__': clusterClubs(4)
运行结果如下: