简介:这是一份面向高校学生与深度学习入门者的电力负荷预测实战项目源码,围绕Python与LSTM构建多特征预测模型,适用于毕业设计、期末大作业及课程设计等场景,帮助读者快速理解时序预测的完整实现流程。压缩包共8个文件,约830KB,包含3个py脚本分别负责数据处理、模型训练与结果可视化,2个md文档提供项目说明与使用指引,另有2个csv与1个xlsx存放原始及预处理后的电力负荷数据,结构清晰、便于按模块查阅。代码附有详细注释,新手也能看懂,下载后简单部署即可运行。目前已有239人学习关注,项目在数据清洗、特征构造、LSTM建模与预测评估等环节均有体现,读者可据此掌握多特征输入下的负荷预测思路,并在此基础上替换数据集或调整网络结构,完成自己的课题任务。
1. 多特征 LSTM 电力负荷预测:为什么单变量模型总是差一口气
做过电力负荷预测的人大多有过这样的经历:拿历史负荷序列直接喂给 LSTM,训练损失降得很漂亮,一到周末、节假日或者气温骤变那几天,预测曲线就明显跑偏。问题往往不在模型本身,而在于输入只有负荷这一个维度。电力负荷本质上是气温、湿度、日期类型、历史惯性共同作用的结果,单变量模型等于让网络去猜那些它根本看不到的信息。
这个标题讲的方案,核心就是把「多特征」这件事做扎实:用 Python 和深度学习框架搭一个 LSTM 网络,输入同时包含负荷、气象、时间标签等多路特征,输出未来一段时间的负荷值。它适合已经会写基础 Python、想从单变量时序预测进阶到工程可用方案的从业者,也适合电力、能源、园区运维方向需要做短期负荷预估的开发者。下面从数据、特征、模型到排错,把这条链路完整走一遍。
2. 多特征电力负荷预测的数据准备与特征工程
2.1 数据集长什么样,字段怎么对齐
电力负荷预测的原始数据通常来自两个源头:一是负荷采集系统导出的时序记录,粒度多为 15 分钟或 1 小时;二是气象部门或公开气象接口提供的温度、湿度、降雨等数据。两份数据的时间戳粒度往往不一致,气象数据常见是逐小时甚至逐日,负荷数据可能是 15 分钟一条。第一步不是建模,而是把时间轴对齐。
常见做法是统一到负荷数据的最高频率,气象数据用前向填充加线性插值补齐。比如温度逐小时一条,15 分钟粒度下就用插值生成中间三个点。对齐后每条记录应该包含:时间戳、负荷值、温度、湿度、是否节假日、星期几、小时。字段命名建议统一成英文小写下划线,避免后续脚本里出现中文列名导致的编码问题。
| 字段名 | 类型 | 含义 | 缺失处理 |
|---|---|---|---|
| timestamp | datetime | 采集时间 | 去重、排序 |
| load | float | 有功负荷 | 线性插值 |
| temperature | float | 气温 | 前向填充+插值 |
| humidity | float | 相对湿度 | 前向填充 |
| is_holiday | int | 是否节假日 | 默认 0 |
| weekday | int | 星期几 0-6 | 由时间戳推导 |
| hour | int | 小时 0-23 | 由时间戳推导 |
对齐之后要做异常值处理。负荷数据里常见的异常是采集设备掉线导致的连续零值,或者量程跳变产生的尖峰。零值不能简单当真实负荷,否则模型会学到「半夜负荷为零」这种错误模式。判断方法是看连续零值长度,超过两个采集周期就标记为缺失,用同日同时段的历史均值填补。
2.2 特征工程:把时间标签变成网络能吃的数字
LSTM 吃的是数值,is_holiday这种 0/1 标签可以直接用,但weekday和hour是周期性变量,直接当连续数值喂进去会出问题——网络会认为 23 点和 0 点相差很远,实际上它们相邻。正确做法是做周期编码,用正弦余弦把周期变量映射到单位圆上。
import numpy as np import pandas as pd def cyclical_encode(df, col, period): """把周期变量编码成正弦余弦两列""" df[col + '_sin'] = np.sin(2 * np.pi * df[col] / period) df[col + '_cos'] = np.cos(2 * np.pi * df[col] / period) return df # hour 周期 24,weekday 周期 7 df = cyclical_encode(df, 'hour', 24) df = cyclical_encode(df, 'weekday', 7)这段代码的逻辑是把hour和weekday各拆成两列。参数period是该变量的周期长度,小时是 24,星期是 7。编码后原始的hour、weekday列可以保留也可以删掉,保留不影响,但要注意别让网络对同一信息重复学习。周期编码的好处是 23 点和 0 点的编码值在圆上相邻,网络能正确理解这种邻近关系。
负荷本身还需要做归一化。不同季节、不同园区的负荷量级差异很大,用全局最大最小值归一化到 0-1 区间是最省事的做法。但要注意,归一化参数必须用训练集统计出来,再应用到验证集和测试集,否则会引入未来信息泄露。这是时序预测里最容易被忽略的坑之一。
from sklearn.preprocessing import MinMaxScaler # 只用训练集拟合 scaler train_len = int(len(df) * 0.7) scaler = MinMaxScaler() scaler.fit(df[['load']].iloc[:train_len]) df['load_scaled'] = scaler.transform(df[['load']])这里train_len取 70% 作为训练集边界,scaler.fit只喂训练段数据。后面验证和测试段的load用同一个 scaler 变换,保证量纲一致。如果反过来用全量数据拟合,测试集的极值会提前影响归一化范围,评估结果会虚高。
2.3 滑窗构造:多特征怎么组织成 LSTM 的输入张量
LSTM 的输入是三维张量,形状为(样本数, 时间步长, 特征数)。多特征方案里,特征数就是负荷加气象加时间标签的总列数。滑窗的作用是把连续时序切成一个个定长片段,每个片段对应一个预测目标。
def make_windows(data, target_col, feature_cols, window_size, horizon): """滑窗切分,返回 X 和 y""" X, y = [], [] for i in range(len(data) - window_size - horizon + 1): x_window = data[feature_cols].iloc[i:i + window_size].values y_target = data[target_col].iloc[i + window_size:i + window_size + horizon].values X.append(x_window) y.append(y_target) return np.array(X), np.array(y) feature_cols = ['load_scaled', 'temperature', 'humidity', 'is_holiday', 'hour_sin', 'hour_cos', 'weekday_sin', 'weekday_cos'] X, y = make_windows(df, 'load_scaled', feature_cols, window_size=24, horizon=1)window_size=24表示用过去 24 个时间步预测未来,horizon=1表示预测下一个点。如果数据是 15 分钟粒度,24 步就是 6 小时历史;如果是小时粒度,就是一天历史。这个参数没有标准答案,一般先用一个采集周期的整数倍试,比如小时数据用 24 或 48,再看验证集误差调整。feature_cols里必须包含load_scaled本身,否则模型看不到负荷的历史惯性,预测会退化成纯气象回归。
3. LSTM 模型搭建与训练:从网络结构到损失曲线
3.1 网络结构怎么定:层数、隐藏单元和 Dropout
多特征 LSTM 的网络结构不需要太深。两到三层 LSTM 堆叠,每层隐藏单元 64 到 128,后面接一个全连接输出层,对大多数负荷预测场景已经够用。层数再多容易过拟合,尤其是数据量只有一两年的时候。隐藏单元数可以按特征数的 4 到 8 倍起步,比如 8 个特征就用 64 个隐藏单元。
import torch import torch.nn as nn class LoadLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, input_size) out, _ = self.lstm(x) # 取最后一个时间步的输出 out = out[:, -1, :] return self.fc(out)input_size等于feature_cols的长度,这里是 8。num_layers=2表示两层 LSTM 堆叠,dropout=0.2在层间做正则。batch_first=True让输入张量第一维是 batch,符合 PyTorch 常见习惯。前向传播里取out[:, -1, :]是拿最后一个时间步的隐藏状态做预测,这是单步预测的标准做法。如果要做多步预测,这里需要改成序列到序列的结构,或者把输出维度改成horizon。
3.2 训练循环与早停:损失不降时该看什么
训练部分用 Adam 优化器,学习率 1e-3 起步,损失函数用 MSE 或 Huber。Huber 对异常值更稳,负荷数据里偶发尖峰多的时候优先选它。早停是必须的,验证集损失连续若干轮不降就停,避免过拟合。
from torch.utils.data import TensorDataset, DataLoader # 划分训练验证测试 n = len(X) train_end = int(n * 0.7) val_end = int(n * 0.85) X_train, y_train = X[:train_end], y[:train_end] X_val, y_val = X[train_end:val_end], y[train_end:val_end] X_test, y_test = X[val_end:], y[val_end:] train_loader = DataLoader( TensorDataset(torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)), batch_size=64, shuffle=True ) model = LoadLSTM(input_size=len(feature_cols)) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.HuberLoss() best_val = float('inf') patience, wait = 10, 0 for epoch in range(200): model.train() for xb, yb in train_loader: pred = model(xb) loss = criterion(pred, yb) optimizer.zero_grad() loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): val_pred = model(torch.tensor(X_val, dtype=torch.float32)) val_loss = criterion(val_pred, torch.tensor(y_val, dtype=torch.float32)).item() if val_loss < best_val: best_val = val_loss wait = 0 torch.save(model.state_dict(), 'best_lstm.pt') else: wait += 1 if wait >= patience: print(f'早停于 epoch {epoch}') breakbatch_size=64是常见起点,数据量小可以降到 32。shuffle=True只在训练集用,验证和测试不能打乱,否则时序顺序被破坏。早停的patience=10表示验证损失连续 10 轮不创新低就停。保存best_lstm.pt是为了留下验证集最优的权重,而不是最后一轮的权重。训练时如果训练损失一直降但验证损失往上走,说明过拟合,优先加 Dropout 或减层数,而不是继续加数据。
3.3 评估指标:别只看 MSE
负荷预测的评估不能只看 MSE。MSE 对量级敏感,不同数据集之间没法比。工程上更常用 MAPE 和 RMSE,MAPE 能直观看出百分比误差,RMSE 保留量纲。计算 MAPE 时要注意负荷接近零的时段会导致除零,通常加一个极小值或者只统计负荷大于某个阈值的点。
def mape(y_true, y_pred, eps=1e-6): return np.mean(np.abs((y_true - y_pred) / (y_true + eps))) * 100 def rmse(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) model.eval() with torch.no_grad(): test_pred = model(torch.tensor(X_test, dtype=torch.float32)).numpy() # 反归一化 test_pred_inv = scaler.inverse_transform(test_pred) y_test_inv = scaler.inverse_transform(y_test.reshape(-1, 1)) print('MAPE:', mape(y_test_inv, test_pred_inv)) print('RMSE:', rmse(y_test_inv, test_pred_inv))反归一化这一步经常被漏掉,导致算出来的误差是归一化尺度下的,没有物理意义。scaler.inverse_transform要用训练时拟合的同一个 scaler。MAPE 在 3% 到 5% 以内对短期负荷预测算不错,超过 10% 就要回头查特征或数据对齐问题。
4. 多特征 LSTM 落地避坑:五条血泪经验
4.1 现象:验证损失正常但测试集 MAPE 爆高
原因通常是归一化参数泄露。如果 scaler 用全量数据拟合,测试集的极值参与了归一化范围计算,模型在训练时间接看到了测试段的分布信息,验证集因为和训练集同源看不出问题,一到测试就露馅。解决方法是严格只用训练段拟合 scaler,验证和测试段只做 transform。检查方式很简单:打印 scaler 的 data_min_ 和 data_max_,看是否落在训练段范围内。
4.2 现象:模型预测曲线整体平移,形状对但数值偏
这是典型的特征尺度不一致。负荷归一化到 0-1,温度可能还是 20 到 35 的原始值,LSTM 的输入门会被大数值特征主导,负荷特征被淹没。解决办法是所有输入特征都做归一化或标准化,温度用 MinMax 或 Z-score 都行,关键是量纲统一。检查方式是打印 X_train 每个特征维度的均值和方差,差异超过一个数量级就要处理。
4.3 现象:周末和节假日预测误差明显大于工作日
原因是日期类型特征表达不够。只用is_holiday一个 0/1 标签,网络分不清周六、周日和法定节假日的差异。改进做法是加一列day_type,把工作日、周末、节假日分成三类做独热编码,或者把is_holiday拆成is_weekend和is_holiday两列。另外节假日前后一天的负荷模式也特殊,可以加一列is_adjacent_holiday标记。
4.4 现象:训练损失震荡不收敛
常见原因是学习率太大或者 batch 太小。Adam 默认 1e-3 对多数场景合适,如果损失曲线剧烈震荡,先降到 1e-4 试。另一个原因是数据没打乱,训练集按时间顺序喂进去,每个 batch 内的样本高度相关,梯度方向单一。shuffle=True能缓解,但时序预测里打乱会破坏窗口内的时序结构,正确做法是打乱窗口顺序而不是窗口内的时间步,DataLoader 的 shuffle 正好是这个粒度。
4.5 现象:多步预测越往后越离谱
如果horizon大于 1,直接让 LSTM 输出多个值,后面的步长误差会累积。常见做法是改成自回归预测,每次只预测一步,把预测值拼回输入再预测下一步。但这样误差同样会累积,而且推理慢。更稳的方案是序列到序列结构,编码器读历史,解码器逐步输出,训练时用 teacher forcing。如果只是做日前预测,horizon=24配合 seq2seq 比直接多输出头更可靠。
5. 让多特征 LSTM 再准一点的三个进阶技巧
第一个技巧是加入负荷的差分特征。原始负荷序列是非平稳的,直接预测绝对值难度大。加一列load_diff,即当前负荷减上一时刻负荷,让网络同时看到绝对水平和变化趋势。差分特征在归一化后同样要处理,通常用 Z-score 而不是 MinMax,因为差分有正有负。实测里加差分特征能让 MAPE 降 0.5 到 1 个百分点,代价是特征维度增加,训练稍慢。
第二个技巧是注意力机制。在 LSTM 输出后加一个注意力层,让网络自己决定历史 24 步里哪些时刻对当前预测更重要。实现上可以用一个线性层算注意力权重,再对 LSTM 各时间步输出做加权求和。代码不复杂,但要注意注意力权重可视化时别只看平均值,逐样本看能发现模型是否真的学到了「早高峰前几小时负荷开始爬升」这类模式。
class AttentionLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.attn = nn.Linear(hidden_size, 1) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # (batch, seq, hidden) weights = torch.softmax(self.attn(out), dim=1) context = (out * weights).sum(dim=1) return self.fc(context)self.attn把每个时间步的隐藏状态映射成一个分数,softmax 归一化成权重,再对隐藏状态加权求和。相比直接取最后一步,注意力能利用整个历史窗口的信息。参数上hidden_size和 LSTM 保持一致,不需要额外调。
第三个技巧是滚动预测加误差修正。先用模型跑一遍测试集,把预测误差按小时统计出来,得到一个误差修正表。正式预测时,模型输出加上对应小时的修正值。这个方法在负荷预测里很实用,因为误差往往有日内规律,比如傍晚时段系统性偏低。修正表要用验证集统计,不能用测试集,否则又是信息泄露。我自己的习惯是每次换数据集先跑一版无修正的基线,再决定要不要加修正,避免过度工程。
这三个技巧不用全上,按数据质量和误差来源选。特征工程没做干净之前,加注意力或修正表都是治标。希望帮到你。
本文还有配套的精品资源,点击获取