继续跟着机器学习白皮书学习k-近邻算法,这个例子给的样本集是三个特征一个标签的。
问题描述
这个问题讲的是一个女生想给自己约会的男生分类,特征是:
ffMiles:每年获得的飞行常客里程数
percentTats:玩视频游戏消耗的时间百分比
iceCream:每周消费的冰淇淋公斤数
标签的值是:
①不怎么好
②比较一般
③较为不错
为了方便,在给出的样本集里用数字1/2/3来表示:
模块实现
#-*-coding:utf-8-*-
from numpy import * #科学计算包
import operator #运算符模块
from matplotlib import pyplot as plt
#上一行的末尾不能打注释!
#将文本记录转化为numpy(以tab分割的[3特征,1标签]的文本)
def file2matrix(filename):
fr=open(filename) #打开文件
arrayOLines=fr.readlines() #读取文件
numberOfLines=len(arrayOLines) #获得文件行数
returnMat=zeros((numberOfLines,3)) #行数=文件行数,列数=3的零矩阵
classLabelVector=[]
index=0
#对于文件读进来的每一行
for line in arrayOLines:
line=line.strip() #strip()去掉首尾空格
listFromLine=line.split('\t') #按Tab分割成列表
#把这一行的特征给特征矩阵第index行
returnMat[index,:]=listFromLine[0:3]
#把这一行的标签给标签向量第index行
classLabelVector.append(int(listFromLine[-1]))
index+=1 #指向下一行
return returnMat,classLabelVector #返回特征矩阵,标签向量
#k-近邻算法(输入的特征向量,训练集特征向量矩阵,训练集标签向量,k值)
def KJL(inX,dataSet,labels,k):
#用shape[0]读取训练集第一维的长度(行数即训练集实例数)
dataSetRow=dataSet.shape[0]
'''
#用shape[1]读取训练集第二维的长度(列数即特征数)
dataSetCol=dataSet.shape[1]
#读取输入向量的列数(特征数)
inXCol=len(inX)
#训练集标签向量的个数
labelsLen=len(labels)
#对训练集的判定
if labelsLen!=dataSetRow:
print "训练集的特征集和标签集实例数目不等"
return
#对输入的判定
if inXCol!=dataSetCol:
print "用于测试的的实例的特征数目和训练集特征数目不等"
return
'''
#用tile()将输入的特征向量重复成和训练集特征向量一样多的行
#变成2维,里面的维度重复1次,外面一层重复dataSetRow次
diffMat=tile(inX,(dataSetRow,1))
#减去训练集特征向量矩阵得到存偏差的矩阵
diffMat=diffMat-dataSet
#将减出来的偏差矩阵每个元素平方
sqDiffMat=diffMat**2
#对行求和,表示这个实例和这行对应的训练集实例的L2范数的平方
sqDistances=sqDiffMat.sum(axis=1)
#为了方便就不开根号(**0.5)了
#argsort()返回其从小到大排序的排序索引序列
sortIndex=sqDistances.argsort()
#空字典,用来存各个标签在前k邻居中出现的次数
classCount={}
#找前k个距离最近的,也就是排序下标从0~k-1的
for i in range(k):
#暂存第i近(从0计数)训练集实例的标签
voteIlab=labels[sortIndex[i]]
#先取字典中以这个标签为key的value值,如果没有则返回0
#加上1作为以这个标签为key的value值
classCount[voteIlab]=classCount.get(voteIlab,0)+1
#把classCount用iteritems()方法变成可迭代对象传入
#用operator.itemgetter()方法定义一个函数给参数key,这个函数按1号域排序
#将reverse参数显示修正为True,表示降序排序(找频数最大的)
sortedClassCount=sorted(
classCount.iteritems(),
key=operator.itemgetter(1),
reverse=True)
#排序好后,第0个对象就是要找的那个频率最高的实例的[标签,频率]了
#只返回标签
return sortedClassCount[0][0]
#展示kNN算法的数据集(特征矩阵,标签向量,第i号特征为横轴,第j号特征为纵轴)
def kNNshow(dataMat,labelVec,i,j):
fig=plt.figure()
ax=fig.add_subplot(111) #1行1列的第1个
#把第0号特征和1号特征作为两维度绘制点,传入它们的list以绘制点
#scatter()的第3和第4个参数决定点的颜色和大小,把类别号乘以一个系数以放大这种差异
ax.scatter(dataMat[:,i],dataMat[:,j],15.0*array(labelVec),15.0*array(labelVec))
plt.show()
#不同的特征,范围可能不同,作差得到的值可能差别很大
#如果它们的重要程度一样,显然不应如此
#可以将不同取值范围的特征值数值归一化到0~1之间
def autoNorm(dataMat):
#下面参数0表示从列中取最大最小
minVals=dataMat.min(0) #每列的最小值存到minVals里
maxVals=dataMat.max(0) #每列的最大值存到maxVals里
ranges=maxVals-minVals #每列最大最小值之差,存到ranges里
newDataMat=zeros(shape(dataMat)) #用来存归一化后的矩阵
m=dataMat.shape[0] #取第0维即行数
newDataMat=dataMat-tile(minVals,(m,1)) #把最小值重复m成m行,用原值减去
newDataMat=newDataMat/tile(ranges,(m,1)) #把减完的每个偏差值除以自己列最大和最小的差值
return newDataMat,ranges,minVals #返回归一化之后的矩阵,原范围,原最小值
#测试分类器效果(不用传参的函数称为"自包含的")
def datingClassTest():
hoRatio=0.10 #表示拿出10%的数据作为测试集
dataMat,labelVec=file2matrix(r'datingTestSet2.txt') #调用成员函数入读入文件中的数据,形成特征矩阵和标签向量
newMat,ranges,minVals=autoNorm(dataMat) #调用成员函数对特征矩阵归一化
m=newMat.shape[0] #获得样本特征集的行数m
numTestVecs=int(m*hoRatio) #用这个比例计算测试集的样本数目numTestVecs
errorCount=0.0 #已经分类错误的样本数目,用小数是为了后面作除法得错误率方便
#对测试集的每个条目(样本集中前numTestVecs个样本)而言
for i in range(numTestVecs):
#分类结果标签=KJL成员函数返回值
#(前面的特征作测试集,后面的样本作训练集,k=3)
classifierResult=KJL(newMat[i,:],\
newMat[numTestVecs:m,:],\
labelVec[numTestVecs:m],3)
#每次测试说明测试集给出的标签和真实标签分别是什么
print "分类器给出的标签: %d,真实标签: %d"\
%(classifierResult,labelVec[i])
#如果不同表示学习机器给出的分类结果错误
if(classifierResult!=labelVec[i]):
errorCount+=1.0 #那么分类错误数目加上1
#最后打印出测试错误率是多少
print "测试错误率是: %f"%(errorCount/float(numTestVecs))
#真正拿新样本做出预测的函数
def classifyPerson():
#结果集,训练集标签向量里存的标签是1,2,3,这里是其内在含义
resultList=['不怎么好','比较一般','较为不错']
#提示输入0号特征
ffMiles=float(raw_input("每年获得的飞行常客里程数?"))
#提示输入1号特征
percentTats=float(raw_input("玩视频游戏所消耗时间百分比?"))
#提示输入2号特征
iceCream=float(raw_input("每周消费的冰淇淋公斤数?"))
dM,lV=file2matrix(r'datingTestSet2.txt') #读入训练集(特征矩阵,标签向量)
newMat,ranges,minVals=autoNorm(dM) #对训练集特征矩阵做归一化
inArr=array([ffMiles,percentTats,iceCream]) #把输入的特征组合到一行
#分类结果标签=KJL成员函数返回值
#注意传入的特征向量要用各特征最小值minVals和各特征范围ranges归一化
#这就是它们也要被autoNorm训练集归一化函数返回的原因
classifierResult=KJL((inArr-minVals)/ranges,\
newMat,lV,3) #把训练集全部传入,做k=3的近邻算法
print "分类结果:",resultList[classifierResult-1] #KJL返回的标签号和list下标有减1的关系
运行结果
还是把写好的模块和数据集放到一个目录下,然后cmd进入那个目录打开python,把模块import进来,直接调用里面的成员方法就可以用了。
①获得样本集数据
数据存放在txt文件中,读进矩阵以后,用切片操作对每一行的记录进行切片,从而得到特征矩阵和标签向量,也就是得到了可使用的样本集。
②数据归一化
因为在k-近邻算法中,Lp距离总是要用到值之间的差来做计算,比如欧氏距离,对于不同类型的特征,差值总是有大有小的,这样做k-近邻算法时对距离的贡献是不一样的,如果我们认定这些特征同等重要,应该尽量保证它们对距离的贡献一致,归一化可以缓解这个问题(我个人认为不能完全解决这个问题,因为即便归一化以后,差值出现的概率密度还是不一样的)。
这里将各个特征都归一化到0~1之间,返回归一化以后的特征矩阵,以及各个特征的最小值向量和各个特征的范围向量,这两个向量看起来是冗余的,实际上对于外来样本,也要做归一化,这个时候就需要用到它们了!
③画样本集的散点图
关于画散点图,书上给出了一个非常巧妙的方法。因为scatter的第3和第4个参数指定点的颜色和大小,而我们要把相同标签的点画成同一个颜色去观察,并且样本集里的标签完全是123这样的数字,所以可以把这个数值乘以一个放大的倍数,从而把不同类之间的差异放大,之间传给scatter的这两个参数上,从而画出不同样式的点,又能保证同类的点样式一致!
我把这个过程封装成了一个函数放进了这个模块里,因为特征有很多,在二维图上每次只能展示两个特征的关系,所以设定了两个参数i和j,传入它们以决定要以哪个特征为横轴,哪个特征为纵轴去观察数据集。
这里,我用刚刚归一化后的特征矩阵来画散点图观察:
④对外来样本作分类预测
这个是整个算法要实现的目标功能,已经封装好了,不用去像前面那样手动读样本集/归一化等,不过没有封装画图。
对于输入的外来样本的三个特征,能够使用k(里面设置成k=3)近邻算法得到预测的标签。