【机器学习实战】K-近邻算法之海伦约会
文章是阅读《机器学习实战》【美】Peter Harrington著 李锐李鹏 曲亚东 王斌 译 的读书笔记,具体可以去参考这本书。
1.准备数据:文本文件中解析数据
海伦将约会数据放在datingTestSet.txt中,每个样本数据占一行,总共有1000行。海伦的样本主要包含以下三种特征:
- 每年获得的飞行常客里程数
- 玩视频游戏所消耗时间百分比
- 每周消费的冰淇淋公升数
在上述数据输入到分类器之前,需要将待处理数据格式改变为分类器可以接受的格式。由csv文件构建相对应的numpy数组, 输入为文件名字字符串,输出为训练样本矩阵和类标签向量。
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()
# 得到文件行数
numberOfLines = len(arrayOLines)
# 创建返回的numpy数组,因为是海伦约会数据集有3个特征
returnMat = zeros((numberOfLines, 3))
classLabelVector = []
index = 0
# 解析文本数据到列表
for line in arrayOLines:
# 截取掉所有的回车字符
line = line.strip()
# 使用'\tab'键将整行数据转换为列表
listFromLine = line.split('\t')
# 将海伦约会的每一行数据转换为要返回的numpy数据行
returnMat[index, :] = listFromLine[0:3]
classLabelVector.append((listFromLine[-1]))
# 将字符标签转换为数字标签
# if listFromLine[-1] == 'didntLike':
# classLabelVector.append(1)
# if listFromLine[-1] == 'smallDoses':
# classLabelVector.append(2)
# if listFromLine[-1] == 'largeDoses':
# classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
加载数据
2.标题分析数据
使用Matplotlib制作原始数据的散点图分析数据
博主陈多鱼这块代码写的很好,在这借鉴了一下,感兴趣可以去看原帖,感谢博主
https://blog.csdn.net/qq_42338771/article/details/108270830
代码及其解释
# 图像数据分析
def showData(returnMat, classLabelVector):
fig, axs = plt.subplots(nrows=2, ncols=2, sharex=False, sharey=False, figsize=(13, 8))
labelColors = []
for i in datingLables:
if i == 1:
labelColors.append('black')
if i == 2:
labelColors.append('orange')
if i == 3:
labelColors.append('red')
# 设置第一张图片,飞行里程数和游戏百分比, 散点大小为15, 透明度0.5
axs[0][0].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=labelColors, s=15, alpha=0.5)
# 设置标题和坐标轴备注
axs0_title_text = axs[0][0].set_title('flight_play')
axs0_xlabel_text = axs[0][0].set_xlabel('飞行里程')
axs0_ylabel_text = axs[0][0].set_ylabel('游戏时间')
# 中文标题正常显示
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.setp(axs0_title_text, size=9, weight='bold', color='red')
plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black')
# 设置第二张图片, 飞行里程数和冰淇淋
axs[0][1].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 2], color=labelColors, s=15, alpha=0.5)
axs1_title_text = axs[0][1].set_title('flight_eat')
axs1_xlabel_text = axs[0][1].set_xlabel('飞行里程')
axs1_ylabel_text = axs[0][1].set_ylabel('冰淇淋量')
plt.setp(axs1_title_text, size=9, weight='bold', color='red')
plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black')
# 设置第三张图片,游戏时间和冰淇淋量
axs[1][0].scatter(x=datingDataMat[:, 1], y=datingDataMat[:, 2], color=labelColors, s=15, alpha=0.5)
axs2_title_text = axs[1][0].set_title('play_eat')
axs2_xlabel_text = axs[1][0].set_xlabel('游戏时间')
axs2_ylabel_text = axs[1][0].set_ylabel('冰淇淋量')
plt.setp(axs2_title_text, size=9, weight='bold', color='red')
plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
# 设置图例
didntLike = mlines.Line2D([], [], color='black', marker='.',
markersize=6, label='didntLike')
smallDoses = mlines.Line2D([], [], color='orange', marker='.',
markersize=6, label='smallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.',
markersize=6, label='largeDoses')
# 添加图例
axs[0][0].legend(handles=[didntLike, smallDoses, largeDoses])
axs[0][1].legend(handles=[didntLike, smallDoses, largeDoses])
axs[1][0].legend(handles=[didntLike, smallDoses, largeDoses])
# 画出图片
plt.show()
数据分析图例
这三张图可以看出来,第一张图通过飞行里程数和游戏百分比,还是可以把三种约会类型比较清晰的分类出来,而后面两张图类别中互相混合,无法分类出来。
3.数据归一化
约会网站的原始数据,可以看出来如果使用欧氏距离的话,飞行里程数与另外两个类别的数差距比较大,影响过大,但是海伦认为这三种数据同等重要,处理不同范围的特征值的时候,一般将数值归一化,减小数级的影响,将取值范围处理到0到1或者-1到1之间,下面的公式将数值范围处理在0到1之间。
n
e
w
v
a
l
u
e
=
(
o
l
d
v
a
l
u
e
−
m
i
n
)
/
(
m
a
x
−
m
i
n
)
newvalue = (oldvalue - min)/(max-min)
newvalue=(oldvalue−min)/(max−min)
数值归一化范围-1到1之间公式:
n
e
w
v
a
l
u
e
=
2
∗
(
o
l
d
v
a
l
u
e
−
m
i
n
)
/
(
m
a
x
−
m
i
n
)
−
1
newvalue = 2*(oldvalue - min)/(max-min) - 1
newvalue=2∗(oldvalue−min)/(max−min)−1
max与min分别是数据集中的最小特征值和最大特征值,改变数值取值范围会增加分类器的复杂度,但是会使结果更加准确。
代码数值归一化0到1区间函数autoNorm()。
# 数值归一化函数
def autoNorm(dataSet):
minValue = dataSet.min(0)
maxValue = dataSet.max(0)
ranges = maxValue - minValue
# 同样规格的零矩阵记录数值归一化的结果
normDataset = zeros(shape(dataSet))
# 记录数据行数,即数据条数
m = dataSet.shape(0)
# 复制minvalue到同样大小,然后做数据相减操作
normDataset = dataSet - tile(minValue, (m, 1))
# 复制ranges到同样规格大小,然后做数据相除操作
normDataset = normDataset/tile(ranges, (m, 1))
# 返回归一化结果,范围,最小值
return normDataset, ranges, minValue
这里面操作的是特征值(数值)相除,不是向量相除,在numpy库中,向量相除使用函数linalg.solve(matA, matB)。
4.分类算法:KNN算法实现
使用的是之前文章中使用的欧式距离计算的方法来判断分类,详细参考文章
https://blog.csdn.net/qq_43587460/article/details/135983033#comments_31185167
# 分类器,使用欧几里得距离
def classify0(inx, dataset, labels, k):
'''
:param inx: 未知标签数据特征
:param dataset: 样本数据
:param labels: 样本标签
:param k: k近邻取值
:return: 判断标签值
'''
# 距离计算
datasetSetSize = dataset.shape[0]
diffMat = tile(inx, (datasetSetSize, 1)) - dataset
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distance = sqDistances ** 0.5
# 距离排序
sortedDistIndicies = distance.argsort()
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sorteClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sorteClassCount[0][0]
5.测试算法:作为完整程序验证分类器
评估算法的正确率,通常我们用90%的数据来作为训练样本来训练分类器,用10%的数据去测试分类器,检测分类器的正确率。10%的测试数据应该是随机选择的,海伦约会数据没有根据特定的目的来排序,可以随意选取10%的数据而不影响随机性。
可以使用错误率来检测分类器的性能,错误率就是分类器给出错误结果的次数除以测试数据的总数,完美分类器的错误率为0,而错误率为1.0的分类器不会给出任何正确的分类结果。
代码:
def datingClassTest():
# 测试数据比例
hoRatio = 0.10
datingDataMat, datingLables = file2matrix('datingTestSet.txt')
normMat, ranges, minValues = 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, :],\
datingLables[numTestVecs:m], 3)
print("the classifier came back with:%d, the real answer is:%d" % (classifierResult, datingLables[i]))
# 分类器分类数据与预测数据对比,最后计算错误率
if(classifierResult != datingLables[i]): errorCount += 1.0
print("the total error rate is:%f" % (errorCount/float(numTestVecs)))
测试分类器结果:
可以看出,构建的分类器的错误率是5%,效果还不错,可以改进检测数据的量(hoRatio)与近邻判断的数量(k的值),来检测错误率是否会发生变化。
6.使用算法,构建完整可用的系统
写了一个人工智能,最想的还是可以帮忙来预测数据,来使用自己的构建的机器学习算法,构建预测函数。
代码及其解释如下:
# 约会网站预测函数
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 filer miles earned per year?"))
iceCream = float(raw_input("liters of ice cream consumed per year?"))
# 数据读取
datingDataMat, datingLables = file2matrix('datingTestSet.txt')
# 数据归一化
normMat, ranges, minValues = autoNorm(datingDataMat)
# 输入的数据格式整合,适配机器学习算法
inArr = array([ffMiles, percentTats, iceCream])
# 对数据进行归一化处理,并进行预测
classifierResult = classify0((inArr - minValues)/ranges, normMat, datingLables, 3)
print("You will probably like this person:", resultList[classifierResult - 1])
使用效果:
7.完整代码
from numpy import *
import operator
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
from pip._vendor.distlib.compat import raw_input
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()
# 得到文件行数
numberOfLines = len(arrayOLines)
# 创建返回的numpy数组,因为是海伦约会数据集有3个特征
returnMat = zeros((numberOfLines, 3))
classLabelVector = []
index = 0
# 解析文本数据到列表
for line in arrayOLines:
# 截取掉所有的回车字符
line = line.strip()
# 使用'\tab'键将整行数据转换为列表
listFromLine = line.split('\t')
# 将海伦约会的每一行数据转换为要返回的numpy数据行
returnMat[index, :] = listFromLine[0:3]
# 文本标签收集
# classLabelVector.append((listFromLine[-1]))
# 将字符标签转换为数字标签
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
if listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
if listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
# 加载数据
datingDataMat, datingLables = file2matrix('datingTestSet.txt')
# print(datingDataMat)
# print('------------')
# print(datingLables[0:20])
# 图像数据分析
def showData(returnMat, classLabelVector):
fig, axs = plt.subplots(nrows=2, ncols=2, sharex=False, sharey=False, figsize=(13, 8))
labelColors = []
for i in datingLables:
if i == 1:
labelColors.append('black')
if i == 2:
labelColors.append('orange')
if i == 3:
labelColors.append('red')
# 设置第一张图片,飞行里程数和游戏百分比, 散点大小为15, 透明度0.5
axs[0][0].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=labelColors, s=15, alpha=0.5)
# 设置标题和坐标轴备注
axs0_title_text = axs[0][0].set_title('flight_play')
axs0_xlabel_text = axs[0][0].set_xlabel('飞行里程')
axs0_ylabel_text = axs[0][0].set_ylabel('游戏时间')
# 中文标题正常显示
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.setp(axs0_title_text, size=9, weight='bold', color='red')
plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black')
# 设置第二张图片, 飞行里程数和冰淇淋
axs[0][1].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 2], color=labelColors, s=15, alpha=0.5)
axs1_title_text = axs[0][1].set_title('flight_eat')
axs1_xlabel_text = axs[0][1].set_xlabel('飞行里程')
axs1_ylabel_text = axs[0][1].set_ylabel('冰淇淋量')
plt.setp(axs1_title_text, size=9, weight='bold', color='red')
plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black')
# 设置第三张图片,游戏时间和冰淇淋量
axs[1][0].scatter(x=datingDataMat[:, 1], y=datingDataMat[:, 2], color=labelColors, s=15, alpha=0.5)
axs2_title_text = axs[1][0].set_title('play_eat')
axs2_xlabel_text = axs[1][0].set_xlabel('游戏时间')
axs2_ylabel_text = axs[1][0].set_ylabel('冰淇淋量')
plt.setp(axs2_title_text, size=9, weight='bold', color='red')
plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
# 设置图例
didntLike = mlines.Line2D([], [], color='black', marker='.',
markersize=6, label='didntLike')
smallDoses = mlines.Line2D([], [], color='orange', marker='.',
markersize=6, label='smallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.',
markersize=6, label='largeDoses')
# 添加图例
axs[0][0].legend(handles=[didntLike, smallDoses, largeDoses])
axs[0][1].legend(handles=[didntLike, smallDoses, largeDoses])
axs[1][0].legend(handles=[didntLike, smallDoses, largeDoses])
# 画出图片
plt.show()
showData(datingDataMat, datingLables)
# 分类器,使用欧几里得距离
def classify0(inx, dataset, labels, k):
'''
:param inx: 未知标签数据特征
:param dataset: 样本数据
:param labels: 样本标签
:param k: k近邻取值
:return: 判断标签值
'''
# 距离计算
datasetSetSize = dataset.shape[0]
diffMat = tile(inx, (datasetSetSize, 1)) - dataset
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distance = sqDistances ** 0.5
# 距离排序
sortedDistIndicies = distance.argsort()
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sorteClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sorteClassCount[0][0]
# 数值归一化函数
def autoNorm(dataSet):
minValue = dataSet.min(0)
maxValue = dataSet.max(0)
ranges = maxValue - minValue
# 同样规格的零矩阵记录数值归一化的结果
normDataset = zeros(shape(dataSet))
# 记录数据行数,即数据条数
m = dataSet.shape[0]
# 复制minvalue到同样大小,然后做数据相减操作
normDataset = dataSet - tile(minValue, (m, 1))
# 复制ranges到同样规格大小,然后做数据相除操作
normDataset = normDataset/tile(ranges, (m, 1))
# 返回归一化结果,范围,最小值
return normDataset, ranges, minValue
# 归一化实例
normMat, ranges, minValues = autoNorm(datingDataMat)
# print(normMat)
# print('-----------')
# print(ranges)
# print('-------------')
# print(minValues)
# 错误率测试代码
def datingClassTest():
# 测试数据比例
hoRatio = 0.10
datingDataMat, datingLables = file2matrix('datingTestSet.txt')
normMat, ranges, minValues = 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, :],\
datingLables[numTestVecs:m], 3)
print("the classifier came back with:%d, the real answer is:%d" % (classifierResult, datingLables[i]))
# 分类器分类数据与预测数据对比,最后计算错误率
if(classifierResult != datingLables[i]): errorCount += 1.0
print("the total error rate is:%f" % (errorCount/float(numTestVecs)))
# 测试分类器
# datingClassTest()
# 约会网站预测函数
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 filer miles earned per year?"))
iceCream = float(raw_input("liters of ice cream consumed per year?"))
# 数据读取
datingDataMat, datingLables = file2matrix('datingTestSet.txt')
# 数据归一化
normMat, ranges, minValues = autoNorm(datingDataMat)
# 输入的数据格式整合,适配机器学习算法
inArr = array([ffMiles, percentTats, iceCream])
# 对数据进行归一化处理,并进行预测
classifierResult = classify0((inArr - minValues)/ranges, normMat, datingLables, 3)
print("You will probably like this person:", resultList[classifierResult - 1])
if __name__ == '__main__':
classifyPerson()
从简单的开始,一步步进步吧。如有错误,欢迎指正,积极学习。