简介:基于MATLAB实现卷积神经网络(CNN)手写数字识别的完整源码,面向机器学习入门者、计算机视觉初学者以及需要在MATLAB环境中快速搭建图像分类模型的开发者。以MNIST数据集为对象,通过一个可直接运行的m脚本串联数据导入、标签分离、像素归一化与增强、卷积层/池化层/全连接层定义、损失函数与优化器配置、训练及验证等关键环节,并附带测试集预测与准确率评估思路,可帮助读者理解CNN在图像识别中的实际工作流程。压缩包内仅含1个m脚本文件,大小2KB,内容紧凑便于逐行研读和二次修改。目前已有323人学习下载,适合作为课程设计、毕业设计或深度学习入门练手参考,也可在此基础上继续尝试dropout、早停、迁移学习等改进策略。
1. 一张 28×28 的灰度图,凭什么让 CNN 在 MATLAB 里达到 99% 准确率
手写数字识别是卷积神经网络入门绕不开的样例,MNIST 数据集里的每张图只有 28×28 像素、灰度单通道,但就是这样一个不起眼的小图,把“卷积层如何提取局部特征”“池化如何压缩空间信息”“全连接层如何做分类”这几件事讲得明明白白。很多工程师第一次接触 CNN 都在 Python 生态里完成,但在工业界和科研院所,MATLAB 依然是算法验证的高频工具,Deep Learning Toolbox 提供了从数据加载到训练再到代码生成的完整链路,不必依赖第三方框架就能跑通 LeNet-5 级别的模型。
这篇文章从零开始,用 MATLAB 实现一个能够识别 MNIST 手写数字的 CNN,覆盖离线数据集读取、网络结构设计、训练参数调整、结果验证和部署导出五个环节。标题里出现的 “CNN_MINIST_matlab.rar” 这类资源包,本质也就是把这几部分代码和训练好的网络打包分发。与其去解压别人整理好的模型,不如自己把网络搭起来,这样换数据集、改结构、调参时心里才有底。
2. MNIST 数据集与 MATLAB 环境准备:从加载本地数据到设计首个 CNN
2.1 为什么用 MATLAB 而不是直接抄 Python 版 LeNet-5
Python 生态里做 MNIST 分类有太多现成代码,PyTorch 和 TensorFlow 都内置了数据集下载接口。但实际工程里有两个常见情况:一是公司内网隔离,在线下载 MNIST 经常遇到连接失败,热搜里 “torchvision 下载 mnist 会 404” 就是这个问题的真实写照;二是信号处理、通信、控制类项目本身强依赖 MATLAB,算法同事用 MATLAB 完成预处理和仿真,识别模块最好也留在同一套工具链里。Deep Learning Toolbox 在 R2017b 之后提供了完整的 layer 定义和 trainNetwork 接口,用起来和 Python 框架的模式非常接近:先定义网络层,再喂入数据,最后看训练曲线。
2.2 用 imageDatastore 管理本地 MNIST 数据,绕开在线下载问题
数据准备是整个流程里最容易出错也最容易被忽略的一步。MNIST 官方提供的是 IDX 二进制格式,而 MATLAB 的 imageDatastore 本身不认 IDX,常见做法是先把四张 IDX 文件转换成按类别分目录存放的图片,或者直接读取别人转换好的 .mat 文件。如果手头拿到的是一个 .rar 压缩包,大概率里面已经分好了 train 和 test 的图片目录,用 imageDatastore 就能直接接管。
% 假设数据目录结构为: % data/train/0, data/train/1, ..., data/train/9 % data/test/0, data/test/1, ..., data/test/9 trainFolder = fullfile(pwd, 'data', 'train'); imdsTrain = imageDatastore(trainFolder, ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames'); testFolder = fullfile(pwd, 'data', 'test'); imdsTest = imageDatastore(testFolder, ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames');这段代码的关键在LabelSource参数,它让 datastore 自动把子目录名当作标签,数字 0 到 9 的文件夹名会被映射成 categorical 类型标签,训练时不需要手动维护一份图像路径和标签的对照表。IncludeSubfolders必须设为 true,否则 datastore 只扫描顶层目录,一张图都读不到。如果数据是以 .mat 文件存储的,可以用load读出 images 和 labels 两个变量,再手动构造 augmentedImageDatastore 或 arrayDatastore,两种方式对后续训练流程没有本质区别。
2.3 训练集与验证集划分及图像预处理
MNIST 官方训练集有 60000 张图,测试集有 10000 张图,但训练过程里最好从训练集中再切出一部分做验证,用来观察模型有没有过拟合。splitEachLabel是处理这种划分的最直接手段,它能保证每个类别被抽出的比例一致,避免随机抽样导致某个数字在验证集中偏多或偏少。
imdsTrainPart = splitEachLabel(imdsTrain, 0.9, 'randomized'); imdsVal = splitEachLabel(imdsTrain, 0.1, 'randomized');这里有个容易踩的坑:splitEachLabel会修改传入的 datastore,如果先执行第一次切分,再对原变量执行第二次,第二次切分作用的是剩下的 10% 数据,而不是重新从全量中抽样。所以上面代码里先用imdsTrain切出 90% 作为最终训练集,再对原imdsTrain切 10%,两个变量互不冲突。MATLAB 对输入图像默认不做尺寸归一化,MNIST 图片本身是 28×28 灰度图,通道数为 1,网络第一层直接写imageInputLayer([28 28 1])即可。
3. 设计 CNN 数字识别网络:从 LeNet-5 到 MATLAB 的 layer 数组
3.1 LeNet-5 结构与 MNIST 尺寸的匹配关系
LeNet-5 是 1998 年 Yann LeCun 提出的卷积网络,MNIST 就是它的成名数据集。经典结构依次是:卷积层(6 个 5×5 核)、平均池化、卷积层(16 个 5×5 核)、平均池化、三个全连接层。原始 LeNet-5 的输入是 32×32,MNIST 原图只有 28×28,所以经典做法是在进入网络前把图补零到 32×32,或者在网络第一层使用 Padding 保持空间尺寸。
网络结构越深,特征图尺寸和通道数要按规律变化。MNIST 这类简单任务不需要 ResNet 级别的深度,3 到 4 个卷积层就足以让测试准确率超过 99%。设计原则是前几层用卷积加池化逐步压缩空间分辨率、扩大通道数,最后一层全连接输出 10 个类别得分。
下面是 LeNet-5 的核心参数对照表,方便在 MATLAB 里逐个核对:
| 层名称 | 操作 | 核尺寸 / 数量 | 输出尺寸(输入 28×28 时) |
|---|---|---|---|
| 输入层 | 灰度图归一化 | 无 | 28×28×1 |
| C1 卷积 | 卷积 + ReLU | 5×5 / 6 | 28×28×6 |
| S2 池化 | 平均池化 | 2×2,步长 2 | 14×14×6 |
| C3 卷积 | 卷积 + ReLU | 5×5 / 16 | 14×14×16 |
| S4 池化 | 平均池化 | 2×2,步长 2 | 7×7×16 |
| C5 卷积 | 卷积 + ReLU | 5×5 / 120 | 3×3×120 |
| 全连接 | 展平后全连接 | 120 → 84 → 10 | 10 |
这个结构让每个数字图片经过两次降采样,最终把空间信息压缩成 120 维特征,再映射到 10 个数字类别。对 MNIST 来说,把 C3 的卷积核数量从 16 改成 32、甚至把 C1 的 6 改成 16,都能换来准确率提升,代价是训练时间变长。在 MATLAB 里验证结构最快捷的方法是打印网络结构图,用analyzeNetwork可以直接看到每一层的尺寸,比手算可靠得多。
3.2 在 MATLAB 里用 layer 数组定义卷积神经网络
Deep Learning Toolbox 的核心 API 之一是layers数组,按从输入到输出的顺序把所有层拼起来,trainNetwork会按顺序执行前向传播和反向传播。和 Python 框架相比,MATLAB 不需要写 forward 函数,配置网络更像填表。
% 定义 CNN 层数组,适配 28×28 单通道输入 layers = [ imageInputLayer([28 28 1], 'Name', 'input', 'Normalization', 'none') convolution2dLayer(3, 16, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool2') fullyConnectedLayer(84, 'Name', 'fc1') reluLayer('Name', 'relu3') fullyConnectedLayer(10, 'Name', 'fc2') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ];这个网络不是 LeNet-5 的逐层复制,而是按现代 CNN 的常见微调策略改出来的轻量变体:3×3 卷积核代替 5×5,卷积后紧跟 batchNorm,激活函数用 ReLU 代替 tanh,池化用最大池化代替平均池化。选 3×3 的原因是两层 3×3 堆叠等价于一层 5×5 的感受野,但参数量更少、非线性更强,这是如今卷积网络设计的默认选择。Padding='same'让卷积不改变特征图尺寸,池化层负责把尺寸减半,经过两个池化层后 28×28 变成 7×7,全连接层接在展平后的 784 维向量上。
参数初始化不需要手动设置,MATLAB 默认使用 He 初始化,配合 batchNorm 可以保证深层网络在前几次迭代不会梯度爆炸。如果去掉 batchNorm,学习率要保守地设为 1e-3 以下;有了它,可以放宽到 1e-2。classificationLayer是整个数组的收尾,它要求上一层的输出是类别得分加 softmax,二者缺一不可。
3.3 训练超参数如何设置:学习率、批次大小与轮数
网络定义只是骨架,超参数决定了它能不能收敛。用trainingOptions统一管理训练配置,这是 MATLAB 训练 CNN 的标准入口。
options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MiniBatchSize', 128, ... 'MaxEpochs', 8, ... 'ValidationData', imdsVal, ... 'ValidationFrequency', 30, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true);参数表如下,实际调参时按这个优先级逐个试:
| 参数 | 推荐初始值 | 作用与调整方向 |
|---|---|---|
| InitialLearnRate | 0.001 | 最优范围 1e-4 到 1e-2,过大不收敛,过小收敛慢 |
| MiniBatchSize | 128 | 显存/内存允许下可调到 256,MNIST 这种小图影响不大 |
| MaxEpochs | 8~10 | MNIST 用小网络 8 轮足够,加深结构需要更多轮 |
| Shuffle | every-epoch | 每轮打乱样本顺序,防止模型记住批次顺序 |
| ValidationFrequency | 30 | 每 30 个迭代做一次验证,太频会拖慢训练 |
| ValidationPatience | 5 | 验证准确率连续 5 次不提升则提前停止,避免过拟合 |
ValidationData复用上一节切出来的 imdsVal。MNIST 分类是相对简单的任务,如果训练到第 8 轮验证准确率还在 99% 以下,先检查数据是否有归一化问题,再考虑加深网络,不要盲目加卷积核数量。
4. 数字识别训练全流程:损失曲线、混淆矩阵与常见失败原因
4.1 训练与验证的完整调用逻辑
trainNetwork 是训练入口,传入 datastore、layers 和 options,返回训练好的网络对象 net。训练过程中Plots参数会生成动态曲线,展示训练损失和验证准确率,这是判断收敛的最直观依据。
net = trainNetwork(imdsTrainPart, layers, options);训练完成后用测试集评估,test 数据在导入时从未参与训练,只用来评估模型的泛化能力。
predLabels = classify(net, imdsTest); testAccuracy = mean(predLabels == imdsTest.Labels); fprintf('测试准确率: %.2f%%\n', testAccuracy * 100);classify返回每个样本的预测标签,与真实标签逐位比较后求平均就是准确率。对 MNIST 来说,一个设计合理的 CNN 在 10000 张测试图上的准确率应该在 98.5% 以上,改成上述带 batchNorm 的结构后通常能到 99.1% 到 99.3% 之间。如果怎么调都到不了 98%,先怀疑数据预处理,再怀疑网络结构。
4.2 用混淆矩阵定位识别错误的数字
准确率只给一个数字,看不出模型在哪类样本上犯错。混淆矩阵把 10 个类别的预测结果展开成 10×10 矩阵,行代表真实标签,列代表预测标签,对角线是正确分类数,非对角线是混淆情况。
cm = confusionchart(imdsTest.Labels, predLabels); cm.RowSummary = 'row-normalized'; cm.ColumnSummary = 'column-normalized';row-normalized让每一行的数值变成百分比,可以直观看出某个数字有多少比例被误判成了其他数字。MNIST 里最常见的混淆对是 4 和 9、7 和 2、3 和 8,原因是这些数字的局部笔画在低分辨率下形态接近。如果混淆矩阵里某一行的准确率特别低,说明网络没有抓到该数字的判别特征,可以单独抽出这批样本可视化观察,而不是盲目加层。
可视化错误样本是排查模型问题的标准手段,常见做法是把预测错误的图片和真实标签、预测标签并排画出来。
misIdx = find(predLabels ~= imdsTest.Labels); numPlot = min(length(misIdx), 12); figure; for i = 1:numPlot idx = misIdx(i); subplot(3, 4, i); img = readimage(imdsTest, idx); imshow(img); title(sprintf('真实: %s, 预测: %s', ... string(imdsTest.Labels(idx)), string(predLabels(idx)))); endreadimage按序号读取 datastore 中的图像,读取后可以正常显示和预处理。看这些错误样本时重点关注两类情况:一类是手写潦草到人眼也无法辨认的,这类错误属于数据本身歧义,模型再怎么调也救不回来;另一类是模型自信地预测错误且实际特征与预测数字明显不符,这类问题属于特征提取层的表达力不足,说明网络太浅或者卷积核数量不够,需要加深加宽而不是继续调学习率。
4.3 损失不下降和过拟合的排查路径
训练曲线是调参的第一情报来源,两条常见曲线对应两类问题。训练损失和验证准确率都不动,大概率是学习率太低或者梯度计算有问题;训练损失下降但验证准确率在某个点后停滞甚至下降,这是过拟合信号,要增加数据增强或减小模型容量。
第一个排查对象是数据格式。classify对输入图像的通道数敏感,MNIST 是灰度单通道,卷积网络输入层写成 3 通道会直接报维度错误,但即使网络能跑,灰度图被imresize成三通道时数值也不会变差,瓶颈往往在Normalization上。'Normalization', 'none'意味着图像按原始像素范围 0 到 255 进入网络,卷积输出经过 batchNorm 后数值范围被拉回合理区间,这就是为什么加 batchNorm 之后可以不用手工归一化像素值。如果去掉 batchNorm,就必须在输入层设置'Normalization', 'zerocenter'或提前对数据做均值除法,否则梯度容易出现饱和。
第二个排查对象是 Minibatch 大小和 Shuffle 策略。MiniBatchSize设得太大容易让收敛变慢,设太小会让梯度噪声变大,128 在 MNIST 上是一个稳妥值。每条数据训练前打乱顺序也重要,MATLAB 默认数据在训练前会打乱一次,但如果Shuffle设为'never',模型每个 epoch 看到的数据顺序完全一致,收敛路径会偏向固定顺序产生的梯度累积。按上面配置的'every-epoch'是最常见做法。
第三个问题是池化层和卷积核数量的平衡。MNIST 只用了两次下采样,特征图从 28×28 降到 7×7,此时仍有足够空间信息供全连接层分类。如果把第二个卷积层的 32 个核改成 64 个,参数数量翻倍,训练和推理时间明显上升,准确率提升却可能不到 0.1%,这种边际收益递减在数字识别任务里非常典型。
5. 部署与进阶:把 MATLAB 的 CNN 导出为可复用数字识别模块
训练好的net对象只是工作区里的一个变量,要做成可复用的识别模块,需要把它保存成文件。MATLAB 提供了三种典型导出方式,各有适用场景:save保存完整模型到 .mat 文件,适合在 MATLAB 环境内继续使用;exportONNXNetwork导出 ONNX 格式,方便转到 PyTorch 或 TensorRT 推理;codegen配合 GPU Coder 生成 C++ 或 CUDA 代码,适合部署到嵌入式设备和服务器。
% 保存模型,后续可直接 load 回来使用 save('mnist_cnn_net.mat', 'net'); % 导出 ONNX,供其他框架加载 exportONNXNetwork(net, 'mnist_cnn.onnx');读取 .mat 文件时直接load即可恢复完整网络结构。ONNX 导出是跨框架协作的常用路径,导出的模型包含权重和网络拓扑,其他框架用 ONNX Runtime 加载后即可推理。部署到 C++ 端时,用 GPU Coder 生成代码前要保证之前定义网络时每一层都使用'Name'参数指定了明确的层名,否则代码生成阶段会因为层名称冲突而报错。
这里给出一个部署前的推理验证技巧:用单张图片测试完整链路,确保输入尺寸和网络输入层严格一致。很多时候模型已经有了,但部署后识别率骤降,问题出在推理时忘了把图像 resize 到 28×28。MATLAB 的imresize默认用双三次插值,训练时 datastore 读入的原图是 28×28,部署时如果输入一张手机拍摄的 300×300 图片,直接丢给网络会报维度错误或得到奇怪输出。
% 单张图片识别完整链路 img = imread('digit.jpg'); if size(img, 3) == 3 img = rgb2gray(img); end img = imresize(img, [28 28]); label = classify(net, img); disp(label);classify可以接受单张图片,但要求尺寸匹配。上面这段代码补上了灰度转换和尺寸缩放,是部署前最容易遗漏的部分。对生产级识别来说,倒置、旋转、局部遮挡是真实场景中的常见变量,仅靠 MNIST 训练集很难覆盖,这也正是为什么很多数字识别项目在 MNIST 达标后,还需要用augmentedImageDatastore对训练集做随机仿射变换来增强鲁棒性——把数据增强加入训练流程的那一步,往往才是准确率从实验室水平走向可用水平的真正分水岭。
本文还有配套的精品资源,点击获取