机器学习中有两类的大问题,一个是分类,一个是聚类。分类是根据一些给定的已知类别标号的样本,训练某种学习机器,使它能够对未知类别的样本进行分类。这属于supervised learning(监督学习)。而聚类指事先并不知道任何样本的类别标号,希望通过某种算法来把一组未知类别的样本划分成若干类别,这在机器学习中被称作 unsupervised learning (无监督学习)。在本文中,我们关注其中一个比较简单的聚类算法:k-means算法。
一、k-means算法
通常,人们根据样本间的某种距离或者相似性来定义聚类,即把相似的(或距离近的)样本聚为同一类,而把不相似的(或距离远的)样本归在其他类。
我们以一个二维的例子来说明下聚类的目的。如下图左所示,假设我们的n个样本点分布在图中所示的二维空间。从数据点的大致形状可以看出它们大致聚为三个cluster,其中两个紧凑一些,剩下那个松散一些。我们的目的是为这些数据分组,以便能区分出属于不同的簇的数据,如果按照分组给它们标上不同的颜色,就是像下图右边的图那样:
如果人可以看到像上图那样的数据分布,就可以轻松进行聚类。但我们怎么教会计算机按照我们的思维去做同样的事情呢?这里就介绍个集简单和经典于一身的k-means算法。
k-means算法是一种很常见的聚类算法,它的基本思想是:通过迭代寻找k个聚类的一种划分方案,使得用这k个聚类的均值来代表相应各类样本时所得的总体误差最小。
k-means算法的基础是最小误差平方和准则。其代价函数是:
式中,μc(i)表示第i个聚类的均值。我们希望代价函数最小,直观的来说,各类内的样本越相似,其与该类均值间的误差平方越小,对所有类所得到的误差平方求和,即可验证分为k类时,各聚类是否是最优的。
上式的代价函数无法用解析的方法最小化,只能有迭代的方法。k-means算法是将样本聚类成 k个簇(cluster),其中k是用户给定的,其求解过程非常直观简单,具体算法描述如下:
1、随机选取 k个聚类质心点
2、重复下面过程直到收敛 {
对于每一个样例 i,计算其应该属于的类:
对于每一个类 j,重新计算该类的质心:
}
下图展示了对n个样本点进行K-means聚类的效果,这里k取2。
其伪代码如下:
********************************************************************
创建k个点作为初始的质心点(随机选择)
当任意一个点的簇分配结果发生改变时
对数据集中的每一个数据点
对每一个质心
计算质心与数据点的距离
将数据点分配到距离最近的簇
对每一个簇,计算簇中所有点的均值,并将均值作为质心
********************************************************************
二、Python实现
我使用的Python是2.7.5版本的。附加的库有Numpy和Matplotlib。具体的安装和配置见前面的博文。在代码中已经有了比较详细的注释了。不知道有没有错误的地方,如果有,还望大家指正(每次的运行结果都有可能不同)。里面我写了个可视化结果的函数,但只能在二维的数据上面使用。直接贴代码:
kmeans.py
三、测试结果
测试数据是二维的,共80个样本。有4个类。如下:
testSet.txt
测试代码:
test_kmeans.py
运行的前后结果是:
不同的类用不同的颜色来表示,其中的大菱形是对应类的均值质心点。
四、算法分析
k-means算法比较简单,但也有几个比较大的缺点:
(1)k值的选择是用户指定的,不同的k得到的结果会有挺大的不同,如下图所示,左边是k=3的结果,这个就太稀疏了,蓝色的那个簇其实是可以再划分成两个簇的。而右图是k=5的结果,可以看到红色菱形和蓝色菱形这两个簇应该是可以合并成一个簇的:
(2)对k个初始质心的选择比较敏感,容易陷入局部最小值。例如,我们上面的算法运行的时候,有可能会得到不同的结果,如下面这两种情况。K-means也是收敛了,只是收敛到了局部最小值:
(3)存在局限性,如下面这种非球状的数据分布就搞不定了:
(4)数据库比较大的时候,收敛会比较慢。
k-means老早就出现在江湖了。所以以上的这些不足也被世人的目光敏锐的捕捉到,并融入世人的智慧进行了某种程度上的改良。例如问题(1)对k的选择可以先用一些算法分析数据的分布,如重心和密度等,然后选择合适的k。而对问题(2),有人提出了另一个成为二分k均值(bisecting k-means)算法,它对初始的k个质心的选择就不太敏感,这个算法我们下一个博文再分析和实现。
<span style="font-size:12px;">#coding=utf-8 ''' Created on Feb 16, 2011 k Means Clustering for Ch10 of Machine Learning in Action @author: Peter Harrington 首先,收集用户的信息,可以同时收集用户满意或 不满意的信息,这是因为任何对用户重要的内容都可能影响用户的投票结果。然后,将这些信息 输人到某个聚类算法中。接着,对聚类结果中的每一个簇(最好选择最大簇),精心构造能够吸 引该簇选民的消息。最后,开展竞选活动并观察上述做法是否有效。 聚类是一种无监督的学习,它将相似的对象归到同一个簇中。它有点像全自动分类②。聚类 方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好。本章要学习一种称为K- 均值(K-means)聚类的算法。之所以称之为K一均值是因为它可以发现k个不同的簇,且每个簇的 中心采用簇中所含值的均值计算而成。 K-均值聚类 优点:容易实现。 缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢。 适用数据类型:数值型数据。 K-均值是发现给定数据集的k个簇的算法。簇个数k是用户给定的,每一个簇通过其质心 centroid ),即簇中所有点的中心来描述。 K-均值算法的工作流程是这样的。首先,随机确定k个初始点作为质心。然后将数据集中的 每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的 簇。这一步完成之后,每个簇的质心更新为该簇所有点的平均值。 创建k个点作为起始质心(经常是随机选择) 当任意一个点的簇分配结果发生改变时 对数据集中的每个数据点 ·对每个质心 计算质心与数据点之间的距离 将数据点分配到距其最近的簇 对每一个簇,计算簇中所有点的均值并将均值作为质心 K-均值聚类的一般流程 (1)收集数据:使用任意方法。 (2)准备数据:需要数值型数据来计算距离 于距离计算。 (3)分析数据:使用任意方法。 也可以将标称型数据映射为二值型数据再用 (4) 训练算法:不适用于无监督学习,即无监督学习没有训练过程。 (5)测试算法:应用聚类算法、观察结果 会介绍)来评价算法的结果。 。可以使用量化的误差指标如误差平方和(后面 (6)使用算法:可以用于所希望的任何应用。通常情况下,簇质心可以代表整个簇的数据 来做出决策。 ''' from numpy import * ''' K-均值聚类支持函数 程序清单10-1中的代码包含几个K-均值算法中要用到的辅助函数。第一个函数loadData- Set()和上一章完全相同,它将文本文件导人到一个列表中。文本文件每一行为tab分隔的浮点数。 每一个列表会被添加到dataMat中,最后返回dataMat。该返回值是一个包含许多其他列表的列 表。这种格式可以很容易将很多值封装到矩阵中。 下一个函数distEclud()计算两个向量的欧式距离。这是本章最先使用的距离函数,也可 以使用其他距离函数。 最后一个函数是randCent(),该函数为给定数据集构建一个包含k个随机质心的集合。随机 质心必须要在整个数据集的边界之内,这可以通过找到数据集每一维的最小和最大值来完成。然 后生成。到1.。之间的随机数并通过取值范围和最小值,以便确保随机点在数据的边界之内。 >>> from numpy import * >>> import kMeans >>> datMat = mat(kMeans.loadDataSet('testSet.txt')) >>> min(datMat[:,0]) matrix([[-5.379713]]) >>> min(datMat[:,1]) matrix([[-4.232586]]) >>> max(datMat[:,1]) matrix([[ 5.1904]]) >>> max(datMat[:,0]) matrix([[ 4.838138]]) >>> kMeans.randCent(datMat,2) matrix([[-3.22924597, 2.59331249], [ 3.23970194, 3.49877695]]) >>> kMeans.distEclud(datMat[0],datMat[1]) 5.184632816681332 所有支持函数正常运行之后,就可以准备实现完整的K-均值算法了。该算法会创建k个质心, 然后将每个点分配到最近的质心,再重新计算质心。这个过程重复数次,直到数据点的簇分配结 果不再改变为止。 ''' def loadDataSet(fileName): #general function to parse tab -delimited floats dataMat = [] #assume last column is target value fr = open(fileName) for line in fr.readlines(): curLine = line.strip().split('\t') fltLine = map(float,curLine) #map all elements to float() dataMat.append(fltLine) return dataMat def distEclud(vecA, vecB): return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB) def randCent(dataSet, k): n = shape(dataSet)[1] centroids = mat(zeros((k,n)))#create centroid mat for j in range(n):#create random cluster centers, within bounds of each dimension minJ = min(dataSet[:,j]) rangeJ = float(max(dataSet[:,j]) - minJ) centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1)) return centroids ''' K-均值聚类算法 上述清单给出了K一均值算法。kMeans()函数接受4个输人参数。只有数据集及簇的数目是必 选参数,而用来计算距离和创建初始质心的函数都是可选的。kMeans()函数一开始确定数据集中 数据点的总数,然后创建一个矩阵来存储每个点的簇分配结果簇分配结果矩阵clusterAssment 包含两列:一列记录簇索引值,第二列存储误差。这里的误差是指当前点到簇质心的距离,后边 会使用该误差来评价聚类的效果。 直到所有数据点的簇分配结果不再 改变为止。程序中可以创建一个标志变量clusterChanged,如果该值为True,则继续迭代。 上述迭代使用while循环来实现。接下来遍历所有数据找到距离每个点最近的质心,这可以通过 对每个点遍历所有质心并计算点到每个质心的距离来完成0。计算距离是使用distMeas参数给 出的距离函数,默认距离函数是distEclud(),该函数的实现已经在程序清单10-1中给出。如果 任一点的簇分配结果发生改变,则更新 clusterChanged标志。 最后,遍历所有质心并更新它们的取值. 具体实现步骤如下:首先通过数组过滤来获得给 定簇的所有点; 然后计算所有点的均值,选项axis =0表示沿矩阵的列方向进行均值计算;最 后,程序返回所有的类质 心与点分配结果。 考虑图10-2中的聚类结果,这是在一个包含三个簇的数据集上运行K一均值算法之后的结果, 但是点的簇分配结果值没有那么准确。K一均值算法收敛但聚类效果较差的原因是,K一均值算法收 敛到了局部最小值,而非全局最小值(局部最小值指结果还可以但并非最好结果,全局最小值是 可能的最好结果)。 一种用于度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和),对应程序清单10-2 中clusterAssment矩阵的第一列之和。SSE值越小表示数据点越接近于它们的质心,聚类效果也 越好。因为对误差取了平方,因此更加重视那些远离中心的点。一种肯定可以降低SSE值的方法是 增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。 有两种可以量化的办法:合并最近的质心,或者合并两个使得SSE增幅最小的质心。第一种 思路通过计算所有质心之间的距离,然后合并距离最近的两个点来实现。第二种方法需要合并两 个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程,直到找到合并最佳的两 个簇为止。 ''' def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): m = shape(dataSet)[0] clusterAssment = mat(zeros((m,2)))#create mat to assign data points #to a centroid, also holds SE of each point centroids = createCent(dataSet, k) clusterChanged = True while clusterChanged: clusterChanged = False for i in range(m):#for each data point assign it to the closest centroid minDist = inf; minIndex = -1 for j in range(k): 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):#recalculate centroids ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]#get all the point in this cluster centroids[cent,:] = mean(ptsInClust, axis=0) #assign centroid to mean return centroids, clusterAssment ''' 二分K-均值聚类算法 为克服K-均值算法收敛于局部最小值的问题,有人提出了另一个称为二分K-均值(bisecting K-means)的算法。该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个 簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述 基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。 二分K-均值算法的伪代码形式如下: 将所有点看成一个簇 当簇数目小于k时 对于每一个簇 计算总误差 在给定的簇上面进行K-均值聚类(1}2 ) 计算将该簇一分为二之后的总误差 选择使得误差最小的那个簇进行划分操作 该函数首先创建一个矩阵来存储数据集中每个点的簇分配结果及平方误差,然后计算整个数 据集的质心,并使用一个列表来保留所有的质心O。得到上述质心之后,可以遍历数据集中所有 可为 点来计算每个点到质心的误差值。这些误差值将会在后面用到。 接下来程序进人while循环,该循环会不停对簇进行划分,直到得到想要的簇数目为止。 以通过考察簇列表中的值来获得当前簇的数目。然后遍历所有的簇来决定最佳的簇进行划分。 此需要比较划分前后的SSE。一开始将最小SSE置设为无穷大,然后遍历簇列表centList中的每 一个簇。对每个簇,将该簇中的所有点看成一个小的数据集pts工nCurrCluster。将 ptsInCurrCluster输人到函数kMeans()中进行处理(k=2)o K一均值算法会生成两个质心 (簇),同时给出每个簇的误差值O。这些误差与剩余数据集的误差之和作为本次划分的误差。如 果该划分的SSE值最小,则本次划分被保存。一旦决定了要划分的簇,接下来就要实际执行划分 操作。划分操作很容易,只需要将要划分的簇中所有点的簇分配结果进行修改即可。当使用 kMeans()函数并且指定簇数为2时,会得到两个编号分别为。和1的结果簇。需要将这些簇编号修 改为划分簇及新加簇的编号,该过程可以通过两个数组过滤器来完成O。最后,新的簇分配结果 被更新,新的质心会被添加到centList中。 当while循环结吏时.同kMeans()函数一样,函数返回质心列表与簇分配结果。 ''' def biKmeans(dataSet, k, distMeas=distEclud): m = shape(dataSet)[0] clusterAssment = mat(zeros((m,2))) centroid0 = mean(dataSet, axis=0).tolist()[0] centList =[centroid0] #create a list with one centroid for j in range(m):#calc initial Error clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2 while (len(centList) < k): lowestSSE = inf for i in range(len(centList)): ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]#get the data points currently in cluster i centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) sseSplit = sum(splitClustAss[:,1])#compare the SSE to the currrent minimum 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 bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #change 1 to 3,4, or whatever bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit print 'the bestCentToSplit is: ',bestCentToSplit print 'the len of bestClustAss is: ', len(bestClustAss) centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#replace a centroid with two best centroids centList.append(bestNewCents[1,:].tolist()[0]) clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss#reassign new clusters, and SSE return mat(centList), clusterAssment ''' 对地图上的点进行聚类 对于地理数据应用二分K-均值算法 (1)收集数据:使用Yahoo! P1aceFinder API收集数据。 (2)准备数据:只保留经纬度信息。 (3)分析数据:使用Matplotlib来构建一个二维数据图,其中包含簇与地图。 (4)训练算法:训练不适用无监督学习。 (5)刚试算法:使用10.4节中的biKmeans()函数。 (6)使用算法:最后的输出是包含簇及簇中心的地图。 上述程序包含两个函数:geoGrab()与。assPlaceFind()。函数geoGrab()从、shoo!返回 一个字典,massPlaceFind()将所有这些封装起来并且将相关信息保存到文件中。 在函数geoGrab()中,首先为Yahoo API设置apiStem,然后创建一个字典。你可以为字典 设置不同值,包括flags=J,以便返回JSON格式的结果Do(不用担心你不熟悉JSON,它是 一种用于序列化数组和字典的文件格式,本书不会看到任何JSON o JSON是JavaScript Object Notation的缩写,有兴趣的读者可以在www.json.org找到更多信息。)接下来使用urllib的 urlencode()函数将创建的字典转换为可以通过URL进行传递的字符串格式。最后,打开URL 读取返回值。由于返回值是JSON格式的,所以可以使用JSON的Python模块来将其解码为一个字 典。一旦返回了解码后的字典,也就意味着你成功地对一个地址进行了地理编码。 程序清单10-4中的第二个函数是nassPlaceFind()。该函数打开一个tab分隔的文本文件,获取 第2列和第3列结果。这些值被输人到函数geoGrab()中,然后需要检查geoGrab()的输出字典判断 有没有错误。如果没有错误,就可以从字典中读取经纬度。这些值被添加到原来对应的行上,同时 写到一个新的文件中。如果有错误,就不需要去抽取纬度和经度。最后,调用sleep()函数将 massPlaceFind()函数延迟1秒。这样做是为了确保不要在短时间内过于频繁地调用API。如果频繁 调用,那么你的请求可能会被封掉,所以将massPlaceFind()函数的调用延迟一下比较好。 Error键值给出的是错误编码。0意味着没有错误,其他任何值都代表没有获得要找的地址。 ****************** ERROR该部分有问题 ****************** ''' import urllib import json def geoGrab(stAddress, city): apiStem = 'http://where.yahooapis.com/geocode?' #create a dict and constants for the goecoder params = {} params['flags'] = 'J'#JSON return type params['appid'] = 'aaa0VN6k' params['location'] = '%s %s' % (stAddress, city) url_params = urllib.urlencode(params) yahooApi = apiStem + url_params #print url_params print yahooApi c=urllib.urlopen(yahooApi) return json.loads(c.read()) from time import sleep def massPlaceFind(fileName): fw = open('places.txt', 'w') for line in open(fileName).readlines(): line = line.strip() lineArr = line.split('\t') retDict = geoGrab(lineArr[1], lineArr[2]) if retDict['ResultSet']['Error'] == 0: lat = float(retDict['ResultSet']['Results'][0]['latitude']) lng = float(retDict['ResultSet']['Results'][0]['longitude']) print "%s\t%f\t%f" % (lineArr[0], lat, lng) fw.write('%s\t%f\t%f\n' % (line, lat, lng)) else: print "error fetching" sleep(1) fw.close() ''' 对地理坐标进行聚类 上述程序清单包含两个函数。第一个函数distSLC()返回地球表面两点之间的距离。第二个 函数clus七erClubs()将文本文件中的俱乐部进行聚类并画出结果。 函数distSLC()返回地球表面两点间的距离,单位是英里。给定两个点的经纬度,可以使用 球面余弦定理来计算两点的距离。这里的纬度和经度用角度作为单位,但是sin()以及cos()以 弧度为输人。可以将角度除以180然后再乘以圆周率pi转换为弧度。导人NumPy的时候就会导 人pi 第二个函数clusterClubs()只有一个参数,即所希望得到的簇数目。该函数将文本文件的 解析、聚类以及画图都封装在一起,首先创建一个空列表,然后打开places.txt文件获取第4列和 第5列,这两列分别对应纬度和经度。基于这些经纬度对的列表创建一个矩阵。接下来在这些数 据点上运行biKmeans()并使用distSLC()函数作为聚类中使用的距离计算方法。最后将簇以及 簇质心画在图上。 为了画出这些簇, 首先创建一幅图和一个矩形,然后使用该矩形来决定绘制图的哪一部分。 接下来构建一个标记形状的列表用于绘制散点图。 使用imread()函数基于一幅图像来创建矩阵O, 后边会使用唯一的标记来标识每个簇。下一步 然后使用imshow()绘制该矩阵。 一幅图上绘制一张新的图,这允许你使用两套坐标系统并且不做任何缩放或偏移。 接下来,在同 紧接着,遍历 每一个簇并将它们一一画出来。 标记类型从前面创建的scatterMarkers列表中得到。 使用索引 i%len(scatterMarkers)来选择标记形状,这意味着当有更多簇时, 记。最后使用十字标记来表示簇中心并在图中显示。 可以循环使用这些标 ''' def distSLC(vecA, vecB):#Spherical Law of Cosines a = sin(vecA[0,1]*pi/180) * sin(vecB[0,1]*pi/180) b = cos(vecA[0,1]*pi/180) * cos(vecB[0,1]*pi/180) * \ cos(pi * (vecB[0,0]-vecA[0,0]) /180) return arccos(a + b)*6371.0 #pi is imported with numpy import matplotlib import matplotlib.pyplot as plt def clusterClubs(numClust=5): datList = [] for line in open('places.txt').readlines(): lineArr = line.split('\t') datList.append([float(lineArr[4]), float(lineArr[3])]) datMat = mat(datList) myCentroids, clustAssing = biKmeans(datMat, numClust, distMeas=distSLC) fig = plt.figure() rect=[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 = datMat[nonzero(clustAssing[:,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) ax1.scatter(myCentroids[:,0].flatten().A[0], myCentroids[:,1].flatten().A[0], marker='+', s=300) plt.show() </span>