一、实验目的
熟悉KNN算法的基本流程,同时掌握sklearn机器学习库的使用。
(代码在后面)
本实验首先使用基于Python实现KNN算法实现手写字识别,然后使用sklearn库的KNN算法实现手写字识别。
我们首先使用如下解压命令将kNN_hand_writing.tgz压缩包解压,里面包含两个子目录,分别为trainingDigits和testDigits。trainingDigits为训练数据集的文件,文件数量为1935;testDigits为测试数据集的文件,文件数量为947。每个文件的命名格式都为“真实数字_编号.txt”。为了简便处理,实验中,用txt文本文件表示图片。原图片中像素值为黑色(0,0,0)的像素点在txt中对应的用0表示,像素值为白色(255,255,255)的像素点用1表示。所以,只需要处理这些文本文件即可,不用再去解析图片格式。每个txt文件中,数据共有32行和32列,这是由于原图片的大小为32X32。
KNN的算法流程为:
a.计算已知类别数据集中的点与当前点之间的距离;
b.按照距离递增次序排序;
c.选取与当前点距离最小的k个点;
d.确定前k个点所在类别的出现频率;
e.返回前k个点所出现频率最高的类别作为当前点的预测分类。
在这一步里,我们将定义函数classify0作为KNN算法的核心函数,其函数的完整形式为: def classify0(inX, dataSet, labels, k): 其中各参数含义为:
inX – 用于要进行分类判别的数据,是一个向量(来自测试集)
dataSet – 所有训练数据的集合,是一个矩阵(训练集)
lables – 分类标签
k – KNN算法参数,选择距离最小的k个点
根据KNN算法流程,我们首先应该计算inX这个要判别分类的点到dataSet中每个点之间的距离,dataSet中每个点也是用向量表示的,点与点之间的距离,实则就是求两个向量之间的距离,在数学中有很多距离计算公式,我们这里选择最简单的欧氏距离计算方法。设A和B为两向量,则两向量间的欧氏距离为:
函数的输入参数和距离计算公式都明确了,下面让我们来实现KNN算法。这里,我们使用numpy提供的各种功能来实现该算法,相比于自己纯手写各种线性代数变换操作,使用numpy的效率要高的多。classify0函数的完整如下所示,第2行获得数据集的行数,即已知数据集中所有点的数量。第3到第6行为计算要分类的点到数据集中每个点的距离,计算过程中用到很多numpy的特性。第7行按照距离远近从近到远排序,注意argsort返回的是原始下标排序后的下标序列。你的任务是将统计距离最近的前k个点中每个类别出现的次数代码补充完整,并取出出现频率最高的分类。
接下来,我们用个小例子验证一下KNN算法,随机挑选6位高中生,分别让他们做文科综合试卷的分数和理科综合试卷的分数,下表为分数以及分类信息
直觉上,理科生的理综成绩比较高,文综成绩较低,文科生的文综成绩较高,理综成绩较高。基于这些信息,我们利用kNN算法判 断成绩为(105,210)所属的分类,通过如下代码实现,可以发现结果显示“文科生”,输出结果结果比较符合预期。
接下来,我们将加载手写字训练集数据,由于kNN核心算法中,每个点都是用向量表示的。尽管已经转换为文本形式了,不过图片数据依然是32X32的二维数据格式,首先需要将其转换为一维数组,即表示成一个向量。函数img2vector用于将32X32的二维数组转换为一维数组,该函数传入参数文件名,返回转换后的一维数组:
下面来测试函数img2vector是否正确,传入的文件路径为:kNN_hand_writing/trainingDigits/1_1.txt,如果打印结果如下为1024维向量,函数编写正确。
(图片省略部分输出内容)
接下来我们编写loadTrainData函数,加载所有训练数据。该函数将返回表示训练集数据的矩阵以及每个点对应的分类标签:
我们来验证一下我们的kNN手写字识别算法的准确性,测试使用的数据集位于目录testDigits下。测试的流程为:依次读取testDigits目录下的每个文件,根据文件中的数据,使用分类函数classify0确定其分类,并和其真实分类进行对比,如果一致,表示分类正确,如果不一致,表示分类错误。统计错误的数量,计算错误率,错误率为分类出错的数量除以测试集中测试数据总量。编写handwritingClassTest函数测试基于kNN算法实现的手写字识别系统准确性,前面部分已经给出,你的任务是实现依次读取testDigits目录下的每个文件,根据文件名称获取其真实分类数字,然后使用classify0函数基于kNN算法进行分类,对比两个分类结果。如果不一致,就对errorCount自增1的代码。
最后,添加main入口,调用handwritingClassTest函数,对手写字识别系统进行验证。可以看到测试总数据量为946,出错的数据量为10,错误率为0.010571%。下面结果是k取3的结果,你可以将k的数量进行调整,观察k的值和错误间的联系。
(图片省略部分输出结果)
sklearn.neighbors模块实现了k-近邻算法,点击官网英文文档地址,在sklearn官网上查看sklearn的kNN算法的详细使用手册。我们使用其中的KNeighborsClassifier也可以实现上小节自己手动实现的kNN算法,KNeighborsClassifier函数一共有8个参数,我们这里重点介绍一下n_neighbors,其默认为5,就是KNN算法的k的值,其余参数可以在官方文档学习。
由于基于sklearn提供的kNN算法,修改kNNHandWrittenTest函数,将调用classify0的部分逻辑修改为sklearn提供的kNN接口。两个版本的其他部分的逻辑是完全一样的:
出错的数据数量为12,错误率为0.012685%。你可以根据sklearn的kNN算法手册,调整kNN算法的相关参数,尝试着降低错误率。
# -*- coding: utf-8 -*-
"""
Created on Thu May 12 15:36:45 2022
@author: 86152
"""
'''
.KNN核心算法
KNN的算法流程为:
a.计算已知类别数据集中的点与当前点之间的距离;
b.按照距离递增次序排序;
c.选取与当前点距离最小的k个点;
d.确定前k个点所在类别的出现频率;
e.返回前k个点所出现频率最高的类别作为当前点的预测分类。
'''
import os
import numpy as np
import operator
from os import listdir
def classify0(inX, dataSet, labels, k):
m=dataSet.shape[0] #返回dataSet的行数
diffMat=np.tile(inX,(m, 1))-dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances *0.5
sortedDistIndices = distances.argsort() # argsort函数返回的数组值从小到大的索引值
classCount = {} #用于类别/次数的字典,key为类别, value为次数
for i in range(k):
#取出第近的元素对应的类别
voteIlabel=labels[sortedDistIndices[i]]
#对类别次数进行累加
classCount[voteIlabel]=classCount.get(voteIlabel,0)+1
#根据字典的值从大到小排序
sortedClassCount=sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
#返回次数最多的类别,即所要分类的类别
return sortedClassCount[0][0]
# 文理科生
dataSet=np.array([[250,100],[270,120],[111,230],[130,260],[200,80],[70,190]])
labels=["理科生","理科生","文科生","文科生","理科生","文科生"]
inX=[105,210]
print(classify0(inX,dataSet,labels,3))
#3.数据加载和处理
def img2vector(filename):
returnVect = np.zeros((1, 1024)) #创建1x1024零向量
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32): #每一行的前32个元素依次添加到returnVect中
returnVect[0, 32*i+j] = int(lineStr[j])
return returnVect #返回转换后的1x1024向量
np.set_printoptions(threshold=np.inf) #设置输出不省略
print(img2vector("D:\\课程上的一些文件夹\\机器学习II\\实验\\实验数据集\\实验六KNN\\kNN_hand_writing\\trainingDigits\\1_1.txt"))
def loadTrainData():
hwLabels = [] #测试集的Labels
trainingFileList = listdir('D:\\课程上的一些文件夹\\机器学习II\\实验\\实验数据集\\实验六KNN\\kNN_hand_writing\\trainingDigits') #返回trainingDigits目录下的文件名
m = len(trainingFileList) #返回文件夹下文件的个数
trainingMat = np.zeros((m, 1024)) #初始化训练的Mat矩阵,测试集
for i in range(m): #从文件名中解析出训练集的类别
fileNameStr = trainingFileList[i] #获得文件的名字
classNumber = int(fileNameStr.split('_')[0])#获得分类的数字
hwLabels.append(classNumber)#将获得的类别添加到hwLabels中
#将每一个文件的1x1024数据存储到trainingMat矩阵中
trainingMat[i,:] = img2vector('D:\\课程上的一些文件夹\\机器学习II\\实验\\实验数据集\\实验六KNN\\kNN_hand_writing\\trainingDigits/%s' % (fileNameStr))
return hwLabels,trainingMat
print(loadTrainData())
#4.KNN手写识别
#手写数字识别系统测试代码
def handwritingClassTest():
#样本数据的类标签列表
hwLabels = []
#样本数据文件列表
trainingFileList = os.listdir("kNN_hand_writing/trainingDigits")
#得到文件行数
m = len(trainingFileList)
#初始化样本数据矩阵
trainingMat = np.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('kNN_hand_writing/trainingDigits/%s' %(fileNameStr))
#读取测试数据
testFileList = os.listdir('kNN_hand_writing/testDigits')
#初始化错误率
errorCount = 0.0
mTest = len(testFileList)
errfile = []
#循环测试每个测试数据文件
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
#提取数据向量
vectorUnderTest = img2vector('kNN_hand_writing/testDigits/%s' %(fileNameStr))
#对数据文件进行分类
classifierResult = classify0(vectorUnderTest,trainingMat,hwLabels ,3) #传值k=3
#输出k-近邻算法分类结果和真实的分类
print('分类返回结果: %d,真实结果:%d' %(classifierResult,classNumStr))
#判断k-近邻算法是否准确
if(classifierResult != classNumStr):
errorCount +=1.0
errfile.append(fileNameStr)
print('\n 错误总数: %d' %(errorCount)) #错误的总数
print('错误的是:%s ;' %[i for i in errfile])
print('\n 总错误率: %f' %(errorCount/float(mTest))) #总错误率
handwritingClassTest()
基于sklean
# -*- coding: utf-8 -*-
"""
Created on Sat May 14 18:03:12 2022
@author: 86152
"""
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat May 14 14:04:43 2022
@author: yanxiaoyu
"""
import numpy as np
import operator
from os import listdir
def classify0(inX, dataSet, labels, k):
m = dataSet.shape[0]
diffMat = np.tile(inX,(m,1))-dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances * 0.5
sortedDistIndices = distances.argsort()
#print(sortedDistIndices)
classCount = {}
for i in range(k):
if labels[sortedDistIndices[i]] not in classCount.keys():
classCount[labels[sortedDistIndices[i]]] = 1
else:
classCount[labels[sortedDistIndices[i]]] +=1
#print(classCount)
sort = sorted(classCount.items(),key=lambda x:x[1],reverse=True)
return sort[0][0]
def img2vector(filename):
returnVect = np.zeros((1, 1024)) #创建1x1024零向量
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32): #每一行的前32个元素依次添加到returnVect中
returnVect[0, 32*i+j] = int(lineStr[j])
return returnVect #返回转换后的1x1024向量
def loadTrainData():
hwLabels = [] #测试集的Labels
trainingFileList = listdir('trainingDigits') #返回trainingDigits目录下的文件名
m = len(trainingFileList) #返回文件夹下文件的个数
trainingMat = np.zeros((m, 1024)) #初始化训练的Mat矩阵,测试集
for i in range(m): #从文件名中解析出训练集的类别
fileNameStr = trainingFileList[i] #获得文件的名字
classNumber = int(fileNameStr.split('_')[0])#获得分类的数字
hwLabels.append(classNumber)#将获得的类别添加到hwLabels中
#将每一个文件的1x1024数据存储到trainingMat矩阵中
trainingMat[i,:] = img2vector('trainingDigits/%s' % (fileNameStr))
return hwLabels,trainingMat
from sklearn.neighbors import KNeighborsClassifier as kNN
def handwritingClassTest():
errorCount = 0.0
hwLabels,trainingMat=loadTrainData()
testFileList = listdir('testDigits')
mTest = len(testFileList) #测试数据的数量
#sklearn库使用
neigh=kNN(n_neighbors=3,algorithm='auto') #'auto' 将尝试根据传递给fit方法的值来决定最合适的算法。
neigh.fit(trainingMat,hwLabels)
for i in range(mTest):
fileNameStr = testFileList[i] #获得文件的名字
classNumber = int(fileNameStr.split('_')[0])#获得分类的数字
vectorUnderTest = img2vector('testDigits/%s' % (fileNameStr))
classifierResult = neigh.predict(vectorUnderTest)
print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
if(classifierResult != classNumber):
errorCount += 1.0
print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount/mTest))
if __name__ == "__main__":
#handwritingClassTest2();
handwritingClassTest();