贝叶斯优化LSTM超参数:MATLAB单变量时间序列预测实战
我第一次正经用LSTM做时间序列预测是在一个只有单列历史数据的现场记录任务上。手里就一张表每小时记录一个连续运行参数没有外部变量目标也只有一个预测未来取值。当时按网上最常见的方式直接套MATLAB默认LSTM参数训练集里loss降得挺快可验证集上一画图就露馅——预测曲线几乎是真实曲线的“向右平移”峰值压低谷值抬高一拉残差全是结构性的根本没法用。后来我把调参思路整个换掉不再靠手感一点点试而是改成贝叶斯优化LSTM方案在MATLAB 2021上跑通了完整的单输入单输出时间序列预测流程。这套方案解决的核心问题很直接LSTM这类模型超参数空间大、单次训练又贵人工试错太慢网格搜索又试不起贝叶斯优化恰恰能在二三十次训练内逼近一个可靠参数组合。这篇文章就来拆一拆我这版的完整流程包括数据窗口怎么切、网络结构怎么搭、bayesopt怎么和目标函数配合以及最后怎么评价这个模型是不是真的能用。1. 先把单输入单输出这个问题拆干净1.1 这类时序任务到底在预测什么很多人一听说“LSTM时间序列预测”下意识就以为把整段数据丢进网络让网络自己学规律就行。实际上我们做的单输入单输出回归任务定义要严谨得多。单输入指的是每一个时间步上网络只拿到一个特征值(x_t)不是那种“温度、湿度、风速、功率”组成的高维特征向量。单输出指的是网络在每个预测步只吐出一个值(\hat{y})可能是下一时刻的(x_{t1})也可能是指定步长之后的(x_{th})。我这一版做的是单步预测也就是用过去一段窗口预测紧挨着的下一个值。这个设定看着简单但后续的数据切片、网络输出维度、损失函数选择全都要围绕它来。很多人在这上面栽跟头是因为把单输入单输出理解成了“数据只有一列就能直接喂LSTM”忽略了LSTM期望拿到的是带时间结构的数据而不是一张无序的设计矩阵。1.2 滑动窗口是最标准的起点要让LSTM学习序列规律第一步是把一维时间序列变成“样本集合”。常用办法就是滑动窗口设定一个窗口长度(W)从序列开头往后滑每次取连续(W)个历史值作为模型输入下一个时刻的真实值作为标签。举个例子某段数据是t1: 12.4 t2: 13.1 t3: 12.8 t4: 13.5如果窗口长度(W3)那么样本1的输入是[12.4, 13.1, 12.8]标签是13.5。窗口继续滑一个时刻样本2的输入是[13.1, 12.8, 13.5]标签是下一时刻值。这里有个容易被忽略的点窗口长度(W)其实就是模型能看到的“记忆长度”。窗口太短网络看不到周期和趋势窗口太长样本量减少训练时间变长还可能引入过多老化信息。所以在实际工程里(W)本身就是第一个需要认真对待的超参数。我习惯先用自相关图和业务周期大致圈定一个范围再放进贝叶斯优化里决定是否动态调整。为了避免优化变量太多导致训练时间爆炸这一版我把(W)固定为业务上合理的24也就是按24小时周期建模。1.3 把序列数据处理成MATLAB能直接吃的cell数组MATLAB训练LSTM时最常用的输入格式是cell数组每个cell存放一个序列样本。对于单输入单输出如果走“滑窗即序列”的思路可以构造一个(1 \times W)的行向量作为单个样本sequenceInputLayer(1)表示每个时间步只有一个特征序列长度就是(W)。我来给出一段可以直接改用的数据预处理代码function [XTrain, YTrain] makeSlidingWindows(data, winSize, trainLen) % data: 单列时间序列已归一化 % winSize: 窗口长度 % trainLen: 用于训练的序列长度 trainData data(1:trainLen); XTrain cell(trainLen - winSize, 1); YTrain zeros(trainLen - winSize, 1); for t 1 : trainLen - winSize XTrain{t} trainData(t : t winSize - 1); % 1 x winSize YTrain(t) trainData(t winSize); end end注意几个细节数据必须是列向量data(t:twinSize-1)转成行向量因为cell内矩阵约定是“特征数×序列长度”。循环生成的每个cell都是一个独立样本这样做的好处是预测时也保持同样的窗口逻辑不需要维护LSTM内部状态的迭代更新鲁棒性更好。训练和验证切分时我习惯留出时间上靠后的完整一段做验证而不是随机抽取样本。时间序列数据最忌讳用未来的信息去预测过去随机打乱会让“未来样本”混进训练集评估结果虚高。2. 为什么我坚持用贝叶斯优化代替手动试参2.1 手动试参的典型翻车现场我第一次调LSTM时用的还是老套路固定隐藏单元数然后手动试学习率比如(0.01)不行换(0.001)再不行换(0.005)。每次改完参数都要完整训练一个模型再跑验证集算误差。一个模型三五分钟一天下来能试的组数非常有限。更麻烦的是LSTM的参数不是互相独立的。学习率调低了可能提高稳定性但需要更多训练轮数隐藏单元数增多了拟合能力上升但过拟合风险也上升dropout加多了可能让模型欠拟合。我总是在调好一个参数后发现另一个参数又变成了瓶颈来回拉扯最后得到一组“好像还行”的参数但根本没法确定它离最优有多远。后来我看清楚了这不是我偷懒而是方法本身不适合高维连续参数空间的搜索。人工经验只适合做区间粗筛不适合做精细优化。2.2 贝叶斯优化的核心直觉用不太数学的语言说贝叶斯优化的思想简单说就是“用尽可能少的试验找到尽可能好的参数”。它把LSTM训练过程当做一个昂贵的黑箱函数输入一组超参数输出一个验证集误差。黑箱内部虽然复杂但我们可以不断猜测这个函数的形状并在最有可能降低误差的位置做下一次试验。具体做法分两步根据已经试过的参数组合和对应的误差建立一个代理模型。常用的是高斯过程它不仅能给出某个参数组合下误差的估计值还能给出估计的不确定度。用一个采集函数决定下一次试验点。采集函数会平衡“开发”和“探索”一是去当前误差最低的区域附近继续深挖二是去不确定度大的区域看看是否有意外之喜。MATLAB 2021中的bayesopt函数已经把这些逻辑封装好了我只需要定义优化变量、写一个返回误差的目标函数然后告诉它最多做多少次试验。它内部会自行更新代理模型反复给出一组又一组待训练的超参数。对于LSTM这种单次评估耗时几分钟的任务二三十次试验足够逼近一个可靠的参数组合这是网格搜索很难做到的。2.3 网格搜放在这里为什么划不来网格搜索的逻辑是把每个参数的候选值列出来取所有组合全跑一遍。假设我们要调4个参数每个参数取10个档位就是(10^410000)次训练。哪怕LSTM一次训练只要1分钟也得连续跑快7天。随机搜索比网格搜索好一些但本质上依然是“盲试”。贝叶斯优化则会在每次试验后利用前面的全部信息动态调整下一个参数组合所以同样预算下收敛速度明显更快。调参方式单次评估信息利用适用参数维度30次试验内的收敛性手动试参低3个以下基本靠运气网格搜索低2个以下方案可行但浪费严重随机搜索中中等能覆盖部分区域贝叶斯优化高10个以内通常很好我用这套对比说服了自己既然LSTM训练成本摆在那里每次试验都必须带来新的信息贝叶斯优化就是最理性的选择。3. 用 MATLAB 搭一个能被反复调用的 LSTM 工程骨架3.1 数据处理四件套清洗、归一化、切分、滑窗在进贝叶斯优化之前我先把整个数据预处理流程固定下来避免每次试验都因为数据问题出现波动。第一步是清洗。原始记录里经常有空值、跳点不能直接进网络。我先做简单检查data readmatrix(field_data.csv); % 单列时间序列 data data(~isnan(data)); % 去掉空值 data data(~isinf(data)); % 去掉无穷值第二步是归一化。LSTM内部用的激活函数对输入幅度敏感不归一化的话数值大的特征会主导梯度。我选用z-score归一化也就是mu mean(data); sigma std(data); dataNorm (data - mu) / sigma;这里有个必须强调的细节mu和sigma只用训练集统计不要混入验证集和测试集。否则相当于让模型偷看了未来数据的分布信息评估结果不真实。第三步是切分。按时间顺序前70%训练中间15%做验证集供贝叶斯优化评估最后15%留作最终测试。时间序列不能用随机抽样的方式切分因为相邻样本之间存在重叠信息随机抽样会把未来数据混进训练集。第四步才是制作滑动窗口。这个函数刚才已经写过了训练、验证、测试三个集合都要单独调用生成对应的cell数组。3.2 一个适合SISO预测的最小网络结构对于单输入单输出我最常用的网络结构非常简单inputSize 1; % 每个时间步只有一个特征 numHiddenUnits 100; % 初始值后续交给贝叶斯优化 numOutputs 1; % 单输出 layers [ sequenceInputLayer(inputSize) lstmLayer(numHiddenUnits, OutputMode, last) fullyConnectedLayer(numOutputs) regressionLayer ];每个模块的作用我再解释一下sequenceInputLayer(1)告诉LSTM输入序列每个时间步上的特征维度是1。lstmLayer(numHiddenUnits, OutputMode, last)是核心记忆单元。OutputMode设为last表示只输出最后一个时间步的隐藏状态。因为我们要做的是“看完整个窗口后输出下一个值”而不是把每个时间步都输出一个预测。fullyConnectedLayer(1)把隐藏状态压缩成单个数值输出。regressionLayer计算回归损失也就是均方误差。这个结构足够小但也足够完成大多数单变量时间序列的单步预测。真正影响性能的不是结构复杂度而是超参数组合。3.3 训练选项先固定再留出变量接口trainNetwork的训练选项里也可以嵌入待优化变量。我通常固定几个基础选项减少贝叶斯优化维度options trainingOptions(adam, ... MaxEpochs, 200, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005, ... GradientThreshold, 1, ... Shuffle, never, ... Verbose, false, ... Plots, none);其中Shuffle我设置为never这是时间序列建模的一个重要决策。LSTM训练时如果每个epoch都打乱样本顺序会破坏序列内部的时间上下文而且打乱后的样本可能让模型的收敛过程变得不可控。对于滑窗样本虽然每个样本本身是独立的小序列但整个训练集顺序仍然承载着趋势信息所以我选择不打乱。这一步做完工程骨架已经可用了。接下来要把“隐藏单元数、学习率、批大小、dropout比例”这些关键超参数从固定值变成优化变量。4. 把 LSTM 变成贝叶斯优化的目标函数4.1 用optimizableVariable定义参数搜索空间MATLAB的贝叶斯优化是通过自定义变量对象来定义的。我这一版选了4个关键超参数vars [ optimizableVariable(hiddenUnits, [20 300], Type, integer) optimizableVariable(initialLearnRate, [1e-4 0.1], Type, real, Transform, log) optimizableVariable(miniBatchSize, [16 128], Type, integer) optimizableVariable(dropout, [0 0.5], Type, real) ];几点配置心得hiddenUnits用整数类型取值范围[20, 300]太小的网络学不到趋势太大则容易过拟合而且训练极慢。这个范围对大多数中小规模时间序列都够用。学习率用Typereal并设置Transformlog。因为学习率的最佳值通常在0.001到0.01之间而且它对数量级变化敏感按对数均匀采样比按线性均匀采样更合理。miniBatchSize也是整数变量我限制在16到128。批大小影响梯度稳定性和训练速度太小波动大太大收敛慢。dropout范围[0, 0.5]保留0表示不需要dropout。如果数据量特别大还可以考虑把lstmLayer层数也作为变量但那样目标函数内部要动态组合多层LSTM代码复杂度明显上升我建议先用单层LSTM跑通再逐步扩展。4.2 目标函数里的三个关键控制点贝叶斯优化的目标函数负责接收一组参数训练LSTM在验证集上返回一个误差标量。我需要单独写一个函数文件也可以直接在脚本里写一个匿名函数。trainAndEval (params) lstmObjective(params, XTrain, YTrain, XValid, YValid);对应的目标函数function loss lstmObjective(params, XTrain, YTrain, XValid, YValid) inputSize 1; numOutputs 1; layers [ sequenceInputLayer(inputSize) lstmLayer(params.hiddenUnits, OutputMode, last) dropoutLayer(params.dropout) fullyConnectedLayer(numOutputs) regressionLayer ]; options trainingOptions(adam, ... MaxEpochs, 150, ... MiniBatchSize, params.miniBatchSize, ... InitialLearnRate, params.initialLearnRate, ... GradientThreshold, 1, ... Shuffle, never, ... Verbose, false, ... Plots, none); net trainNetwork(XTrain, YTrain, layers, options); YPred predict(net, XValid); loss rmse(YPred, YValid); end这里有三个必须注意的控制点。第一目标函数内不要加ValidationData选项。因为trainNetwork自带的验证机制主要用于训练过程中的早停在贝叶斯优化的每次试验里都会额外消耗计算量而且它验证的样本定义可能与我们的滑窗验证集不一致。我选择在训练完成后单独调用predict再计算验证集误差这样逻辑最干净。第二误差函数必须返回一个标量。rmse函数在MATLAB R2021中是按列计算的需要确认输入是列向量。为保险起见我在计算前先把预测和目标都整理成列向量YPred predict(net, XValid); YPred YPred(:); YValid YValid(:); loss sqrt(mean((YPred - YValid).^2));第三每次试验都用固定随机种子初始化网络可以增加试验之间的可比性。不过trainNetwork内部的状态控制不完全由全局随机种子决定更可靠的做法是接受一定随机波动然后靠贝叶斯优化自身的稳健性去处理。我的经验是验证集误差波动在1%到3%以内不影响参数排序。4.3 跑一轮bayesopt之后怎么读结果表调用bayesopt的语句如下results bayesopt(trainAndEval, vars, ... MaxObjectiveEvaluations, 30, ... AcquisitionFunctionName, expected-improvement-plus, ... IsDeterministic, false, ... UseParallel, false);MaxObjectiveEvaluations设置的是最大试验次数。我一般设为30因为我的数据集只有几千个样本单次训练约1分钟30次跑完也就半小时到一小时完全能接受。跑完后我第一时间看两个值results.MinObjective实际观测到的最低验证集误差。results.XAtMinObjective对应这个最低误差的参数组合。我还会看一眼结果表观察后面的试验是否还在大幅刷新最低误差。如果最后十次试验都没有明显下降说明搜索基本收敛了如果直到最后一次还在大幅下降说明预算不够需要加大MaxObjectiveEvaluations。bayesopt画出来的图也很直观横轴是试验次数纵轴是误差散点是每次的真实观测值曲线是代理模型的估计。我通常会保存这张图用来判断优化过程和收敛性。5. 取回最优参数后的最终训练与真实评价5.1 用最优参数重新训练和回测贝叶斯优化返回的XAtMinObjective是一个结构体类似这样bestParams results.XAtMinObjective; bestHiddenUnits bestParams.hiddenUnits; bestLearnRate bestParams.initialLearnRate; bestMiniBatch bestParams.miniBatchSize; bestDropout bestParams.dropout;得到最优参数后我用它重新在完整训练数据上训练模型。这里有一个关键选择验证集数据要不要并回训练集我的做法是如果数据量很小为了最终模型效果可以把验证集并回训练集再训一遍。但评估最终模型时必须使用完全没有参与训练和优化的测试集。贝叶斯优化过程中已经间接使用了验证集信息如果再用验证集评估最终模型误差必然偏乐观。所以我在流程开始时就留出一段测试集放在最后单独见模型。回测时要保持和训练时相同的窗口制作方式。把测试集数据切成cell样本调用predict然后反归一化YPredNorm predict(netTrained, XTest); YTestNorm cell2mat(YTest); YPred YPredNorm * sigma mu; YTest YTestNorm * sigma mu;这里必须用前面保存的mu和sigma做反归一化不能用测试集的均值方差重新计算。5.2 RMSE、MAE、R2到底怎么算才合理评价指标我固定用三个RMSE、MAE、R2。e YTest - YPred; rmseVal sqrt(mean(e.^2)); maeVal mean(abs(e)); ssRes sum(e.^2); ssTot sum((YTest - mean(YTest)).^2); r2Val 1 - ssRes / ssTot;RMSE对大误差更敏感适合用来观察那些“偶尔突然偏离很大”的预测。MAE给每个误差同样权重更适合反映整体水平。R2则帮我看模型相对“用均值预测”到底提升了多少。但说实话这三个指标只靠单个数字并不可靠。时间序列预测有个特点如果模型只是把上一步的值平移一下作为预测RMSE可能不错但残差图会呈现明显的锯齿状。所以我会补画三张图真实值与预测值对比、残差随时间的分布、残差的直方图。如果残差还有明显规律说明模型没有抓全时序结构。5.3 预测曲线为什么总比真实曲线慢半拍我在验证阶段总会看到一种现象预测曲线落后真实曲线大约一个步长尤其数据在快速上升或下降时预测值有“拖拽”感。这个现象在单步预测里很常见原因也比较直接模型学到的信息中最近时刻的观测值在预测下一时刻时权重很大当趋势反向时模型惯性还没转过来。可这句话要区分两种情况如果滞后量大约等于预测步长这不是“模型失效”而是单步递归预测的正常表现。只要模型误差和残差分析合理这个滞后是可以接受的。如果滞后量明显大于预测步长或者残差自相关系数很高那就要怀疑窗口长度是不是太短模型没有学会趋势项。处理办法是在最终评价时我不仅看单步预测误差还会把多步递归预测做出来。用测试集最后一个真实窗口生成预测再把预测值作为输入逐点滚动往前推。这种做法会暴露模型在长期预测中的累积误差比单步预测更能说明泛化能力。6. 我自己反复踩过的坑和现在保留的调参习惯6.1 时序数据的样本顺序能不乱就别乱第一版代码里我用的是默认训练选项没注意每个epoch都会对样本做随机打乱。结果训练集误差很低但验证集误差忽高忽低同一个参数组合跑两遍得到的结果差异很大。后来我彻底把Shuffle设为never训练过程变得稳定得多。这也让我明白一个道理LSTM不像普通全连接网络它本身就是建模时间顺序的数据顺序就是信息的一部分。如果不是刻意做序列增强不要轻易打乱。6.2 贝叶斯优化不能只跑一次就拍板有一次我跑完30次试验得到的最优参数是隐藏单元数283、学习率0.008看起来不错。但换了一个随机种子重跑一遍最优组合变成隐藏单元数150、学习率0.004验证集误差只高了0.5%。这说明两件事第一贝叶斯优化找到的区域是可靠的但具体到某一个参数点存在一个“平坦区域”很多组合的误差接近第二最终选择参数时我不只看XAtMinObjective这个点还会把多次优化结果里表现稳定的区域作为候选。比如多次优化都指向隐藏单元数150附近我就优先选它而不是迷信单次最低误差点。6.3 2021版的兼容性用trainNetwork这条主线我这一版方案完全基于MATLAB 2021的经典深度学习工具箱接口也就是trainNetwork、predict、sequenceInputLayer、lstmLayer这一套。这套接口稳定资料多社区里大部分LSTM时序例子都是这个风格照着写不容易遇上大坑。后来有些资料提到的trainNetwork在新版本中被逐步扩展但不影响我们这一版的复现。如果你在R2021上照着跑建议确认三个函数存在optimizableVariable、bayesopt、trainNetwork。这几个在R2021 R2021a之后都是内置可用状态。如果你的版本是更早的R2019b或R2020abayesopt也有但部分参数属性名可能有差异。遇到报错时优先看帮助文档里的optimizableVariable页面确认Transform属性是否支持。6.4 一套参数只能证明一个数据集不是万能钥匙最后想多说一句贝叶斯优化LSTM不是“调一次参数所有时间序列都能用”。模型选出的最优参数和样本长度、采样间隔、数据平稳性、窗口设定都强相关。我从这个项目中养成的习惯是每次切换到新数据集先做归一化和窗口长度检查。保留数据预处理的统一函数便于复用。参数优化结果永远结合业务经验一起判断。曾经有同事想把我某个风速预测项目的最优参数直接搬到另一个站点我劝他重新跑一轮小预算优化只把变量范围缩小到已验证的区间。他跑完发现两个数据集的最优参数差异确实明显。这个例子我记了很久也一直提醒自己贝叶斯优化是帮你省试错成本不是帮你完全替代对数据的理解。回头再想这套流程最有价值的其实不是那组最优参数而是它给了我一个可复用的框架数据窗口化、网络骨架化、参数变量化、评估指标化。现在遇到新的单变量时序预测任务我基本就是把这套框架直接搬过去前十几个小时就能得到一版不靠运气的基线模型。如果你正好在做类似单输入单输出预测建议先按这个流程完整跑一遍再根据业务需求调整窗口和预测步长。