k近邻算法(k-NN)是一种基本分类与回归方法,它有三个基本要素,本文将介绍k近邻算法的模型与kd树。
k近邻算法
给定一个训练数据集,对于新输入的实例,在训练数据集中找到与该实例最临近的k个实例,这k个实例的多数属于某个类,就把该输入实例分为这个类。
数学实现
输入:训练数据集
T
=
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
⋅
⋅
⋅
(
x
N
,
y
N
)
}
T=\{(x_1,y_1),(x_2,y_2),···(x_N,y_N)\}
T={(x1,y1),(x2,y2),⋅⋅⋅(xN,yN)}
其中,
x
i
∈
χ
=
R
n
x_i\in\chi=R^n
xi∈χ=Rn ,
y
i
∈
Y
=
{
c
1
,
c
2
,
⋅
⋅
⋅
,
c
K
}
y_i\in Y=\{c_1,c_2,···,c_K\}
yi∈Y={c1,c2,⋅⋅⋅,cK}为实例的类别,
i
=
1
,
2
,
⋅
⋅
⋅
N
i=1,2,···N
i=1,2,⋅⋅⋅N;实例特征向量
x
x
x
输出:实例x所属的类y。
- 1.根据给定的距离度量,在训练集 T T T中找出与 x x x最近邻的 k k k个点,涵盖这 k k k个点的 x x x的领域记作 N k ( x ) N_k(x) Nk(x)。
- 2.在 N k ( x ) N_k(x) Nk(x)中根据分类决策规则(如多数表决)决定 x x x的类别 y y y。
y
=
a
r
g
m
a
x
c
j
∑
x
i
∈
N
k
(
x
)
I
(
y
i
=
c
j
)
,
i
=
1
,
2
,
3
,
⋅
⋅
⋅
,
N
;
j
=
1
,
2
,
⋅
⋅
⋅
,
K
y=argmax_{cj}\sum_{x_i\in N_k(x)}I(y_i=c_j),i=1,2,3,···,N;j=1,2,···,K
y=argmaxcjxi∈Nk(x)∑I(yi=cj),i=1,2,3,⋅⋅⋅,N;j=1,2,⋅⋅⋅,K
式中
I
I
I为指示函数,即当
y
i
=
c
j
y_i=c_j
yi=cj时
I
I
I为
1
1
1,否则
I
I
I为0.
在
k
=
1
k=1
k=1时称为最近邻算法,k近邻法没有显式的学习过程。
模型建立
模型三要素为:距离度量, K K K的大小和分类规则。
距离度量
- 闵可夫斯基距离(Minkowski Distance)
D ( x , y ) = ( ∑ i = 1 m ∣ x i − y i ∣ p ) 1 p D(x,y)=(\sum\limits_{i=1}^m|x_i-y_i|^p)^\frac{1}{p} D(x,y)=(i=1∑m∣xi−yi∣p)p1
其中 p ≥ 1 p\geq 1 p≥1。 - 当 p = 2 p=2 p=2时,是欧式距离。
- 当 p = 1 p=1 p=1时,是曼哈顿距离。
k k k的选择
k k k的选择会对结果产生重大影响。
- k k k的值过小,极端情况下 k = 1 k=1 k=1,测试实例只和最接近的一个样本有关,训练误差很小,但是如果这个样本恰好是噪点,预测就会出错,即产生了过拟合。
- 如果 k k k值过大,极端情况 k = n k=n k=n,则会产生欠拟合。
姑通常采用交叉验证法来选取合适的 k k k。
分类规则
k
k
k近邻的分类决策通常是多数表决:由测试样本的
k
k
k个临近样本的多数类决定测试样本的类别。有如下规则:
给定测试样本
x
x
x,其最临近的
k
k
k个训练示例构成的集合
N
k
(
x
)
N_k(x)
Nk(x),分类损失函数为
0
−
1
0-1
0−1型损失,如果涵盖
N
k
(
x
)
N_k(x)
Nk(x)区域的类别为
c
j
c_j
cj,则分类误差率为:
1
k
∑
x
i
∈
N
k
(
x
)
I
{
y
i
≠
c
j
}
=
1
−
1
k
\frac{1}{k}\sum_{x_i\in N_k(x)}I\{y_i\neq c_j\}=1-\frac{1}{k}
k1xi∈Nk(x)∑I{yi=cj}=1−k1
∑
x
i
∈
N
k
(
x
)
I
{
y
i
=
c
j
}
\sum_{x_i\in N_k(x)}I\{y_i=c_j\}
xi∈Nk(x)∑I{yi=cj}
要使得分类误差率最小,就是要使
∑
x
i
∈
N
k
(
x
)
I
{
y
i
=
c
j
}
\sum_{x_i\in N_k(x)}I\{y_i=c_j\}
∑xi∈Nk(x)I{yi=cj}最大,所以多数表决规则等价于误分类绿最小。
实现: k d kd kd树
k d kd kd树算法有三步:
- 构造 k d kd kd树
- 搜索 k k k近邻
- 预测
k d kd kd树的构建
- 选取 x ( 1 ) x^{(1)} x(1)为坐标轴,以训练集中的所有数据 x ( 1 ) x^{(1)} x(1)坐标中的中位数作为切分点,将超矩形区域切割成两个子区域。将该切分点作为根结点,由根结点生出深度为1的左右子结点,左节点对应 x ( 1 ) x^{(1)} x(1)坐标小于切分点,右结点对应 x ( 1 ) x^{(1)} x(1)坐标大于切分点。
- 对深度为 j j j的结点,选择 x ( l ) x^{(l)} x(l)为切分坐标轴, l = j ( m o d k ) + 1 l=j(mod k)+ 1 l=j(modk)+1,以该结点区域中训练数据 x ( l ) x^{(l)} x(l)坐标的中位数作为切分点,将区域分为两个子区域,且生成深度为 j + 1 j+1 j+1的左、右子结点。左节点对应 x ( l ) x^{(l)} x(l)坐标小于切分点,右结点对应 x ( l ) x^{(l)} x(l)坐标大于切分点
- 重复2,直到两个子区域没有数据时停止。
实例参考KNN算法和kd树详解(例子+图示)
k d kd kd的搜索
输入:已构造的
k
d
kd
kd树,目标点
x
x
x
输出:
x
x
x的最近邻
- 在kd树中找出包含目标点 x x x的叶结点;从根结点,递归地向下访问kd树,若目标点 x x x当前纬的坐标小于分切点,则移动到左子结点,直到子结点为叶子结点为止。
- 此叶子结点为"当前最近点"。
- 递归地向上回退,在每个结点都进行如下操作:
(a)如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”
(b)当前最近点一定存在于该结点一个子结点对应的区域,检查该子结点的父结点的另一个子结点对应的区域是否有更近的点。具体的,检查另一子结点对应的区域是否与以目标点为球心,以目标点与“当前最近点”间的距离为半径的超球体相交。
(c)如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;如果不相交,则向上退回。
- 当退回到根结点时,搜索结束。最后一个“当前最近点”即为 x x x的最近邻点。
实例参考KNN算法和kd树详解(例子+图示)
Python代码实现
import numpy as np
class Node:
def __init__(self, data, lchild = None, rchild = None):
self.data = data
self.lchild = lchild
self.rchild = rchild
class KdTree:
def __init__(self):
self.kdTree = None
def create(self, dataSet, depth): #创建kd树,返回根结点
if (len(dataSet) > 0):
m, n = np.shape(dataSet) #求出样本行,列
midIndex = int(m / 2) #中间数的索引位置
axis = depth % n #判断以哪个轴划分数据
sortedDataSet = self.sort(dataSet, axis) #进行排序
node = Node(sortedDataSet[midIndex]) #将节点数据域设置为中位数,具体参考下书本
# print sortedDataSet[midIndex]
leftDataSet = sortedDataSet[: midIndex] #将中位数的左边创建2改副本
rightDataSet = sortedDataSet[midIndex+1 :]
print(leftDataSet)
print(rightDataSet)
node.lchild = self.create(leftDataSet, depth+1) #将中位数左边样本传入来递归创建树
node.rchild = self.create(rightDataSet, depth+1)
return node
else:
return None
def sort(self, dataSet, axis): #采用冒泡排序,利用aixs作为轴进行划分
sortDataSet = dataSet[:] #由于不能破坏原样本,此处建立一个副本
m, n = np.shape(sortDataSet)
for i in range(m):
for j in range(0, m - i - 1):
if (sortDataSet[j][axis] > sortDataSet[j+1][axis]):
temp = sortDataSet[j]
sortDataSet[j] = sortDataSet[j+1]
sortDataSet[j+1] = temp
print(sortDataSet)
return sortDataSet
def preOrder(self, node):
if node != None:
print("tttt->%s" % node.data)
self.preOrder(node.lchild)
self.preOrder(node.rchild)
# def search(self, tree, x):
# node = tree
# depth = 0
# while (node != None):
# print node.data
# n = len(x) #特征数
# axis = depth % n
# if x[axis] < node.data[axis]:
# node = node.lchild
# else:
# node = node.rchild
# depth += 1
def search(self, tree, x):
self.nearestPoint = None #保存最近的点
self.nearestValue = 0 #保存最近的值
def travel(node, depth = 0): #递归搜索
if node != None: #递归终止条件
n = len(x) #特征数
axis = depth % n #计算轴
if x[axis] < node.data[axis]: #如果数据小于结点,则往左结点找
travel(node.lchild, depth+1)
else:
travel(node.rchild, depth+1)
#以下是递归完毕后,往父结点方向回朔
distNodeAndX = self.dist(x, node.data) #目标和节点的距离判断
if (self.nearestPoint == None): #确定当前点,更新最近的点和最近的值
self.nearestPoint = node.data
self.nearestValue = distNodeAndX
elif (self.nearestValue > distNodeAndX):
self.nearestPoint = node.data
self.nearestValue = distNodeAndX
print(node.data, depth, self.nearestValue, node.data[axis], x[axis])
if (abs(x[axis] - node.data[axis]) <= self.nearestValue): #确定是否需要去子节点的区域去找(圆的判断)
if x[axis] < node.data[axis]:
travel(node.rchild, depth+1)
else:
travel(node.lchild, depth + 1)
travel(tree)
return self.nearestPoint
def dist(self, x1, x2): #欧式距离的计算
return ((np.array(x1) - np.array(x2)) ** 2).sum() ** 0.5
##运行示例:
#初始值设定
dataSet = [[2, 3],
[5, 4],
[9, 6],
[4, 7],
[8, 1],
[7, 2]]
x = [5, 3]
#调用函数
kdtree = KdTree()
tree = kdtree.create(dataSet, 0)
kdtree.preOrder(tree)
#输出结果
print(kdtree.search(tree, x))