分类器并不会得到百分百正确的结果。检验分类器给出的结果是否符合预期的结果,我们可以用错误率来进行评估。
上一节实验的分类器并没有太大的实际作用,我们将在现实世界中实现k-近邻算法。首先我们会使用k-近邻算法改进约会网站的效果,然后使用k-近邻算法改进手写识别系统。
实例:在约会网站上使用KNN
(1)收集数据:提供文本文件
(2)准备数据:使用python解析文本文件
(3)分析数据:使用matplotlib画二维扩散图
(4)训练算法:不适用于kNN
(5)测试算法:使用提供的部分数据作为测试样本
(6)使用算法:产生简单的命令行程序,然后可以输入一些特征来判断对方是否为自己喜欢的类型。
1、从文本文件中解析数据
1000行数据,包含以下三种特征:
在将上述特征输入分类器之前,必须将接待处理数据的格式改变为分类器可接受的格式。在KNN.py中创建名为file2matrix的函数,用来处理输入的格式问题,该函数的输入为文件名字字符串,输出为训练样本矩阵和类标签向量。
将下列代码加入KNN.
def file2matrix(filename): fr = open(filename) arrayOLines = fr.readlines() #得到文件行数 numberOflines = len(arrayOLines) #生成一个文件行数*3的全零矩阵 用于返回的Numpy矩阵(实际上是一个二维数组) returnMat = zeros((numberOflines,3)) classLabelVector = [] index = 0 for line in arrayOLines: #截取所有的回车字符 line = line.strip() """ split()通过指定分隔符对字符串进行切片,如果参数num 有指定值,则仅分隔 num 个子字符串 split()方法语法: str.split(str="", num=string.count(str)) 参数 str -- 分隔符,默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等。 num -- 分割次数。 """ #将正行数据分割成元素列表 listFromLine = line.split('\t') #前三个元素存储到特征矩阵中 returnMat[index,:] = listFromLine[0:3] #最后一列元素存储到向量中,元素值必须为整型,否则python会将这些元素当作字符串处理 classLabelVector.append(int(listFromLine[-1])) index+=1 return returnMat,classLabelVector
使用上述函数读取文件数据。 ps:由于原样本的标签是英文单词,读取时会报错,将标签改成相应的1,2,3即可。
下面我们将采用图像化的方法更加直观的看待这些数据,以便辨识出一些数据模式。
2、分析数据:使用matplotlib创建散点图:
输出效果如上图所示,散点图的矩阵横纵坐标为datingDataMat矩阵的第二,第三列数据分别表征“玩视频游戏所消耗的时间百分比”和“每周消费冰欺凌公升数”。
但是由于没有使用样本分类的特征值,我们很难从图中看到任何有意义的数据模式信息。一般我们会用不同的颜色表征不同的样本分类。matplotlib提供的scatter函数支持个性化标记散点图上的点。
在调用scatter函数时使用下列参数:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels),15.0*array(datingLabels))
plt.show()
用颜色和尺寸标识了数据点的属性类别。但是依然很难从这张图中得到结论性的信息。
但是采用不同的属性值可以得到更好的效果,图中标识了三个不同的样本分类区域,具有不同爱好的人其类别区域也不同。
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:,0],datingDataMat[:,1],15.0*array(datingLabels),15.0*array(datingLabels))
plt.xlabel('每年获取的飞行常客里程数')
plt.ylabel('玩游戏所耗时间的百分比')
plt.show()
图中展示的两个特征更容易区分数据点从属的类别
3、准备数据:归一化数值
四组数据,如果想要计算样本3和样本4之间的距离,可以是使用下面的公式:
上式中,数字差值最大的属性对计算结果的影响最大,也就是说每年获取的飞行常客里程数对于计算结果的影响将远远大于其他两个特征的影响。产生这种情况的原因是仅仅是因为飞行常客里程数远大于其他的两个特征值。但是我们认为这三个特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重的影响到计算结果。
在处理这种不同范围的特征值时,我们通常采用的方法是将数值归一化。将数值的取值范围处理为0到1或则-1之间。使用下面的公式将任意取值的特征值转化到0-1之间:
我们在KNN.py中增加一个新函数autoNorm(),该函数执行归一化过程:
def autoNorm(dataSet): #min(0)每列的最小值 min(0)每行的最小值 minVals = dataSet.min(0) maxVals = dataSet.max(0) ranges = maxVals - minVals #此时 minVals maxVals为1*3矩阵 #normDataSet为1000*3的0矩阵 normDataSet = zeros(shape(dataSet)) #取出训练集的行数m m = dataSet.shape[0] normDataSet = dataSet - tile(minVals,(m,1)) #特征值相除 在numpy中矩阵除法:linalg.solve(matA,matB) normDataSet = normDataSet/tile(ranges,(m,1)) return normDataSet,ranges,minVals
返回normMat矩阵,我们将在下一节使用取值范围和最小值归一化测试数据。
4、测试算法:作为完整程序验证分类器
使用已有数据的90%作为训练样本来训练分类器,使用其余的10%数据去测试分类器,检验正确率。由于我们的数据并没有按照特定的目的来排序,所以我们只用随机抽取10%即可。
我们定义一个计数器变量,每次分类器错分类数据,计数器加1,程序执行完成之后计数器的结果除以数据点总数即是错误率。
在KNN.py中创建函数datingClassTest
def datingClassTest(): hoRatio = 0.10 datingDataMat,datingLabels = file2matrix('datingTestSet2.txt') norMat, ranges, minVals = autoNorm(datingDataMat) m = norMat.shape[0] numTestVecs = int(m*hoRatio) errorCount = 0.0 for i in range(numTestVecs): classifierResult = classify0(norMat[i,:],norMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3) print("分类器的分类结果:%d,真实的类别是:%d"%(classifierResult,datingLabels[i])) if classifierResult != datingLabels[i]: errorCount += 1.0 print ("错误率是%f"%(errorCount/float(numTestVecs))) print(errorCount)
执行分类器测试程序,我们得到下面的输出结果:
...
我们可以看到,我们这次测试错误率仅为5%。说明我们是可以正确的预测分类的