用Python复现经典算法:手写KNN实现与特征工程实战

用Python复现经典算法:手写KNN实现与特征工程实战 用Python复现经典算法手写KNN实现与特征工程实战当我们在电商平台看到猜你喜欢的推荐或是手机相册自动识别人脸分组时背后很可能就运行着K近邻K-Nearest Neighbors算法。这个诞生于1951年的经典算法至今仍在推荐系统、图像识别等领域发挥着重要作用。本文将带您从零实现KNN算法并深入探讨特征工程的关键技巧。1. KNN算法原理与数学基础KNN算法的核心思想简单得令人惊讶——物以类聚。要判断一个新数据点的类别只需查看它在特征空间中最近的K个邻居这些邻居中占比最高的类别就是预测结果。这种基于实例的学习方法不需要显式的训练过程而是将计算延迟到预测阶段。距离度量是KNN的灵魂所在。假设我们有两个样本点A(x₁,y₁)和B(x₂,y₂)常用的距离计算公式包括欧氏距离L2范数distance sqrt((x₂-x₁)² (y₂-y₁)²)曼哈顿距离L1范数distance |x₂-x₁| |y₂-y₁|闵可夫斯基距离Lp范数的通用形式distance (|x₂-x₁|^p |y₂-y₁|^p)^(1/p)提示当特征量纲差异较大时欧氏距离会偏向数值较大的特征。这就是为什么特征缩放对KNN如此重要。在scikit-learn中KNN分类器的基本用法如下from sklearn.neighbors import KNeighborsClassifier knn KNeighborsClassifier(n_neighbors5) knn.fit(X_train, y_train) predictions knn.predict(X_test)2. 从零实现KNN算法让我们抛开scikit-learn用纯Python实现KNN算法。首先创建一个KNNClassifier类import numpy as np from collections import Counter from sklearn.metrics import accuracy_score class KNNClassifier: def __init__(self, k5, distance_metriceuclidean): self.k k self.metric distance_metric def fit(self, X, y): self.X_train np.array(X) self.y_train np.array(y) def predict(self, X_test): predictions [] for x in X_test: # 计算距离 if self.metric euclidean: distances np.sqrt(np.sum((self.X_train - x)**2, axis1)) elif self.metric manhattan: distances np.sum(np.abs(self.X_train - x), axis1) # 获取最近的k个邻居 k_indices np.argsort(distances)[: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)测试我们的实现# 使用鸢尾花数据集测试 from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split iris load_iris() X, y iris.data, iris.target X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3) # 比较自定义实现与sklearn版本 custom_knn KNNClassifier(k3) custom_knn.fit(X_train, y_train) custom_pred custom_knn.predict(X_test) sklearn_knn KNeighborsClassifier(n_neighbors3) sklearn_knn.fit(X_train, y_train) sklearn_pred sklearn_knn.predict(X_test) print(f自定义KNN准确率: {accuracy_score(y_test, custom_pred):.2f}) print(fSklearn KNN准确率: {accuracy_score(y_test, sklearn_pred):.2f})常见问题排查如果准确率差异较大检查距离计算是否正确当k值较小时可能出现过拟合可尝试增大k值确保输入数据没有缺失值3. 特征工程实战技巧KNN对特征缩放非常敏感因为距离计算直接依赖于特征尺度。假设我们有一个包含年龄20-60岁和收入20000-150000元的数据集未经缩放的收入特征会主导距离计算。3.1 特征缩放方法对比方法公式适用场景Python实现标准化(x - μ)/σ数据分布近似正态StandardScaler归一化(x - min)/(max - min)数据有明显边界MinMaxScaler鲁棒缩放(x - Q₁)/(Q₃ - Q₁)存在异常值RobustScalerfrom sklearn.preprocessing import StandardScaler, MinMaxScaler # 标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 归一化 minmax MinMaxScaler() X_train_minmax minmax.fit_transform(X_train) X_test_minmax minmax.transform(X_test)3.2 特征选择与降维当特征维度较高时KNN会遭遇维度灾难——在高维空间中所有点都变得同样远。解决方法包括方差阈值法移除方差低于阈值的特征from sklearn.feature_selection import VarianceThreshold selector VarianceThreshold(threshold0.1) X_reduced selector.fit_transform(X)PCA降维from sklearn.decomposition import PCA pca PCA(n_components0.95) # 保留95%方差 X_pca pca.fit_transform(X)3.3 类别特征处理对于包含类别型特征如颜色、品牌的数据集需要转换为数值表示有序类别使用标签编码Label Encodingfrom sklearn.preprocessing import LabelEncoder le LabelEncoder() df[color_encoded] le.fit_transform(df[color])无序类别使用独热编码One-Hot Encodingfrom sklearn.preprocessing import OneHotEncoder ohe OneHotEncoder() color_ohe ohe.fit_transform(df[[color]])4. 模型优化与评估4.1 K值选择与交叉验证K值过小会导致模型对噪声敏感过大则可能忽略局部特征。我们可以使用网格搜索结合交叉验证寻找最优K值from sklearn.model_selection import GridSearchCV param_grid {n_neighbors: range(1, 30)} grid GridSearchCV(KNeighborsClassifier(), param_grid, cv5) grid.fit(X_scaled, y) print(f最佳K值: {grid.best_params_[n_neighbors]}) print(f最佳准确率: {grid.best_score_:.2f})4.2 距离权重改进基础的KNN给所有邻居同等投票权改进版可以根据距离赋予不同权重# 加权KNN实现 def predict_weighted(self, X_test): predictions [] for x in X_test: distances np.sqrt(np.sum((self.X_train - x)**2, axis1)) k_indices np.argsort(distances)[:self.k] k_distances distances[k_indices] k_labels self.y_train[k_indices] # 使用距离倒数作为权重 weights 1 / (k_distances 1e-6) # 避免除零 weighted_votes {} for label, weight in zip(k_labels, weights): weighted_votes[label] weighted_votes.get(label, 0) weight predicted max(weighted_votes.items(), keylambda x: x[1])[0] predictions.append(predicted) return np.array(predictions)4.3 多维度评估指标除了准确率我们还应关注混淆矩阵from sklearn.metrics import confusion_matrix import seaborn as sns cm confusion_matrix(y_test, predictions) sns.heatmap(cm, annotTrue, fmtd)分类报告from sklearn.metrics import classification_report print(classification_report(y_test, predictions))对于不平衡数据集可以考虑采用加权准确率或F1-score作为评估指标。5. 实战案例手写数字识别让我们用KNN解决经典的MNIST手写数字识别问题from sklearn.datasets import fetch_openml from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler # 加载数据 mnist fetch_openml(mnist_784, version1, as_frameFalse) X, y mnist.data, mnist.target X_train, X_test, y_train, y_test X[:60000], X[60000:], y[:60000], y[60000:] # 创建管道 pipeline make_pipeline( StandardScaler(), KNeighborsClassifier(n_neighbors5, weightsdistance) ) # 训练与评估 pipeline.fit(X_train, y_train) print(f测试集准确率: {pipeline.score(X_test, y_test):.2f}) # 可视化错误样本 import matplotlib.pyplot as plt preds pipeline.predict(X_test) wrong_idx np.where(preds ! y_test)[0] plt.figure(figsize(10,4)) for i, idx in enumerate(wrong_idx[:5]): plt.subplot(1,5,i1) plt.imshow(X_test[idx].reshape(28,28), cmapgray) plt.title(fPred: {preds[idx]}\nTrue: {y_test[idx]}) plt.tight_layout()性能优化技巧使用PCA将784维降至50-100维尝试不同的距离度量如曼哈顿距离使用KD树或Ball Tree加速近邻搜索6. KNN的局限与应对策略虽然KNN简单有效但也有明显缺点计算复杂度高预测时需要计算与所有训练样本的距离解决方案使用近似最近邻算法如Annoy、FAISS内存消耗大需要存储全部训练数据解决方案使用核心集coreset技术减少数据量对不相关特征敏感解决方案结合特征选择方法类别不平衡问题解决方案采用加权投票或SMOTE过采样在实际项目中KNN常作为基线模型与更复杂的算法如随机森林、SVM进行对比。当特征维度不高且数据分布具有明显局部性时KNN往往能取得不错的效果。