本练习包括对KNN算法原理的介绍,kNN算法的步骤流程,以及如何自己动手实现kNN算法。
一、在VS Code中配置python环境
在VS Code中安装python创建
在工作区中新建.py文件成功即配置成功
二、引入numpy库
1、查看python安装目录
在环境变量中查看python安装目录
可能安装有多个python版本,可以在命令行中输入python --version
查看版本信息,检查是否与环境配置版本一致
2、安装numpy库
-
进入到进入python 版本安装目录下的Script目录中,以
C:\Users\xxx\AppData\Local\Programs\Python\Python312\Scripts\
为例 -
安装numpy的时候需要先更新pip,使用最新版的pip来安装
python -m pip install --upgrade pip
- 开始安装
pip install numpy
- 接下来在命令行窗口运行
python
- 然后运行
from numpy import *
- 没有报错即为成功
Python报错ModuleNotFoundError: No module named 'numpy’解决方法:
https://blog.csdn.net/qq_39779233/article/details/103224712
三、KNN算法介绍
1、kNN算法简介
k近邻法(k-nearest neighbor, kNN)是1967年由Cover T和Hart P提出的一种基本分类与回归方法。它的工作原理是:存在一个样本数据集合,也称作为训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一个数据与所属分类的对应关系。输入没有标签的新数据后,将新的数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 kNN方法在类别决策时,只与极少量的相邻样本有关。由于kNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,kNN方法较其他方法更为适合。
上图中,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果K=3,由于红色三角形所占比例为2/3,绿色圆将被赋予红色三角形那个类,如果K=5,由于蓝色四方形比例为3/5,因此绿色圆被赋予蓝色四方形类。
K最近邻(k-Nearest Neighbor,KNN)分类算法,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。该方法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 KNN方法虽然从原理上也依赖于极限定理,但在类别决策时,只与极少量的相邻样本有关。由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。
KNN算法不仅可以用于分类,还可以用于回归。通过找出一个样本的k个最近邻居,将这些邻居的属性的平均值赋给该样本,就可以得到该样本的属性。更有用的方法是将不同距离的邻居对该样本产生的影响给予不同的权值(weight),如权值与距离成反比。
2、kNN计算流程
- 计算已知类别数据集中的点与当前点之间的距离;
- 按照距离递增次序排序;
- 选取与当前点距离最小的k个点;
- 确定前k个点所在类别的出现频率;
- 返回前k个点所出现频率最高的类别作为当前点的预测分类。
四、算法实现
- 首先要引入我们系统需要用到的Python的相关库。其中,numpy为Python的数值计算扩展库,operator为操作符扩展库,listdir提供了可以查看目录文件列表的函数。
import numpy as np
import operator
from os import listdir
- kNN算法流程如下:
a.计算已知类别数据集中的点与当前点之间的距离;
b.按照距离递增次序排序;
c.选取与当前点距离最小的k个点;
d.确定前k个点所在类别的出现频率;
e.返回前k个点所出现频率最高的类别作为当前点的预测分类。
在kNN.py中,添加一个函数classify0作为kNN算法的核心函数,该函数的完整形式为:
def classify0(inX, dataSet, labels, k):
其中各个参数的含义如下:
inX - 用于要进行分类判别的数据(来自测试集)
dataSet - 用于训练的数据(训练集)
lables - 分类标签
k - kNN算法参数,选择距离最小的k个点
在上述参数列表中,dataSet为所有训练数据的集合,也就是表示所有已知类别数据集中的所有点,dataSet为一个矩阵,其中每一行表示已知类别数据集中的一个点。inX为一个向量,表示当前要判别分类的点。按照上述算法流程,我们首先应该计算inX这个要判别分类的点到dataSet中每个点之间的距离。dataSet中每个点也是用一个向量表示的,点与点之间的距离怎么计算呢?没错,就是求两向量之间的距离,数学上,我们知道有很多距离计算公式,包括但不限于:
- 欧氏距离
- 曼哈顿距离
- 切比雪夫距离
- 闵可夫斯基距离
- 标准化欧氏距离
- 马氏距离
- 夹角余弦
- 汉明距离
- 杰卡德距离& 杰卡德相似系数
- 信息熵
这里,我们选择最简单的欧式距离计算方法。设p和q为两向量,则两向量间的欧氏距离为:
在算法流程,输入参数含义,以及距离计算公式都明确了以后,按照kNN算法的流程,我们就可以实现kNN算法了。这里,我们使用numpy提供的各种功能来实现该算法,相比于自己纯手写各种线性代数变换操作,使用numpy的效率要高的多。classify0的实现如下:
def classify0(inX, dataSet, labels, k):
### Start Code Here ###
#返回dataSet的行数,即已知数据集中的所有点的数量
dataSetSize = dataSet.shape[0]
#行向量方向上将inX复制m次,然后和dataSet矩阵做相减运算
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#减完后,对每个数做平方
sqDiffMat = diffMat ** 2
#平方后按行求和,axis=0表 示列相加,axis-1表示行相加
sqDistances = sqDiffMat.sum(axis=1)
#开方计算出欧式距离
distances = sqDistances ** 0.5
#对距离从小到大排序,注意argsort函数返回的是数组值从小到大的索引值2
sortedDistIndicies = distances.argsort()
#用于类别/次数的字典,key为类别, value为次数
classCount = {}
#取出第近的元素对应的类别
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
#对类别次数进行累加
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
#根据字典的值从大到小排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
#返回次数最多的类别,即所要分类的类别
return sortedClassCount[0][0]
### End Code Here ###
接下来,我们用个小例子验证一下kNN算法,随机挑选的6位高中生,分别让他们做文科综合试卷的分数和理科综合试卷的分数,下表为分数以及分类信息。直觉上,理科生的理综成绩比较高,文综成绩较低,文科生的文综成绩较高,理综成绩较高。基于这些信息,我们利用kNN算法判断成绩为(105,210)所属的分类。
运行结果显示“文科生”,输出符合预期
五、完整代码
import numpy as np
import operator
from os import listdir
#这个函数的核心功能是计算一个测试数据点与已知数据集中所有点的距离,然后根据距离选择最近的 k 个点,
#并根据这些点的分类情况进行投票,最终选择票数最多的类别作为预测结果。
def classify0(inX, dataSet, labels, k):
### Start Code Here ###
#返回dataSet的行数,即已知数据集中的所有点的数量
dataSetSize = dataSet.shape[0]
#行向量方向上将inX复制m次,然后和dataSet矩阵做相减运算
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#减完后,对每个数做平方
sqDiffMat = diffMat ** 2
#平方后按行求和,axis=0表 示列相加,axis-1表示行相加
sqDistances = sqDiffMat.sum(axis=1)
#开方计算出欧式距离
distances = sqDistances ** 0.5
#对距离从小到大排序,注意argsort函数返回的是数组值从小到大的索引值2
sortedDistIndicies = distances.argsort()
#用于类别/次数的字典,key为类别, value为次数
classCount = {}
#取出第近的元素对应的类别
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
#对类别次数进行累加
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
#根据字典的值从大到小排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
#返回次数最多的类别,即所要分类的类别
return sortedClassCount[0][0]
dataSet=np.array([[250,100],[270,120],[111,230],[130,260],[200,80],[70,190]])
labels=["理科生","理科生","文科生","文科生","理科生","文科生"]
# inX=[105,210]
inX=[200,200]
print(classify0(inX,dataSet,labels,3))
六、思考总结
1 、k值的选择是否会影响算法
在K-最近邻算法中,K值的选择确实会对结果产生显著的影响。K值是算法中的一个关键参数,它决定了在进行分类或回归时考虑的最近邻居的数量。以下是K值选择对KNN算法结果的一些影响:
-
过拟合与欠拟合:如果K值过小,模型可能会过于依赖于训练集中的噪声,导致过拟合;如果K值过大,模型可能无法捕捉到数据的局部特性,导致欠拟合。
-
分类边界:较小的K值通常会产生更复杂的分类边界,因为它更多地依赖于局部的点;较大的K值则会产生更平滑的分类边界,因为它考虑了更多的邻居。
-
计算复杂度:K值较小时,计算复杂度较高,因为需要对每个测试点计算与所有训练点的距离;K值较大时,计算复杂度相对较低。
-
结果的稳定性:较小的K值可能导致结果的波动较大,因为单个数据点的变化可能对预测结果产生较大影响;较大的K值则使得结果更加稳定。
-
对异常值的敏感性:较小的K值意味着算法对异常值更敏感,因为异常值可能会被错误地归类为最近的邻居。
2、k值的选择方法
常用的方法:
- 从k=1开始,使用检验集估计分类器的误差率。
- 重复该过程,每次K增值1,允许增加一个近邻。
- 选取产生最小误差率的K。
注意:
- 一般k的取值不超过20,上限是n的开方,随着数据集的增大,K的值也要增大。
- 一般k值选取比较小的数值,并采用交叉验证法选择最优的k值。
3、剔除无效或效果不佳的样本
常见的方法包括:
-
异常值检测与处理:首先,可以使用异常值检测技术(如离群点检测)来识别和剔除那些在特征空间中明显偏离其他样本的异常值。这可以通过统计方法、聚类方法或基于距离的方法来完成。
-
特征选择:在KNN中,特征的选择对算法的性能影响很大。通过分析特征之间的相关性和对分类结果的影响,可以选择保留最相关的特征,剔除对分类结果影响较小的特征。
-
交叉验证:使用交叉验证技术来评估每个样本在模型中的贡献。通过交叉验证,可以识别那些在模型中起着不重要作用或者可能过拟合的样本,并据此进行剔除或者调整。
-
领域知识:结合领域知识来剔除无效样本。
-
数据清洗:在样本收集和数据预处理阶段,进行数据清洗,剔除不完整、不准确或者重复的样本,以保证数据的质量。
4、如何提高算法性能
要提高KNN算法的性能和计算效率,可以考虑以下几个方面的修改:
-
使用更高效的数据结构:目前代码中使用的是NumPy数组来存储数据集,可以考虑使用更高效的数据结构,比如KD树或球树等。这些数据结构可以加速搜索过程,特别是对于大型数据集。
-
减少冗余计算:在当前的代码中,可能存在一些冗余计算,比如重复计算距离的平方和开方。可以通过优化计算过程,避免重复计算,提高效率。
-
使用并行计算:对于大规模数据集,可以考虑使用并行计算来加速距离计算和排序过程。Python中有很多并行计算的库可以使用,比如NumPy的并行功能或者使用多线程/多进程来加速计算过程。
-
降维处理:如果特征空间维度很高,可以考虑使用降维技术来减少计算复杂度,比如主成分分析(PCA)或者特征选择方法来选择最相关的特征。
-
适当选择K值:KNN算法中的K值选择对性能有很大影响。过小的K值容易受到噪声的影响,过大的K值可能会导致过拟合。可以通过交叉验证等技术来选择最优的K值。
-
优化代码逻辑:对算法中的循环和条件语句进行优化,尽量减少不必要的计算和判断。
基于以上思路,以下是修改后的代码示例:
import numpy as np
import operator
# 使用KD树或球树来加速搜索过程,这里示例使用KD树
from sklearn.neighbors import KDTree
def classify0(inX, tree, labels, k):
# 在KD树中查询最近的K个点
_, idx = tree.query([inX], k=k)
# 统计最近K个点的类别
class_count = {}
for i in idx[0]:
vote_label = labels[i]
class_count[vote_label] = class_count.get(vote_label, 0) + 1
# 根据投票结果排序
sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reverse=True)
# 返回票数最多的类别
return sorted_class_count[0][0]
# 实例给出的训练数据
dataSet = np.array([[250, 100], [270, 120], [111, 230], [130, 260], [200, 80], [70, 190]])
labels = ["理科生", "理科生", "文科生", "文科生", "理科生", "文科生"]
# 使用KD树构建索引
tree = KDTree(dataSet, leaf_size=2)
# 测试数据点
inX = [105, 210]
# 选择最近的3个邻居进行分类
print(classify0(inX, tree, labels, 3))
这个修改后的代码利用了KD树来加速搜索过程,从而提高了算法的性能和计算效率。
- 安装
scikit-learn
库,方法如numpy
库安装流程
pip install scikit-learn
- 运行代码
成功
- 如果无法安装
scikit-learn
,也可以考虑使用其他方法来构建KD树,比如自己实现KD树算法或者使用其他第三方库实现。以下是一个简单的KD树的实现示例:
import numpy as np
import heapq
from collections import deque
class KDTreeNode:
def __init__(self, point, axis, label=None):
self.point = point
self.axis = axis
self.label = label
self.left = None
self.right = None
def build_kdtree(points, labels, depth=0):
if not points:
return None
axis = depth % len(points[0])
points.sort(key=lambda x: x[axis])
median_idx = len(points) // 2
node = KDTreeNode(points[median_idx], axis, labels[median_idx])
node.left = build_kdtree(points[:median_idx], labels[:median_idx], depth + 1)
node.right = build_kdtree(points[median_idx + 1:], labels[median_idx + 1:], depth + 1)
return node
def nearest_neighbors(tree, target, k):
def distance(point1, point2):
return np.linalg.norm(np.array(point1) - np.array(point2))
def _search(node):
if not node:
return
dist = distance(node.point, target)
heapq.heappush(nearest, (-dist, node.point, node.label))
axis = node.axis
if target[axis] < node.point[axis]:
_search(node.left)
else:
_search(node.right)
if len(nearest) < k or abs(target[axis] - node.point[axis]) < -nearest[0][0]:
if target[axis] < node.point[axis]:
_search(node.right)
else:
_search(node.left)
nearest = []
_search(tree)
return [(label, point) for _, point, label in heapq.nlargest(k, nearest)]
def classify0(inX, tree, labels, k):
neighbors = nearest_neighbors(tree, inX, k)
class_count = {}
for _, point_label in neighbors:
class_count[point_label] = class_count.get(point_label, 0) + 1
sorted_class_count = sorted(class_count.items(), key=lambda x: x[1], reverse=True)
return sorted_class_count[0][0]
# 训练数据
dataSet = np.array([[250, 100], [270, 120], [111, 230], [130, 260], [200, 80], [70, 190]])
labels = ["理科生", "理科生", "文科生", "文科生", "理科生", "文科生"]
# 构建KD树
tree = build_kdtree(dataSet, labels)
# 测试数据点
inX = [105, 210]
# 选择最近的3个邻居进行分类
print(classify0(inX, tree, labels, 3))
orted(class_count.items(), key=lambda x: x[1], reverse=True)
return sorted_class_count[0][0]
# 训练数据
dataSet = np.array([[250, 100], [270, 120], [111, 230], [130, 260], [200, 80], [70, 190]])
labels = ["理科生", "理科生", "文科生", "文科生", "理科生", "文科生"]
# 构建KD树
tree = build_kdtree(dataSet, labels)
# 测试数据点
inX = [105, 210]
# 选择最近的3个邻居进行分类
print(classify0(inX, tree, labels, 3))