简介:基于MATLAB的CNN卷积神经网络MNIST手写数字识别项目,面向深度学习入门者及MATLAB用户,解决手写数字图像多分类任务。资源采用单层卷积网络提取图像特征,配合双层全连接网络完成分类,并通过误差反向传播算法训练模型,经3轮迭代后在MNIST测试集上取得98.33%的准确率,适合教学演示、课程设计或毕业设计参考。压缩包共22个文件,大小54.81MB,其中包含7个.m源码文件(主程序、训练、评估、数据预处理及Softmax/CrossEntropy等核心层函数)、7个.mat数据文件(训练集、验证集、测试集及对应标签)、7个.png训练过程结果图,以及1个readme.txt说明文档,整体结构清晰、可直接运行。已有4484人浏览学习,资源完整覆盖从数据读取预处理、卷积网络构建、误差反向传播到精度评估的全流程,读者可对照源码理解CNN各层作用、调整参数复现高识别率,是快速上手MATLAB深度学习实践的优质参考资料。
1. 为什么MATLAB也能把手写数字识别做到98%:从MNIST到实战落地
很多人默认深度学习是 Python 的领地,PyTorch、TensorFlow 轮番上阵。但如果你手里只有 MATLAB 2021a,也想跑通手写数字识别,并且把识别率做到 98% 以上,这条路完全走得通。Deep Learning Toolbox 已经把卷积、池化、全连接、Softmax 封装成接近积木的层对象,配合二进制文件读取函数,一套 CNN 从零搭到训练完成不到 100 行代码。MNIST 是手写数字识别最经典的入门数据集,28×28 灰度图,训练集 6 万张、测试集 1 万张,正好用来验证网络结构、调参手感以及踩坑路径。下面这套流程会从数据读取、网络设计、训练调参、避坑到最终测试,完整讲清楚怎么在 MATLAB 2021a 里把识别率稳定做到 98 以上,适合刚接触深度学习的工程师,也适合想用 MATLAB 快速做算法验证的硬件方向开发者。
2. 先把数据地基打牢:MNIST的二进制格式与MATLAB读取
在动手搭网络之前,最花时间的往往不是网络本身,而是把数据弄对。MNIST 的原始文件不是一张张 PNG,而是 IDX 二进制格式,文件头里用四个 int32 描述魔数、图片数量、行数和列数,后面跟着像素数据。如果读取顺序不对,轻则图片左右颠倒,重则维度混乱,后面 trainNetwork 直接报错。
2.1 IDX文件长什么样?手写一个MATLAB读取函数
先看文件头结构。图像文件 train-images-idx3-ubyte 的开头依次是:魔数(0x00000803,即2051)、图片数量(60000)、行数(28)、列数(28)。每个字段都是 uint32 大端字节序。标签文件 train-labels-idx1-ubyte 的魔数是2049,后面直接跟 60000 个 uint8 标签。
我一般会写一个函数来处理这种格式。常见做法是用 fopen/fread 二进制读取,并指定 'ieee-be' 表示大端序。由于 fread 按列读取,后面还要做一次 permute 把行列换回来。这里的关键点在于:IDX 文件按“先列后行”存储,MATLAB 的 reshape 也是列优先,所以直接 reshape 出来的图像会是顺时针旋转过的,必须进行转置才能显示成正常方向。我见过太多人在这一步翻车,最后显示的数字是躺倒的。
function [images, labels] = loadMNISTFromIDX(imageFile, labelFile) % 读取MNIST的IDX二进制文件 % 输出 images: H*W*1*N uint8型数组,labels: N*1 uint8 imgFid = fopen(imageFile, 'rb'); magicImg = fread(imgFid, 1, 'uint32', 'ieee-be'); % 应为2051 numImgs = fread(imgFid, 1, 'uint32', 'ieee-be'); rows = fread(imgFid, 1, 'uint32', 'ieee-be'); cols = fread(imgFid, 1, 'uint32', 'ieee-be'); raw = fread(imgFid, rows * cols * numImgs, 'uint8'); fclose(imgFid); % 按列优先reshape后需要转置,使图像正立 images = reshape(raw, cols, rows, numImgs); images = permute(images, [2 1 3]); % 增加通道维,便于CNN输入:H*W*1*N images = reshape(images, rows, cols, 1, numImgs); lblFid = fopen(labelFile, 'rb'); magicLbl = fread(lblFid, 1, 'uint32', 'ieee-be'); % 应为2049 numLbl = fread(lblFid, 1, 'uint32', 'ieee-be'); labels = fread(lblFid, numLbl, 'uint8'); fclose(lblFid); end逻辑说明:这个函数把四个 IDX 文件变成 MATLAB 能直接训练的数据格式。关键参数是 uint32 配合 'ieee-be',它告诉 MATLAB 按4字节大端去读,如果漏掉 'ieee-be',读出来的是小端解析结果,魔数会变成另一个值,后续数据全乱。reshape 到 rowscols1*N 是 trainNetwork 要求的输入布局:高度、宽度、通道数、样本数。MNIST 是灰度图,所以通道数为1。
然后加载全部数据:
[XTrainAll, YTrainAll] = loadMNISTFromIDX(... 'train-images-idx3-ubyte', 'train-labels-idx1-ubyte'); [XTestAll, YTestAll] = loadMNISTFromIDX(... 't10k-images-idx3-ubyte', 't10k-labels-idx1-ubyte');参数说明:文件名要和你下载得到的完全一致,并且放到当前工作目录。这两个后缀分别是训练集和测试集,官方命名基本固定,不要改文件名。加载后 XTrainAll 是 28×28×1×60000 的 uint8 数组,YTrainAll 是 60000×1 的 uint8。这个函数以后换其他 MNIST 变体(比如时尚品类数据集)只要魔数一致也能重新用。
2.2 归一化、标签转换与乱序,顺序有讲究
很多教程会直接 trainNetwork,但如果你是第一次跑,建议严格按下面这个顺序处理。第一步是把 uint8 转成 single 并除 255,让像素落在 [0,1]。为什么要这样?卷积层内部要做乘加运算,如果输入是 0~255,第一层的激活值会整体偏大,Adam 在梯度更新时容易震荡;而且图像本质上反映的是亮度比例,绝对数值没有意义。第二步是把标签转成 categorical,这是 MATLAB 深度学习训练器的硬性要求,原始 uint8 标签会被拒绝或者被当成回归目标。
第三步是随机打乱。MNIST 原始数据是按标签从 0 到 9 排的,如果不打乱,第一个 mini-batch 可能全是某个数字,梯度方向会一边倒,训练曲线抖动非常明显。固定随机种子可以保证实验结果可复现,至少同一个数据集跑出来的最终精度能对得上。
rng(2021); % 固定随机种子,方便复现 XTrain = single(XTrainAll) / 255; % 归一化到[0,1] XTest = single(XTestAll) / 255; YTrain = categorical(YTrainAll); % 标签转分类 YTest = categorical(YTestAll); idx = randperm(size(XTrain, 4)); % 打乱训练集顺序 XTrain = XTrain(:,:,:,idx); YTrain = YTrain(idx);参数说明:size(XTrain,4) 取的是第4维,也就是样本数 60000,这里容易写错成 3,3 是通道数,会导致打乱只发生在通道维上,数据全部错位。随机种子可以任意选,2021 只是个人习惯。如果你希望每次结果略有差异,去掉 rng 那行即可。
如果不想打乱顺序对验证集有影响,可以这样划分验证集:从打乱后的尾部拿出 5000 张做验证,剩余 55000 张做训练。验证集只用来监控训练过程,不参与梯度更新。
numVal = 5000; XValidation = XTrain(:,:,:,end-numVal+1:end); YValidation = YTrain(end-numVal+1:end); XTrain = XTrain(:,:,:,1:end-numVal); YTrain = YTrain(1:end-numVal);这里的验证集本质上还是训练分布,不能代替测试集。最后用独立测试集评估识别率。
2.3 先显示几张图,别急着训练
数据读写完,我强烈建议先做视觉检查。在训练这种黑匣子模型之前,确认数据方向和灰度范围是最便宜的后悔药。代码很简单:
figure; for i = 1:12 subplot(3,4,i); imshow(XTrain(:,:,:,i)); title(char(YTrain(i))); end如果看到数字是横躺的或者标题和数字对不上,说明 loadMNIST 里的 permute 或乱序索引写错了。不要小看这一步,我曾经在这里漏掉一次 permute,结果训练精度能到 99%,但用 imshow 看所有数字都是旋转过,测试集精度直接掉到 80%。先显示 20 张图,再往下走。
3. 设计CNN网络:从LeNet-5到能上98%的MATLAB层结构
网络结构决定了精度上限,也决定了训练收敛速度。MNIST 任务简单,但不是简单到全连接网络就能搞定;需要利用图像的空间结构。
3.1 为什么卷积核结构对手写数字这么重要
手写数字识别最大的挑战是同一个数字写法千变万化:位置、角度、笔画粗细都不同。全连接网络把每个像素当成独立输入,完全不考虑像素之间的空间关系,所以需要大量参数去记忆每个位置的特征。而 CNN 用卷积核在图像上滑动,每个卷积核只覆盖一个小区域,比如 3×3。它通过感受野捕捉局部特征,比如线段方向、端点、交叉点,而且十字形或弧线无论出现在图片左上角还是右下角,同一个卷积核都能响应到,这就带来平移不变性。
以第一层为例,3×3×6 的卷积核只有 54 个权重参数;而一个 784×128 的全连接层就要 10 万个参数。CNN 靠很少的参数就能达到更高的精度,在只有几万样本的 MNIST 上不易过拟合。池化层进一步做降采样,每次把分辨率减半,增大感受野的同时保留主特征,也能让网络对笔画粗细不那么敏感。这就是 MNIST 任务最适合用 CNN 的原因。
3.2 参考网络结构:每一层的尺寸变化和参数选择
常用做法是先采用类似 LeNet-5 的经典骨架,再根据 MATLAB 的层 API 做一些调整。我这里给一个在 2021a 上稳定到 98% 以上的结构:
| 层序号 | 层类型 | 参数 | 输出尺寸 |
|---|---|---|---|
| 1 | imageInputLayer | [28 28 1], Normalization='none' | 28×28×1 |
| 2 | convolution2dLayer | 3×3×6, Padding='same' | 28×28×6 |
| 3 | reluLayer | - | 28×28×6 |
| 4 | maxPooling2dLayer | 2×2, Stride=2 | 14×14×6 |
| 5 | convolution2dLayer | 3×3×16, Padding='same' | 14×14×16 |
| 6 | reluLayer | - | 14×14×16 |
| 7 | maxPooling2dLayer | 2×2, Stride=2 | 7×7×16 |
| 8 | fullyConnectedLayer | 120 | 120 |
| 9 | fullyConnectedLayer | 10 | 10 |
| 10 | softmaxLayer | - | 10 |
| 11 | classificationLayer | - | - |
选择第一层 6 个滤波器、第二层 16 个滤波器,是参考了 LeNet-5。为什么不是一开始就选 32 或 64?因为 MNIST 的灰度笔画特征种类不多,6 个滤波器足够提取基本笔画;第一层滤波太密会在后期造成过拟合。第二层负责组合低级特征,16 个滤波器能在“表达能力”和“参数规模”之间取得平衡。如果验证精度只到 97 左右,可以尝试把 16 改成 32,但参数量会增加不少,训练时间也变长。全连接层用 120 和 10,10 对应 10 类,120 是常见配置,不必再加一层 84,除非你想压榨最后 0.1 个百分点。
输入层的 Normalization 要设置为 'none',因为我们在第 2 章已经手动归一化;如果这里不写,MATLAB 默认会做零中心化(减均值),等于把我们的 [0,1] 数据再平移一次,对 CNN 影响不大但会造成冗余计算。
3.3 用MATLAB定义网络层并做可视化
直接把这些层写成语义数组:
layers = [ imageInputLayer([28 28 1], 'Name', 'in', 'Normalization', 'none') convolution2dLayer(3, 6, 'Padding', 'same', 'Name', 'conv1') reluLayer('Name', 'relu1') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 16, 'Padding', 'same', 'Name', 'conv2') reluLayer('Name', 'relu2') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool2') fullyConnectedLayer(120, 'Name', 'fc1') fullyConnectedLayer(10, 'Name', 'fc2') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; analyzeNetwork(layers);逻辑说明:每一层的 Name 不是必须的,但建议写上,这样后面 analyzeNetwork 和报错提示能直接定位到具体层。convolution2dLayer 的 'Padding','same' 保证输出尺寸和输入一致,避免特征图不断缩小导致全连接层前展平尺寸难算。maxPooling2dLayer 这里用的是 2×2 窗口加步长 2,每过一个池化层分辨率减半,28→14→7,最终得到 7×7×16 的特征图,再送入全连接层。
在 MATLAB 2021a 中,analyzeNetwork 会弹出一张网络结构图,并且逐个检查维度是否匹配。运行它,如果出现 “layer input sizes do not match” 之类错误,一般就是卷积 padding 或者全连接输入尺寸计算错了。我第一次搭网络时漏掉了第二个卷积层的 'same',analyzeNetwork 立刻提示尺寸不匹配,省去了一次训练时才知道浪费的时间。确认网络结构无误后,保存这个 layers 变量,进入训练环节。如果中途想微调结构,直接修改卷积核数量或增加一层 reluLayer 即可,不需要动数据。
4. 训练模型:trainingOptions参数、监控曲线与调参路径
训练是玄学最多的地方。同样网络结构,不同人跑结果差很多,大多数问题出在 trainingOptions 身上。
4.1 决定精度的几个trainingOptions参数
先给出一份可以直接跑的配置,然后逐一说明为什么这么选。
options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 15, ... 'MiniBatchSize', 128, ... 'ValidationData', {XValidation, YValidation}, ... 'ValidationFrequency', 50, ... 'Shuffle', 'every-epoch', ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.6, ... 'LearnRateDropPeriod', 5, ... 'Plots', 'training-progress', ... 'Verbose', true);优化器选 'adam' 而不是 'sgdm',是因为 Adam 自带自适应学习率,对初始学习率的敏感度低,收敛速度快,适合 MNIST 这种中小规模数据。SGD with momentum 调好了精度可能略高,但需要专门调动量参数,工作量大。
'InitialLearnRate', 0.001 是 Adam 的经典起点。如果训练损失曲线一路平坦,可以试着提高到 0.002;如果震荡特别剧烈,调回 0.0005。0.01 在 MNIST 上通常太激进,曲线会像心跳一样。
'MaxEpochs', 15 表示整个训练集被完整看 15 遍。MNIST 在两层卷积加两层全连接时,15 轮基本可以把验证精度推到 98% 附近。如果使用 CPU 训练,15 轮可能需要 20 分钟左右,具体看机器。
'MiniBatchSize', 128 表示每 128 张图做一次梯度更新。128 在 6 万张训练集中是比较大众的一个选择,既不会让单次迭代太慢,也能保证梯度估计比较平滑。如果 GPU 显存小,可以降 64;如果训练难收敛,可以试 256,但会减少每个 epoch 内的更新次数。
'ValidationData' 和 'ValidationFrequency' 搭配,让训练过程中每 50 次迭代计算一次验证精度。注意这里的 50 是“迭代次数”,不是 epoch。55000/128 约 430 次迭代为一个 epoch,所以约每个 epoch 验证 8~9 次,训练曲线会显示一条验证精度点。验证太频繁会浪费时间,太稀疏则难以发现过拟合节点。
'Shuffle', 'every-epoch' 让每个 epoch 重新打乱数据,避免同样的 batch 顺序在每一轮被重复看到。'piecewise' 加 'LearnRateDropFactor' 和 'LearnRateDropPeriod' 表示每 5 个 epoch 学习率乘以 0.6,让后期在最优解附近做更细的搜索。很多人只设初始学习率,到后面就靠运气,其实这个组合拳对提升 0.3%~0.5% 很有帮助。
'Plots','training-progress' 打开实时曲线,训练时能直接看到损失和精度的走向,这是最好用的调试窗口。如果不开,训练结束后只能看到一行数字,遇到问题不好定位。
4.2 训练网络,并学会读训练进度图
配置写好后,调用 trainNetwork:
net = trainNetwork(XTrain, YTrain, layers, options);注意输入顺序:数据、标签、网络、选项。XTrain 是 H×W×1×N 的数组,YTrain 是 categorical 向量。训练时会自动使用 ValidationData 做监控。
训练进度图中有四条线:训练损失、验证损失、训练精度、验证精度。我一般这样读:
- 如果训练损失持续下降、验证损失也下降,健康,继续。
- 如果训练损失下降但验证损失在第 8 个 epoch 后开始上升,过拟合,需要早停或增强。
- 如果两条损失线都水平不动,先检查学习率和数据归一化,别急着加层。
- 如果精度曲线震荡得很厉害,把 miniBatchSize 调大一点,或者降低学习率。
如果想省时间,可以在 trainingOptions 里加一个早停参数:
'ValidationPatience', 8它的含义是:如果验证损失连续 8 次不再改善,训练提前结束。这个参数在 2021a 中可以和 ValidationData 配合使用,能避免后期空转。不过要注意,MNIST 在 15 轮内一般都能收敛,早停不是必须,但调参试跑时可以把它加上,效率高很多。
4.3 从95到98:一套可复制的调参路径
假设你跑完第一轮,验证精度只有 95%,不用慌,按下面顺序调:
第一步,确认数据没做错。回到第 2 章去显示 20 张预处理后的图片,如果图像正常,再检查 labels 是不是 categorical。95% 通常不是网络问题,是标签和图像对不上,或者归一化被跳过。
第二步,调网络宽度。在 MNIST 上,代码里把第二层卷积的 16 改成 32,重新训练,精度一般能涨 0.5~1 个百分点。如果仍然不够,再加一个 convolution2dLayer+reluLayer,放在 pool2 之后,并调整全连接输入尺寸。
第三步,调学习率。用 Adam 时,如果 loss 曲线在后期来回抖动,把 InitialLearnRate 降到 0.0005,或者把 LearnRateDropFactor 从 0.6 改成 0.5。如果前期 loss 下降太慢,把初始学习率提到 0.002,但只提这个,其他不动。
第四步,调 epoch 和 batch。epoch 从 15 加到 20,miniBatchSize 从 128 改成 64,有时小 batch 带来的噪声反而帮助跳出局部最优,精度会小幅提升。代价是训练时间变长。
第五步,如果精度在 97 附近停滞,使用数据增强(第 6 章会写),或者检查训练集和验证集是否有重叠。验证集从训练集尾部切,如果不打乱就直接切,会造成验证集只包含后半段数据,偏差很大。
训练时间优化方面,我一般会先用 1 万张子集快速调试:
XTrainDebug = XTrain(:,:,:,1:10000); YTrainDebug = YTrain(1:10000); optionsDebug = trainingOptions('adam', 'MaxEpochs', 3, 'Plots', 'none', 'Verbose', false); debugNet = trainNetwork(XTrainDebug, YTrainDebug, layers, optionsDebug);这样两三分钟就能知道网络和数据管道是否通,再全量训练。在 CPU 上跑 MNIST 不算离谱,但也要尽量避免反复试错。
5. MNIST训练避坑指南:MATLAB 2021a下的5个典型翻车现场
这一章专治“看着简单但总是莫名其妙失败”的情况。以下 5 个现象、原因和解决方式,都是我在 MATLAB 2021a 上跑 MNIST 时反复遇到的,按顺序对照自查。
5.1 现象:验证精度卡在90%左右,损失在0.3附近死活不下来
原因:百分之九十是数据没归一化。原始像素是 0~255,直接进网络,虽然能学,但梯度数值量级偏大,Adam 很容易在最优解附近来回摆。另一个可能是图像方向错了,reshape 和 permute 交换没做,数字是躺着或倒着的,CNN 能记住一部分但泛化很差。
解决:回看第 2 章,确认 XTrain = single(XTrainAll)/255;再用 imshow 随机检查几张图,看数字是否正立。如果你发现显示的是黑底白字,那是正常的;如果白底黑字,说明像素值被反相,也会影响收敛。
5.2 现象:trainNetwork直接报错,提示标签类型不对,或分类层缺失
原因:YTrain 还是 uint8 矩阵,而 trainNetwork 要求 categorical 类型;网络层末尾没有 softmaxLayer 和 classificationLayer,MATLAB 不知道这是分类任务。这两个新手错误经常同时出现。
解决:在预处理时执行 YTrain=categorical(YTrainAll);在定义 layers 时确认最后三层是 fullyConnectedLayer(10)、softmaxLayer、classificationLayer。顺序不能反,softmax 必须在 classification 之前,但两者都要有。缺少任何一个,analyzeNetwork 其实不会报错,要等训练时才炸,很恶心。
5.3 现象:训练和验证精度都在98%以上,但换到测试集只有95%
原因:过拟合,或者验证集太小、太接近训练集。MNIST 训练集 6 万张,验证集如果只有 5000 张,网络有一定机会记住一些验证样本的噪声,换到没有见过的官方测试集,识别率自然下降。
解决:不要反复用验证集调参到极限。训练结束后,用官方测试集 XTest/YTest 做一次独立评估,得到真实识别率。如果测试集确实只有 95%,说明模型过拟合,可以增加数据增强,或者把网络变得更窄更浅。另一种可能:测试集里包含了验证集分区,如果之前从 XTrainAll 里切出验证集,测试集是独立的 t10k 文件,不存在重复,这条可以跳过。
5.4 现象:训练特别慢,CPU占用率100%但一个epoch好几分钟
原因:电脑没有可用 GPU,或 MATLAB 没有识别 GPU。trainNetwork 默认 ExecutionEnvironment 是 'auto',会尝试用 GPU,但检测不到就退回 CPU。MNIST 虽然小,但 6 万张图像加两层卷积,15 轮下来可能要二三十分钟,调参期间很痛苦。
解决:先在命令行跑 version('cuda'),如果返回错误说明没有可用 GPU,此时老老实实在 options 里加 'ExecutionEnvironment','cpu',避免它反复试探。调参期间用第 4 章的子集调试法,只跑 1 万张和 3 个 epoch。如果确信要用 GPU,检查并行计算工具箱是否授权、驱动是否支持,在 MATLAB 2021a 里用 gpuDevice 查看。最后,把 MiniBatchSize 调小一点(64)也能减小单次迭代的内存峰值,但不会显著加快总时间。
5.5 现象:精度在98%附近反复横跳,加网络层反而掉到97
原因:模型容量已经接近饱和,再加层容易过拟合训练集,导致验证集、测试集反而下降。MNIST 毕竟只有 6 万张,网络再大也会在简单特征上做无用功。
解决:如果只是想冲更高,用数据增强比加深网络更有效。把第 6 章的增强方法加上,epoch 增加到 25,然后观察测试精度。如果加了增强还是不到 98,建议检查测试集是否和训练集有相同预处理;例如训练时做了归一化,测试时忘记除 255,精度也可能卡在 98 以下。还有一个容易被忽略的点:每次训练随机种子不同,结果会有 ±0.2% 浮动,如果连续两次在 98.0 上下,可以多跑几次取最好模型。
6. 把98%变成可交付:测试、可视化与进一步优化
训练结束不等于完事。识别率 98% 以上,必须用独立测试集来验证,而不是用训练时顺手算的验证精度。
6.1 在测试集上评估识别率并输出混淆矩阵
直接调用 classify 对 XTest 推理:
YPred = classify(net, XTest); acc = mean(YPred == YTest); fprintf('测试集识别率: %.2f%%\n', acc * 100); figure; confusionchart(YTest, YPred);这里的 classify 自动完成前向传播,输出 categorical 预测。mean(YPred == YTest) 就是测试集精度。想要达到标题的 98 以上,最终要看这个数字。confusionchart 会画一张 10×10 热力矩阵,对角线上的数字是正确识别数,其他地方是错误数。用这张图可以很清楚看出哪些数字互相混淆。MNIST 最常见的是 4 和 9、7 和 1、3 和 8,如果这些类偏多,说明卷积核没能捕捉到局部笔画差异。
6.2 可视化误判样本,定位模型盲点
光看混淆矩阵不够,我习惯把误判图片打出来:
misIdx = find(YPred ~= YTest); figure; for i = 1:min(9, numel(misIdx)) subplot(3,3,i); idx = misIdx(i); imshow(XTest(:,:,:,idx)); title(sprintf('真:%s 判:%s', char(YTest(idx)), char(YPred(idx)))); end这样能看到模型是在什么样的笔画下翻车的。比如某个“9”下面卷曲特别小被判成“4”,某个“1”带了个明显的钩被判成“7”。如果误判集中在某种书写偏好,就可以有针对性地做数据增强,比如对训练样本做小角度旋转或横向平移,让网络见过更多变体。
6.3 继续提升:数据增强与模型保存
当测试精度已经到 98% 但还想冲更高,最常见的手段是数据增强。MNIST 官方测试集和训练集都来自同一批人的手写,但现实场景中角度和平移更加随意。用 imageDataAugmenter 可以在训练过程中实时生成稍微旋转、平移过的图像。
augmenter = imageDataAugmenter(... 'RandRotation', [-10 10], ... 'RandXTranslation', [-2 2], ... 'RandYTranslation', [-2 2]); augds = augmentedImageDatastore([28 28], XTrain, YTrain, ... 'DataAugmentation', augmenter); options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 25, ... 'MiniBatchSize', 128, ... 'ValidationData', {XValidation, YValidation}); net2 = trainNetwork(augds, layers, options);注意 augmentedImageDatastore 的输出尺寸是 [28 28],数据源是 XTrain(已归一化);训练时每次 epoch 读取的图像都会随机做一次小变换。加了增强后,建议把 epoch 数从 15 提高到 25,因为每张图不再 100% 重复,需要更多轮数让网络充分收敛。这样跑下来,精度通常能从 98.2% 推高到 98.7% 左右,但提升幅度因机器随机性有浮动。
如果你想把这个网络接到自己的程序里,保存一个 mat 文件即可:
save('mnist_cnn.mat', 'net', 'layers');重新加载后用 classify 对任意 28×28 灰度图做推理,和测试集走同一个流程。关键是输入图像要预处理:变灰度、imresize 到 28×28、转 single、除以 255。这也是我最想强调的:模型是黑匣子,但输入管线必须是白的,直接拿摄像头拍的彩色图塞进去,识别率会掉到很难看的程度。
我自己的教训是:有一次觉得模型已经到 98.4%,就把预处理省了,直接测自己手写的图片,结果十张里错四张。回头看,不是模型不行,是输入尺寸和灰度范围没对齐。从那以后,我养成了在训练前把所有图片显示一遍的习惯,也希望这一点对你有用。希望帮到你。
本文还有配套的精品资源,点击获取