MATLAB实现CNN-LSTM-Attention多输入多输出回归预测 📅 发布时间:2026/9/19 15:01:46 👁 浏览次数: 简介基于MATLAB实现的CNN-LSTM-Attention融合模型多输入多输出回归项目面向有一定MATLAB编程基础并从事时间序列预测、工业过程建模或智能传感数据分析的研究人员与工程师重点解决多源数据联合预测与复杂时序特征提取问题。资源以1个docx文档封装压缩包仅144KB但内容覆盖数据生成、滑动窗口切片、统计标准化、网络搭建、训练配置、反标准化评估及GUI可视化设计全流程。文档含项目背景、模型架构、挑战与解决方案、完整代码示例等模块对CNN卷积特征提取、LSTM长期依赖记忆、注意力权重分配的实现细节逐段拆解便于读者直接修改超参数并移植到新能源功率预测、设备健康评估等场景。项目从数据准备到预测评估形成闭环并提供GUI交互界面用户可通过界面完成数据导入、模型训练、预测分析与结果导出便于非专业用户操作和教学演示。目前已有48人浏览学习适合需要快速搭建可解释、可复用回归预测模型的工程技术人员参考。1. 面对多变量回归预测CNN-LSTM-Attention 为什么值得在 MATLAB 里做多输入多输出回归预测在工程里并不罕见你用前 12 小时的风速、温度、气压去推未来 3 小时的发电功率或者用设备的多通道振动信号同时估计剩余寿命的上下界。这类任务的关键在于数据里既有局部特征比如突变的异常脉冲又有长时间依赖比如缓慢漂移的趋势还往往存在多变量之间的耦合关系。CNN-LSTM-Attention 的组合恰好覆盖了这三个需求CNN 抓局部模式LSTM 捕捉序列依赖Attention 机制对时间步或特征通道做加权筛选。相比单一 LSTM 或纯 CNN这种结构在多输入输出场景下通常有更低的误差也更容易通过注意力权重解释模型关注到了哪些时刻。选择 MATLAB 而不是 Python 来实现这个架构核心理由有三。第一MATLAB 的 Deep Learning Toolbox 从 R2019b 之后对 layerGraph 和 dlnetwork 的支持已经相当完整CNN 和 LSTM 可以直接用内置层拼装不需要手动写反向传播。第二MATLAB App Designer 在构建小型预测工具时非常高效训练好的网络可以打包进 GUI 里的回调函数对不熟悉 Python 部署链路的工程师更友好。第三MATLAB 自带的数据预处理函数如 normalize、fillmissing、split在时序数据清洗上比手写 numpy 代码更直观。这篇文章按数据准备、模型搭建、训练验证、GUI 封装和参数调优的顺序展开我会给出可运行的 MATLAB 代码并说明每一步的设计意图与常见的踩坑点。2. 数据预处理与多输入多输出问题的建模方式2.1 输入输出结构设计先用 cell 数组定好数据格式在 MATLAB 中实现 CNN-LSTM-Attention入门者最容易绊倒的地方不是模型结构而是数据格式。Deep Learning Toolbox 对序列回归任务要求输入格式为 numFeatures × numTimeSteps 的矩阵或 cell 数组每个 cell 是一个样本而多输出回归则要求目标输出为 numResponses × numTimeSteps 的矩阵同样封装在 cell 数组里。以风速和功率预测为例假设你有 6 个输入特征风速、风向、温度、湿度、气压、前一时刻功率需要预测下一时刻的 2 个输出有功功率、无功功率那么对于第 i 个训练样本输入矩阵的尺寸是 6 × windowSize输出矩阵的尺寸是 2 × 1如果只预测一个未来时刻。% 假设 rawData 是 T×N 的原始数据矩阵T 为时间步数N 为特征数 % 其中前 6 列为输入特征第 7、8 列为目标输出 numFeatures 6; numResponses 2; windowSize 24; % 用过去 24 个时刻预测未来 1 个时刻 [XTrain, YTrain] prepareData(rawData, numFeatures, numResponses, windowSize); function [XTrain, YTrain] prepareData(data, numFeat, numResp, winSize) numSamples size(data, 1) - winSize; XTrain cell(numSamples, 1); YTrain cell(numSamples, 1); for i 1:numSamples XTrain{i} data(i:iwinSize-1, 1:numFeat); % 转置为 numFeat × winSize YTrain{i} data(iwinSize, numFeat1:numFeatnumResp); % numResp × 1 end end上述代码中XTrain{i} 做了转置操作原因在于 MATLAB 的 sequenceInputLayer 期望输入维度是 numFeatures × numTimeSteps而不是常见的 samples × time × features 结构。而 YTrain{i} 保持 numResp × 1对应 regressionLayer 的输出维度。如果你的预测目标不是未来一个时刻而是未来多个时刻比如未来 6 个小时的功率曲线那么 YTrain{i} 应当是 numResp × forecastHorizon 的矩阵这要求模型最后的全连接层输出维度为 numResponses × forecastHorizon。提示不要忽略 cell 数组与普通数值矩阵的差异。如果直接将整个数据矩阵送入 trainNetworkMATLAB 会默认每个样本是独立序列而你的数据往往是连续截断的会导致严重的时序信息泄露。2.2 数据归一化与训练集划分避免归一化参数来自测试集在时序预测任务中归一化通常采用 Z-score 或 min-max 两种方式。我一般优先选 min-max 到 [0,1] 区间因为 LSTM 的 tanh 激活函数和注意力机制的 softmax 对输入尺度比较敏感且多输出回归中如果不同输出量纲差异大例如功率以千瓦为单位、温度以摄氏度为单位未归一化的 loss 会被大数值量纲主导。MATLAB 的 normalize 函数提供了便捷接口但必须注意 fit 和 transform 的分离防止测试集信息通过归一化参数泄露到训练过程。% 对每个特征单独做 min-max 归一化 [Xnorm, mu, sigma] zscore(XTrain); % 使用 z-score 方式 % 注意zscore 对矩阵按列计算但 XTrain 是 cell 数组需要循环处理 for i 1:numel(XTrain) XTrain{i} (XTrain{i} - mu) ./ sigma; end % 对测试数据使用同样的 mu 和 sigma而不是重新计算 for i 1:numel(XTest) XTest{i} (XTest{i} - mu) ./ sigma; end这里有一个关键细节zscore在计算均值和标准差时应当基于训练集的所有样本拼接后的矩阵。如果每个样本单独归一化会破坏时间步之间的相对关系。实际做法是将所有样本的二维矩阵沿时间维度拼接计算全局的 mu 和 sigma再对每个样本应用。这在上述代码中通过mu和sigma计算时的数据来源体现——你必须在循环之前对[XTrain{:}]做归一化统计。训练集和测试集的划分也有讲究。随机打乱样本会破坏时序依赖所以应当按时间顺序划分比如前 80% 的数据做训练后 20% 做测试。如果涉及超参数调优中间还需要留出一段验证集或者使用 MATLAB 内置的crossvalind做分层划分尽管时间序列的分层不太严格。我见过不少初学者在这个环节直接调用randperm打乱数据结果模型在测试集上表现好但上线后崩溃就是因为时间序列的局部平稳假设被破坏了。2.3 滑动窗口构造样本重叠采样与步长的取舍滑动窗口构造样本时窗口大小 windowSize 直接决定了模型看到的序列长度。窗口太短模型抓不到长时间依赖窗口太长训练样本数减少且 LSTM 容易出现梯度衰减。常见经验值是覆盖 2~3 个完整周期例如预测日功率曲线如果采样间隔是 1 小时窗口取 24~48 较为合理。步长 stride 决定样本重叠程度stride1 时样本量最大但相邻样本高度相关容易过拟合stridewindowSize 时样本独立但数据量骤降。我一般折中取 stride 为窗口的 1/4 到 1/2。function [XTrain, YTrain] prepareDataStride(data, numFeat, numResp, winSize, stride) numSamples floor((size(data, 1) - winSize) / stride); XTrain cell(numSamples, 1); YTrain cell(numSamples, 1); idx 1; for i 1:stride:size(data, 1) - winSize if idx numSamples, break; end XTrain{idx} data(i:iwinSize-1, 1:numFeat); YTrain{idx} data(iwinSize, numFeat1:numFeatnumResp); idx idx 1; end XTrain XTrain(1:idx-1); YTrain YTrain(1:idx-1); end这个函数在循环中用idx控制实际生成的样本数避免因stride不整除导致边界错误。MATLAB 的cell预分配在这里能明显提升性能如果数据量大建议用cell(numSamples, 1)预分配后逐个赋值再裁剪掉末尾未填充的部分。3. 在 MATLAB 中搭建 CNN-LSTM-Attention 组合网络3.1 网络结构设计从 layerGraph 到 dlnetwork 的两种路径MATLAB 支持两种方式定义这个模型。第一种是用layerGraphtrainNetwork适合纯内置层且不需要自定义训练循环的场景第二种是用dlnetworkmodelGradients适合需要自定义损失函数或者实现复杂注意力层的场景。对于 CNN-LSTM-Attention由于 MATLAB 内置层里没有现成的注意力层要么自己写customLayer要么用attention相关的函数在 dlnetwork 里实现。这里给出一个用layerGraph配合自定义注意力层以functionLayer的方式嵌入的快速方案。% 创建 layerGraph numFeatures 6; numResponses 2; windowSize 24; layers [ sequenceInputLayer(numFeatures, Name, input) % CNN 部分两个卷积层 ReLU 最大池化 convolution1dLayer(3, 32, Padding, same, Name, conv1) reluLayer(Name, relu1) convolution1dLayer(3, 64, Padding, same, Name, conv2) reluLayer(Name, relu2) maxPooling1dLayer(2, Stride, 2, Name, pool1) % LSTM 部分 lstmLayer(64, OutputMode, sequence, Name, lstm1) % 注意力层这里用一个自定义 functionLayer 代替 % 具体实现见 3.2 节的自定义层代码 attentionLayer(Name, attention) % 输出部分 fullyConnectedLayer(numResponses, Name, fc_out) regressionLayer(Name, reg) ]; lgraph layerGraph(layers); % 对于 sequence-to-one 回归LSTM 需要设置 OutputModelast % 但如果要在 LSTM 后接注意力则需要 OutputModesequence这段代码已经指定了lstmLayer的OutputMode为sequence这是因为注意力机制需要 LSTM 每个时间步的隐状态 h_t 来计算加权汇总如果设为last你只能得到最后一个时间步的输出注意力计算无从谈起。maxPooling1dLayer会降低时间维度CNN 部分输出的时间步数变为原来的 1/2LSTM 在这一长度上工作。如果窗口长度为奇数需要先用Padding选项调整或者用globalAveragePooling1dLayer避免长度匹配问题。注意在卷积层中我用了convolution1dLayerMATLAB 的 Deep Learning Toolbox 从 R2020a 开始提供这个层。如果你的版本较老可以将convolution1dLayer(3, 32, Padding, same)替换为convolution2dLayer([3 1], 32, Padding, same)但输入数据就需要 reshape 成 4 维张量操作更繁琐。3.2 注意力机制的自定义实现三种常见形式与选择MATLAB 中实现注意力层最灵活的方式是继承nnet.layer.Layer写一个自定义层然后在layerGraph中调用。另一种方式是直接用dlnetwork在自定义训练循环里写score softmax(fullyConnectedLayer(W, h_t))但这对新手来说调试成本偏高。这里给出基于functionLayer的最小实现适用于 sequence-to-one 且注意力权重是标量的场景。function layer attentionLayer(varargin) layer functionLayer(attentionForward, Name, attention); end function Y attentionForward(X) % X 的维度是 hiddenSize × numTimeSteps × batchSize [hiddenSize, numSteps, batchSize] size(X); % 计算每个时间步的注意力得分这里使用简单的全连接打分 % 实际中可以引入可学习的 W 和 b但 functionLayer 不支持可学习参数 % 所以我们这里用固定初始化或者改用 dlnetwork 实现可学习版本 W randn(1, hiddenSize) / sqrt(hiddenSize); scores W * reshape(X, hiddenSize, []); % 1 × (numSteps*batchSize) scores reshape(scores, numSteps, batchSize); % softmax 归一化 weights softmax(scores, 1); % numSteps × batchSize % 加权求和 Xr reshape(X, hiddenSize, numSteps, batchSize); Y zeros(hiddenSize, 1, batchSize); for b 1:batchSize Y(:, :, b) Xr(:, :, b) * weights(:, b); end end这个实现有两个明显局限。第一functionLayer不能存储可学习参数注意力矩阵 W 是固定随机初始化的这在实际项目中效果不稳定。第二for循环处理 batch 效率低。更推荐的方式是用dlnetwork定义完整的注意力模块配合forward和modelGradients函数做自定义训练。如果希望保持trainNetwork的简洁流程可以用%#function声明自定义层并且通过nnet.layer.Layer子类实现可学习参数但代码量会上升到 100 行以上。介于篇幅这里给一个在dlnetwork下使用的可学习注意力实现思路。% 在 dlnetwork 里注意力模块定义如下 % 假设 lstmOutput 是 dlarray维度为 hiddenSize × numTimeSteps × batchSize function weights computeAttentionWeights(lstmOutput) [hiddenSize, numSteps, ~] size(lstmOutput); % 可学习的打分参数Wq 和 Wk 可以类比 Transformer 中的 query 和 key persistent Wq Wk if isempty(Wq) Wq dlarray(randn(hiddenSize, hiddenSize) / sqrt(hiddenSize)); Wk dlarray(randn(hiddenSize, hiddenSize) / sqrt(hiddenSize)); end % 对每个时间步计算得分 query Wq * lstmOutput(:, end, :); % 用最后一个时间步作为 query keys Wk * lstmOutput; % 所有时间步作为 key scores pagemtimes(query, keys); % batchSize × numSteps weights softmax(scores, 2); % 按时间步归一化 end这里用pagemtimes替代循环计算批量矩阵乘法在 GPU 上能显著加速。query取最后一个时间步的隐状态这符合序列到单点回归的直觉——你需要从整个序列中挑选与最终时刻最相关的信息。实际项目中我建议使用多头注意力的简化版本即把 hiddenSize 拆成几个头分别计算再拼接这能提升模型的表达能力对应热词里的“多头注意力机制原理”。3.3 训练选项参数设置解决 LSTM 梯度消失与过拟合训练 CNN-LSTM-Attention 网络时trainingOptions的参数选择直接影响收敛速度和最终精度。我第一次跑这个模型时最深刻的教训是学习率设置太大0.01导致 LSTM 的梯度爆炸loss 直接变 NaN。对于 LSTM 家族初始学习率通常设在 0.001~0.005 之间并使用adam优化器。options trainingOptions(adam, ... MaxEpochs, 200, ... MiniBatchSize, 64, ... InitialLearnRate, 0.002, ... LearnRateSchedule, piecewise, ... LearnRateDropPeriod, 50, ... LearnRateDropFactor, 0.2, ... GradientThreshold, 1, ... Shuffle, never, ... Verbose, 1, ... Plots, training-progress);GradientThreshold设置为 1 是防止梯度爆炸的关键LSTM 在长序列上梯度范数很容易超过 10如果不截断权重更新一步就会毁掉整个模型。Shuffle设为 never 是因为时序数据不能打乱样本顺序虽然每个样本是独立窗口但保持顺序有助于模型捕捉数据的连续分布。MiniBatchSize的选择取决于数据量和显存如果序列长度是 24 且 hiddenSize 是 6464 的 batch size 在普通 8GB 显卡上都能跑但如果序列长度是 512可能就要降到 16。为了减少过拟合我在实际项目中会在 LSTM 之后加一个dropoutLayer(0.2)在注意力层之后再接一个dropoutLayer(0.1)。注意 dropout 只应在训练时起作用MATLAB 的 trainingOptions 会自动处理这一点不需要手动开关。如果训练 loss 和验证 loss 在 100 轮后差距拉大优先调节 dropout 比例而不是增加 L2 正则化系数因为时序预测中 L2 对 LSTM 的隐藏状态约束过强容易让模型退化为线性预测器。4. GUI 界面设计与代码组织方式4.1 App Designer 搭建预测工具从加载数据到一键预测MATLAB 的 App Designer 是比传统 GUIDE 更现代的 GUI 构建工具支持回调函数的代码管理和组件拖拽布局。针对这个多输入多输出回归预测任务GUI 至少需要以下几个功能模块加载数据按钮支持 .mat 或 .xlsx、训练模型按钮、导入待预测数据按钮、结果显示区以及注意力权重可视化。使用 App Designer 的组件命名规范可以在回调中直接访问训练好的模型。% App Designer 中的训练回调函数部分代码 function TrainButtonPushed(app, event) % 读取当前界面上的数据 data app.DataEditField.Value; % 假设是一个数据路径字符串 rawData load(data).rawData; % 数据预处理使用 2.3 节的 prepareDataStride 函数 [XTrain, YTrain] prepareDataStride(rawData, 6, 2, 24, 6); % 归一化并分割 % ... 这里省略归一化代码参考 2.2 节 % 构建网络 lgraph buildCNN_LSTM_Attention(6, 2, 24); % 设置训练选项 options trainingOptions(adam, ... MaxEpochs, 100, ... MiniBatchSize, 32, ... InitialLearnRate, 0.002, ... GradientThreshold, 1, ... Plots, none, ... Verbose, 0); % 训练 app.Net trainNetwork(XTrain, YTrain, lgraph, options); app.StatusLabel.Text 训练完成; end在上述回调中buildCNN_LSTM_Attention是你定义的函数返回一个 layerGraph。因为 MATLAB 的trainNetwork不接受输出为 cell 数组的 3D 张量所以YTrain必须是 cell 数组其中每个元素是 numResponses × numTimeSteps 的矩阵。这个结构在trainNetwork内部会被自动解析。如果你用dlnetwork做自定义训练则 YTrain 通常是 dlarray 组成的 cell 数组两者在数据格式上略有差异需要在切换实现时同步调整。4.2 模型保存与加载避免每次打开 GUI 重新训练训练一个 CNN-LSTM-Attention 网络动辄几分钟到几十分钟如果 GUI 每次启动都要重新训练体验很糟糕。标准的做法是用save和load函数将训练好的网络保存为 .mat 文件并在 App Designer 的 startupFcn 里检查是否存在模型文件如果有则直接加载。% 保存模型 function SaveModelButtonPushed(app, event) [filename, pathname] uiputfile(*.mat, 保存模型); if isequal(filename, 0) return; end net app.Net; save(fullfile(pathname, filename), net); app.StatusLabel.Text 模型已保存; end % 加载模型在 startupFcn 中调用 function startupFcn(app) if exist(trained_model.mat, file) S load(trained_model.mat); if isfield(S, net) app.Net S.net; app.StatusLabel.Text 已加载已有模型; end end end保存时需要注意trainNetwork返回的SeriesNetwork或DAGNetwork对象不能跨 MATLAB 版本随意加载R2021a 训练的模型在 R2019b 中可能无法识别。因此在 GUI 中保存模型时建议同时保存lgraph的结构层信息和训练选项以便在版本不匹配时能够重建网络结构并重新训练。这里还有一个潜在问题如果模型是在 GPU 上训练的加载到无 GPU 的机器上预测速度会明显变慢但不影响功能。GUI 的预测回调里直接用predict函数即可不需要关注这些底层细节。4.3 结果显示与评估指标在界面上展示 RMSE 与拟合曲线回归预测的 GUI 中最常用的展示方式是绘制真实值与预测值的对比曲线、散点图以及误差分布直方图。App Designer 的 UIAxes 组件可以直接绘制这些图相比旧的plotfigure方式更适合嵌入 UI。计算 RMSE 和 R² 需要用到 MATLAB 的评估函数基础版本可以自己写。% 预测回调中的评估部分 function PredictButtonPushed(app, event) net app.Net; XTest app.TestData; % cell 数组 YTest app.TestTarget; % cell 数组 YPred predict(net, XTest, MiniBatchSize, 32); % 将 cell 数组转换为矩阵用于计算误差 YPredMat cell2mat(YPred); % numSamples × numResponses YTestMat cell2mat(YTest); % 计算 RMSE 和 R² rmse sqrt(mean((YPredMat - YTestMat).^2, 1)); ssRes sum((YTestMat - YPredMat).^2, 1); ssTot sum((YTestMat - mean(YTestMat, 1)).^2, 1); r2 1 - ssRes ./ ssTot; % 绘制第一个输出的对比曲线 plot(app.UIAxes, 1:size(YTestMat, 1), YTestMat(:, 1), b-, ... 1:size(YTestMat, 1), YPredMat(:, 1), r--); legend(app.UIAxes, 真实值, 预测值); app.RMSEEditField.Value rmse(1); app.R2EditField.Value r2(1); end这里要注意一个维度陷阱predict返回的 cell 数组每个元素是 numResponses × 1 或 numResponses × horizon 的矩阵cell2mat(YPred)先沿横向拼接再转置确保得到的二维矩阵是 numSamples × numResponses。如果直接用cell2mat(YPred)得到的矩阵维度是 numResponses × numSamples画图时横纵坐标就反了。这个细节在 GUI 调试时经常耗费大量时间提前注释清楚可以避免后续维护的困惑。5. 注意力权重可视化给模型一个可解释的理由注意力机制在 MATLAB 实现中的最大优势是可以提取中间层的输出从而可视化模型在不同时间步上的关注程度。利用activations函数可以获取 LSTM 层的输出再与注意力权重结合画出热力图。这种可视化不仅是论文里的加分项更是工程调参的重要依据——如果注意力权重集中在几个固定的时间步上说明窗口长度可能设置过大模型在偷懒。% 提取注意力权重并可视化 % 假设 net 是训练好的 DAGNetworkattention 是自定义层名称 % 对于 functionLayer可以使用 activations 获取输出 attOutput activations(net, XTest, attention); % attOutput 维度为 hiddenSize × 1 × numSamples因为注意力层输出加权和 % 如果要可视化每个时间步的权重需要在自定义层中额外输出权重 % 这里给出一种替代方案利用 LSTM 输出与 final output 的梯度近似权重 % 使用 dlgradient 计算输入对输出的敏感度使用activations提取中间层输出时有一个限制如果自定义层是用functionLayer写的它不具备activations支持的层属性调用会报错。解决方法是把注意力层写成一个真正的nnet.layer.Layer子类并且在predict方法中同时输出加权后的结果和权重矩阵或者使用dlnetwork的forward函数搭配dlfeval求梯度。% 集成梯度近似注意力权重的简化实现 function attWeights computeIntegratedGradients(net, X, targetIdx) % 使用 dlfeval 计算梯度 grad dlgradient(dlarray(X), targetIdx); % 对梯度做时间维度的求和与归一化 attWeights squeeze(sum(abs(grad), 1)); attWeights attWeights / sum(attWeights); end上述集成梯度方法的逻辑是如果某个时间步对最终输出的影响大那么该时间步输入的变化会引起输出更大的梯度因此梯度的绝对值可以在一定程度上反映注意力。这种方法不需要修改网络结构适合快速验证。如果项目对可解释性要求高建议直接在模型里加一个并行输出分支即注意力权重本身作为一个输出层通过trainNetwork的多输出机制实现联合训练此时layerGraph需要用到addLayers和connectLayers来构建分叉结构。在实际工程中我还会用注意力权重做数据筛选。例如如果可视化发现模型在 80% 的情况下只关注窗口的最后 5 个时间步那么可以考虑把窗口从 24 缩短到 10这不仅能加快训练速度有时还能提升泛化精度——因为模型不再需要拟合大量与决策无关的历史信息过拟合的风险也随之下降。这一步的验证方式是在缩短窗口后重新训练对比验证集上的 RMSE 是否下降。这里提供一组我常用的排查顺序先看 attention 热力图是否出现了明显的“长尾分布”即少数时间步集中了大部分权重如果是再尝试不同的窗口长度和 LSTM hiddenSize直到热力图分布相对平滑。另一个进阶技巧是将注意力权重与输入特征的物理意义对照。在多输入多输出的场景下你可以在 GUI 里加一个下拉框选择查看哪个输入特征的时间步注意力例如风功率预测任务中关注风速通道的注意力是否与真实的物理滞后时间吻合。如果模型学到的注意力峰值集中在风速突变后的第 3 个时间步而这个滞后恰好符合流场的传输时间那这个模型的可信度就很高反之则要检查数据是否存在对齐错误。将注意力可视化作为一个常规的模型诊断工具而不是论文里的一张配图能显著减少后期调试模型的盲目性。本文还有配套的精品资源点击获取