在这里插入图片描述

K近邻(KNN):最简单的机器学习算法

一、KNN要解决什么问题?

1.1 问题场景:物以类聚

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import warnings
warnings.filterwarnings('ignore')

print("=" * 60)
print("KNN的核心思想:物以类聚,人以群分")
print("=" * 60)

# 生成示例数据
np.random.seed(42)
X, y = make_classification(n_samples=200, n_features=2, n_redundant=0,
                           n_clusters_per_class=1, n_classes=3,
                           random_state=42)

plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.scatter(X[y==0, 0], X[y==0, 1], c='blue', alpha=0.6, label='类别A')
plt.scatter(X[y==1, 0], X[y==1, 1], c='red', alpha=0.6, label='类别B')
plt.scatter(X[y==2, 0], X[y==2, 1], c='green', alpha=0.6, label='类别C')
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.title('训练数据:已知类别的样本')
plt.legend()
plt.grid(True, alpha=0.3)

# 新样本点
new_point = np.array([0.5, 0.5])
plt.subplot(1, 2, 2)
plt.scatter(X[y==0, 0], X[y==0, 1], c='blue', alpha=0.6, label='类别A')
plt.scatter(X[y==1, 0], X[y==1, 1], c='red', alpha=0.6, label='类别B')
plt.scatter(X[y==2, 0], X[y==2, 1], c='green', alpha=0.6, label='类别C')
plt.scatter(new_point[0], new_point[1], c='black', s=200, marker='*', label='新样本?')
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.title('新样本属于哪一类?\n找最近的邻居投票决定')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n💡 直观理解:")
print("   如果一个人周围的朋友都是程序员,那么他也很可能是程序员")
print("   KNN就是基于这个思想:一个样本的类别由它最近的K个邻居决定")

二、KNN的核心原理

2.1 KNN的三要素

def explain_knn_principles():
    """解释KNN的三要素"""
    
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    
    # 1. K值选择
    ax1 = axes[0]
    ax1.axis('off')
    ax1.set_title('1. K值选择', fontsize=12)
    
    # 模拟邻居
    neighbors = [
        ("邻居1", "类别A", 0.7),
        ("邻居2", "类别A", 0.72),
        ("邻居3", "类别B", 0.75),
        ("邻居4", "类别B", 0.78),
        ("邻居5", "类别C", 0.8),
    ]
    
    y_pos = 0.8
    for name, label, dist in neighbors:
        color = 'blue' if label == '类别A' else ('red' if label == '类别B' else 'green')
        ax1.text(0.1, y_pos, f"{name}: {label}, 距离={dist}", fontsize=9, color=color)
        y_pos -= 0.12
    
    ax1.text(0.5, 0.2, "K=3 → 2个A, 1个B → 预测为A\nK=5 → 2个A, 2个B, 1个C → 平局需处理", 
            ha='center', fontsize=9,
            bbox=dict(boxstyle='round', facecolor='lightyellow'))
    
    # 2. 距离度量
    ax2 = axes[1]
    ax2.axis('off')
    ax2.set_title('2. 距离度量', fontsize=12)
    
    distance_formulas = """
    欧氏距离: d = √[(x₁-y₁)² + (x₂-y₂)² + ...]
    
    曼哈顿距离: d = |x₁-y₁| + |x₂-y₂| + ...
    
    闵可夫斯基距离: d = [Σ|xᵢ-yᵢ|ᵖ]^(1/p)
    • p=1: 曼哈顿距离
    • p=2: 欧氏距离
    • p=∞: 切比雪夫距离
    """
    
    ax2.text(0.05, 0.95, distance_formulas, transform=ax2.transAxes, fontsize=9,
            verticalalignment='top', fontfamily='monospace')
    
    # 3. 决策规则
    ax3 = axes[2]
    ax3.axis('off')
    ax3.set_title('3. 决策规则', fontsize=12)
    
    ax3.text(0.1, 0.8, "分类问题: 多数投票", fontsize=10, fontweight='bold')
    ax3.text(0.15, 0.7, "例: 3个邻居 → 2个A, 1个B → 预测为A", fontsize=9)
    
    ax3.text(0.1, 0.5, "回归问题: 平均值", fontsize=10, fontweight='bold')
    ax3.text(0.15, 0.4, "例: 3个邻居的值 [10, 12, 14] → 预测为12", fontsize=9)
    
    ax3.text(0.1, 0.2, "平局处理:", fontsize=10, fontweight='bold')
    ax3.text(0.15, 0.1, "• 随机选择\n• 减小K值\n• 距离加权投票", fontsize=8)
    
    plt.suptitle('KNN的三要素', fontsize=14)
    plt.tight_layout()
    plt.show()

explain_knn_principles()

2.2 K值的影响

def visualize_k_effect():
    """可视化K值对决策边界的影响"""
    
    # 生成数据
    from sklearn.datasets import make_moons
    X, y = make_moons(n_samples=200, noise=0.15, random_state=42)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
    
    # 不同K值的KNN
    from sklearn.neighbors import KNeighborsClassifier
    k_values = [1, 5, 15, 30]
    
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))
    
    # 创建网格
    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
                         np.linspace(y_min, y_max, 200))
    
    for idx, k in enumerate(k_values):
        ax = axes[idx // 2, idx % 2]
        
        # 训练KNN
        knn = KNeighborsClassifier(n_neighbors=k)
        knn.fit(X_train, y_train)
        
        # 预测网格
        Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)
        
        # 绘制决策边界
        ax.contourf(xx, yy, Z, alpha=0.3, cmap='RdBu')
        ax.scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], 
                   c='blue', alpha=0.5, s=20, label='类别0')
        ax.scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], 
                   c='red', alpha=0.5, s=20, label='类别1')
        
        train_acc = knn.score(X_train, y_train)
        test_acc = knn.score(X_test, y_test)
        
        ax.set_title(f'K = {k}\n训练准确率={train_acc:.3f}, 测试准确率={test_acc:.3f}')
        ax.set_xlabel('特征1')
        ax.set_ylabel('特征2')
        ax.legend()
        ax.grid(True, alpha=0.3)
    
    plt.suptitle('K值对决策边界的影响', fontsize=14)
    plt.tight_layout()
    plt.show()
    
    print("\n📊 K值选择建议:")
    print("   K太小 (如K=1): 决策边界复杂,容易过拟合")
    print("   K适中 (如K=5-15): 决策边界平滑,泛化好")
    print("   K太大 (如K=30): 决策边界过于平滑,可能欠拟合")
    print("   经验公式: K ≈ √n (n为样本数)")

visualize_k_effect()

三、从零实现KNN

3.1 完整实现

class KNNFromScratch:
    """从零实现的KNN分类器"""
    
    def __init__(self, k=3, distance_metric='euclidean'):
        """
        参数:
            k: 邻居数量
            distance_metric: 距离度量方式 ('euclidean', 'manhattan', 'minkowski')
        """
        self.k = k
        self.distance_metric = distance_metric
        self.X_train = None
        self.y_train = None
    
    def fit(self, X, y):
        """
        KNN的训练就是记住所有数据
        """
        self.X_train = X
        self.y_train = y
        return self
    
    def _euclidean_distance(self, a, b):
        """欧氏距离: √Σ(aᵢ - bᵢ)²"""
        return np.sqrt(np.sum((a - b) ** 2))
    
    def _manhattan_distance(self, a, b):
        """曼哈顿距离: Σ|aᵢ - bᵢ|"""
        return np.sum(np.abs(a - b))
    
    def _minkowski_distance(self, a, b, p=3):
        """闵可夫斯基距离: [Σ|aᵢ - bᵢ|ᵖ]^(1/p)"""
        return np.sum(np.abs(a - b) ** p) ** (1/p)
    
    def _compute_distance(self, a, b):
        """根据选择的度量方式计算距离"""
        if self.distance_metric == 'euclidean':
            return self._euclidean_distance(a, b)
        elif self.distance_metric == 'manhattan':
            return self._manhattan_distance(a, b)
        elif self.distance_metric == 'minkowski':
            return self._minkowski_distance(a, b)
        else:
            raise ValueError(f"未知的距离度量: {self.distance_metric}")
    
    def _predict_one(self, x):
        """
        预测单个样本的类别
        
        步骤:
        1. 计算与所有训练样本的距离
        2. 找到最近的K个邻居
        3. 统计邻居的类别
        4. 返回出现最多的类别
        """
        # 步骤1: 计算距离
        distances = []
        for i in range(len(self.X_train)):
            dist = self._compute_distance(x, self.X_train[i])
            distances.append((dist, self.y_train[i]))
        
        # 步骤2: 按距离排序,取前K个
        distances.sort(key=lambda x: x[0])
        k_nearest = distances[:self.k]
        
        # 步骤3: 统计邻居的类别
        votes = {}
        for _, label in k_nearest:
            votes[label] = votes.get(label, 0) + 1
        
        # 步骤4: 返回票数最多的类别
        predicted_label = max(votes, key=votes.get)
        return predicted_label
    
    def predict(self, X):
        """批量预测"""
        predictions = [self._predict_one(x) for x in X]
        return np.array(predictions)
    
    def predict_proba(self, X):
        """预测概率(用于多分类)"""
        probabilities = []
        for x in X:
            # 计算距离
            distances = []
            for i in range(len(self.X_train)):
                dist = self._compute_distance(x, self.X_train[i])
                distances.append((dist, self.y_train[i]))
            
            # 取前K个
            distances.sort(key=lambda x: x[0])
            k_nearest = distances[:self.k]
            
            # 计算每个类别的概率
            unique_labels = np.unique(self.y_train)
            probs = {}
            for label in unique_labels:
                count = sum(1 for _, l in k_nearest if l == label)
                probs[label] = count / self.k
            
            probabilities.append(probs)
        
        return probabilities
    
    def score(self, X, y):
        """计算准确率"""
        y_pred = self.predict(X)
        return np.mean(y_pred == y)

# 测试从零实现的KNN
print("\n" + "=" * 60)
print("从零实现的KNN测试")
print("=" * 60)

# 生成数据
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=300, n_features=2, n_redundant=0,
                           n_clusters_per_class=1, n_classes=3,
                           random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

# 测试不同K值
k_values = [1, 3, 5, 7, 9, 11, 15]
train_scores = []
test_scores = []

for k in k_values:
    knn = KNNFromScratch(k=k)
    knn.fit(X_train, y_train)
    train_acc = knn.score(X_train, y_train)
    test_acc = knn.score(X_test, y_test)
    train_scores.append(train_acc)
    test_scores.append(test_acc)
    print(f"K={k:2d}: 训练准确率={train_acc:.4f}, 测试准确率={test_acc:.4f}")

# 可视化K值影响
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.plot(k_values, train_scores, 'bo-', label='训练集', linewidth=2)
plt.plot(k_values, test_scores, 'ro-', label='测试集', linewidth=2)
plt.xlabel('K值')
plt.ylabel('准确率')
plt.title('K值对模型性能的影响')
plt.legend()
plt.grid(True, alpha=0.3)

# 选择最佳K值
best_k = k_values[np.argmax(test_scores)]
knn_best = KNNFromScratch(k=best_k)
knn_best.fit(X_train, y_train)

# 绘制决策边界
plt.subplot(1, 2, 2)
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
                     np.linspace(y_min, y_max, 200))
Z = knn_best.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

plt.contourf(xx, yy, Z, alpha=0.3, cmap='tab10')
plt.scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], c='blue', alpha=0.5, s=15)
plt.scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], c='red', alpha=0.5, s=15)
plt.scatter(X_train[y_train==2, 0], X_train[y_train==2, 1], c='green', alpha=0.5, s=15)
plt.title(f'KNN决策边界 (K={best_k})\n测试准确率={knn_best.score(X_test, y_test):.3f}')
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

四、距离度量的影响

def compare_distance_metrics():
    """对比不同距离度量"""
    
    # 生成数据
    np.random.seed(42)
    X, y = make_classification(n_samples=200, n_features=2, n_redundant=0,
                               n_clusters_per_class=1, random_state=42)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
    
    metrics = ['euclidean', 'manhattan']
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))
    
    for idx, metric in enumerate(metrics):
        knn = KNNFromScratch(k=5, distance_metric=metric)
        knn.fit(X_train, y_train)
        
        # 绘制决策边界
        x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
        y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
        xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
                             np.linspace(y_min, y_max, 200))
        Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)
        
        axes[idx].contourf(xx, yy, Z, alpha=0.3, cmap='RdBu')
        axes[idx].scatter(X_train[y_train==0, 0], X_train[y_train==0, 1], 
                         c='blue', alpha=0.5, s=15)
        axes[idx].scatter(X_train[y_train==1, 0], X_train[y_train==1, 1], 
                         c='red', alpha=0.5, s=15)
        
        test_acc = knn.score(X_test, y_test)
        axes[idx].set_title(f'距离度量: {metric}\n测试准确率={test_acc:.3f}')
        axes[idx].set_xlabel('特征1')
        axes[idx].set_ylabel('特征2')
        axes[idx].grid(True, alpha=0.3)
    
    plt.suptitle('不同距离度量对KNN的影响', fontsize=14)
    plt.tight_layout()
    plt.show()
    
    print("\n💡 距离度量选择建议:")
    print("   欧氏距离: 特征尺度相近时使用")
    print("   曼哈顿距离: 特征尺度差异大时使用")
    print("   闵可夫斯基: 通过p参数调节")

compare_distance_metrics()

五、KNN的优缺点与应用

def knn_pros_cons():
    """KNN的优缺点分析"""
    
    fig, axes = plt.subplots(1, 2, figsize=(14, 6))
    
    # 1. 优缺点对比
    ax1 = axes[0]
    ax1.axis('off')
    ax1.set_title('KNN的优缺点', fontsize=12)
    
    pros_cons = """
    ✅ 优点:
    • 简单直观,易于理解
    • 无需训练(懒惰学习)
    • 对异常值不敏感
    • 可用于分类和回归
    • 非线性决策边界
    
    ❌ 缺点:
    • 预测慢(需要计算所有距离)
    • 内存占用大(存储所有数据)
    • 对特征尺度敏感(需要标准化)
    • 维度灾难(高维数据失效)
    • K值选择困难
    """
    
    ax1.text(0.05, 0.95, pros_cons, transform=ax1.transAxes, fontsize=10,
            verticalalignment='top', fontfamily='monospace')
    
    # 2. 维度灾难演示
    ax2 = axes[1]
    
    # 模拟高维下距离失效
    dimensions = np.arange(1, 50)
    # 随机生成点,计算最小距离与最大距离的比值
    ratios = []
    for d in dimensions:
        points = np.random.rand(100, d)
        # 计算所有点到原点的距离
        distances = np.linalg.norm(points, axis=1)
        ratio = np.min(distances) / np.max(distances)
        ratios.append(ratio)
    
    ax2.plot(dimensions, ratios, 'b-', linewidth=2)
    ax2.set_xlabel('维度')
    ax2.set_ylabel('最小距离 / 最大距离')
    ax2.set_title('维度灾难:高维下所有点距离趋同')
    ax2.grid(True, alpha=0.3)
    ax2.axhline(y=0.5, color='r', linestyle='--', label='随机水平')
    ax2.legend()
    
    plt.suptitle('KNN的优缺点及维度灾难', fontsize=14)
    plt.tight_layout()
    plt.show()
    
    print("\n💡 改进方法:")
    print("   1. 特征缩放: 标准化/归一化")
    print("   2. 降维: PCA/t-SNE")
    print("   3. 使用KD树/球树加速搜索")
    print("   4. 距离加权投票")

knn_pros_cons()

六、实战:KNN完整流程

def complete_knn_pipeline():
    """完整的KNN实战流程"""
    
    print("\n" + "=" * 60)
    print("完整KNN实战流程(鸢尾花分类)")
    print("=" * 60)
    
    from sklearn.datasets import load_iris
    from sklearn.preprocessing import StandardScaler
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.model_selection import cross_val_score
    
    # 1. 加载数据
    iris = load_iris()
    X, y = iris.data, iris.target
    
    print(f"\n数据集: {X.shape[0]}个样本, {X.shape[1]}个特征")
    print(f"类别: {iris.target_names}")
    
    # 2. 划分数据
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
    
    # 3. 特征标准化(KNN对尺度敏感)
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)
    
    # 4. 交叉验证选择K值
    k_range = range(1, 21)
    cv_scores = []
    
    for k in k_range:
        knn = KNeighborsClassifier(n_neighbors=k)
        scores = cross_val_score(knn, X_train_scaled, y_train, cv=5)
        cv_scores.append(scores.mean())
    
    best_k = k_range[np.argmax(cv_scores)]
    print(f"\n交叉验证选择的最佳K值: {best_k}")
    
    # 5. 训练最终模型
    knn_final = KNeighborsClassifier(n_neighbors=best_k)
    knn_final.fit(X_train_scaled, y_train)
    
    # 6. 评估
    train_acc = knn_final.score(X_train_scaled, y_train)
    test_acc = knn_final.score(X_test_scaled, y_test)
    print(f"训练集准确率: {train_acc:.4f}")
    print(f"测试集准确率: {test_acc:.4f}")
    
    # 7. 预测新样本
    new_sample = np.array([[5.1, 3.5, 1.4, 0.2]])  # 山鸢尾的特征
    new_sample_scaled = scaler.transform(new_sample)
    prediction = knn_final.predict(new_sample_scaled)
    print(f"\n新样本预测: {iris.target_names[prediction[0]]}")
    
    # 可视化交叉验证结果
    plt.figure(figsize=(12, 4))
    
    plt.subplot(1, 2, 1)
    plt.plot(k_range, cv_scores, 'bo-', linewidth=2)
    plt.xlabel('K值')
    plt.ylabel('交叉验证准确率')
    plt.title(f'K值选择 (最佳K={best_k})')
    plt.grid(True, alpha=0.3)
    
    plt.subplot(1, 2, 2)
    # 使用PCA降维可视化决策边界
    from sklearn.decomposition import PCA
    pca = PCA(n_components=2)
    X_pca = pca.fit_transform(X_train_scaled)
    
    knn_vis = KNeighborsClassifier(n_neighbors=best_k)
    knn_vis.fit(X_pca, y_train)
    
    x_min, x_max = X_pca[:, 0].min() - 0.5, X_pca[:, 0].max() + 0.5
    y_min, y_max = X_pca[:, 1].min() - 0.5, X_pca[:, 1].max() + 0.5
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
                         np.linspace(y_min, y_max, 200))
    Z = knn_vis.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    plt.contourf(xx, yy, Z, alpha=0.3, cmap='tab10')
    plt.scatter(X_pca[y_train==0, 0], X_pca[y_train==0, 1], c='blue', alpha=0.5, label='山鸢尾')
    plt.scatter(X_pca[y_train==1, 0], X_pca[y_train==1, 1], c='red', alpha=0.5, label='变色鸢尾')
    plt.scatter(X_pca[y_train==2, 0], X_pca[y_train==2, 1], c='green', alpha=0.5, label='维吉尼亚鸢尾')
    plt.xlabel('第一主成分')
    plt.ylabel('第二主成分')
    plt.title('KNN决策边界(PCA降维可视化)')
    plt.legend()
    plt.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()

complete_knn_pipeline()

七、代码讲解与总结

def code_explanation():
    """代码讲解"""
    
    print("\n" + "=" * 60)
    print("代码关键点讲解")
    print("=" * 60)
    
    explanations = {
        "距离计算": """
    # 欧氏距离
    def euclidean_distance(a, b):
        return np.sqrt(np.sum((a - b) ** 2))
    
    理解:直线距离,最常用
    注意:特征需要在同一尺度下
    """,
    
        "KNN预测": """
    def predict_one(x):
        # 1. 计算所有距离
        distances = [dist(x, x_train) for x_train in X_train]
        
        # 2. 找最近的K个
        k_indices = np.argsort(distances)[:k]
        
        # 3. 多数投票
        k_labels = y_train[k_indices]
        return np.bincount(k_labels).argmax()
    
    时间复杂度: O(n) 每次预测
    空间复杂度: O(n) 存储所有数据
    """,
    
        "K值选择": """
    交叉验证选择最佳K值:
    for k in range(1, 21):
        scores = cross_val_score(KNN(k), X, y, cv=5)
        avg_score = scores.mean()
    
    选择使交叉验证得分最高的K值
    """
    }
    
    for title, content in explanations.items():
        print(f"\n📌 {title}")
        print(content)

code_explanation()

八、总结

KNN核心要点:

要素 作用 选择建议
K值 邻居数量 √n,交叉验证选择
距离度量 计算相似度 欧氏距离最常用
决策规则 确定输出 分类用投票,回归用平均

KNN vs 其他算法:

特性 KNN 决策树 SVM
训练时间 中等
预测时间
可解释性
内存占用
非线性 支持 支持 核函数

使用建议:

  • 小数据集、低维度 → KNN效果好
  • 大数据集、高维度 → 先降维或用其他算法
  • 需要可解释性 → 决策树
  • 需要快速预测 → 其他算法

记住:

  • KNN是"懒惰学习"的代表
  • 没有训练过程,直接记住数据
  • 特征缩放至关重要
  • K值需要谨慎选择
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐