本章介绍第一个机器学习算法:A-近邻算法,它非常有效而且易于掌握。首先,我们将探讨女-近邻算法的基本理论,以及如何使用距离测量的方法分类物品;其次我们将使用?7««^从文本文件中导人并解析数据; 再次,本书讨论了当存在许多数据来源时,.如何避免计算距离时可能碰到的一些常见错误;最后,利用实际的例子讲解如何使用匕近邻算法改进约会网站和手写数字识别系统。
一、K-近邻算法概述--------->K-近邻算法采用测量不同特征值之间的距离方法进行分类。
工作原理是:存在一个样本数据集合,也称作训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。输人没有标签的新数据后, 将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最 近 邻 )的分类标签。一般来说,我们
只选择样本数据集中前&个最相似的数据,这就是&-近邻算法中&的出处,通常*是不大于20的整数。最 后 ,选择K个最相似数据中出现次数最多的分类,作为新数据的分类。
1.准备:使用Python导入数据
在构造完整的丨-近邻算法之前,我们还需要编写一些基本的通用函数,kNN.py代码如下
1 def createDataSet(): 2 group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]]) 3 labels = ['A','A','B',''] 4 return group, labels
在上述的代码中需要导入两个模块:
from numpy import*
import operator
然后进入Python开发环境之后,输人下列命令导人上面编辑的程序模块:
>>> import kNN
上 述 命 令 导 人 kNN 模 块 。为了确保输人相同的数据集,kNN模 块 中 定 义 了 函 数createDataSet,在Python命令提本符下输入下属命令
>>> group,labels = kNN.createDataSet()
上述命令创建了变量group 和labels ,在Python 命令提示符下, 输人变量的名字以检验是否正确
地定义变量:
>>> g r o u p
labels
2.从文本文件中解析数据
这里首先给出k-近邻算法的伪代码和实际的Python代码 ,然后详细地解释每行代码的含义。该函数的功能是使用k-近邻算法将每组数据划分到某个类中,其伪代码如下:
K-近邻算法代码:
1 def classify0(inX, dataSet, labels, k): 2 #计算距离 3 dataSetSize = dataSet.shape[0] #求出样本集的行数,也就是labels标签的数目 4 diffMat = tile(inX, (dataSetSize,1)) - dataSet #构造输入值和样本集的差值矩阵 5 #计算欧氏距离 6 sqDiffMat = diffMat**2 7 sqDistances = sqDiffMat.sum(axis=1) 8 distances = sqDistances**0.5 9 sortedDistIndicies = distances.argsort() #求距离从小到大排序的序号 10 #选择距离最小的K个点,对距离最小的K个点统计对应的样本标签 11 classCount={} 12 for i in range(k): 13 voteIlabel = labels[sortedDistIndicies[i]] #取第i+1邻近的样本对应的类别标签 14 classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1 #以标签为key,标签出现的次数为value将统计到的标签及出现次数写进字典 15 #排序 16 sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True) #对字典按value从大到小排序 17 return sortedClassCount[0][0] #返回排序后字典中最大value对应的key
classify0 ()函数有4个输人参数:用于分类的输人向量是丨必, 输人的训练样本集为daaSet,标签向量为labels ,最后的参数义表示用于选择最近邻居的数目,其中标签向量的元素数目和矩阵dataSet 的行数相同。
计算完所有点之间的距离后,可以对数据按照从小到大的次序排序。然后,确定前k个距离最小元素所在的主要分类 , 输人k总是正整数;最 后 ,将classCount字典分解为元组列表,然后使用程序第二行导入运算符模块的itemgetter ,按照第二个元素的次序对元组进行排序©。此处的排序为逆序,即按照从最大到最小次序排序,最后返回发生频率最高的元素标签。
3.如何测试分类器
为了测试分类器的效果,我们可以使用已知答案的数据,当然答案不能告诉分类器,检验分类器给出的结果是否符合预期结果。通过大量的测试数据,我们可以得到分类器的错误率—分类器给出错误结果的次数除以测试执行的总数。错误率是常用的评估方法,主要用于评估分类器在某个数据集上的执行效果。
示例:使用K-近邻算法改进约会网站的配对效果
1.准备数据:从文本文件中解析数据
数据存放在文本文件datingTestSet.txt 中,每个样本数据占据一行,总共有1000行。海伦的样本主要包含以下3种特征:(1)每年获得的飞行常客里程数;(2)玩视频游戏所耗时间百分比;(3) 每周消费的冰淇淋公升数
在kNN.py中创建名为file2matrix的函数,以此来处理输人格式问题。该函数的输人为文件名字符串,输出为训练样本矩阵和类标签向量
下面代码是将文本记录到转换Numpy的解析程序
1 def file2matrix(filename): 2 fr = open(filename) 3 #得到文件行数 4 arrayOLines = fr.readlines() 5 numberOfLines = len(arrayOLines) 6 #创建返回的NumPy矩阵 7 returnMat = zeros((numberOfLines,3)) 8 classLabelVector = [] 9 #解析文件数据到列表 10 index = 0 11 for line in arrayOLines: 12 line = line.strip() 13 listFromLine = line.split('\t') #截取所有的回车字符 14 returnMat[index,:] = listFromLine[0:3]#选取前三个元素,存储在特征矩阵中 15 classLabelVector.append(int(listFromLine[-1])) #将列表的最后一列存储到向量classLabelVector中 16 index +=1 17 return returnMat,classLabelVector
从上面的代码可以看到,Python处理文本文件非常容易。首先我们需要知道文本文件包含多少行。打开文件,得到文件的行数 。然后创建以零填充的矩阵Numpy ( 实际上,NumPy是一个二维数组,这里暂时不用考虑其用途)。为了简化处理,我们将该矩阵的另一维度设置为固定值3 , 你可以按照自己的实际需求增加相应的代码以适应变化的输人值。循环处理文件中的每行数据 , 首先使用函数line.strip()截取掉所有的回车字符,然后使用tab字符\t将上一步得到的整行数据分割成一个元素列表。 接着,我们选取前3个元素, 将它们存储到特征矩阵中。Python语言可以使用索引值-1表示列表中的最后一列元素,利用这种负索引,我们可以很方便地将列表的最后一列存储到向量classLabelVector中。需要注意的是,我们必须明确地通知解释器, 告诉它列表中存储的元素值为整型,否则Python语言会将这些元素当作字符串处理。
在Python命令提示符下输入下面命令:
>>>datingDataMat,datingLabels = kNN.file2matrix('datingTestSet.txt')
现在已经从文本文件中导人了数据, 并将其格式化为想要的格式,接着我们需要了解数据的真实含义。当然我们可以直接浏览文本文件,但是这种方法非常不友好,一般来说,我们会采用图形化的方式直观地展示数据。下面就用?^1!(瓜工具来图形化展示数据内容,以便辨识出一些数据模式。
2.分析数据:使用Matplotlib创建散点图
首先我们使用Matplotlib制作原始数据的散点图,在Python 命令行环境中,输人下列命令:
1 >>>import matplotlib 2 >>>import matplotlib.pyplot as plt 3 >>>fig = plt.figure() 4 >>>ax = fig.add_subplot(111) 5 >>>ax.scatter(datingDataMat[:,1],datingDataMat[:,2]) 6 >>>plt.show()
Matplotlib库提供的scatter函数支持个性化标记散点图上的点。重新输入上面的代码,调用scatter函数时使用下列参数:
ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels),15.0*array(datingLabels))
3.准备数据:归一化数值
在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到 1或者-1到 1之间。下面的公式可以将任意取值范围的特征值转化为0到 1区间内的值:
newValue = (oldValue - min)/(max - min)
其中min和max分别是数据集中的最小特征值和最大特征值。虽然改变数值取值范围增加了分类器的复杂度,但为了得到准确结果,我们必须这样做。我们需要在文件kNN.py 中增加一个新函数autoNorm(), 该函数可以自动将数字特征值转化为0到 1的区间
归一化特征值的代码如下:
1 ### 特征值归一化程序 2 def autoNorm(dataSet): 3 minVals = dataSet.min(0) 4 maxVals = dataSet.max(0) 5 ranges = maxVals - minVals 6 normDataSet = zeros(shape(dataSet)) 7 m = dataSet.shape[0] 8 normDataSet = dataSet - tile(minVals, (m,1)) 9 normDataSet = normDataSet/tile(ranges, (m,1)) #element wise divide 10 return normDataSet, ranges, minVals
在函数autoNorm()中 ,我们将每列的最小值放在变量minVals中 ,将最大值放在变量maxVals中 , 其 中 dataSet.min(0)中的参数0使得函数可以从列中选取最小值,而不是选取当前行的最小值。然 后 ,函数计算可能的取值范围,并创建新的返回矩阵.
为了归一化特征值,我们必须使用当前值减去最小值,然后除以取值范围。需要注意的是,特征值矩阵有1000x3个 值 , 而minVals和range的值都为1x3。为了解决这个冋题,我们使用Numpy库中tile()函数将变量内容复制成输人矩阵同样大小的矩阵,注意这是具体特征值相除,而对于某些数值处理软件包,/可能意味着矩阵除法,但在Numpy 库 中 ,矩阵除法需要使用函数linalg.solve(matA,matB)
在Python命令提示符下, 重 新 加 载 kNN.py 模 块 ,执行autoNorm函数,检测函数的执行结果:
normMat,range,minVals = kNN.atuoNorm(datingDataMat)
4.测试算法:作为完整程序验证分类器
上节我们巳经将数据按照需求做了处理,本节我们将测试分类器的效果,机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器 ,而使用其余的10%数据去测试分类器,检测分类器的正确率。本书后续章节还会介绍一些高级方法完成同样的任务,这里我们还是采用最原始的做法。
前面我们巳经提到可以使用错误率来检测分类器的性能。对于分类器来说, 错误率就是分类器给出错误结果的次数除以测试数据的总数,完美分类器的错误率为0 ,而错误率为1.0的分类器不会给出任何正确的分类结果。代码里我们定义一个计数器变量,每次分类器错误地分类数据,计数器就加1, 程序执行完成之后计数器的结果除以数据点总数即是错误率。
为了测试分类器效果,在kNN.py文件中创建函数datingClassTest,该函数是自包含的,你可以在任何时候在?)也^ 运行环境中使用该函数测试分类器效果。
分类器针对约会网站的测试代码:
1 def datingClassTest(): 2 hoRatio = 0.10 3 datingDataMat,datingLabels = file2matrix('datingTestSet2.txt') 4 normMat,ranges,minVals = autoNorm(datingDataMat) 5 m = normMat.shape[0] 6 numTestVecs = int(m*hoRatio) 7 errorCount = 0.0 8 for i in range(numTestVecs): 9 classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3) 10 print("the classifier came back with :%d,the real answer id:%d" %(classifierResult,datingLabels[i])) 11 if(classifierResult != datingLabels[i]): errorCount+=1.0 12 print("the total error rate is :%f" %(errorCount/float(numTestVecs)))
首先使用了file2matrix和autoNorm()函数从文件中读取数据并将其转换为归一化特征值。接着计算测试向量的数量,此步决定了normMat向量中哪些数据用于测试,哪些数据用于分类器的训练样本;然后将这两部分数据输人到原始kNN分类器函数classify0。最后,函数计算错误率并输出结果。注意此处我们使用原始分类器,本章花费了大量的篇幅在讲解如何处理数据,如何将数据改造为分类器可以使用的特征值。
在Python 命令提本符下重新加载看NN 模块,并输人kNN.datingClassTest( ) , 执行分类器测试程序。
5.使用算法:构建完整可用系统
约会网站预测函数:
1 def classifyPerson(): 2 resultList = ['not at all','in small doses','in large doses'] 3 percentTats = float(input("percentage of time spent playing video game?")) 4 ffMiles = float(input("frequent flier miles earned per year?")) 5 iceCream = float(input("liters of ice cream consumed per year?")) 6 datingDataMat,datingLabels = file2matrix('datingTestSet2.txt') 7 normMat,ranges,minVals = autoNorm(datingDataMat) 8 inArr = array([ffMiles,percentTats,iceCream]) 9 classifierResult = classify0((inArr-minVals)/ranges,normMat,datingLabels,3) 10 print("You will probably like this person :",resultList[classifierResult - 1])
在Python命令提示符下输入kNN.classifyPerson()
示例二:手写识别系统
本节我们一步步地构造使用k-近邻分类器的手写识别系统。为了简单起见,这里构造的系统只能识别数字0到9 。需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小: 宽髙是32像 素 x32像素的黑白图像。尽管采用文本格式存储图像不能有效地利用内存空间,但是为了方便理解,我们还是将图像转换为文本格式。
1.准备数据:将图像转换为测试向量
目录trainingDigits中包含了大约2000个例子, 每个数字大约有200个样本;目录testDigits中包含了大约900个测试数据。我们使用目录trainingDigits中的数据训练分类器,使用目录testDigits中的数据测试分类器的效果。
为了使用前面两个例子的分类器,我们必须将图像格式化处理为一个向量。我们将把一个32x32的二进制图像矩阵转换为1 x 1024的向量,这样前两节使用的分类器就可以处理数字图像信息了。
首先编写一段函数img2vector,将图像转换为向量:该函数创建1x 1024的Numpy 数组 ,然后打开给定的文件,循环读出文件的前32行 ,并将每行的头32个字符值存储在Numpy 数 组中,最后返回数组。
1 ##将图像转换为测试向量 2 def img2vector(filename): 3 returnVect = zeros((1,1024)) 4 fr = open(filename) 5 for i in range(32): 6 lineStr = fr.readline() 7 for j in range(32): 8 returnVect[0,32*i+j] = int(lineStr[j]) 9 return returnVect 10
在Python命令提示符下输入:testVector = kNN.img2vector('testDigits/0_13.txt')
2.测试算法:使用k-近邻算法识别手写数字
上 节 我 们 已 经 将 数 据 处 理 成 分 类 器 可 以 识 别 的 格 式 ,本 节 我 们 将 这 些 数 据 输 人 到 分 类 器 ,检测 分 类 器 的 执 行 效 果。 程 序 清 单 2-6所 示 的 自 包 含 函 数handwritingClassTest()是 测 试 分 类 器的 代 码 , 将 其 写入kNN.py 文 件 中 。 在 写 人 这 些 代 码 之 前 , 我 们 必 须 确 保 将from os import listdir 写 人 文 件 的 起 始 部 分 ,这 段 代 码 的 主 要 功 能 是 从os模 块 中 导 人 函 数listdir, 它 可 以 列出 给 定 目 录 的 文 件 名 。
手写数字识别系统的测试代码:
1 ##手写数字识别系统的测试代码 2 def handwritingClassTest(): 3 hwLabels = [] 4 trainingFileList = listdir('trainingDigits') # 获取目录内容 5 m = len(trainingFileList) 6 trainingMat = zeros((m,1024)) 7 for i in range(m): 8 fileNameStr = trainingFileList[i] #从文件名解析分类数字 9 fileStr = fileNameStr.split('.')[0] 10 classNumStr = int(fileStr.split('_')[0]) 11 hwLabels.append(classNumStr) 12 trainingMat[i,:] = img2vector('trainingDigits/%s' %fileNameStr) 13 testFileList = listdir('testDigits') 14 errorCount = 0.0 15 mTest = len(testFileList) 16 for i in range(mTest): 17 fileNameStr = testFileList[i] 18 fileStr = fileNameStr.split('.')[0] 19 classNumStr = int(fileStr.split('_')[0]) 20 vectorUnderTest = img2vector('testDigits/%s' %fileNameStr) 21 classifierResult = classify0(vectorUnderTest,trainingMat,hwLabels,7) 22 print("the classifier came back with :%d,the real answer is:%d" %(classifierResult,classNumStr)) 23 if(classifierResult != classNumStr):errorCount +=1.0 24 print("\n the totle number of errors is:%d" %errorCount) 25 print("\n the totle error rate is: %f" %(errorCount/float(mTest))) 26
将trainingDigits目录中的文件内容存储在列表中,然后可以得到目录中有多少文件,并将其存储在变量m中。接 着 ,代码创建一个m行 1024列的训练矩阵,该矩阵的每行数据存储一个图像。我们可以从文件名中解析出分类数字 。该目录下的文件按照规则命名, 如文件9_45加的分类是9,它是数字9的第45个实例。然后我们可以将类代码存储在hwLabels向量中,使用前面讨论的img2vextor函数载入图像。在下一步中, 我们对testDigits目_录中的文件执行相似的操 作,不同之处是我们并不将这个目录下的文件载人矩阵中,而是使用classify0()函数测试该目录下的每个文件。
在Python命令提示符中输入:kNN.handwritingClassTest()
k -近邻算法识别手写数字数据集, 错误率为1. 2%。改变变量k的值、 修改函数handwritingClassTest随机选取训练样本、改变训练样本的数目,都会对女-近邻算法的错误率产生影响,感兴趣的话可以改变这些变量值,观察错误率的变化。实际使用这个算法时,算法的执行效率并不高。因为算法需要为每个测试向量做2000次距离计 算 ,每个距离计算包括了1024个维度浮点运算,总计要执行900次 ,此外,我们还需要为测试向量准备2 M B 的存储空间。