☰
KNN手写数字识别源码解析:从txt矩阵到1024维向量实战
2026/10/2 2:00:11 网站建设 项目流程

简介:这份资源面向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 那种几十兆的二进制包,而是把每个手写数字样本直接存成文本矩阵,打开就能看,改起来没有黑匣子。

KNN(K-Nearest Neighbors,K 近邻)是机器学习里少有的「没有训练阶段」的算法:把训练样本全存下来,来一个测试样本,就算它和谁最像,取最近的 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(axis=1) # 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(), key=lambda x: x[1], reverse=True) 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_ratio=0.2, k=3): """按比例切分并评估准确率""" 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:i+1], train_x, train_y, k) if pred == test_y[i]: correct += 1 return correct / len(test_y)

参数说明:test_ratio控制测试集比例,0.2 是常见起点;k逐个传入。实测下来,这份数据在 k=3 到 k=5 之间通常表现最稳,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_ratio=0.2, k=k) print(f'k={k}, 准确率={acc:.4f}')

逻辑说明:先加载全量数据,再对多个 k 值循环评估,一次性看到 K 值对结果的影响。参数说明:test_ratio=0.2表示 20% 做测试;k列表按奇数递增。跑之前确认data/里至少有每个类别若干样本,否则某个数字在测试集里出现、训练集里没有,准确率会异常低。

4.2 单样本预测与结果验证

除了批量评估,我习惯再加一个单样本预测入口,方便肉眼核对:

def predict_one(filepath, data_mat, labels, k=3): 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*i+j写成32*j+i),写反了向量顺序全乱,距离计算完全失效。这四步走完,九成问题能定位。

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_neighbors=3, algorithm='kd_tree', # 可选 'brute' / 'kd_tree' / 'ball_tree' metric='euclidean' ) clf.fit(train_x, train_y) print('准确率:', clf.score(test_x, test_y))

参数说明:algorithm='kd_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, cv=5) print('5折准确率:', scores.mean(), '标准差:', scores.std())

逻辑说明:cv=5把数据分 5 份,轮流做测试集,取平均。参数说明:scores.std()反映稳定性,标准差大说明数据分布不均或样本太少。我一般会在报告里同时给均值和标准差,比单次数字有说服力得多。

6.3 一个我踩过的坑:别在测试集上调 k

刚做这个项目时,我图省事,直接在测试集上把 k 从 3 试到 15,挑准确率最高的那个写进报告。后来被问「你这个 k 是在测试集上选的吧」,当场卡壳。正确做法是:从训练集里再切一部分做验证集,在验证集上选 k,最后才用测试集报最终成绩。虽然这份数据规模小,但流程规范与否,答辩时一眼能看出来。

从那以后我每次做 KNN 相关的作业,都强制走一遍「训练 / 验证 / 测试」三分,哪怕数据只有几十条,也把流程走全。这份源码本身已经把核心链路铺好了,你只需要在evaluate外面再包一层验证集切分,就能把「能跑」升级成「经得起问」。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询