Python《机器学习实战》读书笔记(二)——k-近邻算法

转载请注明转载作者和来处:http://blog.csdn.net/waiting_gy/article/details/78586204

[目录]

第二章 K-近邻算法

2-1 k-近邻算法概述

k-近邻算法采用测量不同特征值之间的距离方法进行分类。

优点——精度高,对异常值不敏感,无数据输入假定。

缺点——计算复杂度高、空间复杂度高。

使用数据范围——数值型和标称型。

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

现在使用k-近邻算法分类爱情片和动作片。有人统计过很多电影的打斗镜头和接吻镜头,如图是6部电影的打斗和接吻镜头数。
图2-1 使用打斗和接吻镜头数分类电影
假如有一部未看过的电影,可以使用k-近邻算法来解决它是爱情片还是动作片。

首先,需要知道这个位置电影存在多少个打斗镜头和接吻镜头,如图所示:
每部电影的打斗镜头数、接吻镜头数以及电影评估类型

图中,问号位置是未知电影的打斗镜头和接吻镜头数已经给出。即使不知道未知电影属于哪种类型,也可以通过某种方法计算出来。

首先计算未知电影与样本集中其他电影的距离,如图所示:
已知电影与未知电影的距离

此处暂时不用关心如何计算得到的这些距离值,使用Python实现电影分类应用时,会提供具体的计算方法。

根据得到的样本集中所有电影与未知电影的距离,按照距离递增排序,可以找到k哥距离最近的电影。

假定k=3,则三个最靠近电影依次是He’s Not Really into Dudes 、Beautiful Woman 、California Man。k-近邻算法按照距离最近的三部电影的类型,决定未知电影的类型,而这三部电影全部是爱情片,因此我们判定未知电影是爱情片。

接着本章讲解如何在实际环境中应用k-近邻算法,同时涉及如何使用Python工具和相关的机器学习术语。

k-近邻算法的一般流程

1. 收集数据:可以使用任何方法
2. 准备数据:距离计算所需要的数字
3. 分析数据:可以使用任何方法
4. 训练算法:此步骤不适用于k-近邻算法
5. 测试算法:计算错误率
6. 使用算法:首先需要输入样本数据和结构化输出结果,然后运行k-近邻算法判定输入数据分别属于那个分类,最后应用对计算出的分类执行后续的处理。

2-1-1准备:使用Python导入数据

首先,创建名为kNN.py的Python模块,将使用的所有代码都写在这个文件里。在window10系统下,我用的第三方Python的IDE(集成开发环境)是Ulipad,利用Ulipad建立了kNN.py的Python模块,并在新建的.py文件里添加如下程序:

from numpy import *
import operator

def createDataSet():
    group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A' ,'A' ,'B' ,'B']
    return group, labels

在上面的代码中,导入了两个模块,第一个是科学计算包Numpy;第二个是运算符模块。k-近邻算法执行排序操作时将使用这两个模块提供的函数。如图所示,是建立的.py文件和对应代码。

建立的.py文件和相应代码

为了方便使用createDataSet()函数,他创建数据集和标签。然后我们保存kNN.py文件。

注意事项
kNN文件可以保存在两个路径下面,在后面Python交互式开发环境导入kNN模块才不会出错。否则,一般出现的错误如图所示。

kNN.py存放路径错误导致的错误

这儿提供三种解决这个错误的方法:
1. 存放在Python安装目录下与python.exe在一个目录下,如图所示。

kNN.py存放路径

2.存放在Python安装目录里Lib文件里的site-packages文件里。如图所示。
kNN.py文件存放位置2

3.还有一种是存放在自己选择的路径里,没有存放在上诉Python安装的目录中,例如我就存放在自己定义的路径里,如图所示:
kNN.py自定义存放路径

第三个方法存放kNN.py模块后,在导入模块前,加入一段程序后再导入模块kNN.py才能导入成功,不然导入失败,其程序如下:

import sys
sys.path.append('E:\Python_Files\CodeofMe\Chapter2')
import kNN

程序实现在自己指定的路径进行搜索。便可找到自己存放的kNN.py模块块。

这样,进入Python交互式开发环境之后,输入下列命令导入kNN模块验证是否能成功导入,

>>> import kNN

如图所示,没有报错信息,则表明导入成功。
导入kNN模块成功

因为我选择进入的环境是带图形界面的Python Shell – IDLE (Python GUI),用它代替了Python的交互式开发环境进行学习,后面的程序都是在该环境下编辑的。

进入Python开发环境之后,输入谢列命令导入编辑好的kNN程序模块:

>>> import kNN

上诉命令导入kNN模块。为了确保输入相同的数据集,kNN中定义了函数createDataSet,在Python命令提示符下输入下列命令:

>>> group,labels = kNN.createDataSet()

上诉命令创建了变量group和labels,在Python命令提示符下,输入变量的名字以检验是否正确地定义变量:

>>> group
array([[ 1. ,  1.1],
       [ 1. ,  1. ],
       [ 0. ,  0. ],
       [ 0. ,  0.1]])
>>> labels
['A', 'A', 'B', 'B']
>>> 

这里有四组数据,每组数据有两个我们已知的属性或者特征值。上面的group矩阵每行包含一个不同的数据,我们可以把它想象成某个日志文件中不同的测量点或者入口。由于人类大脑的限制,我们通常只能可视化处理三维一下的事物。因此为了简单地实现数据可视化,对于每个数据点,我们通常只是用两个特征。

向量labels包含了每个数据点的标签信息,label包含的元素个数等于group矩阵行数。这里我们将数据点(1,1.1)定义为类A,数据(0,0.1)定义为类B。为了说明方便,例子中的数值是任意选择的,并没有给出标签,如图所示是带有类标签信息的四个数据点。
k-近邻算法:带有4哥数据点的简单例子

到现在为止,便知道Python如何解析数据,如何加载数据,以及kNN算法的工作原理。接下来是使用这些方法完成分类任务。

2-1-2 从文本文件中解析数据

使用classfify0函数运行kNN算法,为每组数据分类。该函数的功能是使用k-近邻算法将每组的数据划分到某个类中,其伪代码如下:

对未知类别属性的数据集中的每个点依次执行一下操作:
1. 计算已知类别数据集中的点与当前点之间的距离;
2. 按照距离递增次序排序;
3. 选取与当前点距离最小的k个点;
4. 确定前k个点所在类别的出现频率;
5. 返回前k个点出现频率最高的类别作为当前点的预测分类。

其函数程序清单(k-近邻算法)

def classify0(inX, dataSet, labels, k):
    dataSetSize = dataSet.shape[0] 
    #numpy函数shape[0]返回dataSet的行数,shape[1]返回dataSet的列数
    diffMat = tile(inX, (dataSetSize,1)) - dataSet 
    #在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
    sqDiffMat = diffMat**2 
    #每个元素进行平方    
    sqDistances = sqDiffMat.sum(axis=1)
    #sum()所有元素相加,sum(0)列相加,sum(1)行相加    
    distances = sqDistances**0.5  
    #每个元素开方,计算出距离    
    sortedDistIndicies = distances.argsort() 
    #返回distances中元素从小到大排序后的索引值 
    classCount={} 
    #定一个记录类别次数的字典    
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]  
        #取出前k个元素的类别       
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1 
        #返回字典中,voteIlabel的值,如果没有,则创建,并将值设为0(即第二个参数)。                
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) 
    #将字典按照item的第一维逆向排序。    
    return sortedClassCount[0][0]

classify0()函数有四个输入参数:用于分类的输入向量inX,输入的训练样本集为dataSet,标签向量位labels,最后的参数k表示用于选择最近邻居的数目,其中标签向量的元素数目和矩阵dataSet的行数形同,函数程序使用了欧氏距离公式,计算两个向量点xA和xB之间的距离:

欧式距离公式

该处的程序为:

    dataSetSize = dataSet.shape[0] 
    #numpy函数shape[0]返回dataSet的行数,shape[1]返回dataSet的列数
    diffMat = tile(inX, (dataSetSize,1)) - dataSet 
    #在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
    sqDiffMat = diffMat**2 
    #每个元素进行平方    
    sqDistances = sqDiffMat.sum(axis=1)
    #sum()所有元素相加,sum(0)列相加,sum(1)行相加    
    distances = sqDistances**0.5  
    #每个元素开方,计算出距离    

例如,离 点(0,0)与(1,2)之间的距离计算为:
例子1 欧氏距离

如果数据集存在4个特征值,则点(1,0,0,1)与(7,6,9,4)之间的距离计算为:
例子2 欧氏距离

计算完所有点之间的距离后,可以对数据按照从小到大的次序排序。然后,确定前k个距离最小元素所在的主要分类,该处的程序为:

    sortedDistIndicies = distances.argsort()
    ##返回distances中元素从小到大排序后的索引值   
    classCount={}
    #定一个记录类别次数的字典
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        #取出前k个元素的类别
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
        #返回字典中,voteIlabel的值,如果没有,则创建,并将值设为0(即第二个参数)。       

输出k总是正整数;最后,将classCount字典分解为远足列表,然后使用程序第二行导入运算符模块的itemgetter方法,按照第二个元素的次序对元组进行排序。此处的排序为逆序,即按照从最大到最小次序排序,该处的程代码片:

sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
#将字典按照item的第一维逆向排序。

此处的排序为逆序,即按照从最大到最小次序排序,最后返回法僧频率最高的元素标签。

为了预测数据所在分类,在Python提示符中输入输如下命令:

>>> kNN.classify0([0,0], group, labels, 3)

输出结果如下:

>>> labels
['A', 'A', 'B', 'B']
>>> kNN.classify0([0,0], group, labels, 3)
'B'
>>> 

显然结果是B
到现在为止,我们已经够早了第一个分类器。

2-1-3 如何测试分类器

为了测试分类器的效果,可以使用已知答案的数据,当然答案不能告诉分类器,检验分类器给出的结果是否符合预期结果。通过大量的测试数据,我们可以得到分类器的错误率——分类器给出错误结果的次数除以测试执行的总数。错误率是常用的评估方法,主要用于评估分类器在某个数据集上的执行效果。完美分类器的错误率为0,最差分类器的错误率是1,在这种情况下,分类器根本就无法找到一个正确答案。

2-2 示例:使用k-近邻算法改进约会网站的配对效果

示例:在约会网站上使用k-近邻算法,实现能根据给定的三个人物特征(每年获得的飞行常客里程数、玩视频游戏所耗时间百分比、每周消费的冰淇淋公斤数)预测该人约会的效果,约会效果分为:不喜欢,一般喜欢,特别喜欢。

1. 收集数据:提供文本文件。
2. 准备数据:使用Python解析文本文件。
3. 分析数据:使用Matplotlib画二位扩散图。
4. 训练算法:次不走不适用k-近邻算法。
5. 测试算法:使用提供的部分数据作为测试样本。(测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,被标记为一个错误。)
6.使用算法:产生简答的命令行程序,然后可以输入一些特征数据以判断对方为自己喜欢的类型。

2-2-1 准备数据:从文本文件中解析数据

收集的数据存放在文本文件datingTestSet.txt中,每个样本数据占据一行,共计1000行。样本主要包含以下3个特征:

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

如图所示,是datingTestSet.txt中的数据:

datingTestSet.txt

在将上诉特征数据输入到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式。在kNN.py中创建名位file2matrix的函数,以此来处理输入格式问题。该函数的输入为文件名字符串,输出为训练样本矩阵和类标签向量。

添加到kNN.py中的代码如下:

def file2matrix(filename):
    fr = open(filename) 
    #打开文件
    numberOfLines = len(fr.readlines())          
    #get the number of lines in the file 得到文件行数
    returnMat = zeros((numberOfLines,3))         
    #prepare matrix to return  返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
    classLabelVector = []                        
    #prepare labels return    返回的分类标签向量
    fr = open(filename)
    index = 0  
    #行的索引值
    for line in fr.readlines():
        line = line.strip()  
        #s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' ')
        listFromLine = line.split('\t')   
        #使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。
        returnMat[index,:] = listFromLine[0:3]  
        #将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
        if listFromLine[-1] == 'didntLike':
            classLabelVector.append(1)         
        elif listFromLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        elif listFromLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        #根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表一般喜欢,3非常喜欢

        index += 1
    return returnMat,classLabelVector

从上面的代码可以看到,Python处理文本文件非常容易。

首先,需要知道文本包含多少行,打开文件得到文件的行数。

 fr = open(filename)  
 #打开文件
    numberOfLines = len(fr.readlines())          
    #get the number of lines in the file 得到文件行数

然后,建立以零填充的矩阵NumPy(实际上,NumPy是一个二位数组,这里暂时不用考虑其用途)。

    returnMat = zeros((numberOfLines,3))         
    #prepare matrix to return  返回的NumPy矩阵,解析完成的数据:numberOfLines行,3

为了简化处理,将该矩阵的另一维度设置为固定值3,可以按照自己的实际需求增加相应的代码以适应变化的输入值。循环处理文件中的每行数据。

  for line in fr.readlines():
        line = line.strip()  
        #s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' ')
        listFromLine = line.split('\t')   
        #使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。
        returnMat[index,:] = listFromLine[0:3]  
        #将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
        if listFromLine[-1] == 'didntLike':
            classLabelVector.append(1)         
        elif listFromLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        elif listFromLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        #根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力

在Python命令提示符下输入下面命令:

>>> import sys
>>> sys.path.append('E:\Python_Files\CodeofMe\Chapter2')
>>> import kNN
>>> reload(kNN)
<module 'kNN' from 'E:\Python_Files\CodeofMe\Chapter2\kNN.pyc'>
>>> datingDataMat,datingLabels = kNN.file2matrix('E:\Python_Files\CodeofMe\Chapter2\datingTestSet.txt')

执行file2matrix函数之前,重新加载kNN.py模块,以确保更新的内容可以生效,否则Python将继续使用上次加载的kNN模块。

成功导入datingTestSet.txt文件中的数据之后,可以简单检查一下数据内容。

>>> datingDataMat
array([[  4.09200000e+04,   8.32697600e+00,   9.53952000e-01],
       [  1.44880000e+04,   7.15346900e+00,   1.67390400e+00],
       [  2.60520000e+04,   1.44187100e+00,   8.05124000e-01],
       ..., 
       [  2.65750000e+04,   1.06501020e+01,   8.66627000e-01],
       [  4.81110000e+04,   9.13452800e+00,   7.28045000e-01],
       [  4.37570000e+04,   7.88260100e+00,   1.33244600e+00]])
>>> datingLabels[0:20]
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3]

现在已经从文本文件中导入了数据,并将其格式化为想要的格式,接着们需要了解数据的真实含义。我们可以直接浏览文本文件,但是这种方法非常不友好,一般来说我们采用图像化的方式直观地展示数据。下面就用Python工具来图形化展示数据内容,以便辨识出一些数据模式。

2-2-2 分析数据:使用Matplotlib创建散点图

首先,使用Matplotlib制作原始数据的散点图,在Python命令环境中,输入下列命令:

>>> from matplotlib.font_manager import FontProperties
>>> import matplotlib.lines as mlines
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> import sys
>>> sys.path.append('E:\Python_Files\CodeofMe\Chapter2')
>>> import kNN
>>> reload(kNN)
<module 'kNN' from 'E:\Python_Files\CodeofMe\Chapter2\kNN.pyc'>
>>> font = FontProperties(fname=r"C:\\WINDOWS\\Fonts\\simsun.ttc",size=14)
>>> datingDataMat,datingLabels = kNN.file2matrix('E:\Python_Files\CodeofMe\Chapter2\datingTestSet.txt')
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1],datingDataMat[:,2])
>>> ax_title_text = ax.set_title(u'玩视频游戏所消耗时间占比和每周消费的冰淇凌公斤数',FontProperties=font)
>>> ax_xlabel_text = ax.set_xlabel(u'玩视频游戏所消耗时间占比',FontProperties=font)
>>> ax_ylabel_text = ax.set_ylabel(u'每周消费的冰淇凌公斤数',FontProperties=font)
>>> plt.setp(ax_title_text, size=9, weight='bold', color='blue')
>>> plt.setp(ax_xlabel_text, size=9, weight='bold', color='blue')
>>> plt.setp(ax_ylabel_text, size=9, weight='bold', color='blue')
>>> plt.show()

输出效果如图所示:
没有样本类别标签的约会数据散点图,难以途中的点究竟属于哪个样本分类

由于没有使用样本分类的特征值,很难从图中看到任何有用的数据模式形式。于是,采用彩色或者其他的记号标记不同的样本,以便更好地理解数据信息。Matplotlib库提供的scatter函数支持个性化标记散点图上的点。为了方便,此处的程序并没有在Python GUI环境下编辑,而是在Python的一个IDE(集成开发环境)Ulipad下编写的,并生成了.py模块。其代码如下:

#! /usr/bin/env python
#coding=utf-8

from matplotlib.font_manager import FontProperties
import matplotlib.lines as mlines
import matplotlib
import matplotlib.pyplot as plt
import sys
sys.path.append('E:\Python_Files\CodeofMe\Chapter2')
import kNN
reload(kNN)
font = FontProperties(fname=r"C:\\WINDOWS\\Fonts\\simsun.ttc",size=14)
datingDataMat,datingLabels = kNN.file2matrix('E:\Python_Files\CodeofMe\Chapter2\datingTestSet.txt')
fig = plt.figure()
ax = fig.add_subplot(111)
numberOfLabels = len(datingLabels)
LabelsColors = []
for i in datingLabels:
    if i == 1:
        LabelsColors.append('black')
    if i == 2:
        LabelsColors.append('blue')
    if i == 3:
        LabelsColors.append('red')
ax.scatter(datingDataMat[:,1],datingDataMat[:,2],color=LabelsColors)
ax_title_text = ax.set_title(u'玩视频游戏所消耗时间占比和每周消费的冰淇凌公斤数',FontProperties=font)
ax_xlabel_text = ax.set_xlabel(u'玩视频游戏所消耗时间占比',FontProperties=font)
ax_ylabel_text = ax.set_ylabel(u'每周消费的冰淇凌公斤数',FontProperties=font)
plt.setp(ax_title_text, size=9, weight='bold', color='blue')
plt.setp(ax_xlabel_text, size=9, weight='bold', color='blue')
plt.setp(ax_ylabel_text, size=9, weight='bold', color='blue')
didntLike = mlines.Line2D([], [], color='black', marker='.',markersize=6,label ='Dislike')
smallDoses = mlines.Line2D([], [], color='blue', marker='.',markersize=6, label ='SmallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.',markersize=6, label ='LargeDoses')
ax.legend(handles=[didntLike,smallDoses,largeDoses])
plt.show()

生成的图如图所示:

玩视频游戏所消耗时间占比和每周消费的冰淇凌公斤数

图采用不同的属性值可以得到更好的效果,能够清晰地标识了三个不同的样本分类区域,具有不同爱好的人其区域也不同。

2-2-3 准备数据:归一化数值

下表提出了四组数据:

玩视频游戏所耗时间百分比没面获得的飞行常客里程数每周消费的冰淇凌公斤数样本分类
10.84000.51
2121340000.93
30200001.12
467320000.12

如果想要计算样本3和样本4之间的举例,可以用下面的方法:

样本3和样本4之间的距离

容易发现,上面方程中数字差值最大的属性对计算结果的影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于表中其他两个特征——玩视频游戏的和每周消费冰淇淋公井数——的影响。而产生这种现象的唯一原因,仅仅是因为飞行常客里程数远大于其他特征值。但一般认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。

在处理这种不同取值范围的特征只是,我们通常采用的方法将数值归一化,如将取值范围处理为0到1或者-1到1之间。下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:

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

其中,min和max分别是数据集中的最小特征值和最大特征值。虽然改变数值取值范围增加了分类器的复杂度,但为了得到准确结果,这样做是有必要的。

接着,在kNN.py中增加一个新函数autoNorm(),该函数可以自动将数字特征转化为0到1的区间。

def autoNorm(dataSet):
    #最大值和最小值的范围    
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    #最大值和最小值的范围    
    ranges = maxVals - minVals
    #shape(dataSet)返回dataSet的矩阵行列数
    normDataSet = zeros(shape(dataSet))
    #返回dataSet的行数    
    m = dataSet.shape[0]
    #原始值减去最小值
    normDataSet = dataSet - tile(minVals, (m,1))
    #除以最大和最小值的差,得到归一化数据
    normDataSet = normDataSet/tile(ranges, (m,1))       
    #返回归一化数据结果,数据范围,最小值
    return normDataSet, ranges, minVals

函数autoNorm()中,将每列的最小值放在变量minVals中,将最大值放在变量maxVals中,其中dataSet.min(0)中的参数0使得函数可以从劣种选取最小值,而不是选取当前行的最小值。然后,函数计算可能的取值范围,并创建新的返回矩阵。正如前面给出的公式,为了归一化特征值,我们必须使用当前值减去最小值,然后除以取值范围。需要注意的是,特征值矩阵有1000X3个值,而minVals和range的值都是1X3。为了解决这个问题,我们使用NumPy库中tile()函数将变量内容复制成输入矩阵同样大小的矩阵,注意这是具体特征值相除,而对于某些数值处理软件包,可能意味着矩阵除法,但在NumPy库中,矩阵除法需要使用函数linalg.solve(matA,matB)。

重新加载模块,执行kNN.py模块,执行autoNorm函数,检测函数的执行结果:

>>> reload(kNN)
>>> normMat,ranges,minVals = kNN.autoNorm(datingDataMat)
>>> normMat
array([[ 0.44832535,  0.39805139,  0.56233353],
       [ 0.15873259,  0.34195467,  0.98724416],
       [ 0.28542943,  0.06892523,  0.47449629],
       ..., 
       [ 0.29115949,  0.50910294,  0.51079493],
       [ 0.52711097,  0.43665451,  0.4290048 ],
       [ 0.47940793,  0.3768091 ,  0.78571804]])
>>> ranges
array([  9.12730000e+04,   2.09193490e+01,   1.69436100e+00])
>>> minVals
array([ 0.      ,  0.      ,  0.001156])

2-2-4 测试算法:作为完成程序验证分类器

机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器,而是用其余的10%数据去测试分类器,检测分类器的正确率。

对于分类器来说,错误流程就是分类器给出错误结果的次数除以测试数据的总数,完美分类器的错误流程为0,而错误率为1.0的分类器不会给出任何正确的分类结果。代码里定义了计数器变量,每次分类器错误地分类数据,计数器就加1,程序执行完成后计数器的结果除以数据点总数即是错误率。

为了测试分类器效果,在kNN.py文件中创建函数datingClassTest,其程序代码如下:

def datingClassTest():
    #取所有数据的百分之十
    hoRatio = 0.10      
    #将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中 
    datingDataMat,datingLabels = file2matrix('E:\Python_Files\CodeofMe\Chapter2\datingTestSet.txt')       
    #load data setfrom file
    #数据归一化,返回归一化后的矩阵,数据范围,数据最小值  
    normMat, ranges, minVals = autoNorm(datingDataMat)
    #获得normMat的行数
    m = normMat.shape[0]
    #百分之十的测试数据的个数
    numTestVecs = int(m*hoRatio)
    #分类错误计数
    errorCount = 0.0
    for i in range(numTestVecs):
        #前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
        classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],4)
        print "the classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i])
        if (classifierResult != datingLabels[i]): errorCount += 1.0
    print "the total error rate is: %f" % (errorCount/float(numTestVecs))
    print errorCount

上诉函数首先使用file2matrix和autoNorm()函数从文件中读取数据并将其转换为归一化特征值。接着计算测试向量的数量,次不决定了normMat向量中那些数据用于测试,哪些数据用于分类器的训练样本;然后将这两部分数据输入到原始kNN分类器函数classify0。最后,函数计算错误率并输出结果。

在Python命令提示符下重新加载kNN模块,并输入kNN.datingClassTest(),执行分类器测试程序,得到下面的输出结果:

>>> reload(kNN)
>>> kNN.datingClassTest()
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 2, the real answer is: 2
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 2, the real answer is: 2

......

the classifier came back with: 3, the real answer is: 3
the classifier came back with: 2, the real answer is: 2
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the total error rate is: 0.030000
3.0

分类器处理约会数据集的错误率是3.0%,这是不错的结果。可以改变呐喊声uatingClassTest内变量hoRatio和变量的值,检测错误率是否随着变算法值的变化而增加。依赖于分类算法、数据集和程序设置,分类器的输出结果可能有很大的不同。

这个例子表明我们可以正确地预测分类,错误率仅仅是3.0%,用户完全可以输入位置对象的属性信息,由分类软件来帮助她判定某一对象的可交往成都:不喜欢,一般喜欢,非常喜欢。

2-2-5 使用算法:构建完成可用系统

如今,完成在数据商队分类器进行测试,现在可以是用这个分类器位用户来对人们分类。给出下列程序,给用户提供他对对方喜欢成都进行预测:

def classifyPerson():
    resultList = ['not at all','in small dose','in large doses']
    percentTats = float(raw_input("persontage of time spend playing video games?"))
    ffMiles = float(raw_input("frequent flier miles earned per year?"))
    iceCream = float(raw_input("liters of ice cream consumed per year?"))
    datingDataMat,datingLabels = file2matrix('E:\Python_Files\CodeofMe\Chapter2\datingTestSet.txt')
    normMat,ranges,minVals = autoNorm(datingDataMat)
    inArr = array([ffMiles,percentTats,iceCream])
    classifierResult = classify0((inArr-minVals)/ranges,normMat,datingLabels,3)
    print "You will probaly like this person: ",resultList[classifierResult - 1]

为了了解程序的实际运行效果,输入下列程序:

>>> reload(kNN)
<module 'kNN' from 'E:\Python_Files\CodeofMe\Chapter2\kNN.pyc'>
>>> kNN.classifyPerson()
persontage of time spend playing video games?10
frequent flier miles earned per year?10000
liters of ice cream consumed per year?0.5
You will probaly like this person:  in small dose
>>> 

目前为止,已经完成在数据上建立分类器。

2-3示例 :手写识别系统

此部分将一步步地构造使用k-禁令分类器的手写识别系统。为了简单器件,选则构造的系统只能是识别数字0到9。需要识别的数字已经使用图形处理然间,处理成具有相同的色彩和大小:宽高都是32像素x32像素的黑白图像。尽管采用文本格式存储图像不能有效地利用存储卡空间,但是为了方便理解,我们还是将图像转行成文本格式。

示例:使用k-近邻算法手写识别系统

1. 收集数据:提供文本文件。
2. 准备数据:编写函数classify0(),将图像格式转换为分类器使用的list格式。
3. 分析数据:在Python命令提示符中检查数据,确保它符合要求。
4. 训练算法:此步骤不适用k-近邻算法。
5. 测试算法:编写函数使用提供的部分数据集作为测试样本,测试样本与非测试样本的区别在于测试样本是已经完成分类的数据,如果预测分类与实际分类不同,则表明一个错误。
6. 实用算法:本列没有完成成此步骤,从图像中提取数字,并完成数字识别,美国的邮件分拣系统就是一个实际运行的类似系统。

2-3-1 准备数据:将图像转换为测试向量

实际图像存储在trainingDisgits和testDigists,且目录trainingDigits中包含了大约2000个例子,每个例子的内容是一个数字,如图所示是一个数字为6的例子:

数字6

本例将使用目录trainningDigits中的数据训练分类器,使用目录testDigits中的数据测试分类效果。

为了使用前面两个例子的分类器,我们必须将图像格式化处理为一个向量。我们将把一个32x32的二进制图像矩阵转换为1x1024的向量,这样前两节使用的分类器就可以处理数字图像信息。

首先边写一段函数img2vector,将图像转换为向量:该函数创建1x1042的NumPy数组,然后打开给定的文件,循环读出文件的前32行,并将每行的头32个字符值存储在NumPy,最后返回数组。

def img2vector(filename):
    returnVect = zeros((1,1024))
    fr = open(filename)
    for i in range(32):
        lineStr = fr.readline()
        for j in range(32):
            returnVect[0,32*i+j] = int(lineStr[j])
    return returnVect

将上诉代码输入kNN.py文件中,在Pythong命令行中输入下列命令测试imag2vector函数。

>>> testVector = kNN.img2vector('E:\Python_Files\CodeofMe\Chapter2\\testDigits\\0_45.txt')
>>> testVector[0,0:30]
array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
        1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.])

过程插入笔记
导入文件时出错:

>>> testVector = kNN.img2vector('E:\Python_Files\CodeofMe\Chapter2\testDigits\0_1.txt')

Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>
    testVector = kNN.img2vector('E:\Python_Files\CodeofMe\Chapter2\testDigits\0_1.txt')
  File "E:\Python_Files\CodeofMe\Chapter2\kNN.py", line 178, in img2vector
    returnVect = zeros((1,1024))
TypeError: file() argument 1 must be encoded string without null bytes, not str

正确的导入:

testVector = kNN.img2vector('E:\Python_Files\CodeofMe\Chapter2\testDigits\0_1.txt')

改为:

testVector = kNN.img2vector('E:\Python_Files\CodeofMe\Chapter2\\testDigits\\0_45.txt')

参考上诉报错的解决方法

2-3-2 测试算法:使用k-近邻算法识别手写数字

上节已经完成数据处理成分类器可以识别的格式,本节将这些数据输入到分类器,检测到分类器的执行效果。在kNN.py文件中加入函数handwritingClassTest()。写入这些代码之前,必须确保将from os import listdir写入文件的其实部分,这段代码的主要功能是从os模块中导入函数listdir,它可以列出给定目录的文件名。

def handwritingClassTest():
    hwLabels = []
    #测试集的Labels
    trainingFileList = listdir('E:\Python_Files\CodeofMe\Chapter2\\trainingDigits')           
    #load the training set
    #返回trainingDigits目录下的文件名    
    m = len(trainingFileList)
    #返回文件夹下文件的个数    
    trainingMat = zeros((m,1024))
    #初始化训练的Mat矩阵,测试集
    for i in range(m):
    #从文件名中解析出训练集的类别
        fileNameStr = trainingFileList[i]
        #获得文件的名字
        fileStr = fileNameStr.split('.')[0]     
        #take off .txt
        classNumStr = int(fileStr.split('_')[0])
        #获得分类的数字        
        hwLabels.append(classNumStr)
        #将获得的类别添加到hwLabels中
        trainingMat[i,:] = img2vector('E:\Python_Files\CodeofMe\Chapter2\\trainingDigits\\%s' % fileNameStr)
        #将每一个文件的1x1024数据存储到trainingMat矩阵中
    testFileList = listdir('E:\Python_Files\CodeofMe\Chapter2\\testDigits')        
    #iterate through the test set
    #返回testDigits目录下的文件列表
    errorCount = 0.0
    #错误检测计数    
    mTest = len(testFileList)
    #测试数据的数量    
    for i in range(mTest):
    #从文件中解析出测试集的类别并进行分类测试        
        fileNameStr = testFileList[i]
        #获得文件的名字
        fileStr = fileNameStr.split('.')[0]     
        #take off .txt
        classNumStr = int(fileStr.split('_')[0])
        #获得测试集的1x1024向量,用于训练        
        vectorUnderTest = img2vector('E:\Python_Files\CodeofMe\Chapter2\\testDigits/%s' % fileNameStr)
        classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
        #获得预测结果
        print "the classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr)
        if (classifierResult != classNumStr): errorCount += 1.0
    print "\nthe total number of errors is: %d" % errorCount
    print "\nthe total error rate is: %f" % (errorCount/float(mTest))

注意的是该程序中函数img2vector和listdir文件路径是自己选择的,根据自己训练数据和测试数据存放的位置编写正确的路径

将trainingDigits目录中文件内容存储在列表中,然后可以得到目录中有多少文件,并将其存储在变量m中。接着,代码创建一个m行1024列的训练矩阵,该矩阵的每行数据存储一个图像。可以从文件名中解析出分类数字。该目录下的文件按照规则命名,如文件9_45.txt的分类是9,它是数字9的第45哥实例。然后我们可以将类代码存储在hwLabels向量中,使用前面讨论的img2vector函数载入图像。在下一步中,我们对testDigits目录中的文件执行相似的操作,不同之处是我们并不将这个目录下的文件载入矩阵中,而是使用classify0()函数测试该目录下的每个文件。由于文件中的值已经在0-1之间,所以本届并不需要使用autoNorm()函数。

在Python命令提示符中输入kNN.handwritingClassTest(),测试该函数的输出结果。依赖于机器速度,加载数据集可能需要花费很长世间,然后函数开始,输出结果如下所示。

>>> reload(kNN)
>>> kNN.handwritingClassTest()
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0

......

the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9

the total number of errors is: 11
the total error rate is: 0.011628

k-近邻算法识别手写数字数据及,错误率为1.2%。改变变量k的值、修改函数handwritingClassTest随机选取训练样本、改变训练样本的数目,都会对k-近邻算法的错误率。

实际使用这个算法是,算法的执行效率并不高。因为算法需要为每个测试向量做2000次举例计算,每个距离计算包括1024哥维度浮点运算,总计要执行900次,此外,我们还需要未测试向量准备2MB的存储空间。k-决策树作为k-近邻算法的优化版,可以减少存储空间和计算时间的开销。

2-4 本章小结

k-近邻算法是分类数据最简单最有效的算法,他必须保存全部数据集,如果训练数据集很大,必须使用大量的存储空间。此外,由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。

k-近邻算法的另一个缺陷是他无法给出任何数据的基础结构信息,因为我们无法知晓平均实例样本和典型实例样本据有什么特征。所以通过后面使用改率测量方法处理分类问题,该算法可以解决这个问题。

参考文献

《机器大作战》

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值