2.2 示例:使用k-近邻算法改进约会网站的配对效果
我的朋友海伦一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但她没有从中找到喜欢的人。经过一番总结,他发现曾交往过三种类型的人:
- 不喜欢的人
- 魅力一般的人
- 极具魅力的人
尽管发现了上述规律,但海伦依然无法将约会网站推荐的匹配对象归入恰当的分类。她觉得可以在周一到周五约会哪些魅力一般的人,而周末则更喜欢与那些极具魅力的人为伴。海伦希望我们的分类软件可以更好的帮助她将匹配对象划分到确切的分类中。此外海伦还收集了一些约会网站未曾记录的数据信息,她认为这些数据更助于匹配对象归类。
示例:在约会网站上使用k-近邻算法
(1)收集数据:提供文本文件。
(2)准备数据:使用Python解析文本文件
(3)分析数据:使用Matplotlib画二维扩散图
(4)训练算法:此步骤不适用k-近邻算法
(5)测试算法:使用海伦提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
(6)使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。
2.2.1 准备数据:从文本文件中解析数据
海伦约会已经有一段时间了,她把这些数据存放在文本文件datingTestSet.txt中,每个样本数据占一行,总共有1000行。海伦的样本主要包含以下3中特征:
- 每年获得的飞行常客里程数
- 玩视频游戏所耗时间百分比
- 每周消费冰淇淋公升数
在将上述特征数据输入到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式。在kNN.py中创建名为file2matrix的函数,以此来处理输入格式问题。该函数的输入为文件字符串名,输出为训练样本矩阵和类标签向量。
将下面代码增加到kNN.py中。
程序清单2-2 将文本记录到转换Numpy的解析程序
def file2matrix(filename):
fr=open(filename)
arrayOLines=fr.readlines()
numberOfLines=len(arrayOLines) #1得到文件行数
returnMat=zeros((numberOfLines,3)) #2创建返回的NumPy矩阵zeros(shape,dtype=float)指定数据类型的数组,元素值为0
classLabelVector=[]
index=0
#3解析文件数据到列表
for line in arrayOLines:
line=line.strip()#strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
#该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。
listFromLine=line.split('\t')#split()通过指定分隔符对字符串进行切片
returnMat[index,:]=listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index+=1
return returnMat,classLabelVector
在Pyrhon命令提示符下输入下面命令:
datingDatMat,datingLabels=file2matrix('datingTestSet2.txt')
使用函数file2matrix读取文件数据,必须确保文件datingTestSet2.txt存储在我们的工作目录 中。成功导入datingTestSet2.txt文件中的数据之后,可以简单检查一下数据内容。Python中的的输出结 果大致如下:
print(datingDatMat)
输出
[[4.0920000e+04 8.3269760e+00 9.5395200e-01]
[1.4488000e+04 7.1534690e+00 1.6739040e+00]
[2.6052000e+04 1.4418710e+00 8.0512400e-01]
...
[2.6575000e+04 1.0650102e+01 8.6662700e-01]
[4.8111000e+04 9.1345280e+00 7.2804500e-01]
[4.3757000e+04 7.8826010e+00 1.3324460e+00]]
print(datingLabels[0:20])
输出
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3]
现在已经从文本文件中导入了数据,并将其格式化为想要的格式,接着我们需要了解数据的真实含义。当然我们可以直接浏览文本文件,但是这种方法非常不友好,一般来说,我们会采用图形化的方式直接的展示数据。下面就用Python工具来图形化展示数据内容,以便辨识出一些数据模式。
NumPy数组和Python数组
我们将大量使用NumPy数组,你既可以直接在Python命令行环境中输入from numpy import array将其导入,也可以通过直接导入所有NumPy库内容来将其导入。由于NumPy库提供的数组操作并不支持Python自带的数组类型,因此在编写代码时要注意不要使用错误的数组类型。
2.2.2 分析数据:使用Matplotlib创建散点图
首先我们使用Matplotlib制作原始数据的散点图,在Python命令行环境中,输入下列命令:
import matplotlib
import matplotlib.pyplot as plt
fig=plt.figure()#创建一个新的figure对象,创建一个新画布
ax=fig.add_subplot(111)#将画布分为1行1列,area为从左往右从上往下第一块
plt.scatter(datingDatMat[:,1],datingDatMat[:,2])
plt.xlabel('每周消费的冰淇淋公升数',fontProperties='SimHei')#设置横坐标的名称,字体为黑体
plt.ylabel('玩视频游戏所消耗时间百分比',fontProperties='SimHei')#设置纵坐标的名称,字体为黑体
plt.show()
输出效果如下图所示。散点图使用datingDataMat矩阵的第二、第三列数据,分别表示特征值“玩视频游戏所消耗时间百分比”和“每周消费的冰淇淋公升数”。
图2-3 没有样本类别标签的约会数据散点图。难以辨识图中的点究竟属于哪个样本分类
由于没有使用样本分类的特征值,我们很难从图2-3中看到任何有用的数据模式信息。一般来说我们使用色彩或其他的记号来标记不同样本分类,以便更好的理解数据信息。Matplotlib库提供的scatter函数支持个性化标记散点图上的点。重新输入上面的代码,调用scatter函数是使用下列参数:
plt.scatter(datingDatMat[:,1],datingDatMat[:,2],15.0*array(datingLabels),15.0*array(datingLabels))
上述代码利用变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等,尺寸不同的点。你可以看到一个与图2-3类似的散点图。从图2-3中,我们很难看到任何有用的信息,然而由于图2-4利用颜色及尺寸标识了数据点的属性类别,因而我们基本上可以从图2-4中看到数据点所属三个样本分类的区域轮廓。
图2-4 带有样本分类标签的约会数据散点图。虽然能够比较容易地区分数据点丛书类别,但依然很难根据这张图得出结论性信息
本节我们学习了如何使用Matplotlib库图形化展示数据,图2-4使用了矩阵属性列0和1展示数据,虽然可以区别,但图2-5采用不同的属性可以得到更好地效果,图中清晰的表示了三个不同的样本分类区域,具有不同爱好的人其类别区域也不同。
图2-5 每年赢得的飞行常客里程数与玩视频游戏所占的百分比的约会散点图。约会数据有三个特征,通过图中展示的两个特征更容易区分数据点从属的类别
2.2.3 准备数据:归一化数值
表2-3给出了提取的四组数据,如果想要计算样本3和样本4之间的距离,可以使用下面的方法:
我们很容易发现,上面方程中数字差值最大的属性对计算结果影响最大,也就是说,每年获取的飞行常客里程数对计算结果的影响将远远大于表2-3其他两个特征——玩游戏的和每周消费冰淇淋公升数——的影响。而产生这种现象唯一原因,仅仅是因为飞行常客里程数远大于其他特征。但海伦认为这三种特征是同等重要的,因此作为三个等全的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。
在处理这种不同取值范围特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:
程序清单2-3提供了函数autoNorm()的代码
程序清单2-3 归一化特征值
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
在函数autoNorm()中,我们将每列的最小值放在变量minVals中,将最大值放在变量maxVals中,其中dataSet.min(0)中的参数0使得函数可以从列中选取最小值,而不是选取当前行的最小值。然后,函数计算可能的取值范围,并创建新的返回矩阵。正如前面给出的公式,为了归一化特征值,我们必须使用当前值减去最小值,然后除以取值范围。需要注意的是,特征值矩阵有1000x3个值,而minVals和range的值都为1x3。为了解决这个问题,我们使用NumPy库中的tile()函数将变量内容复制成输入矩阵同样大小的矩阵,注意这是具体特征值相除,而对于某些数值处理软件包,/可能意味着矩阵除法,但在NumPy库中,矩阵除法需要使用函数linalg.solve(matA,matB)。
在Python命令提示符下,重新加载kNN.py模块,执行autoNorm函数,检测函数的执行结果:
print(normMat)
输出
[[0.44832535 0.39805139 0.56233353]
[0.15873259 0.34195467 0.98724416]
[0.28542943 0.06892523 0.47449629]
...
[0.29115949 0.50910294 0.51079493]
[0.52711097 0.43665451 0.4290048 ]
[0.47940793 0.3768091 0.78571804]]
print(ranges)
输出
[9.1273000e+04 2.0919349e+01 1.6943610e+00]
print(minVals)
输出
[0. 0. 0.001156]