ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MATLAB实现BiLSTM多输入单输出回归预测:从数据构造到超参数调优

MATLAB实现BiLSTM多输入单输出回归预测:从数据构造到超参数调优 简介面向MATLAB用户的回归预测实战资源采用双向长短期记忆网络BiLSTM实现多输入单输出建模适合有基本机器学习概念、希望快速落地时序或连续值预测任务的开发者学习与复用。压缩包共6个文件核心MATLAB源码负责网络搭建与训练配套Excel数据文件提供可直接运行的输入样本三张训练过程图和一份结果说明文档则辅助评估模型收敛情况及预测效果整体结构清晰、开箱即用。包体仅453KB便于快速下载与本地运行在MATLAB 2018b及以上版本中即可直接运行。已有2056人学习/下载经过实践检验。利用该资源可完整走通数据预处理、BiLSTM模型构建、训练与结果可视化的流程同时理解双向LSTM如何捕获前后文信息从而提升多输入场景下的回归预测精度尤其适合教学演示与科研初步验证。1. 回归预测里的 BiLSTM从“记忆过去”升级成“同时看两头”多输入单输出回归预测常见于设备健康度、电池SOC、工艺参数监控等需要根据连续观测量估计一个连续结果的场景。对这类序列数据单向LSTM读取顺序信息可当前时刻的准确估计有时依赖更宽的时间上下文。BiLSTM即双向长短期记忆神经网络同时以正序和逆序扫描窗口把两个方向隐藏状态拼到一起因此能从同一个滑动窗口里获得“前后”信息。这篇博客提供一套在MATLAB中直接落地的方案从数据重组到训练参数再到评估和进阶部署尽量一次讲清。如果搜过“bilstm代码matlab soc”这类关键字你大概见过不少把XTrain变成三维矩阵的代码。但训练BiLSTM所用的深度学习工具箱Deep Learning Toolbox默认输入是N×1的cell每个cell内部是“特征数×时间步”的矩阵。只有理解了这个数据组织方式所谓的“完整源码和数据”才能在你自己的数据集上跑通而不是直接报维度错误。下文的示例代码均假设你已经导入一份二维表每一行是一个时间采样点各特征为列回归目标在最后一列。我尽量不只给代码还解释为什么这样设置比如训练时要不要Shuffle、最后一层bilstmLayer该用last还是sequence、归一化是否只允许用训练集的统计量。这些细节决定了模型在验证集上是漂亮的曲线还是无法落地的纸面结果。如果你对LSTM已有基础可以直接跳到第4节的超参数检查表如果是第一次用MATLAB做回归建议从第2节的数据格式开始按顺序阅读。2. BiLSTM 的结构与多输入单输出的数据格式双向LSTM不是两个独立LSTM依次训练也不是模型集成。它在同一个网络内并行维护两个循环核方向一正一反最后在隐藏层做拼接。在时间步 t 中正向隐态 \overrightarrow{h}_t 与反向隐态 \overleftarrow{h}_t 都先由各自方向LSTM单元更新再拼成输入给后面回归头的向量 [\overrightarrow{h}_t; \overleftarrow{h}_t]。如果两层均为64单元那么全连接层接收到的就是128维输入。在回归任务中这种拼接让模型的可拟合函数类更大但训练耗时和过拟合成本也相应上升。如果输入变量之间是严格因果关系的比如控制指令只能依赖历史值使用双向结构反而会引入因果泄漏。不过我们讨论的“多输入单输出”是闭式回归模型接收一段长度为 seqLen 的滑窗目标是预测窗内最后一个时刻之后的下一个输出。窗口右侧的数据在推理时已经采集到不属于未来因此BiLSTM的双向信息是合理且几乎不增加部署难度。2.1 从 LSTM 到 BiLSTM回归头看到的“特征”多了哪些纯LSTM单元内部有遗忘门、输入门、输出门但这里可以不多写门的公式。重点在于信息流动单向LSTM输出只包含该时间点之前的信息经非线性压缩后的摘要即使有记忆单元序列末尾的隐态仍受到方向限制。BiLSTM相当于把序列倒过来再读一遍然后把两个方向的编码连接起来因此模型在 t 时刻看到的是窗口内左右各 seqLen 长度范围内的信息。对周期性信号、波形或传感器瞬态变化这种窗口内“过去-未来”上下文能显著降低单点估计的模糊性。在MATLAB中bilstmLayer(64)一句便会自动完成双向拼接不需要手动去拼 h 和 h。bilstmLayer包含正向和反向两套权重所以训练时间和模型文件大小约为单向LSTM的两倍。数据量较少时建议从32个隐藏单元起步不要一开始就堆到128。2.2 MATLAB 的 sequence-to-one 回归格式N×1 cell 还是 3D 数组这是最大的易错点。Deep Learning Toolbox 对序列输入期待的是 N 个观测值组成的 cell 数组每个观测是一个numFeatures × numTimeSteps矩阵。如果写inputSize [numFeatures, seqLen, 1]并把它直接塞给 trainNetwork那是在写 Conv 层而不是 RNN对 BiLSTMcell 数组是第一选择。另一个容易忽略的点是这里“时间步”不是原始数据表结构的行。“行”是特征快照。如果原始数据表有 n 行 m 列列包含7个特征和1个标签那么每个窗口输入是一个 7×seqLen 矩阵。构建方式是对行做平移滑窗而不是对列做循环。% data: n行, 最后一列为回归目标 y seqLen 10; % 特征列与目标列分离 features data(:, 1:end-1); target data(:, end); n size(features, 1); numFeatures size(features, 2); % 滑窗构造训练输入 XTrain cell(n - seqLen, 1); YTrain zeros(n - seqLen, 1); for i 1:n - seqLen % 取第 i 到 iseqLen-1 行转置成 numFeatures × seqLen XTrain{i} features(i:iseqLen-1, :); % 预测第 iseqLen 行的目标值 YTrain(i) target(iseqLen, 1); end % 先对整个序列做 zscore便于快速迭代 features zscore(features); target zscore(target);这段代码的关键在于features(i:iseqLen-1, :)。它把一个二维块从原始数据中切出后转置让同一特征的值沿行排列时间步沿列排列。YTrain(i)对应窗口右移一步后新进入的标签值因此模型学习的是“过去 seqLen 个观测到下一时刻输出”的映射。如果只是预测当前时刻的标签而窗口已经包含当前时刻那就成了普通的静态回归时间信息没有真正参与计算。搜索“bilstm代码matlab soc”时你会发现许多人会把当前SOC也拼进特征甚至把测试集一起做成窗口这样训练线上再漂亮的验证曲线也容易在部署时崩塌。窗口数据和标签的结构可以总结如下表名称维度举例seqLen10, numFeatures7XTrain{i}numFeatures × seqLen7×10XTrain整体N×1cell arrayN×1YTrainN×1vectorN×1如果你从GitHub拿到源码把XTrain定义成n×seqLen×features那大概率是配合sequenceInputLayer加自定义训练循环使用不能直接进trainNetwork。验证这个判断最快的方式是训练结束后查看net.Layers(2).InputSize如果返回[7 10]说明输入层接收的是特征数和时间步而不是 N 个观测。2.3 训练/验证/测试集划分时间序列不能随机 Shuffle由于滑窗样本之间存在重叠相邻样本相关性很强不能用randperm直接洗牌。常见做法是按时间顺序切分。以下代码在已构造完成的XTrain、YTrain上操作numTotal length(YTrain); numTrain floor(0.7 * numTotal); numValid floor(0.15 * numTotal); idxTrain 1:numTrain; idxValid numTrain1:numTrainnumValid; idxTest numTrainnumValid1:numTotal; XTr XTrain(idxTrain); YTr YTrain(idxTrain); Xva XTrain(idxValid); YVa YTrain(idxValid); XTe XTrain(idxTest); YTe YTrain(idxTest);这里先按比例划分再把XTrain中对应索引取出来。注意不能先对XTrain做zscore再划分因为全局归一化会把测试集的均值方差也带进来。第4.2节会专门讲数据泄漏问题。窗口长度为10时前10个样本无法构造输入所以实际可训练样本总数是n - seqLen不是原始 n。3. 用 MATLAB 实现 BiLSTM 回归网络结构、训练选项与预测这里给出一个能够直接替换数据、改动少量参数即可运行的最小网络结构。所有代码基于 Deep Learning Toolbox不需要额外安装第三方工具箱。3.1 网络层定义我一般会这样搭回归网络layers [ sequenceInputLayer(numFeatures, Name, sequence_in) bilstmLayer(64, OutputMode, last, Name, bilstm) dropoutLayer(0.2, Name, dropout) fullyConnectedLayer(32, Name, fc1) reluLayer(Name, relu) fullyConnectedLayer(1, Name, fc_out) regressionLayer(Name, reg_out) ];sequenceInputLayer(numFeatures)的输入是每个时间步的特征数量也就是之前构造窗口中特征矩阵的行数。bilstmLayer(64)中的64是当前隐藏单元数它同时定义正向和反向层的单元数所以实际隐状态拼接后维度是128。OutputMode设为last表示只取序列最后一个时间步的输出。对于单步预测这是正确选择如果使用sequence则会输出每个时间步的预测主要用于编解码或逐点预测任务。dropoutLayer(0.2)放在全连接层前可以防止训练曲线消耗训练速度但验证集指标停滞。fullyConnectedLayer(1)把特征压缩成单个连续值regressionLayer使用半均方误差作为损失函数。对回归问题不要再在其他位置添加softmaxLayer或classificationLayer。3.2 训练选项参数说明options trainingOptions(adam, ... MaxEpochs, 150, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.2, ... LearnRateDropPeriod, 50, ... GradientThreshold, 1, ... Shuffle, never, ... ValidationData, {Xva, YVa}, ... ValidationFrequency, 20, ... Plots, training-progress, ... Verbose, true); net trainNetwork(XTr, YTr, layers, options);这里的Shuffle必须设为never因为滑窗样本存在时间重叠随机打乱会让相邻窗口的相似样本同时出现模型会低估验证误差。对具有明显时间依赖的数据once也不推荐因为never能保证每个 epoch 都看到同样的顺序训练过程更可复现。GradientThreshold 1是避免梯度爆炸的常用手段尤其当 seqLen 加大、网络加深时损失曲线上突然出现NaN的概率会增加。MiniBatchSize不是越大越好对长序列数据过大的 batch 会让显存峰值激增对只有一条连续时序切出的样本相邻窗口高度重叠过小的 batch 反而会增加训练波动。一般先从 16 或 32 开始观察验证曲线再调整。下面用表格汇总几个最关键参数的作用参数推荐范围影响MaxEpochs100~300太大容易过拟合配合验证曲线早停MiniBatchSize16~64梯度稳定性与显存占用InitialLearnRate0.001~0.01过大训练震荡过小收敛缓慢LearnRateDropPeriod40~60学习率衰减间隔决定后期精细度GradientThreshold1防止梯度爆炸Shufflenever保持时间顺序避免数据泄漏3.3 用 predict 完成多输入单输出预测训练完成后用predict而非classify。由于前面先对 target 做了zscore预测结果是归一化值需要反变换回真实量纲YPredNorm predict(net, XTe); YPred YPredNorm * sy muY; YTest YTe * sy muY; % 若 YTe 本来未归一化则不需要反变换 rmse sqrt(mean((YPred - YTest).^2));这里的muY和sy是第2.2节中计算 target 均值与标准差时保留的变量。如果忘了保留可以在构造窗口前先保存一份muY mean(target)sy std(target)。predict返回的是一个列向量顺序与XTe一致。不要用YPred net(XTe)这种方式它处理的是 dlnetwork 或自定义循环普通trainNetwork网络用predict最稳妥。4. BiLSTM 回归预测的关键超参数选择与防过拟合当代码能跑通下一个要解决的就是“为什么我的验证集曲线很高、测试集结果不理想”。这通常可以从超参数范围和数据泄漏两个方面排查。4.1 超参数检查表在动手调参之前先把搜索空间按经验值固定下来。通过检索“多输入单输出”相关项目最多被修改的是 hiddenUnits、seqLen 和 MiniBatchSize。相互依赖很强具体建议如下表超参数建议范围对结果的影响hiddenUnits32~128越大表达力越强但过拟合速度更快seqLen10~30记忆窗口长短可通过偏自相关函数辅助确定Dropout0.1~0.3正则化强度0.2 是常见起点InitialLearnRate0.001~0.01控制参数更新步长MiniBatchSize16~64影响梯度噪声和显存占用MaxEpochs100~300配合早停不要一刀切固定hiddenUnits从32开始。如果验证 RMSE 没有明显下降再往64、96加。加的同时把Dropout从0.2提升到0.3否则训练集曲线很快逼近0验证曲线却波动。seqLen不是越大越好过长的窗口会引入无关噪声并增加训练时间且与 BiLSTM 双向结构的感受野叠加后数据需求呈指数增长。学习率方面Adam优化器对初始学习率不太敏感但仍建议从0.005开始。若损失曲线上出现周期性尖峰把学习率调整为0.001。学习率衰减周期可以设为 MaxEpochs 的三分之一比如150个 epoch 时LearnRateDropPeriod设为50。4.2 数据泄漏这是“能跑但不可信”的真正原因在滑动窗口回归中数据泄漏往往发生在两步第一步先对整个数据集归一化第二步使用整个数据集做交叉验证并随机分割。看一下错误示范% 错误示范 allData zscore(data); X allData(:, 1:end-1); y allData(:, end); % 然后 random split这个做法会让测试集的均值和方差参与归一化形成一种虚拟的“未来信息见”状态。正确做法是只对训练段计算统计量再将其应用到验证与测试段muF mean(features(1:numTrain, :), 1); sf std(features(1:numTrain, :), 0, 1); muY mean(target(1:numTrain)); sy std(target(1:numTrain)); features (features - muF) ./ sf; target (target - muY) ./ sy;“先划分索引再计算训练段统计量再归一化”的做法能保证验证集和测试集的数据分布只是被变换而不参与模型参数拟合。严格来说验证集也会影响超参数选择所以这仍然是一种轻量泄漏但在实践中可接受。评估指标则通过以下代码一起计算YPredN predict(net, XTe); YPred YPredN * sy muY; YTeReal YTe * sy muY; RMSE sqrt(mean((YPred - YTeReal).^2)); MAE mean(abs(YPred - YTeReal)); SSres sum((YTeReal - YPred).^2); SStot sum((YTeReal - mean(YTeReal)).^2); R2 1 - SSres / SStot; fprintf(RMSE%.4f, MAE%.4f, R2%.4f\n, RMSE, MAE, R2);RMSE 对大误差惩罚更重MAE 则反映平均偏差R2 越接近1表示拟合优度越好。在目标值变化范围很小时R2 会天然偏低不能单看一个指标下结论。应同时观察残差是否随机分布在零附近。4.3 交叉验证的时间顺序陷阱普通cvpartition或crossvalind在做 K 折时会随机打乱样本顺序。对时窗重叠构造的样本这种打乱违背了时间顺序模型在训练时可以看到未来验证集的同类片段。时间序列交叉验证的正确做法是滚动预测式划分第一折在最早 40% 数据上训练预测接下来 10%第二折把训练范围扩到 50%预测再接下去的 10%依此类推。在MATLAB中手写滚动验证并不复杂。先按时间顺序划分 fold startfoldNum 5; foldSize floor(numTotal / foldNum); rmseFold zeros(foldNum, 1); for f 1:foldNum trainEnd f * foldSize; valStart trainEnd 1; valEnd min(trainEnd foldSize, numTotal); netFold trainNetwork(XTrain(1:trainEnd), YTrain(1:trainEnd), layers, options); predFold predict(netFold, XTrain(valStart:valEnd)); rmseFold(f) sqrt(mean((predFold - YTrain(valStart:valEnd)).^2)); end这里每折的训练集严格早于验证集验证集永远不会混入训练窗口。由于相邻窗口重复度高这种单步滚动验证仍会低估泛化误差但对比较不同超参数仍然有效。5. 只有一个序列时怎么给 BiLSTM 造出“多输入”特征很多读者拿到的不是多维特征表而是一个单变量时间序列比如一段振动值或温度值。但标题要求“多输入单输出”这时可以自己构造多输入。技巧是把目标变量的滞后值、滚动统计量、甚至一阶差分都作为输入特征列。5.1 用滞后特征矩阵把单序列展开成多维输入假设raw是长度 N 的单列向量。要预测当前时刻的值常见做法是把过去 p 个值作为特征。这里直接构造一个features矩阵行是样本列是滞后特征p 8; raw raw(:); features zeros(length(raw) - p, p); target zeros(length(raw) - p, 1); for t 1:length(raw) - p features(t, :) raw(t:tp-1); target(t) raw(tp); end % 加入滚动均值作为额外输入 windowMean movmean(features, 3, 2); features [features, windowMean];现在features的列数从 p 变成了 p滚动特征数这就是“多输入”的来源。每个样本有多个变量时间步则仍通过第2.2节的滑窗机制定义。注意如果raw已经被 zscoremovmean也需要在同类数据上计算。5.2 把构造好的多维矩阵重新送入 BiLSTM上面的features已经是传统的多输入单输出数据表。对它继续使用第2.2节的滑窗逻辑生成XTrain和YTrainseqLen 10; numFeatures size(features, 2); XTrain2 cell(size(features, 1) - seqLen, 1); YTrain2 zeros(size(features, 1) - seqLen, 1); for i 1:size(features, 1) - seqLen XTrain2{i} features(i:iseqLen-1, :); YTrain2(i) target(iseqLen, 1); end也就是先把单序列变成“多特征”表再在时间方向上窗口化。两个“多”的区别是前者是多个变量后者是多个时间步。BiLSTM 真正处理的是时间步所以输入层节点数仍等于numFeatures而不是seqLen。这一点经常被混淆。部署时如果只拿到当前窗口最后一个样本也要用同样的滞后计算得到features再调用predict。建议将这段逻辑封装成一个函数function yhat bilstm_predict_step(raw_last_p, net, p) % raw_last_p 是长度为 p 的当前窗口向量 feat [raw_last_p(:), mean(raw_last_p)]; yhat predict(net, {feat}); end这样预测时不用重新构造整个训练集只把最新一段数据转换成网络输入格式即可。对于在线预测场景这种封装能减少重复代码也能避免因为遗忘滚动均值长度、忘记转置而导致的维度错误。本文还有配套的精品资源点击获取
返回列表