资讯动态

基于注意力机制的CNN数据分类MATLAB实现与精度提升指南

发布时间:2026/10/18 22:03:10 来源:尧图企业网站定制
简介采用基于注意力机制的卷积神经网络CNN-attention实现数据分类的Matlab代码包面向计算机、电子信息工程、数学等专业学生适用于课程设计、期末大作业与毕业设计。代码整体采用参数化编程参数位置明确、方便更改思路清晰、注释明细兼容Matlab 2014/2019a/2024a可直接运行附带的xlsx案例数据集完成模型训练与测试帮助快速掌握CNN与注意力机制的结合方式。包内共6个文件包含1个m主程序、1个xlsx数据集、4个png网络结构或结果可视化图整体约162KB结构紧凑便于对照理解网络设计。目前已有68人学习下载。借助完整可运行的主程序与配套数据使用者可替换自身数据开展实验并参考可视化图片检验分类效果整套内容适合系统完成深度学习数据分类项目的初学者和进阶者。1. 基于注意力机制的CNN分类为什么你的模型卡在精度瓶颈上数据分类做到最后一公里CNN往往像个黑匣子损失在降、曲线在走但某些类别就是分不清或者样本一多精度就上不去。这个项目标题——基于注意力机制的卷积神经网络CNN-attention实现数据分类附matlab代码——解决的就是这类问题。注意力机制不是玄学它做的事情很朴素让网络在每层特征里学会“该看哪里”把通道和空间上真正有用的信息放大没用的直接压低。对于工业信号分类、故障诊断、光谱识别这类数据形态固定的任务attention模块往往能把卡在85%的准确率往上推几个点。这个方向适合两类人一是已经用CNN做过分类但精度上不去的人二是想在MATLAB里快速搭一个带注意力机制的分类基线、不想碰Python框架的人。下面从原理讲到训练避坑最后落到注意力可视化。2. 注意力机制的本质CNN里的注意力到底在放大什么2.1 通道注意力与空间注意力两种最主流的注意力形态注意力机制最早出圈是在自然语言处理领域但放到CNN里它解决的核心问题只有一个特征图里哪些通道、哪些位置对最终分类真正有贡献。拿一维信号分类来举例。输入是一段1024个采样点的振动信号经过第一层卷积后输出特征图的尺寸是[512, 1, 16]16就是通道数。这16个通道里可能只有3个通道捕捉到了故障冲击的特征另外13个通道对应的是噪声和无关成分。普通CNN的做法是把16个通道一视同仁地往后传注意力机制的做法是给每个通道学一个权重有用的通道权重放大到1.2无用的压到0.3。这就是SENet提出的Squeeze-and-Excitation注意力也就是标题里“attention”最常见的落地形态。空间注意力则是另一个维度它关注的是特征图上的位置信息。比如图像分类里物体只占画面中央空间注意力会学着把边缘区域的权重压低。对数据分类场景来说如果你做的是带噪信号分类空间注意力能自动聚焦到冲击发生的时段。CBAM这类模块把通道注意力和空间注意力串起来用在图像任务里很常见。但针对数据分类我的建议是优先上通道注意力实现简单、不容易翻车空间注意力对信号类数据收益相对有限。2.2 注意力模块放在哪里三种布局的取舍注意力模块的摆放位置直接影响收敛速度和最终精度。我见过不少人在这个环节踩坑把attention当作万能膏药到处贴结果训练时间翻倍、精度反而掉了。常见的布局有三种各有各的适用场景。布局方式做法适用场景训练成本尾部单模块在最后一个卷积层后接一个SEBlock小模型、特征维度低极小主干插入每隔两个卷积块插入一个SEBlock深层网络、多通道特征中等并联辅助attention模块与主干并行结果相加特征互补性强的任务较高我一般会先在最后一个卷积层后面加一个SEBlock跑基线如果提升明显再往前面隔层插入。不要一上来就每个block都加否则梯度流动路径变长反向传播更容易出现梯度消失。前两轮实验用尾部单模块的布局能最快判断attention对这个任务有没有用。2.3 什么数据形态适合CNN-attention选型判断注意力机制不是万能的。根据我实践下来的经验有三类数据形态用attention收益最大。第一类是通道语义明确的数据比如多传感器采集的振动信号每个通道对应一个测点通道注意力学的是“哪个测点更重要”。第二类是信噪比低的数据信号被强噪声淹没时attention能自动把噪声通道压下去这个效果在故障诊断里特别明显。第三类是类别差异局部化的数据比如两类信号只在某个频段有差异空间注意力能定位到那个频段。反过来如果你的数据本身已经是强特征、低噪声、类别差异明显的干净数据CNN不加attention也已经分得很好加模块只是徒增参数量和过拟合风险。判断方法也简单先用普通CNN跑到收敛如果验证精度已经超过95%attention大概率帮不了你如果在85%到93%之间徘徊那基本是特征利用不充分值得把attention加进去试一轮。3. 在MATLAB里复现CNN-attention从数据准备到训练闭环3.1 数据读取与预处理标签、归一化与训练集划分MATLAB里搭CNN-attention第一个要理清的是数据格式。我见过不少人在这里卡住读进来的数据是table格式标签是categorical数组直接塞给trainNetwork就报错。先看一个标准的数据读取流程% 读取CSV格式数据每行一个样本最后一列为标签 data readmatrix(train_data.csv); X data(:, 1:end-1); % 转置为 [特征维度, 样本数] Y data(:, end); % 将标签转为categorical类型 Y_cat categorical(Y); % 数据归一化按特征维度做zscore标准化 mu mean(X, 2); sigma std(X, 0, 2); X_norm (X - mu) ./ sigma; % 按8:2划分训练集和验证集 rng(42); cv cvpartition(Y_cat, HoldOut, 0.2); X_train X_norm(:, training(cv)); Y_train Y_cat(training(cv)); X_val X_norm(:, test(cv)); Y_val Y_cat(test(cv));这段代码做了三件关键的事情。第一数据转置成“特征维度×样本数”的格式这是MATLAB深度学习工具箱的统一要求任何网络层输入的最后一维都必须是样本维度。第二归一化用的是按行计算均值和标准差也就是说每个特征独立标准化而不是对整个矩阵算一个均值和方差后者会抹掉特征间的尺度差异。第三cvpartition按标签比例划分数据集防止某一类样本全部落到训练集或验证集。这里有个容易被忽略的参数rng(42)。这个随机种子必须固定否则每次运行划分结果不同前后两次实验精度没办法对比。我习惯把随机种子放在数据划分之前、网络初始化之前保证整条实验链路可复现。3.2 用dlarray实现SEBlock一个函数搞定通道注意力MATLAB里实现注意力模块有两条路一条是用自定义Layer类封装另一条是用dlnetwork加模型函数。我强烈建议第二条路原因很实际自定义Layer类要把forward和backward都实现backward里矩阵求导写错一个维度就是灾难排查成本极高。而dlnetwork自带自动微分只需要写清楚前向计算逻辑梯度由工具箱自动算。看代码function Z seBlock(X, fc1, fc2) % X: 输入特征图格式为 [高度, 宽度, 通道数, 样本数] % fc1, fc2: 全连接层参数用dlnetwork的Learnables传进来 [~, ~, C, N] size(X); % Squeeze: 全局平均池化把每个通道压缩成一个标量 % 对高度和宽度两个维度求均值得到 [1, 1, C, N] w squeeze(mean(X, [1 2])); % 结果维度 [C, N] % Excitation: 两个全连接层组成瓶颈结构 w fullyconnect(w, fc1); % 降维到 C/r w relu(w); % 非线性 w fullyconnect(w, fc2); % 升维回 C w sigmoid(w); % 归一化到 [0, 1] % 还原维度和原特征图做逐通道乘法 w reshape(w, [1 1 C N]); Z X .* w; end这段代码的核心逻辑只有六行。Squeeze操作对特征图做全局平均池化把每个通道的空间信息压缩成一个数值Excitation操作通过两个全连接层学习通道间的依赖关系第一个全连接把通道数压到原来的四分之一第二个全连接再升回来这个瓶颈结构是为了减少参数量、防止过拟合。最后用sigmoid把权重约束在0到1之间再对原特征图逐通道缩放。这里要特别注意fullyconnect函数的输入格式它期望的输入是[特征维度, 样本数]的dlarray。代码里squeeze(mean(X, [1 2]))的结果正是[C, N]和fullyconnect的参数正好对上。如果你从别的教程里复制来的代码在维度上做了转置运行就会出现“输入维度不匹配”的报错这是最常见的翻车点。3.3 搭建CNN-attention主网络卷积加SEBlock加分类头有了SEBlock函数接下来就是把整个网络串起来。我用的是dlnetwork加自定义模型函数的方式整体结构包括输入层、两个卷积块、一个SEBlock、一个全连接分类头。看构建代码% 定义网络结构 layers [ imageInputLayer([1024 1 1], Normalization, none) convolution2dLayer([32 1], 16, Padding, same, Name, conv1) reluLayer(Name, relu1) convolution2dLayer([16 1], 32, Padding, same, Name, conv2) reluLayer(Name, relu2) ]; % 初始化dlnetwork net dlnetwork(layers); % 给SEBlock初始化两个全连接层的参数 % 通道数为32reduction ratio设为4瓶颈维度为8 fc1 dlarray(randn(8, 32) * 0.01, cb); fc2 dlarray(randn(32, 8) * 0.01, cb); bias1 dlarray(zeros(8, 1), cb); bias2 dlarray(zeros(32, 1), cb);这里的核心设计是将输入信号视作一张“高1024、宽1”的单通道图像用imageInputLayer接收。两个卷积层的卷积核分别是[32 1]和[16 1]只在时间维度上滑动这比直接把信号拉平输入全连接层更能提取局部模式。第二个卷积层的输出通道数是32对应SEBlock的通道数。全连接层的参数用randn初始化标准差取0.01而不是默认的1。这个细节很关键attention模块的初始权重如果太大sigmoid输出的权重会饱和到0或1附近导致前几轮迭代梯度接近零整个模块学不动。用小标准差初始化能让初始权重落在sigmoid的线性区训练起步更稳。3.4 训练循环与参数设置自定义训练让注意力可观测自定义训练循环比trainNetwork多写几行代码但好处是训练过程中的中间变量都能拿到比如每次迭代的注意力权重。这个对后面可视化Attention很有用。标准训练代码如下% 将训练数据转为dlarray格式为 [高度, 宽度, 通道, 样本] X_dl dlarray(X_train, SSCB); X_dl reshape(X_dl, [1024, 1, 1, size(X_train, 2)]); % 初始化Adam优化器参数 trailingAvg []; trailingAvgSq []; learnRate 0.001; numEpochs 30; miniBatchSize 64; % 训练循环 for epoch 1:numEpochs % 打乱数据顺序 idx randperm(size(X_dl, 4)); X_dl X_dl(:, :, :, idx); Y_train Y_train(idx); for step 1:floor(size(X_dl, 4) / miniBatchSize) batchIdx (step-1)*miniBatchSize 1 : step*miniBatchSize; X_batch X_dl(:, :, :, batchIdx); Y_batch Y_train(batchIdx); % 计算损失和梯度 [loss, gradients] dlfeval(modelLoss, net, X_batch, Y_batch, fc1, fc2, bias1, bias2); % Adam更新参数 [net, fc1, fc2, bias1, bias2, trailingAvg, trailingAvgSq] adamupdate(... net, gradients, trailingAvg, trailingAvgSq, epoch*floor(size(X_dl,4)/miniBatchSize)step, learnRate); end % 每个epoch结束后打印损失 disp([Epoch num2str(epoch) , Loss: num2str(extractdata(loss))]); end学习率设定为0.001是Adam优化器的默认推荐值但这个值在自定义网络里经常需要调。loss下降太慢就调大到0.003震荡不收敛就调小到0.0005。miniBatchSize设为64是个平衡值太小的话梯度噪声大训练曲线抖动明显太大显存占用高而且每个epoch的更新次数少收敛变慢。这段代码里有个隐藏的逻辑adamupdate函数的输入参数中有一个是迭代次数我使用的是epoch乘step的全局迭代数而不是每个epoch重置为1。如果错误地把迭代次数写成stepAdam的动量估计会异常后期loss会出现周期性的锯齿波动。这个问题排查起来很费劲但原因就这么简单。4. 训练完成之后曲线判读、混淆矩阵与模型落地4.1 损失曲线怎么读收敛了不等于学对了训练跑完第一件事不是看验证精度而是看训练曲线。我把常见的情况归纳为三类正常收敛、假收敛、震荡不收敛。正常收敛的曲线特征是前10个epoch快速下降后面逐步平缓验证损失和训练损失差距保持在较小范围。假收敛的特征是训练损失降得很快但验证损失在某个点之后开始回升训练和验证曲线出现“开口”这是典型的过拟合信号。震荡不收敛的特征是损失曲线像锯齿一样上下跳动经常伴随NaN出现。区分这些情况的意义在于决定下一步动作。假收敛说明模型容量过大或数据量不足需要加dropout、减小网络宽度、或者做数据增强。震荡不收敛多半是学习率偏高直接调低0.1倍重跑。我见过有人把训练跑了20个epoch看到loss曲线还没稳定就断定模型有问题其实再跑10个epoch就收敛了这种判断为时过早。4.2 混淆矩阵与单类精度总体精度会骗人分类任务只报一个总准确率是最容易误导自己的做法。我处理过的一个模拟项目X总体精度93%看起来不错打开混淆矩阵发现某一类别的识别率只有60%全被分到了另一类。原因是这个类别的训练样本本身就少普通CNN学不到它的特征attention模块把它识别出的微弱差异放大了而已。% 在验证集上推理得到预测标签 Y_pred predict(net, X_val_dl); [~, Y_pred_label] max(extractdata(Y_pred), [], 1); % 绘制混淆矩阵 figure; confusionchart(Y_val, categorical(Y_pred_label));混淆矩阵要按行看召回率、按列看查准率。如果发现某一类召回率低但查准率高说明模型对这个类的判断保守宁可不认也不误判如果召回率和查准率都低那就是这个类别在特征空间里本来就分不开需要考虑加特征或换网络结构。attention模块对这种类别的帮助有限因为注意力再强输入里没有区分性信息也是白搭。4.3 模型保存与部署结构参数和网络参数要一起存训练完成的网络要落地使用保存和加载不能只存权重。dlnetwork是结构定义加参数存储的组合体我用save命令保存整个变量% 保存完整网络和attention参数 save(cnn_attention_model.mat, net, fc1, fc2, bias1, bias2, mu, sigma); % 推理时加载并进行一条新样本的预测 loaded load(cnn_attention_model.mat); new_data (new_sample - loaded.mu) ./ loaded.sigma; new_dl dlarray(reshape(new_data, [1024 1 1 1]), SSCB); result predict(loaded.net, new_dl);这里必须单独保存mu和sigma这两个归一化参数。推理时新样本要用训练集相同的均值方差做标准化否则输入分布不一样模型输出完全不可信。有个血泪教训我早期用过一次把归一化参数放在训练脚本里结果部署时训练脚本找不到了只能重新训练模型。从那以后我把mu和sigma直接跟模型存到同一个mat文件里省掉这层风险。5. CNN-attention避坑指南五个让模型翻车的常见问题5.1 问题一训练loss直接变NaN现象训练迭代到第几次时loss突然变成NaN之后再也恢复不了。原因最常见的是学习率过大导致梯度爆炸尤其是attention模块里全连接层的权重更新过快。第二个常见原因是输入数据里有NaN值归一化时某个特征的标准差为0除以0之后产生NaN。解决先检查数据里有没有NaN用any(isnan(X))排查。数据没问题就把学习率从0.001下调到0.0005重跑。如果仍然NaN检查sigmoid函数的输入Excitation阶段全连接输出值过大sigmoid会饱和梯度接近0反向传播时上游梯度乘以接近0的值会出问题。给全连接层权重初始化标准差从0.01再调小到0.001。5.2 问题二fullyconnect维度不匹配报错现象运行到seBlock函数时报错提示输入维度与全连接层权重维度不匹配。原因fullyconnect函数期望输入格式是[特征维度, 样本数]但mean(X, [1 2])输出的维度在不同版本的MATLAB里可能不同。如果X是单样本squeeze之后会丢掉维度变成[C]而不是[C, 1]。解决不要直接用squeeze改用permute显式控制维度顺序w mean(X, [1 2]); w permute(w, [3 4 1 2]); % 变成 [C, N, 1, 1] w reshape(w, [C N]); % 再压成 [C, N]这个写法在任何维度情况下都不会出错建议作为标准写法固定下来。5.3 问题三加了attention之后精度反而下降现象普通CNN验证精度92%加了SEBlock之后掉到89%训练时间还变长了。原因数据量小的情况下attention模块引入的额外参数导致过拟合。SEBlock里的两个全连接层虽然带瓶颈结构但在一万样本以下的分类任务里额外的参数足以让模型记住训练集中的噪声。解决把reduction ratio从4调大到16让瓶颈更窄参数量大幅下降。如果还不行在SEBlock之前加一层dropout概率设为0.3。另一个办法是给attention模块加L2正则化权重衰减系数从默认的0.0001调到0.001。从另一个角度说如果数据本身特征已经很强去掉attention模块反而更好这不是模型的问题是选型判断的问题。5.4 问题四验证集精度比训练集高现象训练精度90%验证精度93%看起来很好但总觉得不对劲。原因训练过程中dropout或batchnorm的行为在训练和推理时不同。dlnetwork的predict函数默认走推理路径不应用dropout这是正常的。但另一个情况是数据划分有问题验证集和训练集分布不一致或者划分时用了未固定随机种子导致验证集恰好是简单样本。解决检查划分种子是否固定检查每一类样本在训练集和验证集中的占比是否接近整体占比。用cvpartitionBySize和Stratify参数能保证分层划分。正常的预期是训练精度略高于验证精度如果长期倒挂优先检查数据划分而不是模型。5.5 问题五保存再加载后预测结果不一致现象训练完直接推理结果全对保存后重新加载模型推理同一批样本结果乱了。原因保存时只保存了网络权重没有保存归一化参数mu和sigma加载后用错误的标准化方式处理了新数据。或者保存的net对象里某些自定义参数没有包含在内。解决统一用一个结构体打包所有内容model_data.net net; model_data.fc1 fc1; model_data.fc2 fc2; model_data.bias1 bias1; model_data.bias2 bias2; model_data.mu mu; model_data.sigma sigma; save(cnn_attention_complete.mat, model_data);加载时整个结构体读进来推理脚本只依赖model_data这个变量不依赖任何外部脚本里的临时变量。这样换机器、换路径都不会出问题。6. 进阶把注意力权重视觉化让分类决策不再是黑匣子注意力机制最大的价值不只是提精度而是它留下了可解释的中间产物——通道权重。这个权重向量直接告诉你在分类那一刻网络主要依靠哪些特征通道做判断。可视化代码很直接% 对验证集中某一条样本做前向计算 sample_dl X_val_dl(:, :, :, 1); [Y_pred, act] predict(net, sample_dl); % 在模型函数里返回SEBlock的输出权重 % 假设模型函数为 modelWithAttention [Y_pred, attn_weight] modelWithAttention(net, sample_dl, fc1, fc2, bias1, bias2); attn_value extractdata(attn_weight); % 绘制通道注意力权重 figure; bar(attn_value); xlabel(通道序号); ylabel(注意力权重); title(SEBlock输出的通道注意力权重); % 加上阈值线权重高于0.7的通道认为是关键通道 yline(0.7, r--, 关键通道阈值);这里最关键的一步是修改模型函数把seBlock这层的输出值返回出来。在forward计算过程中seBlock内部计算出的权重w就可以作为一个额外的返回值。这样每个测试样本都能得到一条注意力曲线。我习惯在训练集上随机抽50个样本统计每一类的平均注意力分布。如果同一类别的样本注意力分布高度一致说明模型学到了稳定的特征模式如果注意力分布杂乱无章说明模型没有形成稳定的分类依据这时的训练精度即使高换个数据分布就会掉下来。这个检查对故障诊断这类任务尤其重要。我现在每次训练完模型都会先看注意力分布再决定是否部署这个习惯帮我避开过多次模型泛化能力不足的隐患。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑