简介:这是一套面向电力负荷预测的完整Python代码实现,核心算法采用变分模态分解(VMD)结合卷积神经网络(CNN)、双向长短期记忆网络(BiLSTM)与注意力机制(Attention),适用于毕业设计、课程设计以及相关科研实验。针对电力负荷数据波动性强、非线性明显、时序依赖复杂等特点,资源提供了从变分模态分解、特征构造到模型训练评估的整套流程,可帮助学习者快速掌握组合模型在负荷预测中的用法。压缩包内共包含15个文件,主体是6个Python脚本,用于运行主模型和多种对比模型;另有2个Jupyter Notebook用于交互式演示,3个zbak备份文件对重要代码进行留档,1个附赠zip提供扩展内容,并附带电力负荷数据集CSV、分解结果Excel及说明TXT,整体压缩后仅2.86MB,轻量便于获取。该资源已有119人学习,适合需要参考源码、复现实验或进行模型对比的研究者与学生。通过本份资源,可以运行变分模态分解-卷积神经网络-双向长短期记忆网络-注意力机制主模型,同时获得VMD-LSTM、VMD-CNN-BiGRU等对照实验脚本、数据预处理与分解保存代码,以及使用说明,是开展负荷预测课题的实用参考资料。
1. 负荷预测研究到了拼特征提取能力的阶段:VMD-CNN-BiLSTM-Attention就是当前实用度最高的组合之一
电力系统的短期负荷预测,一直是调度、规划和市场化交易的前置环节,预测误差每降一个百分点,背后就是实打实的备用容量和成本。但这几年大家翻车翻得最集中的地方,不是模型不够深,而是原始负荷序列本身就带着强非线性,加上天气、节假日、生产计划这些外部因素的扰动,直接丢给LSTM或者Transformer,效果往往不稳定。我一般会先用VMD(变分模态分解)把负荷序列拆成几个不同频段的模态分量,再用CNN提取局部特征、BiLSTM双向建模时间依赖、Attention层对关键历史时段加权。这条VMD-CNN-BiLSTM-Attention路线,适合做短期负荷预测的研究生和工程师,用Python复现的成本不高,落地效果好。
2. 功原理拆解:VMD为什么值得前置,CNN、BiLSTM和Attention各扛什么活
2.1 VMD:把非平稳负荷序列拆成K个有限带宽模态
负荷序列最麻烦的地方在于它的非平稳性。早晚高峰的快速爬升是高频成分,季节趋势是低频成分,突发天气影响又叠加在中间频段。直接让神经网络在一个非平稳序列上做映射,模型要自己隐式地做频段分离,这等于把复杂问题加在了模型自己身上。VMD的做法是把这个序列分解成K个调幅-调频信号,也就是固有模态函数(IMF),每个IMF相对平稳、带宽有限。
VMD的数学框架是求解一个约束变分问题,目标是最小化每个模态的估计带宽之和。和EMD相比,VMD的分解过程不是递归式的、不靠包络极值来筛,而是整体求解,这就规避了EMD最让人头疼的模态混叠问题,分解结果在可重复性上也更稳。
实际用vmdpy库时,最常调的参数就两个:模态个数K和惩罚因子alpha。K设小了,模态内部仍然混着多个频段;K设大了,同一个频段被拆碎,还会出现两个相邻模态变成“双胞胎”的情况。K到底怎么定?常见做法是观察中心频率:把K从3往上加,同时打印每个IMF的中心频率,如果发现两个模态中心频率几乎重合,说明K过头了。alpha影响带宽约束的严格程度,alpha越大,各模态带宽越窄,经验取值2000在多数负荷数据上表现不差——这是后面章节要展开的调参内容。
2.2 CNN:一维卷积做局部时段特征提取
分解后的每个IMF相对平稳,但仍然有局部模式。CNN在这里做的不是图像识别,而是用一维卷积核在时间轴上滑动,提取相邻时段的局部关联。比如负荷爬坡过程往往持续30到60分钟,如果时间粒度是15分钟,卷积核大小为4的一维卷积就能看到一小时的爬坡形状,卷积核自动学习“爬坡速率”和“峰谷形态”这类局部模板。
CNN参与时序预测有两条常见路线。一条是纯粹用CNN堆感受野,比如TCN;另一条是把CNN当作嵌入层,后面接循环网络。VMD-CNN-BiLSTM-Attention走的是后一条:每个模态先过Conv1d做局部特征变换,输出仍然是序列结构,再交给BiLSTM。这里有个关键点:Conv1d的输入形状是(samples, timesteps, channels),负荷预测里每个模态是单通道序列,卷积后如果想让时间步完整保留,padding用same,不要用valid,否则序列长度被压缩,后续BiLSTM能回顾的历史就短了一截。
2.3 BiLSTM:双向时间依赖建模,补上CNN看不到的长程上下文
CNN的感受野终归有限,两三个卷积层叠加,看到的也只是几小时内的局部上下文。但负荷预测里有个典型场景:工作日早上8点的峰值,往往和前一天的同一时段强相关,也可能和一周前同一天的趋势相关。这种长程依赖CNN干不了,必须靠循环网络。
LSTM通过门控机制记忆长程信息,不过单向LSTM只看到过去的信息。BiLSTM的“双向”在于它同时在时间轴上做正向和反向两次遍历,两个方向的隐状态拼接作为输出。在滑窗模式下,喂给模型的是一个完整的历史窗口(比如最近96个时间步),窗口内每个位置的“未来”就是右侧已经观测到的数据点,所以离线训练和评估完全合法。需要提醒的是,如果做严格的在线实时预测且对延迟极度敏感,双向结构计算量翻倍,此时换成单向LSTM更实际。
2.4 Attention:让模型在预测时刻主动“回顾”最相关的历史片段
BiLSTM的输出是隐状态序列,每个时间步一个向量。传统做法是把最后一个时间步的隐状态取出来接全连接层,但这有一个隐含假设:最后一个隐状态已经把历史信息全部编码完了。这个假设在长序列和强周期信号上并不成立,所以需要注意力机制。
Attention的核心操作是打分和加权:在预测时刻,模型根据当前状态,对历史每个时间步的隐状态计算一个相关性分数,softmax归一化成权重,再做加权求和。权重越大,说明该历史时刻对当前预测越关键。在Keras里不需要手写复杂计算图,自定义一个Layer就能在BiLSTM输出序列上做时间步级的注意力池化。
从数据流看这个组合:VMD负责把非线性问题线性化,CNN负责提取局部形态,BiLSTM负责长程双向建模,Attention负责动态选择关键信息。四者分工不同且不重叠,这也是它在负荷预测竞赛和实际项目里表现稳定的原因。
3. Python实现:VMD分解、CNN-BiLSTM-Attention建模与训练全流程
3.1 数据读取、缺失值处理和归一化
负荷历史数据通常来自SCADA系统或计量自动化系统,常见格式是CSV,列至少有时间戳和负荷值。拿到数据的第一步,先把时间戳设为索引并排序,检查缺失值和重复值。如果索引不是连续的,比如缺了某一天的记录,先补出空行再填充,否则滑窗样本的时间间距不均匀,模型会学到错误的时间节奏。
接下来是归一化,这里有个容易翻车的细节:一定要先划分训练集和测试集,再在训练集上fit归一化器,测试集只用训练集的scale做transform。如果把全部数据一起归一化,测试集的信息就通过scaler泄漏到了训练过程,验证集指标虚高,等上线跑真实预测时立刻被打回原形。
import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 读取负荷数据,两列:ds(时间戳)、y(负荷值) df = pd.read_csv('load.csv', parse_dates=['ds']) df = df.set_index('ds').sort_index() # 如果时间索引不连续,先补全到15分钟间隔 df = df.asfreq('15min') # 异常值处理:连续3个及以上0值,用前后邻域中值替换 zero_mask = (df['y'] == 0).rolling(3).sum() >= 3 if zero_mask.any(): idx = df.index[zero_mask] for i in idx: left = df['y'].loc[:i].iloc[-4:-1].median() right = df['y'].loc[i:].iloc[1:4].median() df.loc[i, 'y'] = np.nanmedian([left, right]) # 划分训练/测试集:前80%训练,后20%测试 n = len(df) train_size = int(n * 0.8) train_df = df.iloc[:train_size] test_df = df.iloc[train_size:] # 只在训练集上fit归一化器,测试集只做transform scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_df[['y']]) test_scaled = scaler.transform(test_df[['y']])这段代码的顺序是刻意安排的:先排序、补全、清洗,再划分,最后归一化,顺序不能乱。rolling(3).sum() >= 3的含义是连续三个或以上零点才判定为异常段,避免误杀夜间低谷的合法低负荷;如果业务场景里0值本身就不正常,阈值可以调低。asfreq('15min')会为缺失时间戳插入空行,空值在后续邻域填充时一并处理。归一化器只在训练集上计算min和max,测试集用同一组参数变换,这是防数据泄露的第一道闸门。
3.2 用vmdpy对负荷序列做变分模态分解
VMD在Python里最方便的实现是vmdpy库,pip install vmdpy即可。核心函数VMD()返回u、u_hat、omega,其中u是分解后的模态分量矩阵,每行是一个IMF,omega记录了每次迭代的中心频率。
对整段序列做VMD是离线研究的常规做法,如果想上线滚动预测,更稳妥的做法是每来一个新点,取过去足够长的窗口重新做VMD,再让模型预测下一步,代价是计算量变大。下面代码按离线研究的标准流程来写。
from vmdpy import VMD import numpy as np # 对整段归一化后的序列做VMD signal = np.concatenate([train_scaled.flatten(), test_scaled.flatten()]) K = 5 # 模态个数,根据中心频率观察调整 alpha = 2000 # 带宽惩罚因子 tau = 0.0 # 噪声容忍度,0表示严格无噪声 DC = 0 # 0表示不强制直流分量进入独立模态 init = 1 # 中心频率初始化方式,1为均匀初始化 tol = 1e-7 # 迭代终止容差 u, u_hat, omega = VMD(signal, alpha, tau, K, DC, init, tol) # u 的形状是 (K, len(signal)),每一行是一个模态分量 print("模态分量矩阵形状:", u.shape) print("各模态中心频率:", omega[-1])VMD()的参数里,DC=0表示第一个模态不由直流分量强制占据,留给算法自己分配;init=1是均匀初始化中心频率,对有明显周期性的负荷数据,比随机初始化收敛更快更稳;tol=1e-7足够,太小会增加迭代次数但结果几乎不变。分解后查看omega[-1]可以帮助判断K是否合理:如果最后两个中心频率几乎重合,就该减小K;如果第一个模态中心频率已经接近0,说明直流分量未单独建模,有些场景下这是正常的,看振幅是否过小来判定。
3.3 构建滑窗样本并设计CNN-BiLSTM-Attention模型
分解得到K个模态后,用滑窗构建训练样本。核心思路是用过去window_len个时间步的K个模态值,预测未来horizon个时间步的原始负荷值。预测目标是原始负荷而不是某个模态的值,所以标签用该时刻所有模态之和,也就是模态在时间维度的叠加恰好还原原始序列。
def make_samples(u, window_len, horizon): # u shape: (n_timesteps, K),已经转置 n = u.shape[0] X, y = [], [] for i in range(0, n - window_len - horizon): X.append(u[i:i+window_len]) # (window_len, K) y.append(u[i+window_len+horizon-1].sum()) # 该时刻所有模态之和 return np.array(X), np.array(y) window_len = 96 # 96个时间步,对15分钟粒度来说就是过去24小时 horizon = 1 # 预测未来1个时间步 u_T = u.T # 转置为 (n_timesteps, K) X, y = make_samples(u_T, window_len, horizon) # 按4:1切分训练与验证 split = int(len(X) * 0.8) X_train, y_train = X[:split], y[:split] X_val, y_val = X[split:], y[split:]这里特别说明标签的构造方式。u_T的每一行是一个时间步上K个模态的值,u[i+window_len+horizon-1]取的是窗口结束位置的目标时刻,.sum()把这个时刻所有模态相加,就得到原始负荷在该时刻的值。滑窗步长为1,样本量最大,但相邻样本高度相关,验证集损失在训练后期可能有震荡;内存紧张时把步长改成4或8可以接受,验证集指标会略降。
模型构建使用Keras的Functional API。输入是(window_len, K),先经过一维卷积提取局部特征,再进BiLSTM,然后接自定义的Attention层做时间步加权求和,最后过全连接输出。
import tensorflow as tf from tensorflow.keras import layers, Model class AttentionLayer(layers.Layer): def __init__(self, return_weight=False, **kwargs): super(AttentionLayer, self).__init__(**kwargs) self.return_weight = return_weight def build(self, input_shape): self.W = self.add_weight(name='att_weight', shape=(input_shape[-1], input_shape[-1]), initializer='glorot_uniform', trainable=True) self.b = self.add_weight(name='att_bias', shape=(input_shape[-1],), initializer='zeros', trainable=True) self.v = self.add_weight(name='att_vec', shape=(input_shape[-1],), initializer='glorot_uniform', trainable=True) super(AttentionLayer, self).build(input_shape) def call(self, inputs): # inputs shape: (batch, timesteps, features) score = tf.nn.tanh(tf.tensordot(inputs, self.W, axes=[2, 0]) + self.b) score = tf.tensordot(score, self.v, axes=[2, 0]) # (batch, timesteps) att_weight = tf.nn.softmax(score, axis=-1) context = tf.reduce_sum(inputs * tf.expand_dims(att_weight, -1), axis=1) if self.return_weight: return context, att_weight # 训练时返回context,诊断时取权重 return context def build_model(window_len, K): inp = layers.Input(shape=(window_len, K)) # CNN:一维卷积提取局部时段特征 x = layers.Conv1D(filters=64, kernel_size=4, padding='same', activation='relu')(inp) x = layers.BatchNormalization()(x) x = layers.Dropout(0.2)(x) # BiLSTM:双向时间依赖建模 x = layers.Bidirectional(layers.LSTM(units=64, return_sequences=True))(x) x = layers.Dropout(0.2)(x) # Attention:对BiLSTM输出的时间步做加权池化 x = AttentionLayer()(x) # 输出层 x = layers.Dense(32, activation='relu')(x) out = layers.Dense(1)(x) model = Model(inputs=inp, outputs=out) model.compile(optimizer='adam', loss='mse', metrics=['mae']) return model model = build_model(window_len, K) model.summary()kernel_size=4配合padding='same',在15分钟粒度下等价于“只看最近一小时内的局部形态”,不会把感受野撑太大。filters=64给了足够多的局部模板数量,数据量只有几千样本时可以考虑减半到32。BiLSTM的units=64和filters保持一致,Attention层的W、b、v参数是模型的一部分,梯度会自动更新,不需要手动设置学习率或正则化——你要做的只是确认输入输出形状对得上。整个模型参数量在几万到十几万之间,CPU能跑,只是慢一些。
3.4 训练策略与反归一化评估
compile用Mean Squared Error,因为负荷预测本质上是回归,MSE对陡峭峰值的惩罚较大,好处是逼着模型拟合峰值,坏处是均方误差会让模型为了降低大误差而牺牲小负荷段的精度。如果更关注平均误差,可以改用MAE或Huber loss。我一般先用MSE训练,再用MAE做评估,两个指标都看。
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) lr_reduce = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-5) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=64, callbacks=[early_stop, lr_reduce], verbose=1 ) # 预测并反归一化 y_pred_scaled = model.predict(X_val).flatten() y_true_scaled = y_val.flatten() y_pred = scaler.inverse_transform(y_pred_scaled.reshape(-1, 1)).flatten() y_true = scaler.inverse_transform(y_true_scaled.reshape(-1, 1)).flatten() from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(y_true, y_pred) rmse = np.sqrt(mean_squared_error(y_true, y_pred)) print(f"MAE: {mae:.2f} MW, RMSE: {rmse:.2f} MW")EarlyStopping的patience=10是经验值:负荷数据的训练loss通常在前30个epoch收敛得差不多,patience太小容易在验证集震荡时错过最优权重,太大又浪费时间。ReduceLROnPlateau每5个epoch没有进展就把学习率降一半,最低到1e-5。反归一化时注意,scaler是在训练集上fit的,inverse_transform出来的是真实MW值。模型输入用了VMD后的模态,但模态和原始值同在0到1区间,所以不需要对模态单独做反归一化,最终预测对准原始负荷值即可。
4. 参数怎么调:从VMD的K值到模型超参数的调优路径
4.1 K值:模态个数的三重判断
K是影响模型上限的第一个关键参数。K设小了,比如2,VMD把负荷拆成低频趋势和高频波动,高频部分仍含多个频段叠加,后续模型要处理的是个“准平稳”序列,压力转嫁给CNN和BiLSTM;K设大了,比如10,相邻模态中心频率接近,信息被重复拆分,训练时间翻倍收益接近零,部分模态在某个时段能量趋于零,成为稀疏模态,神经网络在稀疏输入上容易学偏。
我的判断分三步。第一步看中心频率,打印omega[-1],相邻两个中心频率的比值小于1.2说明靠得太近。第二步看重构误差,用u.sum(axis=0)和原始signal算MSE,如果重构误差大于1e-6量级,分解本身就不准确。第三步做消融实验,固定其它参数,把K从3到7各跑一遍,选验证集MAE最低的。实验室里K=5在多数日负荷曲线上表现稳定,可以当起点,但不同季节的最优K可能不同,换季时要重新标定一次。
4.2 alpha惩罚因子:带宽的松紧程度
alpha控制模态带宽的约束强度,默认2000在大多场景可用,但不代表不用调。alpha偏大时,每个模态带宽收窄,频段分得更细碎,模态数量一定时覆盖的频段范围变小,高频细节重构时损失变大;alpha偏小时,带宽变宽,相邻模态重叠加剧,分解的去相关效果变差。负荷数据里既有关键的日周期(24小时),也有小时级的快速波动,alpha在1000到3000区间做网格搜索就好,它对精度的影响通常小于K值,不用花太多时间去抠。
4.3 CNN的kernel_size与filters:感受野的宽度和模板的容量
kernel_size决定CNN看多长的局部时段。15分钟采样间隔下,kernel_size=4看到1小时窗口,kernel_size=8看到2小时窗口。负荷的爬坡事件通常在1到2小时内完成,所以推荐起点4,想捕获早晚高峰的长坡面轮廓用8也可以。kernel_size加大会增加同层参数量,但参数共享机制让增加变得有限;真正影响参数量的是filters,从32加到64参数量翻倍,训练集够大时精度提升,数据量只有几千样本时64几乎必过拟合,32搭配Dropout更稳。
4.4 BiLSTM的units与层数:不是越深越好
BiLSTM的units决定隐状态容量。64在负荷预测里算“甜点”值,hidden units太低比如16会丢周期特征,太高比如256在几千样本下就是过拟合温床。堆两层BiLSTM在NLP里常见,但负荷预测这种单变量回归任务里,第二层带来的收益经常是个位数百分比,训练时间却翻倍,不划算。我的建议是先跑单层,如果训练损失和验证损失都高,说明欠拟合,再加units或加层;如果训练损失低验证损失高,优先加Dropout而不是加深网络。
4.5 滑窗长度、batch size与学习率的配合
window_len=96表示用过去一天预测未来,这是负荷预测最经典的输入长度,因为日周期性是负荷序列最强的规律之一。想捕捉周周期可以尝试window_len=672(一周),但序列越长BiLSTM的遗忘门压力越大,Attention的加权恰好是对这个问题的缓解。batch size方面,64是内存和收敛速度的折中,样本量2万以下不要用256,大batch收敛变慢,验证集指标波动更大。学习率用Adam默认的1e-3即可,配合ReduceLROnPlateau让它后期自动降低,比预设schedule更省心——负荷数据在不同时间段特征分布会有漂移,自动降学习率能跟着数据走。
下表汇总我常用的起点参数,可以作为第一轮实验的基准:
| 参数 | 起点值 | 调整范围 | 调整依据 |
|---|---|---|---|
| K(模态数) | 5 | 3~7 | 中心频率比值,重构误差 |
| alpha | 2000 | 1000~3000 | 带宽在日周期与小时波动之间平衡 |
| kernel_size | 4 | 4~8 | 1小时~2小时局部窗口 |
| filters | 64 | 32~64 | 数据量小减半,防过拟合 |
| LSTM units | 64 | 32~128 | 优先单层,欠拟合再加 |
| window_len | 96 | 96~672 | 匹配日周期/周周期 |
| batch_size | 64 | 32~128 | 样本量大可增大 |
| Dropout | 0.2 | 0.1~0.4 | 验证集震荡时增大 |
调参这件事到后期多少有点玄学成分,但有了这个基准表,第一轮实验的起点是明确且可复现的,后续每次只改一个变量,记录验证集MAE,比同时改三四个参数然后对着曲线猜原因高效得多。
5. 避坑指南:VMD-CNN-BiLSTM-Attention落地时最常踩的五个坑
5.1 数据泄露:整段归一化和整段VMD让测试集“看”了未来
现象:验证集MAE非常漂亮,比论文里还低,但上线后跑真实新数据,误差直接翻倍。
原因:在划分训练测试集之前就对全量数据做归一化,测试集的均值和极值参与了scaler拟合;或对包含测试段的整段序列做VMD分解,测试段的分量在分解时受未来数据影响。
解决:归一化只在训练集上fit,测试集只做transform。VMD如果按整段分解,只适用于离线研究和对比实验;部署滚动预测时,对每个新样本取过去足够长的窗口做VMD,再让模型预测下一步,这是符合真实部署节奏的做法。
5.2 VMD出现两个中心频率几乎相同的模态
现象:分解结果里有两个模态曲线形状几乎一模一样,重构误差正常,但模型训练变慢,验证集精度不升反降。
原因:K设大了,相邻模态的中心频率在迭代后收敛到同一位置,出现模态重复,业内俗称“双胞胎模态”。
解决:降低K,一般从当前值减1或2,再观察omega判断。如果两个中心频率差小于其中一个的20%,就确认是冗余模态。另一种办法是把alpha调大,让带宽收窄,有时能让两个模态拉开距离,但根治还是要减K。
5.3 模型预测结果被“拉到平均值”附近
现象:验证集MAE中等,画出预测曲线和真实曲线,高峰和低谷都对应不上相位,预测值像被拉平一样,峰谷被低估。
原因:MSE损失下,模型倾向于把预测值收敛到条件均值以避免大的平方误差,峰谷这类极端值天然被平滑。另一种可能是滑窗长度太短,模型还没看到昨天同一时段的信息。
解决:损失函数换Huber或MAE试试;把window_len扩展到96甚至更大;如果还不行,对预测值做后处理,用真实历史同期值的残差修正当天的峰谷误差。这不算作弊,因为负荷预测实际业务里,残差修正是普遍使用的兜底手段。
5.4 训练集和验证集MAE差异很大,验证集不降反升
现象:前几个epoch训练集loss快速下降,验证集loss掉到某个点后一路反弹,没有回头的意思。
原因:过拟合。CNN层或BiLSTM层参数量在数据量面前太大了;也可能是Dropout放的位置不对,只在BiLSTM后加一层,Conv1d的输出直接暴露给后续全连接,隔离不够。
解决:先检查样本量,如果只有几千个,把filters和units都砍一半。Dropout建议在Conv1d后和BiLSTM后都加,rate从0.2起步。EarlyStopping的patience可以降到7,避免验证集反弹几轮后才止损,错过最优权重。
5.5 多模态分别预测再叠加时出现“平移误差”
现象:每个IMF分别训练模型,叠加后MAE不错,但预测曲线整体比真实值滞后一两个采样点。
原因:每个模态模型独立预测时,相位信息没有被统一对齐。低频趋势模态预测得准,高频模态的相位预测误差却在叠加时被放大。
解决:优先用多模态联合输入的单模型方案——K个模态作为K个通道同时输入CNN-BiLSTM-Attention,让Attention层统一对齐模态间的相位信息。如果一定要分模型预测,高频模态的损失函数单独用MAE,并适当降低它在总量中的权重,减少滞后效应。
6. 进阶技巧:用注意力热力图验证模型学到了什么
很多人用Attention只当它是精度工具,其实它也是诊断工具。把Attention层输出的权重取出来,画成热力图,能直观看到模型在预测某个时刻时,把注意力放在了历史的哪些位置。这是我每次搭新数据集时的必做动作:先不看MAE,而是看热力图是否符合负荷预测的常识。
具体做法是在AttentionLayer里加一个return_weight开关,训练时设False,诊断时设True,用同样结构重新实例化模型,再加载训练好的权重。下面的代码演示了诊断流程。
# 构建诊断用模型,输出预测值和注意力权重两个结果 att_model = build_model(window_len, K) # 实际需要在build_model里传入return_weight=True att_model.load_weights('best_model.weights.h5') # 改一下build_model,把AttentionLayer(return_weight=True)传入 # att_model的输出变成 [预测值, 注意力权重] y_pred, (context, att_w) = att_model.predict(X_val[:8]) att_w = att_w.reshape(8, window_len) # 每个样本96个时间步的权重把热力图和真实负荷曲线叠在一起看,两个现象值得关注。第一,模型大概率会在预测早高峰时把高权重放在昨天同一时刻附近,这是日周期规律的机制化体现,说明打分的注意力机制学习到了负荷序列的周期性;第二,如果热力图几乎均匀分布,没有明显聚焦位置,说明Attention没学到有用的对齐关系,模型退化成了BiLSTM取平均池化,精度天花板会低一截。
诊断出第二种问题时,调整方向不是调学习率,而是把window_len调成日周期的整倍数,让模型有机会看到完整的周期起点;或者把注意力打分函数从点积改成加性,提高打分区分度。如果热力图稀疏聚焦正常,说明机制对了,剩下的调参才有意义。
还有两个提升稳定性的习惯。第一,每次训练固定随机种子,同一个测试集上多次运行的MAE在±2%范围内浮动是正常的,单次结果不能代表模型真实水平,取3次的均值再报数。第二,保存验证集表现最好的checkpoint,用restore_best_weights=True或手动load,这比反复调epoch数更务实。
踩过的坑多了以后,我现在的固定流程是:先跑一轮带早停的baseline,再花十分钟看注意力热力图,最后才开始调参。顺序不要反——先确认机制学对了,再谈精度提升,否则参数调得再细也经不起换季的数据分布漂移。希望这篇笔记能帮你在自己的负荷数据上把VMD-CNN-BiLSTM-Attention跑通,并且跑得踏实。
本文还有配套的精品资源,点击获取