拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MATLAB实现CNN-LSTM-Attention多输入多输出回归实战解析

简介面向深度学习与时间序列预测研究人员的MATLAB项目资料以CNN-LSTM-Attention融合模型为主线完整演示多输入多输出回归预测的工程实现。项目覆盖数据生成、滑动窗口切片、输入归一化、卷积特征提取、LSTM时序记忆、注意力权重分配、多输出回归映射、训练优化及误差评估等全流程并配有GUI可视化界面支持模拟数据生成、MAT/CSV导入和结果导出可直接用于工业过程预测、新能源功率预测、设备健康评估等场景。压缩包为1个docx文档仅144KB内容包含项目背景、模型架构、分模块代码详解、目录层次及应用领域便于读者按章节对照学习。目前已有48人学习适合具备MATLAB基础的研究人员、工程师和高校师生尤其适合希望构建可复用深度学习预测范式并增强模型可解释性的技术人员。通过学习可掌握注意力机制与CNN-LSTM结合的具体实现细节理解多输出联合预测的建模思路并借助GUI设计提升工程交付能力。1. CNN-LSTM-Attention在MATLAB里做多输入多输出回归先迈过两道坎多输入多输出回归在MATLAB里落地CNN-LSTM-Attention最常见的卡点不是网络搭不起来而是两件看起来很小的事多输入多输出的数据形状怎么摆以及注意力层到底放在哪个位置、用内置层还是自定义层。只要这两点想清楚从trainNetwork到predict的整条链路能很快跑通。这篇博文按“数据准备 → 网络定义 → 训练调参 → GUI封装”的顺序拆开讲给出一组可以直接改的MATLAB代码、参数表和排错经验程序结构以深度学习工具箱自带的层图API为主不依赖额外的工具箱。适合用MATLAB做时序回归预测的工程师和研究生也适合需要把训练好的模型封装成演示界面的读者MATLAB版本稍旧的照着调整接口名也能复现。2. CNN-LSTM-Attention网络在MATLAB中的结构拆解与层图搭建2.1 多输入多输出的维度布局先把“形状”定下来MATLAB的sequenceInputLayer接收的输入是一个三维数组第一维是每个时间步的特征数第二维是时间步数第三维是样本数。很多第一次接触多输入多输出回归的人会在这一步就把数据转置错。常见做法是保持原始数据为[numFeatures, totalSteps]再通过滑动窗口生成[numFeatures, inputSteps, numSamples]的输入张量。输出侧的维度取决于你预测的是“多特征单步”还是“多特征多步”。如果是前者标签形状是[numTargets, numSamples]如果是后者要把未来N步的所有目标特征展平成一行形状变成[numTargets * outputSteps, numSamples]。这个展平操作必须和网络最后一层fullyConnectedLayer的输出维度严格一致否则训练一开始就会报维度不匹配。任务类型输入X形状标签Y形状多特征单步预测[特征数, 时间步, 样本数][目标特征数, 样本数]多特征多步预测[特征数, 时间步, 样本数][目标特征数×步数, 样本数]单特征单步预测[1, 时间步, 样本数][1, 样本数]我一般会先把numFeatures、inputSteps、outputSteps、numTargets这四个变量用注释写在脚本头部后面所有代码都从这四个变量派生避免在滑窗和全连接层输出维度之间反复改数字。2.2 用层图把CNN、LSTM串起来在MATLAB里搭CNN-LSTM用的不是trainNetwork的层数组就是layerGraph。对于序列回归层数组其实够用不需要显式分叉。一段最简结构如下inputSize 8; % 输入特征数 numHiddenUnits 64; % LSTM隐藏单元数 numResponses 3; % 预测目标特征数 inputSteps 24; % 滑窗长度 layers [ sequenceInputLayer(inputSize, Name, in) convolution1dLayer(3, 32, Padding, same, Name, conv1) reluLayer(Name, relu1) lstmLayer(numHiddenUnits, OutputMode, sequence, Name, lstm1) fullyConnectedLayer(numResponses, Name, fc) regressionLayer(Name, out) ];这段代码里convolution1dLayer(3, 32, Padding, same)表示卷积核宽度为3、输出通道为32same填充保证时间维长度不缩短。这里有个容易踩的坑如果不用same填充卷积后的时间步数会减少LSTM接收到的序列长度就和输入对不上。lstmLayer的OutputMode必须设为sequence因为后面还要接注意力层对时间步做加权如果这里设成lastLSTM只输出最后一个时间步注意力就没有对象了。如果用的MATLAB版本较早找不到convolution1dLayer可以用convolution2dLayer([3 1], 32, Padding, same)代替把时间维放在第一维、通道维放在第二维效果等价。训练时如果内存占用过高可以在卷积和ReLU之后加一个maxPooling1dLayer但步长别用2序列回归对时间分辨率敏感池化过度会丢掉短时特征。2.3 时序注意力机制原理与可训练的自定义层写法CNN-LSTM-Attention里的Attention绝大多数场景指的是时序注意力LSTM输出的每个时间步都有一个隐藏向量注意力机制学习一组权重对这组时间步向量做加权平均把整个序列压缩成一个固定维度的表示再接全连接层。公式上就是打分、softmax归一化、加权求和三步。打分可以用一个可学习的向量W对每个时间步的隐藏状态做内积得到该时间步的重要程度。网络训练时W会逐渐把权重集中到与目标输出相关性更强的时间步上。这里有一个常见误解注意力机制不一定非要多头。对于样本量不大的回归任务单头时序注意力足够多头自注意力机制更适合长序列全局依赖在小数据集上反而容易过拟合。注意力类型适用场景当前任务是否推荐单头时序注意力时间步加权、序列压缩推荐SE / CA通道注意力图像特征图通道加权一般不推荐CBAM通道空间注意力图像检测分割回归场景可不用多头自注意力长序列全局依赖数据量小容易过拟合MATLAB在某些新版本里提供了内置的attentionLayer但为了不依赖版本项目里更常见的是自己写一个无状态的自定义层。下面是可嵌入层图的骨架实现classdef temporalAttentionLayer nnet.layer.Layer properties (Learnable) W % 打分向量尺寸 [1, hiddenSize] end methods function layer temporalAttentionLayer(hiddenSize) layer.Name attn; layer.W randn(1, hiddenSize) * 0.02; end function Z predict(layer, X) % X: [hiddenSize, timeSteps, batchSize] scores pagemtimes(layer.W, X); % [1, T, B] scores reshape(scores, size(X, 2), size(X, 3)); % [T, B] maxS max(scores, [], 1); alpha exp(scores - maxS); alpha alpha ./ sum(alpha, 1); % 时间维归一化 alpha reshape(alpha, 1, size(X, 2), size(X, 3)); Z sum(alpha .* X, 2); % 加权求和 end end end这个类的核心在predictpagemtimes计算每个时间步的打分reshape把打分从[1,T,B]转成[T,B]方便做softmaxmaxS用于数值稳定最后alpha .* X是逐时间步加权。W被声明成Learnable训练时会自动更新。需要注意如果MATLAB版本较老不支持pagemtimes可以换成两层循环分别处理每个样本和时间步但训练速度会慢不少。在层图里插入这个自定义层写法是layers [ sequenceInputLayer(inputSize, Name, in) convolution1dLayer(3, 32, Padding, same, Name, conv1) reluLayer(Name, relu1) lstmLayer(numHiddenUnits, OutputMode, sequence, Name, lstm1) temporalAttentionLayer(numHiddenUnits) % 自定义注意力层 fullyConnectedLayer(numResponses, Name, fc) regressionLayer(Name, out) ];如果训练时报错说自定义层缺少backward方法说明当前版本不能对该层的操作自动求导需要按MATLAB官方自定义层模板补齐反向传播。一个更省事的替代方案是把打分、softmax和加权求和的逻辑写进训练脚本的模型函数里配合dlnetwork使用权重用dlarray管理这样反向传播完全交给自动微分。3. 多输入多输出回归的滑窗、归一化与trainingOptions参数设置3.1 用滑动窗口生成可监督样本时序回归的第一步是把原始连续数据切成“输入窗口 → 输出窗口”的样本对。常见做法是写一个纯函数输入原始矩阵、输入步长、输出步长和目标特征行号输出训练张量。下面这个函数是我常用的版本function [XTrain, YTrain] buildSamples(data, inputSteps, outputSteps, targetIdx) % data: [numFeatures, totalSteps] [numFeatures, totalSteps] size(data); numSamples totalSteps - inputSteps - outputSteps 1; numTargets numel(targetIdx) * outputSteps; XTrain zeros(numFeatures, inputSteps, numSamples); YTrain zeros(numTargets, numSamples); for i 1:numSamples XTrain(:, :, i) data(:, i:iinputSteps-1); block data(targetIdx, iinputSteps:iinputStepsoutputSteps-1); YTrain(:, i) block(:); end end这里的targetIdx是一个行向量用来指定要预测哪些特征在原始数据中的行号。比如原始数据有8个特征想预测第1个和第5个未来3步的值那么targetIdx [1 5]outputSteps 3numTargets 6网络最后的fullyConnectedLayer输出维度就是6。block(:)会把[2, 3]的目标矩阵按列展平成6×1和全连接层的输出维度对应。如果数据里有缺失值先用fillmissing补上再进行滑窗不要在生成样本后删掉有NaN的列否则样本之间的时间连续性会被打断。3.2 归一化用训练集的统计量测试集只用同一组参数归一化参数如果整段数据一起算会把未来信息泄漏进训练集导致离线指标虚高、上线后预测失真。正确做法是用训练集单独计算均值和标准差验证集和测试集复用同一组参数muX mean(XTrain, 2); sigX std(XTrain, 0, 2); XNorm (XTrain - muX) ./ sigX; muY mean(YTrain, 2); sigY std(YTrain, 0, 2); YNorm (YTrain - muY) ./ sigY; % 测试集 XTestNorm (XTest - muX) ./ sigX; % 预测结果反归一化 YPred predict(net, XTestNorm); YPred YPred .* sigY muY;mean(XTrain, 2)是按第二维和第三维求均值结果是一个[numFeatures, 1]的列向量对应每个特征自己的均值。用zscore也可以但要把返回的均值、标准差保存成变量后面GUI里要用。反归一化时YPred的形状是[numTargets, numSamples]sigY是[numTargets, 1]直接用.*广播乘法即可。3.3 trainingOptions里真正影响收敛的4个参数CNN-LSTM组合比纯LSTM更容易发散核心原因是卷积层扩大了参数空间。初学时只需要盯住这四个参数参数常用初值说明MiniBatchSize32~128序列长时调小避免显存溢出InitialLearnRate0.001Adam配合0.001起步发散就降到0.0003MaxEpochs50~100样本少时设小防过拟合ValidationFrequency10~20太小拖慢训练太大发现不了发散一份可跑的配置如下options trainingOptions(adam, ... MiniBatchSize, 64, ... InitialLearnRate, 0.001, ... MaxEpochs, 80, ... ValidationData, {XValNorm, YValNorm}, ... ValidationFrequency, 10, ... Shuffle, every-epoch, ... Plots, training-progress); net trainNetwork(XNorm, YNorm, layers, options);ValidationData中的输入形状必须和训练输入一致都是[numFeatures, inputSteps, numValSamples]标签是[numTargets, numValSamples]。如果报“维度不匹配”优先检查验证集的标签是否做了一样的展平操作而不是先怀疑网络结构。Shuffle设成every-epoch能让每个epoch的批次顺序不同对LSTM类的模型收敛更稳定。4. 通过注意力权重和验证曲线验证模型是否学到时序依赖4.1 从训练好的网络里抽取注意力权重训练完成后一个值得做的验证是画出注意力权重随时间的分布看模型是否真的把注意力集中到了关键时间步。利用activations可以拿到LSTM层的输出H activations(net, XTestNorm, lstm1); % H 可能是 [hiddenSize, timeSteps, batchSize]也可能是 [timeSteps, hiddenSize, batchSize] idx find(arrayfun((l) strcmp(l.Name, attn), net.Layers), 1); Wattn net.Layers(idx).W; % 取第一个样本按需要调整维度 if size(H, 1) ~ size(Wattn, 2) H permute(H, [2 1 3]); end scores Wattn * H(:, :, 1); alpha exp(scores - max(scores)); alpha alpha / sum(alpha); figure; plot(alpha, LineWidth, 1.5); xlabel(时间步); ylabel(注意力权重); title(attention weight over time);如果alpha在所有时间步上几乎均匀说明注意力没有学到选择性问题通常不在网络结构而在输入窗口本身缺乏区分度。可以先做特征相关性分析把和目标输出相关性低的特征剔除再重训。如果alpha集中在最后一个或某几个时间步说明模型依赖短时记忆这时候可以考虑加大inputSteps或把LSTM换成双向LSTM结构。4.2 从训练曲线分辨过拟合、发散和欠拟合训练时打开Plots,training-progress后主要看三条曲线训练Loss、验证Loss和学习率。常见现象和对应处理方式训练Loss下降但验证Loss持续上升过拟合。先把MaxEpochs减半或提前终止保存验证集最优模型。两条Loss都在震荡不降学习率偏大降到0.0003重训通常立竿见影。验证Loss在某一个Epoch突然跳高再恢复梯度爆炸。给trainingOptions加上GradientThreshold。options trainingOptions(adam, ... InitialLearnRate, 0.001, ... GradientThreshold, 1, ... LearnRateSchedule, piecewise, ... LearnRateDropPeriod, 30, ... LearnRateDropFactor, 0.5, ... Plots, training-progress);GradientThreshold设为1表示梯度的L2范数超过1时会被裁剪这对LSTM尤其有效。LearnRateDropPeriod和LearnRateDropFactor配合使用让学习率每30轮衰减为原来的一半后期微调参数而不破坏已学到的特征。4.3 多输出回归不要只用RMSE补上MAE、R2和残差分布多输出回归的评价指标如果只报一个总RMSE掩盖了单个输出通道的误差差异。常见做法是每个输出维度分别算指标再汇总。批量计算代码如下residuals YTest - YPred; rmse sqrt(mean(residuals.^2, all)); mae mean(abs(residuals), all); ssRes sum(residuals.^2, all); ssTot sum((YTest - mean(YTest, all)).^2, all); r2 1 - ssRes / ssTot; figure; histogram(residuals(:), 30); xlabel(残差); ylabel(频数); title(residual distribution);如果all在旧版MATLAB不可用把mean(x, all)改成mean(x(:))。残差直方图如果呈现明显的偏态说明模型对某个输出通道存在系统性偏差这时候检查该通道的数据是否有异常趋势或者该通道是否应该单独做差分后再进入模型。5. 把训练好的模型封装成GUIApp Designer的加载、预测与导出5.1 在App Designer中做“一次加载重复预测”设计GUI时最影响体验的是模型加载方式。如果每次点击预测按钮都执行一次load模型加载耗时会让界面卡顿。正确做法是在App Designer的startupFcn里把网络和归一化参数一次性读入存入app的属性中function startupFcn(app) s load(trained_model.mat); app.net s.net; app.muX s.muX; app.sigX s.sigX; app.muY s.muY; app.sigY s.sigY; end预测按钮的回调只需要做预处理、推理和反归一化function PredictButtonPushed(app, event) data readmatrix(app.FilePathEdit.Value); XNew (data - app.muX) ./ app.sigX; YPred predict(app.net, XNew); YPred YPred .* app.sigY app.muY; plot(app.UIAxes, YPred); end这里data的每一列是一个时间步如果原始数据是行向时间记得先转置。YPred的形状是[numTargets, numSamples]plot时转置成[numSamples, numTargets]每一列是一条预测曲线。GUI里常见的坑是文本框输入路径少写了扩展名readmatrix会直接报错可以在回调里先isfile判断一下并弹出uialert。5.2 保存模型时把归一化参数和滑窗参数一起放进mat文件训练结束后的保存命令建议写成一条save(trained_model.mat, net, muX, sigX, muY, sigY, ... inputSteps, outputSteps, targetIdx);很多GUI里预测结果完全不对原因是只保存了net而忘了保存muX、sigX测试时用了新数据的统计量做归一化。另外在GUI启动时可以用ver(deep)查看深度学习工具箱版本版本过旧时在界面上提示“当前工具箱不支持新版自定义层”避免用户面对莫名其妙的报错。预测结果导出用writematrix(YPred, pred_result.csv)比xlswrite更快且不需要安装Excel。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门