我的朋友海伦一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但她没有从中找到喜欢的人。经过一番总结,她发现曾交往过三种类型的人:
- 不喜欢的人
- 魅力一般的人
- 极具魅力的人
尽管发现了上述规律,但海伦依然无法将约会网站推荐的匹配对象归入恰当的类别。她觉得可以在周一到周五约会那些魅力一般的人,而周末则更喜欢与那些极具魅力的人为伴。海伦希望我们的分类软件可以更好地帮助她将匹配对象划分到确切的分类中。此外海伦还收集了一些约会网站未曾记录的数据信息,她认为这些数据更有助于匹配对象的归类。
示例:在约会网站上使用k近邻算法
- 收集数据:提供文本文件。
- 准备数据:使用Python解析文本文件。
- 分析数据:使用Matplotlib画二维扩散图。
- 训练算法:此步骤不适用于k近邻算法。
- 测试算法:使用海伦提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
- 使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。
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) #❶ 得到文件行数 returnMat = zeros((numberOfLines,3)) #❷ 创建返回的Numpy矩阵 classLabelVector = index = 0 #❸ (以下三行)解析文件数据到列表 for line in arrayOlines: line = line.strip listFromLine = line.split(/'t/') returnMat[index,:] = listFromLine[0:3] classLabelVector.append(int(listFromLine[-1])) index += 1 return returnMat,classLabelVector
从上面的代码可以看到,Python处理文本文件非常容易。首先我们需要知道文本文件包含多少行。打开文件,得到文件的行数❶。然后创建以零填充的矩阵NumPy❷(实际上,NumPy是一个二维数组,这里暂时不用考虑其用途)。为了简化处理,我们将该矩阵的另一维度设置为固定值3
,你可以按照自己的实际需求增加相应的代码以适应变化的输入值。循环处理文件中的每行数据❸,首先使用函数line.strip
截取掉所有的回车字符,然后使用tab字符t
将上一步得到的整行数据分割成一个元素列表。接着,我们选取前3个元素,将它们存储到特征矩阵中。Python语言可以使用索引值-1表示列表中的最后一列元素,利用这种负索引,我们可以很方便地将列表的最后一列存储到向量classLabelVector
中。需要注意的是,我们必须明确地通知解释器,告诉它列表中存储的元素值为整型,否则Python语言会将这些元素当作字符串处理。以前我们必须自己处理这些变量值类型问题,现在这些细节问题完全可以交给NumPy函数库来处理。
在Python命令提示符下输入下面命令:
>>> reload(kNN)>>> datingDataMat, datingLabels = kNN.file2matrix(/'datingTestSet2.txt/')
使用函数file2matrix
读取文件数据,必须确保文件datingTestSet.txt存储在我们的工作目录中。此外在执行这个函数之前,我们重新加载了kNN.py模块,以确保更新的内容可以生效,否则Python将继续使用上次加载的kNN模块。
成功导入datingTestSet.txt文件中的数据之后,可以简单检查一下数据内容。Python的输出结果大致如下:
>>> datingDataMatarray([[ 7.29170000e+04, 7.10627300e+00, 2.23600000e-01], [ 1.42830000e+04, 2.44186700e+00, 1.90838000e-01], [ 7.34750000e+04, 8.31018900e+00, 8.52795000e-01], ..., [ 1.24290000e+04, 4.43233100e+00, 9.24649000e-01], [ 2.52880000e+04, 1.31899030e+01, 1.05013800e+00], [ 4.91800000e+03, 3.01112400e+00, 1.90663000e-01]])>>> 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>>> ax = fig.add_subplot(111)>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2])>>> plt.show
输出效果如图2-3所示。散点图使用datingDataMat矩阵的第二、第三列数据,分别表示特征值“玩视频游戏所耗时间百分比”和“每周所消费的冰淇淋公升数”。
图2-3 没有样本类别标签的约会数据散点图。难以辨识图中的点究竟属于哪个样本分类
由于没有使用样本分类的特征值,我们很难从图2-3中看到任何有用的数据模式信息。一般来说,我们会采用色彩或其他的记号来标记不同样本分类,以便更好地理解数据信息。Matplotlib库提供的scatter
函数支持个性化标记散点图上的点。重新输入上面的代码,调用scatter
函数时使用下列参数:
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2],15.0*array(datingLabels), 15.0*array(datingLabels))
上述代码利用变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等、尺寸不同的点。你可以看到一个与图2-3类似的散点图。从图2-3中,我们很难看到任何有用的信息,然而由于图2-4利用颜色及尺寸标识了数据点的属性类别,因而我们基本上可以从图2-4中看到数据点所属三个样本分类的区域轮廓。
图2-4 带有样本分类标签的约会数据散点图。虽然能够比较容易地区分数据点从属类别,但依然很难根据这张图得出结论性信息
本节我们学习了如何使用Matplotlib库图形化展示数据,图2-4使用了datingDataMat矩阵的第二和第三列属性来展示数据,虽然也可以区分,但图2-5采用矩阵第一和第二列属性却可以得到更好的展示效果,图中清晰地标识了三个不同的样本分类区域,具有不同爱好的人其类别区域也不同。
图2-5 每年赢得的飞行常客里程数与玩视频游戏所占百分比的约会数据散点图。约会数据有三个特征,通过图中展示的两个特征更容易区分数据点从属的类别
2.2.3 准备数据:归一化数值
表2-3给出了提取的四组数据,如果想要计算样本3和样本4之间的距离,可以使用下面的方法:
我们很容易发现,上面方程中数字差值最大的属性对计算结果的影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于表2-3中其他两个特征——玩视频游戏的和每周消费冰淇淋公升数——的影响。而产生这种现象的唯一原因,仅仅是因为飞行常客里程数远大于其他特征值。但海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。
表2-3 约会网站原始数据改进之后的样本数据
在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:
newValue = (oldValue-min)/(max-min)
其中min
和max
分别是数据集中的最小特征值和最大特征值。虽然改变数值取值范围增加了分类器的复杂度,但为了得到准确结果,我们必须这样做。我们需要在文件kNN.py中增加一个新函数autoNorm
,该函数可以自动将数字特征值转化为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使得函数可以从列中选取最小值,而不是选取当前行的最小值。然后,函数计算可能的取值范围,并创建新的返回矩阵。正如前面给出的公式,为了归一化特征值,我们必须使用当前值减去最小值,然后除以取值范围。需要注意的是,特征值矩阵有1000 x 3个值,而minVals
和range
的值都为1 x 3。为了解决这个问题,我们使用NumPy库中tile
函数将变量内容复制成输入矩阵同样大小的矩阵,注意这是具体特征值相除❶,而对于某些数值处理软件包,/
可能意味着矩阵除法,但在NumPy库中,矩阵除法需要使用函数linalg.solve(matA,matB)
。
在Python命令提示符下,重新加载kNN.py模块,执行autoNorm
函数,检测函数的执行结果:
>>> reload(kNN)>>> normMat, ranges, minVals = kNN.autoNorm(datingDataMat)>>> normMatarray([[ 0.33060119, 0.58918886, 0.69043973], [ 0.49199139, 0.50262471, 0.13468257], [ 0.34858782, 0.68886842, 0.59540619], ..., [ 0.93077422, 0.52696233, 0.58885466], [ 0.76626481, 0.44109859, 0.88192528], [ 0.0975718 , 0.02096883, 0.02443895]])>>> rangesarray([ 8.78430000e+04, 2.02823930e+01, 1.69197100e+00])>>> minValsarray([ 0. , 0. , 0.001818])
这里我们也可以只返回normMat
矩阵,但是下一节我们将需要取值范围和最小值归一化测试数据。
2.2.4 测试算法:作为完整程序验证分类器
上节我们已经将数据按照需求做了处理,本节我们将测试分类器的效果,如果分类器的正确率满足要求,海伦就可以使用这个软件来处理约会网站提供的约会名单了。机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器,而使用其余的10%数据去测试分类器,检测分类器的正确率。本书后续章节还会介绍一些高级方法完成同样的任务,这里我们还是采用最原始的做法。需要注意的是,10%的测试数据应该是随机选择的,由于海伦提供的数据并没有按照特定目的来排序,所以我们可以随意选择10%数据而不影响其随机性。
前面我们已经提到可以使用错误率来检测分类器的性能。对于分类器来说,错误率就是分类器给出错误结果的次数除以测试数据的总数,完美分类器的错误率为0,而错误率为1.0的分类器不会给出任何正确的分类结果。代码里我们定义一个计数器变量,每次分类器错误地分类数据,计数器就加1,程序执行完成之后计数器的结果除以数据点总数即是错误率。
为了测试分类器效果,在kNN.py文件中创建函数datingClassTest
,该函数是自包含的,你可以在任何时候在Python运行环境中使用该函数测试分类器效果。在kNN.py文件中输入下面的程序代码。
程序清单2-4 分类器针对约会网站的测试代码
def datingClassTest: hoRatio = 0.10 datingDataMat,datingLabels = file2matrix(/'datingTestSet.txt/') normMat, ranges, minVals = autoNorm(datingDataMat) m = normMat.shape[0] numTestVecs = int(m*hoRatio) errorCount = 0.0 for i in range(numTestVecs): classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:], datingLabels[numTestVecs:m],3) print /"the classifier came back with: %d, the real answer is: %d/" % (classifierResult, datingLabels[i]) if (classifierResult != datingLabels[i]): errorCount += 1.0 print /"the total error rate is: %f/" % (errorCount/float(numTestVecs))
函数datingClassTest
如程序清单2.4所示,它首先使用了file2matrix
和autoNorm
函数从文件中读取数据并将其转换为归一化特征值。接着计算测试向量的数量,此步决定了normMat
向量中哪些数据用于测试,哪些数据用于分类器的训练样本;然后将这两部分数据输入到原始kNN分类器函数classify0
。最后,函数计算错误率并输出结果。注意此处我们使用原始分类器,本章花费了大量的篇幅在讲解如何处理数据,如何将数据改造为分类器可以使用的特征值。得到可靠的数据同样重要,本书后续的章节将介绍这个主题。
在Python命令提示符下重新加载kNN模块,并输入kNN.datingClassTest
,执行分类器测试程序,我们将得到下面的输出结果:
>>> kNN.datingClassTestthe classifier came back with: 1, the real answer is: 1 the classifier came back with: 2, the real answer is: 2..the classifier came back with: 1, the real answer is: 1 the classifier came back with: 2, the real answer is: 2 the classifier came back with: 3, the real answer is: 3 the classifier came back with: 3, the real answer is: 1 the classifier came back with: 2, the real answer is: 2 the total error rate is: 0.024000
分类器处理约会数据集的错误率是2.4%,这是一个相当不错的结果。我们可以改变函数datingClassTest
内变量hoRatio
和变量k
的值,检测错误率是否随着变量值的变化而增加。依赖于分类算法、数据集和程序设置,分类器的输出结果可能有很大的不同。
这个例子表明我们可以正确地预测分类,错误率仅仅是2.4%。海伦完全可以输入未知对象的属性信息,由分类软件来帮助她判定某一对象的可交往程度:讨厌、一般喜欢、非常喜欢。
2.2.5 使用算法:构建完整可用系统
上面我们已经在数据上对分类器进行了测试,现在终于可以使用这个分类器为海伦来对人们分类。我们会给海伦一小段程序,通过该程序海伦会在约会网站上找到某个人并输入他的信息。程序会给出她对对方喜欢程度的预测值。
将下列代码加入到kNN.py并重新载入kNN。
程序清单2-5 约会网站预测函数
def classifyPerson: resultList = [/'not at all/',/'in small doses/', /'in large doses/'] percentTats = float(raw_input( /"percentage of time spent playing video games?/")) ffMiles = float(raw_input(/"frequent flier miles earned per year?/")) iceCream = float(raw_input(/"liters of ice cream consumed per year?/")) datingDataMat,datingLabels = file2matrix(/'datingTestSet2.txt/') normMat, ranges, minVals = autoNorm(datingDataMat) inArr = array([ffMiles, percentTats, iceCream]) classifierResult = classify0((inArr- minVals)/ranges,normMat,datingLabels,3) print /"You will probably like this person: /", resultList[classifierResult - 1]
上述程序清单中的大部分代码我们在前面都见过。唯一新加入的代码是函数raw_input
。该函数允许用户输入文本行命令并返回用户所输入的内容。为了解程序的实际运行效果,输入如下命令:
>>> kNN.classifyPersonpercentage of time spent playing video games?10frequent flier miles earned per year?10000liters of ice cream consumed per year?0.5You will probably like this person: in small doses
目前为止,我们已经看到如何在数据上构建分类器。这里所有的数据让人看起来都很容易,但是如何在人不太容易看懂的数据上使用分类器呢?从下一节的例子中,我们会看到如何在二进制存储的图像数据上使用kNN。