ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CNN卷积神经网络Matlab实现:从LeNet到手写数字识别全流程

CNN卷积神经网络Matlab实现:从LeNet到手写数字识别全流程 简介面向深度学习初学者的 CNN 卷积神经网络 Matlab 实现资源聚焦手写数字识别这一经典计算机视觉任务既能用于课程设计也能作为入门实践参考。包内共 2000 个文件以 1991 张 bmp 格式手写数字图像为主体构成可直接使用的训练与测试数据集另有 8 个 m 源码文件和 1 个 txt 说明覆盖数据预处理、LeNet 网络搭建、模型训练和评估等环节整体压缩包仅 11.36MB轻量且便于快速复现。源码基于 LeNet 架构演示卷积层、池化层、全连接层以及 ReLU 激活函数的实现方式并涉及交叉熵损失函数与 SGD、Adam 优化器的参数配置借助 Matlab 的 trainNetwork、evaluate 函数可以方便地完成网络训练与精度测试txt 文件则用于记录训练日志或运行说明便于对照调整学习率、批量大小等超参数。目前已有 160 人学习下载参考这些代码可以快速搭建自己的手写数字识别系统并理解 Matlab 深度学习工具箱的核心用法。1. CNN卷积神经网络Matlab实现这个压缩包里到底有什么做课程设计或者毕设开题的时候很多人会先找一份能跑的CNN代码而不是先去啃理论。这份资源就是干这个用的解压后是十几个带标签命名的手写数字bmp图片再加上一个基于LeNet思路的Matlab实现脚本目标是把图片输入→模型训练→数字识别整条链路在Matlab里跑通。它的价值不在精度而在于让你在最短时间内看到卷积、池化、全连接这些概念落地成代码的样子。手写数字识别是CNN最经典的入门场景MNIST的28x28输入尺寸决定了网络结构不会太复杂用来理解特征提取和参数共享特别合适。适合哪些人适合课程作业卡在理论懂但写不出代码的同学也适合想快速验证Matlab深度学习工具箱用法的工程人员。2. 搞懂LeNet架构这个压缩包里的CNN为什么这样搭2.1 为什么是CNN而不是普通全连接网络手写数字识别这类图像任务如果把28x28的图片拉直成784维向量扔进全连接网络理论上也能训练但参数量大、收敛慢而且对像素位置极其敏感同样的数字稍微偏移几个像素全连接网络的输出可能就完全变了。CNN的两个核心机制就是为了解决这个问题设计的。一个是局部连接卷积核只看局部区域不会像全连接那样把每个输出节点跟所有输入像素相连。另一个是权值共享同一个卷积核在整张图上滑动参数是同一组这样提取到的特征天然具有平移不变性。这两个特性叠加让CNN在图像任务上参数更少、泛化更好。你拿这份资源里那些bmp图片做实验会发现手写数字的笔画粗细、位置偏移差异很大但卷积特征图能把关键边缘和形状信息稳定地提出来。2.2 先读文件名bmp命名里藏着标签和样本顺序解压之后你会看到一堆类似6_221.bmp、2_806.bmp、4_612.bmp这样的文件。这个命名规则非常直白下划线前的第一位数字就是这个样本的标签下划线后的数字是样本编号。也就是说这是一份手工切分好的、带标注的手写数字图片集不需要额外找label文件。我在复现的时候习惯先写一小段脚本把文件名解析出来顺便确认图片尺寸和通道数这样后面搭网络心里有底files dir(fullfile(data, *.bmp)); names {files.name}; labels zeros(numel(names), 1); for i 1:numel(names) tok regexp(names{i}, ^\d, match, once); labels(i) str2double(tok); end info imfinfo(fullfile(files(1).folder, files(1).name)); fprintf(图片数量: %d, 尺寸: %dx%d, 通道: %d\n, ... numel(names), info.Width, info.Height, info.BitDepth / 8); disp(table(names, labels, VariableNames, {文件名, 标签}));这段代码做的事情是先用dir把data目录下所有bmp文件列出来再用正则表达式^\d匹配文件名开头的连续数字str2double转成数值标签。imfinfo用来查第一张图的宽高和位深位深除以8得到通道数。最后用table打印文件名和标签的对应关系方便人工核对。这一步很值得做因为后面搭建网络时imageInputLayer的输入尺寸必须跟实际图片一致。如果图片不是28x28训练阶段报错还是小事更隐蔽的是标签解析错位导致训练成功但识别一塌糊涂。2.3 LeNet各层尺寸推导5x5卷积核在28x28上怎么走LeNet-5是1998年Yann LeCun提出的架构本来就是为手写数字识别设计的原始输入是32x32后来MNIST流行后大家常用28x28变体。这份资源里的模型基本遵循同样的套路卷积、池化、卷积、池化、全连接。先把各层输出尺寸算清楚这比背结构图有用得多。卷积层输出尺寸公式是 W_out (W_in - F 2P) / S 1其中F是卷积核大小P是paddingS是步长。池化层如果核2x2步长2输出尺寸直接减半。层卷积核/池化核输出尺寸说明输入层-28x28x1灰度图单通道卷积层15x5, 6个核24x24x6无padding步长1池化层12x2, 步长212x12x6下采样卷积层25x5, 16个核8x8x16无padding步长1池化层22x2, 步长24x4x16下采样全连接1-1204x4x16展平全连接2-84中间层输出层-10对应0-9十个类别注意第二层池化之后是4x4x16展平是256个值跟原版LeNet-5的400个值略有不同这是因为输入从32x32缩到了28x28。这不影响模型运行全连接层输入节点的数量跟着特征图展平结果走就行。很多人在这一步翻车就是没算清楚经过两次卷积两次池化后特征图变成了几乘几结果fullyConnectedLayer的输入维度怎么对都对不上。2.4 池化和激活函数为什么这样排LeNet里是先卷积再池化中间夹一个激活函数。卷积提取局部特征后ReLU把负值置零引入非线性池化再做下采样。ReLU在Matlab里对应reluLayer它的优势是计算简单、梯度不容易消失。早年sigmoid在层数深的时候梯度衰减明显训练CNN效果差换成ReLU之后收敛速度快很多。池化层的作用有两个一是降维减少后续计算量二是增强平移鲁棒性。手写数字的笔画粗细不稳定同一个数字写出来位置可能差一两像素池化取邻域最大值后这种小的位置偏移对后续层的影响会被削弱。放在卷积后面而不是前面是为了先提取特征再压缩避免丢失太多有效信息。我在复现时会对照网络图逐一在Matlab里确认每层参数尤其是池化的stride、卷积的padding。脚本里如果stride写默认值1池化尺寸就可能跟预期不同这会连锁影响后面全连接层的输入维度。3. 把代码跑起来从bmp图片目录到trainNetwork出模型3.1 搭网络用layer数组写出一版LeNetMatlab定义网络结构最直接的方式是把层按顺序放进一个数组从输入层开始到classificationLayer结束。下面这段代码就是一套标准的MNIST版LeNetlayers [ imageInputLayer([28 28 1]) convolution2dLayer(5, 6, Padding, 0) reluLayer maxPooling2dLayer(2, Stride, 2) convolution2dLayer(5, 16, Padding, 0) reluLayer maxPooling2dLayer(2, Stride, 2) fullyConnectedLayer(120) reluLayer fullyConnectedLayer(84) reluLayer fullyConnectedLayer(10) softmaxLayer classificationLayer];convolution2dLayer的第一个参数是卷积核尺寸第二个是卷积核个数也就是输出特征图数量。Padding, 0表示不填充步长默认1。maxPooling2dLayer(2, Stride, 2)表示2x2池化窗口、步长2这样输出尺寸精确减半。fullyConnectedLayer(120)和fullyConnectedLayer(84)是中间全连接层fullyConnectedLayer(10)输出十个类别得分最后softmaxLayer把得分转成概率classificationLayer计算交叉熵损失。这段代码里最容易改错的地方是第一个imageInputLayer的通道数。如果你的bmp是24位彩色图而不是8位灰度图这里就得写成[28 28 3]否则训练时输入数据维度匹配不上。3.2 数据读取imageDatastore和标签解析Matlab里处理图片数据集推荐用imageDatastore它会自动管理文件列表和读取配合Labels参数可以把标签绑到每张图上。结合前面第2章解析出来的labels数组数据读取可以写成这样filePaths fullfile({files.folder}., {files.name}.); imds imageDatastore(filePaths, Labels, categorical(labels, 0:9)); numSamples numel(imds.Files); numClasses numel(categories(imds.Labels)); fprintf(样本数: %d, 类别数: %d\n, numSamples, numClasses); augImds augmentedImageDatastore([28 28], imds);categorical(labels, 0:9)的作用是把数值0到9转成类别变量并明确类别顺序。这一步如果省略Matlab会自动按数值排序排序结果仍然是0到9但养成显式指定类别的习惯可以避免后续类别顺序错乱。augmentedImageDatastore([28 28], imds)会把每张图统一缩放成28x28再喂给网络相当于在输入层前做了一次尺寸对齐。这里有个非常关键的现实问题这份资源里的bmp图片数量很少可能只有十几张。如果每类只有一两张样本肯定不够训练出一个严谨的模型。所以数据流这步的重点是把链路跑通而不是追求精度。如果后续要正经做分类你需要补充更多样本或者用这份代码的结构去套MNIST完整数据集。3.3 训练与首次评估trainNetwork和classify的基本套路训练部分的核心是trainingOptions配置训练选项然后trainNetwork把网络结构、数据和选项一起丢进去训练。最简单的一版options trainingOptions(adam, ... InitialLearnRate, 1e-3, ... MiniBatchSize, 32, ... MaxEpochs, 15, ... Verbose, true, ... Plots, training-progress); net trainNetwork(augImds, layers, options); YPred classify(net, augImds); accuracy mean(YPred imds.Labels); fprintf(训练集准确率: %.2f%%\n, accuracy * 100);trainingOptions第一参数是优化器这里用adam它对学习率的敏感度比纯SGD低适合刚上手跑CNN。InitialLearnRate设1e-3是新手起步值如果损失下降太慢可以调到1e-2如果振荡剧烈则降到1e-4。MiniBatchSize是每轮迭代送入的样本数机器内存小就设16或32。MaxEpochs是数据集完整过几遍的次数15轮对这份小数据集来说已经能明显看到损失下降。trainNetwork训练结束后返回一个DAGNetwork或SeriesNetwork对象保存了所有层的权重。classify对输入做前向计算返回每个样本的预测标签跟真实标签比较就能算出准确率。对这份小数据集准确率可能虚高也可能很低这都正常重点是你已经验证了整条代码链路没有任何报错。4. 训练参数怎么设学习率、批次、迭代次数参考值4.1 三个核心超参数的起步值Matlab里训练CNN最常用的函数就是trainingOptions它控制着训练过程几乎全部的关键超参数。下面根据我的复现经验把这几个参数的实际参考值和使用场景列出来参数起步值调整方向InitialLearnRate1e-3损失震荡就调小收敛太慢就调大MiniBatchSize32显存不足调小数据量大调大MaxEpochs15验证精度还在涨就加大过拟合就减小L2Regularization1e-4过拟合时加大到1e-3ValidationFrequency30或50根据每轮迭代数设置让验证不要太频繁学习率是这些参数里最玄学的一个。它对训练的影响远大于其他参数设得太大损失曲线会像心电图一样上下跳甚至发散成NaN设得太小训练几十轮损失还慢慢腾腾。我一般先用1e-3跑一轮观察如果前几个iteration损失就暴跌到接近0说明学习率过大降到1e-4重来如果到第十轮还在线性下降说明学习率偏小可以加大到3e-3。MiniBatchSize影响训练速度和显存占用。批量越大梯度估计越稳但每轮迭代的计算量也越大。手写数字这种小图CPU上32或64都能跑。如果你开了GPU训练但显存不够报错通常出现在训练开始不久这时第一个反应就是把这个值往小调。4.2 数据增强旋转平移之后再用这小数据集上数据增强几乎是刚需。augmentedImageDatastore除了缩放尺寸还可以带上DataAugmentation参数做随机变换。常用的做法是轻微旋转和平移模拟手写数字的自然变化imageAugmenter imageDataAugmenter(... RandRotation, [-10 10], ... RandXTranslation, [-2 2], ... RandYTranslation, [-2 2], ... RandXScale, [0.9 1.1]); augImds augmentedImageDatastore([28 28], imds, ... DataAugmentation, imageAugmenter);RandRotation的[-10 10]表示每张图在每次使用时随机旋转-10到10度。RandXTranslation和RandYTranslation是像素级别的平移范围设得太大会把数字边缘裁掉。RandXScale是水平缩放0.9到1.1表示缩小到90%或放大到110%。要注意的是数据增强发生在每次迭代读取数据时也就是说同一张原始图片在不同轮次会被随机变换成不同的样子。这在效果上相当于扩大了训练集缓解过拟合。但对这份只有十几张图的资源来说增强幅度太激进反而会让模型学不到稳定的特征建议旋转控制在±10度以内平移不超过2像素。4.3 训练过程看什么损失曲线和准确率曲线打开Plots, training-progress后训练窗口里会出现两条曲线一条是损失一条是准确率。看这两条曲线比盯着终端日志直观得多。需要重点观察的是损失曲线的形态。如果损失在初始阶段快速下降然后逐渐变平这是正常的学习轨迹。如果损失不降反升先检查学习率是否太大。如果损失在某个值附近反复震荡可能是MiniBatchSize太小梯度噪声太大。如果损失降到某一个平台后长时间不变化说明模型容量不够或学习率太小可以试着把学习率在现有基础上除以10再继续训练。准确率曲线滞后于损失曲线因为它反映的是分类正确率要等损失降到一定程度才会明显上升。训练集准确率接近100%但验证集上表现不理想这是典型的过拟合信号后面避坑章节会展开讲。4.4 参数调整的优先级清单我踩过几次坑之后给自己定了一个调整顺序遇到训练效果不好按这个顺序排查能省很多时间先看损失曲线形态确认训练没有发散或卡死。再检查数据预处理归一化、尺寸、标签对应关系。之后动学习率它影响最明显一次只改一个数量级。最后才调网络结构宽度比如卷积核数量从6加到12。过拟合时优先上数据增强和L2正则不要一上来就减少层数。这个顺序背后的逻辑是先把数据问题排掉再改训练策略最后才动模型结构。很多人一遇到精度不行就加卷积层结果损失反而变大了那是因为数据层面的问题根本没解决。5. 复现避坑指南Matlab里跑CNN的五个常见问题5.1 输入尺寸对不上先查imfinfo再改网络现象trainNetwork一执行就报错提示输入层数据维度不匹配或者Error using trainNetwork后面跟着一大串层维度信息。原因bmp图片实际尺寸不是28x28或者彩色图有3个通道跟imageInputLayer([28 28 1])对不上。这份资源里的bmp可能是从某个数据集切出来的原始尺寸未必统一。解决训练前必须先跑一遍第2章的imfinfo检查代码确认所有图片的宽高和位深。尺寸不统一就用augmentedImageDatastore强制缩放通道是3就把输入层改成[28 28 3]或者在数据读取时用rgb2gray转灰度。5.2 标签错位导致的高分假象现象训练过程非常顺利打印训练集准确率超过95%但随便挑一张没见过的图片验证识别结果完全是乱的。原因标签解析时文件名排序和标签排序没对齐。比如dir返回的文件顺序是系统排序你手动构造的labels数组如果按另一套顺序排imageDatastore的Labels就跟图片错位了。更隐蔽的是文件名里的数字位数不一致比如10_1.bmp这种两位数标签正则表达式如果只取第一个字符就会判成1。解决解析完后强制打印table对照检查确认每张图的标签是对的。正则用^\d匹配完整连续数字别用固定取一位。如果标签位数可能超过一位解析后还要看max(labels)是否等于实际类别数减1。5.3 特征图尺寸算错全连接层维度爆炸现象网络定义时fullyConnectedLayer(120)前面那层输出的特征图尺寸跟120对不上报错说Layer fc1 is invalid. The input size is 256 but expects 120之类的信息。原因卷积和池化的叠加效果没有手算清楚。比如输入不是28x28而是32x32或者池化层的Stride写成了默认值1都会导致最终展平后的维度跟预期不同。解决用公式一层一层算。我最常用的方法是写个小脚本每定义一层就输出一下该层可接受的最小输入尺寸或者直接按第2章的表格核对数字。算不清的时候就把卷积padding设成Same这样尺寸减半的规律更简单。5.4 GPU显存不足别慌先调批量大小现象使用GPU训练时训练刚开始就提示out of memory on the device或CUDA out of memory。原因MiniBatchSize偏大加上训练过程中的中间激活值缓存占满了显存。手写数字图片虽小但卷积层的中间张量累积起来也不少。解决最直接的办法是把MiniBatchSize从64降到32再降到16直到GPU能容纳。还可以在trainingOptions里加ExecutionEnvironment, cpu强制用CPU训练对小数据集来说速度慢几十秒但绝对稳定。5.5 样本太少训练精度虚高怎么处理现象训练集准确率接近100%但换一批手写图片测试准确率掉到60%以下损失曲线在训练后期明显分离。原因这份资源里的图片数量太少模型把训练样本的细节直接背下来了没有学到可泛化的数字特征。这在深度学习里叫过拟合小数据集上几乎是必然发生。解决先把样本按7比3切出独立验证集别拿训练集精度当最终指标。然后上第4章的数据增强加大L2正则。如果数据集实在太小就把这个资源当流程模板换成完整MNIST数据集再训练。6. 验证与部署用混淆矩阵和predict把模型用起来6.1 用confusionchart看错在哪训练完了先别急着看整体准确率那样的信息粒度太粗。confusionChart函数能画出混淆矩阵一眼看清模型把哪两个数字搞混了YPred classify(net, augImds); YTrue imds.Labels; figure; confusionchart(YTrue, YPred);confusionchart接收真实标签和预测标签自动绘制表格对角线上的数字是分类正确的样本数非对角线上的数字就是错误组合。手写数字任务里最经典的混淆是2和8、4和9、3和5因为它们的局部结构确实相似。看到混淆矩阵之后再去针对性补充样本比盲目调参有效得多。6.2 把模型封装成函数批量识别新图片训练好的网络对象可以直接放进函数里复用。我一般写一个predictDigit函数输入图片路径输出预测数字这样就能脱离训练脚本单独跑function digit predictDigit(net, imgPath) img imread(imgPath); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, [28 28]); img im2double(img); scores predict(net, img); [~, idx] max(scores); digit idx - 1; endpredict返回的是每个类别的概率向量idx是最大概率的下标因为类别是用0到9编码的所以数字标签是idx-1。im2double把像素归一化到0到1之间这一步很重要训练时imageDatastore读出的数据也是归一化后的推理时保持一致才能保证效果。我第一次跑通这个资源时只盯着训练准确率觉得上90%就算完成任务。后来把模型拿到真实场景里测了一组新图片才发现2和8经常互换4的识别率也偏低。看混淆矩阵才发现问题集中在局部轮廓相似的类别上。从那以后我每次训练完CNN都会强制走一遍独立测试集加混淆矩阵再决定要不要调参这个习惯帮我避免了很多看起来很好用起来很差的模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表