机器学习实战(3)—— kNN实战约会网站
老板:小韩啊,别忘了去改进一下约会网站的配对效果。
我:好嘞好嘞!马上工作!!
好了,又要开始一天的工作啦。接着上篇文章老板布置的任务,我们来看一下这次实战的相关信息。
前言
老板的朋友,卡特琳娜一直在使用约会网站寻找适合自己的约会对象。尽管约会网站会给她推荐不同的人选,但她并不是喜欢每一个人。经过一番总结,她发现她曾经交往过三种类型的人:
- 不喜欢的人
- 魅力一般的人
- 极具魅力的人
尽管发现了这样的规律,但是卡特琳娜还是没办法及那个网站推荐的人分类,这可把她给愁坏了!所以,她找到我们,希望我们可以写一个分类软件,来帮助她将匹配对象划分到确切的分类中。
此外,她还手机了一些约会网站未曾记录的信息,她认为这能过够帮到我们!
示例:在约会网站上使用k-近邻算法
(1) 收集数据:卡特琳娜提供的文本文件。
(2) 准备数据:使用Python解析文本文件。
(3) 分析数据:使用Matplotlib画二维扩散图。
(4) 训练算法:此步骤不适用于k-近邻算法。
(5) 测试算法:使用卡特琳娜提供的部分数据作为测试样本。 测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
(6) 使用算法:产生简单的命令行程序,然后卡特琳娜可以输入一些特征数据以判断对方是否为自己喜欢的类型。
2.2.1 准备数据:从文本文件中解析数据
我们需要进行的第一步就是数据解析。我们必须弄明白我们的数据以什么形式存储的,数据中的每一项都代表什么含义。
卡特琳娜提供给我们一个txt文件,叫做datingSet.txt,其中每个样本占一行,总共有1000行。每个样本主要包括以下三个特征:
- 每年获得的飞行常客里程数
- 玩视频游戏所耗时间百分比
- 每周消费的冰淇淋公升数
也就是下面这样的形式:
数据集形式在将上述特征数据输入到分类器之前,必须先进行数据处理,将待处理数据的格式改变为分类器可以接受的格式。接着我们上次写过的kNN.py文件,我们在这个文件中创建名为file2matrix的函数,以此来处理输入格式问题。
该函数的输入为文件名字符串,输出为训练样本矩阵和类标签向量。
def file2matrix(filename):
# 1.得到文件行数
fr = open(filename)
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines)
# 2.创建返回的NumPy矩阵
returnMat = zeros((numberOfLines, 3))
classLabelVector = []
index = 0
# 3.解析文件数据到列表
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命令提示符下输入下面的命令:
>>> reload(kNN)
>>> datingDataMat, datingLabels = kNN.file2matrix('datingSet.txt')
注意:因为书上使用的python2,所以在这里直接使用reload即可。
但是对于 python版本 <= Python 3.3,则需要使用以下命令:
import imp imp.reload(kNN)
对于python版本 >= Python 3.4,则需要使用以下命令:
import importlib importlib.reload(sys)
好了,在输入命令的时候,还有下面两点需要注意:
- 这样读文件的时候,我们要注意:datingSet.txt要存储在我们的工作目录中。
- 在执行这个函数之前,我们需要重新加载kNN.py模块,以确保新的内容可以生效。
在不报错的情况下,我们看一下我们生成的数据:
>>> datingDataMat
array([[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]])
>>> datingLabels
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, ......]
我们现在已经从文本文件中导入了数据,并将其转换成了我们需要的格式。但是这样有一个问题,我们没办法直观地看出数据的含义,所以我们接下来就使用Python工具来图形化展示数据内容。
注意:在分析数据的时候,图形化是一个非常好用的方法。Python中的Matplotlib模块为我们提供了这样的功能。
2.2.2 分析数据:使用Matplotlib创建散点图
这里我们使用Matplotlib来制作原始数据的散点图,我本人也是刚入手机器学习和Python语言,所以对于这个库的使用还不是很了解,后期我也会慢慢出一个Matplotlib的教程,最好能够以通俗的语言来为大家讲明白。
(给自己立的flag,哭着也要让它站稳)
好了,我们继续在上面的Python命令行环境中,输入以下命令:
>>> import matplotlib
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:, 2])
<matplotlib.collections.PathCollection object at 0x00000111E23668D0>
>>> plt.show()
输出效果如下图所示(我是使用Pycharm下的终端输出的,输入命令的时候有些粗心,大家千万不要向我这样):
matlpotlib展示数据这样看,生成的图片虽然能看,但是我们不懂它是什么含义,这我们就得从源码入手了。
datingDataMat[:, 1] 这个代表的是datingDataMat矩阵的第二列数据,代表特征‘玩视频游戏所耗时间百分比’
datingDataMat[:, 2] 这个代表的是datingDataMat矩阵的第三列数据,代表特征‘每周所消费的冰淇淋公升数’
然后,我们看看ax.scatter()这个函数,其原型是:
scatter(x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, edgecolors=None, hold=None, data=None, **kwargs)
暂且不看后面的参数(后面的教程会详细介绍),我们看到,一个x代表横轴,一个y代表纵轴。
好了,这样我们就可以知道这个图的基本含义了,用书上的图表示如下:
图2-3.png由于我们没有使用样本分类的特征值,我们很难从上面的图中看出有用的信息来。一般来说,我们会采用色彩或其他的记号来标记不同样本分类,以便更好地理解数据信息。
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))
<matplotlib.collections.PathCollection object at 0x00000111E41CC9B0>
>>> plt.show()
我们就是利用了变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等,尺寸不同的点,输出效果如下:
matplotlib展示数据彩色这样,我们就可以基本上看到数据点所属三个样本分类的区域轮廓。
2.2.3 准备数据:归一化数值
为什么要归一化数值?这个问题我们慢慢来解决!
我们先来看一组数据:
玩视频游戏所耗时间百分比 | 每年获得的飞行常客里程数 | 每周消费的冰淇淋公升数 | 样本分类 | |
---|---|---|---|---|
1 | 0.8 | 400 | 0.5 | 1 |
2 | 12 | 134000 | 0.9 | 3 |
3 | 0 | 20000 | 1.1 | 2 |
4 | 67 | 32000 | 1 | 2 |
计算以下样本3和样本4之间的距离,如下:
我们很容易发现,上面方程中数字差值最大的属性对计算结果的影响最大。也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于其他两个特征——玩视频游戏所耗时间百分比和每周消费冰淇淋公升数——的影响。而产生这种现象的唯一原因,仅仅是因为飞行常客里程数远大于其他特征值。
但卡特琳娜认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。
所以,在处理这种不同取值范围的特征值时,我们通常就需要采用归一化,如将取值范围处理为0到1或者-1到1之间。下面的公式可以将任意取值范围的特征值转化为0-到1区间的值:
其中min和max分别是数据集中当前特征的最小值和最大值。虽然改变数值取值范围增加了分类器的复杂度,但为了得到准确结果,我们必须这样做。
好了,理论讲了一大堆,我们还是来写代码。继续在kNN.py中编写代码,创建一个函数autoNorm()。
def autoNum(dataSet):
minVals = dataSet.min(0) # 存储每列的最小值,参数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
上面的代码,有一点需要我们注意:特征值矩阵有1000×3个值,而minVals和ranges的值都为1×3。为了解决这个问题,我们使用NumPy库中tile()函数将变量内容复制成输入矩阵同样大小的矩阵,注意这是具体特征值相除 ,而 对于某些数值处理软件包,/可能意味着矩阵除法。
好了,写完之后,我们继续在Pyhon命令提示符下,重新载入kNN.py模块,执行autoNorm函数:
>>> reload(kNN)
>>> normMat, ranges, minVals = kNN.autoNorm(datingDataMat)
>>> normMat
array([[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]])
>>> ranges
array([9.1273000e+04, 2.0919349e+01, 1.6943610e+00])
>>> minVals
array([0. , 0. , 0.001156])
这里我们也可以只返回normMat矩阵,但是下一节我们需要将取值范围和最小值归一化测试数据。
2.2.4 测试算法:作为完整程序验证分类器
老板:小韩啊,写的怎么样了啊?
我:数据可视化,数据处理以及数据归一化我都做完了,接下来,把数据丢到昨天写的分类器就能看到效果了!!
老板:不错不错!继续努力啊!年终奖到时候少不了你的!
我:(美滋滋)没问题,你就放100个心吧。
好了,总算是处理完数据了。接下来我们就可以去看一下我们的分类器效果怎么样了,想想就很开心呢!,如果分类器的正确率满足要求,卡特琳娜就可以使用这个软件来处理约会网站提供的约会名单了!!!
机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器,而使用其余的10%数据去测试分类器,检测分类器的正确率。需要注意的是,10%的测试数据应该是随机选择的,由于卡特琳娜提供的数据并没有按照特定目的来排序,所以我们可以随意选择10%数据而不影响其随机性。
话不多说,直接上代码,我们还是在kNN.py中继续编写:
def datingClassTest():
hoRatio = 0.10
datingDataMat, datingLabels = file2matrix('datingTestSet2.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)))
其实,代码流程还是很简单的。
- 首先使用了file2matrix和autoNorm()函数从文件中读取数据并将其转换为归一化特征值;
- 接着计算测试向量的数量,此步决定了normMat向量中哪些数据用于测试,哪些数据用于分类器的训练样本;
- 然后将这两部分数据输入到原始kNN分类器函数classify0。最后,函数计算错误率并输出结果。
写完之后,我们直接在我们的Python命令提示符下从在kNN.py模块,然后输入kNN.datingClassTest(),执行分类测试程序,我们可以得到以下结果:
约会网站测试.pngYes Yes Yes!!! 效果还是不错的,只有5%的错误率!!!当然,我们也可以改变函数 datingClassTest内变量hoRatio和变量k的值,检测错误率是否随着变量值的变化而增加。不同的参数,分类器的性能会有很大不同的。
老板:哎呦,不错哦!只有5%的错误率
我:那是必须的!
老板:那你在改改,我们就可以给卡特琳娜用了!!
我:没问题,您就等着给我加鸡腿吧!!
2.2.5 使用算法:构建完整可用系统
好了,程序员测试完了,准备发布吧!!!
这里我们就使用这个简答的分类器来帮助卡特琳娜对网站给出的候选人进行分类!!!
def classifyPerson():
resultList = ['not at all', 'in small doses', 'in large doses']
percentTats = float(input('percentage of time spent playing video games?'))
ffMiles = float(input('frequent flier miles earned per year?'))
iceCream = float(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(),这是Python 2的函数,Python3就直接使用input就行。
我们直接看结果:
完整结果.png总算是写完了,虽然都是黑乎乎的控制台程序,但是算法的核心及应用方法我们都会了,前端的包装就不是我们的锅了!!!hiahiahia!!!
最后,还是熟悉的配方!
欢迎大家关注我的公众号,有什么问题也可以给我留言哦!
微信公众号.jpg