机器学习实战k近邻算法(kNN)应用之手写数字识别代码解读

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SCUT_Arucee/article/details/50261739

一.背景简要说明

书中假设待识别的数字已经使用图形处理软件将其处理为32*32的黑白图像,并将图片转换为文本格式。如下图代表数字0:


每个数字的训练样本大概有200个,每个数字的测试样本大概有100个,分别放在trainingDigits和testDigits中。

二.模块代码及注释


from numpy import *
from os import listdir
import operator
import time

#k-NN简单实现函数
def classify0(inX,dataSet,labels,k):

    #求出样本集的行数,也就是labels标签的数目
    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()

    #对距离最小的k个点统计对应的样本标签
    classCount = {}
    for i in range(k):
        #取第i+1近邻的样本对应的类别标签
        voteIlabel = labels[sortedDistIndicies[i]]
        #以标签为key,标签出现的次数为value将统计到的标签及出现次数写进字典
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1

    #对字典按value从大到小排序
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)

    #返回排序后字典中最大value对应的key
    return sortedClassCount[0][0]

#----------------------------------------------------------------------------

#函数运行耗时统计函数
def time_me(fn):
  def _wrapper(*args, **kwargs):
    start = time.clock()
    fn(*args, **kwargs)
    print ("\n%s cost %s second"%(fn.__name__, time.clock() - start))
  return _wrapper

#----------------------------------------------------------------------------

#图像转换函数(32*32图像转换为1*1024向量)
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

#----------------------------------------------------------------------------

#手写数字识别测试函数
@time_me
def handwritingClassTest():

    #初始化类别标签为空列表
    hwLabels = []

    #列出给定目录下所有训练数据的文件名
    trainingFileList = listdir('F:/machinelearninginaction/Ch02/trainingDigits')

    #求训练数据数目
    m = len(trainingFileList)

    #初始化m个图像的训练矩阵
    trainingMat = zeros((m,1024))
    
    #遍历每一个训练数据
    for i in range(m):

        #取出一个训练数据的文件名
        fileNameStr = trainingFileList[i]

        #去掉该训练数据的后缀名.txt
        fileStr = fileNameStr.split('.')[0]

        #取出代表该训练数据类别的数字
        classNumStr = int(fileStr.split('_')[0])

        #将代表该训练数据类别的数字存入类别标签列表
        hwLabels.append(classNumStr)

        #调用图像转换函数将该训练数据的输入特征转换为向量并存储
        trainingMat[i,:] = img2vector('F:/machinelearninginaction/Ch02/trainingDigits/%s' % fileNameStr)

    #列出给定目录下所有测试数据的文件名
    testFileList = listdir('F:/machinelearninginaction/Ch02/testDigits')

    #初始化测试犯错的样本个数
    errorCount = 0.0

    #求测试数据数目
    mTest = len(testFileList)

    #遍历每一个测试数据
    for i in range(mTest):

        #取出一个测试数据的文件名
        fileNameStr = testFileList[i]

        #去掉该测试数据的后缀名.txt
        fileStr = fileNameStr.split('.')[0]

        #取出代表该测试数据类别的数字
        classNumStr = int(fileStr.split('_')[0])

        #调用图像转换函数将该测试数据的输入特征转换为向量
        vectorUnderTest = img2vector('F:/machinelearninginaction/Ch02/testDigits/%s' % fileNameStr)

        #调用k-NN简单实现函数,并返回分类器对该测试数据的分类结果
        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 error is: %d" % errorCount)

    #输出分类器错误率
    print("\nthe total error rate is: %f" % (errorCount/float(mTest)))
    


三.详细解读


仍然是在前面kNN模块的基础上继续添加新的函数或方法。

kNN的核心实现代码前面已给出: 机器学习实战k-邻近算法(kNN)简单实施代码解读

关于kNN示例之改进婚恋网站配对效果的代码解读可参考: 机器学习实战k-近邻算法(kNN)应用之改进婚恋网站配对效果代码解

为了防止代码看上去很冗长,删除了一些无关的函数,新增了3个函数或方法,分别是函数运行耗时统计函数time_me,图像转换函数img2vector和手写数字识别测试函数handwritingClassTest。

1.图像转换函数img2vector

为了可以继续使用之前的kNN简单实现函数,我们要把32*32的二进制图像矩阵转换为1*1024的向量,所以我们定义

图像转换函数img2vector来实现此功能。


★lineStr = fr.readline()

这里是按行读取图像矩阵,每次读取一行并以字符串的形式存储在lineStr中。注意这里不能误使用readlines函数。


returnVect[0,32*i+j] = int(lineStr[j])

利用行列关系逐个取出当前行的每一个字符,并转化为数字,构造出1*1024的向量。


2.手写数字识别测试函数handwritingClassTest

该函数或者说该方法不需要传入参数,主要从训练数据中提取出kNN简单实现函数所需要的参数,如:测试数据的输入特征,训练数据的输入特征矩阵,训练数据的类别标签向量。


trainingFileList = listdir('F:/machinelearninginaction/Ch02/trainingDigits')

列出F:/machinelearninginaction/Ch02/trainingDigits路径下所有训练数据的文件名,文件名是包括后缀类型的。所有文件名都存储在一个列表中,每一个文件名以字符串的形式作为一个列表元素。


fileNameStr = trainingFileList[i]

在使用for循环遍历所有训练数据时,trainingFileList列表的第i个元素即为第i个训练样本的文件名。


fileStr = fileNameStr.split('.')[0]

将该训练样本文件名根据'.'分割成包含一个或多个元素的列表,因为我们的训练数据文件名是'0_10.txt','2_35.txt'等这种形式,第一个数字代表该训练样本的分类标签,即代表什么数字;下划线后的数字意义是这个样本是第多少个样本,如'0_10.txt'表示这是数字0的第10个训练样本。


故使用split('.'),就将文件名分为两个部分存入列表了,如'0_10'成为了列表的第一个元素,即split('.')[0];而'txt'就成为了列表的第二个元素,即split('.')[1]。我们需要从第一个元素中提取出该训练样本的类别标签信息,故只取出split('.')[0]。


classNumStr = int(fileStr.split('_')[0])

这就是前面所说的根据文件名中下划线前后两个数字的意义,从文件名中提取出该训练样本的类别标签信息。


hwLabels.append(classNumStr)

将从文件名中提取到的代表该训练样本类别的数字存入类别标签列表。


trainingMat[i,:] = img2vector('F:/machinelearninginaction/Ch02/trainingDigits/%s' % fileNameStr)

fileNameStr会遍历每一个文件名,所以这里调用图像转换函数img2vector将每一个训练数据的二进制数字图像矩阵存储在trainingMat中,trainingMat每一行都是一个1*1024的向量。


之后对测试样本进行和训练样本类似的分离文件名等操作。但区别如下:


vectorUnderTest = img2vector('F:/machinelearninginaction/Ch02/testDigits/%s' % fileNameStr)

使用for循环遍历每一个测试数据,这里调用图像转换函数img2vector将当前测试数据的二进制数字图像矩阵存储为一个1*1024的向量作为接下来kNN简单实现函数的输入特征。


classifierResult = classify0(vectorUnderTest,trainingMat,hwLabels,3)

调用k-NN简单实现函数(k取3),并返回分类器对该测试数据的分类结果


3.函数运行耗时统计函数time_me

为了测试使用k近邻算法进行手写数字识别耗时情况,我们在kNN模块加入了统计某个函数运行时间的模块。


四.运行结果


在控制窗口导入模块后,运行手写数字识别测试函数。
>>> import kNN
>>> kNN.handwritingClassTest()

运行结果如下:



可以看到分类器一共在11个测试样本上犯了错,错误率为1.2%左右。总共耗时43.5秒。


随机选取训练样本,改变训练样本数目,改变k的值都会对最终的错误率产生影响。实际上,k近邻执行效率并不高,因为一共有900个测试向量,对于每个测试向量,要和2000个训练数据做距离计算,每个距离计算包括了1024个维度的运算。所以我们看到最终分类函数执行了40多秒,耗时相当长。除此之外,k近邻还要保存全部数据集,如果训练数据集非常大,则要占用相当大的存储空间。

猜你喜欢

转载自blog.csdn.net/SCUT_Arucee/article/details/50261739