第2章 K近邻算法(KNN):从原理到工业级实现
2.1 KNN算法原理详解
核心思想:人以群分
KNN(K-Nearest Neighbors)基于一个朴素而强大的假设:相似的数据点在特征空间中彼此靠近。就像在生活中,兴趣相投的人会聚集在一起。
算法工作流程
- 距离计算:计算测试样本与所有训练样本的距离
- 邻居选择:选取距离最小的K个训练样本(K个最近邻)
- 决策制定:
- 分类:K个邻居中出现最多的类别
- 回归:K个邻居的标签平均值
关键数学概念
距离度量(核心公式)
-
欧氏距离(最常用):
-
曼哈顿距离:
-
闵可夫斯基距离(通用形式):
K值选择:平衡的艺术
graph LR
A[小K值] --> B[决策边界复杂]
A --> C[易受噪声影响]
A --> D[可能过拟合]
E[大K值] --> F[决策边界平滑]
E --> G[忽略局部特征]
E --> H[可能欠拟合]
算法特性分析
| 特性 | 说明 | 影响 |
|---|---|---|
| 懒惰学习 | 训练阶段不计算,预测时实时计算 | 训练快,预测慢 |
| 非参数方法 | 不对数据分布做假设 | 适应复杂分布 |
| 维度灾难 | 高维空间距离失去意义 | 需特征选择/降维 |
现实类比:医生会诊
- 新病人症状 → 测试样本
- 查找相似病历 → 找最近邻
- 多位专家会诊 → K值选择
- 综合诊断意见 → 多数投票
2.2 KNN算法Python实现(基于List)
完整实现代码
import numpy as np
from collections import Counter
import matplotlib.pyplot as plt
class KNN:
def __init__(self, k=5, distance_metric='euclidean'):
"""
初始化KNN分类器
参数:
k: 邻居数量
distance_metric: 距离度量方法 ('euclidean', 'manhattan')
"""
self.k = k
self.distance_metric = distance_metric
self.X_train = []
self.y_train = []
def fit(self, X_train, y_train):
"""存储训练数据"""
# 转换为Python原生列表以提高小数据效率
self.X_train = [list(x) for x in X_train]
self.y_train = list(y_train)
def predict(self, X_test):
"""预测测试样本类别"""
predictions = []
for x in X_test:
# 1. 计算距离
distances = self._compute_distances(x)
# 2. 获取最近的k个邻居
k_indices = np.argsort(distances)[:self.k]
k_labels = [self.y_train[i] for i in k_indices]
# 3. 多数投票
most_common = Counter(k_labels).most_common(1)
predictions.append(most_common[0][0])
return predictions
def _compute_distances(self, x):
"""计算单个测试样本到所有训练样本的距离"""
distances = []
for train_point in self.X_train:
if self.distance_metric == 'euclidean':
dist = np.sqrt(sum((a - b)**2 for a, b in zip(x, train_point)))
elif self.distance_metric == 'manhattan':
dist = sum(abs(a - b) for a, b in zip(x, train_point))
else:
raise ValueError("不支持的度量方法")
distances.append(dist)
return distances
def visualize_decision_boundary(self, X, y, title="KNN决策边界"):
"""可视化决策边界(仅支持2D数据)"""
if len(X[0]) != 2:
print("可视化仅支持二维特征数据")
return
# 创建网格点
x_min, x_max = min(p[0] for p in X) - 1, max(p[0] for p in X) + 1
y_min, y_max = min(p[1] for p in X) - 1, max(p[1] for p in X) + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
np.arange(y_min, y_max, 0.1))
# 预测每个网格点
grid_points = np.c_[xx.ravel(), yy.ravel()]
Z = np.array(self.predict(grid_points))
Z = Z.reshape(xx.shape)
# 绘制
plt.figure(figsize=(10, 8))
plt.contourf(xx, yy, Z, alpha=0.4)
plt.scatter(
[p[0] for p in X],
[p[1] for p in X],
c=y, s=50, edgecolor='k'
)
plt.title(f"{title} (k={self.k})")
plt.xlabel("特征1")
plt.ylabel("特征2")
plt.show()
# 测试示例
if __name__ == "__main__":
# 创建模拟数据集
X_train = np.array([
[1.0, 1.1], [1.0, 1.0], [1.5, 1.8],
[2.0, 1.0], [2.0, 2.0], [2.5, 2.5],
[3.0, 3.0], [3.0, 3.5], [3.5, 3.0]
])
y_train = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2])
# 创建测试点
X_test = np.array([[1.8, 1.8], [2.8, 2.8]])
# 训练和预测
knn = KNN(k=3)
knn.fit(X_train, y_train)
predictions = knn.predict(X_test)
print(f"预测结果: {predictions}") # 应输出 [1, 2]
# 可视化决策边界
knn.visualize_decision_boundary(X_train, y_train)
关键代码解析
-
距离计算优化:
- 使用列表推导避免Numpy依赖
- 支持欧氏距离和曼哈顿距离
-
多数投票机制:
most_common = Counter(k_labels).most_common(1) predictions.append(most_common[0][0])- 使用Counter统计标签频率
- 取最高频标签作为预测结果
-
决策边界可视化:
- 创建网格点覆盖整个特征空间
- 预测每个网格点类别
- 使用contourf绘制决策区域
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 训练 | O(1) | O(n) |
| 预测 | O(n) | O(1) |
| 单样本预测 | O(n) | O(1) |
适用场景:小型数据集(n<1000),特征维度低(d<10)
2.3 KD树:高效近邻搜索
为什么需要KD树?
当数据量增大时,暴力搜索的O(n)复杂度不可接受。KD树通过空间分割将复杂度降至O(log n)。
KD树原理
graph TD
A[根节点] --> B[左子树]
A --> C[右子树]
B --> D[左子树]
B --> E[右子树]
C --> F[左子树]
C --> G[右子树]
构建过程
- 选择方差最大的维度作为分割轴
- 找到该维度的中位数作为分割点
- 递归构建左右子树
最近邻搜索
- 从根节点开始深度优先搜索
- 回溯时检查"超球体"是否与分割超平面相交
- 必要时进入另一子树搜索
Python实现
import numpy as np
class KDNode:
__slots__ = ('point', 'axis', 'left', 'right')
def __init__(self, point, axis, left=None, right=None):
self.point = point # 节点数据点
self.axis = axis # 分割轴 (0,1,2,...)
self.left = left # 左子树
self.right = right # 右子树
class KDTree:
def __init__(self, points):
self.root = self._build_tree(points)
def _build_tree(self, points, depth=0):
if not points:
return None
# 选择分割轴(轮换)
k = len(points[0])
axis = depth % k
# 按当前轴排序并取中位数
points_sorted = sorted(points, key=lambda x: x[axis])
mid_idx = len(points) // 2
mid_point = points_sorted[mid_idx]
# 递归构建子树
left_points = points_sorted[:mid_idx]
right_points = points_sorted[mid_idx+1:]
return KDNode(
mid_point,
axis,
left=self._build_tree(left_points, depth+1),
right=self._build_tree(right_points, depth+1)
)
def nearest_neighbor(self, target):
"""查找最近邻"""
return self._nn_search(self.root, target, None, float('inf'))
def _nn_search(self, node, target, best, best_dist):
if node is None:
return best, best_dist
# 计算当前节点距离
dist = self._distance(node.point, target)
if dist < best_dist:
best = node.point
best_dist = dist
# 确定搜索方向
axis = node.axis
if target[axis] < node.point[axis]:
good_side = node.left
bad_side = node.right
else:
good_side = node.right
bad_side = node.left
# 递归搜索"好"侧
best, best_dist = self._nn_search(good_side, target, best, best_dist)
# 检查"坏"侧是否可能有更近点
if bad_side is not None:
# 计算目标点到分割超平面的距离
plane_dist = abs(target[axis] - node.point[axis])
if plane_dist < best_dist:
best, best_dist = self._nn_search(bad_side, target, best, best_dist)
return best, best_dist
def _distance(self, p1, p2):
"""欧氏距离平方(避免开方计算)"""
return sum((a - b)**2 for a, b in zip(p1, p2))
# KDTree测试
if __name__ == "__main__":
# 创建1000个随机点
np.random.seed(42)
points = np.random.rand(1000, 2).tolist()
# 构建KD树
tree = KDTree(points)
# 查找最近邻
target = [0.4, 0.7]
nearest, dist = tree.nearest_neighbor(target)
print(f"目标点: {target}")
print(f"最近点: {nearest}")
print(f"距离平方: {dist:.6f}")
# 暴力验证
min_dist = float('inf')
min_point = None
for p in points:
d = sum((a-b)**2 for a, b in zip(p, target))
if d < min_dist:
min_dist = d
min_point = p
print("\n暴力验证结果:")
print(f"最近点: {min_point}")
print(f"距离平方: {min_dist:.6f}")
print(f"结果一致: {nearest == min_point}")
KD树核心优势
- 高效搜索:平均复杂度O(log n),最坏O(n)
- 空间划分:避免不必要的距离计算
- 动态更新:支持插入和删除操作
KD树 vs 暴力搜索
| 指标 | 暴力搜索 | KD树 |
|---|---|---|
| 构建时间 | O(1) | O(n log n) |
| 查询时间 | O(n) | O(log n) |
| 内存占用 | O(1) | O(n) |
| 适用场景 | 小数据集 | 大数据集 |
工业级实践:Scikit-Learn实现
from sklearn.neighbors import KNeighborsClassifier, KDTree
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler
import numpy as np
# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
# 数据预处理
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.3, random_state=42
)
# 创建KNN分类器
knn = KNeighborsClassifier(
n_neighbors=5, # K值
weights='uniform', # 'distance'可加权投票
algorithm='auto', # 自动选择最优算法
leaf_size=30, # KD树/Ball树叶子大小
p=2, # 距离度量 (1:曼哈顿, 2:欧氏)
metric='minkowski' # 闵可夫斯基距离
)
# 训练模型
knn.fit(X_train, y_train)
# 预测
y_pred = knn.predict(X_test)
# 评估
accuracy = accuracy_score(y_test, y_pred)
print(f"测试准确率: {accuracy:.4f}")
# 使用KD树加速
print("\n使用KD树加速查询:")
kdtree = KDTree(X_train, leaf_size=30)
dist, ind = kdtree.query(X_test, k=5)
# 手动计算预测结果
knn_manual_pred = []
for neighbors in ind:
votes = y_train[neighbors]
most_common = np.bincount(votes).argmax()
knn_manual_pred.append(most_common)
manual_accuracy = accuracy_score(y_test, knn_manual_pred)
print(f"手动KD树查询准确率: {manual_accuracy:.4f}")
关键参数解析
| 参数 | 说明 | 推荐值 |
|---|---|---|
n_neighbors | K值 | 5-20(交叉验证选择) |
weights | 投票权重 | 'uniform'或'distance' |
algorithm | 加速算法 | 'auto'/'kd_tree'/'ball_tree'/'brute' |
leaf_size | 叶子节点大小 | 10-50 |
p | 闵可夫斯基距离参数 | 1(曼哈顿)或2(欧氏) |
性能优化技巧
- 特征标准化:确保所有特征在相同尺度
- 维度约简:PCA或特征选择处理高维数据
- 近似算法:对于超大数据库,使用LSH(局部敏感哈希)
- 并行计算:使用GPU加速距离计算
KNN应用场景
- 推荐系统:寻找相似用户/物品
- 异常检测:识别远离群体的点
- 图像分类:基于图像特征匹配
- 地理信息系统:寻找最近服务点
"KNN是机器学习中最直观的算法之一,它教会我们:理解问题有时不需要复杂模型,只需找到合适的邻居。" —— 吴恩达