实验二 K-近邻算法及应用
1. 实验目的
- 理解K-近邻算法原理,能实现算法K近邻算法;
- 掌握常见的距离度量方法;
- 掌握K近邻树实现算法;
- 针对特定应用场景及数据,能应用K近邻解决实际问题。
2. 实验内容
- 实现曼哈顿距离、欧氏距离、闵式距离算法,并测试算法正确性。
- 实现K近邻树算法;
- 针对iris数据集,应用sklearn的K近邻算法进行类别预测。
- 针对iris数据集,编制程序使用K近邻树进行类别预测。
3. 实验报告要求
- 对照实验内容,撰写实验过程、算法及测试结果;
- 代码规范化:命名规则、注释;
- 分析核心算法的复杂度;
- 查阅文献,讨论K近邻的优缺点;
- 举例说明K近邻的应用场景。
4. 作业信息
| 这个作业属于哪个课程 | 班级链接 |
|---|---|
| 这个作业要求在哪 | 作业链接 |
| 学号 | 3180701101 |
5. 代码
5.1 代码一: 距离度量
import math
from itertools import combinations
#当p=1时,就是曼哈顿距离;
#当p=2时,就是欧氏距离;
#当p→∞时,就是切比雪夫距离。
def L(x, y, p=2):
# x1 = [1, 1]
if len(x) == len(y) and len(x) > 1:
sum = 0
for i in range(len(x)):
sum += math.pow(abs(x[i] - y[i]), p)
return math.pow(sum, 1/p)
else:
return 0
x1 = [1, 1]
x2 = [5, 1]
x3 = [4, 4]
# x1与x2和x3的距离
for i in range(1, 5): #i取值1,2,3,4
r = { '1-{}'.format(c):L(x1, c, p=i) for c in [x2, x3]}
print(min(zip(r.values(), r.keys()))) #当p=i时x2和x3中离x1最近的点的距离
运行结果:
5.2 代码二:编写K近邻算法
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter
# 获取数据
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
data = np.array(df.iloc[:100, [0, 1, -1]]) #iloc函数:通过行号来取行数据,读取数据前100行的第0,1列和最后一列
#画出数据散点图
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0') #将数据的前50个数据绘制散点图
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1') #将数据的50-100之间的数据绘制成散点图
plt.xlabel('sepal length') #给x坐标命名
plt.ylabel('sepal width') #给y坐标命名
plt.legend()
运行结果:
# 绘制数据散点图
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0') # 绘制前50个数据的散点图
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1') # 绘制50-100个数据的散点图
plt.xlabel('sepal length')
plt.ylabel('sepal width') # 设置x,y轴坐标名
plt.legend() # 绘图
运行结果:
X, y = data[:,:-1], data[:,-1] #X为data数据中除去最后一列的数据,y为data数据的最后一列(y中有两类0和1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) #从样本中随机的按比例选取train data和testdata,样本占比为0.2及20
#生成K近邻算法
class KNN:
#初始化
def __init__(self, X_train, y_train, n_neighbors=3, p=2): #K的值( n_neighbors) 临近点个数; p 距离度量(欧氏距离)
self.n = n_neighbors
self.p = p
self.X_train = X_train
self.y_train = y_train
def predict(self, X):
# X为测试集
knn_list = []
for i in range(self.n):
dist = np.linalg.norm(X - self.X_train[i], ord=self.p) #计算测试集与(0-2)训练集的欧式距离
knn_list.append((dist, self.y_train[i]))#在列表knn_list最后(末尾)添加一个元素(dist, self.y_train[i])
for i in range(self.n, len(self.X_train)):#3-20
max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))#找出距离最大的点
dist = np.linalg.norm(X - self.X_train[i], ord=self.p) #计算测试集与(3-20)训练集的欧式距离
if knn_list[max_index][0] > dist: #距离最大的点大于新测试的点,就替换掉距离最大的点
knn_list[max_index] = (dist, self.y_train[i])
# 统计
knn = [k[-1] for k in knn_list]
count_pairs = Counter(knn) #统计各个标签的个数如 蓝点:2 ;黄点:1
max_count = sorted(count_pairs, key=lambda x:x)[-1] #升序排序,取个数最大的标签
return max_count
#用测试集测试算法的正确率
def score(self, X_test, y_test):
right_count = 0
n = 10
for X, y in zip(X_test, y_test):
label = self.predict(X)
if label == y:
right_count += 1
return right_count / len(X_test)
clf = KNN(X_train, y_train)#生成一个算法对象
clf.score(X_test, y_test)#将测试数据代入算法中
运行结果:1.0
test_point = [6.0, 3.0]
print('Test Point: {}'.format(clf.predict(test_point))) #测试数据(6,3)应该属于哪一类
运行结果:Test Point: 1.0
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')#将数据的前50个数据绘制散点图
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')#将数据的50-100个数据绘制散点图
plt.plot(test_point[0], test_point[1], 'bo', label='test_point') #将测试数据点(3,6)绘制在图中
plt.xlabel('sepal length') #给x坐标命名
plt.ylabel('sepal width') #给y坐标命名
plt.legend() #表示不同图形的文本标签图案
运行结果:
5.3 代码三:使用KNeighborsClassifier验证及引入K近邻算法对比验证
from sklearn.neighbors import KNeighborsClassifier
clf_sk = KNeighborsClassifier()
#sklearn.neighbors.KNeighborsClassifier
#n_neighbors: 临近点个数
#p: 距离度量
#algorithm: 近邻算法,可选{'auto', 'ball_tree', 'kd_tree', 'brute'}
#weights: 确定近邻的权重
clf_sk.fit(X_train, y_train)
运行结果:KNeighborsClassifier()
clf_sk.score(X_test, y_test) #测试精确度
运行结果:1.0
clf_sk.predict([[6,3]]) #测试数据[6,3]应该属于哪一类
运行结果:array([1.])
5.4 代码四:针对iris数据集,编制程序使用K近邻树进行类别预测。
import numpy as np
import time
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
class Node(object):
'''结点对象'''
def __init__(self, item=None, label=None, dim=None, parent=None, left_child=None, right_child=None):
self.item = item # 结点的值(样本信息)
self.label = label # 结点的标签
self.dim = dim # 结点的切分的维度(特征)
self.parent = parent # 父结点
self.left_child = left_child # 左子树
self.right_child = right_child # 右子树
class KDTree(object):
'''kd树'''
def __init__(self, aList, labelList):
self.__length = 0 # 不可修改
self.__root = self.__create(aList,labelList) # 根结点, 私有属性, 不可修改
def __create(self, aList, labelList, parentNode=None):
'''
创建kd树
:param aList: 需要传入一个类数组对象(行数表示样本数,列数表示特征数)
:labellist: 样本的标签
:parentNode: 父结点
:return: 根结点
'''
dataArray = np.array(aList)
m,n = dataArray.shape
labelArray = np.array(labelList).reshape(m,1)
if m == 0: # 样本集为空
return None
# 求所有特征的方差,选择最大的那个特征作为切分超平面
var_list = [np.var(dataArray[:,col]) for col in range(n)] # 获取每一个特征的方差
max_index = var_list.index(max(var_list)) # 获取最大方差特征的索引
# 样本按最大方差特征进行升序排序后,取出位于中间的样本
max_feat_ind_list = dataArray[:,max_index].argsort()
mid_item_index = max_feat_ind_list[m // 2]
if m == 1: # 样本为1时,返回自身
self.__length += 1
return Node(dim=max_index,label=labelArray[mid_item_index], item=dataArray[mid_item_index], parent=parentNode, left_child=None, right_child=None)
# 生成结点
node = Node(dim=max_index, label=labelArray[mid_item_index], item=dataArray[mid_item_index], parent=parentNode, )
# 构建有序的子树
left_tree = dataArray[max_feat_ind_list[:m // 2]] # 左子树
left_label = labelArray[max_feat_ind_list[:m // 2]] # 左子树标签
left_child = self.__create(left_tree,left_label,node)
if m == 2: # 只有左子树,无右子树
right_child = None
else:
right_tree = dataArray[max_feat_ind_list[m // 2 + 1:]] # 右子树
right_label = labelArray[max_feat_ind_list[m // 2 + 1:]] # 右子树标签
right_child = self.__create(right_tree,right_label,node)
# self.__length += 1
# 左右子树递归调用自己,返回子树根结点
node.left_child=left_child
node.right_child=right_child
self.__length += 1
return node
@property
def length(self):
return self.__length
@property
def root(self):
return self.__root
def transfer_dict(self,node):
'''
查看kd树结构
:node:需要传入根结点对象
:return: 字典嵌套格式的kd树,字典的key是self.item,其余项作为key的值,类似下面格式
{(1,2,3):{
'label':1,
'dim':0,
'left_child':{(2,3,4):{
'label':1,
'dim':1,
'left_child':None,
'right_child':None
},
'right_child':{(4,5,6):{
'label':1,
'dim':1,
'left_child':None,
'right_child':None
}
}
'''
if node == None:
return None
kd_dict = {}
kd_dict[tuple(node.item)] = {} # 将自身值作为key
kd_dict[tuple(node.item)]['label'] = node.label[0]
kd_dict[tuple(node.item)]['dim'] = node.dim
kd_dict[tuple(node.item)]['parent'] = tuple(node.parent.item) if node.parent else None
kd_dict[tuple(node.item)]['left_child'] = self.transfer_dict(node.left_child)
kd_dict[tuple(node.item)]['right_child'] = self.transfer_dict(node.right_child)
return kd_dict
def transfer_list(self,node, kdList=[]):
'''
将kd树转化为列表嵌套字典的嵌套字典的列表输出
:param node: 需要传入根结点
:return: 返回嵌套字典的列表
'''
if node == None:
return None
element_dict = {}
element_dict['item'] = tuple(node.item)
element_dict['label'] = node.label[0]
element_dict['dim'] = node.dim
element_dict['parent'] = tuple(node.parent.item) if node.parent else None
element_dict['left_child'] = tuple(node.left_child.item) if node.left_child else None
element_dict['right_child'] = tuple(node.right_child.item) if node.right_child else None
kdList.append(element_dict)
self.transfer_list(node.left_child, kdList)
self.transfer_list(node.right_child, kdList)
return kdList
def _find_nearest_neighbour(self, item):
'''
找最近邻点
:param item:需要预测的新样本
:return: 距离最近的样本点
'''
itemArray = np.array(item)
if self.length == 0: # 空kd树
return None
# 递归找离测试点最近的那个叶结点
node = self.__root
if self.length == 1: # 只有一个样本
return node
while True:
cur_dim = node.dim
if item[cur_dim] == node.item[cur_dim]:
return node
elif item[cur_dim] < node.item[cur_dim]: # 进入左子树
if node.left_child == None: # 左子树为空,返回自身
return node
node = node.left_child
else:
if node.right_child == None: # 右子树为空,返回自身
return node
node = node.right_child
def knn_algo(self, item, k=1):
'''
找到距离测试样本最近的前k个样本
:param item: 测试样本
:param k: knn算法参数,定义需要参考的最近点数量,一般为1-5
:return: 返回前k个样本的最大分类标签
'''
if self.length <= k:
label_dict = {}
# 获取所有label的数量
for element in self.transfer_list(self.root):
if element['label'] in label_dict:
label_dict[element['label']] += 1
else:
label_dict[element['label']] = 1
sorted_label = sorted(label_dict.items(), key=lambda item:item[1],reverse=True) # 给标签排序
return sorted_label[0][0]
item = np.array(item)
node = self._find_nearest_neighbour(item) # 找到最近的那个结点
if node == None: # 空树
return None
print('靠近点%s最近的叶结点为:%s'%(item, node.item))
node_list = []
distance = np.sqrt(sum((item-node.item)**2)) # 测试点与最近点之间的距离
least_dis = distance
# 返回上一个父结点,判断以测试点为圆心,distance为半径的圆是否与父结点分隔超平面相交,若相交,则说明父结点的另一个子树可能存在更近的点
node_list.append([distance, tuple(node.item), node.label[0]]) # 需要将距离与结点一起保存起来
# 若最近的结点不是叶结点,则说明,它还有左子树
if node.left_child != None:
left_child = node.left_child
left_dis = np.sqrt(sum((item-left_child.item)**2))
if k > len(node_list) or least_dis < least_dis:
node_list.append([left_dis, tuple(left_child.item), left_child.label[0]])
node_list.sort() # 对结点列表按距离排序
least_dis = node_list[-1][0] if k >= len(node_list) else node_list[k-1][0]
# 回到父结点
while True:
if node == self.root: # 已经回到kd树的根结点
break
parent = node.parent
# 计算测试点与父结点的距离,与上面距离做比较
par_dis = np.sqrt(sum((item-parent.item)**2))
if k >len(node_list) or par_dis < least_dis: # k大于结点数或者父结点距离小于结点列表中最大的距离
node_list.append([par_dis, tuple(parent.item) , parent.label[0]])
node_list.sort() # 对结点列表按距离排序
least_dis = node_list[-1][0] if k >= len(node_list) else node_list[k - 1][0]
# 判断父结点的另一个子树与结点列表中最大的距离构成的圆是否有交集
if k >len(node_list) or abs(item[parent.dim] - parent.item[parent.dim]) < least_dis : # 说明父结点的另一个子树与圆有交集
# 说明父结点的另一子树区域与圆有交集
other_child = parent.left_child if parent.left_child != node else parent.right_child # 找另一个子树
# 测试点在该子结点超平面的左侧
if other_child != None:
if item[parent.dim] - parent.item[parent.dim] <= 0:
self.left_search(item,other_child,node_list,k)
else:
self.right_search(item,other_child,node_list,k) # 测试点在该子结点平面的右侧
node = parent # 否则继续返回上一层
# 接下来取出前k个元素中最大的分类标签
label_dict = {}
node_list = node_list[:k]
# 获取所有label的数量
for element in node_list:
if element[2] in label_dict:
label_dict[element[2]] += 1
else:
label_dict[element[2]] = 1
sorted_label = sorted(label_dict.items(), key=lambda item:item[1], reverse=True) # 给标签排序
return sorted_label[0][0],node_list
def left_search(self, item, node, nodeList, k):
'''
按左中右顺序遍历子树结点,返回结点列表
:param node: 子树结点
:param item: 传入的测试样本
:param nodeList: 结点列表
:param k: 搜索比较的结点数量
:return: 结点列表
'''
nodeList.sort() # 对结点列表按距离排序
least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
if node.left_child == None and node.right_child == None: # 叶结点
dis = np.sqrt(sum((item - node.item) ** 2))
if k > len(nodeList) or dis < least_dis:
nodeList.append([dis, tuple(node.item), node.label[0]])
return
self.left_search(item, node.left_child, nodeList, k)
# 每次进行比较前都更新nodelist数据
nodeList.sort() # 对结点列表按距离排序
least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
# 比较根结点
dis = np.sqrt(sum((item-node.item)**2))
if k > len(nodeList) or dis < least_dis:
nodeList.append([dis, tuple(node.item), node.label[0]])
# 右子树
if k > len(nodeList) or abs(item[node.dim] - node.item[node.dim]) < least_dis: # 需要搜索右子树
if node.right_child != None:
self.left_search(item, node.right_child, nodeList, k)
return nodeList
def right_search(self,item, node, nodeList, k):
'''
按右根左顺序遍历子树结点
:param item: 测试的样本点
:param node: 子树结点
:param nodeList: 结点列表
:param k: 搜索比较的结点数量
:return: 结点列表
'''
nodeList.sort() # 对结点列表按距离排序
least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
if node.left_child == None and node.right_child == None: # 叶结点
dis = np.sqrt(sum((item - node.item) ** 2))
if k > len(nodeList) or dis < least_dis:
nodeList.append([dis, tuple(node.item), node.label[0]])
return
if node.right_child != None:
self.right_search(item, node.right_child, nodeList, k)
nodeList.sort() # 对结点列表按距离排序
least_dis = nodeList[-1][0] if k >= len(nodeList) else nodeList[k - 1][0]
# 比较根结点
dis = np.sqrt(sum((item - node.item) ** 2))
if k > len(nodeList) or dis < least_dis:
nodeList.append([dis, tuple(node.item), node.label[0]])
# 左子树
if k > len(nodeList) or abs(item[node.dim] - node.item[node.dim]) < least_dis: # 需要搜索左子树
self.right_search(item, node.left_child, nodeList, k)
return nodeList
if __name__ == '__main__':
t1 = time.time()
# 获取数据
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
data = np.array(df.iloc[:100, [0, 1, -1]])
X, y = data[:,:-1], data[:,-1] #X为data数据中除去最后一列的数据,y为data数据的最后一列(y中有两类0和1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) #从样本中随机的按比例选取train data和testdata,样本占比为0.2及20
kd_tree = KDTree(X_train,y_train)
t2 = time.time()
label, node_list = kd_tree.knn_algo([6,3],k=5)
print('点%s的最接近的前k个点为:%s'%([6,3], node_list))
print('点%s的标签:%s'%([6,3],label))
t3 = time.time()
print('创建树耗时:',t2-t1)
print('搜索前k个最近邻点耗时:',t3-t2)
运行结果:
6. 实验小结
K近邻算法优缺点:
算法优点:
(1)简单,易于理解,易于实现,无需估计参数。
(2)训练时间为零。它没有显示的训练,不像其它有监督的算法会用训练集train一个模型(也就是拟合一个函数),然后验证集或测试集用该模型分类。KNN只是把样本保存起来,收到测试数据时再处理,所以KNN训练时间为零。
(3)KNN可以处理分类问题,同时天然可以处理多分类问题,适合对稀有事件进行分类。
(4)特别适合于多分类问题(multi-modal,对象具有多个类别标签), KNN比SVM的表现要好。
(5)KNN还可以处理回归问题,也就是预测。
(6)和朴素贝叶斯之类的算法比,对数据没有假设,准确度高,对异常点不敏感。
算法缺点:
(1)计算量太大,尤其是特征数非常多的时候。每一个待分类文本都要计算它到全体已知样本的距离,才能得到它的第K个最近邻点。
(2)可理解性差,无法给出像决策树那样的规则。
(3)是慵懒散学习方法,基本上不学习,导致预测时速度比起逻辑回归之类的算法慢。
(4)样本不平衡的时候,对稀有类别的预测准确率低。当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。
(5)对训练数据依赖度特别大,对训练数据的容错性太差。如果训练数据集中,有一两个数据是错误的,刚刚好又在需要分类的数值的旁边,这样就会直接导致预测的数据的不准确。
K近邻算法的应用场景:
k近邻算法应该是目前工业上还会使用最为简单的算法,并且使用起来也很简单、方便,但是有个前提是数据量不能过大,更不能使用有维数诅咒的数据集。

浙公网安备 33010602011771号