第2章 K近邻算法(KNN):从原理到工业级实现

195 阅读7分钟

第2章 K近邻算法(KNN):从原理到工业级实现

2.1 KNN算法原理详解

核心思想:人以群分

KNN(K-Nearest Neighbors)基于一个朴素而强大的假设:相似的数据点在特征空间中彼此靠近。就像在生活中,兴趣相投的人会聚集在一起。

算法工作流程

  1. 距离计算:计算测试样本与所有训练样本的距离
  2. 邻居选择:选取距离最小的K个训练样本(K个最近邻)
  3. 决策制定
    • 分类:K个邻居中出现最多的类别
    • 回归:K个邻居的标签平均值

关键数学概念

距离度量(核心公式)
  • 欧氏距离(最常用): d(x,y)=i=1n(xiyi)2d(\mathbf{x}, \mathbf{y}) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2}

  • 曼哈顿距离d(x,y)=i=1nxiyid(\mathbf{x}, \mathbf{y}) = \sum_{i=1}^{n}|x_i - y_i|

  • 闵可夫斯基距离(通用形式): d(x,y)=(i=1nxiyip)1/pd(\mathbf{x}, \mathbf{y}) = \left(\sum_{i=1}^{n}|x_i - y_i|^p\right)^{1/p}

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)

关键代码解析

  1. 距离计算优化

    • 使用列表推导避免Numpy依赖
    • 支持欧氏距离和曼哈顿距离
  2. 多数投票机制

    most_common = Counter(k_labels).most_common(1)
    predictions.append(most_common[0][0])
    
    • 使用Counter统计标签频率
    • 取最高频标签作为预测结果
  3. 决策边界可视化

    • 创建网格点覆盖整个特征空间
    • 预测每个网格点类别
    • 使用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[右子树]
构建过程
  1. 选择方差最大的维度作为分割轴
  2. 找到该维度的中位数作为分割点
  3. 递归构建左右子树
最近邻搜索
  1. 从根节点开始深度优先搜索
  2. 回溯时检查"超球体"是否与分割超平面相交
  3. 必要时进入另一子树搜索

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树核心优势

  1. 高效搜索:平均复杂度O(log n),最坏O(n)
  2. 空间划分:避免不必要的距离计算
  3. 动态更新:支持插入和删除操作

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_neighborsK值5-20(交叉验证选择)
weights投票权重'uniform'或'distance'
algorithm加速算法'auto'/'kd_tree'/'ball_tree'/'brute'
leaf_size叶子节点大小10-50
p闵可夫斯基距离参数1(曼哈顿)或2(欧氏)

性能优化技巧

  1. 特征标准化:确保所有特征在相同尺度
  2. 维度约简:PCA或特征选择处理高维数据
  3. 近似算法:对于超大数据库,使用LSH(局部敏感哈希)
  4. 并行计算:使用GPU加速距离计算

KNN应用场景

  1. 推荐系统:寻找相似用户/物品
  2. 异常检测:识别远离群体的点
  3. 图像分类:基于图像特征匹配
  4. 地理信息系统:寻找最近服务点

"KNN是机器学习中最直观的算法之一,它教会我们:理解问题有时不需要复杂模型,只需找到合适的邻居。" —— 吴恩达