Python手写数字识别:基于多元线性回归的BMP图像识别实战 简介这份资源面向Python初学者、机器学习入门者以及需要完成课程设计的学生提供一套完整的手写数字识别系统实现方案。核心思路是先用Windows画图软件绘制28×28像素、黑底白字的数字图像再交由训练好的多元线性回归模型完成0~9的十分类识别帮助读者理解从图像输入到分类输出的完整流程。压缩包共16个文件约251KB包含2个Python脚本分别负责模型训练与识别测试2个CSV文件存放权重与标签数据9个BMP样本图像用于验证另附设计报告Word文档、README说明及LICENSE授权文件结构清晰、便于直接运行与二次修改。目前已有4327人学习下载说明该方案在同类课程设计中具有较高的参考价值。读者可据此掌握手写数字识别的训练与测试全流程理解多分类问题的建模思路并借助报告文档快速整理实验结论适合作为入门机器学习与数字图像处理的实践素材。1. 从一张 28×28 的 BMP 说起这套 Python 手写数字识别系统到底能跑出什么很多人第一次接触手写数字识别脑子里浮现的都是 MNIST 那套 60000 张训练图、10000 张测试图的标准流程跑完一个 softmax 出来 98% 的准确率然后就没有然后了。但这套基于 Python 实现的手写数字识别系统不太一样它的输入不是现成的数据集而是你自己在 Windows 画图软件里画出来的一张 28×28 像素 BMP 图——背景纯黑、数字纯白保存下来直接丢给模型识别。资源包里带着设计报告、训练脚本、测试脚本、热编码标签文件、训练好的权重myweight1.csv还有 1 到 9 的示例 BMP 图等于把「训练—保存权重—加载权重—识别手写图」这条链路完整摊开了。它用的是多元线性回归做多分类不是什么深度网络但恰恰因为简单反而适合课程设计、入门练手、想搞懂「模型到底怎么把像素变成数字」的人。下面我按自己拆包复现的顺序把这份资源从结构、原理到跑通、避坑讲一遍。2. 拆开压缩包文件清单、技术栈与多元线性回归的选型逻辑2.1 资源里到底有什么每个文件干什么用先把包里的东西按角色分一下不然后面跑脚本容易找不到北。核心文件其实就三类训练、测试、权重其余是报告和示例图。文件类型作用设计报告.docx文档课程设计报告含原理、流程图、结果分析手写数字的识别训练.py源码读取训练数据、训练多元线性回归、输出权重手写数字的识别测试.py源码加载权重、读取 BMP、输出预测数字train_label_hotencoding.csv数据训练标签的独热编码one-hotmyweight1.csv数据训练好的模型权重测试脚本直接加载1.bmp ~ 9.bmp数据示例手写数字图28×28黑底白字README.md文档使用说明LICENSE文档开源协议这里有个细节值得注意训练脚本和测试脚本是分开的权重单独存成 CSV。这种「训练一次、权重落盘、测试随时加载」的做法在课程设计里很常见好处是你不用每次识别都重新训练坏处是权重文件和训练数据的维度必须严格对齐否则加载就翻车。2.2 为什么用多元线性回归而不是 CNN手写数字识别本质是 10 分类问题输入是 28×28784 维像素向量输出是 0~9 十个类别。多元线性回归的做法是对每个类别学一组权重计算输入像素的加权和取分数最高的类别作为预测。用公式说就是score X W b其中 X 是 784 维输入W 是 784×10 的权重矩阵b 是 10 维偏置。这套方案在 MNIST 上大概能到 90% 左右的准确率比不上 CNN 的 99%但它有几个现实优势一是实现简单几十行 numpy 就能写完不需要 PyTorch 或 TensorFlow二是训练快CPU 上几分钟出结果三是权重可解释你能直接看到哪些像素对哪个数字贡献大。对于课程设计来说重点往往不是刷准确率而是把「数据预处理—模型训练—权重保存—推理识别」这条链路走通多元线性回归刚好够用。提示如果你后面想换成 softmax 回归或加一层隐藏层准确率会明显提升但报告里的原理部分要跟着改别只改代码不改文档。2.3 环境准备numpy、Pillow 和 Python 版本这套代码依赖很轻常见做法是装 numpy 做矩阵运算、Pillow 读 BMP 图。Python 3.8 及以上都能跑我一般用 3.8 或 3.9太新的版本偶尔会遇到 Pillow 装不上的情况。# 建议先建虚拟环境避免和系统里的包打架 python -m venv venv # Windows 激活 venv\Scripts\activate # 安装依赖 pip install numpy pillow装完之后验证一下import numpy as np from PIL import Image print(np.__version__) print(Image.__version__)如果 numpy 版本在 1.20 以上、Pillow 在 9.0 以上基本没问题。这里不涉及 cv2虽然热搜里常有人问python下载cv2但这套代码用 Pillow 读图就够了没必要多装一个 OpenCV 增加环境负担。3. 训练脚本怎么跑从 CSV 标签到权重落盘的完整链路3.1 数据从哪来标签为什么是独热编码训练脚本要解决的第一件事是「训练数据从哪来」。资源里没有单独的图片训练集只有train_label_hotencoding.csv这个标签文件说明训练图像数据要么是脚本内部生成的要么是另外准备的。常见做法是脚本里用 numpy 随机生成一批 784 维向量模拟像素或者从某个内置数据源读取。不管哪种标签的格式是明确的独热编码。独热编码的意思是数字 3 对应的标签不是3而是一个长度 10 的向量[0,0,0,1,0,0,0,0,0,0]只有第 4 位是 1。这样做的好处是模型输出可以直接和标签做逐元素运算计算损失时不用做类别映射。读取标签的代码大概长这样import numpy as np # 读取独热编码标签假设每行 10 个值 labels np.loadtxt(train_label_hotencoding.csv, delimiter,) print(labels.shape) # 应该是 (样本数, 10) print(labels[0]) # 看第一条是不是只有一个 1参数说明delimiter,是因为 CSV 用逗号分隔如果你的文件是空格分隔就改成空格。labels.shape的第一个维度是样本数第二个维度必须是 10不是 10 就说明标签文件有问题后面训练必崩。3.2 多元线性回归的训练循环与权重保存训练的核心就是不断调整 W 和 b让预测分数和真实标签的差距变小。用均方误差做损失梯度下降做优化代码结构如下import numpy as np # 假设 X 是 (N, 784) 的输入Y 是 (N, 10) 的独热标签 N, D X.shape num_classes 10 # 初始化权重和偏置 W np.random.randn(D, num_classes) * 0.01 b np.zeros(num_classes) learning_rate 0.01 epochs 500 for epoch in range(epochs): # 前向计算 scores X W b # (N, 10) # 均方误差损失 loss np.mean((scores - Y) ** 2) # 反向梯度 grad_scores 2 * (scores - Y) / N grad_W X.T grad_scores grad_b np.sum(grad_scores, axis0) # 更新参数 W - learning_rate * grad_W b - learning_rate * grad_b if epoch % 100 0: print(fepoch {epoch}, loss {loss:.4f}) # 保存权重注意 W 和 b 要一起存 np.savetxt(myweight1.csv, np.hstack([W, b.reshape(-1, 1)]), delimiter,)逻辑说明scores X W b是线性模型的前向计算loss用均方误差衡量预测和真实的差距grad_W和grad_b是损失对参数的梯度更新时用学习率控制步长。最后把 W 和 b 拼在一起存成 CSV这样测试脚本加载一个文件就能拿到全部参数。参数说明learning_rate设 0.01 比较稳太大容易震荡不收敛太小训练慢epochs设 500 是经验值你可以观察 loss 曲线如果 300 轮就不降了说明已经收敛。W初始化用0.01缩放的正态分布避免初始分数过大导致梯度爆炸。注意保存权重时np.hstack把 b 拼到 W 最后一列测试脚本加载时要按同样的顺序拆开否则维度对不上。3.3 训练完怎么验证模型没跑偏训练结束后别急着去测试脚本先在训练脚本里做一次自检拿训练数据的前几条预测一下看预测类别和真实标签是否一致。# 训练后自检 scores X W b pred np.argmax(scores, axis1) true np.argmax(Y, axis1) acc np.mean(pred true) print(f训练集准确率: {acc:.4f})如果准确率低于 0.5说明训练有问题常见原因是学习率太大、epoch 不够、或者标签和输入没对齐。这一步能帮你把问题挡在训练阶段不用等到测试脚本报错再回头查。4. 测试脚本怎么用画图 BMP 识别从像素读取到预测输出4.1 BMP 图的读取与 28×28 像素预处理测试脚本的关键是把一张 BMP 图变成模型能吃的 784 维向量。画图软件保存的 BMP 是黑底白字读取后要做几件事转灰度、缩放到 28×28、归一化、展平。from PIL import Image import numpy as np def load_bmp(path): img Image.open(path).convert(L) # 转灰度 img img.resize((28, 28)) # 统一到 28x28 arr np.array(img, dtypenp.float32) # 转 numpy arr arr / 255.0 # 归一化到 0~1 return arr.reshape(1, -1) # 展平成 (1, 784) x load_bmp(3.bmp) print(x.shape) # (1, 784)逻辑说明convert(L)把彩色图转成单通道灰度resize保证尺寸一致/255.0把像素值压到 0~1 区间和训练时的输入尺度对齐。reshape(1, -1)把 28×28 的二维数组拉成一行 784 列因为模型权重是按 784 维设计的。参数说明如果你的 BMP 是白底黑字读进来会反相需要在convert(L)后加arr 255 - arr做反转。这一步是血泪经验很多人识别结果全是错的就是因为背景色和训练时不一致。4.2 加载权重并做前向推理权重文件myweight1.csv里存的是 W 和 b 拼在一起的结果加载时要按列拆开import numpy as np def load_weights(path, num_classes10): data np.loadtxt(path, delimiter,) W data[:, :num_classes] # 前 10 列是权重 b data[:, num_classes] # 最后一列是偏置 return W, b W, b load_weights(myweight1.csv) x load_bmp(3.bmp) scores x W b pred np.argmax(scores) print(f预测数字: {pred})逻辑说明data[:, :num_classes]取前 10 列作为权重矩阵data[:, num_classes]取最后一列作为偏置。x W b做前向计算np.argmax取分数最高的类别作为预测结果。参数说明num_classes10对应 0~9 十个数字如果你的权重文件列数不是 1110 权重 1 偏置说明保存时格式不对要回去检查训练脚本。4.3 批量测试 1.bmp 到 9.bmp 看识别效果单张测试不过瘾可以把资源里的示例图都跑一遍看哪些识别对了、哪些错了for i in range(1, 10): x load_bmp(f{i}.bmp) scores x W b pred np.argmax(scores) print(f真实: {i}, 预测: {pred}, {正确 if pred i else 错误})这段代码能快速暴露问题。如果大部分都错检查预处理是否反相如果个别错可能是手写风格和训练数据差异大属于正常现象。多元线性回归对书写位置、粗细比较敏感别指望 100% 准确。5. 避坑与排查这套代码最容易翻车的五个地方5.1 现象测试脚本报维度不匹配原因权重和输入列数不一致解决检查 784 和 10报错信息通常是shapes not aligned或matmul维度错误。原因是x的列数不是 784或者W的行数不是 784。解决方法是打印x.shape和W.shape确认前者是(1, 784)、后者是(784, 10)。如果x是(1, 28, 28)说明忘了reshape如果W是(784, 11)说明加载权重时没拆列。5.2 现象识别结果全是同一个数字原因权重没训练好或输入全零解决先自检训练集准确率如果每张图都预测成 0 或某个固定值先看输入是不是全黑。BMP 读取后如果没做归一化或反相像素值可能全是 0 或 255模型输出就退化了。另一个可能是权重文件是初始随机值没经过训练。回去跑训练脚本确认 loss 在下降、训练集准确率在 0.8 以上。5.3 现象BMP 读取后尺寸不对原因画图软件保存的不是 28×28解决统一 resize 并检查背景色Windows 画图默认画布尺寸不是 28×28保存出来可能是几百像素。代码里resize((28, 28))能兜住尺寸问题但背景色兜不住。如果画的时候背景是白色读进来数字是黑的、背景是白的和训练时的黑底白字相反识别必错。解决方法是画图时先填充黑色背景或者代码里做反相。5.4 现象训练 loss 不下降原因学习率太大或标签没对齐解决调小学习率并检查标签形状loss 震荡或直接变成 nan先把learning_rate从 0.01 降到 0.001 试试。如果还不行检查Y的形状是不是(N, 10)以及每行是不是只有一个 1。标签如果没做独热编码scores - Y的广播会出错或算出错误梯度。5.5 现象权重 CSV 加载后全是字符串原因numpy 默认按文本读解决指定 dtype 或确认文件无表头np.loadtxt默认会把所有内容当浮点数读但如果 CSV 里有表头或空行会报could not convert string to float。解决方法是打开 CSV 确认没有表头或者加载时加skiprows1跳过表头。另外保存权重时用np.savetxt默认是科学计数法加载时不影响计算但如果要人工查看可以在保存时加fmt%.6f。6. 进阶玩法把权重 CSV 当黑匣子用混淆矩阵和像素热力图验证模型跑通基础流程之后我一般会做两件事来确认模型不是「碰巧对」一是画混淆矩阵看哪些数字容易混二是把权重矩阵还原成 28×28 图像看模型学到了什么。先做混淆矩阵。把 1.bmp 到 9.bmp 都跑一遍记录真实标签和预测标签用 numpy 统计import numpy as np # 假设已经收集了 true_labels 和 pred_labels true_labels np.array([1,2,3,4,5,6,7,8,9]) pred_labels np.array([...]) # 填入实际预测结果 # 手动构建 10x10 混淆矩阵 cm np.zeros((10, 10), dtypeint) for t, p in zip(true_labels, pred_labels): cm[t, p] 1 print(cm)如果某个数字经常被预测成另一个比如 4 被认成 9、3 被认成 8说明这两个类别的权重向量比较接近线性模型分不开。这时候可以在报告里分析原因比如笔画粗细、倾斜角度的影响比单纯报一个准确率有说服力。再做像素热力图。权重矩阵W是 784×10每一列对应一个数字的权重。把某一列 reshape 成 28×28就能看到模型认为哪些像素对识别这个数字最重要import numpy as np from PIL import Image W, b load_weights(myweight1.csv) for digit in range(10): w_img W[:, digit].reshape(28, 28) # 归一化到 0~255 方便保存 w_img (w_img - w_img.min()) / (w_img.max() - w_img.min()) * 255 Image.fromarray(w_img.astype(np.uint8)).save(fweight_{digit}.png)打开这些图你会发现某些数字的权重集中在特定区域比如 1 的权重可能在中间一列0 的权重在边缘一圈。这就是线性模型的可解释性——它没有卷积核但权重图能直观告诉你「它在看哪里」。如果权重图一片模糊说明训练不充分或学习率太大模型没学到有效特征。还有一个实用技巧把训练脚本里的epochs和learning_rate做成命令行参数方便反复实验。我习惯用argparse加两个参数跑不同组合看准确率变化比每次改代码再运行省事。import argparse parser argparse.ArgumentParser() parser.add_argument(--lr, typefloat, default0.01) parser.add_argument(--epochs, typeint, default500) args parser.parse_args() # 后面用 args.lr 和 args.epochs 替代硬编码这样你可以在命令行里python train.py --lr 0.001 --epochs 1000快速试参。从那以后我每次拿到这种线性模型代码都强制先跑一遍混淆矩阵和权重可视化确认模型真的学到了东西而不是靠数据泄漏或初始化运气。希望这套拆解能帮你把这份资源跑通、讲透课程设计里多拿几分。本文还有配套的精品资源点击获取