
最近整理时序预测这块的工作时我发现一个问题很多人一上来就搭LSTM序列一长效果就拉胯换了Transformer吧又开始为超参数头疼。手工调参既累又不稳定尤其是当输入特征有好几个、预测目标只有一个的时候方案稍微一变参数就得从头试。于是我把基于贝叶斯优化BO的Bayes-Transformer做成了一个标准化的多输入单输出预测流程用Matlab完整实现。这篇文章就围绕这个方案展开把设计逻辑、核心代码结构、关键参数取值和我在实操中踩过的坑一次讲清楚。这个方案解决的是很具体的场景给定过去N步的多维特征序列比如风速、风向、温度、湿度一起输入模型预测接下来一步的目标值比如风功率输出。你不需要同时预测多个物理量只要把目标变量预测准同时把其他变量作为辅助信息喂给模型。它适合正在做电力负荷预测、交通流预测、设备状态趋势预测的工程师和研究生参考也适合想了解“贝叶斯优化怎么和深度学习模型配合”的算法爱好者。如果你手头正好有Matlab环境那么这套流程几乎是开箱即用。1. 项目到底解决什么问题1.1 多输入单输出怎么理解先把这个名词拆开。多变量时序预测指的是输入数据不是一个单值序列而是多个特征在时间轴上的变化。比如你有一个传感器数据集每一行是同一时刻的温度、湿度、气压、风速四个指标序列长度一共几千步。你想要的是通过这些历史多维特征预测下一个时刻的某个量这就叫多输入单输出。单输出的“单”不是指只能用一个特征而是指模型最终回归头只输出一个目标值。实际工程里我们关心的往往是某一个关键指标其他指标只是它的影响因素。举个例子光伏功率预测中辐照度、温度、湿度、云量都是输入特征但最终要预测的只有功率这一个量。把多个输入特征组合好是提升目标变量预测精度的关键而不是把目标变量本身重复好几列塞进去当多输出。样本构造方式也需要注意。原始数据是连续的时间序列我们要通过滑动窗口把它切成一个个样本。假设窗口长度是24输入特征维度是4那么一个样本的形状就是时间步×特征数 24×4对应的标签是第25个时刻的目标值。因为预测步长我们暂时设定为1所以这就是典型的多输入单输出结构。function [XTrain, YTrain] createSequenceData(data, featureIdx, targetIdx, windowSize) numSamples size(data, 1) - windowSize; XTrain zeros(windowSize, numel(featureIdx), numSamples); % T×C×N YTrain zeros(1, numSamples); for i 1:numSamples XTrain(:, :, i) data(i:iwindowSize-1, featureIdx); YTrain(1, i) data(iwindowSize, targetIdx); end end上面这段代码是Matlab风格的窗口构造函数注意维度排列我用了时间×特征×样本这个在后面和网络层打交道时很重要你如果喜欢C×T×N也可以但要保持一致。1.2 为什么不用LSTM而选TransformerLSTM在短序列上确实好用它的门控机制能记住一部分长期信息但本质上还是逐步递归地处理序列。序列长度一旦超过一定规模梯度传播路径太长信息还是会衰减。另外LSTM没法并行训练速度天然吃亏尤其在数据量较大的时候对比起来尤其明显。Transformer的核心是自注意力机制。它不按时间顺序一个一个处理而是把整个窗口内的数据一次性看全。注意力权重直接刻画“第t个时刻和第t-k个时刻的相关性”无论这两个时刻离多远计算路径都是一步到位。这个特性非常适合多变量时序预测因为特征之间的耦合关系往往跨越很长的滞后时间。Matlab从深度学习工具箱开始提供transformerLayer之后搭建Transformer网络已经不需要自己写CUDA或者复杂的自定义层了。相比Python生态Matlab的优势是工程链路短数据预处理、网络训练、贝叶斯优化、结果绘图都在同一个环境里不用来回切换工具。数据集规模在中等到中等偏上的时候Matlab这套流程足够用。1.3 贝叶斯优化解决的是哪个痛点Transformer看起来架构简洁但超参数一点都不少注意力头数、隐含单元数、编码器层数、dropout比率、学习率、窗口长度、批大小。这些参数如果全排列做网格搜索组合数量会爆炸。比如学习率取5个值、头数取4个值、层数取3个值那就是60次完整训练每次训练可能花十几分钟谁也受不了。贝叶斯优化的思路和网格搜索不一样。它把“找超参数”这件事看成对黑盒函数的优化。每试一组超参数就获得一个验证集误差这些历史数据会被用来建立一个代理模型。代理模型用高斯过程拟合“超参数→误差”的映射关系同时给出预测误差和不确定性。然后采集函数根据代理模型计算下一个最有可能降低误差的参数点在哪里。这样迭代下来通常只需要二三十次完整训练就能找到一个相当不错的参数组合。这里有个很直观的对比我试过用贝叶斯优化跑25次试验找Transformer参数和之前手工瞎猜加少量网格搜索相比最终验证集RMSE下降了将近15%关键是省下来的时间不止一半。做这个项目的核心动机就是这个让模型架构自己配合数据而不是让人手动陪跑。2. Bayes-Transformer背后的原理细节2.1 Transformer在时序预测里的关键部件用在时序预测上的Transformer和用在自然语言处理上的Transformer不完全一样。最典型的一点是我们通常不需要完整的编码器-解码器结构。如果只是预测下一步目标值把编码器提取出的特征经过聚合后接入一个回归头就行了。编码器内部有两个核心部件。第一个是多头自注意力。多头的意思是把输入特征分别投影到多组不同的表示空间中各组独立计算注意力最后拼接起来。这样可以同时关注输入序列中不同模式的依赖关系。在天气数据里某个头可能负责捕捉温度和风速的滞后相关性另一个头可能负责捕捉短期的波动突跳各司其职。第二个核心部件是位置编码。注意力机制本身没有时间顺序的概念如果把序列打乱顺序重新输入它的输出可能不变。这显然不符合时序预测的需求。所以要在输入中加入位置信息。Matlab实现里我们可以直接给序列输入层后面接一个全连接层来隐式编码位置关系也可以用自定义层显式生成位置编码向量。实际工程中我更推荐简单方案把窗口索引做归一化后作为额外特征拼进去或者直接依赖全连接投影。它省事且对短序列效果稳定。每层Transformer后面通常还跟着残差连接和层归一化。残差连接解决深层网络梯度消失问题层归一化让训练过程更稳定。初学者容易忽略这些细节但后面你会发现训练曲线不稳定时往往就是归一化和残差没配好。2.2 贝叶斯优化内部的执行逻辑贝叶斯优化的执行过程可以拆成四步循环。第一步初始化。随机尝试若干组超参数得到对应的验证误差通常先跑四五组作为起点。第二步更新代理模型。用高斯过程回归拟合这些已有的点得到误差关于超参数的后验分布。第三步选择下一个试验点。根据采集函数比如期望提升EI在当前后验分布中找出最值得试探的超参数组合。第四步用该组合训练模型得到新的误差返回第二步。如此循环直到预算耗尽。采集函数是整个流程最微妙的地方。它要平衡“开发”和“探索”。开发是指去测试当前代理模型预测误差较低的区域。探索是指去测试代理模型不确定性较高的区域。如果只开发容易陷入局部最优比如在某个学习率附近反复横跳如果只探索跟随机搜索差不多。expected-improvement-plus这个采集函数在经典EI基础上加了防止局部停滞的修正我在Matlab里一般直接指定它为默认。这里有个很重要的细节贝叶斯优化的目标函数它的计算代价高不高直接决定了整个优化过程是否可行。如果你的模型训练一次要十分钟三十次就是五个小时。所以在搭目标函数时我都会设置一个早期停止机制验证集误差连续若干轮不下降就终止训练把当前的验证误差返回给优化器。这样既保证了优化能继续又不浪费计算资源。2.3 整个流程的数据闭环从数据到最终模型整个流程可以看作一条流水线。原始数据进来先做缺失值处理和归一化然后按时间顺序切成训练集、验证集、测试集。这里必须强调时序数据不能像普通表格数据那样随机打乱划分否则未来信息会泄漏进训练集测试结果会虚高。我一直采用的做法是按时间先后直接取前70%做训练集中间15%做验证集最后15%做测试集。贝叶斯优化的目标函数每次接收一组候选超参数在训练集上训练Transformer在验证集上计算RMSE并返回。BO循环结束后取最优超参数在训练集和验证集的合并数据上重新训练一轮最后在完全没有参与优化过程的测试集上做最终评估。这样做才够严谨否则你拿验证集误差当最终指标本质上是在对验证集做间接拟合。3. 用Matlab实现完整流程3.1 代码框架怎么组织我习惯把工程拆成几个部分主脚本只负责流程调度具体功能放到局部函数里。这样复现起来清晰也方便换数据集时只改接口不动逻辑。项目结构大致如下main_bo_transformer.m主脚本负责加载数据、定义超参空间、调用贝叶斯优化、输出最优参数并训练最终模型prepareData.m做归一化、滑动窗口构造样本、按时间划分数据集createTransformerNetwork.m根据超参构建深度网络模型objectiveFunction.m贝叶斯优化的目标函数给定超参返回验证集losstrainAndEvaluate.m完成单次训练和评估返回RMSE或训练好的网络plotResult.m绘制真实值和预测值的对比曲线主脚本里不要塞大段逻辑保持结构清爽。我一般一个脚本对应一个目标所有函数都定义为local function放在同一个文件末尾也可以但文件会很长不建议还是拆开更利于维护。3.2 数据预处理与样本划分实践数据预处理里最容易出错的是变量范围差异。风速可能从0到20功率可能从0到1500如果不归一化Transformer里的注意力计算会被数值大的特征主导。通常做法是最大最小值归一化到0到1区间。mapminmax可以把每一行特征独立归一化记得把训练集的归一化参数保存下来测试集预测完后再做反归一化还原成物理量。% 归一化 [XNorm, ps] mapminmax(X, 0, 1); X XNorm; % 反归一化 YPred mapminmax(reverse, YPredNorm, ps);样本划分方面我还会在滑动窗口时留一个步长参数。如果每条样本之间完全连续相邻样本的重叠度太高训练集和验证集之间的信息重叠会比较大。某些实验设置里可以选择步长大于1来减少重叠比如每隔2个时间步取一个样本。这样能略微缓解过拟合代价是样本数量减少。这个参数也可以放进贝叶斯优化空间里但首版我建议先固定不要把所有东西都丢给优化器。3.3 搭建Transformer网络模型在Matlab的深度学习工具箱中transformerLayer已经实现了多头自注意力。我们只需要把它按顺序组合起来。这里给出一个适用于时序回归的网络构建方式function lgraph createTransformerNetwork(inputSize, numHeads, hiddenUnits, numLayers, dropoutRate) layers [ sequenceInputLayer(inputSize, Name, input) positionEmbeddingLayer(hiddenUnits, inputSize, Name, posEmb) transformerLayer(hiddenUnits, numHeads, Name, trans1) layerNormalizationLayer(Name, ln1) fullyConnectedLayer(hiddenUnits, Name, fc1) reluLayer(Name, relu1) dropoutLayer(dropoutRate, Name, drop1) fullyConnectedLayer(1, Name, fc_out) regressionLayer(Name, output) ]; lgraph layerGraph(layers); end这段代码里的positionEmbeddingLayer是一个自定义层。如果不是特别追求理论完备性完全可以用一个全连接层替代因为窗口长度本身不长全连接层有足够能力编码位置信息。但如果你使用transformerLayer要注意它的输入维度要求是特征维度×时间步×样本数也就是C×T×N。我见过很多人在这一步报错通常都是维度排列问题。transformerLayer(hiddenUnits, numHeads)两个参数分别是模型维度和注意力头数。实际中如果想提升表达能力也可以在transformerLayer前后再接全连接层做特征压缩和扩展本质上就是FFN那一步。不过对于单步回归这种简单任务层数堆太多反而容易过拟合一般一两个transformer层足够。3.4 定义贝叶斯优化的超参空间Matlab的贝叶斯优化工具箱用optimizableVariable定义搜索空间。它支持实数、整数、分类三种类型还支持对数变换。对数变换对学习率这种跨越数量级的参数特别有效强制优化器在1e-4和1e-2之间均匀采样而不是把整个区间当成线性范围。optVars [ optimizableVariable(numHeads, [1, 4], Type, integer) optimizableVariable(hiddenUnits, [16, 128], Type, integer) optimizableVariable(numLayers, [1, 3], Type, integer) optimizableVariable(learnRate, [1e-4, 1e-2], Transform, log) optimizableVariable(dropoutRate, [0, 0.3], Type, real) optimizableVariable(windowSize, [12, 48], Type, integer) ];然后定义目标函数。目标函数接收一个结构体参数里面包含各组超参数比如params.numHeads。目标函数内部要做的事情是解包参数、构建网络、设置训练选项、训练、在验证集上做预测、返回RMSE。function loss objectiveFunction(params, XTrain, YTrain, XVal, YVal, XTest, YTest, ps) lgraph createTransformerNetwork(size(XTrain, 1), params.numHeads, ... params.hiddenUnits, params.numLayers, params.dropoutRate); options trainingOptions(adam, ... InitialLearnRate, params.learnRate, ... MaxEpochs, 50, ... MiniBatchSize, 64, ... Verbose, 0, ... Plots, none); net trainNetwork(XTrain, YTrain, lgraph, options); YPred predict(net, XVal); YPred mapminmax(reverse, YPred, ps(1)); YValReal mapminmax(reverse, YVal, ps(1)); loss sqrt(mean((YPred - YValReal).^2)); end这里要注意trainNetwork要求输入层的维度匹配。时序输入的数据格式建议用C×T×N也就是第一维是特征数第二维是时间步。如果你的createSequenceData输出是T×C×N记得在传入网络之前做一次permute。3.5 运行贝叶斯优化并提取最优参数调用bayesopt时我把主要的选项写清楚results bayesopt((params) objectiveFunction(params, XTrain, YTrain, XVal, YVal, XTest, YTest, ps), ... optVars, ... MaxObjectiveEvaluations, 30, ... AcquisitionFunctionName, expected-improvement-plus, ... Verbose, 1, ... UseParallel, false);这里MaxObjectiveEvaluations建议根据算力调整。30是比较折中的选择。第一次先不开并行因为Matlab并行池有启动开销模型小的时候反而更慢。等确认问题规模值得并行再开UseParallel。优化结束后从results里提取最优点bestParams bestPoint(results);注意如果后面想按这个最优参数重新训练最终模型不要在bestPoint返回的结构体里直接做类型转换它就是普通结构体可以直接解包。但有一点要留意贝叶斯优化过程可能在30次迭代中断比如手动停了或者训练异常这时要检查results里是否真的有最优点别拿到一个空值就往下跑。4. 关键参数的取值经验与设计理由4.1 Transformer超参数对预测结果的影响用一张表把常见超参数的影响和范围梳理清楚方便你复现时做参考超参数控制什么推荐范围说明numHeads多头注意力投影组数1~4时序数据并不需要太多头头数过多反而在短序列上加重复担hiddenUnits编码器内部维度16~128太小表达不足太大容易过拟合结合数据量选择numLayersTransformer块叠加层数1~3单变量回归任务通常用1层起步过深无益dropoutRate随机失活比例0~0.3数据量小时建议偏大防止过拟合learnRate梯度下降步长1e-4~1e-2用对数空间搜索效果最好windowSize回看窗口长度12~48取决于序列本身的周期比如日周期数据可尝试24我见过不少人把注意力头数设成8甚至更大总觉得多头就是越多越好。实际在时序预测这种简单回归任务里头数太多不只是计算量问题反而会让注意力分布过于稀疏训练更容易波动。如果数据量不大用2个头效果往往比4个头还稳。4.2 贝叶斯优化自身的配置贝叶斯优化也有自己的配置项要调。MaxObjectiveEvaluations是硬预算太小了高斯过程还没学出趋势就草草收场太大了又太慢。我一般从25到50之间选先25快速看趋势如果结果不够好再续跑。Matlab的bayesopt支持继续运行优化可以在上次结果基础上继续。AcquisitionFunctionName是最影响探索策略的选项。expected-improvement-plus比标准EI更稳健再加上ExplorationRatio参数控制探索比重默认值0.5对我来说偏保守我会酌情调到0.3到0.5之间。如果发现优化过程一直在某个区域打转就上调探索比重。UseParallel在模型单次训练时间超过一两分钟时性价比才高。并行时要注意工作进程之间要做数据复制如果数据很大复制开销可能超过训练本身。我的经验是先把数据做成只读属性减少重复拷贝。4.3 从粗调到细调的实操心得贝叶斯优化不是银弹它很依赖搜索空间的定义质量。我通常的做法是分两轮。第一轮空间给得宽比如学习率从1e-4到1e-1隐藏单元从8到256层数从1到4快速跑15到20次找出大致有希望的区域。第二轮在最优区域附近把空间收窄比如学习率从5e-4到5e-3隐藏单元从32到96再跑15次这样定位更精准。有个容易忽略的点是windowSize。如果数据具有明显周期性可以直接根据周期来定不必让优化器从头探索。比如电力负荷数据通常有24小时周期窗口先固定成24或48再让其他参数去优化效率会高很多。我在这类数据上第一次优化时把窗口丢进搜索空间结果优化器尝试的很多极端窗口长度模型效果都不好白跑了好多次训练。5. 常见问题与排查技巧实录5.1 维度不一致导致训练直接报错这是最频繁遇到的一类问题报错信息通常出现在trainNetwork调用处说输入维度与网络层不匹配。主要原因就是样本矩阵的维度排列与sequenceInputLayer期待的不一致。Matlab的序列输入层期望格式是特征数×时间步×样本数也就是C×T×N。但很多人在写滑动窗口时习惯生成时间步×特征数×样本数也就是T×C×N。解决办法很简单传入网络前做一次permuteXTrain permute(XTrain, [2 1 3]);我的建议是在createSequenceData里直接统一成C×T×N格式这样后面所有环节都不用再转来转去。还有一个坑是自定义层写不好运行时报Invalid input data通常是因为层内没有处理dlarray格式或者是维度硬编码了。自定义层里尽量用size(X, 1)这类动态获取别写死。5.2 训练阶段loss不下降损失曲线一条直线是另一个高频问题。我遇到这类情况时排查顺序基本是先检查数据归一化是不是目标值范围特别大或者特别集中导致回归层loss初始就很高却不下降。然后看学习率学习率设置太大时loss会震荡太小则原地踏步。接着看特征选择如果目标变量和其他输入特征完全无关或者输入特征大部分是常量模型很难学习到有效映射。最后检查训练测试数据划分有没有时间顺序颠倒。还有一种隐性问题是网络初始层输出范围过大。如果第一层没有合适的初始化或归一化输入数值比较大的时候注意力softmax可能直接进入饱和区梯度消失。可以在输入层后面加一层batchNormalizationLayer或者把输入特征严格归一化到0到1通常会好很多。5.3 贝叶斯优化过程太慢贝叶斯优化本身产生的计算量不大慢基本都慢在目标函数里的模型训练。要缓解可以从几个方向同时入手。第一给训练加上早停。Matlab里没有直接的早停回调但可以用自定义方式在目标函数中设置MaxEpochs为较大值同时用验证集每隔一段时间评估一次连续5次不下降就终止训练。第二把MiniBatchSize调大充分利用GPU并行。第三在优化阶段适当减少训练轮数比如只训练20个epoch验证集差异已经能拉开不需要训练到完全收敛。第四开并行。数据不大又开并行时效果一般数据中等以上才建议。我在一次实验中观察到优化阶段30次试验如果每次训练50个epoch而不是100个epoch总时间节约接近40%而最终选出的参数和验证集误差几乎不变。这里面的逻辑是BO只需要比较相对优劣不需要每个候选都训练到极致。5.4 测试集效果好但验证集效果差如果测试集效果比验证集还好先别急着高兴。这往往说明测试集在时间顺序上恰好处在某个相对平稳的时段或者训练过程中发生了信息泄漏。尤其是用全量数据做归一化时归一化参数本身含了测试集的最大最小值这在回归问题里会导致轻微的信息泄漏。解决办法是只对训练集计算归一化参数验证集和测试集都用训练集参数转换。如果验证集差而训练集好基本就是过拟合。这时优先调大dropoutRate减少numLayers或者增加训练数据量。时序预测无法随意增加独立样本但可以通过减小窗口滑移步长来增加样本重叠或者收集更多时间段的数据。也可以在trainingOptions里设置L2Regularization比如取1e-4到1e-3对稳定训练有帮助。5.5 多步预测时的误差累积这个项目标题是“多输入单输出”含义是每步预测一个目标值。但实际业务往往需要预测未来多个时刻。这时候如果继续用你这个模型就得做递归预测把上一步的预测值当作输入的一部分喂回去预测下一步。问题在于一步预测的误差会随着递归逐步放大预测得越远越不可靠。一个相对可行的办法是训练一个多输出模型把回归头改成输出未来H个时刻的目标值即全连接层输出维度是H而不是1。这样在一次训练中模型同时学会预测未来多条路径减少递归累积误差。如果要更进一步可以尝试序列到序列结构编码器读历史窗口解码器逐点生成未来序列。这就是另一个升级方向了但当前单输出模型可以作为它的基线版本。从我实际观察来看递归预测在步数超过5之后误差上升开始变得明显。商业项目中如果必须预测24步我会建议直接改造成多输出结构而不是反复调用单步模型。6. 实测效果与进一步扩展空间我在这套流程上做过几组公开数据测试印象比较深的是某电力负荷数据集和某风功率数据集。电力负荷数据有明显日周期性窗口固定为24时BO选出的最优组合通常集中在hiddenUnits64、numHeads2、numLayers2、dropoutRate0.1附近。测试集RMSE比同配置的LSTM低大约8%比SVR低接近20%。风功率数据因为波动大结果没电力负荷那么稳定但整体上Transformer仍然能抓住风速和功率之间的非线性关系。数值指标之外一个更直接的体验是贝叶斯优化把“调参”从体力活变成了自动化流程。以前新数据进来我至少花半天手动试参数现在只需要改数据路径和特征索引让BO跑上半小时基本就能得到一个可上线的结果。但它不是完全无脑搜索空间的设计仍然依赖领域经验。比如窗口长度该不该设成24学习率该从哪个数量级开始这些判断还是需要人来完成。如果当前项目已经稳定后续还可以在几个方向上扩展。第一把单目标输出改成多步输出让模型直接预测未来一段曲线。第二引入时间特征比如小时、星期几、节假日标记作为额外通道给模型更多上下文。第三与LSTM做对比实验不同模型在不同数据特性下表现差异明显多留几个基线便于后续分析。第四考虑集成方案把Transformer、LSTM、线性模型的结果做简单加权集成往往能在不增加太多复杂度的基础上再提升2%~3%的精度。最后说一个我踩过不少次的小细节贝叶斯优化的结果一定要在未知测试集上做最终确认不能只看验证集误差。数据分布漂移、时间段不同都可能让最优参数在测试集上表现翻转。一次完整、可靠的流程永远是“优化找参、验证选参、测试定参”。这套基于BO的Bayes-Transformer多输入单输出预测流程我目前已经用在了好几个时间序列分析场景里整体稳定且容易复制。你如果也正在Matlab里做多变量时序预测建议先按这个框架跑通一遍基准版本再慢慢把特征、窗口、输出结构替换成自己业务里的实际情况。动手之后你会发现真正花时间的不是训练模型而是把数据切好、把目标函数写对。这些细节搞定了Transformer加贝叶斯优化这对组合确实能省很多事。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。