实验二 K-近邻算法及应用
博客班级 | 班级链接 |
---|---|
作业要求 | 作业链接 |
学号 | 3180701122 |
一.实验目的
1.理解K-近邻算法原理,能实现算法K近邻算法;
2.掌握常见的距离度量方法;
3.掌握K近邻树实现算法;
4.针对特定应用场景及数据,能应用K近邻解决实际问题。
二.实验内容
1.实现曼哈顿距离、欧氏距离、闵式距离算法,并测试算法正确性。
2.实现K近邻树算法;
3.针对iris数据集,应用sklearn的K近邻算法进行类别预测。
4.针对iris数据集,编制程序使用K近邻树进行类别预测。
三.实验过程及结果
实验代码及注释
import math
from itertools import combinations
#p = 1 曼哈顿距离
#p = 2 欧氏距离
#p = inf 闵式距离minkowski_distance
#度量距离
def L(x, y, p=2):
# x1 = [1, 1], x2 = [5,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)#求解sum=(|x1-y1|^p+|x2-y2|^p+...+|xi-yi|^p)
return math.pow(sum, 1/p)#对sum进行开根计算
else:
return 0
x1 = [1, 1]
x2 = [5, 1]
x3 = [4, 4]
# x1, x2
for i in range(1, 5):
#format格式化函数,通过 {} 和 : 来代替以前的 %
#此时r为一个字典,keys为点与点,values为度量距离
r = { '1-{}'.format(c):L(x1, c, p=i) for c in [x2, x3]}
print(min(zip(r.values(), r.keys())))#zip()将对象中的对应元素打包成一个个元组
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
# data
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)#矩阵数据表,将列名设为iris的特征
df['label'] = iris.target#加入一列为分类标签
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']#重命名列名(需要将每个都列出)
# data = np.array(df.iloc[:100, [0, 1, -1]])
df
#将标签为0、1的两种花,根据特征为长度和宽度打点表示
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
data = np.array(df.iloc[:100, [0, 1, -1]])#按行索引,取出第0列第1列和最后一列,即取出sepal长度、宽度和标签
X, y = data[:,:-1], data[:,-1]#X为sepal length,sepal width y为标签
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)# train_test_split函数用于将矩阵随机划分为训练子集和测试子集
class KNN:
def __init__(self, X_train, y_train, n_neighbors=3, p=2):
"""
parameter: n_neighbors 临近点个数
parameter: p 距离度量
"""
self.n = n_neighbors
self.p = p
self.X_train = X_train
self.y_train = y_train
def predict(self, X):
# 取出n个点,放入空的列表,列表中存放预测点与训练集点的距离及其对应标签
knn_list = []
for i in range(self.n):
#np.linalg.norm 求范数
dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
knn_list.append((dist, self.y_train[i]))
#再取出训练集剩下的点,然后与n_neighbor个点比较大叫,将距离大的点更新
#保证knn_list列表中的点是距离最小的点
for i in range(self.n, len(self.X_train)):
'''此处 max(num,key=lambda x: x[0])用法:
x:x[]字母可以随意修改,求最大值方式按照中括号[]里面的维度,
[0]按照第一维,
[1]按照第二维
'''
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)
#g更新最近邻中距离比当前点远的点
if knn_list[max_index][0] > dist:
knn_list[max_index] = (dist, self.y_train[i])
# 统计分类最多的点,确定预测数据的分类
knn = [k[-1] for k in knn_list]
#counter为计数器,按照标签计数
count_pairs = Counter(knn)
#排序
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)
test_point = [6.0, 3.0]#预测点
print('Test Point: {}'.format(clf.predict(test_point)))
#预测点
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
#打印预测点
plt.plot(test_point[0], test_point[1], 'bo', label='test_point')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
from sklearn.neighbors import KNeighborsClassifier
clf_sk = KNeighborsClassifier()
clf_sk.fit(X_train, y_train)
clf_sk.score(X_test, y_test)
# kd-tree每个结点中主要包含的数据结构如下 class KdNode(object):
def __init__(self, dom_elt, split, left, right):
self.dom_elt = dom_elt # k维向量节点(k维空间中的一个样本点)
self.split = split # 整数(进行分割维度的序号)
self.left = left # 该结点分割超平面左子空间构成的kd-tree
self.right = right # 该结点分割超平面右子空间构成的kd-tree
class KdTree(object):
def __init__(self, data):
k = len(data[0]) # 数据维度
def CreateNode(split, data_set): # 按第split维划分数据集exset创建KdNode
if not data_set: # 数据集为空
return None
# key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较
# operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为需要获取的数据在对象
#data_set.sort(key=itemgetter(split)) # 按要进行分割的那一维数据排序
data_set.sort(key=lambda x: x[split])
split_pos = len(data_set) // 2 # //为Python中的整数除法
median = data_set[split_pos] # 中位数分割点
split_next = (split + 1) % k # cycle coordinates
# 递归的创建kd树
return KdNode(median, split,
CreateNode(split_next, data_set[:split_pos]), # 创建左子树
CreateNode(split_next, data_set[split_pos + 1:])) # 创建右子树
self.root = CreateNode(0, data) # 从第0维分量开始构建kd树,返回根节点
# KDTree的前序遍历 def preorder(root):
print (root.dom_elt)
if root.left: # 节点不为空
preorder(root.left)
if root.right:
preorder(root.right)
#对构建好的kd树进行搜索,寻找与目标点最近的样本点:
from math import sqrt
from collections import namedtuple
#定义一个namedtuple,分别存放最近坐标点、最近距离和访问过的节点数
result = namedtuple("Result_tuple", "nearest_point nearest_dist nodes_visited")
def find_nearest(tree, point):
k = len(point) # 数据维度
def travel(kd_node, target, max_dist):
if kd_node is None:
return result([0] * k, float("inf"), 0) # python中用float("inf")和float("-inf")表示正负
nodes_visited = 1
s = kd_node.split # 进行分割的维度
pivot = kd_node.dom_elt # 进行分割的“轴”
if target[s] <= pivot[s]: # 如果目标点第s维小于分割轴的对应值(目标离左子树更近)
nearer_node = kd_node.left # 下一个访问节点为左子树根节点
further_node = kd_node.right # 同时记录下右子树
else: # 目标离右子树更近
nearer_node = kd_node.right # 下一个访问节点为右子树根节点
further_node = kd_node.left
temp1 = travel(nearer_node, target, max_dist) # 进行遍历找到包含目标点的区域
nearest = temp1.nearest_point # 以此叶结点作为“当前最近点”
dist = temp1.nearest_dist # 更新最近距离
nodes_visited += temp1.nodes_visited
if dist < max_dist:
max_dist = dist # 最近点将在以目标点为球心,max_dist为半径的超球体内
temp_dist = abs(pivot[s] - target[s]) # 第s维上目标点与分割超平面的距离
if max_dist < temp_dist: # 判断超球体是否与超平面相交
return result(nearest, dist, nodes_visited) # 不相交则可以直接返回,不用继续判断
#----------------------------------------------------------------------
# 计算目标点与分割点的欧氏距离
temp_dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(pivot, target)))
if temp_dist < dist: # 如果“更近”
nearest = pivot # 更新最近点
dist = temp_dist # 更新最近距离
max_dist = dist # 更新超球体半径
# 检查另一个子结点对应的区域是否有更近的点
temp2 = travel(further_node, target, max_dist)
nodes_visited += temp2.nodes_visited
if temp2.nearest_dist < dist: # 如果另一个子结点内存在更近距离
nearest = temp2.nearest_point # 更新最近点
dist = temp2.nearest_dist # 更新最近距离
return result(nearest, dist, nodes_visited)
return travel(tree.root, point, float("inf")) # 从根节点开始递归
data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
kd = KdTree(data)
preorder(kd.root)
from time import clock
from random import random
# 产生一个k维随机向量,每维分量值在0~1之间
def random_point(k):
return [random() for _ in range(k)]
# 产生n个k维随机向量
def random_points(k, n):
return [random_point(k) for _ in range(n)]
ret = find_nearest(kd, [3,4.5])
print (ret)
N = 400000
t0 = clock()
kd2 = KdTree(random_points(3, N)) # 构建包含四十万个3维空间样本点的kd树
ret2 = find_nearest(kd2, [0.1,0.5,0.8]) # 四十万个样本点中寻找离目标最近的点
t1 = clock()
print ("time: ",t1-t0, "s")
print (ret2)
实验结果截图
四.代码
4.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最近的点的距离
4.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)#将测试数据代入算法中
test_point = [6.0, 3.0]
print('Test Point: {}'.format(clf.predict(test_point))) #测试数据(6,3)应该属于哪一类
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() #表示不同图形的文本标签图案
4.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)
clf_sk.score(X_test, y_test) #测试精确度
clf_sk.predict([[6,3]]) #测试数据[6,3]应该属于哪一类
4.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)
五.实验小结
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近邻算法应该是目前工业上还会使用最为简单的算法,并且使用起来也很简单、方便,但是有个前提是数据量不能过大,更不能使用有维数诅咒的数据集。