1. KNN算法和KD - tree总结
1.1 KNN算法
模型
K近邻(K - Nearest Neighbors,KNN)算法是一种基本的分类与回归方法。它的模型实际上是对特征空间的划分,给定一个训练数据集,对于新的输入实例,在训练数据集中找到与该实例最邻近的 \(K\) 个实例,然后根据这 \(K\) 个实例的类别来决定新实例的类别(分类问题)或值(回归问题)。在分类问题中,通常采用多数表决规则来确定新实例的类别。
策略
KNN算法的策略是经验风险最小化。要使误分类率最小即经验风险最小,就要使“后验概率最大”(对应你提到的 [插图],这里后验概率指的是在已知特征向量的情况下,属于某一类别的概率)。在分类问题中,多数表决规则等价于经验风险最小化。设分类的损失函数为 0 - 1 损失函数:
经验风险为:
对于新的输入 \(x\),它的类别预测为:
其中 \(N_k(x)\) 是 \(x\) 的 \(K\) 近邻点的集合,\(I\) 是指示函数,这个多数表决规则能使经验风险最小。
方法
KNN算法的核心是计算实例之间的距离,常用的距离度量是欧氏距离:
对于新的输入实例,计算它与训练数据集中所有实例的距离,然后选取距离最近的 \(K\) 个实例,根据多数表决规则确定其类别。
1.2 KD - tree
KD - tree(K - Dimensional Tree)是一种对 \(K\) 维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。
模型
KD - tree 是一种二叉树,它将 \(K\) 维空间进行递归划分。每个内部节点表示一个超矩形区域,通过选择一个坐标轴和该坐标轴上的一个分割点,将该区域划分为两个子区域。叶节点存储一个或多个实例点。
策略
KD - tree 的构建策略是通过递归地选择坐标轴和分割点,使得树的结构尽可能平衡,从而提高搜索效率。通常选择方差最大的坐标轴作为分割坐标轴,选择该坐标轴上的中位数作为分割点。
方法
- 构建 KD - tree:从根节点开始,选择一个坐标轴和分割点,将数据集划分为两部分,分别作为左右子树的数据集,递归地构建左右子树。
- 搜索 KD - tree:对于一个查询点,从根节点开始,根据查询点在分割坐标轴上的值与分割点的大小关系,选择进入左子树或右子树进行搜索。同时,需要回溯检查其他可能包含最近邻的区域。
2. Python代码实现
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score# 定义 KD - tree 节点类
class KDNode:def __init__(self, point, split_axis, left=None, right=None):self.point = pointself.split_axis = split_axisself.left = leftself.right = right# 构建 KD - tree
def build_kdtree(points, depth=0):if len(points) == 0:return Nonek = len(points[0])split_axis = depth % ksorted_points = sorted(points, key=lambda point: point[split_axis])median_index = len(sorted_points) // 2return KDNode(sorted_points[median_index],split_axis,build_kdtree(sorted_points[:median_index], depth + 1),build_kdtree(sorted_points[median_index + 1:], depth + 1))# 计算两点之间的欧氏距离
def euclidean_distance(point1, point2):return np.sqrt(np.sum((np.array(point1) - np.array(point2)) ** 2))# 在 KD - tree 中搜索最近的 K 个点
def knn_search_kdtree(root, point, k, depth=0, heap=None):if root is None:return []if heap is None:heap = []k = min(k, len(heap) + 1)split_axis = root.split_axisnext_branch = Noneopposite_branch = Noneif point[split_axis] < root.point[split_axis]:next_branch = root.leftopposite_branch = root.rightelse:next_branch = root.rightopposite_branch = root.left# 递归搜索下一个分支knn_search_kdtree(next_branch, point, k, depth + 1, heap)# 计算当前节点到查询点的距离dist = euclidean_distance(point, root.point)if len(heap) < k:heap.append((dist, root.point))heap.sort(key=lambda x: x[0])elif dist < heap[-1][0]:heap[-1] = (dist, root.point)heap.sort(key=lambda x: x[0])# 检查对面的分支是否可能包含更近的点if len(heap) < k or abs(point[split_axis] - root.point[split_axis]) < heap[-1][0]:knn_search_kdtree(opposite_branch, point, k, depth + 1, heap)return heap# KNN 分类器
class KNNClassifier:def __init__(self, k=3):self.k = kself.kdtree = Noneself.labels = Noneself.X_train = Nonedef fit(self, X, y):self.kdtree = build_kdtree(X)self.labels = yself.X_train = Xdef predict(self, X):predictions = []for point in X:neighbors = knn_search_kdtree(self.kdtree, point, self.k)neighbor_points = [neighbor[1] for neighbor in neighbors]neighbor_indices = []for neighbor_point in neighbor_points:for i, train_point in enumerate(self.X_train):if np.array_equal(train_point, neighbor_point):neighbor_indices.append(i)breakneighbor_labels = [self.labels[index] for index in neighbor_indices]most_common = Counter(neighbor_labels).most_common(1)[0][0]predictions.append(most_common)return np.array(predictions)# 生成随机数据
X, y = make_classification(n_samples=200, n_features=2, n_informative=2, n_redundant=0, random_state=42, n_clusters_per_class=1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)# 创建 KNN 分类器并训练
knn = KNNClassifier(k=3)
knn.fit(X_train, y_train)# 进行预测
y_pred = knn.predict(X_test)# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print(f"KNN 分类器的准确率: {accuracy}")# 绘制决策边界
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),np.arange(y_min, y_max, 0.02))
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)# 绘制决策区域
plt.contourf(xx, yy, Z, alpha=0.4)# 绘制训练数据点
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, edgecolors='k', marker='o', s=80, label='Training Data')
# 绘制测试数据点
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, edgecolors='k', marker='s', s=80, label='Test Data')plt.title('KNN Decision Boundary')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
plt.show()
代码解释
- KDNode类:定义了KD - tree的节点结构,包含节点的坐标、分割轴以及左右子节点。
- build_kdtree函数:递归地构建KD - tree,根据当前深度选择分割轴,使用中位数作为分割点。
- euclidean_distance函数:计算两点之间的欧氏距离。
- knn_search_kdtree函数:在KD - tree中搜索最近的 \(K\) 个点,使用递归和回溯的方法。
- KNNClassifier类:实现了KNN分类器,包括训练和预测方法。
- 数据生成和评估:使用
make_classification
函数生成随机分类数据,将数据划分为训练集和测试集,训练KNN分类器并计算准确率。
KD - tree(K维树)是一种用于对K维空间中的数据点进行组织和索引的数据结构,常用于高效地进行最近邻搜索等操作。其构造和查询的复杂度如下:
- 构造复杂度:
- 理想情况下,每次划分都能将数据集均匀地分成两部分,对于包含 \(n\) 个数据点的 \(k\) 维数据集,构造KD - tree的时间复杂度为 \(O(nlogn)\)。这是因为在每一层,我们需要 \(O(n)\) 的时间来找到划分点,而树的深度为 \(O(logn)\)。
- 最坏情况下,数据点分布不均匀,导致树的构造退化为线性结构,此时构造KD - tree的时间复杂度为 \(O(n^2)\)。
- 查询复杂度:
- 在理想情况下,查询一个点的最近邻时,每次迭代都能排除一半的空间,时间复杂度为 \(O(logn)\)。
- 最坏情况下,查询复杂度可能达到 \(O(n)\),例如当数据点分布呈病态,或者查询的点位于树的边界附近,需要访问大量节点时。
实际应用中,KD - tree的性能通常较好,能够在接近 \(O(logn)\) 的时间复杂度内完成查询操作,尤其是对于低维数据(如二维、三维数据)效果更为显著。但随着维度的增加,KD - tree的性能可能会下降,出现“维度灾难”问题。
HNSW(Hierarchical Navigable Small World)是一种用于高维空间中近似最近邻搜索的算法。以下是对HNSW的解释以及它与KNN、KD - tree的对比:
HNSW算法原理
- HNSW构建了一个分层的图结构,每个节点在不同层次上与其他节点相连。底层包含所有的数据点,随着层次的升高,节点数量逐渐减少,形成一种类似于金字塔的结构。
- 在搜索时,从高层开始,利用节点之间的连接快速定位到可能包含目标最近邻的区域,然后在底层进行更精确的搜索。这种分层结构和连接方式使得HNSW能够在高维空间中高效地进行近似最近邻搜索,大大减少了搜索时间和空间复杂度。
对比
- 搜索效率
- KNN:在数据量较大时,每次搜索都需要遍历整个数据集来计算距离并找到最近邻,搜索效率较低,时间复杂度为 \(O(n)\),其中 \(n\) 是数据集的大小。
- KD - tree:通过构建树结构来划分数据空间,在理想情况下可以将搜索复杂度降低到 \(O(logn)\),但在高维空间中性能会下降,甚至可能退化为线性搜索。
- HNSW:在高维空间中具有出色的搜索效率,能够在较短时间内找到近似最近邻。它利用分层图结构和启发式搜索策略,大大减少了搜索的范围和时间,通常比KD - tree在高维数据上表现更好。
- 空间复杂度
- KNN:不需要额外的空间来存储数据结构,只需要存储数据集本身,空间复杂度为 \(O(n)\)。
- KD - tree:需要额外的空间来存储树结构,包括节点信息、划分维度等,空间复杂度通常为 \(O(n)\),但在某些情况下可能会更高。
- HNSW:构建的分层图结构需要较多的额外空间来存储节点之间的连接信息,空间复杂度相对较高,一般为 \(O(nc)\),其中 \(c\) 是一个与图的连接密度相关的常数。
- 数据适应性
- KNN:对数据的分布没有特殊要求,适用于各种类型的数据,但对于大规模数据和高维数据,性能会受到影响。
- KD - tree:对于低维数据且分布较为均匀的数据表现较好,但对于高维数据和分布不均匀的数据,可能会出现性能下降甚至失效的情况。
- HNSW:适用于各种类型的数据,尤其是在高维数据上表现出较好的适应性和鲁棒性,能够处理不同分布的数据。
- 准确性
- KNN:是一种精确的最近邻搜索算法,只要计算出所有数据点与查询点的距离,就能准确找到最近邻。
- KD - tree:在理想情况下能够准确找到最近邻,但在某些情况下,由于树的划分方式和数据分布的影响,可能无法找到全局最优的最近邻。
- HNSW:是一种近似最近邻搜索算法,它不能保证找到的一定是全局最优的最近邻,但在大多数情况下,能够找到非常接近真实最近邻的结果,在实际应用中,这种近似结果通常已经能够满足需求。
综上所述,KNN是一种简单直接的精确最近邻搜索算法,适用于小规模数据;KD - tree在低维数据上有较好的性能,但对高维数据适应性有限;HNSW则是一种高效的近似最近邻搜索算法,在高维数据和大规模数据上表现出色,能够在较短时间内找到近似最优解,在实际应用中得到了广泛的应用。