☰
PatchTST与贝叶斯优化实战:综合能源多变量负荷预测方案
2026/10/19 5:52:05 网站建设 项目流程

直接开题。前两个月我手上的一个综合能源负荷预测项目,最终就落在了“PatchTST + 贝叶斯优化”这套方案上。起初我也是从LSTM、TCN一路试过来的,指标马马虎虎,一旦预测步长拉到24小时以上,误差曲线就开始失控。后来把PatchTST搬进来,再用贝叶斯优化把三个关键超参搜了一遍,整体预测表现才真正稳下来。这篇文章就把整个项目的思路、代码实现、实验细节和踩坑记录整理出来,给正在做多变量时间序列预测、或者想尝试PatchTST但不太确定怎么落地的朋友一个完整参考。

这个项目面向的目标很明确:综合能源场景下,电负荷、冷负荷、热负荷等多条序列相互耦合,需要一个能同时建模多变量、支持长时间多步预测、又不会在调参上浪费大量人力的方案。适合有一定Python和深度学习基础、但还没系统上手PatchTST的读者,也适合那些已经跑通Transformer但总被超参折磨的团队。

1. 项目整体思路与方案选型

1.1 综合能源负荷预测在解决什么问题

综合能源系统和传统电力系统最大的区别,在于负荷形态的多样性和强耦合。同一套供能系统里,电、冷、热、气可能来自同一个能源站,比如燃气轮机先发电,余热再驱动吸收式制冷或者供暖。这类系统的负荷序列之间不是独立波动的,而是存在明显的时序关联和物理约束:气温升高时电制冷负荷上升,热负荷下降;工作日和节假日的电负荷形态完全不同,冷热负荷却可能延续前几天累积的墙体和管网蓄热效应。

我在项目里使用的是某园区综合能源系统一年的运行数据,采样间隔15分钟,单日96个时间点。包含的特征有:总有功功率、制冷站冷冻水供水温度、热网回水温度、室外温度、太阳辐照度、节假日标记等十几路信号。这种数据如果只用单一负荷序列做单变量预测,等于主动丢掉了大量可用的环境信息和耦合关系,结果在拐点处经常慢半拍。所以“多变量输入 + 多变量输出”在这个场景里不是可选项,而是刚需。

但多变量也带来一个老问题:不同负荷的量纲和波动特性差别太大。电负荷可能上下几千千瓦,冷负荷跟着气温慢慢爬坡,热负荷在冬季有尖锐的早晚高峰。把它们塞进同一个预测模型,如果模型结构处理不好,很容易出现“训练时电负荷主导loss、推理时热负荷一塌糊涂”的尴尬局面。这也是我后来坚持选择PatchTST而不是普通Transformer的重要原因之一。

1.2 方案选型:为什么是PatchTST而不是传统Transformer

变形金刚的结构天然适合序列建模,但它有一个明显短板:对长时间序列做逐点attention,计算复杂度和输入长度近似平方增长,真要把历史窗口拉到192、336甚至720个时间点,训练显存和耗时都很难接受。PatchTST的思路,是把时间序列先切成一组“图块序列”,每个图块覆盖一段连续的时间窗口,Transformer只对这几十个图块做attention,而不是对几百个原始时间点逐点操作。这样既保留了Transformer的全局建模能力,又把计算量和显存降到了工程上非常好用的区间。

另一个容易被忽略的设计是通道独立。PatchTST默认让每个变量通道各自走一条共享的Transformer编码器,相互之间不做attention。这个设计一开始让我有些怀疑:既然说好多变量耦合建模,为什么又要把通道拆开?实验中我对比了通道独立和通道混合两种做法,结果在综合能源数据上,通道独立的误差反而更低、训练更稳定。原因也不难理解:不同负荷序列在attention层互相干扰,模型很容易把注意力集中在“噪声最大”或者“量纲最大”的那条序列上,通道独立等于给了每条序列一个平等的建模机会,同时又通过共享编码器隐式学习了它们之间的共性模式。如果你想让模型显式利用跨变量关系,也可以在编码器后加一层通道相关性建模模块,但就我这个项目的测试来看,收益不太明显,还增加了过拟合风险。

1.3 为什么还要叠一层贝叶斯优化

很多人会有疑问:PatchTST本身已经很强了,直接把Transformer里的d_model、n_heads、dropout这些参数照搬默认值不行吗?我之前也这么干过,实际效果非常一般。PatchTST的敏感超参其实不少:patch长度决定每个图块覆盖多少原始时间点,stride决定图块之间的重叠程度,编码器层数决定了模型容量,而d_model和dropout又直接控制着过拟合和训练稳定性。

这些参数之间是相互影响的。patch长度拉大,图块数量减少,模型更快但可能丢失局部细节;d_model加大,拟合能力更强但需要更多数据。想靠网格搜索找最优组合,搜索空间会爆炸;靠人工试错,一个组合从训练到验证至少要几十分钟,试不了几组就失去耐心了。贝叶斯优化解决的就是这个“黑盒、昂贵、非凸”的调参问题:它用概率代理模型拟合超参数和验证误差之间的关系,再通过采集函数选择下一组最有潜力的参数,实测下来用很少的尝试次数就能逼近人工调参一整天才能达到的效果。

2. 数据准备与特征工程要点

2.1 多变量特征的构建与含义

我最后确定的输入特征一共14个维度,分成三类:

  • 负荷类:总有功功率、热负荷功率、冷负荷功率,这是预测目标,同时也是输入特征,因为负荷序列本身就具有很强的自相关性。
  • 环境类:室外温度、湿度、太阳辐照度、风速。综合能源负荷和环境变量关系密切,尤其是温度和辐照度,对冷负荷和光伏相关的电负荷影响非常大。
  • 日历类:小时、星期、是否节假日、是否工作日。这类特征用周期性编码生成,小时采用正弦余弦编码,星期和节假日用独热编码,避免模型把时间数字当成连续数值。

这里有一个细节值得多说一句:时间特征不能简单地把“小时”作为一个0到23的整数喂进去,否则凌晨0点和23点在数值上距离很远,但它们在负荷形态上其实是相邻的。正弦余弦编码可以把周期关系映射到单位圆上,使0点和23点的表示距离最近。

  • 特征说明: 小时 sin/cos、星期 onehot、节假日 onehot、温度、湿度、辐照度、风速、历史负荷等

2.2 窗口切分、归一化与数据泄露防范

滑动窗口的构造遵循一个固定逻辑:用过去一段时间的多变量数据预测未来一段时间的多变量数据。项目里我设置了两个窗口参数:历史窗口context_len = 336,大约是3.5天;预测长度pred_len = 96,对应未来一天。每次滑动步长和预测长度保持一致,这样可以保证训练集和验证集之间没有重叠覆盖。

归一化这里要特别小心,这也是最容易出“数据泄露”的地方。我在做标准化时,只用训练集的均值和方差去归一化验证集和测试集,绝对不能在全体数据上算统计量,否则相当于把未来的分布信息提前暴露给了模型。项目里我选择对每条变量通道分别做标准化,而不是把所有特征拼在一起做全局标准化,原因是不同量纲的序列混在一起,标准化后依然可能被数值分布拉偏。

综合能源负荷还有一个特殊问题:冷热负荷随季节变化明显,夏季冷负荷高、热负荷低,冬季反过来。如果训练集只包含某一季的数据,模型很难泛化到另一季。我的做法是把全年数据按时间顺序切分成训练、验证、测试三段,而不是随机打乱,这样更贴近真实的“用过去预测未来”场景,虽然验证集误差会难看一些,但结果才真正有说服力。

2.3 特征工程中的两个实际坑

第一个坑是历史负荷步长太短。很多入门方案喜欢用“过去24小时预测未来24小时”,这个配置在做短期预测时没问题,但面对冷负荷这种有明显惯性效应的序列,模型很容易只学到“今天和昨天长得像”,遇到天气突变时反应迟钝。把历史窗口拉到336以后,模型能够看到两三个完整周期,效果提升非常明显。

第二个坑是节假日标记的处理。公休假期的负荷曲线和普通工作日差别很大,但一年里节假日样本本来就少,模型很难只靠几个样本学会这个模式。我的做法是把节假日特征和星期特征分开编码,同时确保训练集里包含至少两轮完整的公共假期样本。如果你的数据只有半年或更短,这一点尤其要留心,否则模型的节假日预测基本等于瞎猜。

3. 核心代码实现与参数解析

3.1 Patch嵌入层的代码实现

PatchTST最关键的一步就是把原始时间序列切成图块。PyTorch里用unfold就能很干净地完成这个操作:

import torch def create_patch(x, patch_len=16, stride=8): """ x: (B, C, L), B是batch, C是变量通道数, L是原始序列长度 """ B, C, L = x.shape # 计算图块数量 num_patches = (L - patch_len) // stride + 1 # unfold沿最后维度滑窗 patches = x.unfold(dimension=-1, size=patch_len, step=stride) # patches: (B, C, num_patches, patch_len) return patches.reshape(B, C, num_patches, patch_len)

patch_len决定每个图块包含多少个原始时间点,stride是滑动的步长。当stride < patch_len时,相邻图块之间有重叠,模型能看到局部信息的两种位置编码,相当于一种数据增强;当stride = patch_len时,图块之间没有重叠,计算效率最高,但边界信息的利用会弱一些。项目里我最终选择的组合是patch_len=16、stride=8,也就是50%重叠。输入长度为336时,图块数量是(336 - 16) // 8 + 1 = 41,这个数量对Transformer来说非常友好,attention矩阵只有41×41,显存开销和训练速度都远优于对336个原始时间点做逐点处理。

图块生成后还要过一层线性映射和一个位置编码层,把每个 patch_len 维的向量映射到 d_model 维:

import torch.nn as nn class PatchEmbedding(nn.Module): def __init__(self, patch_len, d_model, dropout=0.1): super().__init__() self.patch_len = patch_len self.proj = nn.Linear(patch_len, d_model) self.dropout = nn.Dropout(dropout) def forward(self, patches): # patches: (B, C, num_patches, patch_len) x = self.proj(patches) # (B, C, num_patches, d_model) x = self.dropout(x) return x

注意这里每个变量通道都会走同一个线性映射,这是通道独立的体现,它的参数在所有通道之间共享,所以不会因为通道数量增加而显著增加参数量。如果你项目里各通道的量纲差异特别大,可以试试为每个通道单独做一次标准化之后再进PatchEmbedding,效果会更稳定一些。

3.2 Transformer编码器与预测头的关键配置

PatchTST的主体是标准的Transformer编码器,但有几个位置要注意。首先是是否在attention层加入相对位置偏置,我的实验显示,在综合能源这种周期性很强的数据里,相对位置编码比绝对位置编码更有效,因为负荷曲线最看重“当前时刻距离历史某个峰值有多远”。PyTorch原生TransformerEncoder没有内置相对位置编码,我用了RoPE旋转位置编码来替代,实现方式不复杂,收益却很直接。

import torch.nn as nn from einops import rearrange class PatchTSTEncoder(nn.Module): def __init__(self, d_model, n_heads, d_ff, n_layers, dropout=0.1): super().__init__() encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=n_heads, dim_feedforward=d_ff, dropout=dropout, activation='gelu', batch_first=True, norm_first=True # Pre-LN结构,训练更稳定 ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=n_layers) def forward(self, x): # x: (B*C, num_patches, d_model) return self.encoder(x)

这里我特意把norm_first设为True。Pre-LN结构在Transformer训练里的确比Post-LN更稳,梯度不容易爆炸,对学习率的容忍度也高不少。可以这么理解:Post-LN像先把房子盖好再装修,装修出错整栋楼都可能塌;Pre-LN像每盖一层就检查一次承重,局部的小偏差不会无限累积。

预测头部分,每个通道从编码器输出的num_patches维序列通过一个全连接层直接映射到pred_len维。这个结构看起来简单,但它要求输入窗口的图块数量足够多,才能把时间信息充分压缩到底层特征里。如果历史窗口太短,图块太少,预测头的压力就会很大,这也是我刚才强调把context_len拉到336的原因之一。

3.3 贝叶斯优化搜索超参数的实操代码

贝叶斯优化的落地我选的是Optuna库,内部默认用的TPE采样算法,和理论上的高斯过程版本效果接近,但工程集成更省事。直接用一个objective函数把训练和验证过程包起来就行:

import optuna import torch import numpy as np def objective(trial): # 每次trial从建议分布里采样一组超参 d_model = trial.suggest_int("d_model", 64, 256, step=32) n_heads = trial.suggest_categorical("n_heads", [4, 8]) n_layers = trial.suggest_int("n_layers", 1, 4) dropout = trial.suggest_float("dropout", 0.1, 0.4, step=0.05) patch_len = trial.suggest_int("patch_len", 8, 32, step=8) lr = trial.suggest_float("lr", 1e-4, 1e-2, log=True) model = build_patchtst( d_model=d_model, n_heads=n_heads, n_layers=n_layers, dropout=dropout, patch_len=patch_len ) val_loss = train_and_evaluate(model, lr=lr, epochs=50) return val_loss study = optuna.create_study(direction="minimize", sampler=optuna.samplers.TPESampler(seed=42)) study.optimize(objective, n_trials=30) print(study.best_params)

这个搜索空间看起来不大,但如果用网格搜索,5个维度至少几百组实验,贝叶斯优化30次就能收敛到很不错的区域。我的经验是搜索空间一定要先圈定合理范围,不能凭感觉把范围拉得过大。比如n_heads候选就直接给4和8,因为对于Embeding维度64到256的网络,更大的头数反而会把单个头的维度压得太小,影响attention表达力。

还有一个容易被忽略的点:每组trial内部需要固定随机种子,否则同一组超参两次跑出来的验证loss差异会盖过超参之间的真实差异。我在train_and_evaluate函数里设置了完整的种子控制,include了Python内置random、numpy和PyTorch的随机种子。

4. 实验设计与提升效果分析

4.1 对比实验:不同基线模型的差距

为了让结果有可比性,我固定了同样14维特征、同样336输入长度、同样96预测长度,和几个常见基线做了对比:

模型验证集MSE验证集MAE稳定性
LSTM(2层128单元)0.01870.0982一般
TCN(残差堆叠)0.01790.0944较稳
标准Transformer0.01620.0884容易过拟合
PatchTST(手动调参)0.01340.0791稳定
PatchTST(贝叶斯优化)0.01180.0742稳定

从这张表能看出两层信息。第一,Transformer类模型整体优于序列模型,标准Transformer虽然在验证集上表现不差,但在训练过程中会出现明显的过拟合信号,早停之前和之后的差距很大。第二,PatchTST在同样数据上比标准Transformer又低了一截,这个提升主要来自patch化之后模型对局部模式的感知更强,同时训练更充分。

贝叶斯优化在这个基础上又贡献了约12%的相对MSE下降。这个数字应该理性看待:它不是模型结构本身的质变,而是把原来“差不多能用”的配置推到了“接近这个结构和数据组合的天花板”。如果只追求能跑通,手动调参完全够用;但如果要写进交付报告或者上线对比,这12%往往就是排名和效果的分水岭。

4.2 多步预测误差随预测长度的变化

我特意对比了不同预测长度下的误差变化趋势,这里选用了PatchTST(贝叶斯优化版本)和LSTM作为代表:

预测长度LSTM MSEPatchTST MSE
24(6小时)0.00890.0057
48(12小时)0.01260.0083
96(24小时)0.01870.0118
192(48小时)0.02910.0166

误差随着预测长度拉长而上升是必然的,但上升的速度差异很有信息量。LSTM在预测长度从24拉到192时,MSE膨胀了3倍以上,而PatchTST只膨胀了大约2倍。这说明patch化的表示方式不仅提升了短中期预测精度,更重要的是在长步长下减缓了误差累积。原因也好理解:LSTM这种逐点递归的结构,每一步预测都依赖上一步输出,一旦某一步开始偏差,后续会被不断放大;PatchTST是直接进行一次序列到序列的映射,所有的预测点共享同一个编码表示,没有递归路径,误差不会像滚雪球一样传递。

4.3 贝叶斯优化到底带来了多少提升

看一下贝叶斯优化最终搜索到的一组典型参数,能更直观地感受它在做什么:

参数手动初值贝叶斯优化结果
d_model128192
n_heads88
n_layers32
dropout0.20.15
patch_len1624
lr3e-46.8e-4

这里有一个反直觉的发现:手动配置时我倾向于把层数堆多一点,认为模型参数越多表达力越强,但贝叶斯优化给出的最优层数只有2层。原因在于任务本身的数据量级只有几十万条样本,3层编码器的容量已经超过了数据能提供的有效信息量,过拟合的风险大于拟合能力的提升。类似地,patch_len从16增加到24,意味着每个图块覆盖6小时而非4小时,模型更侧重于提取小时级别的趋势,而不是被15分钟级的随机波动干扰。lr从3e-4调到6.8e-4,配合dropout降低,整体训练速度和收敛精度都更理想。

这些超参之间的交互用人工试错很难同时发现,但贝叶斯优化在30次搜索里就把这个组合找出来了,这也是我强烈建议在这个场景用贝叶斯优化而不是手调的核心原因。

5. 常见问题与排查实录

5.1 模型不收敛、loss震荡怎么办

综合能源数据在训练时最常见的现象是:loss先下降,几轮之后开始剧烈震荡,甚至出现NaN。排查顺序我一般这样排。

先看学习率和batch size是否同步合理。Transformer类模型对学习率的敏感度比LSTM更高,我记得有一次只是把batch size从32调成128,原来能用的3e-4学习率马上就显得偏大,训练曲线开始抖动。这种情况我会优先降低学习率一个数量级,同时检查输入数据中是否有异常值,比如某一天传感器断线回传的0值,直接混进历史窗口会污染整个batch。

再看norm_first是否开启。如果用的是PyTorch默认的Post-LNTransformerEncoderLayer,梯度稳定性会差不少,尤其在层数较多时。项目里我把所有对比模型都统一成Pre-LN结构,训练过程的稳定性肉眼可见地改善了。

最后还要检查label是否有spike。冷负荷在极端天气下可能出现突刺,如果不对这些值做截断或平滑处理,模型会被单点异常拉到错误方向。我的做法是对训练集的每个通道做99.5%分位截断,避免极端样本主导梯度。

5.2 贝叶斯优化搜索过慢怎么加速

贝叶斯优化本身并不慢,慢的是每一组trial都要完整训练几十轮。我踩过这个坑:第一次跑30个trial,每个trial训50轮,总共1500轮训练,一台普通卡跑了将近两天,效率低到让人想放弃。

后来用了几个加速手段,效果非常明显:

  • 先在小规模的训练子集上试跑,比如只用三分之一的训练样本,确认搜索空间和baseline正常后,再用全量数据精调。
  • 把epochs从50降到20,关注相对排名而不是绝对误差。贝叶斯优化关心的是“哪组超参更好”,不完全依赖绝对精度。
  • 开启trial.prune早停机制,如果一个trial在中间阶段的loss已经明显差于当前最优,就提前终止,能节省大量无效训练时间。

这套组合下来,后面再跑30个trial基本一个白天就完成了,精度没有明显损失。

5.3 复现性、种子设置与结果可比性

时间序列预测项目特别容易在复现性上翻车。由于训练集是按时间顺序切分的,和随机打乱的图像数据集不同,你一旦换了切分方式,哪怕模型结构不变,指标都会差很多。我在项目里把数据切分的起点、窗口滑动的步长、训练和验证的分界时间点全部固定下来,并写进配置文件:训练集使用全年数据的80%,验证集10%,测试集10%,严格按下标切分。

随机种子方面,我固定了所有能想到的来源:Python的random.seed、numpy.random.seed、torch.manual_seed、torch.cuda.manual_seed_all,以及CUDNN的deterministic模式和benchmark模式关闭。贝叶斯优化每次重跑时,严格根据随机种子保持一致,否则搜索结果也会有细微差别,这会直接破坏对比实验的可信度。

另外,如果要在多卡环境跑,还要注意数据加载器DataLoader的shuffle顺序,多进程的worker也会引入随机性。稳妥起见,我习惯在每次采样前设置DataLoader的generator,确保每一个epoch的数据顺序完全一致。

6. 个人实操体会与扩展建议

6.1 我自己踩过的几个坑

第一个印象最深的坑是特征归一化的时机。我第一版代码为了省事,在构造窗口之后直接对整个特征矩阵做了标准化,单独跑模型时误差很低,但一换预测场景就崩。后来排查发现,验证集的信息混进了归一化统计量,模型在训练阶段就已经“偷看”了未来数据的分布。这个错误非常隐蔽,因为训练loss会异常好看,但上线后一遇到分布偏移就原形毕露。

第二个坑是把patch的作用理解得太简单。我一开始以为patch_len越大越好,结果调到32之后模型反而变差了。原因很简单,大patch会丢掉15分钟级别的细节变化,负荷在整点前后的快速爬坡会被平滑掉。后来用贝叶斯优化把patch_len定为24,也就是每个patch覆盖6小时,恰好平衡了趋势提取和细节保留。这个数值在不同数据里未必一致,但提醒了我一个原则:patch_size应该和业务周期对齐,而不是拍脑袋选一个看起来合理的数字。

第三个坑是训练轮数不够。PatchTST在综合能源数据上的收敛速度比LSTM慢,前20轮看起来指标一般,如果按传统经验在30轮早停,很可能错过后面更优的区间。我最终设置了50轮训练和按验证loss早停的机制,让模型跑满足够多的迭代再评估。

6.2 下一步可以怎么扩展

这套框架的直接扩展方向有两个。一是把预测范围延伸到园区储能和光伏出力的联合预测,因为综合能源系统里负荷和发电是一体两面,多变量预测天然可以容纳这类新增通道。二是把损失函数从单纯的MSE替换成分位数损失,可以让模型同时输出预测区间,这对运行调度更有实际价值,毕竟调度员需要的不只是一个数值,而是一个置信范围。

如果你手头的数据量再大一些,比如多个园区、一年以上粒度更细的数据,可以试试把通道独立的PatchTST改成每通道独立Embedding但不共享编码器的变体,或者加入分支式的通道注意力模块。这个方向我还在试,从目前的小规模实验看,只要能控制好过拟合,预测尖峰负荷的潜力会比现在的版本更好。

整体来说,PatchTST加贝叶斯优化这套组合,在综合能源负荷多变量预测场景下,既有理论上的合理性,也有工程上的可操作性。如果你正在评估新的负荷预测方案,不妨从这套结构开始,把数据和超参搜索空间准备到位,你会很快看到它和其他基线之间的差距。

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

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

立即咨询