1. k k k 近邻法是基本且简单的分类与回归方法。 k k k 近邻法的基本做法是:对给定的训练实例点和输入实例点,首先确定输入实例点的 k k k 个最近邻训练实例点,然后利用这 k k k 个训练实例点的类的多数来预测输入实例点的类。
2. k k k 近邻模型对应于基于训练数据集对特征空间的一个划分。 k k k 近邻法中,当训练集、距离度量、 k k k 值及分类决策规则确定后,其结果唯一确定。
3. k k k近邻法三要素:距离度量、 k k k 值的选择和分类决策规则。常用的距离度量是欧氏距离及更一般的 L p L_p Lp 距离。 k k k 值小时, k k k 近邻模型更复杂; k k k值大时, k k k近邻模型更简单。 k k k值的选择反映了对近似误差与估计误差之间的权衡,通常由交叉验证选择最优的 k k k。
常用的分类决策规则是多数表决,对应于经验风险最小化。
4. k k k近邻法的实现需要考虑如何快速搜索k个最近邻点。kd 树是一种便于对 k 维空间中的数据进行快速检索的数据结构。kd 树是二叉树,表示对 k k k 维空间的一个划分,其每个结点对应于 k k k 维空间划分中的一个超矩形区域。利用 kd 树可以省去对大部分数据点的搜索, 从而减少搜索的计算量。
设特征空间 x x x 是 n n n 维实数向量空间 x i , x j ∈ X x_{i}, x_{j} \in \mathcal{X} xi,xj∈X
x i = ( x i ( 1 ) , x i ( 2 ) , ⋯ , x i ( n ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(n)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),⋯,xi(n))T
x j = ( x j ( 1 ) , x j ( 2 ) , ⋯ , x j ( n ) ) T x_{j}=\left(x_{j}^{(1)}, x_{j}^{(2)}, \cdots, x_{j}^{(n)}\right)^{\mathrm{T}} xj=(xj(1),xj(2),⋯,xj(n))T
则 x i x_i xi, x j x_j xj 的 L p L_p Lp 距离定义为:
L p ( x i , x j ) = ( ∑ i = 1 n ∣ x i ( i ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{i=1}^{n}\left|x_{i}^{(i)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(∑i=1n∣∣∣xi(i)−xj(l)∣∣∣p)p1
Python 代码实现:
import math
def L(x, y, p=2):
sum = 0
for i in range(len(x)):
sum += math.pow(abs(x[i] - y[i]), p)
return math.pow(sum, 1/p)
为了方便,我们使用了 鸢尾花 数据集:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from collections import Counter
from sklearn.model_selection import train_test_split
iris = load_iris()
df = pd.DataFrame(data=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_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
print(f'data.shape: {data.shape}')
print(f'X_train.shape: {X_train.shape}')
print(f'X_test.shape: {X_test.shape}')
print(f'y_train.shape: {y_train.shape}')
print(f'y_test.shape: {y_test.shape}')
我们采用了前 100 行数据,包含 2 类花,每一类 50 个样本,每一个样本有 4 个特征值
接着我们使用了 sklearn.model_selection 模块的 train_test_split
方法将数据集划分为训练数据和测试数据,其中 20% 划分为测试数据。具体的使用方法可以参考:sklearn.model_selection.train_test_split
输出结果如下:
data.shape: (100, 3)
X_train.shape: (80, 2)
X_test.shape: (20, 2)
y_train.shape: (80,)
y_test.shape: (20,)
该模型包括了初始化构造 KNN()
、预测 predict(X)
和预测正确率 score(X_test, y_test)
三个方法:
from functools import cmp_to_key
class KNN:
def __init__(self, X_train, y_train, n_neighbors=3, p=2):
self.n = n_neighbors
self.p = p
self.X_train = X_train
self.y_train = y_train
def predict(self, X):
# n nearest neighbors
knn_list = []
# distance from X to all neighbors
distances = [L(X, point, self.p) for point in X_train]
# sort by distance
items = list(zip(X_train, y_train, distances))
items.sort(key=cmp_to_key(lambda item1, item2: item1[-1]-item2[-1]))
# decide
knn_list = [item[0] for item in items[:self.n]]
class_list = [item[1] for item in items[:self.n]]
c = Counter(class_list).most_common()
return Counter(class_list).most_common()[0][0]
def score(self, X_test, y_test):
right_count = 0
for X, y in zip(X_test, y_test):
if self.predict(X) == y:
right_count += 1
else:
print(X, y)
return right_count / len(X_test)
其中使用了Counter()
容器、zip()
方法和 list 的 sort()
排序,使用方法举例:
from collections import Counter
from functools import cmp_to_key
L = list('eabcdabcaba')
c = Counter(L)
print(c)
print(c.most_common())
words = [item[0] for item in c.most_common()]
freqc = [item[1] for item in c.most_common()]
print(words, freqc)
items = list(zip(words, freqc))
print(items)
items.sort(key=cmp_to_key(lambda x, y: x[1] - y[1]))
print(items)
结果为:
Counter({'a': 4, 'b': 3, 'c': 2, 'e': 1, 'd': 1})
[('a', 4), ('b', 3), ('c', 2), ('e', 1), ('d', 1)]
['a', 'b', 'c', 'e', 'd']
[4, 3, 2, 1, 1]
使用剩下 20% 的数据用于测试:
clf = KNN(X_train, y_train)
score = clf.score(X_test, y_test)
print(score) # 1.0
print(clf.predict([6.2, 3])) # 1.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')
plt.scatter(6.2, 3, label='test')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
plt.show()
预测成功率达到了 100%
进一步,我们可以绘制出空间划分的图:(图略)
sklearn.neighbors 定义了最近邻算法,我们需要使用的是 sklearn.neighbors.KNeighborsClassifier 分类器:
from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=3, p=2)
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
print(f'score = {score}') # 1.0
KNeighborsClassifier()
的主要参数如下(参考官网为准):
kd 树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。kd 树是二叉树
,表示对 维空间的一个划分(partition)。构造 kd 树相当于不断地用垂直于坐标轴的超平面将维空间切分,构成一系列的k维超矩形区域。kd 树的每个结点对应于一个维超矩形区域。
输入: k k k 维空间数据集 T = x 1 , x 2 , … , x N T={x_1, x_2,…,x_N} T=x1,x2,…,xN,
其中 x i = ( x i ( 1 ) , i ( 2 ) , ⋯ , x i ( k ) ) T , i = 1 , 2 , … , N x_i=(x_{i}^{(1)},_i^{(2)},⋯,x_i^{(k)})^T, i=1,2,…,N xi=(xi(1),xi(2),⋯,xi(k))T,i=1,2,…,N;
输出:kd树
开始
重复
结束
kd 树节点
每一个节点存储了当前的空间划分的维度,节点的元素、左子节点和右子节点:
class Node:
def __init__(self, elem, split, left, right):
self.elem = elem
self.split = split # dimension-id
self.left = left
self.right = right
构建 kd 树
首先记录下空间划分的维度总数,接着采用 递归
的方式从根节点出发,向左右子节点递归:
每一个节点存储的是当前空间划分条件下的 “中点”,对于每一个带划分的序列,首先按照划分维度进行排序,取出中位数放入节点,把剩下的序列分别放入左子节点和右子节点进行递归 (空间划分的维度进行自增 split = (split + 1) % k
)
class KdTree:
def __init__(self, data):
k = len(data[0]) # dimentions
def createNode(split, data_set):
if not data_set:
return None
data_set.sort(key=lambda x: x[split])
split_pos = len(data_set) // 2
median = data_set[split_pos]
split_next = (split + 1) % k
return Node(
median,
split,
createNode(split_next, data_set[:split_pos]),
createNode(split_next, data_set[split_pos+1:]))
self.root = createNode(0, data)
接着我们创建一颗 kd 树,层次遍历查看结果:
def levelorder(root):
queue = []
queue.append(root)
while queue != []:
curr = queue.pop(0)
if curr.left:
queue.append(curr.left)
if curr.right:
queue.append(curr.right)
print(curr.elem)
L = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
tree = KdTree(L)
levelorder(tree.root)
结果如下:
[7, 2]
[5, 4]
[9, 6]
[2, 3]
[4, 7]
[8, 1]
预测
利用 kd 树寻找最临近点:
nearest
nearest
;寻找当前最近邻节点另一子节点有无更近的点(检测另一子节点对于的区域划分是否与以目标点和 nearest
间距为半径构成的球体有相交),有的话跳转到另一节点寻找最近邻点;没有的话,继续向上回退;nearest
REFERENCES: