
简介一份面向具备深度学习和时间序列分析基础的研发人员与技术爱好者的多变量时间序列预测项目文档。项目围绕VMD-NRBO-Transformer-GRU展开先以变分模态分解去除冗余与噪声再借助Transformer的自注意力捕获长距离依赖、GRU建模局部动态最后用牛顿-拉夫逊优化算法改善参数收敛效率典型场景包括设备预测维护、金融市场分析、智慧交通流量预测等。压缩包为docx格式仅1个文件、约88KB章节完整覆盖项目背景、目标意义、挑战与解决方案、模型架构、代码示例、特点与创新等目录结构便于按需定位。已有148人学习文档对数据预处理质量、VMD参数设置、模型结构设计与优化算法调试等关键环节均给出落地说明并体现模块化设计思路适合需要快速掌握该组合模型并将方案迁移到自身业务中的读者。1. 多变量时间序列预测为什么要叠 VMD、NRBO、Transformer、GRU做过多变量时间序列预测的人都有体会数据量大不等于信息足风速、温度、气压、历史负荷堆在一起模型常常顾此失彼。之前我在风电功率预测项目里吃过亏单一Transformer抓得住长期趋势但遇到突变天气峰谷位置几乎全偏换成GRU短期动态好了长周期规律又丢。后来我把预测链路改成 VMD 分解加 NRBO 寻优再配合 Transformer-GRU 混合模型才把验证集上的滞后问题压下去。这套方案并不是玄学本质是一条清晰的数据处理流水线先把非平稳序列拆成平稳模态再让优化算法自动找到超参组合最后用两种网络结构分别捕捉长期依赖和局部变化。适合的人群也很明确就是那些用单一模型已经调到头、手上有多变量数据、且不满足于“整体趋势对、峰谷全偏”的预测场景。2. VMD 变分模态分解把多变量序列拆成可学习的子序列2.1 从 EMD 到 VMD为什么先分解再预测时序预测里最常见的问题不是模型不够深而是输入信号太脏。原始负荷曲线里既有日周期性又有天气引起的突变还有噪声。如果直接把这种非平稳序列喂给 Transformer模型很快会学到一个取巧的规律复制上一个时刻的值。因为对模型来说当前时刻和上一时刻最像比学习真正的因果关系省力得多。这就是很多预测项目里曲线“滞后一个点”的根源。传统做法里有人先用差分有人用小波去噪但差分损失了相位信息小波又要人工选基函数。VMD 的思路是把原始序列看作多个围绕中心频率振动的窄带模态用一个带约束的变分问题一次性把模态估计出来。目标很直观每个模态的估计带宽之和最小同时所有模态相加等于原始信号。和 EMD 比VMD 有两个明显优势。第一EMD 是递归剥残差前面模态的误差会一路传下去VMD 是整体求解模态混叠少。第二EMD 在序列两端经常出现飞翼VMD 通过边界约束把端点效应控制得好很多。这就是它适合作为多变量时间序列预测前置处理器的原因。那“多变量”和 VMD 怎么结合工程上有两种常见做法。第一种是只对目标列做 VMD其他变量例如风速、温度当外生输入。第二种是对所有变量都做 VMD再在分量级上做相关性筛选。第二种看似信息无损实际会让特征维度爆炸比如 12 个变量每列拆 5 个模态就是 60 个通道训练成本和过拟合风险都明显上升。我一般只对目标列做 VMD外生变量保持原始尺度。这样既能拿到平稳的目标分量又不会让模型输入维度失去控制。2.2 滑动窗口与数据归一化多变量样本怎么构造动手跑 VMD 之前先把数据组织好。多变量预测的标准做法是滑动窗口用过去 T 个时刻的所有特征预测未来 h 步的目标值。下面这段代码把原始二维表转换成模型输入和标签。一个容易忽略的点是 horizon 参数很多人写的“多步预测”其实是把未来 24 个点放在窗口后面让模型一次输出工程上更关心的是未来第 h 小时的值这时用固定索引取目标列更方便。import numpy as np def build_multivariate_dataset(data, window_size, horizon): data: 二维数组形状 [n_samples, n_features] 最后一列是预测目标列例如负荷/功率 window_size: 用过去多少个时刻作为输入 horizon: 预测未来第 h 步而不是预测接下来 h 步 返回: X: [n_samples, window_size, n_features] y: [n_samples, 1] X, y [], [] data np.asarray(data, dtypenp.float32) n len(data) for i in range(n - window_size - horizon 1): X.append(data[i : i window_size, :]) y.append(data[i window_size horizon - 1, -1]) return np.array(X), np.array(y) # 示例5 个变量样本长度 800 raw np.random.randn(800, 5) raw[:, -1] np.cumsum(np.abs(raw[:, -1])) # 构造非平稳目标列 X, y build_multivariate_dataset(raw, window_size24, horizon3) print(X.shape, y.shape) # 输出: (774, 24, 5) (774, 1)这段代码有两个值得强调的设计。第一窗口包含全部变量列VMD 分解完目标列后再把模态分量拼到特征维度上。第二训练集、验证集、测试集按时间顺序切分不要随机打乱。时间序列随机打乱属于经典的“数据泄漏”会让验证集指标虚高。接着是归一化。这里有一条血泪经验先切窗再归一化而且只用训练集统计量。不要在整个原始序列上做 MinMaxScaler然后再切窗。因为 scaler 的 min 和 max 来自全序列测试段的信息已经提前参与统计等于在训练阶段偷看了未来。from sklearn.preprocessing import MinMaxScaler # 按时间顺序切分前 70% 训练中间 10% 验证最后 20% 测试 train_end int(len(raw) * 0.7) val_end int(len(raw) * 0.8) train_raw raw[:train_end] val_raw raw[train_end:val_end] test_raw raw[val_end:] scaler MinMaxScaler() scaler.fit(train_raw) train_scaled scaler.transform(train_raw) val_scaled scaler.transform(val_raw) test_scaled scaler.transform(test_raw) X_train, y_train build_multivariate_dataset(train_scaled, 24, 3) X_val, y_val build_multivariate_dataset(val_scaled, 24, 3) X_test, y_test build_multivariate_dataset(test_scaled, 24, 3)切分时注意区间不要重叠。比如训练集用到第 560 个点验证集应该从第 560 个点之后开始而不是再从第 540 个点重新切。如果样本量很小可以用交叉验证但要按时间块切分不能像普通分类任务那样随机抽样。2.3 VMD 最小可运行代码与两个必调参数现在对目标列做 VMD。常见的做法是使用 vmdpy 这类封装库你也可以按论文里的优化框架自己实现。下面这段代码把目标列拆成 K 个模态重点观察两个参数模态数 K 和惩罚因子 alpha。from vmdpy import VMD def vmd_decompose(series, K5, alpha2000, tau0, DC0, init1, tol1e-7): series: 一维数组 K: 模态数气象/负荷数据常用 3~8 alpha: 惩罚因子默认 2000噪声大时降到 500~1000 tau: 噪声容忍度为 0 表示严格重构 series np.asarray(series, dtypenp.float64).reshape(-1, 1) u, u_hat, omega VMD(series, alpha, tau, K, DC, init, tol) # u: shape [K, len(series)]每行是一个 IMF return u, omega u_train, omega_train vmd_decompose(train_scaled[:, -1], K5, alpha2000) print(u_train.shape) # (5, 560) print(omega_train) # 各模态中心频率vmdpy 返回三件套里我实际最关注的是 omega。如果相邻中心频率非常接近说明 K 给多了两个模态在抢同一段频带如果最后一个中心频率异常高说明模型在硬造高频噪声。把 omega 打出来看一眼比看误差曲线更直观。另一个常踩的坑是 VMD 对序列长度敏感长度在 2 的整数次幂附近时迭代更稳定。数据太长就先分段分解再按时间拼接。模态拼到滑窗特征里还有一个对齐问题。VMD 分解的是整段目标列分解后每个模态长度等于原始序列长度而滑窗样本的末尾才是当前时刻。拼特征时要让窗口最后一个位置对齐模态的当前时刻别整体平移错位。我一般这样处理def concat_vmd_features(X, imf_list): X: 滑窗后的样本 [n_samples, window_size, n_features] imf_list: [K, len(series)] 的模态矩阵 返回: [n_samples, window_size, n_features K] n_samples, window_size, n_features X.shape new_feats [] for k in range(imf_list.shape[0]): imf imf_list[k] feat np.zeros((n_samples, window_size)) for i in range(n_samples): start i window_size feat[i, :] imf[start : start window_size] new_feats.append(feat) return np.concatenate([X, np.stack(new_feats, axis-1)], axis-1)每次分解完我都习惯做一次重构误差检查。VMD 在 DC0、tau0 时重构误差接近零如果误差超过 1e-6说明 alpha 过小或序列末端有缺失值。别跳过这步因为整个模型的可解释性建立在“分解加回归能还原真实序列”这个基础上。3. NRBO 牛顿-拉夫逊优化算法给模型找一组稳定超参数3.1 为什么超参数寻优会选 NRBOTransformer-GRU 模型能调的东西太多了Transformer 的层数、头数、d_modelGRU 的隐层节点数、层数学习率、dropout、batch_size、梯度裁剪阈值还有上游 VMD 的 K 和 alpha。手调这些参数本质上是在一个高维黑匣子里试错。网格搜索在参数少的时候还行参数一多组合数爆炸贝叶斯优化需要构造代理模型对刚搭好的 PyTorch 训练流程来说侵入感太强。NRBO 这类基于种群的启发式算法最大的好处是只需要一个“输入一组参数、输出验证集损失”的黑盒函数就能直接跑。NRBO 的名字来自牛顿-拉夫逊法。经典牛顿法在解非线性方程时用切线去逼近零点每一步都朝函数值变化更快的方向走。NRBO 把这种思想搬到群体优化里让每个候选解既朝当前最优解移动又保留一定种群多样性。和 PSO、GWO 相比NRBO 在低维连续参数空间上收敛速度中上实现简单很适合和深度学习训练流程组合。更重要的是它不要求目标函数可导。这里的目标函数是训练一组 Transformer-GRU 后在验证集上算出来的损失整个过程离散、有噪声、不一定光滑梯度类方法根本用不了。3.2 优化变量、目标函数与搜索边界先把优化问题定义清楚。外层是 NRBO内层是 PyTorch 训练。NRBO 迭代一次就要完整训练一个模型所以在真实项目里迭代轮数别给太多种群大小一般 8 到 12迭代次数 10 到 20 次是比较常见的设置。参数边界可以这样定参数下界上界说明learning_rate1e-41e-2对训练稳定性影响最大nhead28d_model 必须能被 nhead 整除num_layers13数据量小就倾向 1gru_hidden16128GRU 隐层节点数dropout00.3防止过拟合vmd_K38VMD 模态数alpha2003000VMD 惩罚因子注意最后一组参数vmd_K 和 alpha 不是模型参数但它们直接改变输入特征。NRBO 调参时把这两个变量一起放进搜索空间让优化器自己去权衡“分解到什么程度最利于后续模型”。这是把 VMD 和 NRBO 结合在一套框架里的关键只调网络参数不调分解参数效果会打折扣。目标函数用验证集 RMSE 还不够我一般用验证集的加权 MAPE因为 MAPE 对峰谷更敏感。如果目标是风功率这类本身有物理上下限的序列可以加一个边界惩罚预测值超出装机容量范围时在损失里额外加一个惩罚项。这样 NRBO 搜索到的参数不会只在平均值上好看。3.3 一个可用的 NRBO 简化实现下面是一版剔除论文细节后的简化实现保留牛顿-拉夫逊优化算法最核心的“朝最优切线方向移动”思想。实际项目中也可以直接搜 NRBO 的开源实现再替换这里的更新公式但要注意收敛条件、边界处理和浮点稳定性。import numpy as np def nrbo_minimize(objective, bounds, pop_size10, max_iter15): 牛顿-拉夫逊优化思想的简化实现 objective: 输入参数字典返回损失值 bounds: 每个参数的 (下界, 上界) keys list(bounds.keys()) # 初始化种群 population {} for k in keys: low, high bounds[k] population[k] np.random.uniform(low, high, pop_size) best_loss float(inf) best_params None for it in range(max_iter): # 评估当前种群 for i in range(pop_size): params {k: population[k][i] for k in keys} loss objective(params) if loss best_loss: best_loss loss best_params params # 朝最优解移动加入一阶差分扰动 for i in range(pop_size): for k in keys: new_val ( population[k][i] np.random.rand() * (best_params[k] - population[k][i]) 0.1 * np.random.randn() ) low, high bounds[k] population[k][i] np.clip(new_val, low, high) return best_params, best_loss这个代码有两点必须改。第一gru_hidden、nhead、num_layers 是离散整数NRBO 搜索到连续值后要在 objective 内部手动取整否则传入 PyTorch 时会报错。第二更新公式里的随机扰动系数 0.1 不是固定值。数据噪声大时可以把这个系数提到 0.2 增加种群多样性避免 NRBO 早早收敛到局部最优。但扰动过大又会让最优解在后期反复跳走。一种经验做法是前期让扰动大后期逐步降低和退火思想类似代码上可以简单写成0.2 * (1 - it / max_iter)。外层调用时还要注意 objective 不能只训练固定 epoch 就结束。我一般会加上早停目标函数返回的是“训练过程中最好的验证损失”而不是最后一轮的损失。否则 NRBO 的搜索会被过拟合后期噪声干扰选出来的参数在测试集上不稳定。4. 搭建 Transformer-GRU 模型长期注意力与局部门控的组合4.1 模型结构怎么组织串联、并联还是拆分子分支Transformer 在长时间序列上的优势是全局感受野GRU 的优势是轻量、保留动态更新。把两者拼在一起最常见的两种结构是串联和并联。串联是输入先经过 TransformerEncoder得到每个时间步的编码特征再送入 GRU最后取 GRU 最后一个时间步的隐状态接全连接输出。并联是 Transformer 和 GRU 各自接收原始输入输出特征拼接后过全连接。并联保留了两路特征但参数更多在小数据集上更容易过拟合。在 VMD 已经做了频段分离的情况下我一般用串联。因为分解后模态已经平稳模型接下来要做的是把长期规律和短期变化组合在一起。Transformer 的输出自带全局上下文GRU 再跟着局部走势走一遍能把注意力漏掉的时序依赖补回来。实验里串联结构在峰谷附近的误差通常比并联小训练也更快。4.2 PyTorch 实现最小可运行版 Transformer-GRU下面是一版可直接跑的模型定义。注意代码里做了几个工程化处理输入先经过线性投影到 d_model 维再进 TransformerEncoder保证输入特征维度变化时不用改模型主体GRU 取最后一个时间步隐状态而不是对所有时间步做平均。import torch import torch.nn as nn class TransformerGRU(nn.Module): def __init__(self, input_size, d_model, nhead, num_layers, gru_hidden, gru_layers, output_size, dropout0.1): super().__init__() assert d_model % nhead 0, d_model 必须能被 nhead 整除 self.input_proj nn.Linear(input_size, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.gru nn.GRU(d_model, gru_hidden, num_layersgru_layers, batch_firstTrue, dropoutdropout) self.head nn.Linear(gru_hidden, output_size) self.dropout nn.Dropout(dropout) def forward(self, x): # x: [batch, window_size, input_size] x self.input_proj(x) # [batch, window_size, d_model] x self.encoder(x) # 输出仍是 [batch, window_size, d_model] out, _ self.gru(x) # 每个时刻都有局部更新 out out[:, -1, :] # 取最后一个时间步的隐状态 return self.head(self.dropout(out))参数说明这块值得多写几行。input_size 是滑窗特征维度比如原始 5 个变量加 5 个 VMD 模态那 input_size 就是 10。d_model 是 Transformer 内部向量维度常见取值 64 或 128。nhead 必须能整除 d_model每个头实际分配到的维度是 d_model/nhead。dim_feedforward 一般设为 d_model 的 4 倍再大在小样本上容易过拟合。GRU 层数大于 1 时 dropout 参数才真正生效层数设为 1 时 PyTorch 会忽略 dropout 设置别误以为没生效。最后一行out[:, -1, :]是很多人会忽略的细节。整个序列送入 GRU 后最后一个时间步的隐状态是模型对最新状态的压缩编码。取最后一个时刻比把每个时刻的平均值当特征要稳因为在时间预测里最新信息权重最大。如果你的任务是预测未来第 h 步而非下一步也可以取倒数第 h 个位置的隐状态但那样通常没有直接调 horizon 好用。4.3 训练循环梯度裁剪、学习率调度和早停模型定义好后训练循环要考虑三个工程问题梯度裁剪防 NaN学习率调度防过拟合早停保存最优权重。下面是一个可以直接套用的训练函数。def train_model(model, X_train, y_train, X_val, y_val, epochs80, lr1e-3, patience10, clip1.0): optimizer torch.optim.AdamW(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 ) loss_fn nn.MSELoss() best_val float(inf) early_stop_cnt 0 for epoch in range(epochs): model.train() train_losses [] for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), clip) optimizer.step() train_losses.append(loss.item()) model.eval() with torch.no_grad(): val_pred model(X_val) val_loss loss_fn(val_pred, y_val).item() scheduler.step(val_loss) if val_loss best_val - 1e-4: best_val val_loss torch.save(model.state_dict(), best_model.pt) early_stop_cnt 0 else: early_stop_cnt 1 if early_stop_cnt patience: print(fearly stop at epoch {epoch}) break model.load_state_dict(torch.load(best_model.pt)) return best_val这里三个参数都是血泪经验产物。第一clip1.0对 Transformer-GRU 通常够用如果损失逐渐变成 NaN先把 clip 降到 0.5而不是急着改学习率。第二scheduler.step(val_loss)必须传验证集损失不能传训练损失。传训练损失会让调度策略被过拟合阶段的震荡带偏该降学习率的时候不降。第三best_val - 1e-4这个缓冲阈值能避免验证集损失小范围抖动时反复保存权重。数据噪声高时可以把阈值放宽到 1e-3减少硬盘写次数。5. 避坑指南多变量预测最容易翻车的 5 个环节5.1 数据泄漏VMD 分解用到了未来信息现象验证集 RMSE 很低测试集完全崩掉或者预测峰谷比真实信号提前几个点看起来模型“未卜先知”。原因最常见的是先对整个数据集做 VMD 分解再切训练集和测试集。VMD 的约束求解是全序列一起做的测试段的波形参与了模态中心频率的估计模态里已经带着未来信息。模型训练时等于提前见过了测试段的大致形状。解决先按时间切好 train/val/test只对训练段目标列分解。测试段要用训练段分解出的中心频率去重构不是把测试段单独重新跑一次 VMD。如果用的是 vmdpy 这类批处理接口需要自己封装一个“训练段分解 测试段前向分解”的流程测试段只负责在固定模态上投影。5.2 NRBO 把模型训练成了过拟合专家现象NRBO 迭代十轮目标函数值越来越低把最优超参搬到新数据上效果还不如默认参数。原因外层优化目标函数用的是“验证集上的一次训练结果”小数据上验证集噪声大NRBO 本质上是在拟合验证集噪声。任何不限数据集规模的元启发式优化最终都会钻验证集的空子。解决给 objective 加两层保险。第一每次评估用多折验证取平均值不要单靠一个验证切片。多折验证会让每次训练成本翻倍但选出来的参数更稳。第二限制 NRBO 迭代轮数和种群数比如 pop_size8、max_iter15。迭代到后半程不要追求损失继续下降而是观察最优参数是否在某个组合附近反复震荡如果是说明搜索已经收敛到局部区域。5.3 Transformer-GRU 训练经常出现 NaN现象前几个 epoch 正常某一步 loss 突然变成 NaN之后全部是 NaN。原因常见是学习率太大梯度在反向传播中被 GRU 跨层放大另一种是输入数据里有极值MinMaxScaler 在切分后某一列零方差导致除零还有一种可能是 d_model 和 nhead 组合导致维度爆炸。解决先查输入用np.isfinite(X).all()和np.isfinite(y).all()过一遍。然后加梯度裁剪。如果问题还在把 Adam 的 eps 从默认 1e-8 调到 1e-6。最后再考虑把 d_model 从 128 降到 64因为小数据集上高维投影会放大数值误差。5.4 VMD 模态混叠与虚假周期现象分解出的第一个 IMF 看起来和原始序列几乎一样其余分量接近零或者某个 IMF 呈现明显等幅正弦肉眼一看就知道不真实。原因K 设置偏大或 alpha 设置偏大。VMD 为了满足重构约束会给中间频率硬造模态噪声大时噪声也会被单独分离成一个伪周期模态。解决打印 omega 中心频率若相邻频率差小于频率分辨率的 2 倍就降低 K。先跑 K3再跑 K5对比中心频率分布和重构误差。同时把 alpha 从 2000 降到 1000让模态带宽适当放宽。这里的一个经验是K 宁可少不要多少一个模态只是重构误差稍大多一个模态会引入虚假特征影响后面模型的可解释性。5.5 预测滞后与峰谷偏低模型在用捷径现象预测曲线比真实曲线整体右移峰谷永远矮一截。这种滞后问题在多变量时间序列预测里非常常见。原因模型发现用上一时刻的观测值推断下一时刻最省力于是进入“复制上一个值”的局部最优。VMD 之后目标列变得更平稳这个捷径更容易被模型走通。解决最直接的做法是让模型预测目标列的一阶差分预测完再累加还原。差分会让非平稳序列变得平稳模型无法通过复制原值偷懒。第二种办法是在损失函数里对峰值附近的样本加大权重让模型重视峰谷误差。第三种办法是显式把滞后写入评价指标例如在验证集上计算预测序列与真实序列的互相关偏移滞后超过一个采样点就扣分。三种办法选一种即可我通常选差分因为它改动最小、效果最直接。6. 进阶验证把结果从“指标好看”变成“真正可用”6.1 分量归因看模型到底用了哪个模态当 VMD 把目标列拆成 5 个 IMF模型未必每个都在用。一个实用的验证方法是对输入分量做逐列置零观察预测误差变化。做法很简单模型训练好后把输入特征里的某个 IMF 列全部置为 0重新跑一次测试集看 RMSE 变化。某个分量置零后误差跳增说明模型确实学了这一模态误差几乎不变说明该模态是冗余信息。这个方法不带严谨的统计学意义但比跑 SHAP 快得多适合在项目排期紧张时快速定位模型行为。def evaluate(model, X, y): model.eval() with torch.no_grad(): pred model(X) return torch.sqrt(((pred - y) ** 2).mean()).item() def importance_by_zeroing(model, X_test, y_test, feature_cols): base_rmse evaluate(model, X_test, y_test) for col in feature_cols: X_masked X_test.clone() X_masked[:, :, col] 0.0 rmse evaluate(model, X_masked, y_test) delta rmse - base_rmse print(fcol {col}: rmse {rmse:.4f} delta {delta:.4f})注意置零必须作用在模型输入特征维度上而不是在 VMD 分解前的原始序列上做。如果你把某个 IMF 置零后误差变大超过 3%这个分量可以保留如果误差反而变小说明模型把这个分量当噪声拟合了下一步就该回到 VMD 阶段把 K 调小或者直接去掉这个分量再训练。6.2 多步滚动预测误差会在滚动中滚雪球如果任务是未来 24 小时预测一种做法是训练 horizon1 的模型然后滚动 24 次。但滚动预测有个致命问题每次用模型自己的预测值回灌窗口误差逐时放大尤其是在峰值附近。更稳的做法是同时跑两个版本对比一个用真实观测值回灌一个用预测值回灌。前者给出理论上限后者给出实际可用性。如果预测值回灌到第 8 步就开始发散说明模型长程记忆不够可能需要降低 GRU 层数或者把输出改造为多步头一次预测 24 个目标点而不是滚动推理。6.3 评价指标别只看 RMSE把滞后惩罚写进去最后一个进阶技巧是评价指标。RMSE 会把偏置和滞后混在一起算掩盖实际问题。建议额外加一个带时序惩罚的指标对峰谷附近的时间点加大权重。这个方法实现简单但能逼着模型把注意力放在真正重要的拐点上。def weighted_rmse_with_lag_penalty(y_true, y_pred, peak_positions): err (y_true - y_pred) ** 2 weight np.ones_like(y_true) for pos in peak_positions: weight[max(0, pos - 1): pos 2] 2.0 return np.sqrt((err * weight).mean())peak_positions 可以用差分阈值检测出来也可以从业务侧拿。如果加了滞后惩罚后指标明显变差说明模型在峰谷附近确实犯懒了需要回到模型结构层面去修而不是继续调学习率。这套 VMD-NRBO-Transformer-GRU 链路我前前后后改过很多次最值钱的经验不是某一处代码而是每加一个模块都保留一个可解释的验证动作VMD 之后看重构误差NRBO 之后看超参稳定性Transformer-GRU 训练完做置零验证。把这三步养成固定习惯多变量预测项目才不至于变成全黑盒炼丹。希望帮到你。本文还有配套的精品资源点击获取