资讯详情 Matlab实战:CNN-LSTM图像分类模型猫狗数据集搭建
📅 2026/10/22 8:24:09
说实话最初接到这个任务时我并没有多想无非是拿一个现成的分类网络微调一下。但真正动手之后发现纯CNN在一批猫狗照片上跑出的结果总差口气——光照复杂、角度刁钻、局部遮挡多的图模型往往只盯着纹理细节忽略了不同区域之间的上下文关系。后来把LSTM接到CNN后面让卷积层输出的特征图按空间位置展开成序列LSTM像人扫视图片一样逐块读取这些特征效果立刻不一样了。这篇文章就把这套CNN-LSTM图像分类模型在Matlab 2022版里的完整搭建过程写清楚使用猫狗二分类数据集实战程序结构和图像组织方案都会给出可直接参考的版本。如果你正在做图像分类、准备课程设计或者想把“CNN提特征、LSTM建模长程依赖”这套组合用在Matlab项目里这篇内容应该能帮你省下不少自己摸索的时间。我会从原理讲到训练选项再到常见报错的排查尽量让大家能把代码真跑起来。1. 从纯CNN到CNN-LSTM我为什么在Matlab里搭这个网络1.1 纯CNN在图像分类里的瓶颈在哪里CNN擅长提取局部特征。卷积核在图像上滑动捕捉边缘、纹理、形状片段层层叠加后获得越来越抽象的表达。对于猫狗二分类这种任务纯CNN的表现通常已经不错但它有个天然短板卷积操作看的是“局部感受野”哪怕堆了很多层最终对全局关系的建模仍然依赖深层大感受野的累积中间的信息交互不够直接。举个我实际遇到的例子。有一张侧趴着的猫尾巴卷到身体前面背景是类似颜色的地毯。纯CNN会花很多权重去学习猫毛纹理和地毯纹理的差异但对“猫的头部大概在哪个区域、尾巴应该离头部多远”这种空间上下文关系捕捉得比较弱。结果就是分类边界容易受局部干扰测试集上总有几个难样本反复出错。LSTM不一样。它天然适合处理有顺序的数据每一步都能看到前一步的状态相当于记住并更新一条“记忆线”。如果把图像特征按空间位置展开成序列LSTM就能在读取每个局部区域的同时把之前看过的区域信息一起考虑进去。这种设计刚好补上CNN对全局上下文建模的短板。1.2 CNN和LSTM的分工逻辑整套网络的思路可以拆成两段。第一段是CNN主干负责把原始图像从像素级压缩成高层的空间特征图。这段的核心目标是提取出“哪里有猫的耳朵”“哪里像狗的鼻子”这一类信息输出是一个多维特征图每个位置对应原图某一块区域的语义抽象。第二段是LSTM。CNN输出的特征图不是一个向量而是有高度、宽度、通道数的立体结构。把它按空间位置展开每个位置上的通道值就形成一个向量整张特征图就变成了一个带顺序的向量序列。LSTM从左到右、从上到下逐个读取这些位置每读一个位置都会更新内部状态最后用最后一个时间步的隐藏状态作为整张图的全局表达送到全连接层做分类。这样分工的好处是CNN负责“看”LSTM负责“把看到的东西串起来”。原来CNN里需要靠很深网络才能勉强建模的长程空间依赖现在由LSTM直接处理整体结构更清晰训练起来也更容易控制。1.3 这套模型适合哪些场景如果你手里是标准图像分类任务数据量又很大纯CNN甚至预训练模型微调可能就够用。但如果你想加深对深度学习模型组合的理解、课程设计需要体现“CNN-LSTM”这类混合结构、或者你的图像本身具有明显的空间布局规律比如医学影像里的组织区域关系、文档图像里的版面结构那这套模型就非常适合。我用的猫狗数据集是一种典型验证场景类别少、图像内容差异大、但难样本多。用这个数据集把整套流程跑通之后换到其他图像分类任务只需要改数据路径和最后的分类层数量迁移成本很低。2. 数据准备猫狗数据集的目录组织与预处理细节2.1 数据集目录结构与标签映射在Matlab里做图像分类最顺手的数据组织方式是按类别建文件夹。这样用imageDatastore读取时Matlab会自动把子文件夹名当作标签省去手动标注的麻烦。我的目录结构是这样的cats_dogs/ ├── train/ │ ├── cat/ │ │ ├── cat_001.jpg │ │ └── cat_002.jpg │ └── dog/ │ ├── dog_001.jpg │ └── dog_002.jpg └── val/ ├── cat/ └── dog/实际项目中我建议直接从公开渠道下载一批猫狗图片各类别数量控制在1000张左右。太少的话LSTM学不到稳定的空间依赖太多的话训练时间会拉长。注意检查文件夹命名大小写要一致否则LabelSource读取时可能出现标签不一致的问题。用imageDatastore读取时核心代码是imds imageDatastore(cats_dogs, ... IncludeSubfolders, true, ... LabelSource, foldernames);这里有一个容易忽略的细节如果所有图片都在train文件夹下而没有单独的val文件夹建议先读取全部数据再用splitEachLabel划分。我习惯先保证目录干净train和val分开存放后面做验证集的时候会更省事。2.2 统一尺寸与数据增强的选择CNN-LSTM网络的输入尺寸我选的是227×227×3。这个尺寸既不会太大导致显存压力又不至于太小丢失细节。给模型投喂数据前所有图片都要缩放到这个尺寸。用augmentedImageDatastore可以一边缩放一边做数据增强非常方便。数据增强这一环我做项目时踩过不少坑。增强太弱模型学不到泛化能力增强太狠比如把猫的尺寸缩放得过小、平移过多会导致训练集和真实分布偏离。我最终采用的增强参数如下aug imageDataAugmenter(... RandXTranslation, [-20 20], ... RandYTranslation, [-20 20], ... RandXReflection, true, ... RandScale, [0.9 1.1]);为什么选平移20像素和缩放10%因为猫狗图像的主体通常集中在画面中央小幅平移和缩放可以模拟拍摄角度和距离的轻微变化又不会把主体移出画面。RandXReflection对猫狗这类对称性较强的物体非常有效能直接让有效样本量翻倍。2.3 训练集与验证集的合理划分很多初学者喜欢把全部数据丢进去训练最后才发现没有验证集。验证集的作用不仅是看准确率更重要的是在训练过程中监视过拟合。我建议从原始数据中分出15%到20%作为验证集并且保证猫狗两类在验证集中数量均衡。使用splitEachLabel时要注意设置随机种子否则每次运行的结果可能完全不同rng(2024); [imdsTrain, imdsVal] splitEachLabel(imds, 0.8, randomized);这里0.8表示保留80%作为训练集剩下20%自动成为验证集。加上randomized参数后每次划分都是随机进行的。如果不设置rng同一个脚本两次运行拿到的训练集都不一样后续对比实验结果时就会很麻烦。3. 网络结构设计逐层拆解与参数推算3.1 从输入到特征图卷积基座的设计网络的卷积部分我参考了经典结构的思路但做了精简主要为了控制参数量让普通显卡也能跑得动。整体结构从输入到LSTM之前是这样设计的imgSize [227 227 3]; baseLayers [ imageInputLayer(imgSize, Name, input) convolution2dLayer([11 11], 96, Stride, 4, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer([3 3], Stride, 2, Name, pool1) convolution2dLayer([5 5], 256, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer([3 3], Stride, 2, Name, pool2) convolution2dLayer([3 3], 384, Padding, same, Name, conv3) batchNormalizationLayer(Name, bn3) reluLayer(Name, relu3) convolution2dLayer([3 3], 256, Padding, same, Name, conv4) batchNormalizationLayer(Name, bn4) reluLayer(Name, relu4) maxPooling2dLayer([3 3], Stride, 2, Name, pool3) ];算一下特征图尺寸输入227×227经conv1步长4得到ceil(227/4)57即57×57×96pool1后变成28×28×96conv2保持28×28×256pool2后14×14×256conv3和conv4保持尺寸不变pool3后最终得到7×7×256的特征图。为什么第一层卷积核用11×11、步长4因为输入尺寸偏大时大步长可以快速缩小特征图减少计算量。后面的3×3小卷积核用于在较小尺寸的空间上精细提取特征同时把通道数提高到256保证特征表达足够丰富。3.2 sequenceFoldingLayer把“图像”变成“空间序列”这是整篇文章最关键的一层。CNN输出的特征是7×7×256它本质是一张“空间特征图”有高度、宽度、通道。LSTM要求输入是序列所以必须把特征图转换成序列格式。我用的办法是sequenceFoldingLayer。它的作用是把一个三维特征图按空间位置折叠成序列7×7共有49个位置每个位置有256个通道值因此输出就是一个长度49、每步特征维度256的序列。这种“空间序列”到底代表什么呢我理解它相当于把图像分成49个区域LSTM像视线一样依次扫过这些区域。第一个时间步读左上角区域第二个时间步读右上角区域依次类推。LSTM的隐藏状态会把已经看过的区域信息保留下来所以当读到右下角时模型已经记住了左上角的猫耳朵形态和中部区域的猫脸轮廓最终形成完整的空间上下文判断。代码里接入这一层非常直接foldLayer sequenceFoldingLayer(Name, fold);需要注意sequenceFoldingLayer在训练时会有一个MiniBatchSize属性需要与训练选项保持一致这一点我后面专门讲坑的时候会详细说。3.3 LSTM层与分类层输出模式与过拟合控制特征序列送到LSTM后网络设计如下lstmLayers [ lstmLayer(128, OutputMode, last, Name, lstm) dropoutLayer(0.5, Name, drop) fullyConnectedLayer(2, Name, fc) softmaxLayer(Name, softmax) classificationLayer(Name, classout) ];LSTM隐藏单元数选128是我在64和256之间对比后的折中。64表达能力偏弱256训练明显变慢且容易过拟合128在猫狗二分类上表现最稳。OutputMode设为last是因为分类任务只需要最后一个时间步汇总全部49个位置的最终状态不需要输出每个位置的中间结果。LSTM和全连接层之间我加了dropout比例0.5。LSTM这种循环结构参数量不小没有dropout时训练集准确率很容易冲到接近100%验证集却停在92%上下。加上dropout后过拟合现象明显缓解。最后一层fullyConnectedLayer(2)对应猫、狗两个类别softmax和classificationLayer负责输出概率和计算分类损失。这里唯一要注意的是输出节点数必须与标签类别数一致如果改成了猫狗以外的三分类记得把2改成3。4. 训练选项与超参数每一组数字背后的取舍4.1 学习率与批大小怎么定训练CNN-LSTM时我反复试过几组学习率最终选定1e-3的初始学习率。原因是网络里有BatchNormalization层它对学习率不那么敏感1e-3在这种中等规模网络上收敛速度合适又不会像1e-2那样出现loss震荡。批大小我选32。这个值和sequenceFoldingLayer的MiniBatchSize有直接关系后面会详细解释。从效果看批大小32配合学习率1e-3在验证集上的准确率曲线比较平滑没有出现剧烈波动。如果你显存不够可以尝试16但此时建议把学习率降到5e-4否则梯度噪声太大。训练选项的完整配置如下options trainingOptions(adam, ... InitialLearnRate, 1e-3, ... MiniBatchSize, 32, ... MaxEpochs, 20, ... Shuffle, every-epoch, ... ValidationData, auimdsVal, ... ValidationFrequency, 20, ... Verbose, true, ... VerboseFrequency, 10, ... Plots, training-progress, ... ExecutionEnvironment, auto);有GPU时ExecutionEnvironment设为auto会自动调用GPU没有GPU时会自动退回CPU。用CPU训练的话相同数据量大概会慢3到5倍建议把MaxEpochs降到10以内先跑通流程。4.2 epoch、验证频率与数据打乱MaxEpochs选20是我在猫狗数据集上观察到的收敛拐点。前5个epoch训练准确率从80%迅速升到95%之后增速变慢15个epoch后验证准确率基本稳定在97%左右再训练下去也没有明显提升。ValidationFrequency设为20意思是每20次迭代验证一次。为什么不是每个epoch都验证因为一个epoch大约有50个batch每个epoch结束时验证已经足够观察趋势。设得太频繁会拖慢训练尤其是验证集比较大时每次验证都要在整个验证集上跑一遍前向计算。Shuffle设为every-epoch每个epoch开始前都把训练数据重新打乱。这个设置对LSTM尤其重要。如果数据顺序固定模型会逐渐记住批次之间的表面顺序关系导致验证集表现不稳定。4.3 训练进度图怎么读Matlab训练过程中弹出的进度图包含两组曲线上面是准确率下面是损失。蓝色是训练集黑色是验证集。我习惯盯着验证集损失看。如果训练损失持续下降、验证损失却反弹上升基本可以断定过拟合。这时候不是调低学习率而是加强dropout或数据增强。如果训练损失和验证损失同时高位不动说明学习率太大或网络结构有问题。如果验证损失在某个值附近反复横跳多半是批大小偏小、梯度噪声过大。一个我自己的经验看到准确率到98%就急着停掉训练其实不可取还是让训练把20个epoch跑完然后对比验证集上的最终结果。有时候最后一个epoch会有意外惊喜因为LSTM这类模型在后期的微调阶段会悄悄优化掉不少边缘样本。5. 可直接运行的Matlab代码5.1 主脚本从数据读取到模型训练把上面的模块串起来就是一套完整可运行的训练脚本。命名我习惯用main_cnn_lstm.m整个过程分四段读取数据、构建网络、设置训练参数、启动训练。% 1. 读取与增强数据 imds imageDatastore(cats_dogs, ... IncludeSubfolders, true, ... LabelSource, foldernames); rng(2024); [imdsTrain, imdsVal] splitEachLabel(imds, 0.8, randomized); aug imageDataAugmenter(... RandXTranslation, [-20 20], ... RandYTranslation, [-20 20], ... RandXReflection, true, ... RandScale, [0.9 1.1]); auimdsTrain augmentedImageDatastore([227 227], imdsTrain, DataAugmentation, aug); auimdsVal augmentedImageDatastore([227 227], imdsVal); % 2. 构建CNN-LSTM网络 baseLayers [ ... 上面写过的卷积层 ... ]; foldLayer sequenceFoldingLayer(Name, fold); lstmLayers [ ... 上面写过的LSTM分类层 ... ]; layers [baseLayers; foldLayer; lstmLayers]; % 3. 设置训练选项 options trainingOptions(adam, ... InitialLearnRate, 1e-3, ... MiniBatchSize, 32, ... MaxEpochs, 20, ... Shuffle, every-epoch, ... ValidationData, auimdsVal, ... ValidationFrequency, 20, ... Plots, training-progress); % 4. 开始训练 net trainNetwork(auimdsTrain, layers, options);如果你在某一版本里遇到layer数组拼接时报维度不匹配的错误可以用layerGraph手动连接lgraph layerGraph(layers); lgraph connectLayers(lgraph, pool3, fold);trainNetwork对层的连接顺序有严格检查autoconnect偶尔会处理不好sequenceFoldingLayer这种特殊层手动connect会更稳。5.2 特征图可视化确认CNN学到了什么训练完之后我建议抽一张验证集图片可视化中间卷积层的特征图确认模型没有把学习引向奇怪的方向。代码很简单testImg readimage(imdsVal, 1); act activations(net, testImg, relu4); actMat act(:,:,:,1); actMat mat2gray(actMat); montage(actMat, Size, [8 8]);这里取relu4的输出因为它是送入LSTM前的最后一层卷积特征最能代表模型最终提取到的空间特征质量。如果特征图看起来是一堆均匀噪点说明网络没有学到有效结构如果能隐约看出猫或狗的轮廓说明特征提取是正常的。特征图可视化还有一个作用帮你判断是否需要加深CNN。如果特征图过于稀疏大部分位置都是黑色说明当前任务对CNN来说太简单可以适当减少卷积层如果特征图过于模糊则可以加一层卷积提升表达力。5.3 评估准确率与混淆矩阵训练完成后在验证集上做最终评估。这一步必须先reset数据存储否则分类顺序会错位reset(auimdsVal); YPred classify(net, auimdsVal); YVal imdsVal.Labels; accuracy sum(YPred YVal) / numel(YVal); fprintf(验证集准确率: %.2f%%\n, accuracy * 100); figure; confusionchart(YVal, YPred);我跑通后验证集准确率稳定在96%到98%之间。如果猫狗图片本身差异较大通过这套流程达到95%以上是比较正常的。混淆矩阵要重点看“狗被误判成猫”和“猫被误判成狗”哪个方向更多。这个信息能帮你反推数据问题比如某一类图片整体偏暗、角度特殊就需要在增强时针对这类样本增加亮度扰动或旋转角度。6. 实战中踩过的坑与备选方案6.1 sequenceFoldingLayer与MiniBatchSize的烦恼我第一次跑这个网络时卡在了一个很隐蔽的报错上。训练刚开始就提示sequenceFoldingLayer的MiniBatchSize必须与训练选项里的MiniBatchSize一致。问题在于sequenceFoldingLayer在构造时没有显式暴露MiniBatchSize的设置接口训练时它和trainingOptions里的MiniBatchSize必须严格相等。我当时的解决方法是把训练选项里的MiniBatchSize固定为32同时在构造layerGraph后去读一下fold层的MiniBatchSize确认一致layers(ismember({layers.Name}, fold))如果你的代码是在旧版本的Matlab运行时还有可能碰到sequenceFoldingLayer不被trainNetwork支持的情况。这种情况不要死磕直接跳到下面的两步法方案。6.2 GPU显存不足与过拟合对策LSTM层虽然是序列建模但在Matlab里它同样需要大量矩阵运算显存占用比纯CNN高不少。我用一块中端显卡跑的时候batch32一度接近显存上限。遇到Out of Memory时最直接的办法是把MiniBatchSize调到16同时把LSTM隐藏单元从128降到64。降batch后要注意同步调整学习率。我通常的做法是batch减半学习率也减半从1e-3降到5e-4这样收敛稳定性不会受到太大影响。过拟合的另一个对策是增强dropout强度。0.5的dropout在中等数据集上效果不错如果你发现验证准确率明显低于训练准确率可以尝试把dropout提高到0.6同时增强RandScale的范围到[0.85 1.15]。6.3 不依赖sequenceFoldingLayer的两步法如果你的Matlab版本不支持sequenceFoldingLayer或者你想彻底回避它带来的训练选项限制可以用两步法实现同样的CNN-LSTM效果。第一步用预训练网络或自己训练的CNN提取每张图的特征图并将特征图reshape成序列featureSize 6 * 6 * 256; XTrain cell(numel(imdsTrain.Files), 1); for i 1:numel(imdsTrain.Files) img readimage(imdsTrain, i); img imresize(img, [227 227]); act activations(cnnNet, img, relu4); act squeeze(act(:,:,:,1)); XTrain{i} reshape(act, size(act,1)*size(act,2), size(act,3)); end第二步用LSTM网络对序列分类lstmLayers [ sequenceInputLayer(256) lstmLayer(128, OutputMode, last) dropoutLayer(0.5) fullyConnectedLayer(2) softmaxLayer classificationLayer];这种方式的好处是每一步都可以单独调试先确认CNN特征提取质量再训练LSTM分类器。缺点是不能端到端联合调优但作为学习目的和中等规模数据来说效果已经足够。我在实际项目里遇到特殊的层兼容性问题时都是先用两步法把流程跑通再回头优化端到端结构。这种做法在排错时特别高效。最后再分享一个体会。这个CNN-LSTM结构对我来说最大的价值并不是在准确率上比纯CNN高了多少而是它逼着我把模型的每个中间环节都看得清清楚楚。CNN输出的特征图是模型真正“看到”的东西LSTM则强迫模型按顺序去理解整张图。做图像分类项目时遇到瓶颈不妨试试把模型拆开看看往往比盲目堆更多卷积层有效得多。后续如果想进一步提升可以在这个框架基础上加入注意力机制或者把LSTM替换成双向结构扩展方向非常多。