简介这份资源面向Python初学者、机器学习入门者以及需要完成期末大作业或课程设计的学生提供一套基于KNN算法的手写数字识别完整实现方案。项目以Python编写核心识别脚本配合手写数字图像数据集帮助读者理解KNN分类思想、距离度量与投票机制并可直接运行验证识别效果。压缩包共约2000个文件以txt格式的数字图像样本为主另含1个py源码文件与1个md说明文档整体约785KB体积轻便、结构清晰便于快速部署与二次修改。源码中附有代码注释新手也能看懂适合作为课程设计、期末大作业或算法练习的参考模板。目前已有202人学习下载读者可从中获得完整的KNN手写数字识别实现思路、数据集组织方式与可复用的代码框架用于课程作业提交或进一步扩展实验。1. 拆开这份 KNN 手写数字识别源码为什么它成了期末大作业的常客如果你正在搜「KNN算法手写数字识别源码」大概率是两种情况要么课程设计 deadline 逼近要么想找一个能跑通、能看懂、能改的最小闭环项目。这份资源正好卡在这个位置上——一个KNN.py主程序加一批以「标签_序号.txt」命名的样本文件比如5_71.txt、0_24.txt、8_86.txt外加一份 README。它不依赖 MNIST 那种几十兆的二进制包而是把每个手写数字样本直接存成文本矩阵打开就能看改起来没有黑匣子。KNNK-Nearest NeighborsK 近邻是机器学习里少有的「没有训练阶段」的算法把训练样本全存下来来一个测试样本就算它和谁最像取最近的 K 个邻居投票决定类别。手写数字识别是它最经典的落地场景——每个数字是 32×32 的 0/1 矩阵展平成 1024 维向量距离一算类别就出来了。这份源码的价值不在于算法多新而在于它把「数据读取 → 向量化 → 距离计算 → 投票 → 准确率评估」整条链路摊开给你看新手能跟熟手能改。适合拿它当课程设计底座、当 KNN 入门练手或者当「我到底懂不懂 KNN」的自测题。2. 数据长什么样从 txt 矩阵到 1024 维向量的转换逻辑2.1 样本文件的命名规则与内容结构先别急着跑代码把数据看懂比什么都重要。这批样本文件名格式统一是标签_编号.txt比如5_71.txt表示这个文件是数字 5 的第 71 个样本0_24.txt是数字 0 的第 24 个样本。文件名本身就是标签这是这份资源最省事的地方——不用额外维护一张 label 映射表读文件名就能拿到 ground truth。打开任意一个 txt你会看到 32 行、每行 32 个字符字符只有 0 和 1。1 代表笔画经过的位置0 代表空白。这就是经典的 32×32 二值化手写数字表示法和《机器学习实战》里用的格式一致。下面是我从5_71.txt里截的前几行感受一下00000000000001111000000000000000 00000000000011111110000000000000 00000000000111111111000000000000 00000000001111111111100000000000 00000000011111111111110000000000这种纯文本格式的好处是零依赖、可肉眼检查、方便手动造样本代价是文件数量一多磁盘占用和 IO 次数会上去。做课程设计够用真上生产得换二进制或直接上框架自带的数据集加载器。2.2 把 32×32 矩阵展平成特征向量KNN 算的是向量距离所以第一步必须把二维矩阵压成一维。常见做法是逐行拼接第 1 行 32 个字符接第 2 行 32 个字符一直接到第 32 行得到一个长度 1024 的列表。下面这段是我一般会写的读取函数带完整注释import numpy as np import os def img2vector(filename): 把 32x32 的 txt 矩阵转成 1x1024 的 numpy 向量 return_vect np.zeros((1, 1024)) # 预分配避免反复 append with open(filename) as f: for i in range(32): # 固定 32 行 line_str f.readline() for j in range(32): # 每行固定 32 列 if line_str[j] 1: return_vect[0, 32 * i j] 1 # 行优先展平 return return_vect逻辑说明32 * i j是行优先展平的标准索引公式保证同一位置的像素在向量里位置固定。参数说明filename传完整路径返回 shape 为(1, 1024)后面做距离计算时可以直接广播。注意这里用np.zeros预分配而不是list.append样本一多差距就出来了——append 每次可能触发扩容1024 维乘以上千样本累积开销不小。2.3 标签提取与数据集组织标签直接从文件名切出来。5_71.txt按_分割取第一段就是5。我一般会写一个load_dataset把目录下所有 txt 扫一遍返回特征矩阵和标签向量def load_dataset(folder): 扫描目录下所有 标签_编号.txt返回 (特征矩阵, 标签列表) files [f for f in os.listdir(folder) if f.endswith(.txt)] m len(files) data_mat np.zeros((m, 1024)) labels [] for idx, fname in enumerate(files): label int(fname.split(_)[0]) # 文件名前缀即标签 data_mat[idx, :] img2vector(os.path.join(folder, fname)) labels.append(label) return data_mat, labels这里有个容易翻车的点os.listdir返回顺序不保证稳定如果你后面要做「训练集/测试集按比例切分」必须先sorted()再切否则每次跑出来的划分都不一样准确率会飘。我一般会在files那行加sorted()让结果可复现。3. KNN 分类器实现距离度量、K 值选择与投票机制3.1 欧氏距离还是曼哈顿距离KNN 的核心就一句话算距离、取邻居、投票。距离度量最常用欧氏距离公式是各维度差值的平方和开根号。对 1024 维的二值向量欧氏距离计算量是 1024 次减法、1024 次平方、1024 次加法再开一次根号。下面是我常用的向量化实现def classify(in_x, data_set, labels, k): KNN 分类in_x 是 1x1024 测试向量data_set 是 m x 1024 训练矩阵 # 广播做差一次性算出所有训练样本的距离平方 diff data_set - in_x # shape: (m, 1024) sq_dist (diff ** 2).sum(axis1) # shape: (m,) dist np.sqrt(sq_dist) # argsort 取距离最小的 k 个索引 sorted_idx dist.argsort() class_count {} for i in range(k): vote labels[sorted_idx[i]] class_count[vote] class_count.get(vote, 0) 1 # 按票数降序取第一名 sorted_count sorted(class_count.items(), keylambda x: x[1], reverseTrue) return sorted_count[0][0]逻辑说明data_set - in_x利用 numpy 广播把(m,1024)和(1,1024)直接相减避免写双重循环。参数说明k是邻居数量一般取奇数避免平票labels顺序必须和data_set行顺序严格对应否则投票全乱。如果维度再高、样本再多欧氏距离的开根号可以省掉——比较大小只看平方和开根号是单调变换不影响排序能省一点算力。3.2 K 值怎么选从 3 到 15 的实测区间K 值是 KNN 唯一的超参数也是最玄学的地方。K 太小模型对噪声敏感一个错标样本就能带偏结果K 太大决策边界被抹平邻近类别的样本被稀释。我在这份数据上一般从 3 开始试步长 2试到 15。下面是我常用的评估脚本def evaluate(data_mat, labels, test_ratio0.2, k3): 按比例切分并评估准确率 m len(labels) split int(m * (1 - test_ratio)) # 注意这里假设数据已按类别打散否则要先 shuffle train_x, train_y data_mat[:split], labels[:split] test_x, test_y data_mat[split:], labels[split:] correct 0 for i in range(len(test_y)): pred classify(test_x[i:i1], train_x, train_y, k) if pred test_y[i]: correct 1 return correct / len(test_y)参数说明test_ratio控制测试集比例0.2 是常见起点k逐个传入。实测下来这份数据在 k3 到 k5 之间通常表现最稳k 超过 10 后准确率开始下滑。注意切分前一定要 shuffle否则如果文件是按类别顺序排列的训练集里可能根本没有某个数字测试时必错。3.3 投票平票的处理当 k 取偶数或者多个类别票数相同时简单取sorted_count[0]会偏向字典序靠前的类别这是个隐蔽的坑。稳妥做法是 k 强制取奇数如果业务上必须用偶数可以加一层「距离加权投票」——离得近的邻居票权更大权重取距离倒数# 距离加权投票片段 weight 1.0 / (dist[sorted_idx[i]] 1e-9) # 加极小值防除零 class_count[vote] class_count.get(vote, 0) weight这样即使平票距离更近的类别也会胜出比随机或按字典序合理得多。代价是每次投票多一次除法样本量大时能感知到但课程设计规模完全无感。4. 跑通全流程从目录结构到准确率输出4.1 目录组织与入口脚本拿到资源后我建议先按下面结构摆好避免路径问题project/ ├── KNN.py ├── README.md └── data/ ├── 0_24.txt ├── 1_76.txt ├── 3_47.txt ├── 5_15.txt ├── 5_23.txt ├── 5_41.txt ├── 5_71.txt └── 8_86.txt把所有标签_编号.txt统一放进data/KNN.py里用相对路径引用。这样换机器、换目录都不用改代码。入口脚本大致长这样if __name__ __main__: data_mat, labels load_dataset(data) print(样本总数:, len(labels)) for k in [3, 5, 7, 9]: acc evaluate(data_mat, labels, test_ratio0.2, kk) print(fk{k}, 准确率{acc:.4f})逻辑说明先加载全量数据再对多个 k 值循环评估一次性看到 K 值对结果的影响。参数说明test_ratio0.2表示 20% 做测试k列表按奇数递增。跑之前确认data/里至少有每个类别若干样本否则某个数字在测试集里出现、训练集里没有准确率会异常低。4.2 单样本预测与结果验证除了批量评估我习惯再加一个单样本预测入口方便肉眼核对def predict_one(filepath, data_mat, labels, k3): vec img2vector(filepath) pred classify(vec, data_mat, labels, k) true_label int(os.path.basename(filepath).split(_)[0]) print(f文件: {filepath} | 真实: {true_label} | 预测: {pred}) return pred true_label这个函数的价值在于当整体准确率不对劲时你能立刻定位是哪个样本被分错了再回去看它的矩阵是不是笔画太淡、或者标签文件名写错了。批量指标看趋势单样本看个案两个一起用排错最快。4.3 准确率不达预期时的排查顺序如果跑出来准确率明显偏低比如低于 0.7按这个顺序查第一确认labels和data_mat行顺序是否严格对应错位是最常见的低级错误第二确认切分前有没有 shuffle没 shuffle 会导致训练集类别不全第三打印几个测试样本的预测结果看是不是集中在某几个类别上错如果是多半是那几类样本量太少第四检查img2vector的索引公式有没有写反32*ij写成32*ji写反了向量顺序全乱距离计算完全失效。这四步走完九成问题能定位。5. 避坑与常见问题那些让准确率莫名掉点的细节5.1 现象准确率每次跑都不一样原因os.listdir返回顺序不稳定加上没有固定随机种子切分结果每次不同。解决在load_dataset里对文件名sorted()并在切分前用np.random.seed(42)固定随机状态。这样同一份数据、同一组参数结果可复现写报告时也拿得出手。5.2 现象某个数字永远识别不对原因该数字的训练样本太少或者样本本身质量差笔画断裂、偏移严重。解决先统计每个类别的样本数样本数明显偏少的类别补样本再单独把该类别的 txt 打开看确认 1 的分布是否合理。KNN 对样本均衡度比很多算法都敏感因为它完全依赖邻居。5.3 现象k 增大后准确率先升后降原因这是 KNN 的正常行为不是 bug。k 太小时噪声主导k 太大时远处不相关样本被拉进投票。解决把 k 从 3 到 15 全跑一遍画一条准确率曲线取峰值附近的奇数 k。别迷信某个固定值不同数据分布最优 k 不同。5.4 现象距离计算报维度不匹配原因img2vector返回的是(1,1024)而data_set是(m,1024)广播本身没问题但如果某个 txt 行数不是 32读出来向量长度就不是 1024相减直接报错。解决在img2vector里加一行校验assert len(return_vect[0]) 1024或者读文件时判断行数提前把坏样本挑出来。5.5 现象预测结果全是同一个数字原因多半是labels传参时被覆盖成了单一值或者classify里labels和data_set来自不同次加载行数对不上导致索引越界后被异常吞掉。解决在classify开头加assert len(labels) data_set.shape[0]让问题在入口就暴露而不是等到投票阶段出诡异结果。6. 进阶技巧把 KNN 从「能跑」推到「能交差」6.1 用 KD-Tree 加速近邻搜索样本量小的时候暴力算全量距离没问题样本上千后每次预测都要遍历全部训练样本评估一轮就慢下来了。常见做法是上 KD-Tree 或 Ball-Tree把近邻搜索从 O(m) 降到接近 O(log m)。sklearn的KNeighborsClassifier内置了这两种结构切换只要改一个参数from sklearn.neighbors import KNeighborsClassifier clf KNeighborsClassifier( n_neighbors3, algorithmkd_tree, # 可选 brute / kd_tree / ball_tree metriceuclidean ) clf.fit(train_x, train_y) print(准确率:, clf.score(test_x, test_y))参数说明algorithmkd_tree在维度不太高1024 维其实已经偏高时有效维度再高 KD-Tree 会退化反而不如暴力。这份数据 1024 维KD-Tree 优势有限但作为课程设计里「我知道有更优结构」的加分点写进报告很合适。6.2 交叉验证替代单次切分单次 80/20 切分的准确率波动大写报告时容易被质疑。换成 5 折交叉验证结果更稳from sklearn.model_selection import cross_val_score scores cross_val_score(clf, data_mat, labels, cv5) print(5折准确率:, scores.mean(), 标准差:, scores.std())逻辑说明cv5把数据分 5 份轮流做测试集取平均。参数说明scores.std()反映稳定性标准差大说明数据分布不均或样本太少。我一般会在报告里同时给均值和标准差比单次数字有说服力得多。6.3 一个我踩过的坑别在测试集上调 k刚做这个项目时我图省事直接在测试集上把 k 从 3 试到 15挑准确率最高的那个写进报告。后来被问「你这个 k 是在测试集上选的吧」当场卡壳。正确做法是从训练集里再切一部分做验证集在验证集上选 k最后才用测试集报最终成绩。虽然这份数据规模小但流程规范与否答辩时一眼能看出来。从那以后我每次做 KNN 相关的作业都强制走一遍「训练 / 验证 / 测试」三分哪怕数据只有几十条也把流程走全。这份源码本身已经把核心链路铺好了你只需要在evaluate外面再包一层验证集切分就能把「能跑」升级成「经得起问」。希望帮到你。本文还有配套的精品资源点击获取