
简介基于注意力机制的CNN-attention数据分类Matlab实现面向计算机、电子信息工程、数学等专业学生适用于课程设计、期末大作业与毕业设计。资源压缩包共6个文件以可直接运行的main.m主程序为核心附数据集.xlsx及4张结构或结果示意图整体仅162KB轻量易用且兼容Matlab 2014、2019a、2024a等多个版本。已有68人浏览学习。代码采用参数化编程关键参数便于修改注释详细能帮助读者理解卷积神经网络与注意力机制的结合方式附带案例数据可直接导入运行适合从零上手深度学习分类任务。通过实际运行与调试读者可掌握CNN-attention建模流程、Matlab深度网络搭建思路并能将方法迁移至自己的数据集是一份兼具教学与实操价值的入门资料。1. 基于注意力机制的卷积神经网络 CNN-attention数据分类卡在基准线上时这是最值得先试的一味药用 CNN 做数据分类最容易遇到的情况不是跑不起来而是精度卡在一个不上不下的位置加深网络、换激活函数、调 dropout折腾一圈涨不了零点几个百分点。基于注意力机制的卷积神经网络也就是 CNN-attention在这个阶段几乎是性价比最高的改法。它不换主干只给特征图加一个可学习的通道加权门控让模型自己决定哪些特征值得放大、哪些该忽略。这篇文章按我实际做方案的经验顺序来写为什么需要注意力、CNN 主干怎么搭、SE 注意力在 MATLAB 里的实现与训练以及几个高频翻车点。适合正在用 MATLAB 做信号或图像分类、手里已经有一个能跑出基线的模型的工程师。2. 为什么需要注意力CNN 在分类任务里的三个结构性局限2.1 卷积的局部偏好感受野决定它天然看不到全局CNN 的基本结构是卷积核在局部窗口内滑动3×3 或 5×5 的核只看得到周围一小块。堆叠多层可以扩大感受野但这种扩大是隐式的、间接的。分类任务里真正决定类别的特征往往不在某个局部纹理里而藏在“哪个通道更重要”这种全局关系里。比如信号分类中高频段的某个窄带特征和整个信号的能量分布共同决定类别单靠卷积核的局部扫描很难把这种全局关系直接表达出来。这也是 CNN 和 RNN 在处理序列时的一个关键差异RNN 天然按时间步累积上下文CNN 则需要不断堆层才能覆盖长距离依赖。你可以在主干后面强行堆到很深来“看全”但代价是参数翻倍、训练变慢、过拟合风险加大。注意力机制解决的正是这个问题——它不扩大感受野而是给每个通道或每个空间位置算一个可学习的权重告诉分类头“这一路特征应该信多少”。跟 SVM 这类传统分类器相比CNN 的优势是自动提特征但自动提出来的特征之间没有优先级。最后那个池化层把所有特征图压成一个向量每个通道对分类的贡献被一视同仁。注意力机制要做的就是在池化之前按通道重要程度重新分配权重。2.2 三类注意力选型通道、空间、混合注意力怎么选常见做法是分三类通道注意力、空间注意力和混合注意力。通道注意力以 SESqueeze-and-Excitation为代表做法是全局平均池化后接两个全连接输出每个通道的缩放系数。计算量很小效果稳定适合大多数分类任务。空间注意力关注“在哪里看”输出一张和特征图同尺寸的权重图适合目标位置分散的数据。混合注意力两者都做典型代表是 CBAM效果好但参数翻倍。如果你做的是坐标信息敏感的输入还可以考虑 coordinate attention它在通道注意力基础上把位置信息编码进去。我的选型经验先用通道注意力打底。原因很简单分类任务里通道维度的信息冗余往往比空间维度严重而且 SE 的实现和调参成本最低。如果加了 SE 之后涨幅不理想再叠加空间注意力而不是一上来就上 CBAM。下面表格是三个参数的直观对比。类型代表做法计算开销适用场景通道注意力SE低通道数多、特征冗余大的分类空间注意力卷积生成空间权重图中目标位置分散、噪声多混合注意力CBAM中高精度优先、显存充足2.3 在 MATLAB 里实现 attention 的优势与代价MATLAB 里做注意力机制通常不需要像 Python 那样维护一堆依赖。Deep Learning Toolbox 自带dlnetwork、dlarray、dlgradient这套自定义训练循环工具链RNN、CNN 加上手写的可微函数都可以拼在同一张计算图里。新版本对自定义训练循环的语法越来越友好在线版也能跑但做自定义训练循环建议在本地环境跑版本太旧会缺函数。代价是MATLAB 的自定义层classdef … nnet.layer.Layer需要自己写反向传播这一点比 Python 的nn.Module麻烦很多。所以我一般不用自定义层而是把注意力逻辑写成一个普通函数在训练循环里用dlgradient自动求梯度。这样一个函数就能把通道加权的计算图接进 CNN 主干省掉手写 backward 的过程。具体实现就是下一章要展开的内容。3. MATLAB 从零搭 CNN-attention从数据预处理到 SE 注意力块落地3.1 数据进模型之前归一化、维度重组与训练/验证划分假设你手里有一份已经存成.mat的数据X的形状是[H, W, C, N]Y是[N,1]的整数标签。第一步不是直接跑网络而是做两件事先划分数据再算归一化参数。顺序错了后面会很麻烦尤其是归一化参数如果混进验证集信息验证精度会虚高这一点我在第五章再细说。% 加载数据X: [H, W, C, N]Y: [N,1] load(data.mat); % 固定随机种子保证后续划分和打乱可复现 rng(2024); idx randperm(size(X,4)); X X(:,:,:,idx); Y Y(idx); % 按 70 / 15 / 15 划分训练、验证、测试 nAll size(X,4); nTrain round(0.70 * nAll); nVal round(0.15 * nAll); XTrain X(:,:,:, 1:nTrain); YTrain Y(1:nTrain); XVal X(:,:,:, nTrain1:nTrainnVal); YVal Y(nTrain1:nTrainnVal); XTest X(:,:,:, nTrainnVal1:end); YTest Y(nTrainnVal1:end); % 只看训练集统计量验证集和测试集复用同一组参数 mu mean(XTrain, [1 2 4]); % 在 H、W、N 三个维度上求均值 sigma std(XTrain, 0, [1 2 4]); % 同上求标准差保留通道维 XTrain (XTrain - mu) ./ (sigma eps); XVal (XVal - mu) ./ (sigma eps); XTest (XTest - mu) ./ (sigma eps);这段代码里有两个关键参数要解释一下。mean和std的维度向量[1 2 4]表示对高度、宽度和样本数三个维度统计通道维C被保留意思是每个通道单独归一化。后面加eps是为了防止某个通道标准差为 0 时除出 NaN数据量小的时候这个坑很常见。维度重组也是很多人踩坑的地方。MATLAB 的dlnetwork默认接受SSCB布局也就是H×W×C×N。如果你的原始数据是一维信号比如长度T的单通道波形那就把H设为 1W设为T输入变成[1, T, 1, N]。不要在dlarray这一步才做重组提前用普通数组排好形状后面调试时能少很多麻烦。3.2 CNN 主干怎么搭卷积层、批归一化、池化的参数设置接着搭 CNN 主干。我做分类任务时习惯用一个简洁的四层结构两个卷积块加上各自的最大池化。注意力机制要插在主干之后、分类头之前所以主干只需要负责提特征不要接全连接层。全连接层放到注意力之后单独管理。layers [ imageInputLayer([H W C], Name, input, Normalization, none) convolution2dLayer([3 3], 32, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer([2 2], Stride, 2, Name, pool1) convolution2dLayer([3 3], 64, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer([2 2], Stride, 2, Name, pool2) ]; lgraph layerGraph(layers); net dlnetwork(lgraph);第一层imageInputLayer的Normalization必须设为none因为已经在 3.1 手动归一化了如果这里再归一化一次等于重复处理而且验证集用的是训练集统计量这里再算一次会破坏这个逻辑。卷积层第一层输出 32 个通道第二层翻倍到 64这是分类 CNN 最常见的做法越往深层通道数越多空间分辨率越小。maxPooling2dLayer([2 2], Stride, 2)会把特征图的宽高各压缩一半。如果你的数据是1×T×C×N这种一维形态需要把卷积核改成[1 3]池化核改成[1 2]否则 MATLAB 会在H1的维度上报维度错误。这一步是新手最容易翻车的点看到报错先检查池化核是不是想当然用了默认 2×2。3.3 SE 注意力块的实现可学习参数如何接入计算图SE 注意力的核心思路是先对特征图做全局平均池化把每个通道压成一个标量然后过两个全连接最后用 sigmoid 输出 0 到 1 之间的缩放系数。这里的关键是用fullyconnect而不是常规的全连接层因为它直接接收C×N布局的dlarray适合在自定义训练循环里当函数调用。function Z seBlock(X, ratio, W1, B1, W2, B2) % X : H×W×C×N 的 dlarraySSCB 布局 % ratio: 通道压缩比一般取 4~8 C size(X, 3); reduced max(1, floor(C / ratio)); % 全局平均池化得到 1×1×C×N gp mean(X, [1 2]); % 转成 C×N 布局供 fullyconnect 使用 gpVec permute(gp, [3 4 1 2]); % 第一个全连接C - reduced升维前的信息压缩 s fullyconnect(gpVec, W1, B1); s relu(s); % 第二个全连接reduced - C恢复原始通道数 s fullyconnect(s, W2, B2); s sigmoid(s); % 还原成 1×1×C×N利用广播对每个通道缩放 s reshape(s, 1, 1, C, size(X, 4)); % 门控输出 Z X .* s; endreduced是压缩后的通道数等于原始通道数除以ratio。这里要特别注意如果你的主干最后一层是 64 通道ratio取 4reduced就是 16如果取 16reduced就是 4信息压缩过度会导致掉点。等通道数少的时候比如 32 通道ratio16压成 2 个通道基本不可用。这个参数我一般从 4 开始试。permute(gp, [3 4 1 2])这一行很多人看不懂。gp的布局是1×1×C×N而fullyconnect要求输入是C×N所以要把通道维放到第一维、样本维放到第二维。MATLAB 的dlarray在做这种维度重排时会保留维度标签后面计算梯度时不会断链。注意力和主干之间不需要额外连接层。X .* s是一个逐元素乘s是1×1×C×NMATLAB 的隐式扩展会让它沿 H 和 W 方向广播相当于对整张特征图的每个通道统一缩放。这是通道注意力的标准做法比加残差支路更直接。若想让注意力退化成恒等映射可以把这行改成X X .* s属于进阶调法。SE 的可学习参数要用dlarray初始化不能直接用普通数组。初始化的方差要小否则训练初期注意力会把特征图压到接近 0。我的惯例是第一个全连接用0.01量级第二个用 He 初始化。reduced 16; % 根据实际通道数调整 W1 dlarray(randn(reduced, 64) * 0.01); B1 dlarray(zeros(reduced, 1)); W2 dlarray(randn(64, reduced) * sqrt(2/reduced)); B2 dlarray(zeros(64, 1));3.4 前向与损失函数用 dlgradient 一次拿到全部梯度注意力参数不能挂在dlnetwork的 Layer 列表里所以它不在net.Learnables中。要让自动微分同时更新主干和 SE 参数做法是让modelLoss作为dlfeval的传入函数内部用dlgradient一次性对所有参数求梯度。function [loss, gradsNet, gradsSe] modelLoss(net, X, Y, ratio, seParams) % seParams: {W1,B1,W2,B2,Whead,Bhead} 的 cell 数组 W1 seParams{1}; B1 seParams{2}; W2 seParams{3}; B2 seParams{4}; Whead seParams{5}; Bhead seParams{6}; % CNN 主干提取特征 feat predict(net, X); % SE 注意力加权 featAtt seBlock(feat, ratio, W1, B1, W2, B2); % 全局平均池化后接分类头 gp mean(featAtt, [1 2]); % 1×1×C×N gpVec permute(gp, [3 4 1 2]); % C×N scores fullyconnect(gpVec, Whead, Bhead); % K×N % 交叉熵损失 loss crossentropy(scores, Y); % 同时求主干和 SE 参数的梯度 [gradsNet, gW1, gB1, gW2, gB2, gHeadW, gHeadB] ... dlgradient(loss, net.Learnables, W1, B1, W2, B2, Whead, Bhead); gradsSe {gW1, gB1, gW2, gB2, gHeadW, gHeadB}; end这段代码里有个逻辑需要理解crossentropy(scores, Y)要求scores是K×N布局Y是 one-hot 编码后的K×N。所以在 4.1 准备 batch 时Y 要做 one-hot 转换不能直接用整数标签。dlgradient可以同时接受多个参数输入这里把net.Learnables和 SE 的六个参数都传进去返回值数量必须和输入参数数量严格对应。分类头权重Whead、Bhead也放在seParams里一起更新这样整个模型只有两个更新入口一个dlnetwork主干一个 SE 加分类头的 cell 数组。4. 训练配置与超参数调试让 attention 真正涨点的三个旋钮4.1 训练循环怎么写adamupdate 与两类参数更新模型定义好之后训练循环是另一个容易写乱的地方。因为 SE 参数不在dlnetwork里所以adamupdate要分两次调用一次更新主干一次更新 SE 加分类头。% 超参数 ratio 4; learnRate 2e-3; numEpochs 30; miniBatchSize 16; % SE 和分类头参数 numClasses length(unique(YTrain)); reduced 16; seParams { dlarray(randn(reduced, 64) * 0.01), ... dlarray(zeros(reduced, 1)), ... dlarray(randn(64, reduced) * sqrt(2/reduced)), ... dlarray(zeros(64, 1)), ... dlarray(randn(numClasses, 64) * 0.01), ... dlarray(zeros(numClasses, 1)) }; % Adam 状态变量空数组代表第一次迭代 avgGradNet []; avgSqGradNet []; avgGradSe []; avgSqGradSe []; numIterPerEpoch floor(size(XTrain,4) / miniBatchSize); globalIter 0; for epoch 1:numEpochs for i 1:numIterPerEpoch globalIter globalIter 1; idx (i-1)*miniBatchSize 1 : i*miniBatchSize; XBatch dlarray(XTrain(:,:,:,idx), SSCB); YBatch onehotencode(YTrain(idx), 2, ClassNames, 1:numClasses); YBatch dlarray(YBatch, CB); [loss, gradsNet, gradsSe] dlfeval(modelLoss, ... net, XBatch, YBatch, ratio, seParams); % 分开更新两类参数 [net, avgGradNet, avgSqGradNet] ... adamupdate(net, gradsNet, avgGradNet, avgSqGradNet, globalIter, learnRate); [seParams, avgGradSe, avgSqGradSe] ... adamupdate(seParams, gradsSe, avgGradSe, avgSqGradSe, globalIter, learnRate); end % 每个 epoch 结束在验证集上算一次精度方便看趋势 valAcc computeAccuracy(net, seParams, XVal, YVal, ratio); fprintf(Epoch %d, loss %.4f, valAcc %.2f%%\n, epoch, ... extractdata(loss), valAcc * 100); end这段代码是完整的可跑结构有两个细节需要说明。第一个是adamupdate支持把 cell 数组作为第一个参数传入但 cell 里的每个元素必须是dlarray且gradsSe的元素顺序要和seParams完全一致。我把分类头的Whead、Bhead排在 SE 参数后面就是为了在更新和取梯度时都能按同一个顺序遍历。第二个是验证精度不能写在dlfeval里面因为验证不需要梯度直接用普通前向计算即可否则会白白占用显存。4.2 掉点先看这三个参数reduction、残差、dropout加注意力之后如果精度不涨甚至下跌别急着怀疑方法先看这几个参数。参数推荐范围作用与调试方向ratio4~8通道压缩强度通道数越少ratio 越小残差连接可选X .* s改为X X .* s稳定训练dropout0.3~0.5在注意力之后加防止过拟合通道分布ratio是最容易出问题的一个。论文里常写 16但那是基于 ResNet 的大通道数前提。你自己搭的 CNN 主干最后一层往往只有 64 或 32 通道ratio 取 16 会把通道压到 2~4 个注意力机制根本没有足够维度去表达通道关系。我的做法是先用 4如果涨点明显再试 6、8整个过程不超过三次实验。残差连接是在seBlock里把Z X .* s改成Z X X .* s。改成这种形式后s 学习的是“需要额外放大多少”初始值接近 0 时模型退化成普通 CNN训练更平稳。如果你发现加了注意力后在训练集上 loss 下降变慢可以考虑这个改法。dropout 放在注意力之后、全局平均池化之前用来对抗注意力对训练集通道分布的过度记忆。信号分类里样本间差异大注意力很容易记住某几个通道的强激活加 dropout 后模型会更依赖整体通道分布。4.3 数据划分与实验对照保证基线可信调参之前先把实验规范确定下来不然你根本分不清涨点来自注意力还是来自运气。两个老生常谈但经常被忽略的点固定数据划分、多次重复取均值。% 同一个 rng 保证每次实验的数据划分完全一致 rng(2024); trainIdx randperm(nAll, nTrain); valIdx randperm(nAll - nTrain, nVal); % 跑每个配置前都要重置随机种子再初始化网络 rng(2024); net dlnetwork(lgraph);我一般对比同一个配置跑 3 到 5 次记录测试精度的均值和方差。只跑一次就说“注意力涨了 2 个点”是最常见的玄学误判因为 CNN 不同随机种子之间的波动可能就有 1 到 2 个点。注意每次跑之前都要重新rng(2024)并且连数据划分一起固定。还有一个容易忽略的点onehotencode的ClassNames参数必须显式指定否则它按YTrain中出现顺序排序不同 batch 的类别顺序可能不一致导致标签错位但又不报错。这个错位比较隐蔽loss 会下降但精度上不去排查起来很费时间。5. CNN-attention 常见问题排查五条高频翻车记录与解法5.1 加了注意力反而掉点reduction 和层位置是第一嫌疑人现象同样的数据、同样的训练配置在主干后面加上 SE 注意力块之后测试精度不升反降训练 loss 下降速度也变慢。原因基本集中在两个点。一是ratio设置过大通道被压得只剩几个注意力自己都学不到有效信息二是注意力加在了太浅的位置浅层特征图还携带大量空间细节强行做通道压缩会把这些细节抹掉。解决先把ratio调到 4 甚至 2跑一轮看训练 loss 是否恢复。然后确认注意力只加在最后一个卷积块之后不要在每个卷积层后面都加。浅层特征需要保留空间结构通道注意力对它们来说是破坏性的。5.2 梯度死活不更新attention 参数被 extractdata 断了链现象训练循环能跑loss 也正常但打印 SE 层的W1发现数值一直没变或者出现“Undefined function dlgradient”这类报错。原因在modelLoss内部对dlarray调用了extractdata或stripdims把自动微分需要追踪的数据链切断了。注意力参数确实参与了前向计算但梯度无法流回它训练就变成了主干单训。解决检查seBlock和modelLoss里的所有中间变量确保没有extractdata。如果为了打印观察某个中间结果只能在训练循环外单独前向时用不能出现在dlfeval的回调链里。另一个排查办法是在modelLoss里故意把某个seParams{i}乘 2看 loss 是否变化如果不变化说明它根本没接入计算图。5.3 验证 loss 降但测试精度差归一化参数泄漏现象验证集上 loss 一直在降看起来训练很顺利但测试集精度比验证低很多两者差距明显大于正常范围。原因最常见的泄漏是归一化时用了全数据集的均值和方差或者先归一化再划分训练验证集。验证集和测试集的信息提前混进了训练数据模型在验证集上看到的分布早就不“陌生”了测试集才是真正的照妖镜。解决严格遵守 3.1 的顺序——先划分再在XTrain上算mu和sigma然后对验证集和测试集做同样的变换。如果用了数据增强增强后的样本也要确保不包含测试集信息。这个坑在深度学习里面几乎每年都会遇到属于数据科学界的经典血泪教训。5.4 显存突然爆炸attention 放在了空间分辨率最高的浅层现象加入注意力后在训练的第一个 epoch 就报Out of memory而同样的主干不加注意力则能正常训练。原因注意力块的广播乘法X .* s会复制特征图如果把它放在池化之前的浅层特征图尺寸大广播后的中间变量会占掉大量显存。浅层特征图的通道数少但空间大显存消耗比深层更严重。解决把注意力块从浅层移到最后一个池化层之后。那个位置特征图已经是H/4 × W/4空间维度小了四倍。另外一个办法是减小miniBatchSize从 32 降到 16但这不是根治只算后悔药。如果你坚持在浅层加注意力考虑把s先降采样再乘而不是直接对全尺寸特征图广播。5.5 复现不了别人的论文精度随机性没有锁住现象按论文结构复现模型别人测试精度 97%你调了两天还在 93% 附近波动而且每次跑结果还不一样。原因可能是数据划分不同也可能是初始化不同、batch 顺序不同。注意力机制对初始权重比较敏感W1初始化大了模型一开始就震荡训练很难收敛到同一个点。解决把rng固定下来训练前设置rng(2024)初始化用固定种子生成。对比时不要只跑一次至少跑 5 次取均值。你可以把最终模型的Whead和Bhead单独保存成.mat方便后续复现。神经网络的随机性没法完全消掉但固定种子加多次重复能把玄学成分降到最低。6. 验证模型在学特征还是学噪声特征图可视化与错分样本回看6.1 用中间层特征图检查注意力是否有效训练完别急着收工。我有一个习惯先用一个 batch 的测试样本做一次前向把注意力加权前后的特征图分别拿出来对比看注意力有没有把激活集中在少数通道上。这一步能快速区分模型是在学特征还是在学噪声。% 取一个测试样本前向拿到 pool2 的输出 XOne dlarray(XTest(:,:,:,1), SSCB); feat predict(net, XOne, Outputs, pool2); act extractdata(feat); % 可视化前 16 个通道的特征图 for k 1:16 subplot(4, 4, k); imagesc(squeeze(act(:,:,k,1))); colormap gray; title(sprintf(ch %d, k)); end如果注意力有效应该看到少数通道的激活值明显更大大部分通道趋于平坦。如果所有通道看起来都很均匀、没有主次说明注意力没有学到有区分度的通道关系这时先回去调ratio比继续堆层更有用。6.2 注意力权重分布与错分样本回看再看注意力权重的分布。把训练好的 SE 参数单独前向取出s的值画一条曲线横轴是通道编号纵轴是缩放系数。gp mean(feat, [1 2]); gpVec permute(gp, [3 4 1 2]); s sigmoid(fullyconnect(relu(fullyconnect(gpVec, W1, B1)), W2, B2)); plot(squeeze(extractdata(s)));正常的分布应该是大部分值在 0.3 到 0.8 之间少数通道接近 0 或逼近 1。如果所有值都接近 1说明注意力退化成了恒等映射等于白加如果大量值接近 0说明模型把大部分通道直接屏蔽了信息损失严重。最后用confusionchart看一下错分样本集中在哪几类把那些样本的特征图逐个拿出来对比。我遇到最多的情况是模型对某两类样本的错分始终稳定在同几个通道上说明注意力虽然有用但没有完全解决特征混淆这时该考虑加空间注意力或增广数据。做完这几步再回去调参每一次改动都有依据而不是靠运气试。我做这类方案的习惯是先确保能复现再谈涨点先看一眼特征再动超参。这个顺序帮我避免了很多次无效训练希望帮到你。本文还有配套的精品资源点击获取