本文中介绍的是《机器学习实战》一书中关于KNN算法的一个实战案例:海伦约会案例
<!--MORE-->
海伦女士一直在使用约会网站来寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但是海伦不是喜欢每个人。经过一番的总结,她发现自己喜欢过3个类型的人:
海伦自己通过一段时间搜集一份数据,她将这些数据存放在文本文件datingTestSet中,每个样本数据占据一行,总共有1000行,主要包含以下3种特征:
真心吃货,冰淇淋都能成为其找对象的指标😃
数据的大致样貌如下:
在将上面的数据输入到分类器之前,必须将待处理的数据格式改变为分类器可以接受的格式。分类器接受的数据格式分为两个部分:
因此将文本记录转成Numpy的解析程序:
import numpy as np
"""
函数说明:打开文件并解析,对数据进行分类:1-不喜欢 2-魅力一般 3-极具魅力
参数:
filename 文件名
返回值:
returnMat 特征矩阵
classLabelVector 分类标签向量
修改时间:
2021-02-28
"""
def file2matrix(filename):
# 打开文件
fr = open(filename)
# 读取文件全部内容
arrayOLines = fr.readlines()
# 得到文件行数
numberOfLines = len(arrayOLines)
# 返回的Numpy矩阵,解析完成的数据:numberOfLines * 3的大小
returnMat = np.zeros((numberOfLines, 3)) # 全0矩阵
# 返回的分类标签向量
classLabelVector = []
# 行的索引值
index = 0
for line in arrayOLines: # 对全部内容进行遍历循环
# 删除空白字符(\n,\r,\t,' ')
line = line.strip()
# 根据\t进行切割
listFromLine = line.split('\t')
# 将数据的前3列取出来,存放在returnMat的特征矩阵中
returnMat[index,:] = listFromLine[0:3]
# 根据文本标记的喜欢程度进行分类:1-不喜欢 2-魅力一般 3-极具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1 # 每次循环索引值加1,对returnMat有用
return returnMat, classLabelVector # 返回特征矩阵和分类标签
if __name__ == "__main__":
filename = "datingTestSet.txt"
returnMat, classLabelVector = file2matrix(filename)
print(returnMat)
print(classLabelVector)
调用上面的函数并执行观察效果:
在上面我们已经顺利导入了数据,并且进行了解析,格式化为分类器需要的数据,接下来需要通过可视化的方式来直观地呈现数据,得到一些初步的结论。
# -*- coding: UTF-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.lines as mlines
import matplotlib.pyplot as plt
import numpy as np
"""
函数说明:打开数据并解析,对数据进行分类:1-不喜欢 2-一般魅力 3-极具魅力
参数:
filename 文件名
返回值:
returnMat 特征矩阵
classLabelVector 分类标签向量
修改时间:
2021-02-28
"""
def file2matrix(filename):
# 打开文件
fr = open(filename)
# 读取文件全部内容
arrayOLines = fr.readlines()
# 得到文件行数
numberOfLines = len(arrayOLines)
# 返回的Numpy矩阵,解析完成的数据:numberOfLines * 3的大小
returnMat = np.zeros((numberOfLines, 3)) # 全0矩阵
# 返回的分类标签向量
classLabelVector = []
# 行的索引值
index = 0
for line in arrayOLines: # 对全部内容进行遍历循环
# 删除空白字符(\n,\r,\t,' ')
line = line.strip()
# 根据\t进行切割
listFromLine = line.split('\t')
# 将数据的前3列取出来,存放在returnMat的特征矩阵中
returnMat[index,:] = listFromLine[0:3]
# 根据文本标记的喜欢程度进行分类:1-不喜欢 2-魅力一般 3-极具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1 # 每次循环索引值加1,对returnMat有用
return returnMat, classLabelVector
"""
函数说明:数据可视化
参数:
- 特征矩阵
- 分类标签
返回值:无
时间:2021-02-28
"""
def showdatas(returnMat, classLabelVector):
# 显示中文配置:Songti SC系统中的中文字体之一
plt.rcParams['font.sans-serif']=['Songti SC'] # 用来正常显示中文标
plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
# 设置2*2的画布
fig,axs = plt.subplots(nrows=2, ncols=2,sharex=False,sharey=False,figsize=(13,8))
# 特征数据长度
numberOfLabels = len(returnMat)
# 设置空标签来存储
LabelsColors = []
for i in classLabelVector: # 对每个标签遍历,并且赋上对应的颜色
if i == 1:
LabelsColors.append('black')
if i == 2:
LabelsColors.append('orange')
if i == 3:
LabelsColors.append('red')
# 画出散点图:散点大小15 透明度0.5
axs[0][0].scatter(x=returnMat[:,0],y=returnMat[:,1],color=LabelsColors,s=15,alpha=0.5)
# 设置标题、x轴、y轴
axs0_title_text = axs[0][0].set_title(u'每年获得的飞行常客里程数与玩视频游戏所消耗时间占比')
axs0_xlabel_text = axs[0][0].set_xlabel(u'每年获得的飞行常客里程数')
axs0_ylabel_text = axs[0][0].set_ylabel(u'玩视频游戏所消耗时间占比')
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=returnMat[:,0],y=returnMat[:,2],color=LabelsColors,s=15,alpha=0.5)
# 设置标题、x轴、y轴
axs1_title_text = axs[0][1].set_title(u'每年获得的飞行常客里程数与每周消费的冰激淋公升数')
axs1_xlabel_text = axs[0][1].set_xlabel(u'每年获得的飞行常客里程数')
axs1_ylabel_text = axs[0][1].set_ylabel(u'每周消费的冰激淋公升数')
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=returnMat[:,1],y=returnMat[:,2],color=LabelsColors,s=15,alpha=0.5)
# 设置标题、x轴、y轴
axs2_title_text = axs[1][0].set_title(u'玩视频游戏所消耗时间占比与每周消费的冰激淋公升数')
axs2_xlabel_text = axs[1][0].set_xlabel(u'玩视频游戏所消耗时间占比')
axs2_ylabel_text = axs[1][0].set_ylabel(u'每周消费的冰激淋公升数')
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()
if __name__ == "__main__":
# 打开的文件名
filename = "datingTestSet.txt"
# 处理数据
returnMat, classLabelVector = file2matrix(filename)
# 显示图片
showdatas(returnMat, classLabelVector)
下表中给出了一部分数据,如果想计算样本3和样本4之间的距离,可以使用欧式距离的公式来进行计算:
样本 | 玩游戏所耗时间占比 | 每年获得的飞行里程数 | 每周消耗的冰淇淋公升数 | 样本分类 |
---|---|---|---|---|
1 | 0.8 | 4000 | 0.5 | 1 |
2 | 12 | 134000 | 1 | 3 |
3 | 0 | 20000 | 1.2 | 2 |
4 | 62 | 32000 | 0.3 | 2 |
计算公式如下图所示:
$$\sqrt{(0-62)^2+(20000-32000)^2+(1.2-0.3)^2}$$
我们发现:上面方程中数字差值最大的属性对计算结果的影响是最大的,也就是说:
每年获取的飞行里程数对于计算结果的影响是远大于其他两个特征的
但是在海伦的心中:这3个因素是同等重要的,因此作为作为3个等权重的特征之一,飞行里程数并不应该严重地影响到计算的结果。
在处理这种不同取值范围的特征值时,我们通常采用的是归一化的方法,将取值范围控制在0-1或者-1到1之间,常用的归一化方法有:
下面是一个0-1归一化的函数:
import numpy as np
"""
函数说明:打开文件并解析,对数据进行分类:1-不喜欢 2-魅力一般 3-极具魅力
参数:
filename 文件名
返回值:
returnMat 特征矩阵
classLabelVector 分类标签向量
修改时间:
2021-02-28
"""
def file2matrix(filename):
# 打开文件
fr = open(filename)
# 读取文件全部内容
arrayOLines = fr.readlines()
# 得到文件行数
numberOfLines = len(arrayOLines)
# 返回的Numpy矩阵,解析完成的数据:numberOfLines * 3的大小
returnMat = np.zeros((numberOfLines, 3)) # 全0矩阵
# 返回的分类标签向量
classLabelVector = []
# 行的索引值
index = 0
for line in arrayOLines: # 对全部内容进行遍历循环
# 删除空白字符(\n,\r,\t,' ')
line = line.strip()
# 根据\t进行切割
listFromLine = line.split('\t')
# 将数据的前3列取出来,存放在returnMat的特征矩阵中
returnMat[index,:] = listFromLine[0:3]
# 根据文本标记的喜欢程度进行分类:1-不喜欢 2-魅力一般 3-极具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1 # 每次循环索引值加1,对returnMat有用
return returnMat, classLabelVector # 返回特征矩阵和分类标签
"""
函数作用:数值归一化
函数参数:
特征矩阵 returnMat
返回值:
归一化后的特征矩阵 normDataSet
数据范围 ranges
最小值 minVal
"""
def autoNormal(dataSet):ßßßß
# 获取最大值和最小值,二者的范围
minVal = dataSet.min()
maxVal = dataSet.max()
ranges = maxVal - minVal
# 矩阵的行列数shape
normDataSet = np.zeros(np.shape(dataSet))
# (原始值 - 最小值)
normDataSet = dataSet - np.tile(minVal, (m, 1))
# 除以 (max-min)
normDataSet = normDataSet / np.tile(ranges, (m, 1))
# 返回归一化数据、数据范围、最小值
return normDataSet, ranges, minVal
if __name__ == "__main__":
filename = "datingTestSet.txt"
returnMat, classLabelVector = file2matrix(filename)
normDataSet, ranges, minVal = autoNorm(returnMat)
print(normDataSet)
print(ranges)
print(minVal)
上面我们已经按照需求处理了数据,并且对数据做了归一化处理,接下来我们将开展机器学习中一个重要的内容:评估算法的准确率。通常我们使用提供的数据中90%作为训练集,剩下的10%作为测试集去检验分类器的准确率。
10%的测试集是随机选择的
import numpy as np
"""
函数说明:KNN算法分类
函数参数:
inX 用于分类的数据集(测试集)
dataSet 用于训练的数据(训练集)
labels 分类标签
k 算法参数,选择距离最近的k个点
修改时间:
2021-02-28
"""
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0] # 文件行数,即大小
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet # np.tile表示在两个方向上的重复次数,达到和原始数据相同的shape,以便能够相减
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances ** 0.5 # 以上3步:距离相减再平方,再求和,再开根号
# 获取到的是索引值!!!
sortedDistIndices = distances.argsort() # 全部距离从小到大排序后的索引值
classCount = {} # 存储类别次数的字典
for i in range(k):
voteIlabel = labels[sortedDistIndices[i]] # 根绝每个索引,取出对应的前k个元素的类别
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1 # 计算类别次数;get方法返回指定键的值,否则返回默认值
# python3中使用item()
# reverse表示降序排序字典
# key=operator.itemgetter(0)表示根据字典的键进行排序
# key=operator.itemgetter(1)表示根据字典的值进行排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0] # 返回次数最多的类别,即所要分类的类别
"""
函数说明:打开文件并解析,对数据进行分类:1-不喜欢 2-魅力一般 3-极具魅力
参数:
filename 文件名
返回值:
returnMat 特征矩阵
classLabelVector 分类标签向量
修改时间:
2021-02-28
"""
def file2matrix(filename):
# 打开文件
fr = open(filename)
# 读取文件全部内容
arrayOLines = fr.readlines()
# 得到文件行数
numberOfLines = len(arrayOLines)
# 返回的Numpy矩阵,解析完成的数据:numberOfLines * 3的大小
returnMat = np.zeros((numberOfLines, 3)) # 全0矩阵
# 返回的分类标签向量
classLabelVector = []
# 行的索引值
index = 0
for line in arrayOLines: # 对全部内容进行遍历循环
# 删除空白字符(\n,\r,\t,' ')
line = line.strip()
# 根据\t进行切割
listFromLine = line.split('\t')
# 将数据的前3列取出来,存放在returnMat的特征矩阵中
returnMat[index,:] = listFromLine[0:3]
# 根据文本标记的喜欢程度进行分类:1-不喜欢 2-魅力一般 3-极具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1 # 每次循环索引值加1,对returnMat有用
return returnMat, classLabelVector # 返回特征矩阵和分类标签
"""
函数说明:分类器测试函数
函数参数:无
返回值:
归一化后的特征矩阵 normDataSet
数据范围 ranges
数据最小值 minVal
修改时间:2021-02-28
"""
def datingClassTest():
filename = 'datingTestSet.txt'
# 得到特征矩阵和分类标签
returnMat, classLabelVector = file2matrix(filename)
# 归一化过程
normDataSet, ranges, minVal = autoNorm(returnMat)
# 测试集比例10%
hoRatio = 0.1
# 获得特征矩阵的行数
m = normDataSet.shape[0]
# 测试集的个数
numTestVecs = int(m * hoRation)
# 分类错误计数
errorCount = 0.0
for i in range(numTestVecs):
classifierResult = classify0(normDataSet[i,:], normDataSet[numTestVecs:m,:], dating)
print("分类结果: %d\t真实类别:%d"%(classifierResult,classLabelVector[i]))
if classifierResult != classLabelVector[i]:
errorCount += 1.0
print("错误率:%f%%"%(errorCount / float(numTestVecs)*100))
import numpy as np
import pandas as pd
import plotly_express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
data = pd.read_table('datingTestSet.txt',header=None)
data
查看的主要信息包含
使用matplotlib包绘制不同属性两两之间的散点图
1、对每行数据的标签进行颜色的标注
colors = []
for i in range(len(data)):
m = data.iloc[i,-2] # 样本分类的值进行判断赋值;我已经添加了一列新的数据,原始的数据标签在倒数第二列
if m == 'didntLike':
colors.append('black')
if m == 'smallDoses':
colors.append('orange')
if m == 'largeDoses':
colors.append('red')
colors[:20]
2、解决中文字体无法显示问题
一般情况下,通过下面的代码是可以直接在Jupyter notebook中显示中文的:
plt.rcParams['font.sans-serif']=['SimHei'] # 用来显示中文
但是可能自己的电脑系统中没有安装相应的字体,因此需要查看系统的字体,找到和中文相关的,查看系统字体的方法:
from matplotlib import font_manager
a = sorted([f.name for f in font_manager.fontManager.ttflist])
for i in a:
print(i) # 从A-Z的显示
看到了一个宋体,设置成宋体即可:
# 显示中文配置:Songti SC 是上面查看到的字体之一
plt.rcParams['font.sans-serif']=['Songti SC'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
# 设置画布大小
pl = plt.figure(figsize=(12,8))
# 设置3个子画布
fig1 = pl.add_subplot(221)
plt.scatter(data.iloc[:,0], # x轴数据
data.iloc[:,1], # y轴数据
marker='.', # 标记方式:原点
c=colors) # 颜色
plt.xlabel("飞行里程数")
plt.ylabel("玩游戏消耗百分比")
fig1 = pl.add_subplot(222)
plt.scatter(data.iloc[:,0],data.iloc[:,2],marker='.',c=colors)
plt.xlabel("飞行里程数")
plt.ylabel("每周冰淇淋公升数")
fig1 = pl.add_subplot(223)
plt.scatter(data.iloc[:,1],data.iloc[:,2],marker='.',c=colors)
plt.xlabel("玩游戏消耗百分比")
plt.ylabel("每周冰淇淋公升数")
plt.show()
px.scatter(data, # 绘图的数据
x="飞行里程数", # x轴
y="玩游戏消耗百分比", # y轴
color="对象标签" # 如何标记颜色
)
同理绘制另外两个图形:
在这里我们采用0-1标准化
过程:(x-min) / (max - min),定义一个归一化函数:
## 定义归一化函数
def minmax(dataSet):
minD = dataSet.min()
maxD = dataSet.max()
normSet = (dataSet - minD) / (maxD - minD)
return normSet
对测试数据的归一化(只对数据部分归一化,标签不用):
# 1、只对数据部分归一化
# 2、归一化之后通过concat函数拼接起来
testNew = pd.concat([minmax(test.iloc[:,:3]), test.iloc[:,3]], axis=1)
testNew
对原始数据的归一化:
# 原始数据的归一化
# 1、只对数据部分归一化
# 2、归一化之后通过concat函数拼接起来
dataNew = pd.concat([minmax(data.iloc[:,:3]), data.iloc[:,3]], axis=1)
dataNew
取出前90%的数据,因为本身海伦收集的数据就是无任何特殊意义随机收集的,所以我们直接取出前90%作为训练集,剩下的作为测试集即可:
# 海伦的数据本身就是无意义的排列,因此取出前90%作为训练集,后面的10%作为测试集
def randSplit(dataSet, rate=0.9):
n = dataSet.shape[0] # [1000,4] 取出1000,实际上就是data的长度len(data)
m = int(n * rate) # 取出前90%
train = dataSet.iloc[:m,:] # 前90%的行,所有列
test = dataSet.iloc[m:,:] # 后面的行及所有列
test.index = range(test.shape[0]) # 测试集test的上索引需要重置
return train,test
# 调用函数
train,test = randSplit(dataNew)
def dataClassify(train,test,k):
n = train.shape[1] - 1 # train除去标签的所有列
m = test.shape[0] # test的行数
result = [] # 存放最终的结果
for i in range(m):
dist = list((((train.iloc[:,:n] - test.iloc[i,:n]) ** 2).sum(1))**0.5) # 计算训练集中的每个数据和测试集中某个数据的欧氏距离
dist_l = pd.DataFrame({'dist':dist, 'labels':(train.iloc[:,n])}) # 计算出来的距离和对应训练集的标签构成DF型数据
dr = dist_l.sort_values(by='dist')[:k] # 根据标签排序取出前k个数据
re = dr.loc[:,'labels'].value_counts() # 前k个数据中统计每个标签出现的票数,票数高的则为测试集数据的标签
result.append(re.index[0]) # re.index[0]表示票数最高的分类
result = pd.Series(result)
test['predict'] = result # 测试集中添加预测的结果
acc = (test.iloc[:,-1] == test.iloc[:,-2]).mean() # 原始结果和预测结果的对比
print(f"模型预测准确率{acc}")
return test
对测试集的数据调用分类器,显示出判断效果:
KNN算法 | |
---|---|
功能 | 分类(核心),回归 |
算法类型 | 有监督学习-惰性学习 |
数据输入 | 特征矩阵至少包含k个训练样本,数据标签</br>特征空间中的各个特征的量纲需要统一,如果不统一,需要做归一化处理</br>自定义的的超参数k |
数据输出 | KNN分类:输出的是标签中的某个类别</br>KNN回归:输出的是对象的属性值,该值是距离输入的数据最近的k个训练样本标签的均值 |
k
个点;k
个点所在类别的出现频率;k
个点所出现频率最高的类别作为当前点的预测分类。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。