上一篇我们已经初步接触到了K-近邻算法,这次我们用一个案例来进一步加深我们对算法的理解以及数据准备过程中,明白数据处理的重要性。
我的朋友海伦一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的
人选,但她没有从中找到喜欢的人。经过一番总结,她发现曾交往过三种类型的人:
·不喜欢的人
·魅力一般的人
·极具魅力的人
尽管发现了上述规律,但海伦依然无法将约会网站推荐的匹配对象归人恰当的分类。她觉得可以在周一到周五约会那些魅力一般的人,而周末则更喜欢与那些极具魅力的人为伴。海伦希望我们的分类软件可以更好地帮助她将匹配对象划分到确切的分类中。此外海伦还收集了一些约会网站未曾记录的数据信息,她认为这些数据更有助于匹配对象的归类。
海伦收集约会数据巳经有了一段时间,她把这些数据存放在datingTestSet.txt。
没有数据集的小伙伴访问下面网站,点击右侧的“随书下载”进行下载
http://www.ituring.com.cn/book/1021
海伦的样本主要包含以下3种特征:
每年获得的飞行常客里程数
玩视频游戏所耗时间百分比
每周消费的冰淇淋公升数
(1)收集数据:提供文本文件。
(2)准备数据: 使用python解析文本文件。
(3)分析数据:使用matplotlib画二维扩散图。
(4)训练算法:此步驟不适用于k-近邻算法。
(5)测试算法:使用海伦提供的部分数据作为测试样本。
测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
(6)使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否 为自己喜欢的类型。
import operator
from numpy import *
from os import listdir
import matplotlib
import matplotlib.pylab as plt
该部分函数已经在上一篇文章提到了过了
# 计算函数
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
diffMat = tile(inX, (dataSetSize,1)) - dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances**0.5
sortedDistIndicies = distances.argsort()
classCount={}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
在将特征数据输人到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式。
创建名为file2matrix的函数,以此来处理输人格式问题。
该函数的输人为文件名字符串,输出为训练样本矩阵和类标签向量。
# 1.将文本记录转换到numpy的解析程序
def file2matrix(filename):
# python 自带的open函数 打开我们要解析的数据集
fr = open(filename) # <_io.TextIOWrapper name='datingTestSet.txt' mode='r' encoding='cp936'>
numberOfLines = len(fr.readlines()) # 得到文件行数
# ['40920\t8.326976\t0.953952\tlargeDoses\n', '14488\t7.153469\t1.673904\tsmallDoses\n',.......]
# 该数据集有 三个特征值 一个目标向量
returnMat = zeros((numberOfLines, 3)) # numpy.zeros():创建一个和文件同行数 3列的矩阵(元素为0)
# >>> np.zeros((2, 1))
# array([[ 0.],
# [ 0.]])
classLabelVector = []
index = 0
fr = open(filename) # 这里要注意 我们再一次打开了文件
# 原因是文件再打开后被使用了一次后自动关闭了 导致我们后面继续读取文件时 读取不到信息 就无法正常操作了
for line in fr.readlines(): # 解析文件数据到列表
line = line.strip() # strip():移除字符串头尾指定的字符序列 无参数为空格和回车字符
# str = "123abcrunoob321"
# print (str.strip( '12' )) # 字符序列为 12
# 首尾的12 都被移除了 顺序不重要
listFromLine = line.split('\t') # split 切分数据 返回值 列表
print("\t分割后的列表:", listFromLine) # 分割后的列表: ['40920', '8.326976', '0.953952', 'largeDoses']
returnMat[index, :] = listFromLine[0:3] # 提取前三个特征值赋值给矩阵
print("第", index, "行得到的列表", returnMat[index, :]) # 第 0 行得到的列表 [4.092000e+04 8.326976e+00 9.539520e-01]
classLabelVector.append(listFromLine[-1]) # 提取位于尾末的目标向量(标签信息)
index += 1
return returnMat, classLabelVector
# 2.分析数据 散点图函数封装
# 根据画出的散点图 我们可以自己直观看到 喜欢程度的分布
def printData(data):
# 我们用散点图 直观显示 特征值的数据分布 三个特征 我们可以画三个子图 这里我们就用一个作案例了
# 用matplotlib创建大图fig 设置字体 黑体
plt.rcParams['font.sans-serif'] = ['Simhei']
fig = plt.figure()
# 定义ax为大图中的子图 111:一行一列第一个
ax = fig.add_subplot(111)
# 画ax子图 坐标轴为特征值列表 索引为1和2的特征
plt.scatter(data[:, 1], data[:, 2])
plt.xlabel("玩游戏所占时间")
plt.ylabel("每周消费冰激凌数")
plt.show()
在数据集当中我们发现乘坐飞机里程数数据远大于其他两个特征值
在计算距离的过程中 该数据的计算结果严重影响到了其他两个特征值的权重
为了使三特征值的权重平等 所以我们必须要进行处理
在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围 处理为0到1或者-1到1之间。
下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:
newValue = {oldValue-min)/ (max-min)
其中max和min分别是数据集中的最小特征值和最大特征值。
虽然改变数值取值范围增加了分类器的复杂度,但为了得到准确结果,我们必须这样做。
# 3.数据归一化处理
def autoNormal(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))
# tile(a,reps): a :要复制的值 reps:复制的次数 本次案例中 (m,1) m:复制m行 1:复制1列
normDataSet = normDataSet / tile(ranges, (m, 1)) # element wise divide
return normDataSet, ranges, minVals
前面我们巳经提到可以使用错误率来检测分类器的性能。对于分类器来说,错误率就是分类 器给出错误结果的次数除以测试数据的总数,完美分类器的错误率为0 ,而错误率为1.0的分类器 不会给出任何正确的分类结果。代码里我们定义一个计数器变量,每次分类器错误地分类数据,计数器就加1, 程序执行完成之后计数器的结果除以数据点总数即是错误率。
# 测试分类器
def datingClassTest(filename):
hoRatio = 0.1 # 随机取数据集的10% 海伦提供的数据集本身是无序的 所以这里直接取10%
datingDataMat,datingLabels = file2matrix(filename) # 加载原始数据
# 查看散点图
printData(datingDataMat)
normMat, ranges, minVals = autoNormal(datingDataMat) # 加载归一化数据
m = normMat.shape[0]
numTestVecs = int(m*hoRatio) # 取得10%数据
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: %s, the real answer is: %s" % (classifierResult, datingLabels[i]))
# the classifier came back with: largeDoses, the real answer is: largeDoses
# the classifier came back with: smallDoses, the real answer is: smallDoses
# the classifier came back with: didntLike, the real answer is: didntLike
if (classifierResult != datingLabels[i]): errorCount += 1.0
print("the total error rate is: %d" % ((errorCount/float(numTestVecs))*100), "%")
print(errorCount)
# the total error rate is: 5 %
# 5.0
if __name__ == '__main__':
datingClassTest("datingTestSet.txt")
测试结果错误率为5%,也就是我们有95%的正确率,看起来还不错!
我们可以改变函数datingClassTest内变量hoRatio和变量K的值,检测错误率是否随着变量值的变化而增加。依 赖于分类算法、数据集和程序设置,分类器的输出结果可能有很大的不同。
有兴趣的小伙伴可以自己创造一些数据 来测试一下自己会不会得到海伦的芳心哦