简介这份程序包面向模式识别与机器学习初学者及课程实践者围绕手写数字识别这一经典任务提供KNN近邻、K-means聚类与BP神经网络三种方法的完整实现可用于ATM、智能终端等场景的入门练手与结课作业参考。压缩包共12个文件、约60.11MB包含3个Python脚本分别对应自写算法、调库K-means与sklearn直接识别另有MNIST二进制数据文件、老师提供的手写体数据集压缩包、作业格式参考文档及readme说明覆盖从数据读取到模型对比的完整流程。目前已有1230人学习下载。读者可借此对比三种算法在同一数据集上的效果差异理解无监督聚类与监督分类的适用边界并借助现成数据集与参考文档快速复现实验、完成课程作业或搭建自己的识别系统。1. 拆开这个手写数字识别包三种方法到底该怎么选带过模式识别课设的人多半有过这种经历老师扔过来一个压缩包里面躺着train-images.idx3-ubyte这种连后缀都看不懂的文件外加几个命名随意的.py脚本要求用三种方法做手写数字识别并对比结果。这个包就是为这种场景准备的——它把 MNIST 原始二进制文件、自写实现、调库实现和一份参考文档打包在一起核心覆盖 KNN、K-means、BP 神经网络三条技术路线。适合正在做模式识别结课作业的学生也适合想拿 MNIST 练手但不想从官网一步步下数据的从业者。下面我按「数据怎么读 → 三种方法怎么跑 → 哪里容易翻车」的顺序拆一遍。2. 先把 idx 二进制文件读明白MNIST 原始格式解析与验证很多人拿到train-images.idx3-ubyte第一反应是「这什么东西」直接跳过转头去用sklearn.datasets.load_digits里那个 8×8 的小数据集。但那个只有 1797 个样本和 MNIST 的 70000 张 28×28 图像完全不是一个量级作业要求用老师给的数据集时根本对不上。所以第一步必须把 idx 格式吃透。2.1 idx 文件头结构与字节偏移MNIST 的 idx 格式其实很简单就是「魔数 维度信息 像素数据」的平铺。图像文件和标签文件的头部结构不同读的时候偏移量要分开算。图像文件如train-images.idx3-ubyte的头部字节偏移长度含义典型值04魔数205144图像数量6000084行数28124列数2816784×N像素数据0-255标签文件如train-labels.idx1-ubyte的头部字节偏移长度含义典型值04魔数204944标签数量600008N标签数据0-9魔数 2051 和 2049 是固定值读的时候可以用来校验文件有没有拿错。所有多字节整数都是大端序big-endian这一点在 x86 机器上直接用int.from_bytes不指定字节序就会读反。2.2 用 numpy 读取并做形状校验下面这段代码是我常用的读法不依赖任何深度学习框架纯 numpy 就能跑import numpy as np import os def load_mnist_images(filepath): 读取 MNIST idx3-ubyte 图像文件返回 (N, 28, 28) 的 uint8 数组 with open(filepath, rb) as f: # 前 4 字节是魔数大端序读取 magic int.from_bytes(f.read(4), big) if magic ! 2051: raise ValueError(f魔数不对期望 2051实际 {magic}检查文件是否拿错) num_images int.from_bytes(f.read(4), big) rows int.from_bytes(f.read(4), big) cols int.from_bytes(f.read(4), big) # 剩余字节全部是像素按 uint8 读入后 reshape buf f.read(num_images * rows * cols) data np.frombuffer(buf, dtypenp.uint8) return data.reshape(num_images, rows, cols) def load_mnist_labels(filepath): 读取 MNIST idx1-ubyte 标签文件返回 (N,) 的 uint8 数组 with open(filepath, rb) as f: magic int.from_bytes(f.read(4), big) if magic ! 2049: raise ValueError(f魔数不对期望 2049实际 {magic}) num_labels int.from_bytes(f.read(4), big) buf f.read(num_labels) return np.frombuffer(buf, dtypenp.uint8) # 使用示例四个文件分别加载 train_X load_mnist_images(train-images.idx3-ubyte) train_y load_mnist_labels(train-labels.idx1-ubyte) test_X load_mnist_images(t10k-images.idx3-ubyte) test_y load_mnist_labels(t10k-labels.idx1-ubyte) print(f训练集: {train_X.shape}, 标签: {train_y.shape}) print(f测试集: {test_X.shape}, 标签: {test_y.shape}) # 预期输出: 训练集: (60000, 28, 28), 标签: (60000,) # 测试集: (10000, 28, 28), 标签: (10000,)逻辑说明int.from_bytes(f.read(4), big)负责按大端序解析头部四个整数np.frombuffer直接把字节缓冲区映射成数组比逐字节循环快几个数量级。参数方面rows和cols从文件头读出来而不是写死 28这样同一段代码也能读 Fashion-MNIST 等同样格式的数据集。2.3 读完之后先做三件事数据加载完不要急着往模型里灌先做三个检查。第一确认train_X.shape[0] train_y.shape[0]图像和标签数量必须一致不一致说明文件配对错了。第二随机抽几张图用 matplotlib 显示出来肉眼确认像素值在 0-255 之间且数字可辨认如果显示出来是全黑或全白多半是字节序读反了。第三统计标签分布MNIST 训练集每个数字大约 6000 张如果某个数字只有几百张说明文件被截断过。import matplotlib.pyplot as plt # 随机抽 5 张看看 fig, axes plt.subplots(1, 5, figsize(10, 2)) for i, ax in enumerate(axes): idx np.random.randint(0, len(train_X)) ax.imshow(train_X[idx], cmapgray) ax.set_title(f标签: {train_y[idx]}) ax.axis(off) plt.show() # 标签分布统计 unique, counts np.unique(train_y, return_countsTrue) for digit, cnt in zip(unique, counts): print(f数字 {digit}: {cnt} 张)提示如果readme.txt里写了数据集的来源或预处理方式先读一遍再动手。有些老师给的包会把图像做过归一化或二值化直接套用标准 MNIST 的参数会出问题。3. 三种方法跑通KNN、K-means、BP 神经网络的实现与参数数据读进来之后三种方法各有各的脾气。KNN 几乎不用训练但预测慢K-means 是无监督的得额外做映射BP 神经网络效果最好但调参最费时间。这一章把三种方法的可运行代码和关键参数都过一遍。3.1 KNN距离度量选择与 K 值调优KNN 的思路直白把 28×28 拉平成 784 维向量算测试样本和所有训练样本的距离取最近的 K 个投票决定类别。手写数字识别自写.py里应该就是这套逻辑。自己写的时候有两个参数要定距离度量和 K 值。import numpy as np from collections import Counter def knn_predict(train_X, train_y, test_X, k3, metriceuclidean): KNN 分类支持欧氏距离和曼哈顿距离 # 拉平为 (N, 784) train_flat train_X.reshape(len(train_X), -1).astype(np.float32) test_flat test_X.reshape(len(test_X), -1).astype(np.float32) predictions [] for i, test_sample in enumerate(test_flat): if metric euclidean: # 欧氏距离sqrt(sum((a-b)^2)) distances np.sqrt(np.sum((train_flat - test_sample) ** 2, axis1)) elif metric manhattan: # 曼哈顿距离sum(|a-b|) distances np.sum(np.abs(train_flat - test_sample), axis1) else: raise ValueError(f不支持的距离度量: {metric}) # 取距离最小的 k 个索引 k_indices np.argsort(distances)[:k] k_labels train_y[k_indices] # 投票取众数 most_common Counter(k_labels).most_common(1)[0][0] predictions.append(most_common) if (i 1) % 1000 0: print(f已预测 {i1}/{len(test_flat)}) return np.array(predictions) # 注意全量 60000 训练集跑 KNN 会非常慢 # 建议先取子集测试流程 subset 5000 pred knn_predict(train_X[:subset], train_y[:subset], test_X[:200], k3) acc np.mean(pred test_y[:200]) print(fKNN 准确率{subset} 训练样本200 测试样本: {acc:.4f})逻辑说明np.argsort(distances)[:k]拿到距离最小的 K 个索引Counter投票。参数方面K 一般取 3 或 5取 1 容易受噪声影响取太大则边界模糊。距离度量上欧氏距离对像素差异更敏感曼哈顿距离对离群点更鲁棒作业里可以两种都跑一遍对比。注意全量 60000 样本跑 KNN 预测 10000 张测试图纯 Python 循环大概要几十分钟建议先用子集验证流程或者用sklearn.neighbors.KNeighborsClassifier加速。3.2 K-means聚类数设定与标签映射K-means 是无监督的它只负责把数据分成 K 个簇不告诉你每个簇对应哪个数字。调库的kmeans手写数字识别.py用的是 sklearn 的KMeans跑完之后需要额外做一步「簇到数字的映射」。from sklearn.cluster import KMeans from scipy.stats import mode # 为了速度先取子集 n_samples 10000 X_subset train_X[:n_samples].reshape(n_samples, -1).astype(np.float32) y_subset train_y[:n_samples] # 聚类数为 10对应 0-9 kmeans KMeans(n_clusters10, initk-means, n_init10, random_state42) kmeans.fit(X_subset) # 映射对每个簇找训练集中该簇样本最多的真实标签 labels kmeans.labels_ mapping {} for cluster_id in range(10): mask labels cluster_id if mask.sum() 0: # 取该簇内出现最多的真实标签作为该簇的数字 most_common mode(y_subset[mask], keepdimsTrue)[0][0] mapping[cluster_id] most_common print(簇到数字的映射:, mapping) # 在测试集上评估 test_flat test_X[:2000].reshape(2000, -1).astype(np.float32) test_clusters kmeans.predict(test_flat) test_pred np.array([mapping.get(c, -1) for c in test_clusters]) acc np.mean(test_pred test_y[:2000]) print(fK-means 映射后准确率: {acc:.4f})逻辑说明KMeans的n_clusters10是因为有 10 个数字类别initk-means比随机初始化更稳定n_init10表示跑 10 次取最优结果避免陷入局部最优。映射这一步是 K-means 做分类的关键mode取簇内众数标签如果某个簇里标签很分散说明这个簇没有对应好某个数字准确率就会掉。参数方面random_state固定住方便复现n_init调大更稳但更慢。3.3 BP 神经网络网络结构、学习率与训练轮次BP 神经网络是三种方法里效果最好的。sklearn库直接手写数字识别.py可能用的是MLPClassifier但作业通常要求自己实现反向传播。下面是一个两层网络784-128-10的 numpy 实现核心部分import numpy as np def relu(x): return np.maximum(0, x) def relu_derivative(x): return (x 0).astype(np.float32) def softmax(x): # 减去最大值防止溢出 exp_x np.exp(x - np.max(x, axis1, keepdimsTrue)) return exp_x / np.sum(exp_x, axis1, keepdimsTrue) class BPNetwork: def __init__(self, input_size784, hidden_size128, output_size10, lr0.01): # He 初始化适合 ReLU self.W1 np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size) self.b1 np.zeros(hidden_size) self.W2 np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size) self.b2 np.zeros(output_size) self.lr lr def forward(self, X): self.z1 X self.W1 self.b1 self.a1 relu(self.z1) self.z2 self.a1 self.W2 self.b2 self.a2 softmax(self.z2) return self.a2 def backward(self, X, y_true): batch_size X.shape[0] # 输出层梯度softmax 交叉熵的导数简化为 (p - y) y_onehot np.zeros((batch_size, 10)) y_onehot[np.arange(batch_size), y_true] 1 dz2 (self.a2 - y_onehot) / batch_size dW2 self.a1.T dz2 db2 np.sum(dz2, axis0) # 隐藏层梯度 da1 dz2 self.W2.T dz1 da1 * relu_derivative(self.z1) dW1 X.T dz1 db1 np.sum(dz1, axis0) # 更新权重 self.W2 - self.lr * dW2 self.b2 - self.lr * db2 self.W1 - self.lr * dW1 self.b1 - self.lr * db1 def train(self, X, y, epochs10, batch_size64): X X.reshape(len(X), -1).astype(np.float32) / 255.0 # 归一化 for epoch in range(epochs): # 打乱顺序 indices np.random.permutation(len(X)) X_shuffled, y_shuffled X[indices], y[indices] for i in range(0, len(X), batch_size): X_batch X_shuffled[i:ibatch_size] y_batch y_shuffled[i:ibatch_size] self.forward(X_batch) self.backward(X_batch, y_batch) # 每轮报告训练准确率 pred np.argmax(self.forward(X[:2000]), axis1) acc np.mean(pred y[:2000]) print(fEpoch {epoch1}/{epochs}, 训练子集准确率: {acc:.4f}) # 训练 net BPNetwork(hidden_size128, lr0.01) net.train(train_X, train_y, epochs10, batch_size64) # 测试 test_flat test_X.reshape(len(test_X), -1).astype(np.float32) / 255.0 test_pred np.argmax(net.forward(test_flat), axis1) test_acc np.mean(test_pred test_y) print(fBP 神经网络测试准确率: {test_acc:.4f})逻辑说明forward里先线性变换再 ReLU 激活输出层用 softmax 转概率backward里输出层梯度简化为(预测概率 - 真实标签)这是 softmax 加交叉熵的标准结论。参数方面hidden_size128是常见起点调到 256 或 512 可能更好但更慢lr0.01比较稳太大容易震荡太小收敛慢batch_size64兼顾速度和稳定性。归一化除以 255 很重要不除的话梯度会爆炸。4. 避坑与排查三种方法跑不通时先看这几条这一章记录的是我在跑这个包时踩过的坑按「现象 → 原因 → 解决」写遇到问题可以逐条对照。4.1 现象KNN 跑了几十分钟还没出结果原因全量 60000 训练样本、10000 测试样本纯 Python 双重循环计算距离复杂度是 O(N×M×784)算下来几十亿次操作。解决先用 5000 训练样本和 200 测试样本验证流程确认代码没问题后再考虑用sklearn的KNeighborsClassifier底层用树结构加速或者用 numpy 的广播机制批量算距离而不是逐样本循环。4.2 现象K-means 准确率只有 30% 左右原因K-means 是无监督的聚类结果和真实标签的对应关系是随机的如果映射时某个簇的众数标签选错了整个簇的预测都会错。解决检查映射逻辑确保每个簇取的是簇内出现最多的真实标签另外可以尝试n_init20多跑几次取最优或者对图像做 PCA 降维后再聚类减少噪声干扰。4.3 现象BP 神经网络 loss 不下降或变成 NaN原因常见的有三种——学习率太大导致梯度爆炸、没有做输入归一化、权重初始化全部为零。解决先把学习率降到 0.001 试试确认输入除以了 255权重用 He 初始化或 Xavier 初始化不要用np.zeros。如果 loss 变成 NaN在 softmax 里减最大值那一步不能省。4.4 现象读 idx 文件时报「魔数不对」原因文件拿错了比如把标签文件当图像文件读或者文件下载不完整被截断。解决先确认文件名对应关系——train-images配train-labelst10k-images配t10k-labels然后用os.path.getsize检查文件大小训练图像文件应该是 47040016 字节16 60000×784对不上就是文件有问题。4.5 现象三种方法准确率差距很大不知道以哪个为准原因KNN 和 K-means 在 MNIST 上本身就不如神经网络KNN 大概 95% 左右K-means 映射后可能只有 50-70%BP 神经网络能到 97% 以上。解决作业要求对比时把三种方法的准确率、训练时间、预测时间都列出来分析差异原因——KNN 没有训练但预测慢K-means 无监督所以精度低BP 有监督且能学到非线性特征所以效果好。5. 进阶技巧用混淆矩阵和 PCA 可视化把作业做出深度如果作业只要求跑通三种方法上面几章已经够了。但想拿高分或者真正理解模型哪里出了问题可以加两个分析混淆矩阵和 PCA 降维可视化。混淆矩阵能看出模型在哪些数字上容易混。比如 4 和 9、3 和 8、1 和 7 是常见的混淆对。用 sklearn 的confusion_matrix几行就能画出来from sklearn.metrics import confusion_matrix import seaborn as sns # 以 BP 神经网络为例 cm confusion_matrix(test_y, test_pred) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(预测标签) plt.ylabel(真实标签) plt.title(BP 神经网络混淆矩阵) plt.show() # 找出混淆最多的数字对 cm_norm cm / cm.sum(axis1, keepdimsTrue) for i in range(10): for j in range(10): if i ! j and cm_norm[i][j] 0.02: print(f数字 {i} 被误判为 {j} 的比例: {cm_norm[i][j]:.2%})PCA 可视化则是把 784 维的图像降到 2 维看不同数字在空间里是否分得开。如果两个数字的簇重叠严重模型在这两个数字上就容易出错from sklearn.decomposition import PCA # 取 5000 个样本做 PCA pca PCA(n_components2) X_pca pca.fit_transform(train_X[:5000].reshape(5000, -1).astype(np.float32)) plt.figure(figsize(10, 8)) scatter plt.scatter(X_pca[:, 0], X_pca[:, 1], ctrain_y[:5000], cmaptab10, s5, alpha0.6) plt.colorbar(scatter, label数字) plt.title(MNIST PCA 降维可视化前两个主成分) plt.show() print(f前两个主成分解释方差比例: {pca.explained_variance_ratio_.sum():.2%})这两个分析加起来不到 30 行代码但能让作业从「跑通了」变成「有分析」。我一般还会把三种方法的准确率、训练时间、预测时间整理成一张对比表放在报告里一目了然。从那以后我每次拿到新的数据集包都强制先走一遍「读文件头 → 校验形状 → 可视化抽样 → 跑子集」的流程确认数据没问题再上模型。这个习惯帮我省了很多事后排查的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?