news 2026/10/22 6:21:21

Matlab中ResNet18迁移学习MNIST手写数字识别完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Matlab中ResNet18迁移学习MNIST手写数字识别完整实践

MNIST手写数字数据集几乎是每个接触深度学习的人绕不开的入门关卡,按理说用它跑个LeNet或者两三层的卷积网络就能拿到99%以上的准确率,为什么还要上ResNet18这种为224×224大图设计的深层残差网络?坦白说,我刚开始拿到这个题目时也觉得有点用力过猛。但真正把整个流程走下来才发现,这个组合其实很适合用来验证迁移学习、网络结构适配、训练策略调整这一整套方法论。尤其是当你打算用Matlab而不是Python框架来做深度学习仿真时,整个过程会碰到很多“文档里没写但在实操中必然出现”的细节。

这篇文章就从一个实际跑通的Matlab仿真实验出发,把ResNet18在MNIST上的完整流程拆开讲讲:数据怎么装、网络怎么改、参数怎么调、结果怎么判断、报错怎么排。无论你是做课程设计、毕业课题,还是单纯想把深度学习这一套工具链在Matlab里捋清楚,这篇都能给你一个可以直接复现的路线图。

1. 为什么要在MNIST上跑ResNet18,这个实验的真实价值在哪里

先回答一个最直接的问题:MNIST一共就60000张训练图、10000张测试图,每张还是28×28的灰度小图,传统卷积网络几轮训练就能刷到99%以上,为什么非要用ResNet18?

我的看法是,这个实验的真正价值不在于“把MNIST准确率刷得多高”,而在于搞清楚三件事:第一,一个为ImageNet这种大尺寸彩色图设计的预训练模型,能不能通过迁移学习适配到完全不同的数据分布上;第二,在Matlab的深度学习工具箱里,输入尺寸、通道数、分类头这些网络结构层面的约束到底是怎么影响训练的;第三,深层网络的“深”在小数据集上到底是优势还是负担,这需要实测数据而不是拍脑袋。

MNIST作为实验平台有一个天然好处:数据量小、迭代速度快。用ResNet18在224×224输入上做迁移学习,单机GPU跑6个epoch大概也就是几十分钟的事。这意味着你可以大胆地去试不同学习率、不同优化器、不同数据增强方案,而不是像训练ImageNet那样一次实验要几天。所以这个组合并不是“杀鸡用牛刀”,而是一个低成本、高可解释性的深度学习实验平台。

对做课程设计或者毕设的同学来说,这个课题还覆盖了图像分类的全部标准环节:数据加载、网络搭建、模型训练、性能评估、错误分析。用这一套流程训练出来的结果天然带“可可视化”的属性,混淆矩阵、误判样本都是现成的分析素材,写论文或者做汇报的时候非常有东西可以讲。对已经会用Python的人,这篇也能帮你快速了解Matlab在深度学习流程里的语法特点和思维差异。

2. 数据准备:MNIST在Matlab里的三种装载方式

MNIST数据的原始格式是IDX二进制文件,不是图片文件夹。Matlab里想要拿到能直接训练的数据,常见路子有三条。

2.1 直接用工具箱自带的示例函数加载

Deep Learning Toolbox在多数发行版里都带了一组示例数据函数,其中digitTrain4DArrayData和digitTest4DArrayData可以直接返回已经整理好的四维数组。用法很简单:

[XTrain, YTrain] = digitTrain4DArrayData; [XTest, YTest] = digitTest4DArrayData; % 查看维度 disp(size(XTrain)); % 28×28×1×60000

这里的四维数组格式是 H×W×C×N,也就是高度、宽度、通道数、样本数,跟Python里常见的 N×C×H×W 顺序完全不一样,刚切换过来的人第一件事就是在这里犯迷糊。YTrain是60000×1的标签数组,但trainNetwork要求标签必须是categorical类型,所以还需要转一下:

YTrain = categorical(YTrain); YTest = categorical(YTest);

如果用的版本里没带这两个函数,也不慌,走下面第二种方式。

2.2 自己解析官方IDX二进制文件

从官网下载的train-images-idx3-ubyte.gz和train-labels-idx1-ubyte.gz需要先解压,然后用下面这个函数读取:

function [images, labels] = loadMNIST(imageFile, labelFile) % 读图像文件(大端序) fid = fopen(imageFile, 'rb', 'b'); fread(fid, 1, 'uint32', 0, 'b'); % magic number numImages = fread(fid, 1, 'uint32', 0, 'b'); rows = fread(fid, 1, 'uint32', 0, 'b'); cols = fread(fid, 1, 'uint32', 0, 'b'); imgData = fread(fid, inf, 'uint8', 0, 'b'); fclose(fid); % 按列优先组织成四维数组 images = reshape(imgData, cols, rows, numImages); images = permute(images, [2 1 3]); images = reshape(images, rows, cols, 1, numImages); % 读标签文件 fid = fopen(labelFile, 'rb', 'b'); fread(fid, 1, 'uint32', 0, 'b'); numLabels = fread(fid, 1, 'uint32', 0, 'b'); labels = fread(fid, inf, 'uint8', 0, 'b'); fclose(fid); end

这里最容易踩的坑是字节序。IDX文件是大端存储,Matlab里fopen必须指定机器格式为'b',漏掉的话读出来的维度和像素值全是乱的,而且不会报错,非常坑。初次解析时建议先打印前几个像素值和标签值,跟MNIST官方文档核对一下再往下走。

2.3 三种加载方式对比

方式优点缺点适合场景
内置示例函数零配置、直接四维数组版本依赖,部分发行版没有快速验证实验流程
自己解析IDX文件通用性强、理解数据底层组织需处理字节序和维度排列需要自定义数据读取的正式实验
从文件读取再自行切分灵活、可做训练集比例调整代码量最大需要做消融实验或数据量缩减

不管用哪种方式,最后拿到的一定是28×28×1×N的uint8数组。像素范围0-255,建议归一化到[0,1]区间再进网络,常见做法是:

XTrain = double(XTrain) / 255; XTest = double(XTest) / 255;

这一步看起来简单,但跳过它直接训练往往会导致前期loss下降很慢,因为预训练模型期望的输入分布是接近ImageNet统计的,在迁移学习时保持输入尺度一致性很重要。

3. 适配ResNet18输入:28×28单通道与224×224三通道的冲突

直接拿着28×28灰度图去训练一个现成的resnet18,不报错才奇怪。因为ResNet18在Matlab里默认输入层是224×224×3,而且网络内部所有卷积层的设计都是基于这个分辨率推算的。想跑通实验,必须解决三个维度上的冲突。

3.1 三个必须解决的冲突点

第一个是分辨率。28×28直接进去,经过首层7×7步长2卷积和3×3步长2最大池化后,特征图已经缩到7×7左右,再经过后续多个步长卷积,空间尺寸会变得很小,最后全局平均池化层很可能收到连卷积核大小都覆盖不了的张量,直接报维度错误。就算硬调整步长让尺寸能传导下去,特征也几乎被压没了,分类效果会很差。

第二个是通道数。MNIST是单通道灰度图,而ResNet18的输入层强制要求三通道RGB。第三个是类别数,预训练模型最后的全连接层是1000类,MNIST只需要10类。

我的建议是,不要在输入层上硬改,而是用augmentedImageDatastore在数据读取阶段统一完成尺寸缩放和灰度到RGB的通道扩展。这是Matlab给你安排好的标准接口,也是最省事、最不容易出问题的路径。

3.2 用augmentedImageDatastore完成尺寸与通道转换

augmentedImageDatastore可以理解为一个“带预处理的数据管道”:它读取原始图像数据,按要求缩放尺寸、复制通道,同时还能做数据增强。对MNIST来说,我们让它在读取时把每张28×28灰度图放大到224×224,并把单通道复制成三通道。

imageSize = [224 224]; augimdsTrain = augmentedImageDatastore(imageSize, XTrain, YTrain, ... 'DataAugmentation', imageAugTrain); augimdsTest = augmentedImageDatastore(imageSize, XTest, YTest);

注意,augmentedImageDatastore接收原始数组时,会自动把灰度图复制到RGB三通道,不需要你手动repmat。很多人一听到“灰度转RGB”就想着自己写循环去复制通道,白白浪费时间,还会把数据内存翻三倍,完全没有必要。

3.3 替换网络末端的分类头

拿到预训练ResNet18后,要做的就是用layerGraph把网络结构打开,把最后的全连接层和分类层换成10类版本:

net = resnet18; lgraph = layerGraph(net); numClasses = 10; newFC = fullyConnectedLayer(numClasses, 'Name', 'fc_new'); lgraph = replaceLayer(lgraph, 'fc1000', newFC); newClassLayer = classificationLayer('Name', 'class_new'); lgraph = replaceLayer(lgraph, 'ClassificationLayer_predictions', newClassLayer);

这里不同Matlab版本里层的名字可能有差异,比如旧版本可能不叫fc1000而是fc,或者分类层名称后带了时间戳后缀。保险的做法是先用analyzeNetwork(lgraph)把整个网络结构打开看一眼,确认你要替换的层具体叫什么名字,再写replaceLayer。

最后一个细节是,ResNet18里接在fc1000后面的softmax层(通常叫prob)可以不用动,因为softmax层的输出维度是自动跟随上一层全连接层的,你只要换了fc层和分类层,它就会自动适配成10维输出。

3.4 为什么保留预训练权重而不是随机初始化

这是整个实验里最关键的一个设计决策。ResNet18的预训练权重是在ImageNet上通过监督学习得到的,底层那些卷积核已经学会了边缘、纹理、颜色等通用低级特征。MNIST虽然跟ImageNet差异很大,但手写数字的边缘和纹理结构依然可以复用这些底层特征。迁移学习的思路就是:底层特征提取器基本不动,只重新训练顶层的分类部分。

我在同样的数据上做过对照:预训练权重做迁移学习,6个epoch测试准确率就能到99%以上;而完全重头随机初始化训练同一个ResNet18,20个epoch还在97%附近波动,而且前几个epoch的loss下降曲线非常不稳定。原因很直接,ResNet18将近1200万参数,MNIST只有6万张训练图,随机初始化去拟合这个数据规模,严重过拟合几乎是必然的。

这里给一个训练选择的对比表:

方案收敛速度最终准确率过拟合风险适用场景
预训练权重+迁移学习快(4-6轮达到99%)高(99.2%左右)低数据量小的常规实验
随机初始化从零训练慢(15-20轮仍在波动)中等(97%-98%)高专门研究网络结构本身

所以除非你的实验目的就是研究ResNet18在MNIST上的结构表达能力,否则不要浪费算力去随机初始化训练。

4. 训练参数选择与收敛过程分析

网络结构改好之后,训练参数就是决定实验成败的下一个变量。Matlab的trainingOptions提供了完整的配置面,下面这几个参数的组合是我在MNIST上反复调过之后比较稳的一套。

4.1 优化器、学习率和批大小的实测组合

options = trainingOptions('sgdm', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 6, ... 'MiniBatchSize', 64, ... 'Shuffle', 'every-epoch', ... 'ValidationData', augimdsVal, ... 'ValidationFrequency', 30, ... 'Verbose', false, ... 'Plots', 'training-progress'); netTrained = trainNetwork(augimdsTrain, lgraph, options);

有几个参数值得单独说。优化器我在sgsdm和adam之间对比过,adam在前3个epoch的收敛曲线更陡,但中期会出现轻微的过拟合抖动;sgdm收敛稍慢但曲线平滑,最终准确率两者差距不大。MNIST这种规模的数据,选哪个都不会翻车,但如果你希望训练过程可视化时曲线干净好看,sgdm是更好的选择。

学习率是这里最敏感的参数。迁移学习场景下,底层预训练特征已经很好了,你只需要用较小的学习率微调,0.001是一个安全的起点。如果开到0.01,第一个batch的loss就可能直接冲到NaN,因为浅层预训练权重在大学习率下会被破坏。如果开到0.0001,收敛速度会明显变慢,到第6个epoch还没有完全收敛。

MiniBatchSize我建议用64或者128。MNIST图像极小,即便放大了到224×224,64张图一个batch的内存占用也只有几十兆,对现代显卡来说毫无压力。增大的batch_size能加快训练,但要注意如果显存紧张或者用的是CPU训练,64是一个更稳妥的取值。

4.2 数字识别的数据增强要克制

数据增强是训练图像分类模型的常规操作,但用在MNIST上必须克制。最大的禁忌是水平翻转——数字2水平翻转后看起来还是2,但6翻转后会变成9,9会变成6,这种语义反转直接污染标签。我看到过不少初学者稀里糊涂加了'RandXReflection', true,然后发现验证准确率始终上不去,还百思不得其解。

对MNIST比较安全的数据增强是平移和轻微旋转,因为手写数字的笔画有一定偏移,增强后反而能提升泛化能力:

imageAugTrain = imageDataAugmenter( ... 'RandRotation', [-5 5], ... 'RandXTranslation', [-3 3], ... 'RandYTranslation', [-3 3]);

注意imageAugTrain要传给训练集的augmentedImageDatastore,测试集不要做任何增强,因为测试集必须保持原始分布才能得出可比的准确率。我在实验里也试过不加任何增强,测试准确率大概在99.1%左右;加上轻微平移旋转增强后能到99.4%左右。提升不算大,但已经是肉眼可见的区别。

4.3 训练过程中如何判断是否过拟合

训练时设置'Plots', 'training-progress',Matlab会实时画出训练准确率和损失曲线,同时在设置ValidationData后还会叠加验证集的曲线。判断是否过拟合很简单:训练准确率不断上升但他验证准确率在某个epoch后开始下降或者停滞,同时验证损失反转上升,这就是过拟合的典型信号。

在我的实验里,迁移学习的ResNet18在第4个epoch左右训练准确率和验证准确率就已经非常接近了,之后曲线基本平行走平,没有明显过拟合迹象。这跟预训练权重带来的强先验有关。如果你用的是随机初始化,过拟合信号会来得早得多,具体表现为训练准确率高到99.5%以上且验证准确率停在97%左右不再动,这时候最有效的措施是加正则化项或者在数据增强上做文章,而不是调学习率。

另外建议单独留出1500-2000张训练图作为验证集,不要让trainNetwork拿整个训练集跑。可以用cvpartition或splitEachLabel做切分,这样训练过程中能看到验证集表现,而不必等全部训练结束。

5. 测试集评估与误判样本的微观分析

训练完成只是前半段,真正体现实验价值的是对测试集的评估。这一步既能验证模型泛化能力,又能通过误判分析找到模型的行为特征。

5.1 准确率与混淆矩阵

测试集评估的核心代码非常短:

YPred = classify(netTrained, augimdsTest); accuracy = mean(YPred == YTest); figure; cm = confusionchart(YTest, YPred); cm.Title = 'ResNet18 on MNIST Test Set';

classify会自动把测试集通过augimdsTest的尺寸转换管道,无需额外预处理。confusionchart画出来的矩阵里对角线就是正确分类的样本数,非对角线位置能直观看到哪些数字互相混淆。

在我跑的一组实验里,测试准确率稳定在99.4%左右,也就是10万张测试图里大概有60张左右被分错。别觉得60个错很多,MNIST里有一批标注本身就有争议——有些手写数字的写法人眼都不一定能分清楚,别说模型了。

5.2 误判样本可视化:到底哪里看错了

为了搞清楚误判长什么样,我写了一段脚本把预测错误的样本拼成一张大图:

misIdx = find(YPred ~= YTest); sampleIdx = misIdx(randperm(numel(misIdx), min(24, numel(misIdx)))); figure; montage(reshape(XTest(:,:,:,sampleIdx), 28, 28, 1, []));

观察下来,最集中的误判类型有几类:潦草的0被看成6,因为上半部有个明显的出头;8的上下两个圈比例不匀时会被看成3;3的上半部分太平会被看成5。这些错误有一个共同特征:信息量极低、笔画走向处在类别边缘。换句话说,人类标注者也大概率拿不准这些样本该归哪类。

这类误判分析放在课程设计里是一个非常加分的亮点,因为多数人会停在“准确率99.4%”这个数字上,而你还能说明白剩下的0.6%错在哪、为什么错、有没有改进空间,这才是一个完整的实验闭环。

5.3 与浅层网络的对比思考

我用一个三层的简单卷积网络在同样预处理条件下做了对照实验。浅层网络在测试集上也能达到98.8%左右,但训练轮数需要上到15轮才收敛。这说明什么?对于MNIST这种低复杂度任务,ResNet18的深度优势并没有完全发挥出来,或者说数据量根本不足以逼迫深层网络展现它的表达上限。它的赢面主要来自迁移学习的预训练先验,而不是残差结构本身。

这给实操的启示是:如果只是想把MNIST跑出一个漂亮的数字,LeNet级别的网络就够了,根本不用上ResNet18;但如果你想研究“预训练模型在跨域小数据上的适配机制”或者“深度网络在低分辨率数据上的行为变化”,这个课题就有真正的实验意义。搞清楚实验目的,比盲目追求高准确率重要得多。

6. 仿真实验中遇到的典型报错与排查思路

跑通整个实验的过程远没有现在写的这么顺利。下面把我在Matlab里遇到的几类报错和排查思路捋一遍,这些错误在换数据、换网络的时候大概率还会碰到。

6.1 通道数不一致

典型报错内容是Input data must have 3 channels。根因是网络输入层要求224×224×3,但直接喂了28×28×1。解决路径不是去改网络输入层,而是用augmentedImageDatastore统一转换,如果你必须手写数据预处理循环,那就用repmat(img, [1 1 3])手动复制通道,但性能会差不少。

6.2 输入尺寸不匹配

报错形式通常是维度校验失败,提示某一层的输出尺寸和下一层的期望尺寸冲突。这个错误最容易出现在你手动改输入层尺寸之后,因为ResNet18后面的卷积和池化层对空间分辨率有严格依赖,28×28并不能顺畅传导到全局平均池化层。记住一个原则:要么整体重新设计网络结构,要么老老实实resize到224×224,不要只改第一层。

6.3 loss变成NaN

训练到一半loss变NaN,十有八九是学习率过高。我第一次把InitialLearnRate设成0.01,第一个iteration就爆了。排查方法:先降到0.001再试,如果还爆,检查是否输入数据里有NaN或者归一化是否遗漏。另外数据增强里如果旋转角度开得过大(比如[-180 180]),超出合理范围也会让某些样本变成极端输入,导致数值不稳定。

6.4 GPU没被调用,训练时间异常长

如果你确认机器有独显但训练速度还是慢得像CPU,多半是trainingOptions里的ExecutionEnvironment参数没设置对。可以显式指定:

options = trainingOptions('sgdm', ..., 'ExecutionEnvironment', 'gpu');

或者先在命令行敲一下gpuDevice确认Matlab能识别显卡。遇到过一种情况是机房电脑的显卡算力太低,Matlab判定不满足CUDA版本要求自动退回CPU,这种情况下只能接受CPU训练,或者用'auto'让程序自己选。

6.5 常见错误速查表

报错现象根因解决办法
Input data must have 3 channels灰度图直喂三通道网络用augmentedImageDatastore自动转RGB
维度校验失败输入分辨率与网络设计不符resize到224×224,不要只改输入层
loss出现NaN学习率过高或输入含NaN学习率降到0.001,检查归一化
replaceLayer找不到层版本差异导致层名称不同先用analyzeNetwork查看实际层名
训练速度极慢GPU未被识别检查gpuDevice或显式设置ExecutionEnvironment

还有一个容易被忽视的问题:不同Matlab版本中resnet18的分类层名称可能不一样,我在replaceLayer时报过“找不到层”的错误,最后靠analyzeNetwork(lgraph)看清楚全图才定位到正确名称。所以遇到层操作相关报错,永远是先可视化再动手,不要凭记忆写层名。这套流程跑完之后,你会发现Matlab做深度学习的核心逻辑其实很清晰:数据管道的设计、网络结构的可视化检查、训练参数的实证调优,每一步都有对应的工具支撑。换个数据集、换个预训练网络,整个方法论照样能平移过去。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 6:21:21

MoveIt 2机械臂运动规划实战:从A*到RRT/RRT*算法解析与避障应用

第一次正经用 MoveIt 2 给六轴机械臂做避障规划时,我差点被一个“绕路”问题搞到崩溃:明明障碍物在右边,规划出来的轨迹却先往左画了一个大弧线,再绕回来夹取目标。后来查了底层的规划日志,才发现是默认的 OMPL 配置选…

作者头像 李华
网站建设 2026/10/22 6:21:21

ROS2 学习路线全解析:从环境搭建到通信机制实战

你有没有遇见过这样的场景:想学 ROS2,却在各种教程、资料和官方文档之间来回横跳,装了环境之后却不知道下一步到底该干什么。带新人的时候我经常被问到“ROS2 和 ROS1 到底先学哪个”“为什么我的节点收不到数据”“为什么换个终端就找不到包…

作者头像 李华
网站建设 2026/10/22 6:21:15

Envoy深度解析:L4/L7代理、Filter链与线程模型调优实践

Envoy 这个词第一次出现在我工作流里时,我的第一反应是“又一个负载均衡器”。但真正把一个请求从客户端打进 Envoy、再打到后端,然后看着访问日志和指标一点点还原出它内部的执行路径之后,我才意识到:这玩意儿和传统代理完全不是…

作者头像 李华
网站建设 2026/10/22 6:20:36

基于扩展卡尔曼滤波的IMU磁力计姿态估计Matlab实现

姿态估计这个词,玩过无人机、机器人或者AR/VR的人都绕不开。手机横竖屏切换、飞行器姿态自稳、头显的头部追踪,靠的都是把IMU(陀螺仪加速度计)和磁力计的原始数据,变成一条连续稳定的三维姿态曲线。这套基于扩展卡尔曼…

作者头像 李华
网站建设 2026/10/22 6:18:51

深入解析onbeforeunload:破解页面离开拦截与误用陷阱

1. 先弄清楚 onbeforeunload 拦的是什么、拦不住什么做前端的人几乎都在某个时刻被onbeforeunload恶心过——明明只想关个页面,浏览器非要弹一层“系统生成的离开确认框”,网站自己再叠一层自定义弹窗,两三层下来,用户想走都走不掉…

作者头像 李华
网站建设 2026/10/22 6:18:15

蚁群算法在多无人机协同任务规划中的工程实践与调参指南

简介:这份PDF来自“华为杯”第十三届全国研究生数学建模竞赛,以蚁群算法为背景,完整呈现多无人机协同任务规划赛题的研究与求解方案,适合数学建模参赛者、无人机路径规划研究者及相关专业学生参考。内容从协同侦查、通信约束下调度…

作者头像 李华