K-近邻算法—基本原理与实战
概述
k-近邻算法(k-Nearest Neighbor, KNN),是一个理论上比较成熟的方法,也是最简单的机器学习算法之一,用于预测数据的类别,以及对数据进行分类。该方法的简要思路就是采用测量不同特征值之间的距离来进行分类。
特点
工作原理
存在一个样本数据集合,也称作训练样本集,并且样本集中的每个数据都存在标签,即我们知道样本集中每一个数据与所属分类的对应关系。当输入一个没有包含标签(也就是对应的类别)的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后用算法提取样本集中特征与新数据最相似的数据(最近邻)的分类标签。一般来说,只取样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数。最后选择k个最相似数据中出现次数最多的分类,作为新数据的分类(标签)。
使用欧式距离公式,计算两个向量点xA和xB的距离:
kNN算法的一般流程
(1)收集数据:可以使用任何方法。
(2)准备数据:距离计算所需要的数值,最好是结构化的数据格式。
(3)分析数据:可以使用任何方法。
(4)训练算法:k-近邻算法不需要训练算法。
(5)测试算法:计算错误率。
(6)使用算法:首先需要输入样本数据和结构化的输出结果,然后运行k-近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续的处理。
实施kNN分类算法
#kNN算法的伪代码
#功能:使用k-近邻算法将数据划分到某个类中
#对未知类别属性的数据集中的每个点依次执行以下操作:
(1)计算已知类别数据集中的点与当前点之间的距离;
(2)按照距离递增次序对数据集中的点进行排序;
(3)选取与当前点距离最小的k个点;
(4)确定前k个点所在类别的出现频率;
(5)返回前k个点出现频率最高的类别作为当前点的预测分类;
简单分类示例
from numpy import *
import operator
#createDataSet():产生样本集数据和样本集中每个数据的类型信息
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
函数createDataSet()初始化了四个样本数据,每个样本数据含有两个特征值,并且还初始化了标签信息,其中第一个数据的标签是A,第二个是A,第三个是B,第四个是B。产生的样本数据用于对新数据进行分类预测。
#classify0:k-近邻算法的基本实现,返回数据的类别预测信息
#inX:用于分类的输入向量
#dataSet:训练样本集
#labels:训练样本集对应的标签向量
#k:选择最近邻居的数目
from numpy import *
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0] #获取样本数据的行数
diffMat = tile(inX, (dataSetSize, 1)) - dataSet #将输入向量扩展到和样本数据同样大小,然后与样本数据相减
sqDiffMat = diffMat ** 2; #对相减后的数组平方(每个元素平方);如果是矩阵,表示矩阵平方(矩阵乘法)
sqDistances = sqDiffMat.sum(axis=1) #axis=0表示将矩阵或数组的每个元素相加;
#axis=1表示将矩阵或数组的每行的每个元素相加,相加的结果(数量和行数相同)组成一个一维的数组
distances = sqDistances ** 0.5 #对数组中的每个元素求平方根
sortedDistIndicies = distances.argsort() #对distances数组中的元素进行排序,返回从小到大的索引值
classCount = {} #创建一个空字典
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]] #根据距离从小到大或取样本数据的类别
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
#将类别作为key,出现次数作为value,类别每出现一次,key值加1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
#根据字典中value值的大小进行排序,默认从小到大,添加reverse=True表示从大到小
return sortedClassCount[0][0] #返回出现次数最高的类别
运行classify0(),传入上面生成的数据,返回B。
分类器的测试
为了检验分类器给出的答案是否符合预期,就需要对分类器进行测试。通过大量的测试数据,我们可以得到分类器的错误率——分类器给出错误结果的次数/测试执行总次数。完美的分类器错误率为0,最差的分类器错误率为1.0。
归一化特征值
对于大多数数据来说,其每个特征的特征值可能数值差距很大。例如某个人,身高属性1.7m,而体重属性70KG,这两个属性数值差距比较大,如果直接用欧拉距离公式计算,体重对分类结果的影响会大大提升,而真正意愿则是体重和身高对分类结果的影响相同,因此需要对这些数据进行归一化。
使用下列公式可以将任意取值范围的特征值转化为0到1区间内的值:
其中oldValue是需要进行归一化的特征值,min是所有同类特征值最小的特征值,max是所有同类特征值最大的特征值。
代码示例:
def autoNorm(dataSet):
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = zeros(shape(dataSet)) #创建一个和dataSet大小相同的数组
m = dataSet.shape[0] #dataSet的行数
normDataSet = dataSet - tile(minVals, (m, 1)) #minVals扩展:行*m,列*n
normDataSet = normDataSet/tile(ranges, (m, 1)) #特征值相除
return normDataSet, ranges, minVals
示例一:使用kNN算法改进约会网站的配对效果
这里使用的示例都来自书籍《机器学习实战》,源码下载
(1)收集数据:提供文本文件。
(2)准备数据:使用Python解析文本文件。
(3)分析数据:使用Matplotlib画二维扩散图。
(4)训练算法:此步骤不适用于k-近邻算法。
(5)测试算法:使用训练数据中10%的数据进行测试。
(6)使用算法:产生简单的命令行程序,然后输入特征值进行判断。
准备数据:将文本记录转换成NumPy的解析程序:
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines)
#获取列数
cols = len(arrayOLines[0].split('\t'))
returnMat = zeros((numberOfLines, cols - 1))
classLabelVector = []
index = 0
for line in arrayOLines:
line = line.strip()
listFromLine = line.split('\t')
returnMat[index, :] = listFromLine[0:cols-1]
classLabelVector.append(int(listFromLine[-1])) #特征值为int型
index += 1
return returnMat, classLabelVector
分析数据:使用Matplotlib创建散点图
import matplotlib
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2])
plt.show()
准备数据:归一化数值
采用上述方法进行归一化
测试算法:作为完整程序验证分类器
#测试分类器的效果
def datingClassTest():
hoRatio = 0.10
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
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], 10)
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))
使用算法:构建完整可用系统
def classifyPerson():
resultList = ['not at all', 'in small doses', 'in large doses']
percentTats = float(input("percentage of time spent palying video games?"))
ffMiles = float(input("frequent flier miles earned per year?"))
iceCream = float(input("liters of ice cream consumed per year?"))
datingDataMat, datingLables = file2matrix('datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = array([ffMiles, percentTats, iceCream])
classifierResult = classify0((inArr - minVals)/ranges, normMat, datingLables, 3)
print("You will like the person: ", resultList[classifierResult - 1])
示例二:手写识别系统
原理:将图像经过图形处理软件转为为二进制的形式,然后将二进制图片转化为一个向量,每一位二进制都是向量的一个元素,将大量向量再加上对应的类别作为训练数据集。使用算法时,使用没有类别的向量,计算其与训练数据集向量的距离,通过kNN算法预测出类别信息。
准备数据:将图像(已经是二进制格式)转换为测试向量
def img2vector(filename):
returnVect = zeros((1, 1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0, 32*i + j] = int(lineStr[j])
return returnVect
测试算法:使用k-近邻算法识别手写数字
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir('trainingDigits') #listdir需要使用 from os import listdir
m = len(trainingFileList)
trainingMat = zeros((m, 1024))
for i in range(m):
fileNameStr = trainingFileList[i] #每个文件的文件名,如:0_1.txt
fileStr = fileNameStr.split('.')[0] #获取除后缀的文件名,如:0_1
classNumStr = int(fileStr.split('_')[0]) #获取文件的标签,如0_1表示数字0的第一个训练数据,则该数据的的标签是1
hwLabels.append(classNumStr)
trainingMat[i, :] = img2vector('trainingDigits/%s' % fileNameStr)
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)
print("The classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr))
if classifierResult != classNumStr:
errorCount += 1.0;
print("The total number of errors is: %d" % errorCount)
print("The total error rate is: %f" % (errorCount / float(mTest)))