机器学习实战笔记:k-means和k-NN (一)

PS
该部分内容所设计到的程序源码已经存在我的github上,地址奉上:

https://github.com/AdventureSJ/ML-Notes/tree/master

欢迎各位大佬批评指正,也欢迎各位好友fock or star!

Thank You!

监督学习和无监督学习

对于很多和我一样的朋友们,作为一个机器学习的初学者,在刚开始接触这两个算法的时候,总是弄不清楚这两者的区别。因此,今天特意专门写一篇文章来阐述这两个算法的原理以及实践,在巩固自己学习知识的同时,能让更多的初学者能够理解这两个算法。

首先我们要理解什么是监督学习和无监督学习。监督学习就是最常见的分类问题(注意和聚类区分),通过已有的训练样本(即已知数据机器对应的输出)去训练得到一个最优模型,然后对于新的数据,可以根据这个模型去预测得到一个结果,对输出进行简单的判断从而实现分类的目的,具有对位置数据分类的能力。通俗的说,监督学习最重要的就是要给训练集样本人为标注标签,K-近邻算法就是常见的监督学习算法之一。

那么无监督学习就是输入的数据没有被标记,那么我们就需要根据样本间的相似性对样本集进行分类,使得同一类的数据样本之间的差异性最小化,不同类之间的差异性最大化。无监督学习算法不是告诉计算机该怎么做,而是让它自己去学习怎么样去做事情。K-means就是常见的无监督学习算法之一。

接下来为了缩短这一篇篇幅的长度,我将这个内容分成两篇文章进行阐述,在这篇文章中主要讲解K-means算法的原理以及对应的python实战样例。

K-means算法

在介绍k-means算法之前,先讨论一下簇识别。簇识别给出聚类结果的含义。假定有一些数据,现在将相似数据归纳到一起,簇识别会告诉我们这些簇到底是什么。聚类和分类的最大不同在于,分类的目标事先已知,而聚类则不一样。因为其产生的结果与分类相同,只是类别没有定义,所以聚类有时也被称为无监督分类。

下面会构建K-means算法并观察其实际效果。接下来还会讨论简单的K-means算法中的一些缺陷。为了解决其中的一些缺陷,可以通过后处理产生更好的簇。接着会给出一个更有效的称为二分K-means的聚类算法。本章的最后会给出一个实例,该实例应用二分K-means算法寻找同时可以造访多个夜生活热点地区的最佳停车位。

优点:代码简单,容易实现
缺点:1、可能收敛到局部最小值,在大规模数据集上收敛较慢;2、假如簇内含有异常点,将导致均值偏离严重(对噪声和孤立点数据敏感);3、初始聚类中心点的选择对聚类结果有较大的影响,一旦初始值选择的不好,可能无法得到有效的聚类效果。
适用的数据类型:数值型数据
K-means是发现给定数据集的k个簇的算法。簇的个数k是我们自己人为给定的,每个簇通过其质心,即簇中所有点的中心来描述。

K-means算法的基本流程:首先随机出K个初始点作为质心,然后将数据集中的每个点分配到一个簇中,具体来讲,为每个点找距离其最近的质心点,并将其分配给该质心所对应的簇。这一步完成后,每个簇的质心更新为该簇所有点的平均值。

伪代码:
1、随机选择k个点作为起始质心点
2、 依次遍历每一个数据点,每一次遍历(假设遍历得到的数据点为[latex]D_{i}[/latex])执行下面操作
3、 对数据集中的每个数据点[latex]D_{i}[/latex]
4、 对每个质心
5、 计算每个质心和数据点之间的距离
6、 将数据点分配给距离其最近的簇
7、 对每一个簇,计算簇中所有点的均值并将均值作为质心,不断循环更新质心

代码实现

"""
Created on Mon Oct 15 13:46:35 2018

@author: sj
"""

from numpy import *

'''
函数说明:加载数据集
Parameters:
    filename-数据集的文件名
Returns:
    dataMat-返回的数据集
Modify:
    2018/10/15
'''
def loadDataSet(filename):
    dataMat = []
    fr = open(filename)
    for line in fr.readlines():             #遍历数据集每一行                   
        curLine = line.strip().split('\t')  #按制表符分割每一行数据,返回一个列表
        fltLine = list(map(float,curLine))        #将列表中数据类型转换为float类型
        dataMat.append(fltLine)
    return dataMat

'''
函数说明:计算两个向量之间的欧氏距离
Parameters:
    vecA,vecB - 数据点向量
Returns:
    输入向量之间的欧氏距离
Modify:
    2018/10/15
'''
def distEclud(vecA,vecB):
    return sqrt(sum(power(vecA-vecB,2)))    #计算两个向量之间的欧氏距离

'''
函数说明:为给定数据集构建一个包含k个随机质心的集合
Parameters:
    dataSet - 数据集样本
    k - 要分类的类别数
Returns:
    返回簇质心
Modify:
    2018/10/15
'''
def randCent(dataSet,k):
    n = shape(dataSet)[1]                           #数据集的维度,这里坐标是两维
    centroids = mat(zeros((k,n)))                   #初始化质心位置为0
    for i in range(n):
        minJ = min(dataSet[:,i])                    #获取数据集中最小的坐标值
        rangeJ = float(max(dataSet[:,i]) - minJ)    #获取数据集中坐标的范围
        centroids[:,i] = minJ + rangeJ * random.rand(k,1) #随机产生在数据集坐标范围内的质心点
    return centroids

保存好文件之后,我们可以测试一下文件是否正常运行,首先可以看一下矩阵中的最大值和最小值,然后看看randCent()函数能否生成min到max之间的值,最后测试一下距离计算方法。

import kMeans
import numpy as np

dataMat = np.matrix(kMeans.loadDataSet('testSet.txt'))
print(min(dataMat[:,0]))
print(min(dataMat[:,1]))
print(max(dataMat[:,0]))
print(max(dataMat[:,1]))
print(kMeans.randCent(dataMat,2))
print(kMeans.distEclud(dataMat[0],dataMat[1]))

所有函数均正常运行后,就可以准备实现完整的k-means算法了。该算法会创建k个质心,然后将每个点分配到最近的质心,再重新计算质心位置。这个过程会重复数次,直到数据点的簇分配结果不再改变为止。打开kMeans.py文件添加下面的代码。

函数说明:k-means聚类算法
Parameters:
    dataSet - 输入样本的矩阵
    k - 质心点也就是要分类的个数
    distMeas - 两个点之间的欧氏距离
    createCent = randCent
Returns:
    centroids - 更新后的质心点坐标
    clusterAssment - 包含着每个数据点对应的簇类别以及和对应簇中心的距离
Modify:
    2018/10/16
'''

def kMeans(dataSet,k,distMeas=distEclud,createCent=randCent):
    m = shape(dataSet)[0]                   #数据集样本数
    clusterAssment = mat(zeros((m,2)))      
    centroids = createCent(dataSet,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,:],dataSet[i,:])
                if distJI < minDist:
                    minDist = distJI
                    minIndex =j                   #寻找最近的质心点
            if clusterAssment[i,0] != minIndex:
                clusterChanged = True
            clusterAssment[i,:] = minIndex,minDist**2  #将每个数据点对应的簇(质心类别)以及距离保存在clusterAssment中
        print(centroids)
        for cent in range(k):                           #更新质心的位置
            ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]   #矩阵名.A将mat矩阵转化为array,然后通过数组过滤获得给定簇的所有数据点
            centroids[cent,:] = mean(ptsInClust,axis=0)     #计算所有点的均值
    return centroids,clusterAssment

接下来我们就可以可视化这个算法的效果了。在测试文件中输入下面的代码:

import numpy as np
import matplotlib.pyplot as plt
dataMat= np.mat(kMeans.loadDataSet('testSet.txt'))
myCentroids,clusteAssing = kMeans.kMeans(dataMat,4)
#plt.scatter(dataMat[:,0].tolist(),dataMat[:,1].tolist())
for i in range(4):
    PtsInClust = dataMat[np.nonzero(clusteAssing[:,0].A==i)[0]]
    if i == 0:
        plt.scatter(PtsInClust[:,0].tolist(),PtsInClust[:,1].tolist(),
                    marker='x',color='r',label='0',s=12)
        plt.scatter(myCentroids[i,0],myCentroids[i,1],color='r',marker='+',s=180)
    if i == 1:
        plt.scatter(PtsInClust[:,0].tolist(),PtsInClust[:,1].tolist(),
                    marker='s',color='g',label='1',s=12)
        plt.scatter(myCentroids[i,0],myCentroids[i,1],color='g',marker='+',s=180)
    if i == 2:
        plt.scatter(PtsInClust[:,0].tolist(),PtsInClust[:,1].tolist(),
                    marker='p',color='b',label='2',s=12)
        plt.scatter(myCentroids[i,0],myCentroids[i,1],color='b',marker='+',s=180)
    if i == 3:
        plt.scatter(PtsInClust[:,0].tolist(),PtsInClust[:,1].tolist(),
                    marker='v',color='k',label='3',s=12)
        plt.scatter(myCentroids[i,0],myCentroids[i,1],color='k',marker='+',s=180)

最后我们可以看到如下所示的算法效果图,可以看到测试数据集的数据点大致被很好的区分。

使用后处理来提高聚类性能

在k-menas聚类算法中簇的数目k是一个用户自定义的参数,那么用户如何才能知道k的选择是否正确呢,即哪一个k值能产生较好的分类效果。在包含簇分配结果的矩阵中保存着每个点的误差,即该点到簇质心的平方值。下面会讨论利用这个误差来评价聚类质量的方法。

一种度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和),对应上述程序中的clusterAssment矩阵的第一列之和。SSE值越小表示数据点越接近于特们的质心,聚类效果也越好。因为对误差取了平方,因此更加重视那些远离中心的点。一种可以降低SSE的方法是增加簇的个数,但这违背了聚类的目标:在保持簇数目不变的情况下提高簇的质量。

我们可以对生成的簇进行后处理,即将具有最大SSE值得簇划分为两个簇。具体实现可以将最大簇包含的点过滤出来然后在这些点上运行k-means算法,其中的k设为2。但是如果遇到40维的数据应该如何去做?

有两种可以量化的办法:合并最近的质心,或者合并两个使得SSE增幅最小的质心。第一种思路通过计算所有质心之间的距离,然后合并距离最近的两个点来实现。第二种方法需要合并两个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程 ,直到找到合并最佳的两个簇为止。接下来将讨论利用上述簇划分技术得到更好的聚类结果的方法。

二分k-means算法

为克服k-means算法收敛于局部最小值的问题,有人提出了另一个称为二分均值 (bisectingK-means)的算法,该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对"其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。

二分k-means算法的伪代码如下:
1 将所有点看成一个簇
2 当簇数目小于k时
3 对于每一个簇
4 计算总误差
5 在给定簇上面进行k-menas聚类(k=2)
6 计算将该簇一分为二之后的总误差
7 选择使得误差最小的那个簇进行划分操作
另一种做法是选择SSE最大的簇进行划分,直到簇数目达到用户指定的数目为止。这个做法听起来并不难实现。下面就来看一下该算法的实际效果。 打开kMeans.py文件然后加人下面的代码。

函数说明:二分k-means聚类算法
Parameters:
    dataSet - 数据集样本
    k - 簇的类别数目
    distMeas - distEclud
Returns:
    mat(centList) - 包含质心数据的矩阵
    clusterAssment - 包含数据点对应质心以及距离平方误差值数据的矩阵
Modify:
    2018/10/17
'''

def biKmeans(dataSet, k, distMeas=distEclud):
    m = shape(dataSet)[0]                   #数据集样本数目
    clusterAssment = mat(zeros((m,2)))
    centroid0 = mean(dataSet, axis=0).tolist()[0]
    centList =[centroid0]                   #初始化质心矩阵
    for j in range(m):                      #计算数据集与初始质心之间的SSE
        clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
    while (len(centList) < k):
        lowestSSE = inf                      #初始SSE设为无穷大
        for i in range(len(centList)):
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] #通过数组过滤获得对应簇i的数据集样本
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) #K -均值算法会生成两个质心(簇),同时给出每个簇的误差值
            sseSplit = sum(splitClustAss[:,1])                                 #当前数据集的SSE
            sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])#未划分数据集的SSE
            print ("sseSplit, and notSplit: ",sseSplit,sseNotSplit)
            if (sseSplit + sseNotSplit) < lowestSSE: #更新最小误差值
                bestCentToSplit = i
                bestNewCents = centroidMat
                bestClustAss = splitClustAss.copy()
                lowestSSE = sseSplit + sseNotSplit
        #将k-means得到的0、1结果簇重新编号,修改为划分簇及新加簇的编号
        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]             #将原来的质心点替换为具有更小误差对应的质心点
        centList.append(bestNewCents[1,:].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss        #重新构建簇矩阵
    return mat(centList), clusterAssment

再来可视化看一下算法效果吧

import numpy as np
import matplotlib.pyplot as plt
dataMat= np.mat(kMeans.loadDataSet('testSet2.txt'))
cenList,myNewAssment = kMeans.biKmeans(dataMat,3)
#plt.scatter(dataMat[:,0].tolist(),dataMat[:,1].tolist())
for i in range(3):
    PtsInClust = dataMat[np.nonzero(myNewAssment[:,0].A==i)[0]]
    if i == 0:
        plt.scatter(PtsInClust[:,0].tolist(),PtsInClust[:,1].tolist(),
                    marker='x',color='r',label='0',s=12)
        plt.scatter(cenList[i,0],cenList[i,1],color='r',marker='+',s=180)
    if i == 1:
        plt.scatter(PtsInClust[:,0].tolist(),PtsInClust[:,1].tolist(),
                    marker='s',color='g',label='1',s=12)
        plt.scatter(cenList[i,0],cenList[i,1],color='g',marker='+',s=180)
    if i == 2:
        plt.scatter(PtsInClust[:,0].tolist(),PtsInClust[:,1].tolist(),
                    marker='p',color='b',label='2',s=12)
        plt.scatter(cenList[i,0],cenList[i,1],color='b',marker='+',s=180)

示例:对地图上的点进行聚类

假如有这样一种情况:你的朋友尼古拉斯赵四希望你带她去城里庆祝他的生日。由于其他一些朋友也会过来,所以需要你提供一个大家都可行的计划。尼古拉斯赵四给了你一些他希望去的地址。这个地址列表很长,有70个位置。我把这个列表保存在文件portlandClub.txt中,该文件和源代码一起打包。这些地址其实都在俄勒冈州的波特兰地区。

也就是说,一晚上要去70个地方!你要决定一个将这些地方进行聚类的最佳策略,这样就可以安排交通工具抵达这些簇的质心,然后步行到每个簇内地址。尼古拉斯赵四的清单中虽然给出了地址,但是并没有给出这些地址之间的距离远近信息。因此,你要得到每个地址的纬度和经度,然后对这些地址进行聚类以安排你的行程。

收集数据:适用Yahoo!PlaceFinder API收集数据
准备数据:只保留经纬度信息
分析数据:使用Matplotlib构建一个二维数据图,其中包含簇与地图
训练算法:训练不适用于无监督学习
测试算法:biKmeans()函数
使用算法:最后的输出是包含簇以及簇中心的地图
准备数据,利用下述封装好的代码,并将其加入kMeans.py文件。

函数说明:雅虎API地址获取功能封装函数
Parameters:
    stAddress - Portland文件的第一列数据
    city      - Portland文件的第二列数据
Returns:
    json格式的地理编码
Modify:
    2018/10/21
'''
      
import urllib
import json
def geoGrab(stAddress, city):
    apiStem = 'http://where.yahooapis.com/geocode?'  #设置apiStem
    params = {}
    params['flags'] = 'J'    #返回json格式结果
    params['appid'] = 'aaa0VN6k'
    params['location'] = '%s %s' % (stAddress, city)
    url_params = urllib.urlencode(params)   #将创建的字典转换为可以通过URL进行传递的字符串格式
    yahooApi = apiStem + url_params      
    #print (yahooApi)
    c=urllib.urlopen(yahooApi)          #打开URL读取返回值
    return json.loads(c.read())

'''
函数说明:获取地理位置的经纬度信息并添加到原来的地址行最后
Parameters:
    fileName - 存储地理位置的文件
Returns:
    places.txt -输出文件
Modify:
    2018/10/21
'''

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()

再调用如下的一句代码就能将portLandClub中的地理位置重新编码。

kMeans.massPlaceFind('portlandClub.txt')

现在我们有了包含地理位置的数据文件,接下来可以对这些俱乐部进行聚类。在此过程中使用雅虎的API获得每个点的经纬度信息,然后根据这些信息计算数据点与簇质心的距离。

这里要聚类的俱乐部给出的信息为经度和维度,但这些信息对于距离计算还不够。 在北极附近每走几米的经度变化可能达到数10度 ;而在赤道附近走相同的距离,带来的经度变化可能只是零点几。可以使用球面余弦定理来计算两个经纬度之间的距离。为实现距离计算并将聚类后的俱乐部标识在地图上,打开kMeans.py文件,添加下面的代码。

函数说明:球面距离计算函数以及绘图函数
Parameters:
    vecA,vecB - 包含经纬度信息的向量
Returns:
    两个经纬度之间的距离
Modify:
    2018/10/21
'''

def distSLC(vecA, vecB):
    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 

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', '&gt;', '&lt;']
    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()
    #在上述测试文件中执行kMeans.clusterClubs(5),测试聚类效果

测试效果如图所示:

小结

聚类是一种无监督的学习方法。所谓无监督学习是指事先并不知道要寻找的内容,即没有目标变量。聚类将数据点归到多个簇中,其中相似数据点处于同一簇,而不相似数据点处于不同簇中。聚类中可以使用多种不同的方法来计算相似度。

一种广泛使用的聚类算法是k均值算法,其中k是用户指定的要创建的簇的数目。K -均值聚类算法以k个随机质心开始。算法会计算每个点到质心的距离。每个点会被分配到距其最近的簇质心,然后紧接着基于新分配到簇的点更新簇质心。以上过程重复数次,直到簇质心不再改变。这个简单的算法非常有效但是也容易受到初始簇质心的影响。为了获得更好的聚类效果,可以使用另一种称为二分k均值的聚类算法。二分k-均值算法首先将所有点作为一个簇,然后使用k-Means 算 法 (k = 2 ) 对其划分。下一次迭代时,选择有最大误差的簇进行划分。该过程重复直到k个簇创建成功为止。二分k-均值的聚类效果要好于k-均值算法。

话外谈k-means优化:

k-means算法的调优一般可以从以下角度出发

  • k均值聚类本质上是一种基于欧氏距离度量的数据划分方法,均值和方差大的维度将对数据的聚类效果产生决定性的影响,所以未作归一化处理和统一单位的数据是无法直接参与运算和比较的,同时,离群点或者少量的噪声数据就会对均值产生较大的影响,导致中心偏移,因此使用k-均值聚类算法之前需要对数据做预处理
  • 合理选择k值。k值的选择是k均值聚类最大的问题之一,这也是k均值聚类算法的主要缺点,k值的选择一般基于经验或者多次实验结果。例如手肘法就是尝试不同的k值,并将不同的k值所对应的损失函数画成折线,横轴为k的取值,纵轴为误差平方和定义的损失函数。手肘法认为拐点就是k的最优值。但手肘法是一个经验方法,缺点是不能自动化,因此研究院又提出了一个更先进的方法,其中包括比较有名的Gap Statistics方法,该方法的优点是不需要肉眼去判断,只需要找到最大的Gap Statistics所对应的k即可,因此该方法也适用于批量优化作业。
  • 采用核函数是另一种可以尝试的优化方向,传统的欧氏距离度量方式,使得k均值算法本质上假设了各个数据簇的数据具有一样的先验嫌疑,并呈现球星或者高维球形分布,这种分布在实际生活中并不常见,面对非凸的数据分布数据时,可能需要引入核函数来优化,这时算法又是通过一个非线性映射,将输入空间的数据点映射到高维的特征空间中,并在新的特征空间中进行聚类。非线性映射增加了数据点线性可分的概率,从而在经典的聚类算法失效的情况下,通过引入核函数可以达到更为准确地结果。

猜你喜欢

转载自blog.csdn.net/cv_pyer/article/details/88967407
今日推荐