资讯动态

Matlab实现手写数字识别:KNN与BP神经网络双算法GUI对比

发布时间:2026/9/8 7:38:07 来源:尧图企业网站定制
手写数字识别这个题目在机器学习入门圈子里算是“Hello World”级别的经典了。但说实话用Matlab完整做一套还带GUI界面的程序网上资源大多东一块西一块要么只有算法脚本要么只是画了个界面但识别逻辑很鸡肋。我这次直接整合了一个完整方案在同一个GUI界面里同时实现K近邻KNN和BP神经网络两种算法一个偏传统统计方法、一个偏参数化学习模型两个算法可以实时切换对比效果。这套程序的核心价值在于它不是只能识别固定数据集的演示品而是允许你直接用鼠标在画板上写字实时看两种算法的预测结果和置信度。项目代码基于Matlab环境利用GUIDE工具构建窗口界面手工实现了图像预处理、特征提取、分类器和GUI交互整个过程不依赖Matlab内置的Classification Learner这类黑盒工具。无论你是做课程设计、毕业设计还是单纯想快速理解这两个算法在图像识别场景下的差异这套代码都可以直接改、直接跑。我尽量把关键细节、选型逻辑和踩过的坑都讲清楚很多经验是文档里不写的。1. 项目整体设计与思路拆解1.1 为什么选KNN和BP这对“冷热”组合很多人让我推荐双算法组合时第一反应是SVM加CNN。但实际落地在Matlab的GUI环境里这套组合反而不方便。SVM对参数调优敏感CNN在纯CPU环境训练太慢而且MNIST级别的精度对比很容易被算力差距掩盖。我最终选了KNN和BP神经网络核心原因有三个。第一这两种算法分别代表了机器学习的两个重要流派。KNN是典型的基于实例的懒惰学习它不显式训练模型而是把训练数据全部存下来预测时通过计算距离来投票。BP神经网络则是参数化模型的代表把知识压缩到权重矩阵里预测时只做矩阵运算。把这两个算法放在同一个界面对比可以非常直观地看到“记忆数据”和“归纳数据”的区别这对理解机器学习原理特别有帮助。第二两者的特征输入可以完全共用一套。图像预处理和特征提取管线不用改喂给KNN的是一个784维28x28像素拉直的向量喂给BP的输入层也恰好是784个神经元。共用数据流能大幅减少代码冗余也容易保证对比实验的公平性。第三Matlab对这两类算法的工程支持都很成熟。KNN的sort排序、mode投票都是基础函数BP有完整的newff或feedforwardnet工具箱不需要你从零手写反向传播。这让我能把主要精力放在GUI交互和算法对比设计上而不是深陷数学公式的debug。1.2 GUI界面布局不是把控件堆上去就完事界面设计遵循三个原则低频操作放菜单、高频操作放大按钮、结果反馈要一眼可见。主窗口包含四个核心区域画板区、算法选择区、结果显示区和训练状态区。画板区用Matlab的axes控件实现固定在窗口左侧占据一个较大的正方形区域用户直接用鼠标在坐标轴内部拖动写字。算法选择区用了两个radiobutton单选框分别对应KNN和BP模式选中即切换识别逻辑。结果显示区在右侧用静态文本控件显示预测数字同时用柱状图或text控件展示每个类别0~9的置信度分布。训练状态区放了一个axes用于绘制训练误差下降曲线还有一个进度条文本防止用户在模型训练时误以为程序卡死。整个GUI用GUIDE工具搭建生成的.fig文件和.m文件分开管理控件回调函数集中在.m文件里。这个布局在实际使用中体验比较好的地方在于用户写完一个字后视线不用来回乱扫眼睛从画板向右平移就能同时看到结果、置信度和算法类型整个过程比较流畅。我在早期版本把结果显示放在窗口底部实际测试发现用户头部频繁上下移动用久了会累后来才挪到右侧。2. 核心算法原理与Matlab实现细节2.1 KNN分类器距离度量与K值选择KNN的实现原理很简单但细节决定成败。核心思想是给定一个待识别样本在训练集中找到与它距离最近的K个样本让这K个邻居投票决定它的类别。距离度量我采用欧氏距离。之所以不用曼哈顿距离或余弦相似度是因为对MNIST这种像素值在0到1之间的灰度图欧氏距离能更均衡地反映像素级别差。每个像素的差平方后求和再开方等价于在784维空间里求直线距离对比效果直接。K值的选择是另一个关键。K太小比如K1或2模型对噪声敏感手写笔画的断点都会造成误判。K太大比如超过50会把远距离样本也纳入投票模糊分类边界。经过在MNIST测试集上的多次实验K10到15区间准确率最稳定我这套程序默认K10界面里允许手动调整。实测K10在10000个测试样本上准确率约97.5%K1会掉到不足88%差距非常大。Matlab里没有现成的KNN分类器命令我这里写了一个纯手工实现版。关键之处在于用向量化运算代替for循环避免逐样本计算距离导致速度太慢function predLabel knnClassify(trainData, trainLabel, testSample, k) % trainData: n x 784 的训练特征矩阵 % trainLabel: n x 1 的训练标签 % testSample: 1 x 784 的待识别样本 % k: 近邻个数 % 计算测试样本与所有训练样本的欧氏距离 diff trainData - testSample; distances sqrt(sum(diff.^2, 2)); % 排序获取最近的k个样本 [~, sortedIdx] sort(distances); kNearestIdx sortedIdx(1:k); kLabels trainLabel(kNearestIdx); % 投票取众数 predLabel mode(kLabels); end这个函数返回的predLabel就是KNN判断出的数字。实现上有个小陷阱trainData和testSample必须是double类型如果从GUI的image数据里直接获取可能是uint8直接相减会因为无符号整数的溢出问题产生完全错误的结果。建议统一用im2double或直接在预处理时就转成double。2.2 BP神经网络结构配置与训练参数BP神经网络的工作原理是输入信号从输入层进入经过隐含层的加权求和与非线性激活后在输出层产生预测结果再根据真实标签计算误差误差沿网络反向传播利用梯度下降法逐层调整权重和偏置。网络结构的设计需要结合手写数字识别的特性。输入层神经元数量固定为784对应28x28的图像像素每个神经元接收一个像素的灰度值。输出层10个神经元分别对应数字0到9哪个神经元的输出最大就判定为哪个数字。隐含层的神经元数量是需要权衡的太少则网络表达能力不足容易出现欠拟合太多则会过拟合训练集且训练速度显著下降。我在多组实验中测试了10、20、30、50、100个隐含层神经元最终默认设为20在准确率和训练开销之间取了平衡点。激活函数方面隐含层使用tansig双曲正切S形函数它输出范围在-1到1之间相比logsig的0到1范围梯度更大、收敛更快而且均值为0的特性有助于缓解梯度消失。输出层使用softmax函数把10个神经元的原始输出转换成和为1的概率分布这样可以直接作为“置信度”在GUI上展示。这一点很多教程都没提到他们直接用purelin线性输出作为最终结果虽然分类效果差不多但无法直观展示模型对每个数字的概率判断界面体验大打折扣。训练函数我倾向于traingdx即自适应学习率带动量的梯度下降法。相比最基础的traingd它能在训练过程中自动调整学习率平坦区域加大步长、陡峭区域减小步长同时引入动量项避免陷入局部极值。相比更快的trainlmLevenberg-Marquardttraingdx虽然在训练速度上慢一些但内存占用小对CPU环境的兼容性也更好训练过程也更稳定不太容易出现振荡。核心训练代码如下% 构建BP网络 net feedforwardnet(20); net.layers{1}.transferFcn tansig; % 隐含层激活函数 net.layers{2}.transferFcn softmax; % 输出层激活函数 net.trainFcn traingdx; % 训练函数 % 训练参数配置 net.trainParam.epochs 1000; % 最大迭代次数 net.trainParam.lr 0.01; % 初始学习率 net.trainParam.mc 0.9; % 动量因子 net.trainParam.goal 1e-5; % 目标误差 net.trainParam.min_grad 1e-6; % 最小梯度 net.trainParam.showWindow false; % 关闭训练窗口GUI内显示 % 转换为分类器模式输出softmax net train(net, trainData, trainLabelOneHot);这里有个比较关键的细节trainData的维度是784 x n即每列是一个样本这是因为Matlab神经网络工具箱要求输入矩阵的格式为“特征数 x 样本数”。而KNN的输入格式是“样本数 x 特征数”两者恰好转置。在同一个GUI里两套算法共用数据流时一定要在边界处做好转置处理避免“维度不匹配”的错误。trainLabelOneHot是标签的one-hot编码维度是10 x n每一列对应一个样本所属类别的位置为1、其余为0。这是因为输出层softmax是针对多分类问题的网络的期望输出必须是一个0/1向量不能直接用数字标签。我在工程里为用户提供训练接口点击界面上的“重新训练”按钮后程序从本地MNIST子集中抽取样本执行BP训练。整套训练在普通笔记本CPU上耗时约30秒到2分钟取决于样本量和隐含层神经元数。我建议在GUI上显示实时误差曲线配合进度文本用户在等待时能明确感知程序运行状态。2.3 图像预处理识别准确率的隐形功臣不管是KNN还是BP它们消费的都是固定维度的特征向量所以画板上手写的图像必须先经过标准化处理。处理流程按顺序分为四步。第一步是灰度化。GUI画板上的笔画颜色一般是RGB格式背景为白色前景为黑色或蓝色。直接用rgb2gray转换能把彩色画布转为单通道灰度图。第二步是二值化。这一步非常重要它把像素值压缩成0和1两个值大幅减少后续计算的干扰项。我倾向于用imbinarize配合自适应阈值法这样能根据画板局部的明暗自动确定阈值避免外来光线或阴影导致的误判。为了降低笔画内的孔洞和边缘毛刺二值化后还会执行一次形态学闭运算用imclose填充笔画断点和细小空洞。这个操作用一个3x3的全1结构元素实测能把“等笔画复杂数字”的识别率提高2到3个百分点。第三步是尺寸归一化。画板上用户写字大小随意需要统一缩放到28x28像素。直接imresize到一个28x28的小图像但有个细节原图中笔画可能只占中间一小块区域直接缩放会把小字放得形状失真。正确做法是先计算笔画连通区域的边界框切割出笔画的实际包围矩形然后用padarray填充边界保持笔画位于图像中心最后再统一缩放到28x28。这个流程能消除书写位置偏移带来的干扰。第四步是像素归一化。把28x28的图像数据从uint8的0到255映射到double的0到1。这一步是BP网络训练的硬性要求因为sigmoid和tansig这类激活函数对输入数值范围敏感。输入范围过大容易导致梯度爆炸训练发散。核心预处理代码function feat preprocessHandwritten(img) % img: 画板坐标轴截取的RGB图像 % feat: 1 x 784 的double特征向量 % 灰度化 二值化 grayImg rgb2gray(img); bwImg imbinarize(grayImg, adaptive); bwImg imclose(bwImg, strel(square, 3)); % 提取笔画边界框 [rows, cols] find(bwImg 0); % 0表示笔画像素 if isempty(rows) feat zeros(1, 784); % 空白图像 return; end top min(rows); bottom max(rows); left min(cols); right max(cols); cropImg bwImg(top:bottom, left:right); % 填充为正方形 sideLen max(size(cropImg)); paddedImg ones(sideLen, sideLen); [ch, cw] size(cropImg); rStart floor((sideLen - ch) / 2) 1; cStart floor((sideLen - cw) / 2) 1; paddedImg(rStart:rStartch-1, cStart:cStartcw-1) cropImg; % 缩放至28x28并转为double resizedImg imresize(paddedImg, [28 28]); feat double(resizedImg(:)); % 拉直为1x784 end这套预处理流程我实测能让手写板识别和MNIST测试集共享同一套特征空间两种算法都能正常识别GUI上随手的书写。如果没有这个对齐步骤即使算法再优秀识别率也很难超过80%。3. GUI界面搭建与核心交互实现3.1 用GUIDE还是App Designer我的选择Matlab里目前有两个主要的GUI搭建方式——老牌的GUIDE和新一代的App Designer。我在这个项目中选择了GUIDE原因有三个。一是代码可读性好。App Designer把控件属性和回调封装在类定义里对初学者来说抽象程度太高。GUIDE生成的是直观的.m文件和.fig文件控件回调函数就是普通函数逻辑一目了然更适合课程设计和毕业设计场景。二是它天然适合画板交互。GUIDE的axes控件可以直接绑定ButtonDownFcn和WindowButtonMotionFcn回调你能轻松捕获鼠标按下、拖动、释放事件实现自由绘图。App Designer里虽然也能做但事件传递机制相对繁琐需要额外的鼠标对象监听操作。三是兼容性。GUIDE在大多数Matlab版本上都能运行包括一些还在用老版本的实验室电脑。App Designer则要求较新的版本兼容性反而受限。当然GUIDE本身也有局限最明显的是官方对其支持力度在逐年减弱新版本中一直提示“GUIDE即将移除”。如果这个项目是长期使用可以考虑迁移到App Designer。但作为教学示范GUIDE依然是最合适的载体。3.2 手写板的鼠标响应与绘图实现手写板的核心逻辑是监听鼠标事件在用户拖动鼠标时在坐标轴上画出一条连续的轨迹。Matlab中实现思路分三个事件即鼠标按下、鼠标移动、鼠标释放。在GUIDE生成的主figure回调中设置figure1_WindowButtonDownFcn、figure1_WindowButtonMotionFcn和figure1_WindowButtonUpFcn三个回调。为了区分“鼠标是否在画板区域按下”的状态用handles.isDrawing和handles.lastPoint两个字段保存状态。绘制时不是简单地在最新点画一个圆而是连接上一次记录点和当前点画一条线段。这样能保证快速滑动鼠标时笔画依然连续不会出现断点。线段宽度用line的LineWidth属性设置默认40像素这样画出来的字笔画粗度接近真实手写笔触。绘图核心代码function figure1_WindowButtonDownFcn(hObject, eventdata, handles) % 获取鼠标在画板axes内的坐标 ax handles.axes_pad; currentPoint get(ax, CurrentPoint); x currentPoint(1,1); y currentPoint(1,2); % 判断坐标是否在画板范围内 xLim get(ax, XLim); yLim get(ax, YLim); if x xLim(1) x xLim(2) y yLim(1) y yLim(2) handles.isDrawing true; handles.lastX x; handles.lastY y; guidata(hObject, handles); end end function figure1_WindowButtonMotionFcn(hObject, eventdata, handles) if handles.isDrawing ax handles.axes_pad; currentPoint get(ax, CurrentPoint); x currentPoint(1,1); y currentPoint(1,2); % 从上一点到当前点画线段 hold(ax, on); plot(ax, [handles.lastX x], [handles.lastY y], ... k-, LineWidth, 40, LineSmoothing, on); hold(ax, off); handles.lastX x; handles.lastY y; guidata(hObject, handles); end end function figure1_WindowButtonUpFcn(hObject, eventdata, handles) handles.isDrawing false; guidata(hObject, handles); end这里有一个比较影响体验的点hold(ax, on)和hold(ax, off)不能少而且正确顺序是先hold on再plot再hold off。如果没有hold每画一条新线段会清空之前的线段轨迹画不出来。清空按钮的功能也有讲究。直接cla清空坐标轴但必须同时重置坐标轴范围。因为第一次画图时Matlab可能自动调整坐标轴范围如果不清除重置后续绘图时坐标范围可能错乱导致画的字被拉伸或压缩。稳妥做法是在cla之后显式设置xlim和ylim。function btn_clear_Callback(hObject, eventdata, handles) cla(handles.axes_pad); set(handles.axes_pad, XLim, [0 1], YLim, [0 1]); set(handles.axes_pad, XTick, [], YTick, []); set(handles.text_result, String, ); set(handles.axes_confidence, Visible, off); end3.3 识别按钮的回调流程点击“识别”按钮后程序需要按顺序完成五件事截取画板图像、预处理提特征、选择算法、执行分类、刷新界面结果。这个流程串行执行在普通CPU上总耗时在KNN模式下约100毫秒以内BP模式下约10毫秒以内。获取画板图像的方式比较特殊不能用常见的getframe或print因为这些方法依赖屏幕渲染窗口被遮挡或截屏失败时容易出错。稳定做法是直接读取坐标轴的CData属性。由于画板上的线段是用plot画的矢量图形axes没有CData解决方法是先把axes内容导出为图像再转成矩阵。导出图像用copyobj和print结合的方式或者更直接的方法是先用getframe(handles.axes_pad)获取坐标轴区域图像。getframe在代码中被反复验证过稳定性良好截取的图像尺寸和画板大小保持一致。function btn_recognize_Callback(hObject, eventdata, handles) % 1. 截取画板坐标轴图像 frame getframe(handles.axes_pad); img frame.cdata; % 2. 预处理 feat preprocessHandwritten(img); % 3. 判断空白 if sum(feat) 1 set(handles.text_result, String, 未检测到笔迹); return; end % 4. 算法选择 if get(handles.radiobutton_knn, Value) pred knnClassify(appData.trainData, appData.trainLabel, feat, appData.kValue); confidences computeKnnConfidence(appData.trainData, appData.trainLabel, feat, appData.kValue); else pred bpPredict(appData.bpNet, feat); confidences bpConfidence(appData.bpNet, feat); end % 5. 刷新结果显示 set(handles.text_result, String, [识别结果: num2str(pred)]); displayConfidenceBar(handles.axes_confidence, confidences); end需要说明的是我在上面的代码里引用了appData结构体它是一个全局存储的结构通过guidata保存和传递。训练好的BP网络、训练数据矩阵、K值等数据都放在这个结构里供所有回调函数访问。4. 完整工程结构、训练与特征数据准备4.1 工程文件组成与运行流程整个工程目录结构建议如下main_gui.figGUI窗口布局文件main_gui.mGUI主程序及所有回调函数knnClassify.mKNN分类函数trainBPNetwork.mBP网络构建和训练函数preprocessHandwritten.m图像预处理函数loadTrainingData.m加载MNIST训练和测试数据的函数mnist文件夹存放MNIST数据集的子文件夹运行流程是首先运行main_gui.m打开主界面界面打开时会自动加载一个预训练好的BP网络文件并以并行方式加载MNIST训练数据。用户可以直接在画板上写字并点击识别。如果想重新训练BP则点击“训练网络”按钮程序调用loadTrainingData加载训练集再调用trainBPNetwork执行训练训练完成后自动把新网络保存到net.mat文件。工程里对数据集的处理比较特殊。MNIST原始格式是IDX文件Matlab读起来比较费劲。为了方便复制我建议先把MNIST转存为mat格式用一个两个版本通用的脚本一次性完成。转换后训练集变量名是trainImages784 x 60000的double矩阵和trainLabels1 x 60000的double向量测试集变量名是testImages784 x 10000和testLabels1 x 10000。4.2 BP网络训练的整体流程封装训练过程我自己封装成一个独立函数trainBPNetwork.m方便GUI按钮回调调用。里面有几个比较有用的实践细节。首先是样本均衡问题。MNIST原始训练集中不同数字的样本数量不完全相等虽然差别不大但为了保证BP训练不偏向某类数字我在训练前对训练集做了二次抽样使每个数字的样本数一致。每类抽取2000个总计20000个样本。其次是训练集和验证集分离。我从20000个训练样本中再抽取10%作为验证集用于观察训练过程中的过拟合情况。一旦验证集误差在连续若干轮迭代中不再下降程序会自动提前终止训练防止过拟合。再一个细节是输入数据的标准化。虽然MNIST像素值天然在0到255之间我转成double并除以255后落在0到1区间但这个范围对tansig激活函数来说依然偏小。tansig在0附近的梯度最陡为了充分利用这一优势我把输入再做一个线性变换从0到1映射到-1到1。具体操作是inputData (inputData - 0.5) * 2。训练函数代码function net trainBPNetwork(trainImages, trainLabels) % 样本均衡抽样 numPerClass 2000; selectedIdx []; for label 0:9 classIdx find(trainLabels label); randIdx randsample(classIdx, numPerClass); selectedIdx [selectedIdx, randIdx]; end subImages trainImages(:, selectedIdx); subLabels trainLabels(selectedIdx); % 输入标准化到[-1, 1] subImages subImages / 255; subImages (subImages - 0.5) * 2; % one-hot编码 subLabelsOneHot full(ind2vec(subLabels 1)); % 标签0对应第1行 % 打乱顺序 shuffleOrder randperm(length(selectedIdx)); shuffledImages subImages(:, shuffleOrder); shuffledLabels subLabelsOneHot(:, shuffleOrder); % 划分训练集和验证集 valPercent 0.1; valNum floor(length(selectedIdx) * valPercent); valImages shuffledImages(:, 1:valNum); valLabels shuffledLabels(:, 1:valNum); trainImagesFinal shuffledImages(:, valNum1:end); trainLabelsFinal shuffledLabels(:, valNum1:end); % 构建网络 net feedforwardnet(20); net.layers{1}.transferFcn tansig; net.layers{2}.transferFcn softmax; net.trainFcn traingdx; net.trainParam.epochs 1000; net.trainParam.lr 0.01; net.trainParam.mc 0.9; net.trainParam.goal 1e-5; net.trainParam.min_grad 1e-6; net.trainParam.max_fail 20; net.trainParam.showWindow false; % 训练 net train(net, trainImagesFinal, trainLabelsFinal); % 验证集评估 outputs net(valImages); [~, predIdx] max(outputs, [], 1); predLabels predIdx - 1; trueLabelsIdx vec2ind(valLabels); trueLabels trueLabelsIdx - 1; accuracy sum(predLabels trueLabels) / length(trueLabels); disp([验证集准确率: num2str(accuracy * 100) %]); % 保存 save(net.mat, net); end4.3 KNN的特征降维处理KNN在原始784维特征上直接计算欧氏距离内存和时间的开销都不小而且高维特征存在“维度灾难”即在高维空间中距离度量的区分度会下降。为了在保持精度的同时加速KNN我引入PCA降维把特征从784维降到50维。PCA的原理是把高维数据投影到一组方差最大的正交方向主成分上。前50个主成分累积保留了原始数据90%以上的方差信息足以刻画手写数字的关键差异同时计算量降低到原来的不到10%。在工程中PCA的变换矩阵只需要在加载训练数据时计算一次然后保存到appData.pcaMat和appData.meanVec中。预测时先减均值再投影function reducedFeat pcaTransform(feat, pcaMat, meanVec) centeredFeat feat - meanVec; reducedFeat centeredFeat * pcaMat; end值得注意的是GUI手写板图像经过预处理后得到的特征与MNIST数据的特征分布非常接近用MNIST计算出的PCA变换矩阵可以直接用于GUI图像。这是整个系统能统一处理“数据集识别”和“手写板识别”的关键前提。5. 常见问题与排查技巧实录5.1 识别准确率低先检查预处理而非算法很多人训练完模型后发现准确率上不去第一反应是调整算法参数换网络结构但往往忽略了下游的特征输入质量。我调试时遇到过最典型的案例手写板图片中背景不是纯白色而是带有轻微的灰色纹理二值化阈值固定后处理效果极差识别率不到50%。换成adaptive自适应阈值后立刻恢复到90%以上。如果预处理流程没问题再检查是不是数据集和GUI手写板之间存在字体风格差异。MNIST数据集是偏手写体的英文数字如果用户用标准印刷体在画板上写字模型可能会不习惯。解决思路是增加训练样本多样性或者把GUI手写板采集的样本追加到训练集里重新训练。最后再检查算法参数。5.2 BP训练不收敛或发散训练不收敛的现象是误差曲线不下降甚至上升最终准确率接近随机水平10%。最常见的原因是学习率设置过大导致权重更新越过最优解产生震荡。traingdx的自适应学习率机制理论上能自动调节但初始lr设置太高依然会震荡。建议初始值先设0.001试训观察误差曲线的下降斜率再逐渐调高。另一个原因是输入数据没有标准化。如果直接输入原始的0到255灰阶值tansig和softmax的梯度很容易被大数值输入推入饱和区梯度消失训练停滞。必须做归一化和均值中心化。5.3 GUI运行卡顿画板笔迹不跟手画板卡顿和鼠标绘制笔迹“断线”主要有两个原因。第一个原因是每个鼠标移动事件都调用plot函数而plot的开销不低连续快速移动时事件堆积导致UI线程阻塞。解决方法是限制绘图频率比如每隔一个事件才真正执行plot或者在鼠标快速移动时用两步插值连接线段减少plot调用次数。第二个原因是训练好的BP网络数据过大回调里每次识别都要重新加载。稳妥做法是把BP网络保存在handles结构体或appData结构体中训练或加载一次后常驻内存。5.4 两种算法识别结果长期一致无法验证对比价值如果不管写什么数字KNN和BP给出的结果总是相同无法体现对比价值最可能的原因是训练数据量过少或者特征过于简单。当训练集只有几百个样本时两种算法都只能记住常见的写法遇到稍微潦草的手写体都倾向于归类到常见的模板上就会产生一致结果。建议把训练样本扩充到每类2000个以上这时KNN和BP在特定数字上的差异就会显现。比如手写“0”和“6”这种形似数字KNN更依赖局部细节的匹配而BP更能抓住整体轮廓的特征两者往往会给出不同的置信度分布。5.5 常见问题速查表现象可能原因解决方案画板绘图断线线段绘制频率低用插值连接减少plot调用识别结果全是0二值化逻辑反了检查笔画像素是否为0还是1KNN速度极慢未做PCA降维使用pcaTransform降低到50维BP训练发散学习率过大或未标准化调整lr输入映射到[-1,1]界面打开后控件空白未设置axes可见范围初始化时设置XLim和YLim按钮回调报错handles为空使用了错误对象句柄统一用handles传递数据调用guidata更新6. 我对这套方案的几点经验总结整套程序写下来我最深的体会是双算法并排展示时两个算法的预测置信度分布图是整个GUI里最有教育价值的元素。KNN的置信度分布相对离散当两个数字相似时第二高置信度可能紧咬第一高置信度这直观地暴露了KNN在度量空间上对局部特征敏感的弱点。而BP经过softmax之后置信度集中度更高通常主类别的概率会明显高于其他数字这是因为网络把判别边界学得更平滑了。这个项目后续还可以继续扩展的方向很多。比如在现有两种算法基础上加入CNN分支用Matlab的Deep Learning Toolbox构建卷积神经网络对比效果会更明显。或者把BP网络的权重以图像形式可视化能看到隐含层学到的是类似边缘检测器和笔画片段的特征这种可视化对理解深度学习非常有帮助。还有一个建议是如果你打算把这个程序用于课程设计答辩最好提前准备好一批单字截图用它们展示识别效果的稳定性避免现场手写时因鼠标控制不熟练导致识别失败。我实际测试中用鼠标写字确实比在手机上用触控笔写字要别扭一些字容易歪七扭八这种情况下识别率会从标准数据集上的95%以上掉到90%左右因此几个备用样本能帮你兜底。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价