机器学习笔记 2 —— K 近邻法与 kd 树

文章目录

  • 1. 理论部分
    • 1.1 K近邻法
    • 1.2 距离度量
  • 2. k 近邻法的 Python 实现
    • 2.1 数据集的预处理
    • 2.2 模型构建
    • 2.3 测试模型
    • 2.4 scikit-learn
  • 3. kd 树
    • 3.1 构造平衡kd树算法
    • 3.2 kd 树的 Python 实现


1. 理论部分

1.1 K近邻法

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 树可以省去对大部分数据点的搜索, 从而减少搜索的计算量。


1.2 距离度量

设特征空间 x x x n n n 维实数向量空间 x i , x j ∈ X x_{i}, x_{j} \in \mathcal{X} xi,xjX

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=1nxi(i)xj(l)p)p1

  • p = 1 p= 1 p=1 曼哈顿距离
  • p = 2 p= 2 p=2 欧氏距离
  • p = ∞ p= \infty p= 切比雪夫距离

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)


2. k 近邻法的 Python 实现

2.1 数据集的预处理

为了方便,我们使用了 鸢尾花 数据集:

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

机器学习笔记 2 —— K 近邻法与 kd 树_第1张图片

输出结果如下:

data.shape: (100, 3)
X_train.shape: (80, 2)
X_test.shape: (20, 2)
y_train.shape: (80,)
y_test.shape: (20,)


2.2 模型构建

该模型包括了初始化构造 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]

2.3 测试模型

使用剩下 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%

机器学习笔记 2 —— K 近邻法与 kd 树_第2张图片

进一步,我们可以绘制出空间划分的图:(图略)


2.4 scikit-learn

sklearn.neighbors 定义了最近邻算法,我们需要使用的是 sklearn.neighbors.KNeighborsClassifier 分类器:
机器学习笔记 2 —— K 近邻法与 kd 树_第3张图片

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() 的主要参数如下(参考官网为准):

  • n_neighbors: 临近点个数
  • p: 距离度量
  • algorithm: 近邻算法,可选{‘auto’, ‘ball_tree’, ‘kd_tree’, ‘brute’}
  • weights: 确定近邻的权重


3. kd 树

kd 树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。kd 树是二叉树,表示对 维空间的一个划分(partition)。构造 kd 树相当于不断地用垂直于坐标轴的超平面将维空间切分,构成一系列的k维超矩形区域。kd 树的每个结点对应于一个维超矩形区域。


3.1 构造平衡kd树算法

输入: k k k 维空间数据集 T = x 1 , x 2 , … , x N T={x_1, x_2,…,x_N} Tx1,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,i1,2,,N

输出:kd树

开始

  • 构造根结点,根结点对应于包含 T T T k k k 维空间的超矩形区域。
  • 选择 x ( 1 ) x^{(1)} x(1) 为坐标轴,以T中所有实例的 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(modk)+1 lj(modk)+1,以该结点的区域中所有实例的 x ( l ) x^{(l)} x(l) 坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( l ) x^{(l)} x(l) 垂直的超平面实现。
  • 由该结点生成深度为 j + 1 j+1 j+1 的左、右子结点:左子结点对应坐标 x ( l ) x^{(l)} x(l) 小于切分点的子区域,右子结点对应坐标 x ( l ) x^{(l)} x(l) 大于切分点的子区域。
  • 将落在切分超平面上的实例点保存在该结点。

结束

  • 直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。

3.2 kd 树的 Python 实现

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:

  1. 李航统计学习方法
  2. scikit-learn
  3. Introduction to Machine Learning with Python
  4. lihang-code-master

你可能感兴趣的:(机器学习,python,机器学习,算法)