YAOTU INSIGHTS

LSTM多特征电力负荷预测实战:从数据预处理到模型评估

LSTM多特征电力负荷预测实战:从数据预处理到模型评估
简介这是一份基于深度学习LSTM的多特征电力负荷预测程序源码包面向计算机科学、人工智能、数据科学、物联网等方向的课程设计、毕业设计与项目入门。程序围绕城市居民电能负荷预测展开一方面利用历史负荷、温度、湿度、风速等特征预测当前负荷完成粗度预测另一方面考虑季节、日/周/月周期等影响因子完成细度预测并通过V1至V5版本对比不同输入组合和预测方式。资源共8个文件以Python源码、Markdown说明文档、CSV数据文件及压缩数据集为主整体仅830KB便于快速下载与二次开发。已有409人学习下载适合希望结合真实数据理解LSTM时序建模、特征处理与负荷预测流程的读者。包内附项目说明与原始数据代码在Python3.6、TensorFlow2.0/Keras环境中验证可用可帮助读者直接运行、对照实验并扩展。1. 电力负荷预测的 LSTM 方案这份源码到底解决什么问题做课程设计和入门时序预测的人最常卡住的一步不是模型原理而是“数据怎么喂进去、跑完怎么算数”。这套 Python 基于深度学习 LSTM 的多特征电力负荷预测程序源码把从数据清洗、序列构造、模型训练到指标评估的整条链路都串好了还附带一份能直接用的数据集和项目说明文档。它不是给你一个孤零零的模型文件而是一个能跑通、能改参、能截图写进报告的完整工程。适合两类人一是拿电力负荷预测做毕设或课程设计的在校生需要一个能复现的 baseline二是想搞清楚多特征时序预测到底怎么组织数据的从业者看的是特征拼接、归一化和预测评估这些实操细节。2. 多特征怎么进 LSTM数据预处理与序列构造2.1 为什么单变量不够要上多特征电力负荷曲线的自相关性很强今天下午三点和昨天下午三点的负荷往往非常接近所以哪怕只用历史负荷序列去预测LSTM 也能学出个大概。但这种“大概”在天气突变、节假日、工作日切换的时候会明显失真同样是一个周一下午冬天开暖气和夏天开空调负荷差异可能超过百分之二三十清明和国庆放在一起模型只会觉得它们都是“非工作日”区分不出消费模式差异。多特征预测的核心思路是把负荷之外的影响因子显式地拼到输入里常见的候选特征包括温度、湿度、风速、星期几、是否节假日、尖峰平谷时段标记。这里的多特征不是指多加几列数据就完事而是指每一行时间点上的特征向量要和负荷序列在时间轴上对齐然后一起进滑动窗口。做课程设计时你不需要把所有候选特征都塞进去选两三个对负荷影响最明显的就行源码里的数据集恰好就是这么组织的一般是一张带时间戳的二维表。2.2 先看数据长什么样我解开这份工程后第一件事不是跑train.py而是打开数据集看表头和前几行。这类负荷数据集的常规组织方式是每行一个采样时间点列包含时间戳、负荷值、温度、湿度、星期几、节假日标记等。表格列数越多后面归一化和序列构造需要考虑的维度就越多。列名含义数据类型在模型中的角色datetime时间戳datetime排序与划分依据load电力负荷kW/MWfloat预测目标 ytemp气温℃float输入特征hum相对湿度%float输入特征weekday星期几0-6int输入特征is_holiday是否节假日0/1输入特征时间戳列不会直接喂给 LSTM它只用来排序和切分训练测试集。如果数据集是按整点采样的一天就有 24 个点如果按 15 分钟采样一天就有 96 个点。这个频率直接决定了后面滑动窗口的典型取值我一般习惯用“以周为周期”的经验去设窗口长度整点数据取 24 或 16815 分钟粒度取 96 或 672。2.3 滑动窗口构造序列样本LSTM 不能像吃表格一样一次吞一整行数据它需要看到一段连续的历史才能预测“下一个时刻”。滑动窗口做的事情就是把一个长序列切成(样本数, 窗口长度, 特征数)的三维张量。窗口长度是超参数表示每次用过去多少步来预测未来一步特征数是除目标列以外的输入维度。import numpy as np import pandas as pd def create_sequences(data, feature_cols, target_col, window_size24): data: DataFrame已按时间排序 feature_cols: 参与预测的特征列名列表 target_col: 目标负荷列名 window_size: 滑动窗口长度默认24即用过去24个时刻预测下1个时刻 features data[feature_cols].values target data[target_col].values X, y [], [] for i in range(len(data) - window_size): X.append(features[i: i window_size]) # 取连续window_size行的特征 y.append(target[i window_size]) # 预测窗口结束后的下一个值 return np.array(X), np.array(y)这段代码的逻辑是取features[i: i window_size]对应的标签是target[i window_size]。为什么不是target[i window_size - 1]因为我们要预测的是“看完这 24 个小时的数据后下一个整点的负荷”索引对齐时要留出未来那个点。很多新手翻车的点就在这里窗口取到第 24 行标签却写成了第 23 行等于用过去的数据预测已经知道的结果模型在训练集上表现好得离谱一上测试集就露馅。参数上window_size是这份源码里最值得调的超参数之一。取小了模型看不到日周期性取大了序列太长导致训练变慢、早期梯度被稀释。整点数据我一般从 24 起步如果预测结果在测试集上滞后严重就加到 48 或 168 试试。2.4 归一化的三个隐藏细节时序预测的归一化比分类问题更讲究因为涉及时间顺序和跨集信息。源码里的核心处理方式我拆成三点来看第一归一化器必须只在训练集上拟合。如果先对整个数据集算min和max再切训练测试集测试集的分布信息已经泄漏进归一化参数里测试误差会虚低写进论文里经不起复现。第二特征列和目标列是否可以共用同一个归一化器。源码里常见做法是分开处理目标load单独做MinMaxScaler特征列整体做另一个MinMaxScaler。原因是模型输出层通常预测的是目标列的归一化值评估时要反归一化还原真实负荷如果目标列混在其他特征里一起归一化反算回去时要小心维度错位。from sklearn.preprocessing import MinMaxScaler # 训练集和测试集按时间顺序切分先切分再归一化 train_size int(len(df) * 0.8) train_data df.iloc[:train_size].copy() test_data df.iloc[train_size:].copy() # 分别拟合两个scaler避免特征和目标互相干扰 scaler_feat MinMaxScaler() scaler_target MinMaxScaler() train_feat scaler_feat.fit_transform(train_data[feature_cols]) train_target scaler_target.fit_transform(train_data[[target_col]]) # 测试集用训练集上拟合好的scaler直接transform禁止重新fit test_feat scaler_feat.transform(test_data[feature_cols]) test_target scaler_target.transform(test_data[[target_col]])第三weekday这类整数特征要不要参与归一化。数值超过 1 的整数列如果不归一化会主导梯度更新让连续特征失去意义如果直接归一化等于把周一到周日压到 0 到 6 的区间给它们强行排了序。我一般会把weekday和is_holiday这类分类特征单独编码或者干脆在进入模型前也压到 0-1 区间虽然不完美但在课程设计这个规模下完全够用。提示归一化和反归一化是配对使用的。训练时用scaler_target.fit_transform(Y)预测时用scaler_target.inverse_transform(predictions)中间一旦换了 scaler 或混用不同实例预测结果会整体偏移出来的图是一条“形状对但数值不对”的曲线。3. 模型搭建与训练配置LSTM 到底该怎么堆3.1 输入形状决定一切很多人拿到源码后的第一个困惑是X_train.shape为什么是三维的以及这三维分别是什么。LSTM 的输入形状固定为(样本数, 时间步长, 特征数)顺序不能换。时间步长就是我们前面说的window_size特征数是feature_cols的长度样本数则由len(data) - window_size决定。print(f训练集形状: X_train {X_train.shape}, y_train {y_train.shape}) # 输出示例: X_train (3456, 24, 5), y_train (3456,) # 含义: 3456个样本每个样本看过去24步每步有5个特征预测一个值这里的(None, 24, 5)在模型里会自动传递Input(shape(window_size, n_features))时n_features必须是构造X时实际的特征列数。源码里如果报维度对不上最常见的错法是把目标列也当成特征训了或者在create_sequences里漏了一列导致特征数和Dense层之前的维度不匹配。3.2 网络结构的常见配置与选择理由课程设计形态的负荷预测LSTM 结构不需要太深两层堆叠已经能覆盖绝大多数情况。源码里典型的结构是第一层 LSTM 带return_sequencesTrue输出完整序列给第二层第二层 LSTM 再接一个Dense(1)输出预测值。为什么要return_sequencesTrue因为第一层要把每个时间步的隐藏状态都传给第二层如果设成False第一层只输出最后一个时间步的状态信息量少了一大截。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(units64, return_sequencesTrue, input_shape(window_size, n_features)), Dropout(0.2), LSTM(units32, return_sequencesFalse), Dropout(0.2), Dense(units1) # 输出层无激活函数因为这是回归任务 ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()units的取值不是越大越好。64 到 128 个隐含单元在中等规模数据上是常见区间数据量只有几千条时units上到 256 会明显过拟合表现在训练集 loss 很低、测试集 loss 反而升高。Dropout(0.2)的语义是每次训练迭代随机丢弃 20% 的神经元连接用来削弱过拟合放在两个 LSTM 之间比放在最后输出层前更有效。3.3 训练配置里的几个关键参数batch_size和epochs是源码里最早体现效果的参数。整点采样的负荷数据一天 24 个点batch_size32意味着一个 batch 里混入大约一天半的样本batch_size64会多卡一些训练稳定但更耗内存。epochs别凭感觉设我一般配EarlyStopping让它自己停监控测试集 loss连续 10 个 epoch 不下降就打住省得训练集过拟合了还在傻跑。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue # 结束后回滚到验证集loss最小的权重 ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, # loss不降时学习率乘以0.5 patience5, min_lr1e-6 ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop, reduce_lr], verbose1 )关键点有两处restore_best_weightsTrue是一块后悔药没有它即便EarlyStopping触发了模型权重也会停留在最后一个 epoch而不是验证集最优的那个 epochReduceLROnPlateau解决的是“loss 降到一定程度就卡住不动”的情况。训练 loss 曲线如果在 30 个 epoch 后呈一条水平线手动调学习率不如直接交给这个回调。3.4 训练集和验证集要按时间切不要随机切这是多特征时序预测区别于普通分类问题的第二道坎。分类任务随机打乱数据没问题因为样本间是独立的电力负荷的样本是从一个连续序列里切出来的前一个样本的窗口和后一个样本的窗口高度重叠随机切分验证集等于让模型看着答案的相邻片段去考试。源码里正确的处理方式是先按时间顺序在df层面切 80% 训练、20% 测试再从训练集尾部切一小段做验证集。千万不要把create_sequences出来的X拿去做train_test_split(X, y, shuffleTrue)那会导致同一段历史既出现在训练样本里又出现在验证样本里验证 loss 参考价值大打折扣。4. 把预测结果拉回真实尺度反归一化与指标评估4.1 反归一化的维度陷阱模型输出的是归一化后的负荷值范围在 0 到 1 之间直接拿它去和真实负荷对比毫无意义。反归一化时最容易翻车的是把scaler_target.inverse_transform用在了带特征维度的数组上。目标列是一个(n_samples,)的向量特征矩阵却是(n_samples, n_features)两者形状不同scaler 报错之后容易手忙脚乱地乱 reshape。# 预测结果是归一化值shape为 (n_samples, 1) pred_normalized model.predict(X_test) # 反归一化回真实负荷值 pred_load scaler_target.inverse_transform(pred_normalized) real_load scaler_target.inverse_transform(y_test.reshape(-1, 1)) # 计算误差指标 from sklearn.metrics import mean_absolute_error, mean_squared_error import numpy as np mae mean_absolute_error(real_load, pred_load) rmse np.sqrt(mean_squared_error(real_load, pred_load)) mape np.mean(np.abs((real_load - pred_load) / real_load)) * 100 print(fMAE: {mae:.4f}, RMSE: {rmse:.4f}, MAPE: {mape:.2f}%)inverse_transform要求输入是二维的所以先reshape(-1, 1)再传进去。如果直接拿一维数组传参很多版本的 scikit-learn 会报维度错误或者静默返回错误结果。真实场景里我更喜欢用 MAPE 作为第一眼指标因为电力负荷的体量在不同地区差异很大MAE 是 50 还是 500 没有横向参照MAPE 直接告诉你平均偏离百分之几5% 以内算可用10% 以上建议回炉调参。4.2 画图判断模型学没学到东西数值指标只能说明误差多大不能说明模型学的是“规律”还是“记忆”。把测试集前 72 个小时的真实负荷和预测负荷画在同一张图上一目了然如果预测曲线比真实曲线整体滞后几个小时形状却非常像说明模型学到的只是“把昨天的负荷平移过来当天用”没有真正利用温度等特征如果预测曲线在负荷波动处被抹平、只在均值附近浮动说明窗口太短或模型容量不够。import matplotlib.pyplot as plt # 只画测试集前72个点 plt.figure(figsize(12, 5)) plt.plot(real_load[:72], labelReal Load, linewidth2) plt.plot(pred_load[:72], labelLSTM Predict, linestyle--) plt.legend() plt.xlabel(Hour) plt.ylabel(Load) plt.title(Forecast vs Actual (first 72 hours)) plt.grid(alpha0.3) plt.show()判断预测质量的另一个视角是看曲线的峰值和谷值是否“按时出现”。电力负荷预测的模型如果捕捉到了日周期每天早晚高峰的位置应该大致对齐如果高峰预测晚了一两个小时哪怕 MAPE 只有 6%落实到排产调度上也会出问题。4.3 训练集拟合好不等于模型好很多源码工程会把训练集的 R² 或 loss 贴出来证明模型有效但一个 LSTM 在几千条样本上把训练集拟合到 R² 0.99 并不稀奇因为序列样本之间存在大量重叠模型完全可以记住大部分输入输出对。真正有说服力的只有测试集效果——也就是模型从未见过的连续时间段。我在评估这份源码时先看测试集 MAPE 是否在可接受范围内再看训练集和测试集指标差了多少。两者差距越大过拟合越严重。4.4 预测长度和评估口径要对齐源码里如果只做了“单步预测”评估时也是把每个时间点的预测单独拿出来算误差这和“多步预测”是两码事。单步预测每预测一个点都用的是真实历史窗口多步预测则要拿自己的预测结果续接窗口误差会逐步累积。多数课程设计资源默认只做单步预测论文里的 MAPE 自然好看一些。你要是在评估时发现测试集的输入窗口里混进了未来时刻的真实数据那预测结果好到离谱就是数据泄漏不是模型真的学会了。5. 避坑与常见问题训练翻车的五个典型现场5.1 归一化泄漏测试集误差虚低的元凶现象测试集 MAPE 不到 1%画图发现预测曲线和真实曲线几乎完全重合好得不真实。原因归一化时对整份数据包含测试集先fit了MinMaxScaler测试集的极值信息通过min/max泄漏进了训练过程。模型相当于提前知道了测试数据的值域边界。解决严格按照“先切分、后归一化”的顺序scaler.fit只作用在训练集上测试集只调用transform。检查代码里train_test_split和MinMaxScaler.fit_transform的先后顺序就能定位问题。5.2 输入特征用到了未来信息现象训练时误差非常低测试集上也不差但把预测结果拿去和真实负荷逐点对比发现每一天的预测值似乎都“提前知道”了当天的负荷水平。原因把当天的温度和湿度当作特征去预测当天的负荷。气象数据本身是预报值在预测时刻拿不到当天实时温度。严格来说应该用“预测时刻之前的气象预报或前一天同期温度”。解决构造特征时统一用 T-1 时刻或 T-24 时刻的温度、湿度做特征保证任何特征在预测时刻都是已知信息。课程设计通常不苛求这一步但你写论文时要有这个意识。5.3 训练时打乱了样本顺序现象训练 loss 一直震荡降不下去或者验证集表现比随机猜测还差。原因在create_sequences之后使用了train_test_split且没关shuffle时间上相邻的样本被打散到了训练集和验证集的两端LSTM 学到的时间依赖被切碎了。解决时序数据一律按时间顺序切分不要打乱。如果需要打乱来减小样本间相关性只打乱训练集内部的顺序且要保证验证集是连续的时间段。5.4 预测输出是前一条曲线的平移复制现象测试集预测曲线比真实曲线滞后一个时间步图上看就是预测值总比真实值慢一拍。原因负荷序列的自相关太强模型发现“把上一个时刻的负荷直接当成下一个时刻的预测”就能把 loss 压到很低于是选择了这条偷懒路径没有真正利用多特征。解决把基线对比做出来就清楚了——计算一个“naive 预测”直接用当前值做下一步预测的 MAE如果 LSTM 的表现几乎等于 naive 预测说明模型没有学到新规律。改进方向是调入窗口长度、增加节假日或温度相关特征或者在损失函数里引入一阶差分惩罚。5.5 训练 loss 变成 NaN 或者根本不下降现象训练到某个 epoch 后 loss 变成nan或者前几个 epoch 的 loss 一直是同一个数量级完全不动。原因nan一般是学习率过高导致梯度爆炸数据里存在NaN或Inf也会触发loss 不变则可能是特征范围差异大或者window_size设置太小导致模型学不到任何上下文。解决先检查df.isnull().sum()确认没有缺失值再把优化器学习率从默认1e-3降到1e-4试一试。如果降学习率后能正常下降说明不是结构问题而是优化参数问题。特征列如果存在量纲差异悬殊的列回到第 2 章检查归一化有没有漏掉。6. 进阶多步预测与模型调参的验证思路6.1 从单步预测走向滚动多步预测课程设计做到单步预测已经及格但很多实际调度场景要求预测未来 24 个小时的负荷曲线。滚动多步预测的做法是把模型预测出来的值追加到窗口末尾剔除窗口最前面的旧值然后继续喂给模型预测下一个点。它的问题是误差会像滚雪球一样累积预测步数越长偏差越大。def recursive_forecast(model, last_window, n_steps, scaler_feat, scaler_target): last_window: 归一化后的最后一个窗口shape为(1, window_size, n_features) n_steps: 要预测的未来步数 返回: 反归一化后的预测序列 current_window last_window.copy() preds_normalized [] for _ in range(n_steps): next_pred model.predict(current_window, verbose0)[0, 0] preds_normalized.append(next_pred) # 构造下一轮窗口把新预测值接到窗口最后 next_frame current_window[:, -1, :].copy() next_frame[0, 0] next_pred # 假设目标列是第一个特征 new_window np.concatenate( [current_window[:, 1:, :], next_frame[:, np.newaxis, :]], axis1 ) current_window new_window return scaler_target.inverse_transform( np.array(preds_normalized).reshape(-1, 1) )这段代码里最需要注意的是目标列在特征矩阵里的位置。源码里如果特征列的顺序是[load, temp, hum, weekday...]那么next_frame[0, 0]更新的是负荷列如果负荷列不在第一位这里的索引要跟着改否则模型预测的是“下一个温度”然后拿这个假负荷继续滚输出直接崩掉。我一般会让feature_cols固定为把target_col放在第一位的列表省得滚动拼接时索引混乱。6.2 调参最值得动的三个旋钮第一个是window_size。它决定模型能回溯多长的历史日周期数据和周周期数据对窗口长度的需求差很多。整点数据从 24 调到 168 往往会带来肉眼可见的 MAPE 下降再往上调收益就变缓了。第二个是 LSTM 层的units。从 32 往上翻倍试观察验证集 loss 是先降后升还是持续下降持续下降说明模型容量不足先降后升说明过了拟合拐点。第三个是Dropout比例。0.2 和 0.5 的区别在小数据集上比层数翻倍还明显我一般先用 0.2 跑通过拟合明显再加大到 0.4 到 0.5。6.3 用滚动验证代替单次切分单次切分的训练测试方式评估结果只代表“这一个时间段”的表现。电力负荷的分布是随季节漂移的夏天训练出来的模型放到冬天可能直接失效。进阶做法是滚动验证把数据按季度切成多段每次用前 80% 训练、后 20% 测试窗口整体向后滑动最终把多次评估的 MAPE 取平均。改进后的指标如果和你单次切分的结果差距很大说明模型对时间段的泛化能力不足比起继续调参优先补充更多时间范围的数据更有效。这套源码能让你在课程设计里拿到一条完整的预测链路但真正区分“及格”和“优秀”的是你能不能解释清楚为什么这么设参以及能不能在答辩时指出单步预测和滚动多步预测的误差差异。从那以后我拿到任何时序预测类的源码都强制自己走一遍第 5 章的检查清单把归一化泄漏、未来信息泄漏和窗口对齐这三个问题排查干净了再谈调参。回回都是这步省不掉希望帮到你。本文还有配套的精品资源点击获取