
简介基于CNN-LSTM混合神经网络模型的短期负荷预测方法是一份PDF格式的学术论文资源面向电力系统调度人员、负荷预测研究者及深度学习算法工程师。论文针对负荷数据时序性强、非线性明显的特点提出融合卷积神经网络与长短时记忆网络的混合模型并详细说明将历史负荷、气象、日期和峰谷电价数据构造为连续特征图、经CNN提取特征后交由LSTM完成预测的完整流程。资源共1个文件即1个PDF文档压缩包大小1.22MB内容涵盖问题建模、网络结构设计、实验对比及结论分析。目前已有3998人学习或下载该资源。读者可借助其中的公式推导、实验数据和参数设置复现模型也可参考论文中关于江苏省某地区算例的对比结果为短期负荷预测算法选型与工程化落地提供有价值的参考。1. 短期负荷预测为什么绕不开 CNN-LSTM先看数据长什么样做电力系统调度的人都知道短期负荷预测通常指未来 24 小时到 7 天的小时级或 15 分钟级负荷是整个运行计划的地基。预测偏了机组组合、经济调度、需求响应全得跟着改偏差超过 5% 就可能产生真金白银的考核费用。传统方法里ARIMA 处理线性趋势还行但遇到节假日、极端天气、电价波动这些强非线性事件时残差往往大得让人头疼。CNN-LSTM 混合模型这几年能成为热门方案核心原因只有一个它把特征提取和时间依赖建模分开了正好对应负荷数据里两类信息——局部模式比如一天内早晚高峰的形态和长期演化比如一周内的周期性。我在实际项目里最常用的一版结构是前面几层一维卷积网络负责滑动提取相邻时段负荷与温度、湿度、日照的局部特征后面接两层 LSTM 负责把提取到的特征序列按时间顺序建模。这样做比纯 LSTM 收敛快比纯 CNN 记得住长时间的依赖。适合的人群很明确手里有至少一年历史负荷数据和对应气象数据想从传统统计模型迁移到深度学习的工程师。如果连数据质量都没理清先别急着上模型。2. 先把混合模型拆开CNN 在提取什么LSTM 在记忆什么2.1 为什么纯 LSTM 不够偏要前面加 CNN纯 LSTM 的输入通常是原始负荷序列每个时刻点独立进入网络。问题是负荷曲线里有很多短时模式是跨多个时刻的比如下午 14:00 到 17:00 的持续攀升、晚高峰 19:00 前后的快速跳变。这些模式如果靠 LSTM 自己去学它也能学但需要大量数据和更深的隐层训练时间拉长而且容易过拟合。CNN 的一维卷积核天然擅长捕捉这种局部形状。比如一个 kernel_size6 的卷积核扫过 15 分钟粒度的数据就能覆盖一个半小时的形态。多个卷积核并行相当于同时看多种尺度的局部模式——有的关注斜率有的关注峰值。另一个实际原因是特征对齐。原始负荷序列里往往还夹杂着气象特征、日期类型节假日/工作日特征。这些特征尺度不一致直接拼进 LSTM 会造成训练不稳定。先用 CNN 对每个特征各自做卷积提取再在通道维度上拼接比让 LSTM 处理未经对齐的多源输入要稳定得多。我一般在项目里把数据组织成 shape(样本数, 时间步长, 特征数)然后让 Conv1D 在时间步方向上滑动每个卷积核输出一个通道这样特征天然做了局部加权相当于给 LSTM 喂的是“特征摘要”而不是“原始读数”。2.2 LSTM 层到底该放几层隐单元数怎么定常见做法是放一到两层 LSTM。一层 LSTM 适合数据量不大、周期相对稳定的场景两层 LSTM 能捕捉更高阶的时间依赖但训练难度和过拟合风险同步上升。我在一个省级电网数据集上对比过两层 LSTM 比一层在测试集 MAPE 上只降了不到 0.3%但训练时间多了将近一倍。如果你的数据只有一年强烈建议先试一层把隐单元数设在 32 到 64 之间然后观察验证集 loss。如果欠拟合再加一层如果过拟合先加 Dropout而不是无脑堆层。隐单元数的选择有个经验公式输入维度加输出维度的两倍左右可以作为一个起点。比如输入序列长度是 9615 分钟粒度一天输出是未来 24 个点那么 LSTM 隐单元设在 64 左右比较合适。设太大容易让 LSTM 陷入“记住每个样本”的困境导致泛化能力差。另外 LSTM 层要接一个全连接输出层这个全连接的神经元数量等于预测步数。如果预测未来 24 小时、每小时一个点输出层就是 24 个神经元激活函数用 linear因为负荷预测是回归任务不是分类。2.3 CNN-LSTM 的输入形状一个最常见的翻车点很多人第一次跑通模型后预测结果一团糟回头检查发现是输入张量形状错了。Conv1D 要求输入是三维 (batch_size, time_steps, features)。假设我们取过去 96 个时间点15 分钟粒度一天每个时间点有 5 个特征负荷、温度、湿度、节假日标记、星期几那么单个样本形状是 (96, 5)。喂给 Conv1D 时它会沿着 time_steps 维度做卷积每个卷积核在 features 维度上做全连接加权。这里注意features 维度的顺序会影响卷积结果因为卷积核跨特征做线性组合。我一般把负荷放在第一个特征列然后依次是温度、湿度、节假日编码、星期编码。如果你把形状搞成 (batch_size, features, time_steps)Conv1D 默认会对第一个非 batch 维度做卷积结果就是每个特征各自独立做时序卷积特征之间的关联被切断了。这个错误非常隐蔽因为训练 loss 看起来也在下降但收敛速度明显变慢最终精度也差一截。建议在数据预处理时打印一下x_train.shape确认是 (样本数, 时间步, 特征数) 再往下走。3. 数据准备与滑动窗口构建预测未来 24 小时需要哪些特征窗口怎么切3.1 构建监督学习样本用过去 N 步预测未来 M 步短期负荷预测本质上是监督学习用过去一段时间序列预测未来一段序列。最常见做法是滑动窗口法。假设历史数据是 15 分钟粒度一天 96 个点我们想用过去一天预测未来一天那么滑动窗口长度 N96预测长度 M96。每滑动一步15 分钟生成一个样本。但实际项目里我们会让窗口重叠比如每次滑动 2 个点来减少样本数量避免相邻样本高度相关导致过拟合。滑动步长是 1 会产生海量样本训练很慢而且相邻样本几乎一样信息冗余很大。我一般设置step 4这样样本间重叠度适中训练速度和精度都能接受。下面是数据切分的核心代码用 Python 实现import numpy as np def make_samples(data, feature_cols, target_col, input_steps96, output_steps96, step4): 构建滑动窗口样本 data: 二维数组shape(总时间点数, 特征数) feature_cols: 选作输入的特征列索引 target_col: 要预测的目标列索引通常是负荷 input_steps: 输入历史窗口长度 output_steps: 预测未来长度 step: 窗口滑动步长 X, y [], [] n data.shape[0] # 窗口起点从0开始保证输入窗口和输出窗口都在数据范围内 for start in range(0, n - input_steps - output_steps 1, step): end_input start input_steps end_output end_input output_steps X.append(data[start:end_input, feature_cols]) y.append(data[end_input:end_output, target_col]) return np.array(X), np.array(y) # 假设 df 已经按时间排序特征列为 [load, temp, humidity, holiday, weekday] # 特征列索引是 [0,1,2,3,4]目标列索引是 0load X, y make_samples(df.values, feature_cols[0,1,2,3,4], target_col0, input_steps96, output_steps96, step4) print(f样本数: {X.shape[0]}, 输入形状: {X.shape}, 输出形状: {y.shape})逻辑说明make_samples函数的核心是确定每个样本的输入区间和输出区间。start是输入窗口起点end_input是输入窗口终点end_output是输出窗口终点。为了保证输出窗口不越界start最大只能到n - input_steps - output_steps。step4表示每 15 分钟数据取一个样本即一小时 4 个样本。如果你想一天预测一点可以把output_steps设为 1但多步预测往往更实用因为调度需要一条曲线。参数说明input_steps的选择直接影响模型对长期依赖的捕捉能力。96 代表一天192 代表两天。如果你的数据有明显的一周周期性比如工作日和周末负荷差异大那输入窗口至少 7 天即 672 个点15 分钟粒度。但窗口越长LSTM 需要记忆的信息越多训练越难。一个折中方案是输入窗口取 3 天288 点同时在特征里加入星期几编码让 LSTM 自己学出周末规律。我试过 7 天窗口和 3 天窗口加星期特征后者在 MAPE 上反而低一点说明显式特征比一味加长输入窗口更有效。3.2 特征工程的三个必选项温度、星期类型与节假日负荷预测不是只看历史负荷就够的。温度是所有气象特征里影响最大的一个尤其夏季空调负荷和冬季电采暖负荷对温度变化极其敏感。但温度对负荷的影响往往有滞后比如下午 14:00 的气温决定了晚上 20:00 的空调开启程度。所以我在特征里既放当前时刻温度也放未来 24 小时的预测温度如果能拿到气象预报数据则更好否则用历史温度做滞后特征。滞后可以取 2 到 4 小时具体数值要根据当地负荷响应速度来试。你可以在特征里同时加入当前温度、滞后 2 小时温度、滞后 4 小时温度让模型自己选权重。星期类型和节假日特征不能只用一个“是否是节假日”的布尔值。中国的调休制度让很多假期前后的周末变成工作日这会造成严重预测偏差。我的做法是构造三个特征星期几的 one-hot 编码7 维、是否节假日1 维、是否调休工作日1 维。其中调休工作日是指本来周末但因为调休需要上班的日子这个信息可以从政府发布的节假日安排中手工维护一张表。虽然维护起来麻烦但效果立竿见影。没有这张表时模型会把调休工作日预测成周末负荷误差可能拉到 10% 以上。温度和日期特征做好后还需要做归一化。负荷数据量级可能是几百到几千 MW温度是几十度直接喂给网络会让梯度被大数值特征主导。常见做法是每个特征单独做 MinMaxScaler压缩到 [0,1] 区间。注意归一化要先用训练集拟合 scaler再用同一套参数变换验证集和测试集千万不能在整个数据集上做归一化否则会有信息泄露测试集的表现会被高估。代码上用 sklearn 的 MinMaxScaler 分别 fit 训练集再 transform 所有集合即可。3.3 数据切分训练集、验证集、测试集怎么分才不骗自己时间序列数据不能随机打乱划分否则模型会“偷看”未来的信息测试集上的精度会虚高。正确做法是按时序切分比如前 70% 做训练中间 15% 做验证用于调参和早停最后 15% 做测试。验证集和测试集的边界要用日历上的自然断点尽量避开跨越长假或极端天气事件的交界处。比如训练集到 3 月验证集是 4 月测试集是 5 月这样每个月有完整的周一至周日循环季节性评估才可靠。切分完还要注意一个细节如果数据里包含缺失值先用线性插值或前后向填充补好后再做滑窗。CNN-LSTM 对缺失值不鲁棒个别 NaN 会一路传播到损失计算导致 loss 直接变成 NaN。我在一个项目里吃过亏某天数据采集系统故障缺了 30 个点没补训练到第 20 轮 loss 突然变 NaN排查半天才发现。所以补缺失值的代码要放在构建样本之前并且补完要检查还有没有 NaNdef fill_missing(df, methodlinear): # 只对数值列插值最好是按时间排序后调用 df df.interpolate(methodmethod, limit_directionboth) # 仍可能残余 NaN前向填充兜底 df df.fillna(methodffill).fillna(methodbfill) return df assert not df.isnull().any().any(), 数据中仍有缺失值4. 模型搭建与训练配置用 Keras 搭一个可复现的 CNN-LSTM 基准模型4.1 最小可跑通的模型结构定义直接上代码用 TensorFlow/Keras 搭一个双层结构Conv1D MaxPooling LSTM Dense。这套结构足够作为基准跑通后再按需加深或加分支。import tensorflow as tf from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, Conv1D, MaxPooling1D, LSTM, Dense, Dropout def build_cnn_lstm(input_shape, output_steps): input_shape: (time_steps, n_features) output_steps: 预测未来多少个点 inputs Input(shapeinput_shape) # 第一层一维卷积64个卷积核每个核长度6跨特征做线性组合 x Conv1D(filters64, kernel_size6, paddingsame, activationrelu)(inputs) # 池化压缩时间维度一半减少LSTM输入长度加快训练 x MaxPooling1D(pool_size2)(x) # 第二层一维卷积提取更抽象的局部特征 x Conv1D(filters32, kernel_size3, paddingsame, activationrelu)(x) # 展平时间维度不行LSTM需要保留时间步。这里直接返回序列 x LSTM(units64, return_sequencesFalse, dropout0.2, recurrent_dropout0.1)(x) # 输出层线性激活输出未来 output_steps 个负荷值 outputs Dense(output_steps, activationlinear)(x) model Model(inputs, outputs) model.compile(optimizeradam, lossmse, metrics[mae]) return model model build_cnn_lstm(input_shape(96, 5), output_steps96) model.summary()逻辑说明Conv1D的第一个参数filters代表卷积核数量每个卷积核生成一个通道。这里第一层用 64 个核第二层 32 个核逐层把特征维度加深、时间维度不变paddingsame保证输出长度和输入一致。MaxPooling1D(pool_size2)把时间步长度减半从 96 变成 48。这样 LSTM 只需处理 48 个时间步训练速度提升明显而且池化还能抑制过拟合因为它是一种局部降采样让网络不那么在意精确位置。LSTM 的dropout0.2是对输入到 LSTM 的激活做随机丢弃recurrent_dropout0.1是对循环状态内部的隐状态做丢弃。两个 dropout 共同防止过拟合但别设太大否则会欠拟合。参数说明kernel_size的选择和输入时间粒度相关。15 分钟粒度下kernel_size6覆盖 1.5 小时小时粒度下kernel_size6覆盖 6 小时。如果数据粒度更细比如 5 分钟一个点那 kernel_size 应该相应加大到 12 甚至 24。这个参数的物理含义是“模型一次关注多长的局部趋势”。可以先设为 6再看验证集指标调整。filters数量不是越多越好64 到 128 是常见区间超过 128 在小数据集上过拟合很严重。LSTM 的units和output_steps的关系不大但有一个底线units 至少要能容纳输出步数的信息量。输出 96 个点时units 设 32 也能跑但 64 更稳妥。4.2 训练回调早停、学习率衰减和模型保存的配合训练 CNN-LSTM 最怕两件事过拟合和训练发散。过拟合的典型信号是训练 loss 持续下降但验证 loss 先降后升。解决方法是早停EarlyStopping和模型保存。学习率衰减则能让模型在训练后期更精细地逼近最优解。推荐在 Keras 里配置三个回调from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks [ EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue, verbose1), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6, verbose1), ModelCheckpoint(best_model.h5, monitorval_loss, save_best_onlyTrue, verbose1) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size64, callbackscallbacks, verbose1 )逻辑说明EarlyStopping的patience15表示验证集 loss 连续 15 个 epoch 没有改善就停止训练并恢复到验证集最佳权重。ReduceLROnPlateau的patience5表示连续 5 个 epoch 验证集 loss 停滞学习率就减半。这样既能让模型在前期大步快跑又能在后期小步精调。ModelCheckpoint会在每个 epoch 结束时比较验证集 loss只保存最佳模型到本地文件。这样即使训练中途崩溃或后期过拟合也能用最早的最佳模型。参数说明batch_size64是一次喂给模型多少个样本。如果样本数很大滑动窗口法通常会生成几万到几十万样本batch_size 可以设大些比如 128 或 256加速训练。但 batch_size 太大容易导致收敛不稳定因为梯度平均掉了很多个体特征。我在小数据集上喜欢用 32大数据集用 128。epochs200只是一个上限实际训练会由早停决定何时停。训练时如果发现 loss 是 NaN优先检查数据里是否有 NaN 或无穷值其次看学习率是否过大。默认 Adam 学习率 0.001 在大多数情况下没问题。4.3 训练过程中的实盘监控指标不能只看 loss很多人训练完只看 loss 很低就以为成功了这是典型的黑匣子思维。回归任务的 loss 是均方误差MSE量纲是负荷值的平方。如果一个数据集的负荷均值是 500 MWMSE100 意味着平均误差约 10 MW但不同时段的误差分布差异巨大。我建议训练过程中实时监控一个更有业务含义的指标MAPE平均绝对百分比误差。Keras 没有内置 MAPE可以用自定义指标或者在回调外部计算。最简单的方法是在训练时同时用metrics[mae]训练完再做预测用 sklearn 的mean_absolute_percentage_error算最终指标。更实用的监控方式是画图训练完成后把预测曲线和真实曲线叠在一起看时段性偏差。比如白天预测好、晚上偏差大那可能是夜间温度特征没用好工作日准、周末差那是日期特征不够。弄一个简单的可视化代码import matplotlib.pyplot as plt def plot_forecast(y_true, y_pred, start_idx0, length96): plt.figure(figsize(12, 4)) plt.plot(y_true[start_idx:start_idxlength], labelTrue, linewidth2) plt.plot(y_pred[start_idx:start_idxlength], labelPred, linewidth2, linestyle--) plt.legend() plt.title(Load Forecast Comparison) plt.ylabel(Load (MW)) plt.show()训练结束后把归一化前的真实负荷和预测负荷记得 inverse_transform画出来肉眼看曲线是否贴合峰值和拐点。如果预测曲线比真实曲线平滑很多通常是 LSTM 单元数太多导致过度平均如果峰值出现时间错位可能是输入窗口不够长LSTM 没有学到完整的日前模式。5. 训练踩坑与参数调优5 个让我翻过车的真实问题5.1 归一化时信息泄露验证集精度虚高到不敢信现象模型在验证集上 MAPE 只有 1.8%一上测试集就变成 6%相差巨大。原因我在构建样本前对整个数据集做了 MinMaxScaler也就是归一化时统计了包含测试集在内的全局最小值和最大值。这相当于把测试集的分布信息提前透露给了模型验证时自然表现好但实际部署时新数据的最大值可能超出训练时见过的范围预测就飘了。解决严格只对训练集 fit scaler再用同样的 scaler transform 验证集和测试集。代码上注意把scaler.fit(X_train)和X_train scaler.transform(X_train)分开写不要用fit_transform一步到位因为那会在整个数据上重新计算。正确顺序是先切分数据再归一化顺序千万不能反。5.2 节假日预测系统性偏低全是调休惹的祸现象清明节、国庆节前后几天预测负荷比实际负荷低很多误差高达 12%。原因模型只用了“是否节假日”这个布尔特征但中国的调休制度导致节假日前后某些周末要上班这些日子的负荷特征是工作日形态模型却按周末去预测。另外法定节假日期间本身也有工作日在假期中的情况比如五一假期前的周日上班。解决花了半天维护一张调休映射表生成一个“是否调休上班日”特征并把它作为输入。同时把节假日特征改成连续值而不是布尔值。比如除夕到初七权重是 0.2 到 0.8 不等表示负荷下降程度具体数值可以参考历史同期负荷与正常工作日负荷的比值。设置权重后模型能学到不同节假日的不同影响。5.3 LSTM 训练到一半 loss 变成 NaN检查了一个下午现象训练第 14 轮loss 突然变成 NaN前几轮一切正常。原因数据里有几个无穷大值通常来自传感器故障或人工录入错误。比如某个时刻温度被录成 999归一化后变成接近 1 的大数卷积层和 LSTM 的梯度计算遇到异常大值直接溢出。解决训练前强制做一次数据清洗。用np.isfinite()检查所有输入输出数组把非有限值替换为列中位数或插值结果。同时把异常值检测加入预处理流程常见做法是 3σ 原则超过均值加减 3 倍标准差的点视为异常用前后均值替换。注意替换后仍然要再检查一次 NaN。5.4 卷积核数量翻倍测试集误差反而升高了现象把第一层卷积核从 64 加到 128训练 loss 降得更快但测试集 MAPE 从 3.2% 涨到 4.1%。原因卷积核太多导致模型容量过剩而训练数据只有一年模型把训练集中的局部噪声也当成了模式学进去。尤其在负荷曲线本身波动大的地区比如有大量分布式光伏高频波动被卷积核忠实记录反而干扰了 LSTM 对长期趋势的建模。解决卷积核数量不是越大越好。把第一层固定在 32 到 64 之间第二层减半。如果还过拟合增大 dropout 或用MaxPooling1D的比例从 2 改成 4强制模型更关注粗粒度趋势。另一个有效做法是给 CNN 部分加kernel_regularizerl2让卷积核权重不要太大抑制对噪声的敏感。5.5 预测曲线整体滞后一小时像平移了一样现象预测曲线和真实曲线形状几乎一样但整体向右平移了一个采样点导致峰值位置错位MAPE 很大。原因输入窗口和输出窗口之间的对齐偏差。滑动窗口生成样本时如果输入窗口的末尾和输出窗口的起点重叠了一个点比如end_input start input_steps但end_output end_input output_steps而数据索引是从 0 开始的可能多算了一个点LSTM 实际上在预测已知的当前值加未来值造成“延迟预测”。解决检查样本构建逻辑确保输入窗口的最后一位是第input_steps个点输出窗口从第input_steps1个点开始。最容易的验证方法打印一组样本看X[0, -1, :]和y[0, 0]的时间间隔是否为 15 分钟。如果是 0 分钟说明窗口重叠了修正end_input start input_steps为end_input start input_steps - 1即可具体遵循环的索引语义。6. 部署预测的落地技巧把模型接到真实调度流程里还要留好更新机制模型训练完不是终点。我一般会做三件事一是把预测结果写成标准格式的 CSV 文件方便调度系统直接导入二是对模型做定期重训练因为负荷模式会随季节、用电结构变化三是加一个基于残差的简单修正层用来处理模型在特殊时段的系统性偏移。这个修正层是个很实用的技巧。训练完成后的测试集残差序列按小时统计平均偏差如果发现某几个小时的预测长期偏高或者偏低比如早高峰 8:00 总是偏低 3%就生成一张小时级修正表。预测时把模型输出加上对应小时的偏差修正。这个修正表可以每周更新一次用最近一个月的预测残差重新计算。这种经验修正虽然不“智能”但非常有效尤其是对于调度中有硬性考核的场景。另一个技巧是对 LSTM 的return_sequences做变体。如果预测未来 96 个点而你想让模型在每个时刻都输出该时刻的未来值可以使用return_sequencesTrue并接一个 TimeDistributed 层。但这会让模型输出维度变成(batch_size, time_steps, output_steps)显存占用和计算量大幅上升。我实际用下来单步输出即return_sequencesFalse已经足够因为负荷预测通常只关心一条曲线不关心中间过程的概率分布。最后聊聊模型更新频率。我见过很多团队训练一次用一年结果夏季空调负荷爆发时预测完全不可用。正确做法是每周定时重新训练一次用最近 4 周的滑动窗口数据。训练时可以沿用已有的最佳权重作为初始化而不是从随机权重开始这样微调几个 epoch 就能收敛时间成本很低。代码上就一行model.load_weights(best_model.h5)然后继续model.fit但要注意重新生成滑动窗口样本时必须使用新的数据并且重新归一化用滚动窗口内的均值和方差。我做负荷预测这些年最深的一个教训是模型结构只是整个系统里的一小环数据质量和特征工程决定了误差的下限而模型调优只是在逼近这个下限。遇到效果差时先别急着换网络结构回去查归一化、查特征、查窗口重叠这三个点解决 80% 的问题。希望这段踩坑和调参经验能帮你在自己的负荷预测项目里少走弯路把精力花在真正有回报的地方。本文还有配套的精品资源点击获取