从原理到代码:用Python+sklearn完整实现KNN算法(附鸢尾花分类实战)

当你第一次听说KNN算法时,可能会被它"简单粗暴"的特性所吸引——毕竟,用距离来决定数据归属的直觉实在太符合人类思维了。但真正动手实现时,你会发现这个看似简单的算法藏着不少学问:如何选择最优的k值?不同距离度量对结果有什么影响?为什么我的分类边界看起来不太对劲?

本文将带你从数学原理出发,通过鸢尾花分类的完整案例,手把手实现KNN算法。我们不仅会用sklearn的KNeighborsClassifier完成基础建模,还会深入算法内部,用Python原生代码重现核心逻辑。特别地,我会分享几个在教学中发现新手最容易踩的坑,以及如何通过可视化快速诊断模型问题。

1. KNN算法原理深度解析

KNN(K-Nearest Neighbors)的核心思想可以用一句话概括:物以类聚。想象你在陌生城市找餐馆,大概率会相信周围10家餐馆中有8家好评的那家,而不是独自获得差评的那家。这就是KNN的朴素哲学——让邻居为你投票。

1.1 距离度量的艺术

算法第一步是计算距离,但"距离"在数据科学中有多种定义。最常用的是闵可夫斯基距离的两种特例:

# 欧式距离(L2范数)
def euclidean_distance(a, b):
    return np.sqrt(np.sum((a - b)**2))

# 曼哈顿距离(L1范数)
def manhattan_distance(a, b):
    return np.sum(np.abs(a - b))

不同距离度量的选择会显著影响分类结果:

距离类型 计算公式 适用场景 计算效率
欧式距离 √(Σ(xi-yi)²) 连续特征、各向同性数据
曼哈顿距离 Σ xi-yi
余弦相似度 (A·B)/( A

提示:当特征量纲差异大时,务必先做标准化!否则数值大的特征会主导距离计算。

1.2 k值选择的博弈论

k值大小直接影响模型表现,这是一个典型的偏差-方差权衡问题:

  • 小k值:模型复杂度高(低偏差,高方差),容易过拟合
  • 大k值:模型复杂度低(高偏差,低方差),可能欠拟合

通过交叉验证寻找最优k值的代码示例:

from sklearn.model_selection import GridSearchCV

params = {'n_neighbors': range(1, 31)}
knn = KNeighborsClassifier()
grid = GridSearchCV(knn, params, cv=5)
grid.fit(X_scaled, y)

print(f"最佳k值: {grid.best_params_['n_neighbors']}")

2. sklearn实战:鸢尾花分类全流程

让我们用经典的鸢尾花数据集演示完整的工作流。这个数据集包含150个样本,每个样本有4个特征(花萼长宽、花瓣长宽),分为3个品种。

2.1 数据准备与探索

from sklearn.datasets import load_iris
import pandas as pd

iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['target'] = iris.target

# 查看特征分布
import seaborn as sns
sns.pairplot(df, hue='target', palette='viridis')

数据预处理的关键步骤:

  1. 拆分训练测试集(保持类别比例)
  2. 特征标准化(KNN对尺度敏感)
  3. 检查类别平衡性
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.2, stratify=iris.target)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)  # 注意用训练集参数转换测试集

2.2 模型训练与调参

KNeighborsClassifier的核心参数详解:

from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier(
    n_neighbors=5,        # 邻居数量
    weights='uniform',    # 'distance'表示按距离加权
    algorithm='auto',     # 自动选择最优算法
    p=2,                  # 距离度量(2=欧式,1=曼哈顿)
    metric='minkowski'    # 闵可夫斯基距离
)

knn.fit(X_train_scaled, y_train)

通过网格搜索优化多个参数:

from sklearn.model_selection import GridSearchCV

param_grid = {
    'n_neighbors': range(3, 15),
    'weights': ['uniform', 'distance'],
    'p': [1, 2]
}

grid_search = GridSearchCV(knn, param_grid, cv=5, n_jobs=-1)
grid_search.fit(X_train_scaled, y_train)

print(f"最佳参数组合: {grid_search.best_params_}")

2.3 结果分析与可视化

评估模型表现的完整流程:

from sklearn.metrics import classification_report, confusion_matrix

y_pred = grid_search.best_estimator_.predict(X_test_scaled)
print(classification_report(y_test, y_pred))

# 绘制混淆矩阵
sns.heatmap(confusion_matrix(y_test, y_pred), 
            annot=True, fmt='d', cmap='Blues')

决策边界可视化(以两个主要特征为例):

from mlxtend.plotting import plot_decision_regions

# 只取前两个特征进行二维可视化
X_2d = X_train_scaled[:, :2]  
knn_2d = KNeighborsClassifier(n_neighbors=5)
knn_2d.fit(X_2d, y_train)

plt.figure(figsize=(10, 6))
plot_decision_regions(X_2d, y_train, clf=knn_2d)
plt.xlabel('标准化花萼长度')
plt.ylabel('标准化花萼宽度')
plt.title('KNN决策边界(k=5)')

3. 从零实现KNN算法

理解sklearn的黑箱实现后,让我们用原生Python实现算法核心:

3.1 基础版本实现

import numpy as np
from collections import Counter

class MyKNN:
    def __init__(self, k=5, p=2):
        self.k = k
        self.p = p  # 控制距离度量(1=曼哈顿,2=欧式)
    
    def fit(self, X, y):
        self.X_train = X
        self.y_train = y
    
    def predict(self, X):
        predictions = []
        for x in X:
            # 计算所有训练样本的距离
            distances = np.linalg.norm(self.X_train - x, ord=self.p, axis=1)
            
            # 获取最近的k个样本的索引
            k_indices = np.argpartition(distances, self.k)[:self.k]
            
            # 获取这些样本的标签并进行投票
            k_nearest_labels = self.y_train[k_indices]
            most_common = Counter(k_nearest_labels).most_common(1)
            predictions.append(most_common[0][0])
        
        return np.array(predictions)

3.2 性能优化技巧

原生实现的效率问题:

  • 每次预测都要计算全量距离 → O(N)复杂度
  • 没有利用树结构加速查询

改进方案:

  1. 使用KD-Tree或Ball-Tree预处理数据
  2. 实现加权投票(距离倒数作为权重)
  3. 添加并行计算支持
from scipy.spatial import KDTree

class OptimizedKNN:
    def __init__(self, k=5, weights='uniform'):
        self.k = k
        self.weights = weights
    
    def fit(self, X, y):
        self.tree = KDTree(X)
        self.y_train = y
    
    def predict(self, X):
        distances, indices = self.tree.query(X, k=self.k)
        
        if self.weights == 'distance':
            weights = 1 / (distances + 1e-6)  # 避免除零
            weighted_votes = np.zeros((len(X), len(np.unique(self.y_train))))
            
            for i, (idx, w) in enumerate(zip(indices, weights)):
                for j, weight in zip(idx, w):
                    weighted_votes[i, self.y_train[j]] += weight
                    
            return np.argmax(weighted_votes, axis=1)
        else:
            k_nearest_labels = self.y_train[indices]
            return np.array([Counter(x).most_common(1)[0][0] 
                           for x in k_nearest_labels])

4. 高级应用与疑难解答

4.1 处理类别不平衡问题

当某些类别样本远多于其他类别时,简单的多数投票会偏向多数类。解决方案:

  1. 加权投票:设置weights='distance',使近邻有更大权重
  2. 调整k值:增大k值可能缓解问题,但需验证
  3. 采样方法:对多数类欠采样或对少数类过采样
# 使用class_weight参数平衡类别
from sklearn.utils.class_weight import compute_sample_weight

sample_weights = compute_sample_weight('balanced', y_train)
knn.fit(X_train_scaled, y_train, sample_weight=sample_weights)

4.2 高维数据挑战

随着特征维度增加,"维度灾难"问题显现:

  • 所有样本的距离趋于相似,区分度降低
  • 所需样本量指数级增长

应对策略:

  • 特征选择:选择信息量大的特征
  • 降维:PCA或t-SNE等降维技术
  • 调整距离度量:尝试余弦相似度等更适合高维的度量

4.3 实时预测优化

当需要快速响应预测请求时:

  1. 使用近似最近邻算法(ANN)如Spotify的Annoy
  2. 对数据进行聚类预处理,先定位大致区域
  3. 考虑局部敏感哈希(LSH)等技术
# 使用Annoy加速近邻查询
from annoy import AnnoyIndex

t = AnnoyIndex(X_train.shape[1], 'angular')  # 使用余弦相似度
for i, x in enumerate(X_train):
    t.add_item(i, x)
t.build(10)  # 构建10棵树

# 查询近似最近邻
indices = t.get_nns_by_vector(test_sample, n=5)

在实际项目中,我发现当特征数超过50时,原生的KNN实现效率会显著下降。这时使用KD-Tree或Ball-Tree能带来10倍以上的速度提升,特别是在实现了早终止机制的情况下。另一个实用技巧是对连续特征进行分桶处理,这能在保持精度的同时大幅减少计算量。

Logo

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

更多推荐