机器学习之k-近邻算法学习

最近学习的《机器学习实战》,书中的代码思想介绍比较简单,网上搜索的博客也层出不穷,所以自己写个关于机器学习的博客,加深自己的学习印象,也可以让自己以后可以随时回顾。
机器学习的主要任务就是对数据进行分析挖掘,提炼出有价值的信息,其中也包括了很多的算法,本篇博客介绍的也是其中之一的k-近邻算法。所以先对k-近邻算法做个介绍,他的工作原理就是存在一个样本数据集合,也称为训练样本集(大量已知的分类数据),并且样本集中每个数据都存在标签,即我们知道样本集中每一个数据与所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征中特征最相似的数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-邻近算法中k的出处,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
k-近邻算法
优点:精度高,对异常值不敏感,无数据输入假定。

缺点:计算复杂程度高(需要计算新的数据点与样本集中的每个数据的“距离”,以判断是否是前k个邻居),空间复杂程度高(有巨大的数据需要存储)。(kNN算法的分类计算复杂程度和训练集中的文档数目成正比,如果训练集中文档综述为n,那么kNN的分类复杂程度为O(n))

适用的数据范围:数值型(需要进行计算距离)和标称型(数值型目标变量可以从无限的数值集合中取值,如1,2等,主要用于回归分析;标称型目标变量的结果只在有限目标集中取值,如真与假,主要用于分类)。

可能文字的介绍比较枯燥,简单来说,k-近邻算法采用测量不同特征值之间的距离来进行方法分类,我们也可以结合相应的例子来分析,对于电影题材,包括了动作片和爱情片,动作片中会存在接吻镜头,爱情片中也会有打斗场景,我们不能仅仅依靠是否存在接吻镜头或者是动作场景来判断影片类型,但是爱情片中的接吻镜头更多,动作片中打斗场景也更加频繁,可以基于此类场景在某部影片中出现的次数来对该电影进行分类。

电影名称 打斗镜头 接吻镜头 电影类型
California Man 3 104 爱情片
He's Not Really into Dudes 2 100 爱情片
Beautiful Woman 1 81 爱情片
Kevin Longblade 101 10 动作片
Robo Slayer 3000 99 5 动作片
Amped II 98 2 动作片
18 90 未知
上表就是每部电影的打斗镜头数,接吻镜头数以及电影评估类型的统计,这就是已知的数据集合,也就是训练样本集,这些数据有两种特征,打斗镜头数和接吻镜头数,我们也知道每个电影的标签,就是电影类型,我们的一般判断,就是该影片的哪种镜头多,那么我们就根据此信息进行定义,就比如这未知的电影,它的接吻镜头比打斗镜头多,我们认为它应该是爱情片,但是k-近邻算法会根据这些已有的数据集,算出未知电影与它们之间的距离(当然方法后面会介绍,其实也就是欧拉距离计算),数据如下表:
电影名称 与未知电影的距离
California Man 20.5
He's Not Really into Dudes 18.7
Beautiful Woman 19.2
Kevin Longblade 115.3
Robo Slayer 3000 117.4
Amped II 118.9
根据这个表的数据,我们从小到大排序,选出最近邻的k个,我们假定k=3,那么三个最靠近的电影的就是He's Not Really into Dudes,Beautiful Woman,California Man,k-近邻算法按照距离最近的三部电影的类型,决定未知电影的类型,而这三部都是爱情片,因此判定出它为爱情片。这就是k-近邻算法的相关介绍。对于k-近邻算法的一般流程:

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

我们用python导入数据,可以进行如下代码:

#准备数据集
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
group,labels = createDataSet()
print(group)
print(labels)

运行的结果如下:
在这里插入图片描述
由此我们就创建了已知数据的特征值和相应的标签信息,即分类信息,对于k-近邻算法的思想就是:

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

明确它的运行思想,同时,我们根据两点距离公式计算距离,即 d = ( x 1 x 2 ) 2 + ( y 1 y 2 ) 2 d=\sqrt{(x1-x2)^2+(y1-y2)^2} ,对于两个向量点xA与xB之间的距离,就是 d = ( x A 0 x B 0 ) 2 + ( x A 1 x B 1 ) 2 d=\sqrt{(xA0-xB0)^2+(xA1-xB1)^2} (当然,这个公式不仅仅限制于二维,多维也可以进行相应的计算),我们就可以相应的得出代码(我也对代码进行相应的解释,如有不正确的地方,还望指出):

#k-近邻算法,分类器
def classify0(inX,dataSet,labels,k):
#参数:输入向量(测试向量),训练样本集,标签向量,最近邻居数
    dataSetSize = dataSet.shape[0]#shape函数获得矩阵的行数值,若是为1,则获得列数值
    diffMat = tile(inX,(dataSetSize,1)) - dataSet
    #通过tile获得与dataSet相同的行数并计算差值,tile:函数格式,tile(A,reps),A的类型众多,reps的类型也有很多,可以是tuple,list,dict,array,int,bool,但不可以是float,string,matrix(矩阵),作用就是重复A的各个维度。
    #注:矩阵的一维是列,二维是行。
    #若reps只为单一数字,就是重复列。

    sqDiffMat = diffMat**2
    #距离的平方
    sqDistances = sqDiffMat.sum(axis=1)
    #axis为0,就是普通的相加,即对应的列相加,axis为1,就是每一行向量相加
    #计算平方和
    distances = sqDistances**0.5#计算距离
    sortedDistIndicies = distances.argsort()
    #argsort的作用就是将其元素从小到大排列,然后提取其对应的index
    #索引输出,与c语言的结构体排序相似
    classCount = {}
    for i in range(k):
    #k-近邻算法的取前k个值
        voteIlabel = labels[sortedDistIndicies[i]]
        #因为是从小到大排序,所以直接正向取值,取得数值的下标,就是索引
        #再获取它对应的分类标签
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
        #get查找voteIlabel是否在字典中,在的话返回对应的值,不在返回0
        #计算所在类别出现的频数
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    #itemgetter返回根据指定对象值从大到小的排序输出,参数为1,就是按字典的
    #值进行排序,它的值就是频数
    return sortedClassCount[0][0]
    #返回频率最高的元素标签(排序后处于第一的位置)

由此运用k-近邻算法构建的分类器也完成,我们可以对之前的电影分类案例用k-近邻算法进行判定,我们以接吻镜头和动作镜头数作为特征值,而电影的题材作为分类标签,即动作片和爱情篇:

from numpy import *
import operator
def createDataSet():#创建数据集和标签
    group = array([[3,104],[2,100],[1,81],[101,10],[99,5],[98,2]])
    labels = ['爱情片','爱情片','爱情片','动作片','动作片','动作片']
    return group,labels
def classify0(inX,dataSet,labels,k):#K近邻算法
    dataSetSize = dataSet.shape[0]
    diffMat = 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.items(),key=operator.itemgetter(1),reverse=True)
    return sortedClassCount[0][0]

group,labels = createDataSet()
print(group)
print(labels)
a = classify0([18,90],group,labels,3)
print(a)

最后的运行结果为:
在这里插入图片描述
从结果我们也可以看到分类结果和我们的判断也是一样的。
看到这肯定有人会问“分类器何种情况下会出错?”,或者是“答案是否总是正确?”,这些问题的答案是否定的,分类器并不会得到百分之百正确的结果,同时我们也可以使用多种方法来检测分类器的正确率,此外分类器的性能也会受到多种因素的影响,如分类器的设置和数据集等,不同的算法在不同的数据集上的表现可能完全不同。为了测试分类器的效果,我们可以使用已知的答案数据,当然答案不能告诉分类器,检验分类器给出的结果是否符合预期结果。通过大量的测试数据,我们可以得到分类器的错误率,就是分类器给出的错误结果的次数除以测试执行的总数,错误率是常用的评估方法,主要用于评估分类器在某个数据集上的执行效果,完美的分类器的错误率是0,最差的分类器的错误率是1.0,这种情况,分类器根本无法找到一个正确答案,所以接下来我们来看两个例子,学习错误率的评估,同时也学习k-近邻算法运用于实际生活。
样例1.使用k-近邻算法改进约会网站的配对效果
我的朋友海伦一直是使用在线约会网站寻找自己的约会对象,总结过后,她发现曾经交往过三种类型的人:
1.不喜欢的人
2.魅力一般的人
3.极具魅力的人
她希望我们的分类软件可以更好的帮助她降匹配到的对象划分到确切的分类中,此外她还收集了一些约会网站未曾记录的数据信息,她认为这些数据可以更有助于匹配对象的归类,所以我们运用k-近邻算法去分类,基本步骤是:

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

她收集的约会数据存放在文本文件datingTestSet.txt中(虽然书中前言给了地址,但是为了方便,给出地址,文本下载,点击Source Code),每个样本数据占据一行,总共有1000行,而这些样本主要包括3种特征:
1.每年获得的飞行常客里程数
2.完视频游戏所消耗的时间
3.每周消费的冰激凌公升数(说实话这些特征有点奇葩)
准备数据:从文本文件中解析数据
上述特征数据输入到分类器之前,必须降待处理数据的格式改变为分类器可以接受的格式,所以我们需要写一个函数来处理格式问题,该函数的输入为文件名字符串,输出为训练样本矩阵和类标签向量。我们可以写出代码:

def file2matrix(filename):#处理输入格式的问题
    fr = open(filename)#打开文件filename
    arrayOLines = fr.readlines()#读取全部行并返回全部内容
    numberOfLines = len(arrayOLines)#统计所有的行数
    returnMat = zeros((numberOfLines,3))
    #numberOflines是一个数字表示行数,创建了一个行数为numberOflines列为3的
    #的且元素都为0的矩阵,因为文件中只有四种数据,待会给出
    #zeros函数的格式:numpy.zeros(shape,dtype=float,order='C'),作用就是构造特定的矩阵,其中矩阵的各个位置的元素值都为0
    classLabelVector = []
    index = 0
    for line in arrayOLines:#对行进行遍历
        line = line.strip()#移除每行的头尾的换行符,空格
        listFromLine = line.split('\t')
        #每一行以制表符'\t'作为分隔符分隔,将整行数据分割成一个
        #元素列表
        returnMat[index,:] = listFromLine[0:3]
        #选取列表的前三个元素放入到returnMat这个矩阵中,其中放入的位置是
        #指定的index所在行,同时全部的列(:号是指所有的列)
        #作为特征矩阵
        classLabelVector.append(int(listFromLine[-1]))
        #将列表中的最后的一列存储到classLabelVector中
        #作为标签向量
        index += 1
        #进入下一行
    return returnMat,classLabelVector
    #返回特征矩阵和标签向量
datingDataMat,datingLabels = file2matrix('datingTestSet.txt')
print(datingDataMat)
print(datingLabels)

当然如此按照书上运行会出现报错:
在这里插入图片描述
这其实还是与datingTestSet.txt文件有问题,
在这里插入图片描述
它的向量标签是字符串,代码中用int强行转换出现报错,所以你可以用特殊情况处理,比如listFromLine[-1]==‘didntLike’,classLabelVector.append(1),或者,你可以运用第二个文件,datingTestSet2.txt,之后的内容我是采用这种方法,就是改成如下datingDataMat,datingLabels = file2matrix(‘datingTestSet2.txt’),当然我们可以看看这个文件中的内容:
在这里插入图片描述
这方法相比之下更加方便,我们再次运行,结果如下:
在这里插入图片描述
这样看我们已经导入了数据并且格式化为想要的格式。
分析数据:使用Matplotlib创建散点图
我们先用Matplotlib制作原始数据的散点图,可以使用如下代码(同样进行解释):

from numpy import *
import operator
import matplotlib
import matplotlib.pyplot as plt
def file2matrix(filename):#处理输入格式的问题
    fr = open(filename)#打开文件filename
    arrayOLines = fr.readlines()#读取全部行并返回全部内容
    numberOfLines = len(arrayOLines)#统计所有的行数
    returnMat = zeros((numberOfLines,3))
    #numberOflines是一个数字表示行数,创建了一个行数为numberOflines列为3的
    #的且元素都为0的矩阵,因为文件中只有四种数据,待会给出
    #zeros函数的格式:numpy.zeros(shape,dtype=float,order='C'),作用就是构造特定的矩阵,其中矩阵的各个位置的元素值都为0
    classLabelVector = []
    index = 0
    for line in arrayOLines:#对行进行遍历
        line = line.strip()#移除每行的头尾的换行符,空格
        listFromLine = line.split('\t')
        #每一行以制表符'\t'作为分隔符分隔,将整行数据分割成一个
        #元素列表
        returnMat[index,:] = listFromLine[0:3]
        #选取列表的前三个元素放入到returnMat这个矩阵中,其中放入的位置是
        #指定的index所在行,同时全部的列(:号是指所有的列)
        #作为特征矩阵
        classLabelVector.append(int(listFromLine[-1]))
        #将列表中的最后的一列存储到classLabelVector中
        #作为标签向量
        index += 1
        #进入下一行
    return returnMat,classLabelVector
    #返回特征矩阵和标签向量

datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')
fig = plt.figure()
#创建画布,并且定义给fig
ax = fig.add_subplot(111)
#等价于fig.add_subplot(1,1,1),生成子图,它的参数一是子图总行数,参数而是子图
#的总列数,参数三是子图的位置
ax.scatter(datingDataMat[:,1],datingDataMat[:,2])
#面向对象绘制散点图,创建了x,y轴
plt.show()
#展示

结果如图:
在这里插入图片描述
这就是以玩游戏所消耗时间百分比为x轴,以每周消费的冰淇淋公升数为y轴的数据散点图,由于没有使用分类的特征值,我们确实看不出什么有用的数据模式信息,一般来说我们会采用色彩或其他的记号来标记不同的样本分类,Matplotlib库提供的scatter函数支持个性化标记散点图上的点,所以我们可以改成:

ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLab\
els),15.0*array(datingLabels))
#每个点对应的大小,设置成与标签有关的数字,比如为1,点的大小为15*1,可以辨别不同的类别
#每个点对应的颜色,设置为与标签有关的数字,同一类的标签的颜色和大小相同,便于辨别(datingLabels中的值为1,2,3)

更改之后的结果是:
在这里插入图片描述
所以通过Matplotlib的数据可视化,虽然可以区别,但效果不是很好。
准备数据:归一化数值
对于文件中的数据,比如要计算样本3与样本4之间的距离,可以使用下面的方法: d = ( 0 67 ) 2 + ( 20000 32000 ) 2 + ( 1.1 0.1 ) 2 d=\sqrt{(0-67)^2+(20000-32000)^2+(1.1-0.1)^2}
对于上面的算式,我们很容易发现,数字差值最大的属性对计算结果影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响远远大于其他的两个特征的影响,产生这种现象的唯一原因,仅仅是因为飞行常客里程数远大于其他特征值,但海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重影响到计算结果。
所以在处理这种不同的取值范围的特征值时,我们通常采用的方法是将数值归一化,数值归一化,避免因为某些数值过于大,从而产生较大的误差,归一化可以将取值范围处理为0到1之间,公式为:

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

其中min和max分别是数据集中的最小特征值和最大特征值,改变数值取值范围虽然增加了分类器的复杂度,但是为了得到准确结果,必须如此,所以我们写一个转换函数:

def autoNorm(dataSet):#归一化特征值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    #返回dataSet中每列中的最小值和最大值
    #min函数:
    #a.min():是返回矩阵a中所有元素的最小元素
    #a.min(0):是返回矩阵所有列中元素的最小值,每一列都要返回一个值
    #a.min(1):是返回所有行中元素的最小值,每一行都要返回一个值
    #max()函数也是一样的道理
    ranges = maxVals-minVals
    #计算可能的取值范围
    normDataSet = zeros(shape(dataSet))
    #构造一个与dstaSet行列数相同的零矩阵
    m = dataSet.shape[0]
    #获得dataSet矩阵的行数
    normDataSet = dataSet - tile(minVals,(m,1))
    #原始值减去最小值,并且为了保持矩阵同样大小,将内容复制
    normDataSet = normDataSet/tile(ranges,(m,1))
    #这里的除法是具体的特征值相除,在numpy库中,矩阵除法需要使用
    #linalg.solve(matA,matB)
    return normDataSet,ranges,minVals
    #返回归一化特征值,数据范围和最小值

我们可以运行看看结果:
在这里插入图片描述
从上面的结果看,数据已经归一化,我们也返回了最小值和数据的取值范围,开始我也很懵逼为什么要返回这两个数据值,但后面的代码中要用到这两个数据,到时候我们再说,继续往下看。
机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有的数据的90%作为训练样本来训练分类器,而使用剩下的10%数据去测试分类器,检测分类器的正确率。同时我们要注意,10%的测试数据应该是随机选择的,例子中海伦提供的数据没有按照特定的目的来排序,所以我们可以随意选择10%数据而不影响随机性。对于错误率我们是用错误的结果次数除以测试执行的总数,所以我们可以定义一个错误计数器变量,每次分类器错误的分类数据,计数器就加一,最后再除以数据点总数就是错误率。我们可以创建一个函数完成相应的操作:

def datingClassTest():#测试分类器的效果
    hoRatio = 0.10
    #定义0.1,意思就是获取所有数据的10%
    datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')
    #打开文件并且处理好输入格式并且返回特征矩阵和分类向量
    normMat,ranges,minVals = autoNorm(datingDataMat)
    #数据归一,获取特征值,数据范围和最小值
    m = normMat.shape[0]
    #获取归一后的特征矩阵的行数
    numTestVecs = int(m*hoRatio)
    #确定10%的测试数据的个数
    errorCount = 0.0
    #定义分类错误计数变量
    for i in range(numTestVecs):#numTestVecs个数据作为测试数据,剩下的作为训练集
        classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],\
                                     datingLabels[numTestVecs:m],4)
         #测试数据的分类操作
        print("the classifier came back with : %d,the real anwser is :%d"\
              %(classifierResult,datingLabels[i]))
        if (classifierResult != datingLabels[i]):
            errorCount += 1.0
            #分类错误计数加一
    print("the total error rate is :%f%%"%(errorCount/float(numTestVecs)*100))

相应的运行上面的代码,我们可以看看运行结果:
在这里插入图片描述
结果是5%,这个结果还是很不错的,当然并不一定每个人的结果都是一样的,这个结果与datingClassTest内变量hoRatio和k的值有关,同时也依赖于分类算法,数据集和程序设置。
使用算法:构建完整系统
我们已经对分类器进行了测试,我们可以使用这个分类器为海伦对人们分类,通过程序输入某个人的信息,程序会给出她对对方的喜欢程度的预测值。下面我给出完整代码,大家不懂的可以自行去试试:

from numpy import *
import operator
import matplotlib
import matplotlib.pyplot as plt
def classify0(inX,dataSet,labels,k):#K近邻算法
    dataSetSize = dataSet.shape[0]#获得矩阵的行数值,若是为1,则获得列数值
    diffMat = tile(inX,(dataSetSize,1)) - dataSet
    #通过tile获得与dataSet相同的行数
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    #axis为0,就是普通的相加,对应的列相加,axis为1,就是每一行向量相加
    distances = sqDistances**0.5#计算距离
    sortedDistIndicies = distances.argsort()
    #argsort的作用就是将其元素从小到大排列,然后提取其对应的index
    #索引输出
    classCount = {}
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
        #查找voteIlabel是否在字典中,在的话返回对应的值,不在返回0
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    #itemgetter返回根据指定对象值从大到小的排序输出
    return sortedClassCount[0][0]

def file2matrix(filename):#处理输入格式的问题
    fr = open(filename)#打开文件filename
    arrayOLines = fr.readlines()#读取全部行并返回全部内容
    numberOfLines = len(arrayOLines)#统计所有的行数
    returnMat = zeros((numberOfLines,3))
    #numberOflines是一个数字表示行数,创建了一个行数为numberOflines列为3的
    #的且元素都为0的矩阵,因为文件中只有四种数据,待会给出
    #zeros函数的格式:numpy.zeros(shape,dtype=float,order='C'),作用就是构造特定的矩阵,其中矩阵的各个位置的元素值都为0
    classLabelVector = []
    index = 0
    for line in arrayOLines:#对行进行遍历
        line = line.strip()#移除每行的头尾的换行符,空格
        listFromLine = line.split('\t')
        #每一行以制表符'\t'作为分隔符分隔,将整行数据分割成一个
        #元素列表
        returnMat[index,:] = listFromLine[0:3]
        #选取列表的前三个元素放入到returnMat这个矩阵中,其中放入的位置是
        #指定的index所在行,同时全部的列(:号是指所有的列)
        #作为特征矩阵
        classLabelVector.append(int(listFromLine[-1]))
        #将列表中的最后的一列存储到classLabelVector中
        #作为标签向量
        index += 1
        #进入下一行
    return returnMat,classLabelVector
    #返回特征矩阵和标签向量
def autoNorm(dataSet):#归一化特征值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    #返回dataSet中每列中的最小值和最大值
    ranges = maxVals-minVals
    #计算可能的取值范围
    normDataSet = zeros(shape(dataSet))
    #构造一个与dstaSet行列数相同的零矩阵
    m = dataSet.shape[0]
    #获得dataSet矩阵的行数
    normDataSet = dataSet - tile(minVals,(m,1))
    #原始值减去最小值,并且为了保持矩阵同样大小,将内容复制
    normDataSet = normDataSet/tile(ranges,(m,1))
    #这里的除法是具体的特征值相除,在numpy库中,矩阵除法需要使用
    #linalg.solve(matA,matB)
    return normDataSet,ranges,minVals
    #返回归一化特征值,数据范围和最小值
def datingClassTest():#测试分类器的效果
    hoRatio = 0.10
    #定义0.1,意思就是获取所有数据的10%
    datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')
    #打开文件并且处理好输入格式并且返回特征矩阵和分类向量
    normMat,ranges,minVals = autoNorm(datingDataMat)
    #数据归一,获取特征值,数据范围和最小值
    m = normMat.shape[0]
    #获取归一后的特征矩阵的行数
    numTestVecs = int(m*hoRatio)
    #确定10%的测试数据的个数
    errorCount = 0.0
    #定义分类错误计数
    for i in range(numTestVecs):#剩下的作为训练集
        classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],\
                                     datingLabels[numTestVecs:m],3)
        print("the classifier came back with : %d,the real anwser is :%d"\
              %(classifierResult,datingLabels[i]))
        if (classifierResult != datingLabels[i]):
            errorCount += 1.0
    print("the total error rate is :%f%%"%(errorCount/float(numTestVecs)*100))

def classifyPerson():#预测函数
    resultList = ['not at all','in small doses','in large doses']
    #三种结果列表,讨厌,有点喜欢,非常喜欢
    percentTats = float(input("percenttage of time spent playing video games?"))
    ffMiles = float(input("frequent flier miles earned per years?"))
    iceCream = float(input("liters of ice cream consumed per year?"))
    #三维特征输入
    datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')
    #导入数据归一化
    normMat,ranges,minVals = autoNorm(datingDataMat)
    #返回特征矩阵,数据范围,最小值
    inArr = array([ffMiles,percentTats,iceCream])
    #将输入的三种特征数据生成numpy数组,测试集
    classifierResult = classify0((inArr-minVals)/ranges,normMat,datingLabels,3)
    #先是将测试集数据归一化,然后进行分类返回结果
    print("you will probably like this person :",resultList[classifierResult - 1])
    #输出结果,列表索引从0开始,分类向量为1,2,3,所以减一获得值
classifyPerson()

我们代入数据来测试[10,10000,0.5]来看看结果:
在这里插入图片描述
那这里我们就在数据上构建了分类器,但是这些数据比较容易,我们可以来看看在不太容易看懂的数据上使用分类器。

样例2.手写识别系统
这一样例中我们要在二进制存储的图像数据上使用k-近邻算法分类,为了简单起见,这里构造的系统只能识别数字0到9,需要识别的数字已经用图形处理软件,处理成具有相同的色彩和大小,宽高是32像素*32像素的黑白的图像,同时为了方便理解,我们将图像转化成文本格式,而使用k-近邻算法的手写识别系统的步骤:

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

同样书中的文件大家可以在上面提及的网站上自行下载,我们也可以看看这些文件的模样,总共是有两个文件trainingDigits和testDigits,在这里插入图片描述
每个文件中都有许多的例子,他们每个文件的命名都是这样在这里插入图片描述
这些命名的意思就是,比如1_22.txt,就是数字1的第22个实例,在这里插入图片描述
上面这个图就是每个图的大概图像,每个图由0与1构成,所以为了使用前面例子的分类器,我们必须图像格式化处理为一个向量,我们将把一个3232的二进制图像转化为11024的向量,这样可以使用前面的分类器处理数字图像信息了,我们可以构建函数:

def img2vector(filename):#二进制图像矩阵转向量
    returnVect = zeros((1,1024))
    #创建一个1*1024的零数组
    fr = open(filename)
    #打开文件
    for i in range(32):
        lineStr = fr.readline()
        #读取前32行
        for j in range(32):
            returnVect[0,32*i+j] = int(lineStr[j])
            #将每行的前32个字符值存储在numpy数组中,因为只有1行,所以0
            #不变
    return returnVect
    #返回数组

测试算法:使用K-近邻算法识别手写数字
上面的函数我们已经将数据处理成分类器可以识别的格式,接下来我们来检测分类器,写一个函数:

from os import listdir#从os模块中导入函数listdir,它可以列出给定目录的文
#件名
def handwritingClassTest():#手写数字识别系统的测试
    hwLabels = []
    trainingFileList = listdir('trainingDigits')
    #获取目录内容,将目录中的文件内容存储在列表中
    m = len(trainingFileList)
    #获取目录中的文件数量
    trainingMat = zeros((m,1024))
    #创建一个m*1024的零矩阵
    for i in range(m):
        fileNameStr = trainingFileList[i]
        #获取每个文件的名称
        fileStr = fileNameStr.split('.')[0]
        #获取文件名称的前称,比如9_45.txt,获得9_45
        classNumStr = int(fileStr.split('_')[0])
        #获取名字的分类,比如例子的9
        hwLabels.append(classNumStr)
        #将其存入列表中
        trainingMat[i,:] = img2vector('trainingDigits/%s'%fileNameStr)
        #处理格式存入图像

    testFileList = listdir('testDigits')
    #与上相似操作
    errorCount = 0.0
    #定义错误计数器
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('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("the total number of errors is :%d"%errorCount)
    print("the total error rate is :%f%%"%(errorCount/float(mTest)*100))

给出完整代码运行测试:

from numpy import *
import operator
from os import listdir
def classify0(inX,dataSet,labels,k):#K近邻算法
    dataSetSize = dataSet.shape[0]
    diffMat = 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.items(),key=operator.itemgetter(1),reverse=True)
    return sortedClassCount[0][0]
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
def handwritingClassTest():#手写数字识别系统的测试
    hwLabels = []
    trainingFileList = listdir('trainingDigits')
    m = len(trainingFileList)
    trainingMat = zeros((m,1024))
    for i in range(m):
        fileNameStr = trainingFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        hwLabels.append(classNumStr)
        trainingMat[i,:] = img2vector('trainingDigits/%s'%fileNameStr)
    testFileList = listdir('testDigits')
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('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("the total number of errors is :%d"%errorCount)
    print("the total error rate is :%f%%"%(errorCount/float(mTest)*100))
handwritingClassTest()

运行结果如下:
在这里插入图片描述
分类器的错误率应该是比较小的,但是运行过程中我们也能发现程序运行的时间是非常久的,执行效率是比较低的,未来应该可以学习更加好的算法来解决问题。
同时,在学习过程中我看见一位大佬的博客提到了用Sklearn这个第三方Python科学计算库来构建手写数字系统,就去搜集了一些有关这个库的一些内容,sklearn库包含了很多机器学习的方式:
1.Classification分类
2.Regression回归
3.Clustering非监督分类
4.Dimensionality reduction数据降维
5.Model Selection模型选择
6.Preprocession数据与处理
它能有效的减少我们特定任务的实现周期,所以我也去网上搜集它的安装方式进行第三方库的下载(有兴趣的伙伴可以自行去找哦),sklearn库中有个sklearn.neighbors.KNeighborsClassifier函数,它可以实现kNN算法,KNeighborsClassifier(n_neighbors=5, weights=‘uniform’, algorithm=‘auto’, leaf_size=30, p=2, metric=‘minkowski’, metric_params=None,n_jobs=1,**kwargs),而对于它的几个参数,我也稍微说明一下(当然我也是找的资料,自己涉猎不深)
n_neighbors:所选用的近邻数,相当于K,默认值为5。
weights:预测的权函数,概率值,参数可为uniform或者是distance。uniform的情况下表示同一权重,就是说所有的近邻点的权重是相等的,distance的情况是不均等的,距离越近的点权重越大,反之就越小,用户自定义的函数,接受一个距离数组,返回一个包含权重的相同形状的数组。
algorithm :用于计算最近邻居的算法,。有{‘auto’, ‘ball_tree’, ‘kd_tree’, ‘brute’}
‘auto’ :根据样本数据自动选择合适的算法。
‘ball_tree’:构建“球树”算法模型。
‘kd_tree’ :‘’kd树‘’算法。
‘brute’ :使用蛮力搜索,即或相当于Knn算法,需遍历所有样本数据与目标数据的距离,进而按升序排序从而选取最近的K个值,采用投票得出结果。
leaf_size:叶的大小,默认是30,针对算法为球树或KD树而言。这个设置会影响构造和查询的速度,以及存储树所需的内存。最优值取决于问题的性质。
metric:用于树的距离度量。默认度量是Minkowski,p=2等价于标准的欧几里德度量。
p:距离度量公式,之前我们使用欧式距离公式进行距离计算,同时还有许多的其他的度量方法,p=2是默认为欧式距离公式进行距离度量。
metric_params:度量函数的附加关键字参数,设置应为dict(字典)形式,一般使用默认的None即可。
n_jobs:要为邻居搜索的并行作业的数量。默认为1。
方法:

fit(X,y) 以X为训练数据,y为目标值拟合模型
get_params([deep]) 获取此估计器的参数
kneighbors([X,n_neighbors,return_distance]) 找到点的K邻域
kneighbors_graph([X,n_neighbors,mode]) 计算X中点的k-邻域(加权)图
predict(X) 预测提供的数据的类标签
predic_proba 返回测试数据X的概率估计
score(X,y[,sample_weight]) 返回给定测试数据和标签的平均精度
set_params 设置此估计器的参数

我们可以使用kneighborsClassifier来代替之前的classify0()函数,当然虽然这函数可以输入矩阵,但为了用上其他的分类器对应,所以也做了向量化处理。

from numpy import *
import operator
from os import listdir
from numpy import *
import operator
from os import listdir
from sklearn.neighbors import KNeighborsClassifier as kNN
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
def handwritingClassTest():#手写数字识别系统的测试
    hwLabels = []
    trainingFileList = listdir('trainingDigits')
    m = len(trainingFileList)
    trainingMat = zeros((m,1024))
    for i in range(m):
        fileNameStr = trainingFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        hwLabels.append(classNumStr)
        trainingMat[i,:] = img2vector('trainingDigits/%s'%fileNameStr)
    neighbor = kNN(n_neighbors = 3,algorithm = 'auto')
    #创建分类器,近邻数定为3,k-邻近算法
    neighbor.fit(trainingMat,hwLabels)
    #拟合模型,前者为测试矩阵,后者为对应的标签,替代了之前的分类函数
    testFileList = listdir('testDigits')
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('testDigits/%s'%fileNameStr)
        classifierResult = neighbor.predict(vectorUnderTest)
        #预测提供的类标签
        print("the classifier came back with : %d,the real answer is : %d"%(classifierResult,classNumStr))
        if (classifierResult != classNumStr):
            errorCount += 1.0
    print("the total number of errors is :%d"%errorCount)
    print("the total error rate is :%f%%"%(errorCount/float(mTest)*100))
handwritingClassTest()

结果如图:
在这里插入图片描述
所以有兴趣的伙伴可以自行去更深入了解sklearn库。
小结:
自己对于机器学习只是处在预习阶段,写下这篇博客让自己能更深的了解内容知识,对于其中如果有什么错误还望各位指出,代码中的注释虽然有点多,但是自己最开始看这些代码的时候,要不断去搜索不懂代码所代表的含义,有一些费时,所以这样写是为了帮助跟我一样有这种情况的伙伴,当然自己去查能学到更多的知识这是毋庸置疑的,最后希望这篇博客能帮助到大家,一起进步!

猜你喜欢

转载自blog.csdn.net/gwk1234567/article/details/107701616
今日推荐