机器学习实战笔记——分类之k-近邻算法

着手学习机器学习了,打算根据《机器学习实战》这本书从头到尾把这书里的代码撸个遍,想必就应该能入门了吧。写笔记目的一是记录,二是加深记忆,三是分享。cd尽量减少摘抄性文字。
书中的所有代码来源可到官网下载:https://www.manning.com/books/machine-learning-in-action

这里博客的代码及相关数据放在了cd的github上:https://github.com/ChenDdon/mechine_learning_practice.git



k-近邻算法



一 定义、优缺点及工作原理

定义:k-近邻算法采用测量不同特征值的距离方法进行分类。
优点:进度稿、对异常值不敏感、无数据输入假定。
缺点:计算复杂度高、空间复杂度高、
适用数据范围:数值型和标称型。

工作原理
存在一个样本数据集合,也称作为训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一个数据与所属分类的对应关系。
输入没有标签的新数据后,将新的数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数,且最好为奇数(涉及到判定分类时的投票过程)。
最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。



二 k-近邻算法实践

2.1 k-近邻算法的一般流程

1、收集数据:任意方法。
2、准备数据:距离计算所需要的数值,最好是格式化的数据格式。
3、分析数据:可以使用任何方法。
4、测试算法:计算错误率。
5、使用算法:首先输入样本数据和结构化的输出结果,然后进行k-近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续处理。


2.2 示例说明(任务说明)

这里cd主要基于书本中给的使用k-近邻算法改进约会网站的配对结果这个示例来写的。

示例背景: 海伦一直用一个约会平台在找对象。海伦将约会的众多对象分成了三类:

  • didntLike;
  • smallDoses;
  • largeDoses

然后,海伦将之前每个约会对象数据记录下来,每个人主要是三个数据:

  • 玩视频游戏所耗时间百分比
  • 每年获取的飞行常客里程数
  • 每周消费的冰淇淋公升数

海伦将每个人的数据以及分类整理在了datingTestSet.txt这个文件里了。希望能有个分类程序,每当她拿到一个没约会过的人的前述三个数据,就能知道这个人她会不会喜欢。

给出的数据:datingTestSet.txt与datingTestSet2.txt这两个文件。其中我们要用的是datingTestSet2.txt这个文件与datingTestSet.txt的区别就在于,数据文件中的第四列数值做了如下转换:

第四列数值意义
largeDoses ——> 3
smallDoses ——> 2
didntLike ——> 1

任务:得到一个基于k-近邻算法的分类器,对一行新的向量能进行正确的分类判定。

分类器需要达到的效果,即:input为1*4的向量;output为相应的分类结果。


2.3 代码实践

首先,创建名为kNN.py的python文件。
然后,要清楚我们要做哪些事情,需要哪些功能,从而确定框架。

#1收集数据
#这里提供的datingTestSet2.txt文件就是收集到的数据
#准备数据:将收集的到的数据转换成格式化的数据格式
def img2matrix():
	pass
#分析数据,可以用matplotlib创建可视化的图形
def analyseShow():
    pass
#分类器构建
def classify0():
	pass
#测试算法部分
def datingClassTest():
	pass
#使用算法部分
def classifyPerson():
    pass

2.3.1 收集数据

因为这个例子里的数据是准备好了的,所以直接拿来用就行了。代码和数据都同意放在文末的连接里吧。


2.3.2 准备数据

准备数据其实也是为了后续处理方便,我们首先看一下这个例子中的数据特征:

40920	8.326976	0.953952	3
14488	7.153469	1.673904	2
26052	1.441871	0.805124	1
75136	13.147394	0.428964	1
38344	1.669788	0.134296	1
72993	10.141740	1.032955	1
35948	6.830792	1.213192	3
...     ...			...			...

这里罗列的数据前几行,前三列是三个特征分别是:每年获得的飞行常客里程数;玩视频游戏所耗时间百分比;每周消费的冰淇淋公升数。
第四列是分类结果,用数字1,2,3表示三种结果。

那么可以将特征数据,与分类结果数据分开来,代码如下:

import numpy as np
def file2matrix(filename):
    with open(filename) as fr:
        lines = fr.readlines()
        numberOfLines = len(lines)         		#获取文件行数
        returnMat = np.zeros((numberOfLines,3)) #准备回传的矩阵-零矩阵
        classLabelVector = []                   #准备返回的结果数值-空list
        index = 0
        for line in lines:
            line = line.strip()
            listFromLine = line.split('\t')
            returnMat[index,:] = listFromLine[0:3]
            classLabelVector.append(int(listFromLine[-1]))
            index += 1
    return returnMat,classLabelVector

结果如以下截图所示:
returnMat与classLabelVector

此外,我们可以看到三个特征属性的数值存在巨大差异,那么在计算两个样本之间的差距时,数值大的特征将占到主导地位,而对于这个示例而言,三种特征是同等重要的。因此,在准备数据阶段,我们还需要将特征值的数据进行归一化处理,如将数值取值均归一化到0到1,或者-1到1之间。(不归一化容易导致欠拟合的情况)

下面公式是将特征值转化为0到1区间的值:

newValue = (oldValue-min)/(max-min)

因此,在准备数据这个模块里我们还需要加进去归一化处理的功能:

#准备数据:将收集的到的数据转换成格式化的数据格式
import numpy as np
def file2matrix(filename):
    with open(filename) as fr:
        lines = fr.readlines()
        numberOfLines = len(lines)         #获取文件行数
        returnMat = np.zeros((numberOfLines,3))        #准备回传的矩阵-零矩阵
        classLabelVector = []                       #准备返回的结果数值-空list
        index = 0
        for line in lines:
            line = line.strip()
            listFromLine = line.split('\t')
            returnMat[index,:] = listFromLine[0:3]
            classLabelVector.append(int(listFromLine[-1]))
            index += 1
    
    #数据归一化的部分
    dataSet = returnMat
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    ranges = maxVals - minVals
    normDataSet = np.zeros(np.shape(dataSet))
    m = dataSet.shape[0]
    normDataSet = dataSet - np.tile(minVals, (m,1))
    normDataSet = normDataSet/np.tile(ranges, (m,1))   #element wise divide
    
#    return returnMat,classLabelVector
    return normDataSet,classLabelVector,ranges, minVals

这样一来,输出的normDataSet就是经过归一化之后的数据了。

由于后续会用到每列的数值范围,因此也一起返回了取值范围(ranges)和最小值(minVals)。


2.3.3 分析数据

这里我们使用matplotlib制作原始数据的散点图:

#分析数据,可以用matplotlib创建可视化的图形
'''
Parameters:
	datingDataMat: 数据特征矩阵(包含n个样本的数据集合)
	datingLabels: 样本标签集合(各个样本的结果的集合)
'''
def analyseShow(datingDataMat, datingLabels):
    import matplotlib.pyplot as plt
    plt.rcParams['font.sans-serif']=['Microsoft YaHei'] #用来正常显示中文标签
    plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
    
    fig = plt.figure(figsize=[8,8],tight_layout=True)
    ax1 = fig.add_subplot(221)
    ax1.scatter(datingDataMat[:,1],datingDataMat[:,2],
                15*np.array(datingLabels),15*np.array(datingLabels),
                alpha=0.7)
    ax1.set_xlabel(r'玩视频游戏所耗时间百分比')
    ax1.set_ylabel(r'每周消费的冰淇淋公升数')
    
    ax2 = fig.add_subplot(222)
    ax2.scatter(datingDataMat[:,0],datingDataMat[:,1],
                15*np.array(datingLabels),15*np.array(datingLabels),
                alpha = 0.7)
    ax2.set_xlabel(r'每年获取的飞行常客里程数')
    ax2.set_ylabel(r'玩视频游戏所耗时间百分比')
    
    ax3 = fig.add_subplot(223)
    ax3.scatter(datingDataMat[:,2],datingDataMat[:,0],
                15*np.array(datingLabels),15*np.array(datingLabels),
                alpha = 0.7)
    ax3.set_xlabel(r'每周消费的冰淇淋公升数')
    ax3.set_ylabel(r'每年获取的飞行常客里程数')
    
    
    #画图legend,这里不是重点,不用细看,就是说明一下图例
    from matplotlib.legend_handler import HandlerLine2D
    ax4 = fig.add_subplot(224)
    dot1, = ax4.plot([3], marker='o', label='didntLike',color='purple')
    dot2, = ax4.plot([3], marker='o', label='smallDoses',color='c')
    dot3, = ax4.plot([3], marker='o', label='largeDoses',color='yellow')
    ax4.tick_params(axis='both',labelsize=0,length=0,width=0)
    ax4.spines['top'].set_color('none')    #设置颜色
    ax4.spines['bottom'].set_color('none')
    ax4.spines['left'].set_color('none')
    ax4.spines['right'].set_color('none')
    plt.legend(loc='center',handler_map={dot1: HandlerLine2D(numpoints=4),
                            dot2: HandlerLine2D(numpoints=4),
                            dot3: HandlerLine2D(numpoints=4)})
    
    plt.savefig('analyseShow.png')
    plt.show()

输出如图:
在这里插入图片描述
通过简单的图像展示,图一很难辨识图中的点究竟属于哪个样本分类;而从图二中相对容易的得看出“每年赢得的飞行常客里程数”与“玩视频游戏所占百分比的约会数据散点图”这两个特征从属的类别;图三虽然能够比较容易得区分的确分数据点从属类别,但依然很难根据这张图得出结论性的信息。

将数据可视化是一个很重要的分析数据的方法。尤其对于未知数据,如果能通过合适的方式进行可视化,那么对于后续分类器的构建等步骤都有很好的指导作用。


2.3.4 分类器的构建

分类器的功能是使用k-近邻算法将每组数据划分到某个类中,
对未知类别属性的数据集中的每个点依次执行以下操作:
(1)计算已知类别数据集中的点与当前点之间的距离;
(2)按照距离距离递增次序排列;
(3)选取与当前点距离最小的k个点;
(4)确定前k个点所在地类别的出现频率;
(5)返回前k个点出现频率最高的类别作为当前点的预测分类;(这也就是为什么k最好选取奇数)

#分类器
'''
Parameters:
	inX : 用于分类的输入向量inX
	dataSet : 输入的训练样本集为dataSet
	labels : 标签向量为labels
	k : 表示用于选择最近邻居的数目
Return:
	sortedClassCount[0][0] : 发生频率最高的元素标签,即分类结果。
'''
def classify0(inX, dataSet, labels, k):
    dataSetSize = dataSet.shape[0]
    diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5
    sortedDistIndicies = distances.argsort()     
    classCount={}          
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

这里,用于衡量两特征向量的“距离”所用的方式是欧氏距离
d = ( x 0 − x 1 ) 2 + ( y 0 − y 1 ) 2 + ( z 0 − z 1 ) 2 . . . d=\sqrt{(x_0-x_1)^2+(y_0-y_1)^2+(z_0-z_1)^2...} d=(x0x1)2+y0y12+(z0z1)2...

但这个衡量标准并不是唯一的。

常用的向量距离度量准则
1欧式距离
2曼哈顿距离
3切比雪夫距离
4马氏距离
5巴氏距离
6汉明距离
7皮尔逊系数
8信息熵

甚至在一些时候我们可以自定义一个函数,用于特殊的情况。


2.3.5 测试算法

在数据准备阶段我们已经将数据格式化成方便处理的格式了,已经归一化处理了。此外还构建好了分类器了,那么接下来我们就需要测试一下算法:

#测试算法
def datingClassTest():
    hoRatio = 0.10      #抽取所有数据的百分之10
    
    #导入数据,并格式化,归一化
    normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
    m = normMat.shape[0]
    numTestVecs = int(m*hoRatio)
    errorCount = 0.0
    for i in range(numTestVecs):
        classifierResult =classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3)
#        print("分类结果: %d, 实际结果: %d" % (classifierResult, datingLabels[i]))
        if (classifierResult != datingLabels[i]): 
            errorCount += 1.0
    print("总错误率: %f" % (errorCount/float(numTestVecs)))
    print("错误的样本数: %d"%errorCount)

测试结果如下:
在这里插入图片描述
分类器处理从约会数据集采样出来的10%的数据的错误率是5%,其实也不赖。


2.3.6 使用算法:构建完整可用系统

基于上述的分类器,在实际应用中,主人公海伦只要拿到一个人的前述三个特征数据,然后在约会之前通过这个分类器给出的分类结果(讨厌,一般喜欢,非常喜欢),就能有95%的概率判断海伦对这个人的喜欢程度。

其实所有的工作前述都已经完成了,这里就是整合调用前述代码的过程:

#使用算法部分
def classifyPerson():
    resultList = ['didntLike','smallDoses','largeDoses']
    
    #获取输入的数据:
    percentTats = float(input("玩视频游戏所耗时间百分比:"))
    ffMiles = float(input("每年获取的飞行常客里程数:"))
    iceCream = float(input("每周消费的冰淇淋公升数:"))
    
    normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
    inArr = np.array([percentTats,ffMiles,iceCream])
    
    #这里用到了范围minVals和ranges
    classifierResult =classify0((inArr-minVals)/ranges,normMat,datingLabels,3)
    
    #给出分类结果
    print("你将会对这个人产生感觉是:",resultList[classifierResult-1])


if __name__ == '__main__':
    classifyPerson()

输入的例子及结果:
在这里插入图片描述
到这里为止,各个模块的函数就写完了。

综合的代码如下:

# -*- coding: utf-8 -*-
"""
@author: ChenD
Modify: 2019-3-7
"""

#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#收集数据
#这里提供的datingTestSet2.txt文件就是收集到的数据

#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#准备数据:将收集的到的数据转换成格式化的数据格式
import numpy as np
def file2matrix(filename):
    with open(filename) as fr:
        lines = fr.readlines()
        numberOfLines = len(lines)         #获取文件行数
        returnMat = np.zeros((numberOfLines,3))        #准备回传的矩阵-零矩阵
        classLabelVector = []                       #准备返回的结果数值-空list
        index = 0
        for line in lines:
            line = line.strip()
            listFromLine = line.split('\t')
            returnMat[index,:] = listFromLine[0:3]
            classLabelVector.append(int(listFromLine[-1]))
            index += 1
    
    #数据归一化的部分
    dataSet = returnMat
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    ranges = maxVals - minVals
    normDataSet = np.zeros(np.shape(dataSet))
    m = dataSet.shape[0]
    normDataSet = dataSet - np.tile(minVals, (m,1))
    normDataSet = normDataSet/np.tile(ranges, (m,1))   #element wise divide
    
#    return returnMat,classLabelVector
    return normDataSet,classLabelVector,ranges, minVals


#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#分析数据,可以用matplotlib创建可视化的图形
def analyseShow(datingDataMat, datingLabels):
    import matplotlib.pyplot as plt
    plt.rcParams['font.sans-serif']=['Microsoft YaHei'] #用来正常显示中文标签
    plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
    
    fig = plt.figure(figsize=[8,8],tight_layout=True)
    ax1 = fig.add_subplot(221)
    ax1.scatter(datingDataMat[:,1],datingDataMat[:,2],
                15*np.array(datingLabels),15*np.array(datingLabels),
                alpha=0.7)
    ax1.set_xlabel(r'玩视频游戏所耗时间百分比')
    ax1.set_ylabel(r'每周消费的冰淇淋公升数')
    
    ax2 = fig.add_subplot(222)
    ax2.scatter(datingDataMat[:,0],datingDataMat[:,1],
                15*np.array(datingLabels),15*np.array(datingLabels),
                alpha = 0.7)
    ax2.set_xlabel(r'每年获取的飞行常客里程数')
    ax2.set_ylabel(r'玩视频游戏所耗时间百分比')
    
    ax3 = fig.add_subplot(223)
    ax3.scatter(datingDataMat[:,2],datingDataMat[:,0],
                15*np.array(datingLabels),15*np.array(datingLabels),
                alpha = 0.7)
    ax3.set_xlabel(r'每周消费的冰淇淋公升数')
    ax3.set_ylabel(r'每年获取的飞行常客里程数')
    
    
    #画图legend,这里不是重点,不用细看,就是说明一下图例
    from matplotlib.legend_handler import HandlerLine2D
    ax4 = fig.add_subplot(224)
    dot1, = ax4.plot([3], marker='o', label='didntLike',color='purple')
    dot2, = ax4.plot([3], marker='o', label='smallDoses',color='c')
    dot3, = ax4.plot([3], marker='o', label='largeDoses',color='yellow')
    ax4.tick_params(axis='both',labelsize=0,length=0,width=0)
    ax4.spines['top'].set_color('none')    #设置颜色
    ax4.spines['bottom'].set_color('none')
    ax4.spines['left'].set_color('none')
    ax4.spines['right'].set_color('none')
    plt.legend(loc='center',handler_map={dot1: HandlerLine2D(numpoints=4),
                            dot2: HandlerLine2D(numpoints=4),
                            dot3: HandlerLine2D(numpoints=4)})
    
    plt.savefig('analyseShow.png')
    plt.show()

#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#分类器的构建
import operator
def classify0(inX, dataSet, labels, k):
    dataSetSize = dataSet.shape[0]
    diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5
    sortedDistIndicies = distances.argsort()    #计算距离
    classCount={}
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
    #python3中用items()替换python2中的iteritems()
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]


#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#测试算法
def datingClassTest():
    hoRatio = 0.1      #抽取所有数据的百分之10
    
    #导入数据,并格式化,归一化
    normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
    m = normMat.shape[0]
    numTestVecs = int(m*hoRatio)
    errorCount = 0.0
    for i in range(numTestVecs):
        classifierResult =classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3)
#        print("分类结果: %d, 实际结果: %d" % (classifierResult, datingLabels[i]))
        if (classifierResult != datingLabels[i]): 
            errorCount += 1.0
    print("总错误率: %f" % (errorCount/float(numTestVecs)))
    print("错误的样本数: %d"%errorCount)

#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#使用算法部分
def classifyPerson():
    resultList = ['didntLike','smallDoses','largeDoses']
    
    #获取输入的数据:
    percentTats = float(input("玩视频游戏所耗时间百分比:"))
    ffMiles = float(input("每年获取的飞行常客里程数:"))
    iceCream = float(input("每周消费的冰淇淋公升数:"))
    
    normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
    inArr = np.array([percentTats,ffMiles,iceCream])
    
    #这里用到了范围minVals和ranges
    classifierResult =classify0((inArr-minVals)/ranges,normMat,datingLabels,3)
    
    #给出分类结果
    print("你将会对这个人产生感觉是:",resultList[classifierResult-1])


if __name__ == '__main__':
    classifyPerson()

(欢迎关注微信公众号,cd的实验报告:cdsreport)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值