☰
多变量时间序列预测实战:VMD分解+NRBO优化+Transformer-GRU混合模型
2026/10/2 4:52:02 网站建设 项目流程

简介:一份面向具备深度学习和时间序列分析基础的研发人员与技术爱好者的多变量时间序列预测项目文档。项目围绕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, dtype=np.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_size=24, horizon=3) 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, K=5, alpha=2000, tau=0, DC=0, init=1, tol=1e-7): """ series: 一维数组 K: 模态数,气象/负荷数据常用 3~8 alpha: 惩罚因子,默认 2000;噪声大时降到 500~1000 tau: 噪声容忍度,为 0 表示严格重构 """ series = np.asarray(series, dtype=np.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], K=5, alpha=2000) 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 在 DC=0、tau=0 时重构误差接近零,如果误差超过 1e-6,说明 alpha 过小或序列末端有缺失值。别跳过这步,因为整个模型的可解释性建立在“分解加回归能还原真实序列”这个基础上。

3. NRBO 牛顿-拉夫逊优化算法:给模型找一组稳定超参数

3.1 为什么超参数寻优会选 NRBO

Transformer-GRU 模型能调的东西太多了:Transformer 的层数、头数、d_model,GRU 的隐层节点数、层数,学习率、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数据量小就倾向 1
gru_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_size=10, max_iter=15): """ 牛顿-拉夫逊优化思想的简化实现 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, dropout=0.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_model=d_model, nhead=nhead, dim_feedforward=d_model * 4, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.gru = nn.GRU(d_model, gru_hidden, num_layers=gru_layers, batch_first=True, dropout=dropout) 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, epochs=80, lr=1e-3, patience=10, clip=1.0): optimizer = torch.optim.AdamW(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=5 ) 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(f"early stop at epoch {epoch}") break model.load_state_dict(torch.load("best_model.pt")) return best_val

这里三个参数都是血泪经验产物。第一,clip=1.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_size=8、max_iter=15。迭代到后半程不要追求损失继续下降,而是观察最优参数是否在某个组合附近反复震荡,如果是,说明搜索已经收敛到局部区域。

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。先跑 K=3,再跑 K=5,对比中心频率分布和重构误差。同时把 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(f"col {col}: rmse {rmse:.4f} delta {delta:+.4f}")

注意置零必须作用在模型输入特征维度上,而不是在 VMD 分解前的原始序列上做。如果你把某个 IMF 置零后误差变大超过 3%,这个分量可以保留;如果误差反而变小,说明模型把这个分量当噪声拟合了,下一步就该回到 VMD 阶段把 K 调小,或者直接去掉这个分量再训练。

6.2 多步滚动预测:误差会在滚动中滚雪球

如果任务是未来 24 小时预测,一种做法是训练 horizon=1 的模型,然后滚动 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 训练完做置零验证。把这三步养成固定习惯,多变量预测项目才不至于变成全黑盒炼丹。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询