第二章 K-近邻算法
- K-近邻算法
- 从文本文件中解析和导入数据
- 使用Matplotlib创建扩展图
- 归一化数值
2.1 k-近邻算法概述
工作原理:
存在一个样本数据集合,并且样本集中每个数据都存在标签(即目标变量,哪个类别)。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,取样本数据集中前k个最相似的数据,最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
三个基本要素:
k-值的选择、距离度量(计算相似的方法)、分类决策规则。
例子:
使用k-近邻算法分类爱情片和动作片。
首先我们如何区分爱情片和动作片,需知道其分别具有哪些特征。直观的感觉动作片打斗场景较多,但也有少量接吻镜头,而爱情片接吻镜头可能比较多,而打斗场景少些,在一部电影中直接以有无这些场景作为特征,显然用来区分两种电影的信息量有点少,难以区分。将打斗场景和接吻镜头分别在两部电影中出现的次数作为特征,信息量稍微多些,容易区分两类电影。
其次,数据说话。首先得有统计了两类电影的打斗场景和接吻镜头的次数,及其电影类型。
未知电影名称具有18次打斗场景,90次接吻镜头,我们将其分为哪类呢?首先需要计算其与样本集中其他电影的距离(此处便是距离度量,可以用欧式距离,曼哈顿距离,cos相似度计算等等)。
按距离递增排序,定K=3,我们知道前3个电影都是爱情片,故将为之电影分为爱情片。
使用python进行k-近邻分类的小例子:
创建模块kNN.py:
kNN.py模块中,导入包numpy和operator,并创建函数createDataSet(),其有两个返回值,分别是特征值和标签值(目标变量)。#!/usr/bin/env python # coding=utf-8 from numpy import * import operator def createDataSet(): group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]]) labels = ['A','A','B','B'] return group, labels
Figure1: group, labels变量
k-近邻算法为代码:
k-近邻算法代码:对未知类别属性的数据集中的每个点依次执行以下操作:
- 计算已知类别数据集中的点与当前点之间的距离;
- 按照距离递增排序;
- 选取与当前点距离最小的k个点;
- 确定前k个点所在类别的出现频率;
- 返回前k个点出现频率最高的类别作为当前点的预测分类;
#!/usr/bin/env python # coding=utf-8 from numpy import * import operator def createDataSet(): group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]]) labels = ['A','A','B','B'] return group, labels def classify0(inX, dataSet, labels, k): #===========计算距离============ dataSetSize = dataSet.shape[0] #shape函数返回array类型的各个维数上的数的个数。对2维来说,即返回行数和列数。 diffMat = tile(inX, (dataSetSize, 1)) - dataSet #tile()函数如同repeat功能,dataSetSize此处为4,将输入向量inX复制4×1份 sqDiffMat = diffMat**2 sqDistances = sqDiffMat.sum(axis=1) distances = sqDistances**0.5 sortedDistIndicies = distances.argsort()#argsort()为numpy包下的函数,返回的是数组值从小到大的索引值 #===========选择距离最小的k个点== classCount = {} for i in range(k): voteIlabel = labels[sortedDistIndicies[i]] classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 #先用get()函数将label I在字典里默认值为0,若无直接+1,也即label I的投票+1。 sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse=True) #按照字典value排序 return sortedClassCount[0][0] #排序完了之后返回一个二维列表,[0][0]代表排序最高的那个类别 #if __name__=="__main__": # group,labels = createDataSet() # target = classify0([0,0],group,labels,3) # print target
Figure2: kNN结果classify0()函数有4个参数:inX为待分类的输入向量,dataSet为训练样本,labels为训练样本的标签向量,k表示最近邻居的数目。
classify0()函数使用欧式距离公式,计算两个向量点xA和xB之间的距离:
如何测试分类器:
需要引入错误率这个概念:分类器给出错误结果的次数除以测试执行的总数。错误率是常用的评估方法,主要用于评估分类器在某个数据集上的执行效果。
2.2 示例: 使用k-近邻算法改进约会网站的配对效果
在约会网站上使用k-近邻算法:
- 收集数据:提供文本。
- 准备数据:使用python解析文本文件。
- 分析数据:使用Matplotlib画二维扩散图。
- 训练算法:此步驟不适用于k-近邻算法。
- 测试算法:使用海伦提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
- 使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。
准备数据:
数据放在文本文件datingTestSet.txt中,共1000行,被"\t"隔开为4列,有3种特征:
- 每年获得的飞行常客里程数。
- 玩视频游戏所耗时间百分比
- 每周消费的冰淇淋公升数。
Figure2-2-1: datingTestSet.txt前10行示例
需要将数据转换为后续方便处理的格式,如特征用一个变量表示,目标变量用另一个变量表示,并将枚举类型的目标类别用数字代替。在kNN.py中创建函数,读入数据,输出训练样本和类别标签向量。
#!/usr/bin/env python # coding=utf-8 import codecs from numpy import * def file2matrix(filename): with codecs.open(filename) as f: arrayOLines = f.readlines() numberOfLines = len(arrayOLines) returnMat = zeros((numberOfLines, 3)) classLabelVector= [] index = 0 like_type = {} like_type['largeDoses'] = 3 like_type['smallDoses'] = 2 like_type['didntLike'] = 1 for line in arrayOLines: line = line.strip() line = line.strip("\n") listFromLine = line.split("\t") returnMat[index, :] = listFromLine[0:3] if listFromLine[-1] in like_type: classLabelVector.append(int(like_type[listFromLine[-1]])) else: print "like_type error",listFromLine[-1] classLabelVector.append(0) index+=1 return returnMat, classLabelVector
Figure2-2-2: 准备数据
分析数据:
使用python工具包Matplotlib创建散点图。
Figure2-2-3: 使用第一列和第二列的属性值画图
Figure2-2-4:使用第一列和第二列的属性值的散点图
Figure2-2-5:使用第二列和第三列的属性值画图
Figure2-2-5:使用第二列和第三列的属性值散点图
准备数据:归一化处理
直接对数据使用欧式距离计算,则会使得高数量级的特征对目标变量影响权重大,但实际上,并非真正如此,对其进行归一化到[0,1)之间,认为其同等重要,故需要对数据进行归一化。如数据如下:
Figure2-2-6: 示例数据
公式:newValue = (oldValue - min)/(max - min)
增加autoNorm()函数:
def autoNorm(dataSet): minVals = dataSet.min(0) #返回每列的最小值,[0, 400, 0.1] maxVals = dataSet.max(0) #返回每列的最大值,[67.134000,1.1] ranges = maxVals - minVals normDataSet = zeros(shape(dataSet)) #dataSet所有元素都变为0,即同dataSet数组一样大小的全零矩阵 m = dataSet.shape[0] #dataSet行数 normDataSet = dataSet - tile(minVals,(m,1)) #tile(minVals,(m,1)):将minVals复制为m*1份 normDataSet = normDataSet/(tile(ranges,(m,1)))#在numpy包中,矩阵除法需要使用函数linalg.solve(matA,matB) return normDataSet, ranges, minVals
Figure2-2-7: 归一化
测试算法:
使用错误率来检验我们的分类器的性能。对于k-近邻算法,我们的主要参数便是三要素:k的选择、距离度量的标准、分类决策的规则。对于已有的数据,将90%作为训练,剩下作为测试。设定k=3,对数据进行归一化各个特征权重一样大使用欧式距离计算,没有其他的变化这样便能够确定错误率,调整训练的数据,调整k值,不使用欧式距离计算相似度,对不同特征采取不同全值等等,都可以使得最终的错误率不一样,为此需要多次实验以得到最好的一组参数使得错误率最小。在kNN.py创建datingClassTest()函数:
def datingClassTest(): hoRatio = 0.10 datingDataMat, datingLabels = file2matrix("datingTestSet.txt") normMat,ranges,minVals = autoNorm(datingDataMat) m = normMat.shape[0] numTestVecs = int(m*hoRatio) errorCount = 0.0 for i in range(numTestVecs): #对前10%行的数据处理 classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m],3) #前10%行的数据作为测试集,并且对测试集中的每一行都进行预测,对比测试集中实际的label #后90%行的数据全部作为训练集,每个测试集样本都要跟90%的训练集计算距离,算出最相似的label, print "the classifier came back with: %d, the real answer is: %d" %(classifierResult, datingLabels[i]) if classifierResult!=datingLabels[i]: errorCount+=1.0 print "the total error rate is: %f" % (errorCount/float(numTestVecs))
Figure2-2-8: 计算算法的错误率
使用算法:
当海伦在约会网上找到某个人并输入他的信息时,我们的程序会自动给出她对对方喜欢程度的预测值。在kNN.py创建classifyPerson()函数:
def classifyPerson(): resultList = ["not at all","in small doses","in large doses"] percentTats = float(raw_input("percentage of time spent playing video games? ")) ffMiles = float(raw_input("frequent flier miles earned per year? ")) iceCream = float(raw_input("liters of ice cream consumed per year? ")) inArr = array([ffMiles, percentTats, iceCream]) datingDataMat, datingLabels = file2matrix("datingTestSet.txt") normMat,ranges,minVals = autoNorm(datingDataMat) #需要对新来的测试集也做归一化,故需要用到ranges和minVals两个变量 classifierResult = classify0((inArr-minVals)/ranges, normMat, datingLabels, 3) print "You will probably like this person: ",resultList[classifierResult - 1]
Figure2-2-9: 预测
数据链接:
datingTestSet.txt
kNN.py
#!/usr/bin/env python # coding=utf-8 import codecs from numpy import * import operator def classify0(inX, dataSet, labels, k): #===========计算距离============ dataSetSize = dataSet.shape[0] #shape函数返回array类型的各个维数上的数的个数。对2维来说,即返回行数和列数。 diffMat = tile(inX, (dataSetSize, 1)) - dataSet #tile()函数如同repeat功能,dataSetSize此处为4,将输入向量inX复制4×1份 sqDiffMat = diffMat**2 sqDistances = sqDiffMat.sum(axis=1) distances = sqDistances**0.5 sortedDistIndicies = distances.argsort()#argsort()为numpy包下的函数,返回的是数组值从小到大的索引值 #===========选择距离最小的k个点== classCount = {} for i in range(k): voteIlabel = labels[sortedDistIndicies[i]] classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 #先用get()函数将label I在字典里默认值为0,若无直接+1,也即label I的投票+1。 sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse=True) #按照字典value排序 return sortedClassCount[0][0] #排序完了之后返回一个二维列表,[0][0]代表排序最高的那个类别 def file2matrix(filename): with codecs.open(filename) as f: arrayOLines = f.readlines() numberOfLines = len(arrayOLines) returnMat = zeros((numberOfLines, 3)) classLabelVector= [] index = 0 like_type = {} like_type['largeDoses'] = 3 like_type['smallDoses'] = 2 like_type['didntLike'] = 1 for line in arrayOLines: line = line.strip() line = line.strip("\n") listFromLine = line.split("\t") returnMat[index, :] = listFromLine[0:3] if listFromLine[-1] in like_type: classLabelVector.append(int(like_type[listFromLine[-1]])) else: print "like_type error",listFromLine[-1] classLabelVector.append(0) index+=1 return returnMat, classLabelVector #datingDataMat, datingLabels = kNN.file2matrix("datingTestSet.txt") def autoNorm(dataSet): minVals = dataSet.min(0) #返回每列的最小值,[0, 400, 0.1] maxVals = dataSet.max(0) #返回每列的最大值,[67.134000,1.1] ranges = maxVals - minVals normDataSet = zeros(shape(dataSet)) #dataSet所有元素都变为0,即同dataSet数组一样大小的全零矩阵 m = dataSet.shape[0] #dataSet行数 normDataSet = dataSet - tile(minVals,(m,1)) #tile(minVals,(m,1)):将minVals复制为m*1份 normDataSet = normDataSet/(tile(ranges,(m,1)))#在numpy包中,矩阵除法需要使用函数linalg.solve(matA,matB) return normDataSet, ranges, minVals #normMat,ranges,minVals = kNN.autoNorm(datingDataMat) def datingClassTest(): hoRatio = 0.10 datingDataMat, datingLabels = file2matrix("datingTestSet.txt") normMat,ranges,minVals = autoNorm(datingDataMat) m = normMat.shape[0] numTestVecs = int(m*hoRatio) errorCount = 0.0 for i in range(numTestVecs): #对前10%行的数据处理 classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m],3) #前10%行的数据作为测试集,并且对测试集中的每一行都进行预测,对比测试集中实际的label #后90%行的数据全部作为训练集,每个测试集样本都要跟90%的训练集计算距离,算出最相似的label, print "the classifier came back with: %d, the real answer is: %d" %(classifierResult, datingLabels[i]) if classifierResult!=datingLabels[i]: errorCount+=1.0 print "the total error rate is: %f" % (errorCount/float(numTestVecs)) #kNN.datingClassTest() def classifyPerson(): resultList = ["not at all","in small doses","in large doses"] percentTats = float(raw_input("percentage of time spent playing video games? ")) ffMiles = float(raw_input("frequent flier miles earned per year? ")) iceCream = float(raw_input("liters of ice cream consumed per year? ")) inArr = array([ffMiles, percentTats, iceCream]) datingDataMat, datingLabels = file2matrix("datingTestSet.txt") normMat,ranges,minVals = autoNorm(datingDataMat) #需要对新来的测试集也做归一化,故需要用到ranges和minVals两个变量 classifierResult = classify0((inArr-minVals)/ranges, normMat, datingLabels, 3) print "You will probably like this person: ",resultList[classifierResult - 1]
2.3示例: 手写识别系统
此处数据使用UCI机器学习资料库http://archive.ics.uci.edu/ml中”手写数字数据集的光学识别“的部分数据。
Figure2-3-1: 数据集
准备数据:将图像转换为测试向量
trainingDigits目录包含1934个例子,每个数字样本大概有200个。作为训练集。
testDigits目录包含946个例子。作为测试集。同训练集没有重叠。
Figure2-3-2: 数字0的手写数字的32×32维二进制数据
为使用数据,首先需要将32×32维的二进制数据转换为1×1024的向量,以便后续处理。
图像转为向量的函数img2vector():
from numpy import * def img2vector(filename): returnVect = zeros((1,1024))#(1,1024)元组作为zeros()函数的第一个参数,创建二维数组 fr = open(filename) for i in range(32): lineStr = fr.readline() for j in range(32): returnVect[0,32*i+j] = int(lineStr[j]) #因为一个0或者一个1作为一个字符串,共32个,直接取lineStr[j]即可 return returnVect
Figure2-3-3: img2vector()函数效果
测试算法:使用k-近邻算法识别手写数字
kNN.py:
# -*- coding: utf-8 -*- """ Created on Thu Sep 3 17:29:21 2015 @author: shifeng """ from numpy import * from os import listdir import operator import time def classify0(inX, dataSet, labels, k): #===========计算距离============ dataSetSize = dataSet.shape[0] #shape函数返回array类型的各个维数上的数的个数。对2维来说,即返回行数和列数。 diffMat = tile(inX, (dataSetSize, 1)) - dataSet #tile()函数如同repeat功能,dataSetSize此处为4,将输入向量inX复制4×1份 sqDiffMat = diffMat**2 sqDistances = sqDiffMat.sum(axis=1) distances = sqDistances**0.5 sortedDistIndicies = distances.argsort()#argsort()为numpy包下的函数,返回的是数组值从小到大的索引值 #===========选择距离最小的k个点== classCount = {} for i in range(k): voteIlabel = labels[sortedDistIndicies[i]] classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 #先用get()函数将label I在字典里默认值为0,若无直接+1,也即label I的投票+1。 sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse=True) #按照字典value排序 return sortedClassCount[0][0] #排序完了之后返回一个二维列表,[0][0]代表排序最高的那个类别 def img2vector(filename): returnVect = zeros((1,1024))#(1,1024)元组作为zeros()函数的第一个参数,创建二维数组 fr = open(filename) for i in range(32): lineStr = fr.readline() for j in range(32): returnVect[0,32*i+j] = int(lineStr[j]) #因为一个0或者一个1作为一个字符串,共32个,直接取lineStr[j]即可 return returnVect def handwritingClassTest(): start_time = time.time() hwLabels = [] #以往利用python os.walk()函数遍历某个文件夹下的文件,这里直接用listdir()更为方便,其返回以文件名为字符串的一个列表 trainingFileList = listdir("trainingDigits") m = len(trainingFileList) trainingMat = zeros((m,1024)) #同训练集一样大小的全0矩阵 for i in range(m): fileNameStr = trainingFileList[i] #某个文件的文件名,如0_0.txt fileStr = fileNameStr.split(".")[0] classNumStr = int(fileStr.split("_")[0]) hwLabels.append(classNumStr) #将某个类别存起来,以便和trainingMat对应起来,作为训练集的labels trainingMat[i, :] = img2vector("trainingDigits/%s" % fileNameStr) #调用函数img2vector每行放一个1每行放一个1×1024的向量 testFileList = listdir("testDigits") errorCount = 0.0 mTest = len(testFileList) for i in range(mTest): fileNameStr = testFileList[i] fileStr = fileNameStr.split(".")[0] classNumStr = int(fileStr.split("_")[0]) #测试集真实类别 vectorUnderTest = img2vector("testDigits/%s" % fileNameStr) classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3) if i <10: 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 errors is: %d" % errorCount print "\nthe total error rate is: %f" % (errorCount/float(mTest)) end_time = time.time() print "All time is ", end_time - start_time
Figure2-3-4: 测试算法
算法效率:
每个测试集样本计算1934次距离运算,每个距离运算1024个浮点运算,936个测试集样本,共计936×1934×1024次浮点运算。需2.1M空间存训练集。
2.4 k-近邻算法优缺点
听闻hr会问一些机器学习算法的优缺点及对比其他算法的优势等等。故除了熟悉具体算法流程,对其特点也需注意。当然,在熟悉其算法后,其特点特性也自然而然的熟悉了。此处也是书中提到的章节小节。
在数据量不是很大时,是作为最简单最有效的算法。
k-近邻算法是基于实例的学习,使用算法必须有接近实际数据的训练样本数。
缺点:
- k-近邻算法对每个测试集样本都使用了一次全部的训练集,第一若是训练集大,需要较大的存储空间,这一点倒不是什么问题,现在处理的数据基本上上G,主要是第二点,因为对每个测试集样本都需要使用一次全部的训练集得到最短的k个距离值,那么计算必然非常耗时。
- k-近邻算法无法给出任何数据的基础结构信息。无法知晓平均实例样本和典型实例样本具有怎样的特征。