𝐾近邻法及python实现
综述
𝐾近邻法是一种基本分类与回归的方法。不具有显式的学习过程。𝐾值的选择,距离度量以及分类决策的规则是𝐾近邻法的三个基本要素。一般来说,只选择样本数据集中前N个最相似的数据。𝐾一般不大于20,最后,选择k个中出现次数最多的分类,作为新数据的分类。
- 优点:精度高、对异常值不敏感、无数据输入假定
- 缺点:计算复杂度高、空间复杂度高
- 适用数据范围:数值型和标称型
𝐾近邻算法
𝐾近邻模型
距离度量
L
p
L_p
Lp距离:
L
p
(
x
i
,
x
j
)
=
(
∑
l
=
1
n
∣
x
i
(
l
)
−
x
j
(
l
)
∣
p
)
1
p
L_p(x_i,x_j)=\left (\sum_{l=1}^{n}\left |x_i^{(l)}-x_j^{(l)} \right | ^p \right )^\frac{1}{p}
Lp(xi,xj)=(l=1∑n∣∣∣xi(l)−xj(l)∣∣∣p)p1
当
p
=
2
p=2
p=2时,为欧氏距离:
L
2
(
x
i
,
x
j
)
=
(
∑
l
=
1
n
∣
x
i
(
l
)
−
x
j
(
l
)
∣
2
)
1
2
L_2(x_i,x_j)=\left (\sum_{l=1}^{n}\left |x_i^{(l)}-x_j^{(l)} \right |^2 \right )^\frac{1}{2}
L2(xi,xj)=(l=1∑n∣∣∣xi(l)−xj(l)∣∣∣2)21
当
p
=
1
p=1
p=1时,为曼哈顿距离:
L
1
(
x
i
,
x
j
)
=
∑
l
=
1
n
∣
x
i
(
l
)
−
x
j
(
l
)
∣
L_1(x_i,x_j)=\sum_{l=1}^{n}\left |x_i^{(l)}-x_j^{(l)} \right |
L1(xi,xj)=l=1∑n∣∣∣xi(l)−xj(l)∣∣∣
当
p
=
∞
p=∞
p=∞时,为各个坐标距离的最大值:
L
(
x
i
,
x
j
)
=
max
l
∣
x
i
(
l
)
−
x
j
(
l
)
∣
L_(x_i,x_j)=\max_{l}\left |x_i^{(l)}-x_j^{(l)} \right |
L(xi,xj)=lmax∣∣∣xi(l)−xj(l)∣∣∣
𝐾值的选择
如果
K
K
K值小,“学习”的近似误差(approximation error)会减小,但 “学习”的估计误差(estimation error) 会增大;噪声敏感;
K
K
K值的减小就意味着整体模型变得复杂,容易发生过拟合。
如果
K
K
K值大,减少学习的估计误差,但缺点是学习的近似误差会增大。
K
K
K值的增大就意味着整体的模型变得简单。
K
=
N
K=N
K=N那么无论输入实例是什么,都将简单的预测它属于在训练实例中最多的类。
在应用中,一般选一个较小的数,使用交叉验证法来选取最优
K
K
K值。
分类决策的规则
多数表决规则有如下解释:如果分类的损失函数为0-1损失函数(
L
(
Y
,
f
(
x
)
)
=
{
1
,
Y
≠
f
(
X
)
0
,
Y
=
f
(
X
)
}
L\left(Y,f(x)\right) = \left \{ \begin{matrix} 1,Y≠f(X)\\ 0,Y=f(X) \end{matrix} \right \}
L(Y,f(x))={1,Y=f(X)0,Y=f(X)}),分类函数为
f
:
R
n
→
c
1
,
c
2
,
.
.
,
c
K
f:R^n→{c_1,c_2,..,c_K }
f:Rn→c1,c2,..,cK,那么误分类的概率是
P
(
Y
≠
f
(
X
)
)
=
1
−
P
(
Y
=
f
(
X
)
)
P(Y≠f(X))=1-P(Y=f(X))
P(Y=f(X))=1−P(Y=f(X))。
对给定的实例
x
∈
X
x∈X
x∈X,其最近邻的
k
k
k个训练实例点构成集合
N
k
(
x
)
N_k (x)
Nk(x)。如果涵盖
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
∑
x
i
∈
N
k
(
x
)
I
(
y
i
=
c
j
)
\frac{1}{k}\sum_{x_i \in N_k(x)}I(y_i≠c_j)=1-\frac{1}{k}\sum_{x_i \in N_k(x)}I(y_i=c_j)
k1xi∈Nk(x)∑I(yi=cj)=1−k1xi∈Nk(x)∑I(yi=cj)
代码
Main.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from myKNN import KNN
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
def pplt(df, test_point=[0, 0], flag=0):
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)
if flag == 1:
plt.plot(test_point[0], test_point[1], 'bo', label='test_point')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
plt.show()
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']
# print(df)
pplt(df)
data = np.array(df.iloc[:100, [0, 1, -1]])
X, y = data[:, :-1], data[:, -1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
print("np.shape(X_train)", np.shape(X_train))
print("np.shape(y_train)", np.shape(y_train))
clf = KNN(X_train, y_train)
print(clf.score(X_test, y_test))
test_point = [6.0, 3.0]
print('Test Point:{}'.format(clf.predict(test_point)))
pplt(df, test_point, flag=1)
myKNN.py
在这里插入代码片import numpy as np
from collections import Counter
class KNN:
def __init__(self, X_train, y_train, n_neighbors=3, p=2):
"""
:param n_neighbors: 临近点的个数
:param 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(x, ord=None, axis=None, keepdims=False) 求范数
# x---矩阵 ord---范数类型 axis---处理类型(=1按行向量处理,=0按列向量处理,=None矩阵范数) keepding---是否保持矩阵的二维特性
dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
knn_list.append((dist, self.y_train[i]))
for i in range(self.n, len(self.X_train)):
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)
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)
max_count = sorted(count_pairs.items(), key=lambda x: x[1])[-1][0]
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)
输出
𝐾近邻法的实现:kd树
构造kd树
k
d
kd
kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。
k
d
kd
kd树是二叉树,表示对k维空间的一个划分(partition)。构造
k
d
kd
kd树相当于不断地用垂直于坐标轴的超平面将
k
k
k维空间切分,构成一系列的
k
k
k维超矩形区域。
k
d
kd
kd树的每个结点对应于一个
k
k
k维超矩形区域。
构造
k
d
kd
kd树的方法如下:构造根结点,使根结点对应于k维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对k维空间进行切分,生成子结点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域(子结点);这时,实例被分到两个子区域。这个过程直到子区域内没有实例时终止(终止时的结点为叶结点)。在此过程中,将实例保存在相应的结点上。
搜索kd树
给定一个目标点,搜索其最近邻。首先找到包含目标点的叶结点;然后从该叶结点出发,依次回退到父结点;不断查找与目标点最邻近的结点,当确定不可能存在更近的结点时终止。这样搜索就被限制在空间的局部区域上,效率大为提高。
包含目标点的叶结点对应包含目标点的最小超矩形区域。以此叶结点的实例点作为当前最近点。目标点的最近邻一定在以目标点为中心并通过当前最近点的超球体的内部,然后返回当前结点的父结点,如果父结点的另一子结点的超矩形区域与超球体相交,那么在相交的区域内寻找与目标点更近的实例点。如果存在这样的点,将此点作为新的当前最近点。算法转到更上一级的父结点,继续上述过程。如果父结点的另一子结点的超矩形区域与超球体不相交,或不存在比当前最近点更近的点,则停止搜索。
代码实现
main.py
import KD
import search
import time
from random import random
data = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
kd = KD.KDTree(data)
KD.preorder(kd.root)
# 产生一个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 = search.find_nearest(kd, [3, 4.5])
print(ret)
N = 400000
t0 = time.perf_counter()
kd2 = KD.KDTree(random_points(3, N)) # 构建包含四十万个3维空间样本点的kd树
ret2 = search.find_nearest(kd2, [0.1, 0.5, 0.8]) # 四十万个样本点中寻找离目标最近的点
t1 = time.perf_counter()
print("Time:", t1 - t0, "s")
print(ret2)
KD.py
# 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]) # 数据维度
self.root = CreateNode(k, 0, data) # 从第0维分量开始构建kd树,返回根节点
def CreateNode(k, 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
median = data_set[split_pos] # 中位数分割点
split_next = (split + 1) % k
# 递归创建kd树
return KdNode(median, split,
CreateNode(k, split_next, data_set[:split_pos]), # 创建左子树
CreateNode(k, split_next, data_set[split_pos + 1:]) # 创建右子树
)
# KDTree 前序遍历
def preorder(root):
print(root.dom_elt)
if root.left:
preorder(root.left)
if root.right:
preorder(root.right)
search.py
"""
对构建好的kd树进行搜索,寻找与目标点最近的样本点
"""
from math import sqrt
from collections import namedtuple
import KD
# 定义一个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)
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")) # 从根节点开始递归