资讯动态

KNN实战手写数字识别:MNIST数据处理与Python实现

发布时间:2026/9/28 15:15:28 来源:尧图企业网站定制
简介一套以KNN算法实现手写数字识别的小型项目资料面向机器学习入门者用来理解分类模型从样本训练到预测评估的完整流程。内容涵盖训练集trainingDigits、测试集testDigits以及核心代码KNN_digits.py可通过读取数字文本文件完成特征转化、距离计算、近邻投票和准确率统计适合课后实验或课程设计。压缩包共2882个文件2880个txt为标注好的手写数字样本1个py为KNN实现源码另有readme说明与使用指引整体大小972KB。txt文件按数字前缀命名标签清晰便于直接对照样本与预测结果KNN_digits.py包含数据读取、特征处理、欧氏距离计算、近邻排序与预测主流程适合逐行阅读。目前已有2807人学习下载。借助这套资料读者能直接运行代码复现识别效果也可调整k值、距离度量或训练集比例做对比实验从而深入掌握KNN算法本质与数据预处理思路。1. 从 MNIST 到 KNN为什么这个老算法还值得亲手写一遍如果你刚接触机器学习大概率听过 MNIST 手写数字识别也大概率看过 KNN 算法“准确率能达到 97%”这种结论。但真到自己动手拿到的往往是一个没有说明的源码包、一堆不知道格式的图片或者一份跑起来全是报错的 Python 脚本。打开KNN算法实现手写数字识别源码及数据集.zip之后你真正要做的事情不是“理解原理”而是让代码在你的机器上跑通、把准确率复现出来、把数据集看懂——这个过程里百分之八十的功夫都花在数据格式、距离度量和参数调整上而不是算法本身。KNNK-Nearest Neighbors的思路简单到可以用一句话讲完拿一个待识别的数字图片去训练集里找最像它的 K 张图少数服从多数决定它是几。它没有显式训练过程没有梯度下降也不需要神经网络那种“黑匣子”调参。但正因为简单它对手写数字这种低维、结构规整、样本量可控的任务反而非常合适不需要 GPU不需要深度学习框架一台普通笔记本电脑用 Python 就能在几分钟内把完整流程跑通。这也是为什么很多课程设计和入门实战都选这个题目的原因——你可以在一个下午之内拿到一套“能跑、能改、能讲清楚原理”的完整项目。这篇文章就是以这个压缩包为对象把 KNN 做手写数字识别从数据集解析、特征选择到 K 值调优和踩坑排查的完整路径过一遍。适合三类人一是做课程设计需要独立完成代码的同学二是想用经典算法验证自己机器学习基础能力的从业者三是想快速搭建一个可演示、可扩展的识别 Demo 的工程师。读完你会有可复现的代码、明确的参数依据以及一份自己写的排错清单。不要只停留在“跑通就完事”这个项目真正值钱的地方在于你能亲手验证一个非常朴素的想法在大约 40000 条样本上能取得怎样的效果以及它的边界在哪里。2. KNN 原理与手写数字识别的适配性先搞清楚它凭什么能干活2.1 距离度量KNN 的唯一核心假设KNN 算法本质上是“物以类聚”的数学化表达。假设有一堆已经标好数字的图片训练集现在来了一张新的、未标注的图片测试样本算法做的事情就是在训练集里找出与这张图“距离最近”的 K 张图统计这 K 张图各自属于哪个数字数量最多的那个数字作为预测结果。这里的“距离”是整个算法的灵魂。常见的做法有三种欧氏距离L2最直观计算两个向量对应位置的差的平方和再开方。曼哈顿距离L1计算两个向量对应位置的差的绝对值之和。余弦相似度更适合文本或高维稀疏向量对图像这种稠密像素向量来说通常不如 L1/L2 直观。如果使用 MNIST 数据集图片会被展平成 784 维向量28×28 像素此时欧氏距离是最常用的选择。原因很简单它对每个像素的差异都敏感并且数学性质好可直接比较大小。曼哈顿距离在图像上表现也不差而且计算量更小不需要开方如果数据集特别大可以考虑用 L1 换 L2 换取速度。可以用一个简单的表格来记住选择逻辑距离度量计算方式适用场景手写数字识别中的表现欧氏距离sqrt(Σ(xᵢ-yᵢ)²)默认选择对稠密向量有效常用效果稳定曼哈顿距离Σxᵢ-yᵢ余弦相似度向量夹角余弦文本、稀疏向量不太适合图像像素2.2 K 值的含义与选择逻辑从 K1 到 K20 的变化趋势K 值决定了算法“听多少邻居的意见”。K1 时预测结果完全由最近的一个样本决定模型非常敏感一个噪声样本就会改变结果容易出现“过拟合”的问题——训练集上的表现可能极好测试集上有轻微偏移就会翻车。K 增大到 5 或 7 时投票有了“容错机制”个别误标样本被压制整体准确率上升。K 继续增大到 20、30模型的“视野”变宽会把距离较远的样本也拉进来投票边界变得平滑但可能会误伤一些本来就处于类边界区域的样本准确率开始下降。在实际项目中K 的取值一般在 3 到 15 之间需要配合验证集来调。常见做法是把训练集再切出一部分做验证或者直接用交叉验证观察不同 K 值下的准确率曲线找到拐点。不要迷信“K 越大越好”或“K1 最准”的简单结论不同数据集、不同特征表示下最优 K 值差异很大。MNIST 上手写数字经过居中归一化后K3 或 K5 往往是不错的起点。2.3 手写数字识别为什么天然适合 KNN特征空间与样本量的匹配KNN 不适合高维大数据集因为维度越高样本间距离越趋向一致区分度急剧下降。但手写数字识别属于“低维、结构化、类别清晰”的任务图像是 28×28 的小尺寸灰度图数字的笔画结构相对固定不同数字之间的像素差异模式明显。MNIST 提供了 60000 张训练图和 10000 张测试图样本量充足类别只有 10 个每个类别有几千条样本。这样的数据分布让 KNN 的“邻近投票”策略有充分的样本支撑不会出现“最近的邻居其实离得很远”的尴尬局面。此外KNN 不需要训练过程意味着你可以把“训练”变成“直接查表”。对 MNIST 这种规模的数据集纯 Python 实现一次全量预测可能耗时几十秒到几分钟取决于代码优化程度但用 NumPy 向量化计算可以把耗时压到秒级。这也是后续章节要重点解决的问题——不是算法本身难而是如何用高效的代码实现看似笨拙的近邻搜索。3. 从零跑通 KNN 手写数字识别环境、数据集、完整代码与参数说明3.1 环境准备与数据集加载拿到压缩包先做这三件事解压KNN算法实现手写数字识别源码及数据集.zip后建议先看一下目录结构。常见的压缩包里会包含训练集图片可能是按文件夹分类的单张图片也可能是一个大的二进制文件测试集图片同样可能是两种格式之一标签文件每张图对应的真实数字一个或多个 Python 源码文件第一步确认 Python 环境和依赖库。KNN 实现至少需要numpy和matplotlib。如果代码里用了sklearn那就还需要scikit-learn。建议先检查环境再跑代码。以下命令可以在终端中直接执行python --version pip show numpy matplotlib scikit-learn如果提示某个库未安装用pip install numpy matplotlib scikit-learn补上。这里要注意 Python 版本3.8 到 3.11 都能正常运行这类代码3.12 以上部分库可能需要更新版本。不建议在没确认这几个依赖的情况下去跑源码否则很容易一上来就被 ModuleNotFoundError 卡住。第二步确认数据集的存储格式。这一步是最容易翻车的地方。如果压缩包里是.npz或.npy格式加载方式完全不同如果是.csv又是另一套逻辑。常见做法是先用 Python 加载一个小批量打印形状看一眼。下面这段代码可以快速探查数据集结构import numpy as np # 先尝试加载常见的 .npz 文件 data np.load(data/mnist.npz) print(data.files) train_images data[x_train] train_labels data[y_train] print(训练集形状:, train_images.shape, 标签形状:, train_labels.shape) print(像素值范围:, train_images.min(), train_images.max())逻辑说明np.load会把.npz文件里的多个数组全部读入内存data.files列出所有键名。打印形状是为了确认数据排列方式是(样本数, 28, 28)还是(样本数, 784)两者在后续代码中处理方式不同。像素值范围也很关键如果范围是0-255需要归一化如果已经是0.0-1.0那直接就能用。参数说明如果你的数据集不是mnist.npz而是图片文件夹就不要硬套这段代码。此时需要用PIL或opencv循环读取图片文件建议先读 10 张测试一下路径是否正确不要直接扫描整个目录——遇到一个损坏文件就会中断整个流程。第三步做一个最小化数据预览。加载完数据后先打印一段标签分布确认类别是否均衡unique, counts np.unique(train_labels, return_countsTrue) print(标签分布:, dict(zip(unique, counts)))如果某个数字的样本量特别少后续 KNN 的识别效果就可能对该类别偏差较大。MNIST 中每个数字的样本量都比较平均6000 张左右但如果用的是自定义数据集这一步能帮你提前发现问题。3.2 数据预处理形状转换、归一化与训练/验证集划分数据加载后第一步永远是确认形状和取值。MNIST 的标准格式是灰度图单通道像素值范围 0 到 255。KNN 计算距离时不同维度的量纲必须统一否则数值范围大的维度会主导距离计算。虽然 784 个像素维度的量纲天然一致都是 0-255但建议还是归一化到 0-1 之间这样可以略微避免计算欧氏距离时出现极大数值累积导致的浮点精度问题同时让距离值更容易解释。另一个关键操作是把图片矩阵展开成一维向量。原始形状如果是(60000, 28, 28)需要转换为(60000, 784)。注意转换顺序reshape是按行优先展开的28×28 的像素矩阵会从第一行开始逐行拼接成长度为 784 的向量这个顺序对所有图片必须一致否则距离计算就失去了对应性。下面给出一个完整的预处理代码块import numpy as np # 假设已加载 train_images, train_labels, test_images, test_labels # 1. 转换为浮点类型并归一化 train_images train_images.astype(np.float32) / 255.0 test_images test_images.astype(np.float32) / 255.0 # 2. 展开成二维矩阵每行是一条样本的 784 维特征 train_images_flat train_images.reshape(train_images.shape[0], -1) test_images_flat test_images.reshape(test_images.shape[0], -1) # 3. 从训练集中切出 20% 作为验证集用于调 K 值 val_size int(train_images_flat.shape[0] * 0.2) val_images train_images_flat[:val_size] val_labels train_labels[:val_size] train_images_final train_images_flat[val_size:] train_labels_final train_labels[val_size:] print(最终训练集:, train_images_final.shape) print(验证集:, val_images.shape)逻辑说明astype(np.float32)是为了转换数据类型避免整数除法问题。如果不转成浮点数/255.0得到的结果将是 0 或一个极大的近似值距离计算会失真。reshape(-1)的写法表示自动推断列数28×28784所以-1会被解析为 784。切分验证集时固定取前 20% 样本不考虑打乱——这在小数据集上不影响整体评估但如果你后续要做严格的交叉验证记得使用np.random.shuffle先打乱数据。参数说明val_size的比例是 0.2也就是 12000 条验证样本。对 KNN 来说验证集越大K 值评估越稳定但计算耗时也会线性增加。如果机器性能一般可以改为 10%。注意不要和测试集混淆验证集用于选 K测试集只用于最终评估一次。3.3 KNN 分类器的核心实现纯 NumPy 写一个高效版本SKlearn 里虽然有现成的KNeighborsClassifier但自己做课程设计时最好亲手写一个低配版原因有两点一是面试或答辩时能讲清楚每个环节二是纯 NumPy 实现能深入理解距离计算和排序的细节。这里给出一份最小但完整的实现import numpy as np from collections import Counter class KNNClassifier: def __init__(self, k5, metricl2): self.k k self.metric metric def fit(self, X, y): # KNN 的 fit 只是保存训练数据 self.X_train X self.y_train y def predict(self, X_test): # X_test: (n_samples, n_features) predictions [self._predict_one(x) for x in X_test] return np.array(predictions) def _predict_one(self, x): if self.metric l2: # 欧氏距离: sqrt(Σ(x_i - y_i)^2) distances np.sqrt(((self.X_train - x) ** 2).sum(axis1)) elif self.metric l1: # 曼哈顿距离: Σ|x_i - y_i| distances np.abs(self.X_train - x).sum(axis1) else: raise ValueError(Unsupported metric) # 取最近的 k 个邻居的索引 k_indices np.argsort(distances)[:self.k] # 统计这 k 个邻居的标签取出现次数最多的作为预测结果 k_labels self.y_train[k_indices] most_common Counter(k_labels).most_common(1)[0][0] return most_common逻辑说明fit方法里实际上没有做任何计算只是把训练数据存下来这正是 KNN 被称为“懒惰学习”的原因。在_predict_one方法中核心操作有两步第一步计算测试样本与所有训练样本的距离得到一个长度为训练样本数的一维数组第二步用np.argsort对距离升序排列取前 K 个索引然后投票。Counter(k_labels).most_common(1)返回的是[(标签, 次数)]这样的列表所以需要两层索引拿到标签值。参数说明k的默认值设为 5metric支持l2和l1。使用 P2 时注意np.sqrt可以省略因为开方是单调递增操作不影响排序结果。省略掉这一步会明显加快计算速度这也是很多工业级实现里的常见优化手段。3.4 用验证集调 K 值画出准确率曲线再决定参数上面这个纯 Python 实现有一个性能问题对每个测试样本都要遍历一次整个训练集。如果验证集有 12000 条样本训练集有 48000 条样本那么一次预测就需要计算 5.76 亿次距离。虽然 NumPy 的向量化计算能把单次距离计算压到极快但总耗时依然很可观。所以调 K 值时建议从验证集中抽取一小部分比如 2000 条来评估避免等待时间过长。下面是调参代码import numpy as np import matplotlib.pyplot as plt # 从验证集中随机抽样 2000 条用于调参 sample_idx np.random.choice(val_images.shape[0], 2000, replaceFalse) val_x_sample val_images[sample_idx] val_y_sample val_labels[sample_idx] k_values [1, 3, 5, 7, 9, 11, 15, 19] accuracies [] for k in k_values: clf KNNClassifier(kk) clf.fit(train_images_final, train_labels_final) pred clf.predict(val_x_sample) acc np.mean(pred val_y_sample) accuracies.append(acc) print(fK{k}, 准确率{acc:.4f}) # 绘制准确率随 K 变化的曲线 plt.plot(k_values, accuracies, markero) plt.xlabel(K) plt.ylabel(Accuracy) plt.title(KNN Accuracy vs K Value) plt.grid(True) plt.show()逻辑说明这里每次循环都重新创建了一个分类器实例并调用了fit其实fit只是保存引用开销很小。真正耗时的是每一次predict调用里的距离计算。2000 条验证样本乘以 48000 条训练样本一次预测大约需要计算 9600 万次距离我的经验是在普通笔记本上几秒内能跑完完全可以接受。参数说明k_values的选择覆盖了从过拟合到欠拟合的典型区间。K1 通常会达到最高训练准确率但验证准确率可能不是最高K3 到 K9 之间往往会有一个峰值。如果你发现准确率在 K1 时最高并且 K 增大后迅速下降说明数据本身就存在一定的噪声干扰需要引入距离加权。K15 以上如果准确率依然很高说明类别边界比较平滑数据质量好但这在真实业务场景中很少见。3.5 最终测试在 10000 张测试集上评估真实效果选定最优 K 值后最后一步是在完全没参与过调参的测试集上评估一次。这一步的意义是验证模型没有过拟合到验证集上。测试集和验证集是两拨数据如果测试准确率和验证准确率相差超过 1%就要警惕训练集和验证集分布不一致或数据泄露的问题。best_k 5 # 根据上面的曲线选择 clf_final KNNClassifier(kbest_k) clf_final.fit(train_images_final, train_labels_final) # 在测试集上评估 predictions clf_final.predict(test_images_flat) test_accuracy np.mean(predictions test_labels) print(f测试集准确率: {test_accuracy:.4f}) # 查看每个数字的分类准确率判断哪些类别比较难分 class_accuracies [] for digit in range(10): idx test_labels digit digit_acc np.mean(predictions[idx] test_labels[idx]) class_accuracies.append(digit_acc) print(f数字 {digit} 的准确率: {digit_acc:.4f})逻辑说明按数字类别分别统计准确率能帮你找到模型的薄弱环节。MNIST 上常见的困惑组合是 4 和 9、3 和 8、7 和 1这些数字在某些人笔迹下非常相似。如果某个数字的准确率低于整体准确率 3% 以上可以考虑为该类别增加更多训练样本或者使用更精细的特征提取方法。注意整个测试集有 10000 张图片如果使用纯 Python 实现这次预测可能会比较慢——在我的笔记本上大约需要 10 到 30 秒。这是正常现象KNN 的预测复杂度是 O(N)没有捷径可走。如果你的机器跑不动可以抽样 2000 条做测试但这样准确率的置信区间会变大不推荐作为最终结果。4. KNN 手写数字识别的 5 个典型翻车点与排查方法4.1 翻车点数据集形状对不上——reshape报错或维度错乱现象运行train_images.reshape(-1, 28*28)时报错提示cannot reshape array of size X into shape (?, 784)或者代码能跑但准确率极低比如 20% 以下。原因最常见的情况是数据集的原始形状不是(n_samples, 28, 28)。有些数据集的图片是 32×32 或 48×48 的比如 EMNIST 就是 28×28 但经过了不同的裁剪方式。另一种可能是数据集是(n_samples, 28, 28, 1)的四维形状多了一个颜色通道维度reshape(-1, 784)会把这四维数据按行优先的顺序强行拼成二维矩阵导致像素错位。还有一种情况比较隐蔽——标签集和图片集的行数不一致比如图片有 60000 张但标签只有 59900 条fit时不报错预测后y_train[k_indices]会越界报错。解决加载数据后立即打印.shape和.ndim确认维度。如果是四维带通道的先squeeze()去掉通道轴。如果图片尺寸不是 28×28先把它缩放或裁剪到统一尺寸。推荐用一个assert语句做防御式检查assert train_images.ndim 3, fexpected 3D array, got {train_images.ndim}D assert train_images.shape[1:] (28, 28), funexpected image size {train_images.shape[1:]} assert train_images.shape[0] train_labels.shape[0], samples and labels length mismatch4.2 翻车点未归一化导致的距离失真现象预测准确率尚可90% 左右但无法达到预期的 97%而且无论怎么调 K准确率都上不去。原因像素值在 0-255 的整数范围内时计算欧氏距离会产生非常大的数值最大可到 sqrt(784×255²)≈7140。由于浮点精度限制一些本应有区分度的细微像素差异在超大数值面前变得无足轻重导致近邻选择不够精确。另外如果训练集和测试集有一个做了归一化而另一个没做分布偏移会让准确率明显下降。解决对所有图片统一执行astype(np.float32) / 255.0确保训练集和测试集使用完全相同的归一化操作。一个隐蔽的坑是如果用matplotlib.pyplot.imshow显示图片后图像数据被隐式转换成了浮点数后续操作如果直接除以 255 就可能出错。保持数据管线干净不要混用不同数据类型的数组。4.3 翻车点K 值选择凭感觉没有画出曲线现象直接拍脑袋选了一个 K 值比如 K1 或 K30验证集上表现不稳定不同随机种子下结果波动很大。原因K 值太小对噪声敏感K 值太大忽视局部结构。没有基于验证集的实际表现进行选择完全依赖直觉或网络上的经验值而不同数据集的最优 K 值差异极大。解决严格按 3.4 节的流程在验证集上对多个 K 值进行扫描画出准确率曲线。推荐另外多做一步重复 3 到 5 次随机抽样验证看每个 K 值下的准确率均值和标准差选择均值最高且标准差较小的 K 值。不要只跑一次就下结论。4.4 翻车点距离度量选择不当准确率卡在 90% 上不去现象使用欧氏距离时准确率不理想换用曼哈顿距离后反而提升了 1% 到 2%但你又不知道为什么会这样。原因MNIST 的像素是稀疏的——大部分像素是黑色背景0 值笔画区域的像素变化有意义。欧氏距离对噪声较大的像素差值敏感曼哈顿距离对异常值更稳健。在某些情况下特别是图片有轻微平移或笔画粗细不均时曼哈顿距离的累计绝对值误差更小因为它不像欧氏距离那样把大差值放大。解决在代码中把距离计算抽象成参数同时测试 L1 和 L2在验证集上比较结果。不要想当然地认为欧氏距离是唯一正确选择。以下是修改后的距离计算段注意 L1 和 L2 的切换只是把np.sqrt和平方变成绝对值和求和if metric l1: dists np.abs(self.X_train - x).sum(axis1) elif metric l2: dists ((self.X_train - x) ** 2).sum(axis1) # 注意不取 sqrt 也能保持排序4.5 翻车点暴力计算导致内存爆炸或等待时间过长现象在测试集上预测时代码跑了 5 分钟还没出结果或者内存直接飙升到占满物理内存。原因纯 Python 循环 每样本独立计算距离时间复杂度是 O(N_test × N_train × dim)dim784。当测试集 10000 条、训练集 48000 条时需要计算 4.8 亿次 784 维距离在 Python 解释器下是灾难性的。另一种极端情况是试图一次性把所有测试样本与所有训练样本的完整距离矩阵加载到内存10000×48000×4 字节约 1.9GB看起来好像能扛住但 Python 对象开销会迅速让内存翻倍。解决第一把循环改成矩阵广播一次计算一批测试样本与全部训练样本的距离第二不要一次性传 10000 条测试样本用batch_size分批预测。推荐在predict方法中加入批处理逻辑def predict_batch(self, X_test, batch_size500): n_samples X_test.shape[0] predictions [] for start in range(0, n_samples, batch_size): batch X_test[start:start batch_size] # 批量计算距离矩阵 (batch_size, n_train) distances np.linalg.norm(self.X_train[None, :, :] - batch[:, None, :], axis2) nearest np.argsort(distances, axis1)[:, :self.k] batch_labels self.y_train[nearest] # 向量化投票简化版用 bincount 处理 preds [np.bincount(labels).argmax() for labels in batch_labels] predictions.extend(preds) return np.array(predictions)逻辑说明这里self.X_train[None, :, :]把训练集扩展成形状(1, N_train, dim)batch[:, None, :]把测试批次扩展成(batch_size, 1, dim)两者相减会自动广播为(batch_size, N_train, dim)然后沿最后一维做 L2 范数计算一步得到所有距离。np.bincount对每个样本的邻居标签做计数argmax直接得到众数避免了 Python 循环中的Counter开销。批大小设为 500内存占用大约500×48000×4×4≈384MB在安全范围内。5. 让 KNN 再快一点、再准一点两个有效且不复杂的优化方向5.1 用 PCA 降维砍掉冗余像素特征减半准确率不降反升KNN 的一个弱点是维度灾难——784 维向量中很多像素是背景区域对分类几乎没有贡献。PCA主成分分析可以把 784 维压缩到 40 到 80 维同时保留主要结构信息。在降维后的空间里计算距离噪声像素的影响被大幅削弱距离计算速度也成倍提升。下面是基于sklearn.decomposition.PCA的用法完整嵌入 KNN 流程from sklearn.decomposition import PCA # 先对训练集拟合 PCA再转换训练集和测试集 pca PCA(n_components50) X_train_pca pca.fit_transform(train_images_final) X_val_pca pca.transform(val_images) X_test_pca pca.transform(test_images_flat) print(PCA 后的维度:, X_train_pca.shape) print(保留的方差比例:, pca.explained_variance_ratio_.sum())逻辑说明fit_transform在训练集上拟合主成分方向并投影transform对验证集和测试集只做投影不做重新拟合。这样保证了训练集和测试集使用完全相同的线性变换。explained_variance_ratio_.sum()告诉你能保留多少原始信息通常在 50 维时能保留 90% 以上。参数说明n_components50是一个经验起点。可以把这个值当成超参数配合 K 一起扫描比如分别尝试 30、50、80、100 维观察验证集准确率的变化。有一个直观的经验法则降维后准确率如果能持平甚至更高说明冗余像素确实在干扰 KNN 的距离计算。降维到 50 维后距离计算的计算量直接从 784 降到 50速度可以提升一个数量级。5.2 距离加权投票让近邻比远邻更有发言权标准 KNN 的投票是等权的K5 时每个邻居对最终结果的影响完全一样。这在样本密度不均匀的场景下会出问题——如果 5 个邻居里有 2 个离待测样本非常近另外 3 个有点远等权投票可能让那 3 个稍远的邻居主导结果。改进方案是按距离倒数加权离得越近的邻居投票权重越大。修改核心投票逻辑如下def _predict_one_weighted(self, x): distances ((self.X_train - x) ** 2).sum(axis1) # 不取 sqrt k_indices np.argpartition(distances, self.k)[:self.k] k_distances distances[k_indices] k_labels self.y_train[k_indices] # 计算权重距离越小权重越大加上一个小常数防止除零 weights 1.0 / (k_distances 1e-8) # 按标签累加权重 score {} for label, weight in zip(k_labels, weights): score[label] score.get(label, 0) weight most_common max(score, keyscore.get) return most_common逻辑说明np.argpartition(distances, self.k)[:self.k]比np.argsort更快——它只需要保证前 K 个是最小的不需要对全部元素排序。weights中的常数1e-8避免了距离为零样本完全一样时除零。距离加权后的投票本质上是“近距离样本说话更算数”在 MNIST 上通常能提升 0.2 到 0.5 个百分点的准确率不算多但如果你为了排名在细抠准确率这一点差距可能就是排名上升的关键。5.3 验证这个项目值不值得深入三个指标决定你的下一步完成上面的优化后不要急着结束。花十分钟做一个系统的结果记录记录三组数字原始 KNN 在测试集上的准确率、PCA 降维后 KNN 的准确率、加权投票 KNN 的准确率。这三组数据放在一起你在答辩或写文档时能直接说明“每一步优化带来了什么改变”。如果三者相差不超过 0.5%说明数据本身非常干净模型瓶颈不在算法而在特征表示——此时可以考虑提取更高级的特征比如方向梯度直方图HOG或局部二值模式LBP配合 KNN 往往能突破 98%。如果你发现加权投票反而降低了准确率不必意外这是因为验证集和测试集的分布差异在起作用。加权投票对噪声更敏感K 值需要重新调整。在我的实践中K5 加权投票的表现通常优于 K5 等权投票但 K9 等权投票可能优于 K9 加权投票——K 变大了以后远距离样本本身已经足够多加权效果被稀释了。这些细节都要以实验数据为准不要预设结论。到了这一步这个 KNN 手写数字识别项目就已经完整跑通并具备了你自己的改进痕迹。我自己第一次做这个项目时卡在reshape上浪费了一个多小时后来养成一个习惯任何数据加载进去先print(shape)再动手。这个习惯帮我避开了很多后续的坑。这次你能从数据集加载一路做到距离加权、PCA 降维已经比很多只调sklearn接口的人理解得更深了。希望这篇笔记帮到你也祝你在调参的路上少踩几个坑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑