最近几年做时序预测,我明显感觉传统那套LSTM、GRU的路子越来越不够用了。尤其是碰到多变量输入、预测单变量的场景,数据维度一多,长程依赖一长,LSTM的梯度问题就被无限放大,调参调得人想摔键盘。后来我把自己的一套流程换成了Transformer结构,再配合贝叶斯优化(BO)自动搜索超参数,效果提升非常明显。这篇文章就把我这套“BO + Bayes-Transformer多变量时序预测(多输入单输出)”的完整方案记录下来,包括建模思路、核心原理、Matlab代码结构和实测中的各种坑,给同样被这个问题折磨的人一个能直接参考的路线。
适用对象很明确:有Python基础但想转Matlab做算法验证的、正在做负荷预测/交通流量预测/金融时序这类多输入单输出项目的同学,还有那些对Transformer停留在“听说过、没上手”状态的研究者。整个方案不依赖任何额外工具箱,纯手写核心代码,跑通一个公开数据集大约需要十几分钟到半小时,很适合作为基线实验或者论文的对比方法。
1. 为什么是Transformer:时序预测任务的核心痛点与建模选择
1.1 从LSTM到Transformer:不只是“换了个结构”
先说一个很多初学者没想透的问题:LSTM在处理多变量时序时到底哪里不行?LSTM本质是逐步递归的,当前时刻的隐状态依赖于上一时刻的输出,这种串行结构带来两个麻烦。第一,长序列下的梯度传播会衰减,虽然加了门控机制,但序列超过100步之后,早期信息能保留多少很难说,这会直接导致预测长周期趋势时后劲不足。第二,它天然难以并行训练,数据量一大,训练时间成倍上涨。
Transformer把“按时间步展开”这个逻辑彻底换掉了,用的是自注意力机制(Self-Attention),任意两个时刻的依赖关系可以被直接建模,距离不再是瓶颈。举个例子:输入是前一周的电力负荷、气温、湿度、风速多个变量,要预测明天的负荷。LSTM需要“一步步读过去”,Transformer则直接把整段历史扔进注意力矩阵里,关键关联在一步之内计算出来,这就是它处理多变量时序的底层优势。
还要注意一个词:Bayes-Transformer。这里所谓的Bayes体现在两个层面,第一个层面是训练时可以用贝叶斯推断的思想对参数做正则化,限制模型复杂度;第二个层面则是在超参数选择上引入贝叶斯优化。很多论文里说“基于BO的Transformer”,实际操作上主要指的是后者——把网络结构参数和训练参数当成一个黑箱函数,用贝叶斯优化去最小化验证集误差。
1.2 多输入单输出建模:本质上是条件映射学习
“多输入单输出”是这类任务的统称,但具体到数据组织方式上,需要做一个滑动窗口切分。假设原始数据有N个采样时刻,每个时刻有D个特征(比如负荷、温度、湿度),记作矩阵X ∈ R^(N×D)。我们定义一个回看窗口长度lookback(比如24个时刻)和预测步长pred_steps(比如未来1个时刻),那么一个训练样本就是前24行数据作为输入,第25行的目标特征作为标签。
输入样本形状:(lookback, D) = (24, 3) 标签样本形状:(1,) 或 (1, 1),取决于输出的维度从矩阵角度看,输入是三维张量(batch_size, lookback, D),标签是二维张量(batch_size, 1)。Transformer内部的注意力计算需要一个维度转换,把D当作Embedding维度,把lookback当作序列长度。这就是深度学习框架里常说的“特征维度与时间维度分离原则”,代码写错的人十有八九是这里搞混了。
1.3 超参数为什么非得用贝叶斯优化
Transformer家族的超参数太多了:d_model(嵌入维度)、n_heads(注意力头数)、num_layers(编码器层数)、dropout、学习率、batch_size、优化器动量参数……一个等分网格搜索假设d_model有4个候选值、n_heads有5个候选值、num_layers有4个候选值,这就80种组合了,每种组合跑一次完整训练,加起来非常不现实。随机搜索倒是快一些,但它不利用已经跑过的结果来指导下一步方向,纯粹靠运气。
贝叶斯优化的核心思路是:把单次训练跑出来的验证损失看作一个未知函数的采样值,先用高斯过程(Gaussian Process)拟合出这个函数的分布,然后利用采集函数决定下一组最值得尝试的超参数。换句话说,它会把“试错过程”本身变成一种带记忆的搜索,越试越准。直观类比:你在一家陌生城市找好吃的火锅店,随机是每家都进去吃一口;网格是挨条街扫一遍;贝叶斯优化则是边吃边记,根据前面的味道推测哪一片街区大概率更好吃,直接走过去。
2. 多输入单输出时序预测的整体流程:从数据到评价指标
2.1 数据预处理:时序问题里一半的坑都在这里
我建模型的第一步永远是做数据清洗和归一化,顺序不能颠倒。缺失值处理我推荐用线性插值或者前向填充,但不要在归一化之后做插值,否则插值的结果会被归一化参数扭曲。归一化这里,我踩过一个很经典的坑:直接在整个数据集上做MinMaxScaler,然后再划分训练集和测试集,这是典型的信息泄露。因为测试集的最大最小值渗透进了训练过程,测试集的误差会被低估,论文里的对比数字看起来很漂亮,但部署之后立刻打回原形。
正确的顺序:切分训练集和测试集,只在训练集上计算归一化参数(min和max),把参数存储下来,再分别应用到训练集和测试集。如果需要做验证集,也一样,验证集和测试集都必须使用训练集统计出来的参数。
% 切分原始数据 trainLen = round(0.7 * length(rawData)); trainData = rawData(1:trainLen, :); testData = rawData(trainLen+1:end, :); % 只在训练集上统计归一化参数 [xtrain, ps] = mapminmax(trainData', 0, 1); xtest = mapminmax('apply', testData', ps);这里mapminmax是Matlab自带函数,'apply'参数会把训练集存下来的min/max应用在新数据上。我特意用它来避免手写归一化公式时不小心写错边界。
2.2 滑动窗口生成与批次组织
预处理之后就是要生成监督学习样本。对Transformer来说,输入维度要整理成(batch, lookback, features)。在Matlab里,深度学习模型默认的首维是特征维,也就是说矩阵通常是(features, lookback),所以滑动窗口生成时要注意转置,不然喂进模型就会报维度不匹配的错误。
每一行代表一个样本: Input[batch, lookback, features] Target[batch, 1]这里有个细节:Matlab神经网络工具箱接受的数据格式和Python的TensorFlow完全不同,输入格式默认是SNC(按列排时序),也就是第一维是特征维,第二维是时间长度,第三维是样本数。很多从Python转过来的用户会在这个细节上栽跟头,把数据维度排成(样本数, 序列长度, 特征数),然后模型就直接报错了。
滑动窗口的生成逻辑其实很简单,用两个循环就能搞定。数据量大的时候我会预分配内存,用zeros先占位,避免运行时动态扩容导致内存碎片和龟速。
2.3 Transformer编码器自建模块:不用别人工具箱的办法
Matlab从R2021a开始内置了transformerLayer,但说实话,用内置层做定制化实验时不够灵活——注意力头数、dropout等预设置不够细,而且你不太容易看清楚每一层的中间状态。我更推荐的做法是:利用Matlab的深度学习自定义层机制,自己写一个单头/多头注意力层,整个结构控制在几十行代码以内,自由度直接拉满。
自定义层的核心接口是predict函数和computePredictions。Multi-Head Self-Attention的数学实现可以拆成三步:把输入X与权重矩阵Wq、Wk、Wv相乘得到Q、K、V矩阵;对Q和K做点积并除以sqrt(d_k)得到注意力分数;用softmax归一化后与V加权求和。多头就是把隐藏维度切分成n_heads份,各自计算注意力后再合并。这个算法在Matlab里用矩阵乘法实现非常快,矢量化的写法在一百行以内能搞定。综合来说,不引工具箱比引工具箱更稳,因为你可以完全控制每个算子,排除黑盒出错的变量。
2.4 评价指标的选择:MAE还是MAPE,要学会看场景
模型跑完总得有个数字来说话,多输入单输出时序预测里最常用的评价指标有四个:MAE(平均绝对误差)、RMSE(均方根误差)、MAPE(平均绝对百分比误差)、R2(决定系数)。我在实际项目中偏好同时看MAE和MAPE,因为MAE能反映绝对偏差,MAPE能反映相对偏差,两者分开来看不容易被单一指标欺骗。
如果你预测的对象数值跨度很大(电力负荷白天和深夜差好几倍),MAPE在小数值段容易爆炸,此时更推荐优先看MAE和RMSE。如果你的业务关心“相对误差到底多大”(比如库存预测,误差百分比直接决定备货量),那就用MAPE考核模型。R2则是用来评估模型解释力,适合学术场景。
MAE = mean(abs(y_true - y_pred)); RMSE = sqrt(mean((y_true - y_pred).^2)); MAPE = mean(abs((y_true - y_pred) ./ y_true)) * 100; R2 = 1 - sum((y_true - y_pred).^2) / sum((y_true - mean(y_true)).^2);跑实验的时候,除了这些整体指标,我还会把每个预测时间点的误差单独拿出来画折线图,能直接看到哪些时段误差特别大,这种信息对排查数据问题是很有用的。
3. 贝叶斯优化调参:搜索空间的确定与采集函数逻辑
3.1 搜索空间设计:别把网格搜索思路搬过来
贝叶斯优化第一步是定义搜索空间。很多人在这一步会犯一个毛病:把每个超参数的候选值都列成一串离散值,比如学习率只能取[0.001, 0.01, 0.1],这本质是网格搜索的变体,完全浪费了贝叶斯优化的连续建模能力。正确做法是给学习率定义一个连续范围[1e-4, 1e-1],并在对数空间里面采样,贝叶斯优化才有空间去“推测中间值”。
以我的项目为例,搜索空间定义大概是这样的:
| 超参数 | 范围 | 采样方式 | 说明 |
|---|---|---|---|
| d_model(嵌入维度) | 32~128 | 整数均匀 | 太小则表达力不足,太大则训练慢 |
| n_heads | 2~8 | 整数均匀 | 需能被d_model整除 |
| num_layers | 1~6 | 整数均匀 | 层数太多容易过拟合 |
| dropout | 0.05~0.5 | 连续均匀 | 防止过拟合的关键 |
| 学习率 | 1e-4~1e-2 | 对数均匀 | Transformer对学习率异常敏感 |
| batch_size | 16~128 | 整数均匀 | 与数据集大小相关 |
关于n_heads有一点要注意,d_model必须能整除n_heads,这是多头注意力机制的结构性约束。贝叶斯优化采样过程中,这两个参数是分别独立采样的,很可能采样出不整除的组合。处理方式有两个:在代码里检查,如果d_model mod n_heads不等于0,就自动调整d_model为一个可整除的近似值;或者把d_model定义成候选倍数的列表,比如[32, 64, 128],n_heads从对应约束里采样,两者联动。第一种更省事,写代码时候一个if判断就解决。
3.2 高斯过程代理与采集函数:贝叶斯优化的发动机
贝叶斯优化迭代里两个关键对象:一个是代理模型(Surrogate Model),一个采集函数(Acquisition Function)。代理模型用高斯过程拟合已有的超参数-验证损失样本点,给出每个候选点的损失均值估计和不确定性(方差)。采集函数则基于这个均值和方差来打分,决定下一组试什么。
最常用的采集函数是EI(Expected Improvement),它的含义是“这个新点相对于当前最优值的期望提升”。数学上说:
EI(x) = E[max(0, f_min - f(x))]
这个公式看起来简单,但隐含了自动平衡探索和利用两种策略——在不确定区域冒险尝试,同时不放过目前表现好的区域再深挖。一种生活化类比:你在找餐厅,EI会综合“这家店评分高不高”和“评价数量多不多”来判断,评分高但还没什么人评价的店,会被认为潜力最大。
在Matlab的贝叶斯优化工具箱(bayesopt)里,其实不需要手写EI,因为已经封装好了。核心调用格式是:
results = bayesopt(@(x) objectiveFunction(x, Xtrain, Ytrain, Xval, Yval), ... vars, 'AcquisitionFunctionName', 'expected-improvement-plus', ... 'MaxObjectiveEvaluations', 15, 'Verbose', 2);注意'expected-improvement-plus'是EI的改进版,引入了噪声项控制,防止重复采样同一个点,实操上我非常推荐加这个选项。MaxObjectiveEvaluations控制总的训练次数,个人经验是15次到30次比较合适,少于15次高斯过程还没拟合到位,多于30次边际收益递减。
3.3 目标函数的写作艺术:最大化信息量,最小化GPU开销
目标函数是BO和实际训练之间的桥梁,它的输入是一组超参数,输出是一个标量损失值。写得不好的目标函数会有一堆隐藏bug,比如每次都从随机初始状态训练、导致同等配置下损失波动极大,贝叶斯优化就会完全迷惑。
我习惯在每个目标函数评估里做三件事:固定随机种子、记录当前超参数、返回验证损失。固定随机种子尤其重要,因为同样的超参数下,网络权重初始化不同,验证损失可能差好几个百分点,这种噪声会让高斯过程拟合出一个“抖动”的曲面,搜索效率直接减半。
种子固定: rng(42); % 每次评估训练前都执行 然后训练模型,但注意这里还是会有数据加载顺序的随机性, 如果用了shuffle,建议固定shuffle的随机种子,或者干脆不做shuffle而用手动打乱。训练轮数(epochs)也是目标函数设计里的关键。我建议在BO阶段用较少的epoch数(比如30~50),快速筛选出有潜力的超参数组合,确定后再拉长epoch跑最终版本。这样做可以大幅缩短BO阶段的耗时。原因在于BO关心的是“相对好坏”而不是“绝对最优”,只要同一个评估协议下能区分参数优劣,信息就足够了。
4. Matlab代码架构解析:我手写的核心模块细节
4.1 主流程设计:让代码可复现、易修改
整体代码分四层组织:数据层、模型层、优化层、评估层。数据层负责读数据、归一化、生成滑动窗口;模型层负责Transformer的前向计算;优化层跑贝叶斯优化,调用目标函数;评估层给出最终指标和画图。每层用独立function文件或者代码分节注释隔开。
%% ========== 1. 数据准备 ========== raw = loadData('your_data.csv'); [Xtr, Ytr, Xval, Yval, Xte, Yte, ps] = ... prepareData(raw, lookback=24, predictStep=1, trainRatio=0.7); %% ========== 2. 定义贝叶斯优化变量 ========== vars = [ optimizableVariable('d_model', [32, 128], 'Type', 'integer') optimizableVariable('n_heads', [2, 8], 'Type', 'integer') optimizableVariable('num_layers', [1, 6], 'Type', 'integer') optimizableVariable('dropout', [0.05, 0.5], 'Type', 'real') optimizableVariable('lr', [1e-4, 1e-2], 'Transform', 'log') optimizableVariable('batchSize', [16, 128], 'Type', 'integer') ]; %% ========== 3. 贝叶斯优化 ========== boResults = bayesopt(@(params) transformerObjective(params, ... Xtr, Ytr, Xval, Yval), vars, ... 'MaxObjectiveEvaluations', 20, ... 'AcquisitionFunctionName', 'expected-improvement-plus', ... 'Verbose', 2); %% ========== 4. 用最优参数训练最终模型 ========== bestParams = bestPoint(boResults); [finalModel, metrics] = trainTransformer(bestParams, Xtr, Ytr, Xte, Yte); plotPrediction(Yte, pred, metrics);这段代码的命名和分节方式是我的个人习惯,好处是别人拿到代码能在一分钟内看懂整个项目骨架,不需要读注释就能判断哪一段负责什么任务。
4.2 自注意力层的Matlab实现要点
如果你不想用Matlab自定义层API,另一个可行路径是直接用脚本实现Transformer的前向计算,结合手写梯度或自动微分。但说实话,手写梯度在多头注意力这种结构里非常容易出错,一步算错整个误差就传导乱了。我更推荐用Matlab自定义层的方式,把前向计算封装在predict里,让Matlab自动处理反向传播。
这里给大家看一下Multi-Head Attention前向计算的伪代码逻辑:
function [y, attnWeights] = multiHeadAttention(X, Wq, Wk, Wv, Wo, nHeads) % X: [dModel, seqLen] % Wq, Wk, Wv: [dModel, dModel] % nHeads: number of attention heads [dModel, seqLen] = size(X); dk = dModel / nHeads; Q = Wq' * X; % [dModel, seqLen] K = Wk' * X; % [dModel, seqLen] V = Wv' * X; % [dModel, seqLen] % 分割成多头 Q = reshape(Q, dk, nHeads, seqLen); K = reshape(K, dk, nHeads, seqLen); V = reshape(V, dk, nHeads, seqLen); % 调整维度让注意力在不同头之间并行计算 scores = pagemtimes(permute(Q, [1 3 2]), permute(K, [3 1 2])) / sqrt(dk); attn = softmax(scores, 1); context = pagemtimes(permute(V, [3 1 2]), attn); % [seqLen, dk, nHeads] ... end这个实现里用到了pagemtimes,这是Matlab R2021b新增的分页矩阵乘法函数,用起来极为顺畅。如果用的是老版本Matlab,可以退化为两层for循环遍历batch和head,速度会慢一些但逻辑相同。注意力权重矩阵attnWeights建议单独存下来,不仅能做可视化,还能检查模型是否学到合理的时间依赖,比如预测明天的负荷时,模型是否会重点关注昨晚同时间段的负荷。
4.3 位置编码:很多人忽略但对Transformer时序预测至关重要
Transformer的注意力计算本身不包含任何顺序信息,这跟LSTM天然按时间展开完全不同。所以必须给输入加上位置编码,让模型能感知“第几个时间步”。常见的方案有两种——正弦余弦固定位置编码和可学习位置编码。时序预测领域,我强烈建议用正弦余弦位置编码,因为时间序列的长度可能会变,固定编码的表示能力不如能泛化到未见过长度的正弦余弦形式。
Matlab实现里我会在数据进入Encoder之前做一次加法广播:
% positionEncoding: [dModel, seqLen],计算一次后缓存 % X_embed: [dModel, seqLen] X_embedded = X_embed + positionEncoding(:, 1:seqLen);这个细节看起来只有一行代码,但取决于位置编码结果的正确性。网上有人直接把初始位置编码全部初始化为0,那等于把顺序信息完全丢掉了,模型性能会下降得非常明显,尤其是lookback比较长的时候。
4.4 目标函数内部的关键保护机制
写BayesOpt的目标函数时,我还会加一个try-catch套件。因为超参数极端情况下会导致数值上溢或者维度错误,尤其是学习率设成1e-2再加上Transformer的LayerNorm,可能会在少量epoch之后出现NaN。如果不对这个错误进行处理,整个BO进程直接崩溃,前面的辛苦白费。
处理方式是把无效的结果返回一个巨大的损失值(比如1000),同时确保代码不会中断。这样高斯过程会把这些点视作不可用的高损失区域,后续自动避开,整个优化流程仍然正常走完。
function loss = transformerObjective(params, Xtr, Ytr, Xval, Yval) try [loss, ~] = trainAndEvaluate(params, Xtr, Ytr, Xval, Yval); if isnan(loss) || isinf(loss) loss = 1000; end catch loss = 1000; end end5. 实测效果:数据、对比与在训练中真正踩过的坑
5.1 数据集和基线设置
我用某公开的建筑能源数据集做了验证,采集频率为15分钟一个点,共多变量特征:室外温度、相对湿度、太阳辐射强度、历史负荷。目标是预测未来第1个时刻的建筑能耗。训练集、验证集、测试集比例是7:1.5:1.5,lookback选48个时间点(相当于12小时历史),预测步长设为1。Baseline选了两个:LSTM和没有BO调参的固定参数Transformer。因为数据集本身不涉及任何真实机构,实验结论仅供方法比较参考。
初始化设置上,LSTM使用了两层128隐藏单元,dropout=0.2,学习率0.001;固定参数Transformer使用d_model=64,n_heads=4,num_layers=2,学习率0.001。BO阶段搜索范围如前述,评估次数25次。每轮训练固定40个epoch,batch_size在BO内部由参数决定。
5.2 数值结果和关键发现
跑完之后的结果非常有意思,我把三个模型在测试集上的表现整理成表:
| 模型 | MAE | RMSE | MAPE(%) | R2 |
|---|---|---|---|---|
| LSTM(手动调参) | 0.0842 | 0.1123 | 7.84 | 0.9134 |
| Transformer(固定默认参数) | 0.0789 | 0.1056 | 7.12 | 0.9251 |
| BO-Bayes-Transformer(本文方案) | 0.0714 | 0.0938 | 6.27 | 0.9387 |
从结果看,BO调参后的Transformer比手动调参的LSTM整体MAE降低了15%左右,比固定参数的Transformer也稳定提升了6%~8%。其中有一个结果让我印象很深刻:BO选出来的学习率是0.0027,dropout是0.31,num_layers是2层,d_model=96。这几个参数组合如果让我手工试,大概率不是第一选择——我通常会选更大的hidden size和更低的dropout,但实测下来过拟合比预想严重,dropout反而是影响泛化最关键的变量。
这里也顺便说一下可复现性设置:所有最终实验固定随机种子为2024,Transformer初始化通过调用Matlab的initializeGlorot自定义函数实现,确保每次跑出来的结果在三位小数级别稳定。如果你的实验每次结果波动超过5%甚至10%,先检查随机种子和shuffle逻辑,不要急着怀疑模型。
5.3 踩坑记录一:维度顺序错乱导致注意力矩阵直接报错
我最早把输入做成(batch, seq_len, features),直接喂给自定义层。Matlab自定义层的前向计算是按照(features, seq_len, batch)来组织的,这两个顺序相差很大。第一次跑就报了一个维度不匹配的红色大错。排查了半天,最后发现是输入数据格式和深度学习约定不一致。从那之后我养成一个习惯:每个数据喂进模型之前,打印一下size,看一眼到底是[特征×时间×样本]还是[样本×时间×特征],这个习惯替我省下了无数debug时间。
5.4 踩坑记录二:归一化信息泄漏让测试集结果虚高
第一次做完整实验时,我图省事,直接对整个数据集一次性归一化,然后切分。跑出的测试集R2高达0.97,自己当时还挺开心的,觉得方法特别强。后来一个做项目的同学提醒了我,说测试集的信息已经通过归一化泄漏到训练阶段了。我把归一化改成只在训练集上拟合参数后,同样的设置R2回落到0.93附近。对比悬殊,令人印象深刻。所以这里再强调一次:任何涉及统计量的预处理,必须在切分之后、并且只在训练集上计算。
5.5 踩坑记录三:BO阶段训练轮数过少导致搜索方向失真
刚开始做BO时,我在目标函数里只跑了10个epoch,想着快点出结果。结果发现BO老是往大的d_model方向走,验证损失却降不下去。后来排查了一下,原因是10个epoch根本不足以让Transformer收敛,损失主要反映的是训练不充分的程度而非模型结构的好坏,导致高斯过程学到一个错误的映射关系。把epoch提到40之后,BO的搜索方向就变得合理多了,最终选出的参数也确实稳定更好。
5.6 常见问题处理建议速查
- 训练出现NaN:检查学习率是否过大或者数据归一化是否有零值或除零;将学习率下限调低到1e-4,并在目标函数里捕获异常。
- 多头注意力维度错误:确认d_model可以整除n_heads,并在代码里增加断言(assert)检查。
- 预测曲线滞后:常见原因是lookback偏短或者位置编码缺失,试试将lookback拉长到48/96,并确认位置编码不是零向量。
- 测试集MAPE异常高:看看目标特征是否有一些接近零的值,MAPE对这种情况非常敏感,改用MAE或sMAPE。
6. 一些个人使用建议:这套方案的边界与扩展方向
整套BO-Bayes-Transformer方案,不只是能用于能源负荷预测。只要是需要把多个历史变量映射到未来一个变量的场景,都可以套用这套框架。交通流预测可以输入历史车流量、天气、节假日特征预测下一时刻流量;金融场景可以输入量价指标预测波动率方向;工业场景可以输入温度、压力、振动信号预测设备剩余寿命的退化趋势。核心改动只在数据预处理和特征构建上,模型骨架不需要动。
扩展方向上面,我个人觉得有几个很值得尝试的:一是把代码从单变量输出改成序列输出(多步预测),这需要在输出层做相应的结构调整,把最后的全连接层输出维度改成pred_steps,损失函数改成预测区间内的累积误差;二是引入PatchTST那种分段嵌入思路,把时间维度的局部特征先做卷积聚合,再进注意力层,很多公开数据集上的效果会再上一个台阶;三是在BO的搜索空间里加入是否使用LayerNorm、是否使用残差连接的开关变量,把架构级选择也纳入自动调参范围。
另外有一个实操层面的经验:如果是做学术对比实验,记得在论文的实验部分写清楚BO评估次数、每个子模型的训练轮数、随机种子等细节,很多论文这些信息缺失,导致别人复现时怎么都对不上数字。我之前同一个实验,随机种子不同跑出来MAE抖动0.003左右,报告值时统一以三组实验的平均值和标准差为标准,这样可以提升结果的可信度。
这套代码跑通周期不长,但里面每一处细节都是靠踩坑换来的。如果是从零开始复现的话,建议先跑通固定参数版本,再接入贝叶斯优化,不要一步到位,否则出了bug你很难定位问题是出在Transformer实现还是BO控制流程。先确保模型可训练、loss能下降、指标合理,再让BO去接管调参,能少掉90%的排查时间。