
简介该资源是基于KNN算法实现手写数字识别的完整入门学习包适合刚开始接触机器学习、希望理解K近邻分类原理的初学者。压缩包内共2882个文件包含2880个txt格式的训练与测试样本以及1个Python源代码文件和1个Readme说明文件整体大小约972KB目录结构清晰便于对照学习。资源已吸引2807人学习使用。通过运行源代码并配合大量数字样本读者可以直观理解KNN算法的核心流程包括样本数据读取、距离计算、近邻投票和分类结果输出同时也能借此体验从数据集准备到模型评估的完整实践路径为后续学习其他机器学习算法打下基础。1. 为什么手写数字识别总拿KNN练手最直观的机器学习入门项目KNN算法是机器学习里少有的“不训练”的算法却能把手写数字识别做到 95% 以上的准确率很多入门者第一次感受到机器学习的威力就是从这份 KNN 手写数字识别项目开始的。这个资源包提供了一整套可复现的素材KNN_digits.py 源码、trainingDigits 训练集、testDigits 测试集以及若干单独的示例数字文件比如 0_45.txt、4_80.txt 这类命名清晰的样本。你不需要安装深度学习框架只要 Python 环境里带着 numpy 就能跑起来。它解决的问题非常具体给定一张 32×32 的 0/1 文本图像判断它是数字几。对于想理解 KNN 原理、熟悉文本数据解析、搞清楚分类流程的初学者来说这是成本最低的实战入口对已经上过理论课的熟手也适合拿来验证自己对距离度量、k 值选择和样本分布的理解。2. 距离度量与投票机制KNN 两个核心代码怎么写才稳2.1 惰性学习与欧氏距离的基本逻辑KNN 的全称是 K-Nearest Neighbors核心思想朴素到近乎“懒人逻辑”训练阶段不做任何学习只是把样本连同标签存起来等到来了一条未知样本就在存储的样本里找距离最近的 k 个邻居让邻居们投票决定类别。这也是它常被称为“惰性学习”的原因——真正的计算全部发生在预测时。手写数字识别里用的距离度量最常见的是欧氏距离。假设我们把一张 32×32 的文本图像展开成一个 1024 维的向量那么两张图的距离就是逐维求差平方、再求和开根号。数学形式不复杂但翻译成代码时有一个容易踩的坑如果直接写双层 for 循环去逐个样本求距离在几百个训练样本时尚可忍受一旦把整个训练集都装进去预测一条样本就要算几百次 1024 维向量的差龟速感会非常明显。常见做法是利用 numpy 的广播机制把训练集拼成一个二维矩阵一次算出目标向量与所有训练样本的距离数组这样代码既短又快。import numpy as np def euclidean_distances(x, X_train): # x: 待预测样本, 形状 (1024,) # X_train: 训练集矩阵, 形状 (n_samples, 1024) diff X_train - x # 广播计算每个维度差值 squared diff ** 2 # 逐维平方 summed squared.sum(axis1) # 沿特征维度求和 distances np.sqrt(summed) # 开根号得到欧氏距离 return distances这段代码里X_train - x是一个形状为 (n_samples, 1024) 的矩阵减法numpy 会自动把 x 广播到每一行axis1指的是沿着特征列方向求和得到每个样本的距离标量。距离越小说明两张数字图像越像。选用这个写法而不是 for 循环一方面是为了让代码接近 KNN_digits.py 这类项目的实际风格另一方面也是为后面做全量测试集预测时节省时间。2.2 k 值与投票策略多数的胜利也会出错找到距离后下一步是确定 k 值。k 表示取距离最近的多少个邻居参与投票。k1 时最简单只看最近的一个样本把它的标签直接拿来用k 增大后多个邻居各自投一票票数最多的类别胜出。这个机制听上去直白但它直接决定了分类边界的平滑程度。k 太小模型容易对噪声敏感训练集里某个歪歪扭扭的样本就可能带偏结果k 太大又可能把距离较远的样本也拉进来投票导致边界过于平滑、细节丢失。在手写数字场景里k 通常取 3 到 15 之间具体取几最好画一条准确率曲线来看。投票时如果每个邻居权重相同就是简单多数制也可以按距离倒数加权让近的邻居话语权更大。后一种方式理论上更精细但在二值的 0/1 文本图像上收益往往不明显这个我在第 4 章会详细讲。def knn_predict(x, X_train, y_train, k): distances euclidean_distances(x, X_train) nearest_idx np.argsort(distances)[:k] # 取距离最小的 k 个索引 nearest_labels y_train[nearest_idx] # 拿到对应标签 # 统计票数返回出现次数最多的标签 unique, counts np.unique(nearest_labels, return_countsTrue) return unique[np.argmax(counts)]np.argsort返回的是距离从小到大排列后的原始索引切片取前 k 个np.unique配合return_countsTrue可以得到每个标签的出现次数np.argmax(counts)取出票数最多的那个下标。这段逻辑几乎就是 KNN_digits.py 里 classify0 函数的微型版理解了它再看完整源码就不会觉得有黑匣子。2.3 把 txt 文件变成向量img2vector 的细节手写数字样本在资源包里并不是图片而是文本文件。文件内部是一个 32×32 的 0/1 矩阵行与行之间用换行分隔每行的 0/1 字符之间通常有空格。这样设计的好处是省去图像解码的依赖任何文本编辑器都能打开查看但坏处是你必须自己写一个解析函数把字符矩阵变成 numpy 向量。文件名也携带标签信息比如 4_80.txt 表示标签是 4、编号是 80这是后续自动读取标签的关键依据。def img2vector(filename): # 返回一个 1x1024 的 numpy 数组 vect np.zeros((1, 1024)) with open(filename, r) as f: for i in range(32): line f.readline().strip() for j in range(32): vect[0, i * 32 j] int(line[j]) return vecti * 32 j是把二维坐标映射到一维索引的关键第 i 行第 j 列的元素落在向量的第 i×32j 个位置line[j]读取的是单个字符int()转成数值。要注意的是strip()必须做否则行尾的换行符会混进来如果文件内容里每行字符间有空格那line[j]的索引就要改成按空格切分后再取这也是新手最容易翻车的地方之一我会在第 5 章展开。3. 完整跑通 KNN_digits.py路径、循环与结果解读3.1 脚本骨架与运行流程KNN_digits.py 的典型结构分四块读取训练集、读取测试集、逐个预测并比对标签、输出准确率。训练集目录下通常有接近两千个 txt 文件测试集有九百多个逐个文件解析后拼成一个大矩阵。测试阶段对每一条测试样本调用 knn_predict把预测结果与文件名自带的真实标签比对统计错误数最后算出准确率。这个流程不复杂但代码的组织方式直接影响你后续能不能快速改参数。python KNN_digits.py如果你只是想在命令行里跑通这一句就够。但脚本里通常需要你先确认训练集和测试集目录是否存在以及路径是否写对了。很多初学者双击运行后遇到FileNotFoundError第一反应是代码有问题其实多数是当前工作目录和脚本里写的相对路径对不上。import os from pathlib import Path base_dir Path(__file__).parent # 脚本所在目录 train_dir base_dir / trainingDigits test_dir base_dir / testDigits train_files os.listdir(train_dir) test_files os.listdir(test_dir) print(ftraining samples: {len(train_files)}, test samples: {len(test_files)})Path(__file__).parent是当前脚本文件所在目录的 Path 对象用/拼接子目录名能避免“从别的目录启动脚本时找不到文件”的经典问题。打印样本数这一步看似多余实则是第一道体检如果训练集目录为空或者路径写错这里立刻暴露。3.2 训练与预测的路径设定训练阶段要做的是把 trainingDigits 下所有文件读成向量并组装成训练矩阵 X_train再用文件名前缀解析出标签数组 y_train。解析函数通常写成int(filename.split(_)[0])含义是取“4_80.txt”里的 4。测试阶段同样处理 testDigits但不需要提前算好所有测试样本的距离——每一条单独和训练集计算即可。这里有一种常见的写法是把测试集也全部读入内存好处是方便批量调试坏处是内存翻倍但对这个规模的数据量完全不是问题。def load_dir_to_matrix(dir_path): file_list os.listdir(dir_path) n len(file_list) X np.zeros((n, 1024)) y np.zeros((n,), dtypeint) for idx, fname in enumerate(file_list): X[idx] img2vector(os.path.join(dir_path, fname)) y[idx] int(fname.split(_)[0]) return X, y X_train, y_train load_dir_to_matrix(train_dir) X_test, y_test load_dir_to_matrix(test_dir)os.listdir返回的文件名顺序不保证稳定但这不影响结果因为每个文件的标签都是从它自己的名字解析出来的和顺序无关。dtypeint是为了让标签数组在后续np.unique统计时保持整数类型避免字符串标签带来的额外转换。把读取逻辑封装成load_dir_to_matrix是值得养成的习惯后面如果你想换数据集只需要改这一个函数。3.3 看懂输出识别结果、错误计数与耗时预测循环是最耗时的一段也是最能看出算法效率差异的地方。error_count 0 for i in range(len(X_test)): prediction knn_predict(X_test[i], X_train, y_train, k5) if prediction ! y_test[i]: error_count 1 print(ferrors: {error_count}, total: {len(X_test)}, accuracy: {1 - error_count / len(X_test):.4f})这段代码每循环一次就对一条测试样本算一遍与全部训练样本的距离。如果训练集有 1900 个样本、测试集有 900 个那就是 170 多万次 1024 维向量的差值运算。跑起来可能要几十秒这是 KNN 的天性——训练零成本预测全成本。输出里最该关注的是 accuracy 前面的 error_count如果错误数异常大比如超过 50问题大概率不在算法本身而在数据解析或标签映射这一点也是第 5 章避坑内容的主角。4. k 值与权重怎么选调参经验与三组对照实验4.1 用交叉验证选 k流程与代码很多教程会直接告诉你 k3 或 k5但实际项目里最稳妥的办法是把训练集再切出一部分做验证跑一遍准确率曲线。交叉验证的思路是把训练集随机分成若干份轮流拿一份当验证集、其余当临时训练集取平均准确率然后对不同的 k 重复这一过程画出 k 与准确率的关系。from sklearn.model_selection import cross_val_score from sklearn.neighbors import KNeighborsClassifier clf KNeighborsClassifier(n_neighbors5, metriceuclidean) scores cross_val_score(clf, X_train, y_train, cv5) print(f5-fold acc: {scores.mean():.4f} ± {scores.std():.4f})这段代码用了 scikit-learn 的 KNeighborsClassifier 来快速验证cv5表示五折交叉验证metriceuclidean指定欧氏距离。注意这里交叉验证的对象是训练集而不是测试集——测试集只能最后用一次否则你用测试集调参得到的结果就有样本泄漏的嫌疑准确率虚高。4.2 三组对照实验距离权重与归一化的选择为了看清楚 k 和权重的影响我习惯固定在同一份训练/测试划分上跑三组对照。以下表格是我在这个文本格式数据集上常见的现象注意不同机器和样本划分会有浮动但趋势是稳定的。实验组k 值权重方式典型准确率观察结论A 组1无权重98% 左右单点最近邻快但波动大B 组5无权重97%~98%稳定边界平滑C 组5距离倒数加权97%~98%与 B 组基本持平C 组为什么没有明显提升原因在于数据形态。这里每个样本是 0/1 二值向量非零位的数量差异本身就很大距离的主要贡献来自“哪些位置不同”而距离倒数加权更擅长处理连续灰度特征下的细微远近差异。二值特征下“近一点”和“远一点”的区分度不够细腻权重带来的增益很小。如果你的数字图像是灰度图而不是二值图C 组的效果可能会更好这点需要你自己实验验证不要照搬结论。4.3 归一化与其他特征取舍KNN 的距离公式对所有维度一视同仁如果某个维度的数值范围特别大它会主导整个距离。手写数字文本是 0/1 二值天然处于同一量纲所以这里不需要额外归一化。但如果哪一天你把灰度图直接拼成向量像素值 0~255 就会让距离计算偏向高亮区域那时必须做归一化常见做法是把所有像素值除以 255 压到 0~1 区间。另一个取舍是特征降维。1024 维并不是每个维度都有判别力图像边缘的大量 0 区域占着维度却不贡献信息。直接砍掉全零列或者用 PCA 压缩到 100 维左右往往能在准确率几乎不变的前提下把距离计算量降一个数量级。这部分我放到第 6 章讲因为那属于跑通之后才值得做的优化。5. 常见问题与避坑路径、标签解析与样本泄漏排查5.1 FileNotFoundError脚本能跑但工作目录不对现象运行 KNN_digits.py 报错找不到 trainingDigits 或 testDigits但在文件管理器里明明能看到这两个文件夹。原因脚本用了相对路径trainingDigits而 Python 解析相对路径的基准是“当前工作目录”也就是你执行python KNN_digits.py时所在的目录。如果从项目外层目录启动脚本它就跑到上一层去找子目录了必然找不到。解决把路径基准改为脚本自身所在目录base_dir Path(__file__).parent后续所有目录都从 base_dir 拼接。从那以后我每次拿到别人的源码第一件事就是全局搜索open(和os.listdir(把所有相对路径改成基于__file__的写法这个习惯帮我省了大量排错时间。5.2 标签解析错位split 切出的是 4 还是 4_80现象准确率骤降到 20% 以下甚至接近随机猜测但代码逻辑看起来毫无问题。原因标签解析用了int(fname.split(_)[0])这没问题问题往往出在 img2vector 里读取文件内容时按空格做了切分但某些 txt 的行尾多了空格、某些行中间是制表符而不是空格导致读入的 0/1 字符错位。还有一种情况是文件内部并非 32 列而是 32 列字符加末尾一个换行line[j]在 j31 时读到了不该有的字符。解决在 img2vector 里先line.split()再取元素不要按固定下标去读字符。同时加一个断言读取时确认每一行可解析出来的元素个数确实是 32不是就打印文件名并跳过。这种“宁可报错也不静默算错”的思路是我在调试这个数据集时的血泪经验。5.3 准确率虚高测试集和训练集样本同源现象测试准确率高达 99%但拿到自己手写的数字图片一识别就翻车怀疑人生。原因训练集和测试集来自同一个人、同一轮采集字形风格高度一致相当于开卷考试。KNN 在这种数据上学到的是“跟谁像”而不是“数字有什么结构特征”一旦遇到不同书写风格距离最近的可能依然是训练集里某个风格相近但标签不对的样本。解决把测试集换成自己采集的样本或者至少从训练集里刻意划出不同批次的数据做验证。这个项目里更实际的做法是自己用画图工具生成几张 32×32 的二值数字图按同样格式存成 txt看看识别准不准——这才是真正考验泛化能力的方式。5.4 k1 的高准确率假象现象k1 跑出来准确率最高k 增大反而下降于是决定把 k 固定成 1。原因k1 确实是偏差最小的选择但方差最大。单一最近邻容易受噪声样本影响一个标注错误或畸形的训练样本就能带偏一整条测试预测。当前数据集里噪声少所以 k1 看起来很美换到更脏的真实数据k1 的稳定性立刻崩盘。解决不看单次准确率而是对多个 k 值跑交叉验证选一个在均值高、方差小的区间里的 k通常落在 3~10。记住训练集上的最优 k 不等于测试集上的最优 k永远用验证集做选择。5.5 性能翻车全量距离计算让程序卡到像死机现象预测循环跑了几分钟还没结束以为脚本写了个死循环。原因KNN 预测阶段是逐样本全量计算复杂度是 O(n_test × n_train × dim)dim 是 1024 维。数据量小的时候无所谓一旦训练集扩容到几万个样本这段代码会慢到让你怀疑 Python 是不是被锁死了。解决先用 numpy 向量化替代 for 循环再考虑 sklearn 的 KDTree 或 BallTree 做近邻搜索。numpy 向量化能把单条样本的距离计算从内层 for 循环变成一次矩阵运算提速通常是 10 倍量级。如果还不够就用 sklearn 的KNeighborsClassifier(algorithmkd_tree)它会自动建树预测复杂度更低。6. 验证与进阶混淆矩阵、k 值曲线与 PCA 提速6.1 混淆矩阵定位易混数字准确率只是个总数真正有价值的分析是看哪些数字被混在一起。混淆矩阵能告诉你9 是不是经常被认成 31 是不是偶尔被认成 7。对这个项目来说混淆矩阵的观察比准确率本身更能指导后续优化方向。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt predictions [knn_predict(X_test[i], X_train, y_train, k5) for i in range(len(X_test))] cm confusion_matrix(y_test, predictions) ConfusionMatrixDisplay(cm).plot() plt.show()跑完这四行你会立刻看到对角线上的深色带和个别散落的亮点偏离对角线的亮点就是系统性的易混数字对。针对这些数字单独挑出样本看往往能发现训练集里某个数字的书写倾斜度特别大或者某个样本的噪声斑块恰好被当成特征。6.2 k 值曲线定 k调参不该靠感觉我习惯把 k 从 1 到 20 全部跑一遍画出准确率曲线再定。曲线如果真的呈现“先升后降”顶点位置就是当前数据下的最佳 k如果曲线平坦如一条直线说明 k 对这个数据集不敏感选一个计算量更低的小 k 即可。6.3 PCA 降维与向量化提速最后一步优化是把 1024 维压到 100 维左右。PCA 在这里的本质是找到方差最大的投影方向让距离计算发生在更紧凑的空间里。常见做法是用 sklearn 的 PCA 拟合训练集然后对训练集和测试集同时做变换再跑 KNN准确率通常不会有明显下降但预测时间能缩短好几倍。如果你还想把识别做成实时摄像头版这一步几乎是必须的否则每帧都要全量算距离硬件再好也扛不住。写完这些我回头看自己第一次跑 KNN 手写数字项目时犯过的错路径写错、标签切分错位、k1 当真理、测试集反复调参——每一步单独拿出来都是小问题堆在一起却能让人在入门阶段卡上好几天。从那以后我每次拿到这类带数据集的源码都强制自己先跑通基线、再做一组 k 值对照实验、最后看一眼混淆矩阵三个步骤走完才算真正理解这个项目。这个习惯让我后续做再复杂的分类任务也没翻过车希望帮到你。本文还有配套的精品资源点击获取