1.导读
k近邻法(k-nearnest neighborm,k-NN)是一种基本分类与回归方法。从这个方法的名字上来看基本上就可以知道这个方法是怎么做的了——不需要训练模型,假定已有数据及其标签(自己输入),然后再输入一个未知的目标值,计算一下和这个目标值最近的“K”个数据分别是哪个,然后统计这k个数据的标签,看看哪类最多就以此预测我们的目标值的标签,用术语来说这个叫做“多数表决规则”,这跟操作系统中取内存时的“局部性原理”有点相似,用古语来说就是:物以类聚,人以群分。这个算法就是这么一个道理了。这次的博客将会从距离的计算方法、k值的选择、普通方法实现,kd tree实现这几个方面来进行分享。
2.距离度量
设特征空间
X
\ X
X是
n
\ n
n维实数向量空间
R
n
\ R^n
Rn,
x
i
,
x
j
∈
X
\ x_i,x_j \in X
xi,xj∈X,
x
i
=
(
x
i
(
1
)
,
x
i
(
2
)
,
.
.
.
,
x
i
(
n
)
)
T
\ x_i =(x_i^{(1)},x_i^{(2)},...,x_i^{(n)})^T
xi=(xi(1),xi(2),...,xi(n))T,
x
j
=
(
x
j
(
1
)
,
x
j
(
2
)
,
.
.
.
,
x
j
(
n
)
)
T
\ x_j =(x_j^{(1)},x_j^{(2)},...,x_j^{(n)})^T
xj=(xj(1),xj(2),...,xj(n))T,
x
i
,
x
j
\ x_i,x_j
xi,xj的
L
p
\ L_p
Lp距离定义为:
.
.
这里 p ≥ 1 \ p \ge 1 p≥1.当 p = 2 \ p=2 p=2时,称为欧氏距离,即
.
.
当 p = 1 \ p=1 p=1时,称为曼哈顿距离,即
.
.
当 p = ∞ \ p=\infty p=∞,为各个坐标距离的最大值,即
.
.
其中我们常用的是欧式距离,在二维坐标轴中求两点的距离也是使用的欧氏距离。我们知道了如何在n维实数向量空间中计算距离,那么问题就解决了一大半了。
3.k近邻算法的普通实现
在这里,k值的选择尤为重要,k值的选择会对算法的结果产生重大的影响:
1.k值选择过小,就会对临近的实例点十分敏感,但是如果这个实例点恰巧是噪声(不利因素)的话,就会预测出错,预计误差就会增大——随着k值的减小,意味着模型变得复杂,容易发生过拟合(在已知数据有较好的效果,但是对未知的数据预测效果很差)。
2.k值选择过大,会使得模型变得简单,如果k值等于实例点的值,那么无论输入什么数据,它预测的值都只会是实例中数量较大的那一方。
所以,在实际中,我们会使用交叉验证来确定k值(参考:《统计学习方法》):
# -*- coding: utf-8 -*-
"""
Created on Tue Jun 2 13:29:39 2020
@author: Fricerice
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
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]])
X, y = data[:,:-1], data[:,-1]
class KNN:
def __init__(self,X_train,Y_train,n_neighbors=3,p=2 ):
self.X_train=X_train
self.Y_train=Y_train
self.n=n_neighbors
self.p=p
self.nn=0
def predict(self,x):
knnList=[]
for i in range(self.n):
distance = np.linalg.norm(self.X_train[i]-x,ord=self.p)
knnList.append((distance,self.Y_train[i]))
for i in range(self.n,len(self.X_train)):
distance = np.linalg.norm(self.X_train[i]-x,ord=self.p)
mindis = min([k[0] for k in knnList])
if distance < mindis:
self.nn=(self.X_train[i],i)
maxIndex = knnList.index(max(knnList,key=lambda x:x[0]))
if knnList[maxIndex][0] > distance:
knnList[maxIndex]=(distance,self.Y_train[i])
result =[k[-1] for k in knnList]
# print(knnList)
countResult = Counter(result)
# print(countResult)
# mindis = [k[0] for k in knnList]
# print(mindis)
maxCount = sorted(countResult,key=lambda x:x)[-1]
# print(maxCount)
return maxCount
def score(self, X_test, y_test):
right_count = 0
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,4)
#print(clf.score(X_test, y_test))
#交叉验证k值
scoresList=[]
for n in range(1,10):#
scores=[]
for i in range(5):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
clf = KNN(X_train,y_train,n_neighbors=n)
scores.append(clf.score(X_test, y_test))
scoresList.append(np.mean(scores))
print(scoresList)
plt.figure()
plt.plot(range(1,10),scoresList)
plt.xlabel('Value of k for KNN')
plt.ylabel('accuracy')
plt.show()
运行结果:
4.kdtree的实现以及查询算法
上面普通实现是进行线性扫描,这样效率较低,为了提高搜索的效率,可以考虑使用平衡二叉树来进行数据存储,以减少计算距离的次数。
4.1构造平衡kd树算法:
输入:k维空间数据集
T
=
{
x
1
,
x
2
,
.
.
.
,
x
n
}
\ T=\{x_1,x_2,...,x_n\}
T={x1,x2,...,xn},其中
x
i
=
(
x
i
(
1
)
,
x
i
(
2
)
,
.
.
.
,
x
i
(
l
)
)
T
,
i
=
1
,
2
,
.
.
.
,
n
\ x_i=(x_i^{(1)},x_i^{(2)},...,x_i^{(l)})^T,i=1,2,...,n
xi=(xi(1),xi(2),...,xi(l))T,i=1,2,...,n,
l
\ l
l为数据的第几维
输出:kd树
构造过程:(构造过程和进行二分查找差不多)
(1)构造根结点,根结点的深度为0,维数选择为
l
=
d
e
p
t
h
%
m
+
1
\ l=depth\%m + 1
l=depth%m+1,其中m为输入数据的维数(很多书里面都写成k,容易和k近邻进行混淆),将数据集按照
l
\ l
l维进行排序,选择中位数作为本次的结点(后面都是这样做)
(2)为根节点构造左右结点:将数据集从中间切开,各分成两份再各自根据维数(深度为1时选择第2维)排序后选择中位数。
(3)重复上述操作直到数据集的数据都被使用完毕。
例子(参考《统计学习方法》):
数据集: T = { ( 2 , 3 ) T , ( 5 , 4 ) T , ( 9 , 6 ) T , ( 4 , 7 ) T , ( 8 , 1 ) T , ( 7 , 2 ) T } \ T=\{(2,3)^T,(5,4)^T,(9,6)^T,(4,7)^T,(8,1)^T,(7,2)^T\} T={(2,3)T,(5,4)T,(9,6)T,(4,7)T,(8,1)T,(7,2)T}
构造结果:
4.2kd树的k近邻搜索
算法实现:
数据结构准备:
kd树、栈1(存储访问的结点)、数组(存储k个最近的)
(1)从根节点开始向下访问结点(按照构造时的维数规则进行比较)直到找到叶节点,然后计算叶节点和目标值的距离作为当前的最小距离(mindis),该叶结点作为最近邻点,并把该最小距离值存储到数组中。
(2)计算叶结点的父节点与目标值的最小距离,与当前最小距离比较,若小于当前最小距离,则替换最近邻点和最近距离值(但是也要把当前值存到上述的数据结构中,以便查找最近k个)
(3)判断是否进入父节点的另一结点:
a.当前叶结点在父节点维数上的距离若小于等于当前最小距离(即目标值和叶结点/父节点距离所形成的超球体是否与父节点的超矩形相交,想象一下刀切苹果的那种样子),则进入,重复(1)
b.若不满足,则将当前父节点当中叶结点,重复(2)
最终根据数组中的值进行判断。
代码实现:
# -*- coding: utf-8 -*-
"""
Created on Tue Jun 2 17:44:59 2020
@author: Frierice
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter
class KdNode:
def __init__(self,axis,data,rightNode,leftNode):
self.axis = axis
self.data = data
self.rightNode =rightNode
self.leftNode =leftNode
class KDTree(object):
def __init__(self,data,k=5):
self.m = len(data[0])-1#划分最大维数,最后一个是标签
self.stackOfKNN=[]
self.k =k #最近点数
self.nn=None #目前最近点
self.mindis=0
def createTreeNode(axis,data):
if len(data)==0:
return None
data=sorted(data,key=lambda x:x[axis])#根据划分维数排序
mid_index = len(data)//2
point = data[mid_index]
next_axis = (axis+1) % self.m
return KdNode(axis,point,createTreeNode(next_axis,data[0:mid_index]),
createTreeNode(next_axis,data[mid_index+1:]))
self.root = createTreeNode(0,data)
def pushStack(self,point,nowPoint):
distance = np.linalg.norm(point[0:-1]-nowPoint[0:-1],ord=2)
if len(self.stackOfKNN)+1 > self.k :
maxIndex = self.stackOfKNN.index(max(self.stackOfKNN,key=lambda x:x[0]))
if self.stackOfKNN[maxIndex][0] > distance:
self.stackOfKNN[maxIndex]=(distance,nowPoint[-1])
else:
self.stackOfKNN.append((distance,nowPoint[-1]))
def findNode(self,testdata,root):
stack = []
if root != None:
stack.append(root)
else:
return -1
nowpoint = stack[-1]
flag=0
# mindis=0
while stack:
# if self.nn != None:
# print(self.nn.data)
if nowpoint.rightNode == None and nowpoint.leftNode == None:
# self.nn = nowpoint
if self.nn !=None:
self.mindis = np.linalg.norm(testdata[0:-1]-self.nn.data[0:-1],ord=2)
distance = np.linalg.norm(testdata[0:-1]-nowpoint.data[0:-1],ord=2)
if distance <= self.mindis:
self.nn = nowpoint
self.mindis=distance
else:
self.nn = nowpoint
self.mindis = np.linalg.norm(testdata[0:-1]-nowpoint.data[0:-1],ord=2)
self.pushStack(testdata,nowpoint.data)
flag=1
if flag==1:
# self.nn = nowpoint
# mindis = np.linalg.norm(testdata[0:-1]-nowpoint.data[0:-1],ord=2)
self.pushStack(testdata,nowpoint.data)
# print(nowpoint.data)
parent = stack.pop(-1)
distance = np.linalg.norm(testdata[0:-1]-parent.data[0:-1],ord=2)
if distance <= self.mindis:
self.nn = parent
self.mindis=distance
self.pushStack(testdata,parent.data)
# nowpoint=parent
left=False
if nowpoint==parent.leftNode:
left=True
nowpoint = parent
if left and parent.rightNode!=None:
if abs(parent.data[parent.axis] - testdata[parent.axis]) <= self.mindis:
self.findNode(testdata,parent.rightNode)
if not left and parent.leftNode!=None:
if abs(parent.data[parent.axis] - testdata[parent.axis]) <= self.mindis:
self.findNode(testdata,parent.leftNode)
else:
nowAxis = nowpoint.axis
if testdata[nowAxis] <= nowpoint.data[nowAxis]:
if nowpoint.rightNode !=None:
stack.append(nowpoint)
nowpoint=nowpoint.rightNode
else:
stack.append(nowpoint)
nowpoint=nowpoint.leftNode
else:
if nowpoint.leftNode !=None:
stack.append(nowpoint)
nowpoint=nowpoint.leftNode
else:
stack.append(nowpoint)
nowpoint=nowpoint.rightNode
def predict(self,data):
self.stackOfKNN=[]
self.findNode(data,self.root)
result =[k[-1] for k in self.stackOfKNN]
countResult = Counter(result)
maxCount = sorted(countResult,key=lambda x:x)[-1]
return maxCount
def score(self, dataset):
right_count = 0
for i in dataset:
label = self.predict(i)
if label == i[-1]:
right_count += 1
return right_count / len(dataset)
iris = load_iris()
#print(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']
#print(df)
data = np.array(df.iloc[:100, [ 0,1 , -1]])
##X, y = data[:,:-1], data[:,-1]
#
scoresList=[]
for n in range(1,10):#
scores=[]
for i in range(5):
X_train, X_test = train_test_split(data, test_size=0.2)
clf = KDTree(X_train,n)
scores.append(clf.score(X_test))
print(len(clf.stackOfKNN))
scoresList.append(np.mean(scores))
print(scoresList)
plt.figure()
plt.plot(range(1,10),scoresList)
plt.xlabel('Value of k for KNN')
plt.ylabel('accuracy')
plt.show()
#data=[[2,3,1],[5,4,1],[9,6,1],[4,7,1],[8,1,1],[7,2,1],[10,100,1]]
#data = np.array(data)
#clf=KDTree(data[:-1],5)
#clf.predict(data[-1])
##
##print(data[-1])
#print(clf.mindis)
#print(clf.nn.data)
5.总结
以上就是最近学习的心得和代码的分享,kdtree实现的代码不够完美,希望大家多点交流指错,谢谢。下一个博客会进行朴素贝叶斯法的分享。
参考:《统计学习方法》