简介面向需要完成多输入多输出回归预测的MATLAB用户这份基于BiLSTM双向长短期记忆神经网络的预测代码可直接用于处理10个输入特征、3个输出变量的典型场景适用于时间序列预测、状态估计等任务。资源压缩包共3个文件包含完整的MATLAB脚本、配套的Excel数据文件以及一份图文说明文档整体大小仅543KB结构简洁明了。命令窗口会输出MAE与R2指标方便读者快速评估模型精度提供的数据与源码一一对应下载后可在MATLAB 2018b及以上版本中直接运行。针对不同版本可能出现的程序乱码问题文档中也给出了用记事本打开复制的简易处理思路。目前已有389人学习下载适合希望快速上手BiLSTM多输出建模、缺少现成数据与完整实现的中初级学习者参考也可作为进一步改进的起点。1. BiLSTM多输入多输出预测数据怎么摆、网络怎么搭、坑在哪做多变量时序预测的工程师迟早会撞上一个问题输入有七八个特征输出却要同时给两三个目标值。拆成多个 LSTM 各跑一遍维护成本翻倍模型之间还互相打架不如一个共享隐藏层的网络。这正是 BiLSTM 双向长短期记忆神经网络做多输入多输出MIMO预测的用武之地——输入侧接收多特征矩阵输出侧用全连接层直接映射出多个响应变量双向结构还能同时利用窗口内前后两个方向的信息。MATLAB 的 Deep Learning Toolbox 把这套流程封装到几十行代码能跑通的程度但真正的难点不在调用 API而在数据怎么摆、序列怎么对齐、超参数怎么设。这篇笔记按“数据格式→网络搭建→训练预测→避坑→进阶”的顺序走一遍适合手里有数据、准备直接落地多输入多输出预测的从业者照着改。2. 多输入多输出数据怎么摆滑窗、时间平移和切分2.1 多输入多输出的数据本质不是多个模型是一个共享隐藏层网络“多输入多输出”这四个字在 MATLAb 里的落地方式和很多人第一直觉不一样。网络定义上trainNetwork 的 sequenceInputLayer 只接收一个序列输入但这个输入可以是任意行数的特征矩阵输出侧由全连接层映射出多个响应值。所以多输入对应的就是特征维度 numFeatures多输出对应的是响应维度 numResponses二者共用一个 BiLSTM 隐藏层而不是给每个输出各训一个网络。这样做有两个实际好处。一是训练成本线性下降三个输出拆成三个 LSTM参数总量接近三倍而共享隐藏层只需要一套双向参数。二是特征交互如果某两个输出变量共享同一段历史模式的驱动单向模型各自学到的表征是隔离的多输出共享层会让输出的梯度同时更新同一批隐藏单元相当于隐式做了多任务学习。对负荷预测、设备劣化预测这类输入相关性强的场景这个优势在数据量不太大时尤其明显。2.2 滑窗与时间平移lookback、horizon 如何对齐输入输出序列数据准备是这套流程里最容易被低估的一步。BiLSTM 输入要求是 1×N 的 cell 数组每个 cell 里是一个二维矩阵行是特征维列是时间步。输出如果做多步预测常见做法是让响应序列和输入序列长度一致但把响应窗口整体向后平移 horizon 步。这样网络的每个时间步都能得到一个监督信号而且不需要编码器-解码器结构。下面这个函数是整套流程的核心按“过去 lookback 步预测未来 horizon 步”的方式切窗function [XCell, YCell] makeShiftedSequenceData(data, lookback, horizon, numFeatures, numResponses) % data: n×p 矩阵前 numFeatures 列为输入特征之后 numResponses 列为响应 % XCell: 1×N cell每个元素是 numFeatures×lookback % YCell: 1×N cell每个元素是 numResponses×lookback n size(data, 1); numSamples n - lookback - horizon 1; XCell cell(numSamples, 1); YCell cell(numSamples, 1); for i 1:numSamples XCell{i} data(i : i lookback - 1, 1 : numFeatures); YCell{i} data(i horizon : i lookback horizon - 1, ... numFeatures 1 : numFeatures numResponses); end end这里最关键的是 YCell 的窗输入窗口覆盖原始数据的第 i 到 ilookback-1 行输出窗口则从 ihorizon 行开始持续 lookback 步。两个窗口长度一致所以 trainNetwork 的 sequence-to-sequence 回归能正常计算逐时间步损失而它们之间偏移了 horizon 行等价于让网络学“看到这段输入后输出往后错开 horizon 步的响应序列”。预测时取输出序列的最后一列得到的就是输入窗口末尾再往后 horizon 步的预测值。lookback 和 horizon 怎么定没有公式能一步到位。我一般先用业务周期定下界比如日负荷数据 lookback 至少取 24然后在这个基础上做二到三次对比实验。horizon 则由业务需求决定预测未来 3 步就把 horizon 设为 3如果预测步数太长直接多步输出误差会变大不如后面用滚动递推。2.3 归一化和数据切分统计量只从训练段算时间序列划分训练集、验证集、测试集时不能随机洗牌必须按时间顺序切否则未来信息会泄漏进训练过程。切分之后还有一个容易翻车的细节归一化的统计量必须只用训练段计算再应用到验证和测试段。用整个数据集算 mu 和 sigma测试集的分布信息就被带进了训练管线验证集效果会虚高。raw readtable(data.csv); data raw{:,:}; n size(data, 1); numFeatures 5; % 输入特征个数 numResponses 2; % 输出响应个数 trRows round(n * 0.7); vaRows round(n * 0.85); % 只用训练段计算统计量避免信息泄漏 muX mean(data(1 : trRows, 1 : numFeatures)); sigX std(data(1 : trRows, 1 : numFeatures)); muY mean(data(1 : trRows, end - numResponses 1 : end)); sigY std(data(1 : trRows, end - numResponses 1 : end)); % 整段数据用训练段的统计量归一化 dataNorm data; dataNorm(:, 1 : numFeatures) (data(:, 1 : numFeatures) - muX) ./ sigX; dataNorm(:, end - numResponses 1 : end) ... (data(:, end - numResponses 1 : end) - muY) ./ sigY; lookback 24; % 过去 24 步 horizon 3; % 预测未来第 3 步 [XTrain, YTrain] makeShiftedSequenceData(dataNorm(1 : trRows, :), ... lookback, horizon, numFeatures, numResponses); [XVal, YVal] makeShiftedSequenceData(dataNorm(trRows 1 : vaRows, :), ... lookback, horizon, numFeatures, numResponses); [XTest, YTest] makeShiftedSequenceData(dataNorm(vaRows 1 : end, :), ... lookback, horizon, numFeatures, numResponses);这里有个小约定我默认响应变量排在整个矩阵的最后 numResponses 列。如果原始数据里响应列混在中间记得把上面的列索引全部换成实际列号不要想当然用 end 往前推。做完这一步XTrain 是 1×N 的 cell里面每个矩阵都是 5×24YTrain 是 1×N 的 cell每个矩阵是 2×24。尺寸对不上trainNetwork 会直接报错所以搭建网络之前先确认一下 size。3. 搭建 BiLSTM 网络双向层、全连接层和回归输出的配置3.1 最小可运行的 BiLSTM 回归网络数据切好之后网络本身反而简单。一个可直接跑通的最小结构是序列输入层 → BiLSTM 层输出每个时间步的隐状态→ Dropout → 全连接层 → 回归层。下面是完整定义numHiddenUnits 96; dropoutRate 0.2; layers [ sequenceInputLayer(numFeatures, Name, in) bilstmLayer(numHiddenUnits, OutputMode, sequence, Name, bilstm) dropoutLayer(dropoutRate, Name, dropout) fullyConnectedLayer(numResponses, Name, fc) regressionLayer(Name, reg)];我特意没有在 sequenceInputLayer 里加 Normalization, zscore因为第 2 节已经手工把输入输出都做了 z-score。如果你不想手工处理输入也可以在输入层加 zscore但输出层没有对应的归一化机制响应侧还是得自己保存 muY 和 sigY。两边的归一化方式不一致后面反归一化时容易漏掉其中一侧。这个网络对每个输入样本做的事情是先由 BiLSTM 沿时间正向和反向各扫一遍再把两个方向的隐状态拼起来于是每个时间步的输出维度是 2×numHiddenUnits 192全连接层把这个 192 维向量压到 numResponses 2 维得到该时间步的两个响应预测回归层对全部时间步的预测值和真实值算 MSE 损失。3.2 双向层必须搞清楚的三个参数bilstmLayer 有三个参数决定了整个网络的形状和训练行为新手上手前值得花几分钟看一遍文档因为它们直接决定了输入输出尺寸。第一个是 numHiddenUnits。它表示每个方向上的隐藏单元数不是双向合并后的总数。也就是说 numHiddenUnits 设为 96实际隐状态维度是 192。很多人在打印网络结构或者写全连接层时数错维度就是这个原因。选择上数据量小几千个样本用 32 到 64数据量大再用 128 以上。双向层的参数量是单向的两倍同样的训练数据双向更容易过拟合。第二个是 OutputMode。设成 sequence 时输出每个时间步的隐状态适合 sequence-to-sequence 回归也就是第 2 节的数据形式设成 last 时只输出最后一个时间步适合 sequence-to-one比如每次只预测未来一个点。两类结构没有绝对的谁更好last 计算量小、建模简单sequence 能提供更密集的梯度信号。第三个是序列长度的灵活性。MATLAB 允许不同样本的序列长度不同只要每个 cell 里的矩阵宽度时间步数可变。同样的滑窗切分后所有样本长度都一样这个自由度很少用但了解它能帮你理解为什么输入必须是 cell 数组而不是一个大矩阵——cell 是给变长序列留的接口固定长度只是它的特例。3.3 全连接层和回归层逐时间步映射多个响应BiLSTM 输出序列之后fullyConnectedLayer(numResponses) 做的是逐时间步的线性映射。也就是说每个时间步的 192 维隐状态向量独立乘同一个权重矩阵变成 2 维响应向量。整个序列共 lookback 步所以全连接层输出是 numResponses×lookback 的矩阵和 YCell 里每个元素的尺寸严格对应。这里有一个新手常卡住的点既然输入窗口是 24 步、输出窗口也是 24 步那最终预测结果是哪个输出序列的每一列理论上都是有效的响应预测但它们的语义不同——第一列对应输入窗口开头向后平移 horizon 步的值最后一列对应输入窗口末尾向后平移 horizon 步的值。做未知未来预测时可用的只有完整输入窗口所以取最后一列作为预测结果。代码里我会用 ySeq(:, end) 来取第 4 节会看到。一个值得养成的习惯是先用单向 LSTM 搭出基线再替换成 bilstmLayer 对比一次。这样你能明确知道双向结构在这个数据上带来了多少提升而不是一上来就上双向。双向层训练时间约为单向的两倍如果提升不到 5% 到 10%在工程上可能要重新权衡。4. 训练与预测超参数调优和稳定性控制4.1 训练选项逐项说明这些参数决定能不能收敛trainNetwork 的训练选项看似一堆真正需要手调的其实就五个学习率、epoch 数、mini-batch、梯度阈值和学习率衰减策略。下面是我在类似任务上常用的配置options trainingOptions(adam, ... MaxEpochs, 120, ... MiniBatchSize, 32, ... InitialLearnRate, 0.002, ... GradientThreshold, 1, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.2, ... LearnRateDropPeriod, 40, ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 10, ... Shuffle, every-epoch, ... Plots, training-progress, ... Verbose, false); net trainNetwork(XTrain, YTrain, layers, options);InitialLearnRate 0.002 是比较稳的起点Adam 的默认学习率偏激进很多序列回归任务用 0.01 会看到 loss 在初期震荡然后一路发散。GradientThreshold 1 做梯度裁剪双向 LSTM 时间步长 24 步反向传播路径长梯度范数很容易在几个 batch 内冲到几十不裁剪的话 loss 曲线会突然跳一个尖峰。LearnRateDropPeriod 和 DropFactor 配成“每 40 个 epoch 学习率降到原来的 20%”这个组合在大多数多输出回归问题上都能让 loss 在后期稳定下降而不是来回抖动。ValidationFrequency 10 表示每 10 次迭代在验证集上算一次误差这个值太大会让你错失过拟合拐点太小会拖慢训练。训练时打开 Plots, training-progress。Loss 曲线在 120 epoch 内通常呈现三个阶段前 10 到 20 个 epoch 快速下降中间一段缓慢下降最后如果验证集 RMSE 开始抬头说明过拟合开始占据了主导。碰到这种情况把 MaxEpochs 砍到拐点附近或者调大 dropoutRate。4.2 预测与反归一化最后一个时间步才是预测结果训练完成后predict 返回的 YPredSeq 是一个 1×N 的 cell每个元素是 numResponses×lookback。取最后一列作为预测值再用第 2 节保存的 muY、sigY 反归一化才能和真实值比较YPredSeq predict(net, XTest, MiniBatchSize, 32); YTrue zeros(numResponses, length(XTest)); YPred zeros(numResponses, length(XTest)); for i 1:length(XTest) YPredictSeq YPredSeq{i}; % numResponses × lookback YPred(:, i) YPredictSeq(:, end); % 取最后一个时间步 YTrue(:, i) YTest{i}(:, end); end % 反归一化回原始量纲 YPredDenorm YPred .* sigY muY; YTrueDenorm YTrue .* sigY muY; rmsePerOutput sqrt(mean((YPredDenorm - YTrueDenorm).^2, 2)); disp(rmsePerOutput);这里的 YPredDenorm 是 numResponses×N 的矩阵每行对应一个输出变量的预测序列。注意 sigY 和 muY 是 1×numResponses 的行向量要和 numResponses×N 矩阵做逐行运算必须先转置成列向量。漏掉这一步或者转置方向搞反是反归一化最常出现的低级错误。如果预测结果和真实值画在一起你会看到一个挺扎心的现象第一个输出变量拟合得很好第二个输出变量的误差明显偏大。这通常不是你网络结构的问题而是两个响应变量的可预测性本身不同。此时去看每一行的 RMSE把差的输出单独拿出来分析特征相关性比盲目加深网络有用得多。4.3 多步预测的滚动递推法何时该用平移对齐的直接多步法适合 horizon 固定的场景但如果你要预测的未来步数很长比如 24 步以后输出窗口会拖得很长误差在时间维度上累积。另一种常见做法是只训练 horizon1 的单步网络再把预测值当成下一次输入的一部分滚进去% 假设 net1 训练时 horizon1输出序列长度为 lookback xWindow dataNorm(end - lookback 1 : end, 1 : numFeatures); % numFeatures×lookback future zeros(numResponses, numSteps); feedbackRows % 响应变量在特征矩阵中的行号例如 1:numResponses for t 1:numSteps yPred predict(net1, {xWindow}); yPred yPred{1}(:, end); % 单步预测 numResponses×1 future(:, t) yPred; xWindow xWindow(:, 2 : end); % 窗口整体左移一步 xWindow(feedbackRows, end) yPred; % 把预测值写回窗口尾部 % 窗口里如果还有其他外部输入特征需要补充未来对应时刻的真实值或估计值 end这段代码有一个前置条件待预测的响应变量必须同时是输入特征的一部分否则“把预测结果拼回窗口”不成立。比如你预测的是负荷输入特征里有历史负荷那可以把预测负荷写回如果你预测的是设备寿命输入里根本没有寿命这个特征滚动递推就没有支点。这类场景用第 2 节的平移对齐法更合理。5. BiLSTM踩坑与排查五个常见翻车场景5.1 训练 loss 不降反升现象trainNetwork 的 loss 曲线在前几个 epoch 冲到 10 以上然后一直震荡或者干脆发散验证集 RMSE 完全不合理。原因最常见的是学习率过大或输出侧没有归一化。BiLSTM 对量纲很敏感响应变量如果一个是 0.1 级别、一个是 1000 级别MSE 会被大数值变量主导梯度方向直接跑偏。解决把 InitialLearnRate 降到 0.001 或 0.0005并确认 YCell 用的是归一化后的数据而不是原始数据。还有一个很快的定位手段先把训练集切一小段出来比如只取 200 个样本看 loss 能否降。如果小样本也发散问题基本在数据预处理或网络配置而不是数据量。5.2 测试集预测值整体滞后现象预测曲线和真实曲线形状一致但整体向右平移了一个窗口看起来“预测跟随在真实值后面跑”。原因这是自回归类时序预测的经典陷阱。当响应变量的历史值本身就是输入特征时模型学到的可能不是因果规律而是“复制最近值”。特别是 lookback 太短、响应变量在输入里占比过高时BiLSTM 发现最省力的拟合方式是记住最后一个时间步的响应值直接输出。解决先看特征相关性确认输入的响应历史值和待预测值是否存在强自相关把 lookback 加大到覆盖一个完整业务周期切断短期复制路径更彻底的做法是用第 2 节的平移对齐法让输出窗口和输入窗口之间有明显 offset网络被迫学习跨步映射。5.3 GPU 显存或内存不足现象训练跑到一半报 “out of memory”或者 MATLAB 直接卡死无响应。原因BiLSTM 的反向传播要缓存两个方向的中间状态显存占用大约是单向 LSTM 的两倍。lookback 长、mini-batch 大、hidden units 多这三个因素叠在一起很容易把显存打满。解决首先把 MiniBatchSize 从 32 减到 16 或 8这是性价比最高的手段其次看 lookback 是否可以减半配合 downsampling最后再考虑降 numHiddenUnits。如果你的数据量没到需要 256 个隐藏单元的程度用 64 或 96 通常就够了。5.4 预测曲线过度平滑接近均值现象预测结果几乎是一条平缓的曲线真实值的波峰波谷全部被抹平。原因输出侧归一化后MSE 的局部最优可能落在“输出平均值”附近尤其是多步预测任务里模型的确定性输出会趋向期望值另一个常见原因是 dropout 太高训练时网络能力被过度弱化。解决把 dropoutRate 从 0.2 降到 0.1 或者试 0.05检查输出响应是否经过 z-score如果响应本身方差很小预测结果自然贴近均值。这类现象在时序预测里很常见不要急着加网络深度先观察训练集上的拟合误差是否也偏大如果训练集上也平滑说明是模型容量或 dropout 的问题。5.5 双向层训练时间翻倍收益却很小现象换成 BiLSTM 后训练时间直接翻倍验证集 RMSE 只降了 2%。原因双向网络在每个时间步要维护两个方向的状态参数数量和计算量都翻倍但如果数据的时间依赖主要是单向的后向方向的收益就不明显。解决先做单向 LSTM 基线记录训练时间和验证误差再切到 BiLSTM 对比。收益小于 5% 时我一般会保留单向模型因为部署和后续调试成本更低。这个对比实验只需要改一个层名10 分钟就能跑完但能帮你在“追效果”和“控成本”之间做出明确选择。6. 进阶技巧自定义训练循环与多输入分支6.1 多输入分支什么时候用得上trainNetwork 只支持一个序列输入如果你有两个采样率不同、物理含义差异很大的输入源比如一组高频振动信号和一组低频工况参数把它们拼成一个特征矩阵会丢失维度间的结构信息。这时可以用层图构建双输入网络两路输入各过一个 BiLSTM再把隐状态拼接起来进全连接层。从 MATLAB R2021a 之后的版本开始dlnetwork 对多输入的支持已经比较成熟自定义训练循环是常见做法。numFeat1 3; numFeat2 2; lgraph layerGraph(); lgraph addLayers(lgraph, [ ... sequenceInputLayer(numFeat1, Name, in1) bilstmLayer(32, OutputMode, last, Name, bilstm1)]); lgraph addLayers(lgraph, [ ... sequenceInputLayer(numFeat2, Name, in2) bilstmLayer(32, OutputMode, last, Name, bilstm2)]); lgraph addLayers(lgraph, [ ... concatenationLayer(1, 2, Name, cat) fullyConnectedLayer(numResponses, Name, fc)]); lgraph connectLayers(lgraph, bilstm1, cat/in1); lgraph connectLayers(lgraph, bilstm2, cat/in2); dlnet dlnetwork(lgraph);注意这里的 dlnetwork 不包含 regressionLayer因为回归层的损失计算要放在自定义训练循环里自己写。concat 沿第一个维度拼接两个 32 维隐状态得到 64 维向量再由全连接层映射到 numResponses 个输出。6.2 模型函数与自定义训练循环骨架多输入 dlnetwork 的 forward 顺序要和网络输入层顺序一致这里输入顺序就是 in1、in2。模型函数返回 loss 和梯度训练循环里用 dlfeval 自动求导function [loss, gradients] modelLoss(dlnet, X1, X2, Y) YPred forward(dlnet, X1, X2); loss mse(YPred, Y); gradients dlgradient(loss, dlnet.Learnables); end主循环里把数据打包成 dlarray格式用 CTB通道、时间、batchfor epoch 1:80 % 这里省略 mini-batch 切分X1/X2/Y 是当前 batch 的 dlarray [loss, gradients] dlfeval(modelLoss, dlnet, X1, X2, Y); [dlnet, avg1, avg2] adamupdate(dlnet, gradients, avg1, avg2, epoch, 0.002); endadamupdate 的三个参数分别是网络可学习参数、梯度和两个动量项。第一次调用时初始化为空数组即可。这个方案的代价是没有 trainNetwork 自带的验证、进度图和早停训练过程的监控需要自己写适合已经把主流程跑通、想进一步定制结构的阶段。6.3 快速验证网络正确性的一个技巧无论用 trainNetwork 还是自定义循环我都建议先跑一个线性自检用随机输入构造一组完全线性的多输出数据如果网络连这种映射都学不出来问题一定在数据管线或网络配置而不是模型能力不足。rng(42); N 1500; x randn(N, 3); y zeros(N, 2); y(:, 1) 2 * x(:, 1) - 0.5 * x(:, 2) 1; y(:, 2) x(:, 3) x(:, 1); data [x, y];然后把 data 丢进第 2 节的滑窗和第 3 节的网络hidden units 设 32跑 30 个 epoch训练集 RMSE 应该轻松降到 0.05 以下。如果降到 0.1 就停住不动十有八九是 YCell 对齐错了——去查输出窗口的索引而不是怀疑网络结构。这个自检我每次换数据格式都会先跑一遍已经帮我省下了不少“调了两天网络最后发现是数据切错”的排查时间。MATLAB 里做 BiLSTM 多输入多输出预测难点集中在数据对齐和超参数上这两块摸清之后剩下的就是反复实验的工程活了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?