@《机器学习实战》
@K-近邻算法
优点:精度高、对异常值不敏感、无数据输入假定
缺点:计算复杂度高、空间复杂度高
适用数据范围:数值型和标称型
存在一个样本数据集合(训练样本集),并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。
输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据的分类标签,即最近邻的分类标签。
一般来说,我们只选取样本数据集中前K个最相似的数据,通常K是不大于20的整数。
最后,选择K个最相似数据中出现次数最多的分类,作为数据的分类。
(1)收集数据:数据是数值型,并带有分类标签
(2)准备数据:通过python从文本文件中解析数据,并归一化特征值
(3)分析数据:使用matplotlib画二维散点图
(4)测试算法:作为对分类器的验证,计算它的错误率和错误个数
(5)使用算法:输入特征数值,来对未知类别的数据进行分类
(1)KNN分类算法的classify0函数有4个参数,其中的inX需要注意,它是输入向量,即待测试数据。它用到了tile函数,这个我在下文的代码解析会提到。
(2)标签向量labels的元素数目和矩阵dataset的行数相同
(3)读取文件数据,必须确保txt等文档存储在当前的工作目录
(4)创建散点图,需要确保环境有matplotlib(这点针对零基础的学习者)
(5)归一化特征值是很有必要的,因为这样能避免较大的特征数据对计算的影响,使得每个特征值对计算结果的影响是一样的。
(6)可以通过改变datingClassTest函数的hoRatio和K值,使得错误率发生变化,错误率是随着它们的变化而增加。
(7)书中“2-5约会网站预测函数”的raw_input函数是python2中的,python3环境会报错,直接换成input函数即可。
(8)同样,书中“2-1 k-近邻算法”,用到的iteritems也是python2中的,在python3环境下,应该换成items。
这一章节书中共有9个函数代码,下面我会逐个展示代码及注释,并做一些相关的解析。
这里,我将对classify0函数和createDataSet函数进行解析。
首先,createDataSet函数是一个很基础的函数,它用来创建数据集和标签,并能返回数据集和标签的值。
而classify0函数,则是这章的精髓。它的功能是将每组数据划分到某个类中。
函数包含4个参数,其中inX是用于分类的输入向量,dataSet是输入的训练样本集,labels是标签向量,K是用于选择最近邻居的数目。
而函数的大体思路可以概括为5个步骤:
(1)计算已知数据集的点与当前点之间的距离
(2)按照距离递增次序排序
(3)选取与当前点距离最小的K个点
(4)确定前K个点所在类别的出现频率
(5)返回前K个点出现频率最高的类别
而这些步骤的解释,我在下面的代码片里有所提及,可以详见注释。
相关代码及注释:
#KNN分类算法,inX是用于分类的输入向量,dataSet是输入的训练样本集,labels是标签向量,K是用于选择最近邻居的数目
def classify0(inX, dataSet, labels, k):
# 以下是第一步:计算已知数据集的点与当前点之间的距离
dataSetSize = dataSet.shape[0] #获取数据集的宽
diffMat = tile(inX, (dataSetSize,1)) - dataSet #使用欧式距离度量,故将输入向量和数据集中各向量相减
sqDiffMat = diffMat**2 #进行平方
sqDistances = sqDiffMat.sum(axis=1) #计算输入向量和数据集中各向量之间的差的平方和
distances = sqDistances**0.5 #计算欧式距离
# 以下是第二步:按照距离递增次序排序
sortedDistIndicies = distances.argsort() #取得输入向量和数据集中各向量欧式距离的从小到大排序的下标值,返回排序后的索引值
# 以下是第三步:选取与当前点距离最小的K个点
classCount={} #定义一个空字典
for i in range(k): #取计算欧氏距离排序后最小的前k个值
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1 #若字典中存在label键,则对应的值+1,如果不存在,则添加这个键设置为0并+1
# 以下是第四步:确定前K个点所在类别的出现频率
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True) #按照字典中的值对字典进行降序排列
# 最后是第五步,返回前K个点出现频率最高的类别
return sortedClassCount[0][0]
#创建数据集和标签
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
补充:
因为函数里有用到tile函数,这个函数将inX向量进行相应的变换,使得它与dataSet矩阵为相似矩阵,并进行相应的操作。
而tile函数的形式是: tile(A,rep),
A: Array类的都可以
rep:A沿着各个维度重复的次数(由外至内)
它的功能是:重复A的各个维度
如果对tile函数感兴趣,可以参照这个Python tile函数详细解释
这里,我将对“准备数据”的相关代码函数进行解析,即file2matrix函数和 autoNorm函数
file2matrix函数的功能是从文本文件中解析数据,输入的参数为文件名字,返回的结果为训练样本矩阵和类标签向量。
file2matrix函数的大体思路可以概况为3个步骤:
(1)得到文件行数
(2)创建返回的Numpy矩阵
(3)解析文件数据到列表
同样,这些步骤的解释,我在下面的代码片里有所提及,可以详见注释。
autoNorm函数的功能是归一化特征值,输入的参数为数据集数据,返回的结果为归一化矩阵,取值范围以及最小值。
至于归一化特征值的作用,我在上文有所提及,可以详见“1.3 注意事项”的第5点。
相关代码及注释:
#从文本文件中解析数据,输入为文件名字,输出为训练样本矩阵和类标签向量
def file2matrix(filename):
# 以下是第一步:得到文件行数
fr = open(filename) #打开文件
numberOfLines = len(fr.readlines()) #计算该文件共有多少行(即共有多少个样本)
# 以下是第二步:创建返回的Numpy矩阵
returnMat = zeros((numberOfLines,3)) #创建返回的Numpy矩阵
classLabelVector = [] #定义下面需要返回的标签
fr = open(filename) #需要再次打开文件,否则会报错
index = 0
# 以下是第三步:解析文件数据到列表
for line in fr.readlines(): #解析文件数据到列表
line = line.strip() #去掉首尾空白符
listFromLine = line.split('\t') #利用空格符分离字符串
returnMat[index,:] = listFromLine[0:3] #将每行样本数据的前3个数据输入返回样本矩阵中
classLabelVector.append(int(listFromLine[-1])) #将每行样本数据的最后一个数据加入类标签向量中
index += 1
return returnMat,classLabelVector #返回训练样本矩阵和类标签向量
#归一化特征值,输入为数据集数据
def autoNorm(dataSet):
minVals = dataSet.min(0) #获得数据集中每列的最小值
maxVals = dataSet.max(0) #获得数据集中每列的最大值
ranges = maxVals - minVals #获取取值范围
normDataSet = zeros(shape(dataSet)) #初始化归一化数据集
m = dataSet.shape[0] #行
normDataSet = dataSet - tile(minVals, (m,1))
normDataSet = normDataSet/tile(ranges, (m,1)) #特征值相除
return normDataSet, ranges, minVals #返回归一化矩阵,取值范围以及最小值
这里,我将对创建散点图的相关代码进行解析。
散点图使用矩阵的第二、第三列数据,分别表示特征值。
而它的scatter函数个性化标记散点图上的点。它利用变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等、尺寸不同的点。它还利用datingDatamat矩阵的第二列和第三列属性来展示数据。
相关代码:
# 创建散点图,散点图使用矩阵的第二、第三列数据,分别表示特征值
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDatamat[:,1], datingDatamat[:,2], 15.0*array(datingLabels), 15.0*array(datingLabels))
matplotlib.pyplot.show()
“测试算法”对应datingClassTest函数,“使用算法”对应classifyperson函数。这里我将对它们的相关代码进行解析。
datingClassTest函数的功能是计算分类器的错误率和错误个数,它输出计算得到的错误率和错误个数。
classifyperson函数的功能是实现对约会网站的对象判断,它需要用户手动输入对应的特征数值,而后输出判断约会对象的类型。
输入样例:
percentage of time spent playing video games?10
frequent flier miles earned per year?10000
liters of ice cream consumed per year?0.5
输出结果:
You will probably like this person: in small doses
同样,这些函数的解释,我在下面的代码片里有所提及,可以详见注释。
相关代码及注释:
#计算分类器的错误率和错误个数
def datingClassTest():
hoRatio = 0.50 #取测试样本占数据集样本的10%
datingDataMat,datingLabels = file2matrix('datingTestSet2(1).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],3) #通过k-近邻算法进行分类,并记录结果
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))) #计算错误率,并输出
print(errorCount) #输出错误的个数
#约会网站的对象判断
def classifyperson():
resultList = ['not at all', 'in small doses', 'in large doses'] #喜欢程度的预测值列表
#输入约会对象的一些特征
percentTats = float(input("percentage of time spent playing video games?"))
ffMiles = float(input("frequent flier miles earned per year?"))
iceCream = float(input("liters of ice cream consumed per year?"))
datingDatamat,datingLabels = file2matrix('datingTestSet2(1).txt') # 从文本文件中解析数据
normMat, ranges, minVals = autoNorm(datingDatamat) #特征归一化处理
inArr = array([ffMiles, percentTats, iceCream]) #输入的约会对象的特征
classifierResult = classify0((inArr - minVals) / ranges, normMat, datingLabels, 3) #判断约会对象的类型
print("You will probably like this person: ", resultList[classifierResult - 1])
手写识别系统包含2个部分,即“准备数据”和“测试算法”。
“准备数据”对应img2vector函数,“测试算法”对应handwritingClassTest函数。这里我将对它们的相关代码进行解析。
img2vector函数的功能是将图像转换为测试向量,它最后返回测试向量,这个向量本身也完全是由01构成,相当于将原来的矩阵每一行首尾相连。
handwritingClassTest函数的功能是使用K-近邻算法识别手写数字,并计算错误率和错误个数。
同样,这些函数的解释,我在下面的代码片里有所提及,可以详见注释。
相关代码及注释:
#将图像转换为测试向量
def img2vector(filename):
returnVect = zeros((1,1024)) #返回一个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]) #这个向量本身也完全是由01构成,相当于将原来的矩阵每一行首尾相连
return returnVect
#识别手写数字
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir('trainingDigits') #导入训练集,‘trainingDigits’是一个文件夹
m = len(trainingFileList) #计算训练样本个数
trainingMat = zeros((m,1024)) #初始化数据集,将所有训练数据用一个m行,1024列的矩阵表示
for i in range(m):
fileNameStr = trainingFileList[i] #获得所有文件名,文件名格式‘x_y.txt’,x表示这个手写数字实际表示的数字(label)
fileStr = fileNameStr.split('.')[0] #去除 .txt
classNumStr = int(fileStr.split('_')[0]) #classnumber为每个样本的分类,用‘_’分割,取得label
hwLabels.append(classNumStr) #将所有标签都存进hwLables[]
trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr) #将文件转化为向量后存入trainingMat[],这里展现了灵活的文件操作
testFileList = listdir('testDigits') #迭代测试集
errorCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0] #去除 .txt
classNumStr = int(fileStr.split('_')[0])
vectorUnderTest = img2vector('testDigits/%s' % fileNameStr) #这部分针对测试集的预处理和前面基本相同
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3) #使用算法预测样本所属类别,调用了前面写的classify0()函数
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)))
from numpy import * #导入科学计算包numpy模块
import operator #导入运算符模块
from os import listdir #用于获取目录中的文件名
import matplotlib #用于创建散点图
import matplotlib.pyplot as plt #将pyplot命名为plt
#KNN分类算法,inX是用于分类的输入向量,dataSet是输入的训练样本集,labels是标签向量,K是用于选择最近邻居的数目
def classify0(inX, dataSet, labels, k):
# 以下是第一步:计算已知数据集的点与当前点之间的距离
dataSetSize = dataSet.shape[0] #获取数据集的宽
diffMat = tile(inX, (dataSetSize,1)) - dataSet #使用欧式距离度量,故将输入向量和数据集中各向量相减
sqDiffMat = diffMat**2 #进行平方
sqDistances = sqDiffMat.sum(axis=1) #计算输入向量和数据集中各向量之间的差的平方和
distances = sqDistances**0.5 #计算欧式距离
# 以下是第二步:按照距离递增次序排序
sortedDistIndicies = distances.argsort() #取得输入向量和数据集中各向量欧式距离的从小到大排序的下标值,返回排序后的索引值
# 以下是第三步:选取与当前点距离最小的K个点
classCount={} #定义一个空字典
for i in range(k): #取计算欧氏距离排序后最小的前k个值
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1 #若字典中存在label键,则对应的值+1,如果不存在,则添加这个键设置为0并+1
# 以下是第四步:确定前K个点所在类别的出现频率
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True) #按照字典中的值对字典进行降序排列
# 最后是第五步,返回前K个点出现频率最高的类别
return sortedClassCount[0][0]
#创建数据集和标签
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 file2matrix(filename):
# 以下是第一步:得到文件行数
fr = open(filename) #打开文件
numberOfLines = len(fr.readlines()) #计算该文件共有多少行(即共有多少个样本)
# 以下是第二步:创建返回的Numpy矩阵
returnMat = zeros((numberOfLines,3)) #创建返回的Numpy矩阵
classLabelVector = [] #定义下面需要返回的标签
fr = open(filename) #需要再次打开文件,否则会报错
index = 0
# 以下是第三步:解析文件数据到列表
for line in fr.readlines(): #解析文件数据到列表
line = line.strip() #去掉首尾空白符
listFromLine = line.split('\t') #利用空格符分离字符串
returnMat[index,:] = listFromLine[0:3] #将每行样本数据的前3个数据输入返回样本矩阵中
classLabelVector.append(int(listFromLine[-1])) #将每行样本数据的最后一个数据加入类标签向量中
index += 1
return returnMat,classLabelVector #返回训练样本矩阵和类标签向量
#归一化特征值,输入为数据集数据
def autoNorm(dataSet):
minVals = dataSet.min(0) #获得数据集中每列的最小值
maxVals = dataSet.max(0) #获得数据集中每列的最大值
ranges = maxVals - minVals #获取取值范围
normDataSet = zeros(shape(dataSet)) #初始化归一化数据集
m = dataSet.shape[0] #行
normDataSet = dataSet - tile(minVals, (m,1))
normDataSet = normDataSet/tile(ranges, (m,1)) #特征值相除
return normDataSet, ranges, minVals #返回归一化矩阵,取值范围以及最小值
#计算分类器的错误率和错误个数
def datingClassTest():
hoRatio = 0.50 #取测试样本占数据集样本的10%
datingDataMat,datingLabels = file2matrix('datingTestSet2(1).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],3) #通过k-近邻算法进行分类,并记录结果
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))) #计算错误率,并输出
print(errorCount) #输出错误的个数
#约会网站的对象判断
def classifyperson():
resultList = ['not at all', 'in small doses', 'in large doses'] #喜欢程度的预测值列表
#输入约会对象的一些特征
percentTats = float(input("percentage of time spent playing video games?"))
ffMiles = float(input("frequent flier miles earned per year?"))
iceCream = float(input("liters of ice cream consumed per year?"))
datingDatamat,datingLabels = file2matrix('datingTestSet2(1).txt') # 从文本文件中解析数据
normMat, ranges, minVals = autoNorm(datingDatamat) #特征归一化处理
inArr = array([ffMiles, percentTats, iceCream]) #输入的约会对象的特征
classifierResult = classify0((inArr - minVals) / ranges, normMat, datingLabels, 3) #判断约会对象的类型
print("You will probably like this person: ", resultList[classifierResult - 1])
#将图像转换为测试向量
def img2vector(filename):
returnVect = zeros((1,1024)) #返回一个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]) #这个向量本身也完全是由01构成,相当于将原来的矩阵每一行首尾相连
return returnVect
#识别手写数字
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir('trainingDigits') #导入训练集,‘trainingDigits’是一个文件夹
m = len(trainingFileList) #计算训练样本个数
trainingMat = zeros((m,1024)) #初始化数据集,将所有训练数据用一个m行,1024列的矩阵表示
for i in range(m):
fileNameStr = trainingFileList[i] #获得所有文件名,文件名格式‘x_y.txt’,x表示这个手写数字实际表示的数字(label)
fileStr = fileNameStr.split('.')[0] #去除 .txt
classNumStr = int(fileStr.split('_')[0]) #classnumber为每个样本的分类,用‘_’分割,取得label
hwLabels.append(classNumStr) #将所有标签都存进hwLables[]
trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr) #将文件转化为向量后存入trainingMat[],这里展现了灵活的文件操作
testFileList = listdir('testDigits') #迭代测试集
errorCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0] #去除 .txt
classNumStr = int(fileStr.split('_')[0])
vectorUnderTest = img2vector('testDigits/%s' % fileNameStr) #这部分针对测试集的预处理和前面基本相同
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3) #使用算法预测样本所属类别,调用了前面写的classify0()函数
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)))
if __name__ == "__main__": # 当程序执行时
# 调用基础函数
group,labels = createDataSet()
a = classify0([0,0], group, labels, 3)
print(a)
# 从文件中解析数据
f = open("datingTestSet2(1).txt", "r")
datingDatamat,datingLabels = file2matrix('datingTestSet2(1).txt')
print(datingDatamat)
print(datingLabels[0:20])
# 创建散点图,散点图使用矩阵的第二、第三列数据,分别表示特征值
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDatamat[:,1], datingDatamat[:,2], 15.0*array(datingLabels), 15.0*array(datingLabels))
matplotlib.pyplot.show()
# 归一化特征值
normMat, ranges, minVals = autoNorm(datingDatamat)
print(normMat)
print(ranges)
print(minVals)
# 计算分类器的错误率和错误个数
datingClassTest()
# 约会网站的对象判断
classifyperson()
# 将图像转换为测试向量
testVector = img2vector('testDigits/0_13.txt')
print(testVector[0,0:31])
print(testVector[0,32:63])
# 计算分类器的错误率和错误个数
handwritingClassTest()
以上是我整理的k-近邻算法的相关知识和代码解析,望对大家有用。由于初写,如有纰漏或者错误,欢迎指出。
以下是对K-近邻算法的评价:
1.算法执行效率低:算法需要为每个测试向量做大约2000次距离计算,每个距离计算包括了1024个维度浮点计算,总共要执行900次。此外,还需要为测试向量准备一定的存储空间。所以这个算法不仅耗时,而且需要不少的存储空间。
2.k-临近算法的另一个缺陷就是它无法给出任何数据的基础结构信息,因此我们无法得知平均实例样本和典型实例样本具体有什么特征。
3.k-近邻算法是分类数据最简单最有效的算法,并且精度高、对异常值不敏感、无数据输入假定。
4.K决策树就是k-近邻算法的优化版,可以节省大量的计算开销。
[1]: 《机器学习实战》[美] Peter Harrington 著 李锐 李鹏 曲亚东 王斌 译