k-近邻算法改进约会网站配对效果
一、理论学习
1. 阅读内容
请务必仔细阅读《机器学习实战》书籍第1章及第2章,本节实验通过解决约会网站配对效果问题来实战 k-近邻算法(k-Nearest Neighbour,KNN)
2. 扩展阅读
本节推荐内容可以辅助书中理论知识,比书中内容更容易理解,可以加深理论知识,请仔细阅读:
二、在线实验
1. 分析需求
我在约会网站看到的人,分为三类:
- 不喜欢的
- 一般喜欢的
- 非常喜欢的
我希望分类算法可以实现替我区分这三类人。
而现在我有一群人的下面的数据:
- 每年的飞行里程
- 玩视频游戏消耗时间的百分比
- 每周吃的冰淇淋(公升)
如何根据这些数据对这群人进行分类?这就是我的需求。
2. 准备数据
下载数据
下载实验所需资料:
$ cd /home/shiyanlou
$ wget http://labfile.oss.aliyuncs.com/courses/499/lab2.zip
$ unzip lab2.zip
将测试数据拷贝到我们自己的目录:
$ cd /home/shiyanlou
$ mkdir mylab2
$ cd mylab2/
$ cp /home/shiyanlou/lab2/datingTestSet2.txt ./
使用 gedit 或 vim 打开数据文件查看如下:
文件中每一行代表一个人的数据,共有4列内容,分别为:
- 每年的飞行里程
- 玩视频游戏消耗时间的百分比
- 每周吃的冰淇淋(公升)
- 分到的类别(1:不喜欢 2:一般喜欢 3:非常喜欢)
根据这些数据我们开始实现KNN算法。
我们需要打开一个 Xfce 终端,输入 ipython
进入到交互式模式来边写代码边测试。
解析数据
为了能让KNN算法可以处理我们的数据,需要将数据读取到矩阵中。
我们实现函数 file2matrix()
来做数据解析:
- 读取数据文件每一行
- 输出为特征矩阵和类标签向量
函数的实现如下,注意这里用到 numpy 来构建矩阵:
from numpy import *
def file2matrix(filename):
# 打开数据文件,读取每行内容
fr = open(filename)
arrayOLines = fr.readlines()
# 初始化矩阵
numberOfLines = len(arrayOLines)
returnMat = zeros((numberOfLines,3))
# 初始化类标签向量
classLabelVector = []
# 循环读取每一行数据
index = 0
for line in arrayOLines:
# 去掉回车符
line = line.strip()
# 提取4个数据项
listFromLine = line.split(‘\t‘)
# 将前三项数据存入矩阵
returnMat[index,:] = listFromLine[0:3]
# 将第四项数据存入向量
classLabelVector.append(int(listFromLine[-1]))
index += 1
return returnMat,classLabelVector
将上述代码输入到ipython中,注意python代码的缩进。
完成后我们测试下函数读取数据的返回值:
这个时候我们看到矩阵中第一项数据都是几千甚至上万,而第二和第三项数据则小很多,为了避免求取KNN距离时第一项影响过大,需要对数据进行第二步处理:归一化,将数值范围处理为0~1
或-1~1
之间。
实验中我们将用下面的公式将数值归一化到0~1
范围内:
newValue = (oldValue-min)/(max-min)
实现一个 autoNorm()
函数来完成数据的归一化,其中使用numpy的tile()
函数来将minVals
处理成与dataSet
同样大小的矩阵,从而可以执行矩阵减法,最后矩阵除法得到归一化后的矩阵:
def autoNorm(dataSet):
# 读取矩阵中数据项的最大和最小值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
# 获得最大和最小值间差值
ranges = maxVals - minVals
# 初始化输出结果
normDataSet = zeros(shape(dataSet))
# 获取矩阵的行数
m = dataSet.shape[0]
# 矩阵运算:实现归一化公式中的 oldValue - min 一步
normDataSet = dataSet - tile(minVals, (m,1))
# 矩阵除法:实现归一化公式中的除法
normDataSet = normDataSet/tile(ranges, (m,1))
# 返回归一化后的数据,数据范围及最小值矩阵
return normDataSet, ranges, minVals
完成后对刚才得到的数据进行归一化处理:
对于这些数据我们可以使用matplotlib
创建散点图来查看。
上述步骤创建的散点图显示的横轴是归一化后的玩视频游戏所耗时间的百分比,纵轴是每周消费冰淇淋数量数据。
最终画出来的图如下图所示:
3. 分析数据
KNN 算法我们在理论学习部分已经有所了解,本节内容将实现这个算法的核心部分:计算“距离”。
如何根据三个特征数据计算两个人之间的距离呢,假设归一化后两个人的数据分别为(0.3,0.5,0.3)
和(0.5,0.2,0.3)
使用一个简单的公式来计算二者间的距离:
(A1−B1)2+(A2−B2)2+(A3−B3)2\sqrt{(A1-B1)^2 + (A2-B2)^2 + (A3-B3)^2}√?(A1−B1)?2??+(A2−B2)?2??+(A3−B3)?2?????
得到这两点的距离为 0.3606。
当我们有一定的样本数据和这些数据所属的分类后,输入一个测试数据,我们就可以根据算法得出该测试数据属于哪个类别。
KNN 算法实现过程:
- 计算测试数据与每个样本数据之间的距离。
- 按照距离递增排序
- 选择与测试数据距离最近的k个点(注意这里的k值会影响到最终的分类错误率)
- 确定这k个点中三个类别出现的概率,出现概率最高的类别就是返回的结果
- 输出结果:算法判断该测试数据属于步骤4中计算得到的类别
算法实现为函数 classify0()
,函数的参数包括:
- inX:测试数据向量
- dataSet:样本数据矩阵
- labels:样本数据的类标签向量
- k:传说中的k值
import operator
def classify0(inX, dataSet, labels, k):
# 获取样本数据数量
dataSetSize = dataSet.shape[0]
# 矩阵运算,计算测试数据与每个样本数据对应数据项的差值
diffMat = tile(inX, (dataSetSize,1)) - dataSet
# sqDistances 上一步骤结果平方和
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.iteritems(), key=operator.itemgetter(1), reverse=True)
# 返回出现频次最高的类别
return sortedClassCount[0][0]
4. 测试算法
完成了对数据的分析后,我们开始测试算法的准确率。测试过程将调用我们上面实现的各个函数。
需要首先说明下,我们的样本数据和测试数据都来自一个数据文件,我们将数据文件的前10%
作为测试数据,后90%
作为样本数据。
测试的步骤:
- 读取数据文件中的样本数据到特征矩阵和类标签向量
- 对数据进行归一化,得到归一化的特征矩阵
- 执行KNN算法对测试数据进行测试,得到分类结果
- 与实际的分类结果进行对比,记录分类错误率
- 打印每个人的分类数据及错误率作为最终的结果
测试函数实现:
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)
# 打印KNN算法分类结果和真实的分类
print "the classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i])
# 判断KNN算法结果是否准确
if (classifierResult != datingLabels[i]): errorCount += 1.0
# 打印错误率
print "the total error rate is: %f" % (errorCount/float(numTestVecs))
测试中的一些数据,其中样本数据900行,测试数据100行:
运行程序进行测试:
最终我们得到错误率为 0.05,可以考虑下有哪些方面可以降低错误率,比如那些值可以影响到最终的错误率。欢迎到实验楼问答与老师和同学进行探讨。
三、经典问答
本节内容不断更新中,列出同学在实验楼问答中提到的有价值的问题供同学参考。
四、完整数据及代码
完整的数据及参考代码可以通过wget下载:
$ cd /home/shiyanlou/
$ wget http://labfile.oss.aliyuncs.com/courses/499/lab2.zip
$ unzip lab2.zip
完整代码:
#-*- coding: utf-8 -*-
from numpy import *
import operator
# 读取数据到矩阵
def file2matrix(filename):
# 打开数据文件,读取每行内容
fr = open(filename)
arrayOLines = fr.readlines()
# 初始化矩阵
numberOfLines = len(arrayOLines)
returnMat = zeros((numberOfLines,3))
# 初始化类标签向量
classLabelVector = []
# 循环读取每一行数据
index = 0
for line in arrayOLines:
# 去掉回车符
line = line.strip()
# 提取4个数据项
listFromLine = line.split(‘\t‘)
# 将前三项数据存入矩阵
returnMat[index,:] = listFromLine[0:3]
# 将第四项数据存入向量
classLabelVector.append(int(listFromLine[-1]))
index += 1
return returnMat,classLabelVector
# 数据归一化
def autoNorm(dataSet):
# 读取矩阵中数据项的最大和最小值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
# 获得最大和最小值间差值
ranges = maxVals - minVals
# 初始化输出结果
normDataSet = zeros(shape(dataSet))
# 获取矩阵的行数
m = dataSet.shape[0]
# 矩阵运算:实现归一化公式中的 oldValue - min 一步
normDataSet = dataSet - tile(minVals, (m,1))
# 矩阵除法:实现归一化公式中的除法
normDataSet = normDataSet/tile(ranges, (m,1))
# 返回归一化后的数据,数据范围及最小值矩阵
return normDataSet, ranges, minVals
# kNN算法实现
def classify0(inX, dataSet, labels, k):
# 获取样本数据数量
dataSetSize = dataSet.shape[0]
# 矩阵运算,计算测试数据与每个样本数据对应数据项的差值
diffMat = tile(inX, (dataSetSize,1)) - dataSet
# sqDistances 上一步骤结果平方和
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.iteritems(), key=operator.itemgetter(1), reverse=True)
# 返回出现频次最高的类别
return sortedClassCount[0][0]
# 算法测试
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)
# 打印KNN算法分类结果和真实的分类
print "the classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i])
# 判断KNN算法结果是否准确
if (classifierResult != datingLabels[i]): errorCount += 1.0
# 打印错误率
print "the total error rate is: %f" % (errorCount/float(numTestVecs))
# 执行算法测试
datingClassTest()