机器学习实战_k近邻算法识别手写数字代码解读

参考文章:

http://blog.csdn.net/scut_arucee/article/details/50261739
http://blog.csdn.net/u014771160/article/details/44308091

一 背景介绍


正所谓识别手写数字(这里仅限0-9,因为我们的数据有限),就是说我们在一块画布上面,然后让这个算法识别这个数字是多少。想一想,这是不是就是我们想要的机器学习的效果呢?举个例子,我们经常用手机上面的手写功能,为什么我们在手机上面写一个歪歪扭扭的字,输入法就可以猜到我们想要的字到底是什么呢?其实是一个道理,当然了,我们这个小系统没有手写输入法那么强大,但是如果理解了这个,那么其他的不就是一个道理了嘛。如果我们也想要做的话,那就得学习更多的算法,并且改进、测试,要做的工作很多呀。好了,我们开始说系统的部分。

        我们选取的是一张32*32像素的画布(就是打开windows画图,把网格调成32*32的,然后以bmp单色位图存储就可以了),然后在这个画布上面写上0-9。写后上后如下图机器学习实战_k近邻算法识别手写数字代码解读

        那么有了这么一张bmp图片对于我们现在来说没有任何用处啊,因为我们是要编程的,总不能告诉计算机说:这就是9的样子,你记住他就行了,其他和它像的都叫9。计算机可没有那么聪明,因为它只认识0和1,所以人类大脑是比计算机聪明的多滴!接下来的工作就是把这张图片变成计算机可以认识的文件了,什么样子呢?看下面

机器学习实战_k近邻算法识别手写数字代码解读

        是的,就是这个样子,但是这些原始数据我们是可以从网上下下来的,所以我会在本文的最后给上下载链接。当我们拿到这些数据之后,就可以直接对这些数据操作啦。那么为什么已经有了这些数据了,我还有说这些数据是怎么来的呢?不是多此一举吗???也是。。。。我这个比较唠叨啦。希望可以写的明白一点。因为比较K-近邻是机器学习的开始嘛。


二 代码详细解读


仍然是在前面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模块加入了统计某个函数运行时间的模块。



三 代码讲解。。。(终于到了=_=)


          第一部分,我们要把这些0101改成另外一种形式,之前我们说过,我们的画布是32*32像素的。那么其实这个txt文件中的0101也是一个32*32的矩阵,但是我们在计算距离的时候通常多少一维的,也就是说是(x1,x2,x3.....xn)这种形式的,所以我们首先应该把32*32的矩阵变成1*1024的形式才方便我们计算距离啊。代码如***释我写的很清楚了):

        

[python] view plain copy
  1. <pre name="code" class="python">
  2. #图像转换函数(32*32图像转换为1*1024向量)
  3. def img2vector(filename):  
  4.     """ 
  5.     filename代表文件名称 
  6.     """  
  7.     #初始化待返回的向量 
  8.     returnVector = zeros((1,1024))##声明一个0矩阵  
  9.     fr = open(filename)  
  10.     for i in range(32):  
  11.         lineStr = fr.readline()##每次读取一行内容,以字符串形式存储   
  12.         #逐个取出当前行的每一个字符,并转化为数字
  13.         for j in range(32):  
  14.             returnVector[0,32*i+j] = int(lineStr[j])##一共32行,全部存储到returnVector里面  
  15.     fr.close()  
  16.     return returnVector  



       第二部分:把所有的图片文件编程1*1024的数组之后是不是就应该计算每个测试样本和原始训练样本的距离了:

       这里用两个函数计算

        

[python] view plain copy
  1. def classify(inX,dataSet,labels,k):  
  2.     """ 
  3.     四个参数,inX是测试向量,dataSet样本向量数据,labels是标签,k是选取前k个做评测 
  4.     tile(A,n)用于重复A矩阵n次     
  5.     argsort()返回的是数组值从小到大的索引 
  6.     list.get(k,d) 
  7.     get()相当于一条if...else...语句,参数k在字典中,字典将返回list[k];如果参数k不在字典中则返回参数d,如果K在字典中则返回k对应的value值; 
  8.     例子: 
  9.     l = {5:2,3:4} 
  10.     print l.get(3,0)返回的值是4; 
  11.     Print l.get(1,0)返回值是0;(该例来源于网络) 
  12.     """  
  13.     dataSetSize = dataSet.shape[0]##shpe函数用于返回矩阵的长度,如shape[0]返回第一维矩阵长度,shape[1]返回第二维矩阵长度以此类推,还有其他功能执行查阅  
  14.     diffMat = tile(inX,(dataSetSize,1)) - dataSet##tile函数主要功能是重复矩阵多少次,重复了测试向量,与每一个样本相减  
  15.     sqDiffMat = diffMat**2##计算平方  
  16.     sqDistances = sqDiffMat.sum(axis = 1)##计算矩阵横轴的和  
  17.     distances = sqDistances**0.5##平方  
  18.     sortedDistIndicies = distances.argsort()##用argsort排序  
  19.     classCount = {}  
  20.     for i in range(k):  
  21.         voteLabel = labels[sortedDistIndicies[i]]##通过索引得到前该距离所属的类型  
  22.         classCount[voteLabel] = classCount.get(voteLabel,0)+1##相应的类型+1  
  23.     sortedClassCount = sorted(classCount.iteritems(),key = operator.itemgetter(1),reverse = True)  
  24.     return sortedClassCount[0][0]  
  25.   
  26. """ 
  27. #手写数字识别测试函数 
  28. classTest()函数用于处理32*32的数据, 
  29. """  
  30. def classTest():  
  31.     file_object = open('result.txt''w')  
  32.     Labels = []  #初始化类别标签为空列表 
  33.     trainingFileList = os.listdir("./digits/trainingDigits/")##listdir函数用列出给定目录下所有训练数据的文件名  ,并且以数组的方式存储  
  34.     length = len(trainingFileList)##获取训练数据数目的数组长度 
  35.     #初始化m个图像的训练矩阵   
  36.     trainingMat = zeros((length,1024))##声明一个length*1024的矩阵用于存储所有样本的向量形式 
  37.     #遍历每一个训练数据   
  38.     for i in range(length): 
  39.         #取出一个训练数据的文件名  
  40.         fileNameStr = trainingFileList[i]##获取列表中每一个文件名(包含扩展名)  
  41.         #去掉该训练数据的后缀名.txt  
  42.         fileName = fileNameStr.split('.')[0]##获取列表中每一个文件名(不包含扩展名)  
  43.         #取出代表该训练数据类别的数字  
  44.         numClass = fileName.split('_')[0]##获取该文件所属的类别(因为文件名都是以‘数字类别_第几个样本.txt’形式的,所以需要进行两次的split函数)  
  45.         #将代表该训练数据类别的数字存入类别标签列表  
  46.         Labels.append(numClass)##以队列的形式加入到Labels的队尾  
  47.         #调用图像转换函数将该训练数据的输入特征转换为向量并存储 
  48.         trainingMat[i,:] = img2vector("./digits/trainingDigits/"+ fileNameStr)##用img2vector()函数处理32*32的图片矩阵,存入trainingMat中  
  49.     #列出给定目录下所有测试数据的文件名  
  50.     testFileList = os.listdir("./digits/testDigits/")##测试组的文件列表,下面的代码意思如上,多余的就不写了 
  51.     #初始化测试犯错的样本个数   
  52.     erreCount = 0.0  
  53.     #求测试数据数目
  54.     lengthTest = len(testFileList) 
  55.     #遍历每一个测试数据   
  56.     for i in range(lengthTest): 
  57.         #取出一个测试数据的文件名   
  58.         fileNameStr = testFileList[i] 
  59.         #去掉该测试数据的后缀名.txt   
  60.         fileName = fileNameStr.split('.')[0]  
  61.         #取出代表该测试数据类别的数字 
  62.         numClass = fileName.split('_')[0]  
  63.         #调用图像转换函数将该测试数据的输入特征转换为向量 
  64.         vectorUnderTest = img2vector("./digits/testDigits/"+fileNameStr)  
  65.         #调用k-NN简单实现函数,并返回分类器对该测试数据的分类结果  
  66.         classifierResult =  classify(vectorUnderTest,trainingMat,Labels,3)  
  67.        print("the classifier came back with: %d, the real answer is: %d" % (classifierResult,classNumStr)) 
  68.        #统计分类器对测试数据分类犯错的个数 
  69.        if (classifierResult != classNumStr): 
  70.             errorCount += 1.0 
  71.     print("\nthe total number of error is: %d" % errorCount) 
  72.    #输出分类器错误率  
  73.     print("\nthe total error rate is: %f" % (errorCount/float(mTest)))