《机器学习》及实战二、K-近邻算法(KNN)理论及实战

一、前言

这章本来应该更新线性模型部分,但一看笔记,内容太少了,没必要特意的更新一章,而且以后的实战会用到这章的知识,在实战中讲解这些不更好吗?( ̄▽ ̄)-----------------------------------好吧!我承认是我懒了(✿◕‿◕✿)。
K-近邻算法开始吧!开始吧!

二、什么叫K-近邻算法(KNN)

简单地说,k近邻算法采⽤测量不同特征值之间的距离⽅法进⾏分类。⼯作原理是:存在⼀个样本数据集合, 也称作训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每⼀数据与所属分类的对应关系。输⼊没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进⾏⽐较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。⼀般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不⼤于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。

优缺点

  • 优点:精度⾼、对异常值不敏感、⽆数据输⼊假定。
  • 缺点:计算复杂度⾼、空间复杂度⾼。
  • 适⽤数据范围:数值型和标称型。

三、简单实例

实例中理解理论部分

众所周知,电影可以按照题材分类,然⽽题材本⾝是如何定义的?由谁来判定某部电影属于哪个题材?也就是说同⼀题材的电影具有哪些公共特征?这些都是在进⾏电影分类时必须要考虑的问题。没有哪个电影⼈会说⾃⼰制作的电影和以前的某部电影类似,但我们确实知道每部电影在风格上的确有可能会和同题材的 电影相近。那么动作⽚具有哪些共有特征,使得动作⽚之间⾮常类似,⽽与爱情⽚存在着明显的差别呢? 动作⽚中也会存在接吻镜头,爱情⽚中也会存在打⽃场景,我们不能单纯依靠是否存在打⽃或者亲吻来判断影⽚的类型。但是爱情⽚中的亲吻镜头更多,动作⽚中的打⽃场景也更频繁,基于此类场景在某部电影中出现的次数可以⽤来进⾏电影分类。本节我们基于电影中出现的亲吻、打⽃出现的次数,使⽤k近邻 算法构造程序,⾃动划分电影的题材类型。
有⼈曾经统计过很多电影的打⽃镜头和接吻镜头,下图显⽰了6部电影的打⽃和接吻镜头数。假如有⼀部未看过的电影,如何确定它是爱情⽚还是动作⽚呢?我们可以使⽤KNN来解决这个问题。
在这里插入图片描述
⾸先我们需要知道这个未知电影存在多少个打⽃镜头和接吻镜头。问号位置是该未知电影出现的镜头数图形化展⽰。详看下图。
在这里插入图片描述
那么未知电影到它们个点的距离怎么算呢?这个电影分类的例子有2个特征,可以用2维实数向量空间,我们高中学过的两点距离公式计算距离,如下。
在这里插入图片描述
计算得到已知电影与未知电影的距离。
在这里插入图片描述
如果是多个特征的情况呢?不单单只是接吻数和打斗数。它们的距离该如何计算?这时我们就可以用欧氏距离(也称欧几里德度量),它和二维距离的求法具有相似之处,公式如下。
在这里插入图片描述
理论说完了,那么下面开始代码实践一下吧!

实例

这⾥⾸先给出k近邻算法的伪代码,方便我们理解。
对未知类别属性的数据集中的每个点依次执⾏以下操作:

  1. 计算已知类别数据集中的点与当前点之间的距离;
  2. 按照距离递增次序排序;
  3. 选取与当前点距离最⼩的k个点;
  4. 确定前k个点所在类别的出现频率;
  5. 返回前k个点出现频率最⾼的类别作为当前点的预测分类。

第一步,导入相关库

"""
简单的KNN分类,分类器
电影的简单分类  
@Author:Yuuuuu、Tian
Tue Feb 11 10:10:21 2020
"""
import numpy as np
import operator   #运算包
import matplotlib.pyplot as plt

第二步,准备数据。

#准备数据
def createDataset():
    group = np.array([[1,101],[5,89],[7,100],[108,5],[115,8],[102,6]])   #特征
    labels = ["爱情片","爱情片","爱情片","动作片","动作片","动作片"]  #标签
    return group,labels
#创建数据集
group,labels = createDataset()
#查看一下数据
print(group)  
print(labels)

数据准备完了,看一下自己写的是否正确。(就是自己看着图片写了个电影的大概位置)
结果:
在这里插入图片描述
接下来,就到了K邻接算法的主体代码啦。注释写的很详细,我自己测试的东西也没删,认真看吧!

def classify0(inX,dataSet,labels,k):
    #求出dataSet多少行
    dataSetSize = dataSet.shape[0]
    #数据和测试的依次相减
    diffMat = dataSet - inX    #测试的数据做运算会自动变为与dataSet同样大小的数据
    #计算距离,平方相加,再开方
    distances = ((diffMat ** 2).sum(axis = 1)) ** 0.5
    #从小到大排序,返回索引,argsort()排序,argsort(x)从小到大,argsort(-x)从大到小
    sortedDistances = distances.argsort()
    #一个记录类别数量的字典
    classCount = {}
    for i in range(k):
        #取出前k个元素的类别,K近邻
        votaIlabel = labels[sortedDistances[i]]
        # dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
        # 计算类别次数
        classCount[votaIlabel] = classCount.get(votaIlabel,0) + 1    #一开始空字典,初始化get(votaIlabel,0)没有返回0
        # python3中用items()替换python2中的iteritems()
        # key=operator.itemgetter(1)根据字典的值进行排序
        # key=operator.itemgetter(0)根据字典的键进行排序
        # reverse降序排序字典
    #按分类个数从大到小排列
    sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse=True)
    # 返回次数最多的类别,即所要分类的类别
    #return sortedClassCount[0]
    return sortedClassCount[0][0]

代码打完了,测试一下。

if __name__ == '__main__':
    #测试集
    test = [100,5]   #通过坐标可知这是动作片,看咱们自写的分类器可以分出来吗?
    #kNN分类
    test_class = classify0(test, group, labels, 3)
    #打印分类结果
    print(test_class)

结果:
在这里插入图片描述
这样,咱们的一个简单的分类器就写完了,你可能会问:“分类器何种情况下会出错?”或者“答案是否总是正确的?”答案是否定的,分类器并不会得到百分百正确的结果,我们可以使⽤多种⽅法检测分类器的正确率。此外分类器的性能也会受到多种因素的影响,如分类器设置和数据集等。不同的算法在不同数据集上的表现可能完全不同。
为了测试分类器的效果,我们可以使⽤已知答案的数据,当然答案不能告诉分类器,检验分类器给出的结果是否符合预期结果。通过⼤量的测试数据,我们可以得到分类器的错误率——分类器给出错误结果的次数除以测试执⾏的总数。
上面我们介绍的例⼦已经可以正常运转了,但是并没有太⼤的实际⽤处,下面将在现实世界中使⽤k近邻算法。让我们开始吧!

四、实例—海伦约会

背景介绍

我的朋友海伦⼀直使⽤在线约会⽹站寻找适合⾃⼰的约会对象。尽管约会⽹站会推荐不同的⼈选,但她没有从中找到喜欢的⼈。经过⼀番总结,她发现曾交往 过三种类型的⼈:

  • 不喜欢的⼈
  • 魅⼒⼀般的⼈
  • 极具魅⼒的⼈

尽管发现了上述规律,但海伦依然⽆法将约会⽹站推荐的匹配对象归⼊恰当的类别。她觉得可以在周⼀到周五约会那些魅⼒⼀般的⼈,⽽周末则更喜欢与那些极具魅⼒的⼈为伴。海伦希望我们的分类软件可以更 好地帮助她将匹配对象划分到确切的分类中。此外海伦还收集了⼀些约会⽹站未曾记录的数据信息,她认为这些数据更有助于匹配对象的归类。
这里,我们也要先写出我们的流程。

  • 收集数据:提供⽂本⽂件。
  • 准备数据:使⽤Python解析⽂本⽂件。
  • 分析数据:使⽤Matplotlib画⼆维扩散图。
  • 训练算法:此步骤不适⽤于k近邻算法。
  • 测试算法:使⽤海伦提供的部分数据作为测试样本。 测试样本和⾮测试样本的区别在于:测试样本是 已经完成分类的数据,如果预测分类与实际类别不同,则标记为⼀个错误。
  • 使⽤算法:产⽣简单的命令⾏程序,然后海伦可 以输⼊⼀些特征数据以判断对⽅是否为⾃⼰喜欢的类型。
    流程有了,开始实现吧!
1.收集数据

数据可以在我的置顶处下载。

2.准备数据

海伦收集约会数据已经有了⼀段时间,她把这些数据存放在⽂本⽂件datingTestSet.txt中,每个样本数据占据⼀⾏,总共有1000⾏(约会了1000次可还行,U•ェ•*U分我一次也好啊U•ェ•*U)。海伦的样本主要包含以下3 种特征:

  • 每年获得的飞⾏常客⾥程数
  • 玩视频游戏所耗时间百分⽐
  • 每周消费的冰琪淋公升数

首先导入相关库

"""
简单的KNN分类,分类器
海伦约会网站的配对
@Author:Yuuuuu、Tian
Tue Feb 11 10:10:21 2020
"""
import numpy as np
from matplotlib.font_manager import FontProperties
import matplotlib.lines as mlines
import matplotlib.pyplot as plt
import operator

数据处理,这步需要打开文件,提取文件中的特征放入特征矩阵,标签放入标签矩阵,并对标签中的中文类别进行数据化的操作,为了后续操作方便。

def file2matrix(filename):
    """
    函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力
    Parameters:
        filename - 文件名
    Returns:
        returnMat - 特征矩阵
        classLabelVector - 分类Label向量
    """
    #打开文件
    fr = open(filename)
    #读取文件内容
    arrayOLines = fr.readlines()
    #获取行数
    numberOLines = len(arrayOLines)
    #返回的Numpy矩阵,解析完成的数据;numberOLines行,3列
    returnMat = np.zeros((numberOLines,3))
    #返回的分类的标签向量
    classLableVector = []
    #行索引
    index = 0
    for line in arrayOLines:
         #s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' '),删除首尾指定元素,中间的无影响
         #主要为删除回车
        line = line.strip()
         #使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。
        listFromLine = line.split('\t')
         #将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵。
        returnMat[index,:] = listFromLine[0:3]    #0-3列,依照行放入特征矩阵
        if listFromLine[-1] == 'didntLike':       #标签放入标签矩阵
             classLableVector.append(1)
        elif listFromLine[-1] == 'smallDoses':
             classLableVector.append(2)
        elif listFromLine[-1] == 'largeDoses':
            classLableVector.append(3)
        index += 1
    return returnMat, classLableVector    #返回特征矩阵,标签矩阵
#-----测试----------------------------------------------------------
if __name__ == '__main__':
    #打开文件名
    filename = "0402Dataset/datingTestSet.txt"
    returnMat, classLableVector = file2matrix(filename)  
    print(returnMat)  #特征矩阵
    print(classLableVector)  #标签

查看一下我们是否将数据放入该放的矩阵内了。
结果:
在这里插入图片描述

3.分析数据

我们要⽤Matplotlib制作原始数据的散点图,因为用图像可以直观的看出各数据的关系。我们有三个特征,二维的坐标轴是无法画出他们的关系的,所以我们两两特征画图。共三个图。

#将数据可视化
def showdatas(datingDataMat,datingLabels):
    #设置汉字格式
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
    #将fig画布分隔成1行1列,不共享x轴和y轴,fig画布的大小为(13,8)
    #当nrow=2,nclos=2时,代表fig画布被分为四个区域,axs[0][0]表示第一行第一个区域
    #创建画布和子图
    fig, axs = plt.subplots(nrows = 2,ncols = 2,sharex = False,figsize = (13, 8))
    numberOfLabels = len(datingLabels)   #获取样本数
    LabelsColors = []   #创建颜色标签
    for i in datingLabels:
        if i == 1:
            LabelsColors.append('black')
        elif i == 2:
            LabelsColors.append('orange')
        elif i == 3:
            LabelsColors.append('red')    #不同分类不同颜色
    #画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第二列(玩游戏)数据画散点数据,
    #散点大小为15,透明度为0.5
    #分成了两行两列,在[0][0]位置作此图。以datingDataMat矩阵的第一(飞行常客例程)、第二列(玩游戏占比)数据画散点数据,
    axs[0][0].scatter(x = datingDataMat[:,0],y=datingDataMat[:,1],color = LabelsColors,s = 15,alpha= 0.5)
    #设置标题,行纵坐标
    axs0_title_text = axs[0][0].set_title('每年获得的飞行常客里程数与玩视频游戏所消耗时间占比', FontProperties=font)
    axs0_xlabel_text = axs[0][0].set_xlabel('每年获得的飞行常客里程数', FontProperties=font)
    axs0_ylabel_text = axs[0][0].set_ylabel('玩视频游戏所消耗时间占', FontProperties=font)
    plt.setp(axs0_title_text, size=10, weight='bold', color='red')
    plt.setp(axs0_xlabel_text, size=10, weight='bold', color='black')
    plt.setp(axs0_ylabel_text, size=10, weight='bold', color='black')
    #画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第三列(消费冰淇凌)数据画散点数据。
    #散点大小为15,透明度为0.5
    axs[0][1].scatter(x = datingDataMat[:,0],y=datingDataMat[:,2],color = LabelsColors,s = 15,alpha= 0.5)
    #设置标题,行纵坐标
    axs0_title_text = axs[0][1].set_title('每年获得的飞行常客里程数与消费冰淇凌占比', FontProperties=font)
    axs0_xlabel_text = axs[0][1].set_xlabel('每年获得的飞行常客里程数', FontProperties=font)
    axs0_ylabel_text = axs[0][1].set_ylabel('消费冰淇凌', FontProperties=font)
    plt.setp(axs0_title_text, size=10, weight='bold', color='red')
    plt.setp(axs0_xlabel_text, size=10, weight='bold', color='black')
    plt.setp(axs0_ylabel_text, size=10, weight='bold', color='black')
    # 画出散点图,以datingDataMat矩阵的第二列(玩游戏)、第三列(消费冰淇凌)数据画散点数据,
    # 散点大小为15,透明度为0.5
    axs[1][0].scatter(x=datingDataMat[:, 1], y=datingDataMat[:, 2], color=LabelsColors, s=15, alpha=0.5)
    # 设置标题,行纵坐标
    axs0_title_text = axs[1][0].set_title('每年玩视频游戏所消耗时间占比与消费冰淇凌占比', FontProperties=font)
    axs0_xlabel_text = axs[1][0].set_xlabel('每年玩视频游戏所消耗时间占比', FontProperties=font)
    axs0_ylabel_text = axs[1][0].set_ylabel('消费冰淇凌', FontProperties=font)
    plt.setp(axs0_title_text, size=10, weight='bold', color='red')
    plt.setp(axs0_xlabel_text, size=10, weight='bold', color='black')
    plt.setp(axs0_ylabel_text, size=10, weight='bold', color='black')
    plt.show()
#-----测试------------------------------------------------------------
if __name__ == '__main__':
    #打开的文件名
    filename = "0402Dataset/datingTestSet.txt"
    #打开并处理数据
    datingDataMat, datingLabels = file2matrix(filename)
    showdatas(datingDataMat, datingLabels)    

结果:
在这里插入图片描述
由图可以直观的看出海伦喜欢那类人,但在细细的分析一下数据,有的数据的取值在2以下,有的则已经飙到了100000。举个例子看一下距离公式,随机找两个样本如下。
在这里插入图片描述
容易发现,上⾯⽅程中数字差值最⼤的属性对计算结果的影响最⼤,也就是说,每年获取的飞⾏常客⾥程数对于计算结果的影响将远远⼤于其他两个特征——玩视频游戏的和每周消费冰淇淋公升数 ——的影响。⽽产⽣这种现象的唯⼀原因,仅仅是因为飞⾏常客⾥程数远⼤于其他特征值。但海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之⼀,飞⾏常客⾥程数并不应该如此严重地影响到计算结果。我们要怎么解决这个问题呢?处理这种不同取值范围的特征值时,通常采⽤的⽅法就是将数值归⼀化,如将取值范围处理为0到1或 者-1到1之间。下⾯的公式可以将任意取值范围的特征值转化为0到1区间内的值。
在这里插入图片描述
原理知道了,那么开始归一化吧!

#数组集体操作进行归一化
def autoNorm(dataSet):
    #找到每列最小最大值
    #min(0)返回该矩阵中每一列的最小值
    #min(1)返回该矩阵中每一行的最小值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    #范围
    rangs = maxVals - minVals
    #创建存放归一化的矩阵
    normDataSet = np.zeros(np.shape(dataSet))
    #进行归一化
    normDataSet = (dataSet - minVals)/ rangs
    #返回归一化结果,范围,最小值
    return normDataSet,rangs,minVals
#------测试-----------------------------------------------------------
if __name__ == "__main__":
    #打开的文件名
    filename = "0402Dataset/datingTestSet.txt"
    #打开并处理数据
    datingDataMat, datingLabels = file2matrix(filename)
    #showdatas(datingDataMat, datingLabels)
    normDataSet, rangs, minVals = autoNorm(datingDataMat)
    print(normDataSet)
    #print(rangs)
    #print(minVals)

结果:
在这里插入图片描述
可以看出,归一化后结果都在0–1之间。

4.编写分类器

分类器可直接用上个实例的即可,原理一样,我就直接复制过来啦!

#分类器函数
def classify0(inX,dataSet,labels,k):
    #求出dataSet多少行
    dataSetSize = dataSet.shape[0]
    #将要测试的数据变为dataSet同样大小的数据
    #inXs = np.tile(inX,(dataSetSize,1))
    #数据和测试的依次相减
    diffMat = dataSet - inX   #各数据依次和它相减
    #计算距离,平方相加,再开方
    distances = ((diffMat ** 2).sum(axis = 1)) ** 0.5
    #从小到大排序,返回索引
    sortedDistances = distances.argsort()
    #一个记录类别数量的字典
    classCount = {}
    for i in range(k):
        #取出前k个元素的类别
        votaIlabel = labels[sortedDistances[i]]
        # dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
        # 计算类别次数
        classCount[votaIlabel] = classCount.get(votaIlabel,0) + 1    #一开始空字典,初始化get(votaIlabel,0)没有返回0
        # python3中用items()替换python2中的iteritems()
        # key=operator.itemgetter(1)根据字典的值进行排序
        # key=operator.itemgetter(0)根据字典的键进行排序
        # reverse降序排序字典
    #按分类个数从大到小排列
    sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse=True)
    # 返回次数最多的类别,即所要分类的类别
    #return sortedClassCount[0]
    return sortedClassCount[0][0]
5.测试算法

我们已经将数据按照需求做了处理,本节我们将测试分类器的效果,如果分类器的正确率满⾜要求,海伦就可以使⽤这个软件来处理约会⽹站提供的约会名单了。机器学习算法⼀个很重要的⼯作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器,⽽使⽤其余的10%数据去测试分类器,检测分类器的正确率。
最原始的做法。需要注意的是10%的测试数据应该是随机选择的,由于海伦提供的数据并没有按照特定⽬的来排序,所以我们可以随意选择10%数据⽽不影响其随机性。 前⾯我们已经提到可以使⽤错误率来检测分类器的性能。对于分类器来说,错误率就是分类器给出错误结果的次数除以测试数据的总数,完美分类器的错误率为0,⽽错误率为1.0的分类器不会给出任何正确的分类结果。代码⾥我们定义⼀个计数器变量,每次分类 器错误地分类数据,计数器就加1,程序执⾏完成之后计数器的结果除以数据点总数即是错误率。
好,开始!

def datingClassTest():
    #打开文件
    filename = "0402Dataset/datingTestSet.txt"
    #读取文件,处理数据
    datingDataMat,datingLables = file2matrix(filename)
    #取10%
    hoRatio = 0.10
    #数据归一化,返回归一化后的矩阵,数据范围,数据最小值
    NormMat,ranges,minVals = autoNorm(datingDataMat)
    #获取元素个数
    m = NormMat.shape[0]
    #取10%
    numTestVecs = int(m * hoRatio)
    #初始化错误个数
    errorCount = 0
    #前numTestVecsge为测试集,其余为训练集
    for i in range(numTestVecs):
        #开始分类
        classifierResult = classify0(NormMat[i,:], NormMat[numTestVecs:m,:],datingLables[numTestVecs:m], 4)   #返回预测类别
        #输出测试与真实值
        print("测试值为:%d\t真实值为:%d"%(classifierResult,datingLables[i]))
        if classifierResult != datingLables[i]:    #判断第i个预测和第i个的真实标签是否一致
            errorCount += 1
    print("错误率:%f%%"%(errorCount/float(numTestVecs) * 100))   
 #-----测试----------------------------------------------------------------
if __name__ == '__main__':
    datingClassTest()   

通过测试,我们知道了这个分类器的正确率。
结果:
在这里插入图片描述

6.使用算法

上⾯我们已经在数据上对分类器进⾏了测试,现在终于可以使⽤这个分类器为海伦来对⼈们分类。我们会给海伦⼀⼩段程序,通过该程序海伦会在约会⽹站上找到某个⼈并输⼊他的信息。程序会给出她对对⽅喜欢程度的预测值。

#构建可用系统
def ClassifyPerson():
    #输出结果
    #上面我们为了好操作,将三种态度转变为1,2,3数字代替,现在到了出结果的过程了,我们要还原回显示文字
    resultList = ['讨厌','有些喜欢','喜欢']      
    #输入条件
    percentTats = float(input("玩游戏消耗占比:"))
    ffmiles= float(input("每年飞行里程数:"))
    iceCream = float(input("每周冰淇凌消费:"))
    #打开数据,归一化
    filename = "0402Dataset/datingTestSet.txt"
    datingDataMat,datingLabels = file2matrix(filename)   #处理数据
    normMat,ranges,minVal = autoNorm(datingDataMat)       #归一化
    #生成测试集,处理我们输入的数据
    inArr = np.array([ffmiles,percentTats,iceCream])
    #归一化
    norminArr = (inArr - minVal) / ranges
    #返回分类结果
    classifyResult = classify0(norminArr,normMat,datingLabels,3)
    print("对此人态度可能为:%s"%(resultList[classifyResult - 1]))  
#--------测试----------------------------------------------------------------
if __name__ == '__main__':
    ClassifyPerson()

我依次输入了一组测试数据,得到结果如下。
在这里插入图片描述
我们已经看到如何在数据上构建分类器。 这⾥所有的数据让⼈看起来都很容易,但是如何在⼈不太容易看懂的数据上使⽤分类器呢?下面我们讲解如何在⼆进制存储的图像数据上使⽤kNN。

五、实例—手写数字识别

本节我们⼀步步地构造使⽤k近邻分类器的⼿写识别系统。为了简单起见,这⾥构造的系统只能识别数字0到9,如下图。需要识别的数字已经使⽤图形处理软件,处理成具有相同的⾊彩和⼤⼩ :宽⾼是32像素x32像素的⿊⽩图像。尽管采⽤⽂本格式存储图像不能有效地利⽤内存空间,但是为了⽅便理解,我们还是将图像转换为⽂本格式。
(具体牛X的手写数字识别,会在深度学习tensorflow那讲-------如果我还更的话d=====( ̄▽ ̄*)b) )
先看这个写法吧!

补充呀!!!

再讲之前,先说一下怎么样将手写的数字变为⽂本格式存储呢?
这是补充部分,后面实例的数据集都是⽂本格式存储的。挺简单的,直接上代码,看我注释(☆▽☆)

"""
k-近邻算法实战之sklearn手写数字识别----补充
@Author:Yuuuuu、Tian
Wed Feb 12 11:13:21 2020
自己写了个数字,照下后想看一下是否可以判断正确
将一图片转化为32*32的图片,然后转为文本格式
"""
#图像转为二进制数据
from PIL import Image
import matplotlib.pylab as plt
import numpy as np
def picTo01(filename):
    """
    将图片转化为32*32像素的文件,用0 1表示
    """
    # 打开图片
    img = Image.open(filename).convert('RGBA')
    # 得到图片的像素值
    raw_data = img.load()
    # 将其降噪并转化为黑白两色,
    #亮度分界线注意,数值越大越亮   128为分界  <128为暗   >128为亮
    #对各图层进行处理
    for y in range(img.size[1]):      #y
        for x in range(img.size[0]):  #x
            if raw_data[x, y][0] < 128:                    
                raw_data[x, y] = (0, 0, 0, 255)
    for y in range(img.size[1]):
        for x in range(img.size[0]):
            if raw_data[x, y][1] < 128:
                raw_data[x, y] = (0, 0, 0, 255)     #黑色
    for y in range(img.size[1]):
        for x in range(img.size[0]):
            if raw_data[x, y][2] < 128:
                raw_data[x, y] = (0, 0, 0, 255)   #黑色
    # 设置为32*32的大小
    img = img.resize((32, 32), Image.LANCZOS)
    # 进行保存,方便查看
    img.save('test.png')
    
    # 得到像素数组,为(32,32,4)
    array = plt.array(img)
    # 按照公式将其转为01, 公式: 0.299 * R + 0.587 * G + 0.114 * B
    gray_array = np.zeros((32, 32))
    # 行数
    for x in range(array.shape[0]):
        # 列数
        for y in range(array.shape[1]):
            # 计算灰度,若为255则白色,数值越小越接近黑色
            gary = 0.299 * array[x][y][0] + 0.587 * array[x][y][1] + 0.114 * array[x][y][2]

            # 设置一个阙值,记为0
            if gary == 255:
                gray_array[x][y] = 0
            else:
                # 否则认为是黑色,记为1
                gray_array[x][y] = 1
    # 得到对应名称的txt文件
    name01 = filename.split('.')[0]
    name01 = name01 + '.txt'
    # 保存到文件中
    np.savetxt(name01, gray_array, fmt='%d', delimiter='')
#-------测试-----------------------------------------------------------
if __name__ == '__main__':
    picTo01('0403Dataset/zixie.png')

我自己写个了如下的数字,命名为zixie.txt,现在通过函数来看一下处理结果吧!
zixie.txt
结果:
在这里插入图片描述
这就是补充的部分啦,情况就是这么个情况,事情就是这么个事情。下面开始进入正题。

正题开始—手写数字识别(sklearn模块实践)

流程如下:

  1. 收集数据:提供了⽂本⽂件。
  2. 准备数据:编写函数classify0(),将图像格式转换为分类器使⽤的list格式。
  3. 分析数据:在Python命令提⽰符中检查数据,确 保它符合要求。
  4. 训练算法:此步骤不适⽤于k近邻算法。
  5. 测试算法:编写函数使⽤提供的部分数据集作为 测试样本,测试样本与⾮测试样本的区别在于测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为⼀个错误。
  6. 使⽤算法:从图像中提取数字,并完成数字识别。
    根据流程开始编写吧!
1.收集数据

数据已放入置顶链接

2.准备数据

提供的trainingDigits中包含了⼤约2000个例⼦,每个例⼦的内容如下图所⽰,每个数字⼤约有200个样本;testDigits中包含了⼤约900个测试数据。我们使⽤⽬录 trainingDigits中的数据训练分类器,使⽤⽬录testDigits 中的数据测试分类器的效果。
在这里插入图片描述
为了使⽤前⾯两个例⼦的分类器,我们必须将图像格式化处理为⼀个向量。我们将把⼀个32x32的⼆进制图像矩阵转换为1x1024的向量,这样前两节使⽤的分类器就可以处理数字图像信息了。 但只讲这种方法多没劲啊,而且代码又复杂,下面咱介绍个简单的,用sklearn的模块来实现(用自写分类器的方法也要自练一下,我就不写了(ง •_•)ง)。
先将32x32的⼆进制图像矩阵转换为1x1024的向量。
导入相关包

"""
手写数字识别---sklearn
@Author:Yuuuuu、Tian
Wed Feb 12 11:13:21 2020
"""
import numpy as np
import operator
from os import listdir
from sklearn.neighbors import KNeighborsClassifier as KNN
from PIL import Image
import matplotlib.pylab as plt

将32*32的二进制图像转变为1 * 1024向量

#将32*32的二进制图像转变为1 * 1024向量
def img2vector(filename):
    #初始化一个1*1024的向量
    returnVect = np.zeros((1,1024))
    fr = open(filename)
    # lineStr1 = fr.readline()
    # lineStr2 = fr.readline()
    #按行读
    for i in range(32):
        #读出一行
        lineStr = fr.readline()   #每执行一次,自动都下行
        for j in range(32):
            returnVect[0,32 * i + j] = int(lineStr[j])
    return returnVect
    #return  lineStr1,lineStr2
#测试
if __name__ == "__main__":
    filename = "0403Dataset/trainingDigits/0_0.txt"   #查看一下0.0.txt文件的转变
    #returnVect,a = img2vector(filename)
    returnVect  = img2vector(filename)
    print(returnVect)
    #print(a)

结果是1*1024的向量,中间省略了。
在这里插入图片描述

3.sklearn中的KNN学习

下面就用到sklearn了,在用此模块前,先介绍一下咱们要用的参数
KNN需要调用的是 sklearn.neighbors.KNeighborsClassifier
KNneighborsClassifier参数说明:参考来源

  • n_neighbors:默认为5,就是KNN中k的值,选取最近的k个点。
  • weights:默认是uniform,参数可以是uniform、distance,也可以是用户自己定义的函数。uniform是均等的权重,就说所有的邻近点的权重都是相等的。distance是不均等的权重,距离近的点比距离远的点的影响大。用户自定义的函数,接收距离的数组,返回一组维数相同的权重。
  • algorithm:快速k近邻搜索算法,默认参数为auto,可以理解为算法自己决定合适的搜索算法。除此之外,用户也可以自己指定搜索算法ball_tree、kd_tree、brute方法进行搜索,brute是蛮力搜索,也就是线性扫描,当训练集很大时,计算非常耗时。kd_tree,构造kd树存储数据以便对其进行快速检索的树形数据结构,kd树也就是数据结构中的二叉树。以中值切分构造的树,每个结点是一个超矩形,在维数小于20时效率高。ball tree是为了克服kd树高纬失效而发明的,其构造过程是以质心C和半径r分割样本空间,每个节点是一个超球体。
  • leaf_size:默认是30,这个是构造的kd树和ball树的大小。这个值的设置会影响树构建的速度和搜索速度,同样也影响着存储树所需的内存大小。需要根据问题的性质选择最优的大小。
  • metric:用于距离度量,默认度量是minkowski,也就是p=2的欧氏距离(欧几里德度量)。
  • p:距离度量公式。我们使用欧氏距离公式进行距离度量。除此之外,还有其他的度量方法,例如曼哈顿距离。这个参数默认为2,也就是默认使用欧式距离公式进行距离度量。也可以设置为1,使用曼哈顿距离公式进行距离度量。
  • metric_params:距离公式的其他关键参数,这个可以不管,使用默认的None即可。
  • n_jobs:并行处理设置。默认为1,临近点搜索并行工作数。如果为-1,那么CPU的所有cores都用于并行工作。

分类器我们直接用模块了,那么下面就直接开始测试呗。

4.测试算法

上面我们已经将数据处理成分类器可以识别的格式, 本节我们将这些数据输⼊到分类器,检测分类器的执⾏效果。在写⼊这些代码之前,我们必须确保将 from os import listdir 写⼊⽂件的起始部分,这段代码的主要功能是从os模块中导⼊函数listdir,它可以列出给定⽬录的⽂件名。 因为我们数据集的命名就是真实标签。

#手写数字分类测试
def handwritingClassTest():
    #测试集的Labels
    hwLables = []
    #返回trainingDigits目录下的文件名
    trainingFileList = listdir('0403Dataset/trainingDigits')
    #返回文件下的数量
    m = len(trainingFileList)
    #初始化训练矩阵
    trainingMat = np.zeros((m,1024))
    #从文件名中得到标签
    for i in range(m):
        #获取文件名
        fileNameStr = trainingFileList[i]   
        #截取标签
        classNumber = int(fileNameStr.split('_')[0])    #真实标签
        #将获得的类别添加到hwLable
        hwLables.append(classNumber)
        #处理数据,转为1*1024,转入trainingMat矩阵中
        trainingMat[i,:] = img2vector('0403Dataset/trainingDigits/%s'%(fileNameStr))   #每个文件向量处理
        
    #构建KNN分类器
    neigh = KNN(n_neighbors=3,algorithm='auto')    #选3个近邻的,使用快速k近邻搜索算法,其余默认
    #用获得的数据拟合模型
    neigh.fit(trainingMat,hwLables)
    #===================================
    #开始测试
    #对测试集进行处理
    #返回测试集下的目录
    testFileList = listdir('0403Dataset/testDigits')
    #初始化错误个数
    errorCount = 0
    m_Test = len(testFileList)
    for i in range(m_Test):
        #获取目录下的名字
        fileNameStr = testFileList[i]
        #截取真实标签
        classNumber = int(fileNameStr.split('_')[0])
        #数据处理,1*1024
        vectorTest = img2vector('0403Dataset/testDigits/%s'%(fileNameStr))

        #开始分类器分类,获得结果
        #classifierResult = classify0(vectorTest, trainingMat, hwLables, 3)
        classifierResult = neigh.predict(vectorTest)   #通过训练的模型预测测试集结果
        print("预测结果:%d\t真实结果:%d"%(classifierResult,classNumber))
        if classifierResult != classNumber:
            errorCount += 1
    print("错了%d个,错误率为:%f%%"%(errorCount,(errorCount/m_Test) * 100))
#--------测试---------------------------------------------
if __name__ == '__main__':
    handwritingClassTest() 

结果:
在这里插入图片描述

5.使用算法

既然咱们前面的补充内容讲解了怎样将图片转化为文本型,那为何不用自己写的数字来验证一下这个分类器呢?不用白不用。
前面我写了一个6,是否可以将它识别出来呢?稍微修改一下上面的测试算法,测试一下吧。

#手写数字分类测试
def handwritingClassTest():
    #测试集的Labels
    hwLables = []
    #返回trainingDigits目录下的文件名
    trainingFileList = listdir('0403Dataset/trainingDigits')
    #返回文件下的数量
    m = len(trainingFileList)
    #初始化训练矩阵
    trainingMat = np.zeros((m,1024))
    #从文件名中得到标签
    for i in range(m):
        #获取文件名
        fileNameStr = trainingFileList[i]
        #截取标签
        classNumber = int(fileNameStr.split('_')[0])
        #将获得的类别添加到hwLable
        hwLables.append(classNumber)
        #处理数据,转为1*1024,转入trainingMat矩阵中
        trainingMat[i,:] = img2vector('0403Dataset/trainingDigits/%s'%(fileNameStr))
    #构建KNN分类器
    neigh = KNN(n_neighbors=3,algorithm='auto')
    #拟合模型
    neigh.fit(trainingMat,hwLables)
    #=======================================
    #开始测试
    #对测试集进行处理
    #返回测试集下的目录
    #数据处理,1*1024
    vectorTest = img2vector('0403Dataset/zixie.txt')
    #开始分类器分类,获得结果
    classifierResult = neigh.predict(vectorTest)
    print("预测结果:%d"%(classifierResult))
#-------使用算法------------------------
if __name__ == '__main__':
    picTo01('0403Dataset/zixie.png')    #先处理我的图像
    handwritingClassTest()

结果:
在这里插入图片描述
可以看到预测的结果是对的( ̄▽ ̄)*。
其实我写了好几个,都识别错了。可测试集的正确率很高呀,难道我字太难看,为难它了???ㄟ( ▔, ▔ )ㄏ
实际使⽤这个算法时,算法的执⾏效率并不⾼。因为算法需要为每个测试向量做2000次距离计算,每个距离计算包括了1024个维度浮点运算,总计要执⾏900次,此外,我们还需要为测试向量准备2MB的存储空间。是否存在⼀种算法减少存储空间和计算时间的开销呢?决策树就是k近邻算法的优化版,可以节省⼤量的计算开销。那么下章我们就讲决策树吧!

六、总结

k近邻算法是分类数据最简单最有效的算法,本章我们通过三个例⼦讲述了如何使⽤k近邻算法构造分类器。k近邻算法是基于实例的学习,使⽤算法时我们必须有接近实际数据的训练样本数据。k近邻算法也必须保存全部数据集,如果训练数据集的很⼤,必须使⽤⼤量的存储空间。此外,由于必须对数据集中的每个数据计算距离值,实际使⽤时可能⾮常耗时。 K近邻算法的另⼀个缺陷是它⽆法给出任何数据的基础结构信息,因此我们也⽆法知晓平均实例样本和典型实例样本具有什么特征。优点也是有的,就是简单好用,容易理解,精度高,理论成熟,既可以用来做分类也可以用来做回归;训练时间复杂度为O(n);无数据输入假定;对异常值不敏感;可用于数值型数据和离散型数据。

  • 本文结合各位大牛所思所想,不胜感激!
  • 如有错误,请不吝指正!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值