一、问题引入
海伦一直使用在线约会网站寻找适合自己的约会对象。她曾交往过三种类型的人:
- 不喜欢的人
- 一般喜欢的人
- 非常喜欢的人
这些人包含以下三种特征
1. 每年获得的飞行常客里程数
2. 玩视频游戏所耗时间百分比
3. 每周消费的冰淇淋公升数
该网站现在需要尽可能向海伦推荐她喜欢的人,需要我们设计一个分类器,根据用户的以上三种特征,识别出是否该向海伦推荐。
二、需求概要分析
根据问题,我们可知,样本特征个数为3,样本标签为三类。现需要实现将一个待分类样本的三个特征值输入程序后,能够识别该样本的类别,并且将该类别输出。
三、程序结构设计说明
根据问题,可以知道程序大致流程如下。
其中输入数据应包含三个值,输出应为喜欢,一般,不喜欢,三个中的一个。
四、K近邻算法的一般流程
1. 数据准备
这包括收集、清洗和预处理数据。
预处理可能包括归一化或标准化特征,以确保所有特征在计算距离时具有相等的权重。
玩视频游戏所耗时间百分比 | 每年获得的飞行常客里程数 | 每周消费的冰淇淋的公升数 | 样本分类 |
| ---- | ------------------------ | ------------------------ | ------------------------ | -------- |
| 1 | 0.8 | 400 | 0.5 | 1 |
| 2 | 12 | 134000 | 0.9 | 3 |
| 3 | 0 | 20000 | 1.1 | 2 |
| 4 | 67 | 32000 | 0.1 | 2 |
我们很容易发现,当计算样本之间的距离时数字差值最大的属性对计算结果的影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于上表中其他两个特征-玩视频游戏所耗时间占比和每周消费冰淇淋公斤数的影响。而产生这种现象的唯一原因,仅仅是因为飞行常客里程数远大于其他特征值。但海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。
在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。**下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:
2. 选择距离度量方法:
确定用于比较样本之间相似性的度量方法,常见的如欧几里得距离、曼哈顿距离等。
3. 确定K值:
选择一个K值**,即在分类或回归时应考虑的邻居数量。这是一个超参数,可以通过交叉验证等方法来选择最优的K值。
4. 找到K个最近邻居:对于每一个需要预测的未标记的样本:
- 计算该样本与训练集中所有样本的距离。
- 根据距离对它们进行排序。
- 选择距离最近的K个样本
5. 预测:
- 对于分类任务:查看K个最近邻居中最常见的类别,作为预测结果。例如,如果K=3,并且三个最近邻居的类别是[1, 2, 1],那么预测结果就是类别1。
- 对于回归任务:预测结果可以是K个最近邻居的平均值或加权平均值。
6. 评估:
使用适当的评价指标(如准确率、均方误差等)评估模型的性能。
7. 优化:
基于性能评估结果,可能需要返回并调整某些参数,如K值、距离度量方法等,以获得更好的性能。
五、算法实现
import numpy as np
import matplotlib as mpl
import operator
# 准备数据,从文本文件中解析数据
def file2matrix(filename):
with open(filename, 'r') as fr: # 打开文件
arrayOLines = fr.readlines() # 读取文件所有内容
numberOfLines = len(arrayOLines) # 得到文件行数
returnMat = np.zeros((numberOfLines, 3)) # 返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
classLabelVector = [] # 返回的分类标签向量
index = 0 # 行的索引值
for line in arrayOLines:
line = line.strip() # 截取掉所有的回车符
listFromLine = line.split('\t') # 将字符串根据'\t'分隔符分割成一个元素列表
returnMat[index, :] = listFromLine[0:3] # 选取前三个元素,存放到特征矩阵中
# 根据文本中标记的喜欢的程度进行分类
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1) # 1代表不喜欢
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2) # 2代表魅力一般
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3) # 3代表极具魅力
index += 1
return returnMat, classLabelVector
# 准备数据,数据归一化处理
def autoNorm(dataSet):
minVals = dataSet.min(0) # 获得每列数据的最小值
maxVals = dataSet.max(0) # 获得每列数据的最大值
ranges = maxVals - minVals # 最大值和最小值的范围
normDataSet = np.zeros(np.shape(dataSet)) # shape(dataSet)返回dataSet的矩阵行列数
m = dataSet.shape[0] # 返回dataSet的行数
normDataSet = dataSet - np.tile(minVals, (m, 1)) # 原始值减去最小值
normDataSet = normDataSet / np.tile(ranges, (m, 1)) # 除以最大和最小值的差,得到归一化数据
return normDataSet, ranges, minVals # 返回归一化数据结果,数据范围,最小值
# KNN算法分类器
# 用于分类的输入向量是inX,输入的训练样本集是dataSet
# 标签向量是labels,最后的参数k表示用于选择最近邻居的数目
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0] # numpy函数shape[0]返回dataSet的行数
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet # 在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
sqDiffMat = diffMat ** 2 # 二维特征相减后平方
sqDistances = sqDiffMat.sum(axis=1) # sum()所有元素相加,sum(0)列相加,sum(1)行相加
distances = sqDistances ** 0.5 # 开方,计算出距离
sortedDistIndices = distances.argsort() # 返回distances中元素从小到大排序后的索引值
# 定一个记录类别次数的字典
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndices[i]] # 取出前k个元素的类别
# 计算类别次数
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 # 字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1),
reverse=True) # key=operator.itemgetter(1)根据字典的值进行排序,reverse降序排序字典
return sortedClassCount[0][0] # 返回次数最多的类别,即所要分类的类别
# 测试算法:验证分类器
def datingClassTest():
filename = "datingTestSet.txt" # 打开的文件名
datingDataMat, datingLabels = file2matrix(filename) # 将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
hoRatio = 0.10 # 取所有数据的百分之十
normMat, ranges, minVals = autoNorm(datingDataMat) # 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
m = normMat.shape[0] # 获得normMat的行数
numTestVecs = int(m * hoRatio) # 百分之十的测试数据的个数
errorCount = 0.0 # 分类错误计数器
for i in range(numTestVecs):
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m],
4) # 前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
print("分类结果:%d\t真实类别:%d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率:%f%%" % (errorCount / float(numTestVecs) * 100))
# 使用算法
def classifyPerson():
resultList = ['不喜欢', '有些喜欢', '非常喜欢'] # 输出结果
# 用户输入
ffMiles = float(input("每年获得的飞行常客里程数:"))
precentTats = float(input("玩视频游戏所耗时间百分比:"))
iceCream = float(input("每周消费的冰激淋公升数:"))
filename = "datingTestSet.txt" # 打开的文件名
datingDataMat, datingLabels = file2matrix(filename) # 打开并处理数据
normMat, ranges, minVals = autoNorm(datingDataMat) # 训练集归一化
inArr = np.array([ffMiles, precentTats, iceCream]) # 生成NumPy数组,测试集
norminArr = (inArr - minVals) / ranges # 测试集归一化
classifierResult = classify0(norminArr, normMat, datingLabels, 3) # 返回分类结果
print("你可能%s这个人" % (resultList[classifierResult - 1])) # 打印结果
# 主函数,测试以上各个步骤,并输出各个步骤的结果
if __name__ == '__main__':
# 打开的文件名
filename = "datingTestSet.txt"
# 打开并处理数据
# 验证分类器
datingClassTest()
# 使用分类器
classifyPerson()
实验结果:
实验小结
本次实验为经典的海伦约会实验,在学习了KNN算法的基础上,选择样本数据集中前k个最相似的数据,就是KNN算法中k的出处。k值过大,会出现分类结果模糊的情况;k值较小,那么预测的标签比较容易受到样本的影响。在实验过程中,不同的k值也会导致分类器的错误率不同。KNN算法是一种简单易懂且容易实现的分类算法,适用于各种数据类型和分布情况,并具有较高的鲁棒性。但是,KNN算法在处理大规模数据时存储和计算复杂度较高,对输入数据的维度敏感,并且预测速度较慢。总之,KNN算法是一个简单但有效的分类算法,能够在很多问题中得到应用。
参考书籍:《机器学习实战》