资讯动态

基于MATLAB的KNN手写数字识别:数据预处理、模型构建与调优

发布时间:2026/9/12 7:42:07 来源:尧图企业网站定制
简介一个基于MATLAB的KNN手写数字识别案例面向机器学习初学者与数字图像处理入门者帮助理解K近邻算法从原理到编码的完整落地流程。工程将150张BMP格式数字图片划分为训练集与测试集配合一个主程序M文件在170KB的压缩包内实现图像二值化特征提取、样本间欧氏距离计算、距离排序以及前K个邻居中标签频次统计的完整闭环由于自带完整图片数据无需额外收集样本即可直接运行并复现识别结果也能借此观察训练集大小对准确率的影响。包内文件命名直观图片按数字类别存放便于逐张检查样本质量并对比不同K值下的分类表现。资源包共151个文件结构精简无冗余现已有3980人学习下载适合作为课堂作业、课程设计或算法入门的便捷参考。1. 手写数字识别为什么从KNN开始在MATLAB里做手写数字识别实际工作中最让人省心的起点是KNN——它没有传统的训练阶段核心动作就两个把样本变成特征向量再按距离投票。实验室常用它当课堂作业工程组拿它当先导基线先验一下数据质量再决定要不要上深度网络。不需要专门下载第三方库MATLAB自带的fitcknn函数就能直接完成近邻搜索与分类训练集给几百张图就有可见的效果。下面把数据读取、KNN分类器构建、参数调优和验证流程逐个讲清楚适合第一次用MATLAB做模式识别的学生也适合想快速估算特征集可行性的从业者。2. 从MNIST数据到matlab特征矩阵读取、归一化与划分2.1 MNIST的idx格式读取与字节序先弄清楚手上数据长什么样。MNIST的图片文件是idx格式四个32位大端整数分别描述魔数、图片数、行数和列数后面跟着28×28784个无符号字节作为像素灰度。很多人第一步吃亏在字节序上x86机器默认小端序而MNIST文件用大端序存放直接用fread读int32得到的前几个数字会完全对不上。function [X, y] readMNIST(imgFile, labelFile) % 读取MNIST图片与标签文件返回特征矩阵和标签向量 fid fopen(imgFile, rb); fread(fid, 1, int32, 0, ieee-be); % 魔数 0x00000803 nImg fread(fid, 1, int32, 0, ieee-be); % 图片数量 nRow fread(fid, 1, int32, 0, ieee-be); % 行数 28 nCol fread(fid, 1, int32, 0, ieee-be); % 列数 28 raw fread(fid, inf, uint8); % 剩余字节是像素 fclose(fid); X reshape(raw, nRow*nCol, nImg); % 每行一张图 X double(X); fid fopen(labelFile, rb); fread(fid, 1, int32, 0, ieee-be); % 魔数 fread(fid, 1, int32, 0, ieee-be); % 标签个数 y fread(fid, inf, uint8); fclose(fid); endfread的第四个参数0表示不跳过字节第五个参数ieee-be强制按大端序解释int32这一行参数最容易漏。漏掉后读出的nImg经常变成1073741824这类天文数字第一时间检查这里。reshape按nRow×nCol顺序把像素排成列再转置成行向量fitcknn按“一行一个样本”的约定直接接收。实际调用时只需要写一行[X, y] readMNIST(train-images.idx3-ubyte, train-labels.idx1-ubyte);注意文件名后缀必须和下载的文件一致。如果数据源是自己扫描的图先用imread读入再imresize到统一尺寸后转成行向量语义相同。MNIST的28×28是固定的自定义采集数据时建议在预处理阶段统一尺寸否则特征向量的长度不一致fitcknn会直接报错。2.2 灰度归一化与统计特征拼接直接使用原始灰度有几个问题不同扫描设备对同一张纸的灰度偏移不同KNN距离计算对量纲又很敏感灰度范围差异会盖过数字形变本身的差异。常见做法是先做min-max归一化再补两个统计特征。Xm reshape(X, 28, 28, []); % 还原成28×28×N的三维数组 Xn zeros(size(X)); for i 1:size(Xm, 3) img Xm(:,:,i); % 逐张图min-max缩放加1e-6防止全黑图除零 img (img - min(img(:))) / (max(img(:)) - min(img(:)) 1e-6); Xn(i,:) img(:); end % 均值与标准差描述整张图的明暗分布 featStat [mean(Xn, 2), std(Xn, 0, 2)]; Xall [Xn, featStat];逐张归一化而不是对整个矩阵归一化目的是保留每张图自己的对比度。拼上均值和标准差后特征维度从784变成786训练预测几乎不增加时间但对整体偏亮或偏暗的样本KNN多了一个补偿依据。彩色扫描件先rgb2gray再走同一流程。深度学习模型可以通过卷积把亮度平移学到普通KNN没有这个能力预处理这步不能省。预处理步骤作用对KNN的实际影响min-max归一化像素压到0~1距离不受绝对亮度影响均值、标准差特征描述整体明暗缓解灰度整体偏移imresize统一尺寸固定特征向量长度距离语义一致2.3 用cvpartition做分层抽样划分MNIST自带训练集与测试集的划分自己采集数据时得手动切分。最容易犯的错是直接随机打乱再切类别不均衡时随机抽样可能让某个数字的测试样本过少。cvpartition按第一个输入的标签做分层留出。rng(42); cv cvpartition(y, HoldOut, 0.2); % 20%作测试 idxTest test(cv); Xtest Xall(idxTest, :); ytest y(idxTest); XtrainVal Xall(~idxTest, :); ytrainVal y(~idxTest); % 训练里再留5%当验证集专门用来选k和距离 cv2 cvpartition(ytrainVal, HoldOut, 0.05); idxVal test(cv2); Xval XtrainVal(idxVal, :); yval ytrainVal(idxVal); Xtrain XtrainVal(~idxVal, :); ytrain ytrainVal(~idxVal);cvpartition默认按类分层每个数字在训练和测试中的占比保持一致。验证集只负责选超参数测试集整个流程里只能碰一次否则拿测试集反复调参最终准确率会虚高。rng(42)固定随机种子让同事在别的机器上执行同一段划分逻辑可以得到完全相同的边界便于复现。3. 用fitcknn实现KNN手写数字分类器参数与手写版对比3.1 KNN决策本质距离加投票KNN算法把每个样本当作特征空间里的一个点预测时计算待测样本与所有训练点的距离取最近的k个邻居按类别投票。没有显式训练环节省事的同时代价是预测阶段要逐个算距离单次预测复杂度O(N×D)样本量大时明显变慢。MATLAB的fitcknn把距离计算和近邻搜索封装成统一接口调用方式和fitctree等分类器一致。mdl fitcknn(Xtrain, ytrain, ... NumNeighbors, 5, ... Distance, euclidean, ... Standardize, true); ypredVal predict(mdl, Xval); valAcc mean(ypredVal yval);NumNeighbors对应当前k值Distance可选euclidean、cityblock、cosine等默认euclideanStandardize建议设为true。像素特征和拼接的统计特征量纲不同如果不标准化数值大的维度会主导距离等于变相给某些像素加了高权重这不是我们想要的特征工程结果。3.2 手写欧氏距离版KNNfitcknn适合生产但调试期先自己写一遍能直观看到距离在哪算、票怎么投。下列代码用矩阵运算一次算出全部距离再用mink取最小的k个。function yPred knnManual(Xtr, ytr, Xte, k) % 朴素KNN逐样本计算欧氏距离并投票 nTest size(Xte, 1); yPred zeros(nTest, 1); for i 1:nTest diff Xtr - Xte(i, :); % 隐式展开得到N×D差矩阵 dist sqrt(sum(diff.^2, 2)); % 逐行平方和再开方 [~, idx] mink(dist, k); % 最小的k个距离的下标 yPred(i) mode(ytr(idx)); % 对邻居标签取众数 end end新版MATLAB里直接用Xtr减去行向量即可触发隐式展开旧版本需要改成bsxfun(minus, Xtr, Xte(i,:))。mink比sort(dist)再取前k个更省内存如果当前MATLAB版本不支持mink用[~, idx] sort(dist); idx idx(1:k);代替。mode在票数相同时返回较小的标签类别分布均匀的手写数字任务里影响不大若明显不均匀则参考3.4的距离加权。3.3 在fitcknn里接入自定义距离fitcknn支持用函数句柄覆盖内置距离。实际项目中改距离度量的需求多来自特定形变模式比如像素局部错位严重时想试试带权重的cityblock变体。myDist (x, z) sqrt(sum((x - z).^2, 2)); % 与手动版一致的欧氏距离 mdl2 fitcknn(Xtrain, ytrain, ... NumNeighbors, 5, Distance, myDist, ... Standardize, true); ypred2 predict(mdl2, Xval);距离函数第一个输入是训练矩阵第二个是单个待测样本或小批量样本返回值必须是列向量长度与第一个输入的行数相等。写加权距离时权重向量维度必须与特征列数一致。自定义距离会关闭fitcknn内部基于KDTree的搜索加速因为树的分割依赖可度量的距离性质样本量小无所谓到十万以上时耗时差异非常大量级大时不建议在自定义距离上硬撑。3.4 fitcknn的DistanceWeight与BreakTiesk的选择之外fitcknn还有两个在高精度阶段值得关注的参数——DistanceWeight和BreakTies。DistanceWeight设为inverse或squareInverse后近邻的投票权重不再一律相等k偏大时能救回边界处的精度。BreakTies决定距离完全相同时候选邻居的取舍数据集中重复样本多时不改这个参数会导致每次运行结果略有抖动。mdl3 fitcknn(Xtrain, ytrain, ... NumNeighbors, 7, ... Distance, euclidean, ... DistanceWeight, squareInverse, ... BreakTies, nearest, ... Standardize, true);fitcknn参数可选值使用建议NumNeighbors正整数从1开始每隔2试一次Distanceeuclidean/cityblock/cosine等先各跑验证集看精度再定DistanceWeightequal/inverse/squareInversek偏大时建议用加权Standardizetrue/false不同量纲特征时必须为trueBreakTiessmallest/nearest/farthest评估要可复现时固定下来DistanceWeightsquareInverse对距离很近的样本放大权重适合手写字形存在笔迹粗细差异的情况。BreakTiesnearest表示从同距离邻居里随机选一个正式评估时建议改成更明确的规则保证重复运行结果一致。这两个参数对最终精度的影响通常小于k和距离度量放在网格搜索的最后阶段调整。4. 调优KNN识别精度的三个实验k值、距离与PCA降维4.1 用循环搜索k值k是KNN唯一最重要的超参数。k1完全贴合训练数据验证集精度常常较高但测试集波动大k过大后边界被平滑连分得很开的两类也会糊在一起。一组MNIST子集上的典型结果如下。k值验证集精度euclidean标准化说明196.8%边界最锐利测试集易波动397.3%平衡点附近597.5%常见最优窗口1596.9%开始过度平滑3195.1%欠拟合明显数值随样本量和预处理细节浮动但趋势很固定k从1到5快速上升10以后回落。不要盯着某一次对比结论直接在验证集上跑一个循环kList 1:2:31; accByK zeros(size(kList)); for j 1:numel(kList) mdl fitcknn(Xtrain, ytrain, ... NumNeighbors, kList(j), ... Distance, euclidean, Standardize, true); accByK(j) mean(predict(mdl, Xval) yval); end [bestVal, bestIdx] max(accByK); bestK kList(bestIdx);bestK是在验证集上选出来的最后再用它训练一次并在测试集上报告最终精度。如果bestK落在搜索边界比如正好是31把范围再往外扩。4.2 距离度量比较欧氏距离假定特征各维度独立且同权重手写体的二维形变并不是各向同性的所以有时cityblock或cosine反而更好。cityblock比较宽容局部像素错位cosine对笔迹整体粗细不敏感。逐一把候选距离在验证集上跑一遍用表或输出行即可对比。distNames {euclidean, cityblock, chebychev, cosine}; res zeros(1, 4); for i 1:4 mdl fitcknn(Xtrain, ytrain, ... NumNeighbors, bestK, ... Distance, distNames{i}, Standardize, true); res(i) mean(predict(mdl, Xval) yval); end disp(table(distNames, res, VariableNames, {Distance, ValAcc}));在MNIST这类灰度数据上欧氏距离和cosine精度通常接近cityblock在字形边缘缺损严重时略有优势。chebychev只取最大绝对差对少数像素点过度敏感基本不推荐。距离比较实验每轮都要重建模型但KNN重建开销很小数据量不到十万条时几秒钟就能完成一轮值得每次都试一下而不是凭印象选。4.3 用PCA降维加速并消除噪声KNN预测要逐个算距离维度越少越快。784维降到40到80维通常能把预测时间缩短一个数量级精度损失经常小于1%。PCA的另一个附带收益是抹掉部分高频像素噪声某些k值下验证精度反而略升。[coeff, score, latent] pca(Xtrain); cumVar cumsum(latent) / sum(latent); kDim find(cumVar 0.99, 1); % 保留99%方差 XtrainPc score(:, 1:kDim); XvalPc (Xval - mean(Xtrain)) * coeff(:, 1:kDim);pca默认对每列做中心化score里的坐标已经带了中心化所以测试集变换时必须先减训练集的均值再乘coeff这里最容易写错。降维后再配合4.1的k搜索做一遍网格选择找到让验证集精度最高的k, dim组合。若特征里还含有均值、标准差这类统计列建议把统计列单独拿出来只对784维像素做PCA最后再拼回去避免统计信息被主成分拆散。5. 用混淆矩阵和错误样本验证KNN模型的三个技巧5.1 confusionchart定位易错数字对模型选完之后只在测试集上跑一次预测然后用混淆矩阵整体检查。ypredTest predict(mdl, Xtest); figure; cm confusionchart(ytest, ypredTest); cm.Normalization row-normalized; cm.Title Test Set Confusion Matrix;row-normalized把每一行归一化能直接看出某个数字被分到其他类别的比例。手写数字里4和9、3和8经常交叉这是字形相似决定的单纯调k和距离只能小幅缓解。混淆矩阵还能看出哪些类别样本数偏少再决定是否要补数据。5.2 把错误样本原图显示出来准确率数字掩盖了错误的具体形态把测试集上预测错误的那几张图原样画出来比看指标更能定位问题。errIdx find(ypredTest ~ ytest); figure; for i 1:min(12, numel(errIdx)) subplot(3, 4, i); imshow(reshape(Xtest(errIdx(i), 1:784), 28, 28)); title(sprintf(true:%d pred:%d, ytest(errIdx(i)), ypredTest(errIdx(i)))); endreshape时记得转置MNIST的像素按行优先存imshow则按列优先显示转置反了图像会旋转90度。如果错误样本里大部分是人也分不清的潦草写法说明误差主要由数据质量决定如果看到大量清晰但被分错的图优先检查预处理和划分过程有没有混入问题数据。5.3 用MATLAB自带digits数据集做跨数据验证MATLAB统计和机器学习工具箱内置了一份digits手写数字数据可以直接拿来检验模型是否过度拟合MNIST的特定书写风格。load digits; % 载入后先whos确认X和y的维度方向 mdlD fitcknn(double(X), y, NumNeighbors, 5, Standardize, true); cvD cvpartition(y, HoldOut, 0.2); idxD test(cvD); accD mean(predict(mdlD, X(idxD, :)) y(idxD));不同版本里digits数据的变量方向可能不一致先确认X是一行一个样本再喂给fitcknn。digits图像的像素已经做过缩放如果你的KNN在MNIST上不错、换到digits就明显下降多半是特征里包含了过度依赖MNIST灰度分布的成分。min-max归一化是否放在每张图上是排查的第一个点。本文还有配套的精品资源点击获取

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

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

免费获取报价