PS
该部分内容所设计到的程序源码已经存在我的github上,地址奉上:
https://github.com/AdventureSJ/ML-Notes/tree/master/KNN
欢迎各位大佬批评指正,也欢迎各位好友fock or star!
Thank You!
k-近邻算法概述
简单的说,k-近邻算法采用测量不同特征值之间的距离方法进行分类,其工作原理是:存在一个样本数据集合,也称作训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。输人没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数。最后选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
- 优点:精度高,对异常值不敏感,无输入数据假定
- 缺点:计算复杂度高,空间复杂度高
- 适用数据范围:数值型和标称型
k-近邻算法的一般流程:
- 收集数据:可以使用任何方法
- 准备数据:距离计算所需要的数值,最好是结构化的数据格式
- 分析数据:可以使用任何方法。
- 测试算法:计算错误率
- 使用算法:首先需要输入样本数据和结构化的输出结果,然后运行k-近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续的处理
对电影类型进行分类
众所周知,电影可以按照题材分类,然而题材本身是如何定义的?由谁来判定某部电影属于哪个题材?也就是说同一题材的电影具有哪些公共特征?这些都是在进行电影分类时必须要考虑的问题。没有哪个电影人会说自己制作的电影和以前的某部电影类似,但我们确实知道每部电影在风格上的确有可能会和同题材的电影相近。那么动作片具有哪些共有特征,使得动作片之间非常类似,而与爱情片存在着明显的差别呢?动作片中也会存在接吻镜头,爱情片中也会存在打斗场景,我们不能单纯依靠是否存在打斗或者亲吻来判断影片的类型。但是爱情片中的亲吻镜头更多,动作片中的打斗场景也更频繁(除了爱情动作片,此处应有一个滑稽脸),基于此类场景在某部电影中出现的次数可以用来进行电影分类。本节基于电影中出现的亲吻、打斗出现的次数,使用k近邻算法构造程序,自动划分电影的题材类型。我们首先使用电影分类测试k近邻算法,然后学习如何在其他系统上使用k近邻算法。
创建一个名为kNN.py的文件,添加以下代码:
import numpy as np
import operator
"""
函数说明:创建数据集
Parameter:无
Return:
group - 数据集
labels - 数据集标签
Modify:
2018/9/4
"""
def creatdatasets():
#创建四组数据的二维特征
group = np.array([[1,101],[5,89],[108,5],[115,8]]) #二维数组第一列表示电影中动作镜头出现的次数,第二列表示亲吻镜头出现的次数。
#创建数据集的标签
labels = ['爱情片','爱情片','动作片','动作片'] #电影的标签
return group,labels
"""
函数说明:KNN算法,分类器
Parameter:
inx - 测试集
dataset - 样本集
labels - 样本集标签
k - 选择距离最小的K个点
Return:
sortedClassCount[0][0] - 分类结果
Modify:
2018/9/4
"""
def classfy1(inx,dataSet,labels,k):
#获取样本集的行数
dataSetsize = dataSet.shape[0]
#将测试集扩充为和样本集一样的结构,并作差值
diffMat = np.tile(inx,(dataSetsize,1)) - dataSet
#计算测试集和样本集的距离值
sqDiffmat = diffMat**2
sqDisttances = sqDiffmat.sum(axis=1)
distances = sqDisttances**0.5
#返回distances中元素从小到大排序后的索引值
sortedDistIndices = distances.argsort()
classCount = {}
for i in range(k):
#获取前K个元素的类别
votellabel = labels[sortedDistIndices[i]]
#计算类别次数
classCount[votellabel] = classCount.get(votellabel,0) + 1
#reverse=True降序排序字典
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
if __name__=='__main__':
group,labels = creatdatasets()
test = [101,20]
test_class = classfy1(test,group,labels,3)
print(test_class)
运行上述代码,输出结果应该是动作片,这是因为测试数据中动作镜头相较于亲吻镜头更多,按照距离远近排序,前三种类别中动作片的概率最大,所以将测试的电影划分为动作片。接下来我们将在现实世界中使用k-近邻算法。
使用kNN算法改进约会网站的配对效果
我的朋友海伦一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但她没有从中找到喜欢的人。经过一番总结,她发现曾交往过三种类型的人:
- 不喜欢的人
- 魅力一般的人
- 极具魅力的人
尽管发现了上述规律,但海伦依然无法将约会网站推荐的匹配对象归人恰当的分类。她觉得可以在周一到周五约会那些魅力一般的人,而周末则更喜欢与那些极具魅力的人为伴。海伦希望 我们的分类软件可以更好地帮助她将匹配对象划分到确切的分类中。此外海伦还收集了一些约会网站未曾记录的数据信息,她认为这些数据更有助于匹配对象的归类 。
准备数据:从文本中解析数据
海伦收集约会数据巳经有了一段时间,她把这些数据存放在文本文件(datingTestSet.txt)中,每个样本数据占据一行,总共有1000行。海伦的样本主要包含以下3种特征:
- 每年获得的飞行常客里程数
- 玩视频游戏所耗时间百分比
- 每周消费的冰淇淋公升数
在将上述特征数据输人到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式 。在kNN.py中创建名为file2matrix的函数,以此来处理输人格式问题。该函数的输人为文件名字符串,输出为训练样本矩阵和类标签向量,将下面的代码增加到kNN.py中。
"""
函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力
Parameter:
filename - 文件名
Return:
returnMat - 特征矩阵
classLabelVector - 分类Label向量
Modify:
2018/9/4
"""
def file2matrix(filename):
#打开文件
fr = open(filename)
#读取每一行,返回列表
fileLines = fr.readlines()
#获得行数
fileRows = len(fileLines)
#构建返回矩阵
returnMat = np.zeros((fileRows,3))
#返回矩阵对应的标签列表
classLabelVector = []
index = 0
for line in fileLines:
line = line.strip()
#去除每一行的空白部分以及各种转义字符
ListFromLine = line.split('\t')
#将每一行的前三列元素放入返回矩阵中
returnMat[index,:] = ListFromLine[0:3]
#将每一行的最后一个元素放入标签列表中
if ListFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif ListFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif ListFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index +=1
return returnMat,classLabelVector
现在已经从文本文件中导人了数据,并将其格式化为想要的格式,接着我们需要了解数据的真实含义。当然我们可以直接浏览文本文件,但是这种方法非常不友好,一般来说,我们会采用图形化的方式直观地展示数据。下面就用python工具来图形化展示数据内容,以便辨识出一些数据模式。
分析数据:使用Matplotlib创建散点图
创建一个测试文件,在文件中输入以下代码:
from numpy import *
import kNN
import matplotlib
import matplotlib.pyplot as plt
datingDataMat,datingLabels = kNN.file2matrix('datingTestSet.txt')
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels),15.0*array(datingLabels))
plt.show()
输出效果如下图所示。散点图使用( datingDataMat矩阵的第二、第三列数据,分别表示特征值 “玩视频游戏所耗时间百分比”和 “每周所消费的冰淇淋公升数”
上述代码利用变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等、尺寸不同的点。 因而我们基本上可以从上图看到数据点所属三个样本分类的区域轮廓。
采用了不同的属性值可能可以获得更加好的效果,使用第一列和第二列数据(分别表示 每年贏得的飞行常客里程数与玩视频游戏所占百分比 )画出的散点图如下图所示,
准备数据:归一化数值
我们通过观察数据集可以看到,每年获取的飞行常客里程数的数字差值远远大于其他两项,因此在计算欧氏距离的时候对计算结果的影响很大,海伦认为这三项特征是同等重要的,因此我们要对获取的数据进行数值归一化处理。
最常用的归一化处理方式有最大最小化处理,或者z-score等等,这里我们使用最大最小值处理数据,在kNN.py文件中添加以下的代码:
"""
函数说明:对数据进行归一化
Parameters:
dataSet - 特征矩阵
Return:
normDataSet - 归一化后的特征矩阵
ranges - 数据范围
minVals - 数据最小值
Modify:
2018/9/4
"""
def autoNorm(dataSet):
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = np.zeros(np.shape(dataSet))
m = dataSet.shape[0]
normDataSet = (dataSet - np.tile(minVals,(m,1)))/np.tile(ranges,(m,1))
return normDataSet,ranges,minVals
经过归一化处理后,每个数据的取值范围就都在0、1之间了。
测试算法:作为完整程序验证分类器
机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器 ,而使用其余的10%数据去测试分类器,检测分类器的正确率。后续还会介绍一些高级方法完成同样的任务,这里我们还是采用最原始的做法。需要注意的是,10%的测试数据应该是随机选择的,由于海伦提供的数据并没有按照特定目的来排序,所以我们可以随意选择10%数据而不影响其随机性。
前面我们巳经提到可以使用错误率来检测分类器的性能。对于分类器来说,错误率就是分类器给出错误结果的次数除以测试数据的总数,完美分类器的错误率为0 ,而错误率为1.0的分类器不会给出任何正确的分类结果。代码里我们定义一个计数器变量,每次分类器错误地分类数据,计数器就加1, 程序执行完成之后计数器的结果除以数据点总数即是错误率 。
在kNN.py文件中添加datingClassTest函数:
"""
函数说明:分类器测试函数
Parameters:
无
Returns:
normDataSet - 归一化后的特征矩阵
ranges - 数据范围
minVals - 数据最小值
Modify:
2018/9/4
"""
def datingClassTest():
filename = "datingTestSet.txt"
datingDataMat,datingLabels = file2matrix(filename) #数据格式转换
hoRatio = 0.1 #测试集占样本集的比率
normMat,ranges,minVals = autoNorm(datingDataMat) #归一化处理
m = normMat.shape[0]
numTestVecs = int(m*hoRatio) #测试集数据个数
errorCount = 0.0
for i in range(numTestVecs):
classifierResult = classfy1(normMat[i,:],normMat[numTestVecs:m,:],
datingLabels[numTestVecs:m],3)
print("分类结果:%d\t真实类别:%d"%(classifierResult,datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率:%f%%"%(errorCount/float(numTestVecs)*100))
我们可以改变函数datingClassTest内变量hoRatio和变量k的值,检测错误率是否随着变量值的变化而增加。依赖于分类算法、数据集和程序设置,分类器的输出结果可能有很大的不同。
使用算法:构建完整可用系统
上面我们已经在数据上对分类器进行了测试,现在终于可以使用这个分类器为海伦来对人们分类。我们会给海伦一小段程序,通过该程序海伦会在约会网站上找到某个人并输入他的信息。程序会给出她对对方喜欢程度的预测值。
'''
函数说明:约会网站测试函数
Parameters:
无
Returns:
无
Modify:
2018/10/22
'''
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"))
datingDatMat,datingLabels = file2matrix("datingTestSet.txt")
normMat,ranges,minVals = autoNorm(datingDatMat)
inArr = np.array([ffMiles,percentTats,icecream])
norminArr = (inArr - minVals) / ranges
classifierResult = classfy1(norminArr,normMat,datingLabels,3)
print("你可能%s这个人" % (resultList[classifierResult-1]))
示例:手写数字识别系统
本节我们一步步地构造使用k-近邻分类器的手写识别系统。为了简单起见,这里构造的系统只能识别数字0到9 ,参见下图。需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小 : 宽髙是32像素*32像素的黑白图像。尽管采用文本格式存储图像不能有效地利用内存空间,但是为了方便理解,我们还是将图像转换为文本格式。
实际图像存储在两个目录内:目录trainingDigits中包含了大约2000个例子,每个例子的内容如上图所示 ,每个数字大约有200个样本;目录testDigits中包含了大约900个测试数据。我们使用目录trainingDigits中的数据训练分类器,使用目录testDigits中的数据测试分类器的效果。两组数据没有覆盖,你可以检查一下这些文件夹的文件是否符合要求.
在测试分类之前,我们必须将图像格式化处理为一个向量。我们将把一个32x32的二进制图像矩阵转换为1x1024的向量,这样前两节使用的分类器就可以处理数字图像信息了。添加以下代码:
"""
函数说明:将32x32的二进制图像转换为1x1024向量。
Parameters:
filename - 文件名
Returns:
returnVect - 返回的二进制图像的1x1024向量
Modify:
2018-10-22
"""
import numpy as np
def img2vector(filename):
#创建1x1024零向量
returnVect = np.zeros((1, 1024))
#打开文件
fr = open(filename)
#按行读取
for i in range(32):
#读一行数据
lineStr = fr.readline()
#每一行的前32个元素依次添加到returnVect中
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
#返回转换后的1x1024向量
return returnVect
测试算法:使用k-近邻算法识别手写数字
"""
函数说明:手写数字分类测试
Parameters:
无
Return:
无
Modify:
2018/10/22
"""
def handwritingClassTest():
#测试集的Labels
hwLabels = []
#返回trainingDigits目录下的文件名
trainingFileList = listdir('trainingDigits')
#返回文件夹下文件的个数
m = len(trainingFileList)
#初始化训练的Mat矩阵,测试集
trainingMat = np.zeros((m, 1024))
#从文件名中解析出训练集的类别
for i in range(m):
#获得文件的名字
fileNameStr = trainingFileList[i]
#获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
#将获得的类别添加到hwLabels中
hwLabels.append(classNumber)
#将每一个文件的1x1024数据存储到trainingMat矩阵中
trainingMat[i,:] = img2vector('trainingDigits/%s' % (fileNameStr))
#返回testDigits目录下的文件名
testFileList = listdir('testDigits')
#错误检测计数
errorCount = 0.0
#测试数据的数量
mTest = len(testFileList)
#从文件中解析出测试集的类别并进行分类测试
for i in range(mTest):
#获得文件的名字
fileNameStr = testFileList[i]
#获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
#获得测试集的1x1024向量,用于训练
vectorUnderTest = img2vector('testDigits/%s' % (fileNameStr))
#获得预测结果
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
if(classifierResult != classNumber):
errorCount += 1.0
print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount/mTest))
"""
函数说明:main函数
Parameters:
无
Returns:
无
Modify:
2018/10/22
"""
if __name__ == '__main__':
handwritingClassTest()
k-近邻算法识别手写数字数据集,错误率为0.4%左右,改变变量k的值、修改函数handwritingClassTest随机选取训练样本、改变训练样本的数目,都会对k-近邻算法的错误率产生影响,感兴趣的话可以改变这些变量值,观察错误率的变化。
小结
k-近邻算法是分类数据最简单最有效的算法,本章通过两个例子讲述了如何使用k-近邻算法构造分类器。k-近邻算法是基于实例的学习,使用算法时我们必须有接近实际数据的训练样本数据。k-近邻算法必须保存全部数据集,如果训练数据集的很大,必须使用大量的存储空间。此外,由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。