机器学习之KNN算法原理及Python实现方法详解

yancey木易的blog 2018-07-09

本文实例讲述了机器学习之KNN算法原理及Python实现方法。分享给大家供大家参考,具体如下:

文中代码出自《机器学习实战》CH02,可参考本站:

机器学习实战 (Peter Harrington著) 中文版

机器学习实战 (Peter Harrington著) 英文原版 [附源代码]

KNN算法介绍

KNN是一种监督学习算法,通过计算新数据与训练数据特征值之间的距离,然后选取K(K>=1)个距离最近的邻居进行分类判(投票法)或者回归。若K=1,新数据被简单分配给其近邻的类。

KNN算法实现过程

(1)选择一种距离计算方式, 通过数据所有的特征计算新数据与已知类别数据集中的数据点的距离;

(2)按照距离递增次序进行排序,选取与当前距离最小的k个点;

(3)对于离散分类,返回k个点出现频率最多的类别作预测分类;对于回归则返回k个点的加权值作为预测值;

算法关键

(1)数据的所有特征都要做可比较的量化

若是数据特征中存在非数值的类型,必须采取手段将其量化为数值。例如样本特征中包含颜色,可通过将颜色转换为灰度值来实现距离计算。

(2)样本特征要做归一化处理

样本有多个参数,每一个参数都有自己的定义域和取值范围,他们对距离计算的影响不一样,如取值较大的影响力会盖过取值较小的参数。所以样本参数必须做一些scale处理,最简单的方式就是所有特征的数值都采取归一化处置。

(3)需要一个距离函数以计算两个样本之间的距离

距离的定义:欧氏距离、余弦距离、汉明距离、曼哈顿距离等,一般选欧氏距离作为距离度量,但是这是只适用于连续变量。在文本分类这种非连续变量情况下,汉明距离可以用来作为度量。通常情况下,如果运用一些特殊的算法来计算度量的话,K近邻分类精度可显著提高,如运用大边缘最近邻法或者近邻成分分析法。

(4)确定K的值

K值选的太大易引起欠拟合,太小容易过拟合。交叉验证确定K值。

KNN分类

分类算法常采用多数表决决定。一个缺点是出现频率较多的样本将会主导测试点的预测结果。解决这个缺点的方法之一是在进行分类时将K个邻居到测试点的距离考虑进去。若样本到测试点距离d,则选1/d为该邻居的权重,统计k个邻居所有类标签的权重和,值最大的就是新数据点的预测类标签。

KNN回归

KNN回归是取K个邻居类标签值得加权作为新数据点的预测值。

优缺点

(1)KNN算法的优点

  • 1.简单、有效。
  • 2.重新训练的代价较低(类别体系的变化和训练集的变化,在Web环境和电子商务应用中是很常见的)。
  • 3.计算时间和空间线性于训练集的规模(在一些场合不算太大)。
  • 4.由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。
  • 5.该算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误分。

(2)KNN算法缺点

  • 1.KNN算法是懒散学习方法(lazy learning,基本上不学习),一些积极学习的算法要快很多。
  • 2.类别评分不是规格化的(不像概率评分)(???)。
  • 3.输出的可解释性不强,例如决策树的可解释性较强。
  • 4.该算法在分类时有个主要的不足是,当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。该算法只计算最近的邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论怎样,数量并不能影响运行结果。可以采用权值的方法(和该样本距离小的邻居权值大)来改进。
  • 5.计算量较大。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本。

KNN实现

import numpy as np
import operator
import matplotlib
import matplotlib.pyplot as plt
from os import listdir
def Create_DataSet():
 group = np.array([[1.0, 1.1],[1.0,1.0],[0,0],[0,0.1]])
 labels = ['A','A','B','B']
 return group,labels

函数Create_DataSet创建一个数据集,坐标轴左下角分类为B,右上角分类为A。

下面函数classify0,计算向量inX与数据集中各点的距离,计算n_estimators个近邻中label频率最高的分类号并返回作为向量inX的分类号。

def classify0(inX, dataSet, labels, n_estimators=3):
 dataSetSize = dataSet.shape[0]
 #print 'in classify0,dataSetSize = \n',dataSetSize
 #转变向量inx格式为datasize行,1列;并计算与dataset元素距离
 diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
 #计算欧氏距离((x0-x1)^2 + (y0-y1)^2 )^(1/2)
 sqDiffMat = diffMat**2 #diffMat每个元素取平方
 sqDistances = sqDiffMat.sum(axis=1)
 distances = sqDistances**0.5
 #排序,将值从小到大排列,返回索引
 sortedDistIndicies = distances.argsort()
 #print 'in classify0,sortedDistIndicies:\n',sortedDistIndicies
 #求与距离最近的k个点的label统计
 classCount={}
 for i in range(n_estimators):
  voteIlabel = labels[sortedDistIndicies[i]] #获取对应label号
  classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
 #对字典排序,按value值降序排列
 sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
 #print 'sortedClassCount[0][0]:\n',sortedClassCount[0][0]
 return sortedClassCount[0][0]

dataSet.shape()函数用于获取矩阵dataSet的大小,shape[0]返回对应行数,shape[1]返回对应列数。

因为需要对每列属性做距离运算,所以需要将输入inX转换为和dataSet相同行数和列数的矩阵,因为inX列数与dataSet中每个元素列数相同,所以需要将其行数进行扩展,np.tile(inX, (dataSetSize,1))将inX行数拓展为dataSetSize行,1表示纵向复制1次,即列不变。

距离公式采用欧式距离计算,得到的距离值为一维列表,分别对应dataSet中每个元素和inX的距离。distances.argsort() 将距离按从小到大排列,并返回索引。例如distance = [0.1,0.5,0.3],distance.argsort()返回[1,3,2] 。返回索引是为了找到对应的label值,并进行统计。

下面for循环用于建立字典并统计前n_estimators个label的个数。key对应label,key_value对应个数。

operator.itemgetter函数,operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为一些序号,即需要获取的数据在对象中的序号;例如a = [1,2,3] ,定义函数b=operator.itemgetter(1),获取对象的第1个域的值,则 b(a)=2;若定义函数b,获取对象的第1个域和第0个的值b=operator.itemgetter(1,0),则b(a)=(2, 1) 。注意operator.itemgetter函数获取的不是值,而是定义了一个函数,通过该函数作用到对象上才能获取值;

sorted函数:Python内置的排序函数sorted可以对list或者iterator进行排序;第一个参数iterable指定要排序的list或者iterable,第二个参数指定排序时进行比较的函数,可以指定一个函数或者lambda函数。例如students为类对象的list,每个成员有三个域,用sorted进行比较时可以自己定cmp函数,例如这里要通过比较第三个数据成员来排序,students = [(‘john', ‘A', 15), (‘jane', ‘B', 12), (‘dave', ‘B', 10)],sorted(students, key=lambda student : student[2]),key为函数,指定取待排序元素的哪一项进行排序,key指定的lambda函数功能是去元素student的第三个域(student[2]),因此sorted排序时会以students所有元素的第三个域来进行排序;也可以这么写sorted(students, key=operator.itemgetter(2)) ,sorted函数也可以进行多级排序,例如要根据第二个域和第三个域进行排序;sorted(students, key=operator.itemgetter(1,2))即先跟句第二个域排序,再根据第三个域排序;第三个参数reverse是一个bool变量,表示升序还是降序排列,默认为false升序排列,定义为True时将按降序排列。

此处sort函数用于对字典进行排序。按key_value降序排列,即对应label个数从大到小排列。返回值为列表,列表元素为元组,元组第一个元素对应label,第二个元素对应label个数。sortedClassCount[0][0]即返回label次数最多的类标号,为inX的label。

下面测试一个简单的向量:

group,labels = Create_DataSet()
sortedClassCount = classify0([0,0.5],group,labels,3)

输出为

sortedClassCount:[(‘B', 2), (‘A', 1)]
sortedClassCount[0][0]:
B

下面函数file2matrix用于从txt中读取原始数据并转化为矩阵。

test.txt格式为

40920 8.326976 0.953952 largeDoses
14488 7.153469 1.673904 smallDoses
26052 1.441871 0.805124 didntLike
75136 13.147394 0.428964 didntLike
……

最后一列为label,值为largeDoses、smallDoses或didntLike。每行元素用\t隔开。转换后label分别对应3、2、1。

转换函数如下:

def file2matrix(filename):
 fr = open(filename)
 numberOfLines = len(fr.readlines())
 print 'in file2matrix,numberOfLines:\n',numberOfLines
 returnMat = np.zeros((numberOfLines,3))
 classLabelVector = []
 fr = open(filename)
 index = 0
 for line in fr.readlines(): #遍历每一行
  line = line.strip() #strip用于删除字符,此处删除空白字符,回车
  listFromLine = line.split('\t') #获取每行的元素列表,元素用\t分开
  returnMat[index,:] = listFromLine[0:3]#取前3个元素,对应属性集
  if(listFromLine[-1] == 'largeDoses'):#有什么有效的方法 将属性值和类标号分开,相互对应
   classLabelVector.append(3)
  elif(listFromLine[-1] == 'smallDoses'):
   classLabelVector.append(2)
  elif(listFromLine[-1] == 'didntLike'):
   classLabelVector.append(1)
  elif(listFromLine[-1] == 3):
   classLabelVector.append(3)
  elif(listFromLine[-1] == 2):
   classLabelVector.append(2)
  elif(listFromLine[-1] == 1):
   classLabelVector.append(1)
  index += 1
 #print 'returnMat = ',returnMat
 #print 'classLabelVector = ',classLabelVector
 return returnMat,classLabelVector #得到属性集和类标号

首先打开文件并获取行数,建立一个相同大小的空矩阵,用于存储转换后的属性集,并新建一个一维列表,用于存放类标号。fr.readlines()读取所有行,得到一个行列表,列表元素为每行内容;readline只读取1行,获取该行元素的列表。
上述函数即返回属性集矩阵和类标号列表。

因为属性值差距较大,为了减少值太大的属性对值小的属性的影响,分类之前还需要进行归一化。归一化方程为(datain-min_val) / (max_val - min_val),输出值都介于0-1。

def autoNorm(dataSet):
 minVals = dataSet.min(0) #获取每列最大值与最小值,(0)指定列,而不是行
 print 'in autoNorm,minVals:',minVals
 maxVals = dataSet.max(0)
 print 'in autoNorm,maxVals:',maxVals
 ranges = maxVals - minVals
 print 'in autoNorm,ranges:',ranges
 normDataSet = np.zeros(np.shape(dataSet))
 m = dataSet.shape[0] #获取行数
 #特征值矩阵为1000x3,minVals值为1x3,使用tile函数扩展为相同大小的矩阵
 #np.tile(minVals, (m,1))矩阵minval,横向复制m次,纵向复制1次
 normDataSet = dataSet - np.tile(minVals, (m,1)) # (data - minval)/(maxval - minval)
 normDataSet = normDataSet/np.tile(ranges, (m,1)) #element wise divide
 print 'in autoNorm,normDataSet = ',normDataSet
 return normDataSet, ranges, minVals

返回归一化以后的属性集。即可进行距离运算并分类。

下面函数即对文件中所有输入的行向量属性进行分类

def datingClassTest(n_estimators=3):
 hoRatio = 0.50
 #(1)读取文件
 datingDataMat,datingLabels = file2matrix('datingTestSet.txt')
 #(2)归一化
 normMat, ranges, minVals = autoNorm(datingDataMat)
 m = normMat.shape[0]
 numTestVecs = int(m*hoRatio)
 errorCount = 0.0
 for i in range(numTestVecs):
  classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],n_estimators=n_estimators)
  if (classifierResult != datingLabels[i]): errorCount += 1.0
 print "in datingClassTest,the total error rate is: %f" % (errorCount/float(numTestVecs))
 print 'in datingClassTest,errorCount:',errorCount

将测试文件分为数据集和用于测试的向量2部分。前一半用于测试,后一半作为数据集,并定义errorCount用于统计出错个数。经过归一化以后的数据集和验证通过for循环计算分类结果,并与实际结果进行对比,得到总出错数和出错率。

执行该函数,结果显示:

in datingClassTest,the total error rate is: 0.064000
in datingClassTest,errorCount: 32.0

更多关于Python相关内容感兴趣的读者可查看本站专题:《Python数学运算技巧总结》、《Python数据结构与算法教程》、《Python函数使用技巧总结》、《Python字符串操作技巧汇总》、《Python入门与进阶经典教程》及《Python文件与目录操作技巧汇总》

希望本文所述对大家Python程序设计有所帮助。

相关推荐