【机器学习实践(2)】K近邻(KNN)模型
根据machine learing in action 第二章改编
machine learing in action 是一本介绍机器学习实例的书,书中大量使用了scipy系列库,像matlab一样使用python。对我们学习python科学计算和理解机器学习都有很大的帮助。
本文根据其第二章内容改编,原作代码有一些问题,这里的代码都是作者重新写的。
转载请注明出处: 本文来自数据火花 http://blog.csdn.net/dataspark
1. 理论基础
K近邻模型,也常被叫做K最邻近结点算法。它是直接拿已经标注的数据做模型的一种统计学习方法(即,不需要额外的训练过程)。
对于待分类的样本点,在已经标注的数据集合中,找到与目标样本点最近的K个(K通常小于20)点,用K个点的标注类别来投票,得票最多的标注类别即作为目标点的分类结果。
2. 用ptyhon实现简单的KNN
2.1待处理数据及其格式
2.2 python实现
需要引入的库:
from numpy import * import numpy import operator
2.2.1 文件读取
定义函数,读取数据文件:
def read_data(fileName, dim): f = open(fileName, 'r') originalData = f.readlines() f.close() rows = len(originalData) index = 0 labelVec = [] multiArray = zeros((rows, dim)) for line in originalData: line = line.strip() lineElems = line.split('\t') multiArray[index, :] = lineElems[0:dim] labelVec.append(int(lineElems[-1])) #without 'int', appended is str index += 1 return multiArray, labelVec
注释1.1:第13行需要注意,必须加上int强制类型转换。由于一行各元素是用字符串切割的方法得到的,元素本身是字符串形式的。
注释1.2:其中函数 zeros((m,n))生成 m*n大小的数组,数组元素全为0;相似的有ones((m,n))
注意函数参数是元组,zeros(m,n)是错误的。
注释1.3:多维数组的元素操作,多维数组的元素操作:multiArray[index, :] ,取第index行的元素(该元素仍是个数组)。
NumPy也允许你使用“点”像b[i,...]。 点(…)代表许多产生一个完整的索引元组必要的分号。如果x是秩为5的数组(即它有5个轴),那么: x[1,2,…] 等同于 x[1,2,:,:,:], x[…,3] 等同于 x[:,:,:,:,3] x[4,…,5,:] 等同 x[4,:,:,5,:]
2.2.2 分类模型
定义KNN分类函数:
参数inX 表示输入的待分类样本点特征, dataSet是已经标注好的样本点特征集合, labelVec是标注的类别标签, k表示取多少个近邻。
def knn_classify(inX, dataSet, labelVec, k): sampleCnt = dataSet.shape[0] expandX = tile(inX, (sampleCnt, 1)) diffArray = expandX - dataSet sqArray = diffArray ** 2 sumArray = sqArray.sum(axis = 1) distArray = sumArray ** 0.5 sortedIndices = distArray.argsort() labelCnt = {} for i in range(k): label = labelVec[sortedIndices[i]] labelCnt[label] = labelCnt.get(label, 0) + 1 sortedLabelCnt = sorted(labelCnt.iteritems(), key = operator.itemgetter(1), reverse=True) return sortedLabelCnt[0][0]
注2.1:tile函数的作用是对数组进行扩展,tile(a, (x,y)),将a横向复制y次,再纵向复制x次,使原数组的元素个数扩大到x*y倍
注2.2:array的shape成员变量,值是tuple,反应了数组的结构(各维的大小)
注2.3:sqArray.sum(), 不指定参数时,以每一个元素为单元,计算总和;指定axis,当axis=0时,按竖方向(以行为单元)求和;axis=1时,按横方向(以列为单元)求和。
相似的有 array.min(), array.max()函数
注2.4:array.argsort(),对数组本身不做排序,排序的结果是数组下标的集合
注2.5:sorted()函数对字典进行排序,必要的参数是字典的迭代项 dict.iteritems(), key指定排序的依据。
这个函数是典型的科学计算思维,有些地方和面向对象思维相悖:比如,样本点的特征和标注类别,没有放在一个类里,而是分成一个array和list,靠下标联系。这样做的主要原因是:为了更好的应用python科学计算中的数组操作。
当有一个目标样本点inX输入时,调用该函数可以得到结果。
dataSet, labelVec = read_data(dataFile, dim) l = knn_classify((0.3,0.4,0.5), dataSet, labelVec, 5)
2.2.3 效果检验
交叉验证KNN模型在测试集(测试集下载地址)上的效果:
def cross_validate(crossTimes, dataFile, dim, k): dataSet, labelVec = read_data(dataFile, dim) dataSet, mins, ranges = autoNorm(dataSet) sampleCnt = dataSet.shape[0] testSetCnt = sampleCnt / crossTimes errorCnt = 0 for i in range(crossTimes): testSet = dataSet[i * testSetCnt : i * testSetCnt + testSetCnt] trainSet = zeros((sampleCnt - testSetCnt, dim)) testLabelVec = labelVec[i * testSetCnt : i * testSetCnt + testSetCnt] trainLabelVec = labelVec[:i * testSetCnt] + labelVec[i * testSetCnt + testSetCnt:] trainSet[0: i * testSetCnt, :] = dataSet[0: i * testSetCnt, :] trainSet[i * testSetCnt:] = dataSet[i * testSetCnt + testSetCnt:] t = array(list(dataSet[0: i * testSetCnt, :]) + list(dataSet[i * testSetCnt + testSetCnt:])) print t == trainSet for j in range(testSetCnt): l = knn_classify(testSet[j], trainSet, trainLabelVec, k) if l != testLabelVec[j]: print "estimated:%d, real:%d" % (l, testLabelVec[j]) errorCnt += 1 print "precision:%f" % ((sampleCnt - errorCnt * 1.0) / sampleCnt)
注3.1:上面的函数中, trainSet是由dataSet分割后再组合而成,将两个数组组合到一起,可以通过强制转换array为list,再回转成array来做:
def mergeArray(array1, array2): merged = array(list(array1) + list(array2)) return merged
用以下方式调用交叉验证函数时(9次交叉验证,K取值5):
cross_validate(9, r"knn_data.txt", 3, 5)
发现准确率只有 0.785000
2.2.4 效果改进
观察数据发现,各维特征的大小很大均衡,归一化能有效减小各维尺度差异带来的问题:
def autoNorm(dataSet): minVals = dataSet.min(0) maxVals = dataSet.max(0) ranges = maxVals - minVals sampleCnt = dataSet.shape[0] expandMinus = tile(minVals, (sampleCnt, 1)) expandDivider = tile(ranges, (sampleCnt, 1)) normDataSet = (dataSet - expandMinus) / expandDivider return normDataSet, minVals, ranges
在交叉验证函数中,加入归一化:
def cross_validate(crossTimes, dataFile, dim, k): dataSet, labelVec = read_data(dataFile, dim) dataSet, mins, ranges = autoNorm(dataSet) sampleCnt = dataSet.shape[0] testSetCnt = sampleCnt / crossTimes errorCnt = 0 for i in range(crossTimes): testSet = dataSet[i * testSetCnt : i * testSetCnt + testSetCnt] trainSet = zeros((sampleCnt - testSetCnt, dim)) testLabelVec = labelVec[i * testSetCnt : i * testSetCnt + testSetCnt] trainLabelVec = labelVec[:i * testSetCnt] + labelVec[i * testSetCnt + testSetCnt:] trainSet[0: i * testSetCnt, :] = dataSet[0: i * testSetCnt, :] trainSet[i * testSetCnt:] = dataSet[i * testSetCnt + testSetCnt:] t = array(list(dataSet[0: i * testSetCnt, :]) + list(dataSet[i * testSetCnt + testSetCnt:])) print t == trainSet for j in range(testSetCnt): l = knn_classify(testSet[j], trainSet, trainLabelVec, k) if l != testLabelVec[j]: print "estimated:%d, real:%d" % (l, testLabelVec[j]) errorCnt += 1 print "precision:%f" % ((sampleCnt - errorCnt * 1.0) / sampleCnt)
结果准确率达到了:
precision:0.952000