k-近邻算法概述
优点:精度高、对异常值不敏感、无数据输入假定。
缺点:计算复杂度高、空间复杂度高。
适用数据范围:数值型和标称型。
k-近邻算法的一般流程
- 收集数据:可以使用任何方法。
- 准备数据:距离计算所需要的数值。
- 分析数据:可以使用任何方法。
- 训练算法:此步骤不适用于k-近邻算法。
- 测试算法:计算错误率。
- 使用算法:首先需要输入样本数据和结构化的输出结果,然后运行k-近邻算法判定输入数据分别属于哪几个分类,最后应用对计算出的分类执行后续的处理。
准备阶段:
先创建KNN.py的文件,增加如下代码:
from numpy import *
import operator
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
在上述代码中,导入了两个模块,一个事科学计算包NumPy;第二个是运算符模块。
从文本中解析数据
k-近邻算法的伪代码如下:
对未知类别属性的数据集中的每个点依次执行以下操作:
- 计算已知类别数据集中的点与当前点之间的距离;
- 按照距离递增次序排序;
- 选取与当前点距离最小的k个点;
- 确定前K个点所在类别的出现频率;
- 返回前K个点所在类别的出现频率;
- 返回前K个点出现频率最高的类别作为当前点的预测分类。
接下来定义classify0():
def classify0(inX, dataSet, labels, k): #距离计算:inX:用于分类的输入向量;dataSet:输入的训练样本集;labels:标签向量;k:最邻近邻居的数目;
dataSetSize = dataSet.shape[0] #计算dateSet的行数,也就是训练样本的个数;
diffMat = tile(inX, (dataSetSize, 1)) - dataSet #将输入inX经过tile函数变为n列1行,其中每列都是inX,一共是dataSetSize列,再与dataSet相减;
sqDiffMat = diffMat ** 2 #结果平方
sqDistances = sqDiffMat.sum(axis=1) #求出距离的平方
distances = sqDistances ** 0.5 #开根号求出距离
sortedDistIndicies = distances.argsort() #返回距离从小到大排序后的索引值
classCount = {}
for i in range(k): #选择距离最小的k个点
voteIlabel = labels[sortedDistIndicies[i]] #最小的k个距离对应的标签
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse = True) #排序
return sortedClassCount[0] [0]
classify0有四个输入参数:inX:用于分类的输入向量;dataSet:输入的训练样本集;labels:标签向量;k:最邻近邻居的数目;
其中标签向量labels的元素数目和矩阵dataSet的行数相同。
变量distances表示两个向量点的位置
tile函数表示为:
将输入inX经过tile函数变为n列1行,其中每列都是inX,一共是dataSetSize列
使用k-近邻算法改进约会网站的配对效果
在约会网站上使用k-近邻算法的伪代码:
- 收集数据:提供文本文件。
- 准备数据:使用Python解析文本文件。
- 分析数据:使用Matplotlib画二维扩散图。
- 训练算法:此步骤不适用于k-近邻算法。
- 测试算法:使用书中所提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
- 使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。
准备数据:从文本文件中解析数据
样本中主要包含以下三种特征:
- 每年获得的飞机常客里程数
- 玩游戏视频所消耗时间百分比
- 每周消费的冰淇淋公升数
我们需要将待处理的数据改变为分类器可以接受的格式,在KNN.py中创建名为file2matrix的函数,用来处理格式问题,该函数的输入为文件名字符串,输出为训练样本矩阵和类标签向量。
将下面的代码增加到KNN中。
def file2matrix(filename):
fr = open(filename) #打开文档
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines) #得到文件行数
returnMat = zeros((numberOfLines, 3)) #因为要用到3个特征,所有数字为3,行数表明多少个人
classLabelVector = [] #定义一个空的数组
index = 0 #初始化索引值为零
for line in arrayOLines: #遍历数组
line = line.strip() #删除所有空白的回车字符,删除\n
listFromLine = line.split('\t') #将上一步得到的整行数据分割成一个元素列表,以\t分割,并单一保持
returnMat[index, :] = listFromLine[0:3] #选取前三个元素,存到特征矩阵中,每行存储三个
classLabelVector.append(listFromLine[-1]) #将利用索引值-1索引的的最后一列元素储存到classLabelVector中, 我们必须明确告诉解释器,告诉它列表中存储的元素值为整型
index += 1 #索引自增
return returnMat, classLabelVector #返回特征值abc,返回感兴趣度
(注:由于原作者在classLabelVector.append()中用的是int(listFromLine[-1]),这将导致程序报错,删掉int即可)
我们先得到文件的行数,之后创建以零填充的矩阵Numpy,因为这里要用到三个特征,所以我们将该矩阵的另一维度设置为固定值3,其中的详细步骤都在代码中作了注释。
使用Matplotlib创建散点图
代码如下:
import KNN
import matplotlib
import matplotlib.pyplot as plt
datingDataMat,datingLabels = KNN.file2matrix('C:\Users\lxy\ML\datingTestSet2.txt')
font = FontProperties(fname=r"C:\\WINDOWS\\Fonts\\simsun.ttc", size=14)#C:\WINDOWS\Fonts #定义注释字体
fig = plt.figure()
ax = fig.add_subplot(111)
plt.scatter(datingDataMat[:,0],datingDataMat[:,1],c=datingLabels,s=35,alpha=0.4,marker='o')
plt.ylabel(u'玩游戏所耗视频时间百分比',fontproperties = font)
plt.xlabel(u'飞机里程',fontproperties = font)
python将绘制如下图所示:
我们可以清晰地看到有三个颜色不同的点,分别表示喜欢程度,但何种颜色表示何种喜欢程度我们却不得而知。这与书中24页的图有一定差距,书中也没有给详细的代码。经过网上查找,发现代码如下:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
n = 1000 #number of points to create
xcord1 = []; ycord1 = []
xcord2 = []; ycord2 = []
xcord3 = []; ycord3 = []
markers =[]
colors =[]
fw = open('testSet.txt','w')
for i in range(n):
[r0,r1] = random.standard_normal(2)
myClass = random.uniform(0,1)
if (myClass <= 0.16):
fFlyer = random.uniform(22000, 60000)
tats = 3 + 1.6*r1
markers.append(20)
colors.append(2.1)
classLabel = 1 #'didntLike'
xcord1.append(fFlyer); ycord1.append(tats)
elif ((myClass > 0.16) and (myClass <= 0.33)):
fFlyer = 6000*r0 + 70000
tats = 10 + 3*r1 + 2*r0
markers.append(20)
colors.append(1.1)
classLabel = 1 #'didntLike'
if (tats < 0): tats =0
if (fFlyer < 0): fFlyer =0
xcord1.append(fFlyer); ycord1.append(tats)
elif ((myClass > 0.33) and (myClass <= 0.66)):
fFlyer = 5000*r0 + 10000
tats = 3 + 2.8*r1
markers.append(30)
colors.append(1.1)
classLabel = 2 #'smallDoses'
if (tats < 0): tats =0
if (fFlyer < 0): fFlyer =0
xcord2.append(fFlyer); ycord2.append(tats)
else:
fFlyer = 10000*r0 + 35000
tats = 10 + 2.0*r1
markers.append(50)
colors.append(0.1)
classLabel = 3 #'largeDoses'
if (tats < 0): tats =0
if (fFlyer < 0): fFlyer =0
xcord3.append(fFlyer); ycord3.append(tats)
fw.close()
fig = plt.figure()
ax = fig.add_subplot(111)
#ax.scatter(xcord,ycord, c=colors, s=markers)
type1 = ax.scatter(xcord1, ycord1, s=20, c='red')
type2 = ax.scatter(xcord2, ycord2, s=20, c='green')
type3 = ax.scatter(xcord3, ycord3, s=20, c='blue')
ax.legend([type1, type2, type3], ["Did Not Like", "Liked in Small Doses", "Liked in Large Doses"], loc=2)
ax.axis([-5000,100000,-2,25])
#plt.scatter(datingDataMat[:,0],datingDataMat[:,1],c=datingLabels,s=35,alpha=0.4,marker='o')
plt.ylabel(u'玩游戏所耗视频时间百分比',fontproperties = font)
plt.xlabel(u'飞机里程',fontproperties = font)
plt.show ()
这样就可以产生带有不同喜欢程度的标志的不同颜色的点,如下图:
归一化处理
我们可以发现,在没有作归一化处理之前三种属性中飞机里程数对于计算结果的影响最大,而我们在这里认为这三种属性是同等重要的。所以我们通常是采用将数值归一化,在这里我们将取值范围处理为0到1之间,公式如下:
newValue = (oldValue - min) / (max - min)
其中min和max分别是数据集中的最小特征值和最大特征值,因此我们在文件KNN.py增加一个新函数autoNorm(),该函数可以自动的将数字特征值转化为0到1的区间。
代码如下:
def autoNorm(dataSet): #进行归一特征值处理
minVals = dataSet.min(0) #选取当前列中中最小值
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = zeros(shape(dataSet)) #得到dataSet的行数,依据该行数创建空数组
m = dataSet.shape[0]
normDataSet = dataSet - tile(minVals, (m, 1)) #用tile将变量内容复制成输入矩阵同样大小的矩阵,并在下方进行具体特征值相除
normDataSet = normDataSet/tile(ranges, (m, 1))
return normDataSet, ranges, minVals
得到的normDataSet为三中特征值做完归一化之后的数组,ranges为极差,minVals为最小值。
作为完整程序验证分类器
分类器针对约会网站的测试代码:
def datingClassTest(): #测试代码
hoRatio = 0.10 #定义10%用于用于测试分类器,剩下的90%用于训练样本
datingDataMat, datingLabels = file2matrix('C:\Users\lxy\ML\datingTestSet2.txt') #导入数据
normMat, ranges, minVals = autoNorm(datingDataMat) #前三列数据进行归一化处理
m = normMat.shape[0] #得到总行数
numTestVecs = int(m * hoRatio) #用于测试的数量,其中m * hoRatio是一个浮点型,而数组中是整数,所以需要转化为整型
errorCount = 0.0 #设置初始错误值为0.0
for i in range(numTestVecs): #设置循环
classfierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:], datingLabels[numTestVecs:m],3) #分类器(需要测试的向量,训练样本集,标签集合,K)
print "the classifier came back with: %s, the real answer is: %s" %(classfierResult, datingLabels[i]) #书上给的是%d,会报错,报错为:TypeError: %d format: a number is required, not str,改为%s即可,原因是
if (classfierResult != datingLabels[i]):
errorCount += 1.0
print "the total error rate is: %f" %(errorCount/float(numTestVecs)) #没有对齐就会报出IndentationError: unindent does not match any outer indentation level的错误
得到如下的输入结果:
可以看到错误率为5%,这表明该算法可以正确的预测分类。
构建完整可用系统
这段小程序会让使用者在约会网站上找到某个人并输入他的信息,程序就会给出她对对方喜欢的程度。
将下列代码加入到KNN.py并重新载入KNN:
def classifyPerson(): #构建完整可用系统
resultList = ['not at all', 'in small does', 'in large does'] #感兴趣程度
percentTats = float(raw_input("玩游戏所占时间比?"))
ffMiles = float(raw_input("每年的飞机里程数?"))
iceCream = float(raw_input("吃冰淇淋的公升数?")) #其中raw_input提供了输入功能
datingDataMat, datingLabels = file2matrix('C:\Users\lxy\ML\datingTestSet2.txt') #导入数据
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = array([ffMiles, percentTats, iceCream]) #inArr是归一化之前的datingDataMat数组的行
classifierResult = classify0((inArr-minVals)/ranges, normMat, datingLabels,3) #先归一化,然后调出分类函数
#print type(datingLabels), type(classifierResult)
print ("You will probably like this person ", resultList[int(classifierResult)-1])
raw_input()允许用户输入文本命令。最后输入:
KNN.classifyPerson()
结果如下:
结束。