文章目录
KNN算法实例
KNN算法(K近邻算法)是一种很朴实的机器学习方法,既可以做分类,也可以做回归。分类问题是机器学习非常重要的一个组成部分,它的目标是根据已知样本的某些特征,判断一个样本属于哪个类别。
一、案例
- 案例背景
将收集的收集的一些数据存放在文本文件datingTestSet1.txt中,每个样本数据占一行,总共有1000行。(数据前三列分别表示:豆子植物的茎长,豆子的重量和豆子的宽度。最后一列则表示豆子的种类,总共有三种,分别为黑豆、绿豆和红豆)
- datingTestSet.txt中的个别数据:
二、具体实现代码实现
1.准备数据
代码如下:
# 准备数据:从文本文件中解析数据
import numpy as np
def file2matrix(filename):
#打开文件
fr = open(filename)
#读取文件所有内容
arrayOLines = fr.readlines()
#得到文件行数
numberOfLines = len(arrayOLines)
#返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
returnMat = np.zeros((numberOfLines,3))
#返回的分类标签向量
classLabelVector = []
#行的索引值
index = 0
for line in arrayOLines:
#s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' ')
line = line.strip()
#使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。
listFromLine = line.split('\t')
#特征矩阵
returnMat[index,:] = listFromLine[0:3]
#标签分类
if listFromLine[-1] == 'RedBean':
classLabelVector.append(1)
elif listFromLine[-1] == 'MungBean':
classLabelVector.append(2)
elif listFromLine[-1] == 'BlackBean':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
filename = 'E:\MechineLearning\KNN\KNN\datingTestSet1.txt'
datingDataMat, datingLabels = file2matrix(filename)
print(datingDataMat) #归一化后的数据
print(datingLabels) #每一行数据的标签
执行结果:
上面的代码读取了datingTestSet1.txt里面的数据,将datingTestSet1.txt里面值经过处理后循环遍历,前三个字段的值存储在returnMat中,然后根据最后一个字段来判断豆子的分类,并将其对应的标签值(1,2,3)添加到classLableVector中,然后返回特征矩阵returnMat和分类标签向量classLableVector,然后将datingTestSet1.txt文件路径作为参数调用file2matrix()函数,将解析得到的特征矩阵存储在datingDataMat中,将分类标签存储在datingLabels中。打印归一化后的特征矩阵datingDataMat和每一行数据的标签datingLabels。
2.分析数据:使用Matplotlib创建散点图
代码如下:
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
# 数据展示
def showData(datingDataMat, datingLabels):
fig, axs = plt.subplots(nrows=2, ncols=2, sharex=False, sharey=False, figsize=(13, 8))
LabelsColors = []
for i in datingLabels:
if i == 1:
LabelsColors.append('black')
if i == 2:
LabelsColors.append('orange')
if i == 3:
LabelsColors.append('red')
axs[0][0].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=LabelsColors, s=15, alpha=.5)
axs0_title_text = axs[0][0].set_title('height_weight')
axs0_xlabel_text = axs[0][0].set_xlabel('height')
axs0_ylabel_text = axs[0][0].set_ylabel('weight')
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=LabelsColors, s=15, alpha=.5)
# 设置标题,x轴label,y轴label
axs1_title_text = axs[0][1].set_title('height_width')
axs1_xlabel_text = axs[0][1].set_xlabel('height')
axs1_ylabel_text = axs[0][1].set_ylabel('width')
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=LabelsColors, s=15, alpha=.5)
# 设置标题,x轴label,y轴label
axs2_title_text = axs[1][0].set_title('weight_width')
axs2_xlabel_text = axs[1][0].set_xlabel('weight')
axs2_ylabel_text = axs[1][0].set_ylabel('width')
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')
# 设置图例
BlackBean = mlines.Line2D([], [], color='black', marker='.',
markersize=6, label='BlackBean')
MungBean = mlines.Line2D([], [], color='orange', marker='.',
markersize=6, label='MungBean')
RedBean = mlines.Line2D([], [], color='red', marker='.',
markersize=6, label='RedBean')
# 添加图例
axs[0][0].legend(handles=[BlackBean, MungBean, RedBean])
axs[0][1].legend(handles=[BlackBean, MungBean, RedBean])
axs[1][0].legend(handles=[BlackBean, MungBean, RedBean])
# 显示图片
plt.show()
showData(datingDataMat,datingLabels)
执行结果:
这段代码实现了使用Matplotlib创建散点图。具体功能:创建一个包含2行2列的子图,即总共4个子图。根据传入的数据和标签,在第一个子图中绘制豆子植物的茎长和豆子的重量的散点图。在第二个子图中绘制豆子植物的茎长和豆子的宽度的散点图。在第三个子图中绘制豆子的重量和豆子的宽度的散点图。设置每个子图的标题、x轴标签和y轴标签。根据标签的不同,将散点的颜色分别设置为黑色、橙色和红色,分别表示不同标签的散点颜色。最后显示绘制好的散点图。
3. 对数据进行归一化
归一化:在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。
在此示例中的具体代码:
def autoNorm(dataSet):
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
#shape(dataSet)返回dataSet的矩阵行列数
normDataSet = np.zeros(np.shape(dataSet))
#返回dataSet的行数
m = dataSet.shape[0]
#用归一化公示进行处理
normDataSet = dataSet - np.tile(minVals, (m, 1))
normDataSet = normDataSet / np.tile(ranges, (m, 1))
#返回归一化数据结果,数据范围,最小值
return normDataSet, ranges, minVals
normDataSet, ranges, minVals = autoNorm(datingDataMat)
print(normDataSet)
print(ranges)
print(minVals)
执行结果:
归一化的具体实现原理可以看前面的博客https://blog.csdn.net/m0_63047033/article/details/133522587,对数据进行归一化处理后,返回归一化数据结果,数据范围,最小值。
4.测试算法:分类器的实现
import operator
# 输入:inX - 用于分类的数据(测试集);dataSet - 训练集;labes - 分类标签;K - KNN算法参数,选择距离最小的K个点
# 输出:sortedClassCount[0][0] - 分类结果
def classify0(inX, dataSet, labels, k):
#numpy函数shape[0]返回dataSet的行数
dataSetSize = dataSet.shape[0]
#在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#计算欧式距离
#二维特征相减后平方
sqDiffMat = diffMat**2
#sum()所有元素相加,sum(0)列相加,sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
#开方,计算出距离
distances = sqDistances**0.5
#返回distances中元素从小到大排序后的索引值
sortedDistIndices = distances.argsort()
#定一个记录类别次数的字典
classCount = {}
for i in range(k):
#取出前k个元素的类别
voteIlabel = labels[sortedDistIndices[i]]
#dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
#计算类别次数
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
#返回次数最多的类别,即所要分类的类别
return sortedClassCount[0][0]
m = normDataSet.shape[0]#返回数据集中样本的数量
numTestVecs = int(m*0.1)
errorCount = 0.0
for i in range(numTestVecs):
classifyResult = classify0(normDataSet[i,:],normDataSet[numTestVecs:m,:],
datingLabels[numTestVecs:m],4)
print("分类结果:%d,真实类别:%d" % (classifyResult,datingLabels[i]))
if(classifyResult!=datingLabels[i]):
errorCount += 1.0
print("错误率:%f%%" %(errorCount/float(numTestVecs)*100))
运行结果:
normDataSet.shape[0]返回数据集中样本的数量,numTestVecs = int(m*0.1)计算测试集的样本数量,因此这里采用了90%的训练集和10%的测试集的划分方式。在测试样本中找到训练集中距离该样本最近的前K个样本,统计这K个样本所属不同类别的出现次数,将出现次数最多的类别作为该测试样本的类别。在代码中我距离度量方法采用的是欧氏距离公式。距离度量方法同样可以参考之前的博客https://blog.csdn.net/m0_63047033/article/details/133522587
5.使用算法:输入数据进行测试
# 通过输入豆子的三维特征,进行分类输出
def classifyPerson():
#输出结果
resultList = ['黑豆','绿豆','红豆']
#三维特征用户输入
ffMiles = float(input("豆子植物的茎长:"))
precentTats = float(input("豆子的重量:"))
iceCream = float(input("豆子的宽度:"))
#打开的文件名
filename = "datingTestSet1.txt"
#打开并处理数据
datingDataMat, datingLabels = file2matrix(filename)
#训练集归一化
normMat, ranges, minVals = autoNorm(datingDataMat)
#生成NumPy数组,测试集
#inArr = np.array([precentTats, ffMiles, iceCream])
inArr = np.array([ffMiles, precentTats, iceCream])
#测试集归一化
norminArr = (inArr - minVals) / ranges
#返回分类结果
classifierResult = classify0(norminArr, normMat, datingLabels, 3) #直接指定k为3
#打印结果
print("这个豆子的种类是:%s" % (resultList[classifierResult-1]))
classifyPerson()
用户输入豆子的三维特征,包括豆子植物的茎长、豆子的重量和豆子的宽度。程序会根据输入的特征值,对训练集中的豆子进行分类,并输出豆子属于哪一种类型(黑豆、绿豆和红豆),辞职我们指定的k值为3?当我输入的三个数据分别为(1600,56,2)时,执行的结果如下图:
三、数据的分析
在上面的案例中我们设置的k=3,错误率为4%,可以看出模型效果还不错。当我们改变k值,或者改变
- 改变k值
- 当我们把k值增大10倍,当k=30时,此时我们得到的错误率仍为4%,这是因为当k增大时,模型会变得过于宽松,整体模型变得简单,甚至忽略了数据中的重要信息,当k增大到数据样本个数时,因为此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的类
- 当我们把k减小为1或2时,此时得到的错误率仍然为4%,但是时输入不变时,输出结果变成了‘红豆’,此时可能发生了过拟合或者欠拟合问题,导致错误率一直不变。但是预测的结果却发生了变化。
- 改变测试集和训练集的划分方式
- 当把90%的训练集和10%的测试集的划分方式改为80%的训练集和20%的测试集的划分方式时,当输入的数据不变时,错误率增大了,变成了8%,通过不断调整**,当改变改变测试机和训练集的划分方式时,错误率也会发生改变。**
四、模型优化
- K值的选择:KNN算法需要手动选定K值,较小的K值会容易出现过拟合问题;而较大的K值则会容易出现欠拟合问题。一般使用交叉验证等技术来确定最优的K值。
- 距离度量:KNN算法实现的关键在于距离的计算,不同的距离度量方式会影响KNN的分类效果。除了案例中的欧式距离、我们还应用曼哈顿距离(Manhanttan Distance)和切比雪夫距离(Chebyshev Distance)。在有些具体的问题中,也可以采用自定义的距离度量来优化模型。
五、knn算法的优点与缺点
- 优点
- 简单,易于理解,易于实现,无需估计参数。
- 对数据没有假设,准确度高,对异常点不敏感。
- 适合对稀有事件进行分类。
- 适合于多分类问题。
- 缺点
- 当特征非常多的时候,计算量太大。
- 样本不平衡问题(有些类别的样本数量很多,而其它样本的数量很少)。
- 对训练数据依赖度特别大,对训练数据的容错性太差。对训练数据依赖度特别大,对训练数据的容错性太差。