资讯详情 短期电价预测实战:LSTM与TCN深度学习模型对比与陷阱解析
📅 2026/10/18 14:54:26
简介面向电力市场分析与深度学习研究者该资源是一套完整的短期电价预测代码工程核心采用CTLSTM三维时序卷积与双向LSTM融合结构并给出特征筛选、建模、训练与对比的完整流程。作者利用皮尔逊相关系数、互信息和随机森林从电价、气象与负荷数据中提取关键变量最终选定电力负荷、湿度、hour_sin与hour_cos四项输入在澳大利亚某地区2010年下半年真实数据集上验证CTLSTM测试集RMSE为17.17较单层LSTM与CNN-LSTM降低约42%—43%。压缩包共16个文件包含Python源码特征工程、数据生成、CTLSTM模型与主程序、训练历史与预测结果图表、特征重要性图、皮尔逊热力图、互信息图、对比CSV、原始数据xlsx、依赖txt及README说明整体仅2.3MB轻量易部署。该套资源已有123人学习适合想快速复现实验、改进时序预测模型或支撑电力市场决策的研究者与工程师可沿用目录结构训练自己的电价数据。1. 短期电价预测为什么这件事值得用深度模型硬啃短期电价预测在能源数据分析里是个出了名的难啃问题一天之内电价能从谷值跳到峰值又跌回去传统 ARIMA、GARCH 这类线性模型很难同时抓住日内双峰和七日周期。用深度学习做这件事最常被提到的两个模型就是 LSTM长短期记忆网络和时序卷积网络TCN。这份代码资源把从数据清洗、滑窗切分到 LSTM/TCN 训练评估的整套流程都拆了出来适合已经跑通过基础时序模型、想换个思路的从业者也适合研究生拿来当毕业课题的起步模板。读完你会发现真正影响预测精度的往往不是模型结构而是数据怎么切、特征怎么构造、预测怎么滚动。2. 数据与特征工程从原始负荷到能喂进 LSTM 的滑窗样本2.1 原始数据先对齐时间索引、缺失值与节假日的处理电价预测第一步不是建模而是把数据整理成干净、连续、时间对齐的表格。我拿到一份某区域电力市场的小时级公开数据字段通常只有 time、load、temp、price 四列其中 time 是时间戳load 是系统负荷temp 是气温price 是实时电价。别小看这一步原始数据里经常出现时间错乱、部分时段缺失、周末与工作日混杂的问题直接建模会让模型学到错误的周期。import pandas as pd import numpy as np # 读取小时级电价数据列为 time, load, temp, price df pd.read_csv(electricity.csv, parse_dates[time]) df df.set_index(time).sort_index() # 剔除周末样本周末和工作日的电价曲线形态差异太大 df df[df.index.dayofweek 5] # 缺失值处理先向前填充缺失过多的时点直接丢弃 df df.ffill().dropna()逻辑说明parse_dates把 time 列解析成时间索引sort_index保证时间顺序dayofweek 5是去掉周六周日因为周末负荷基数低、电价峰值时段和工作日完全不同混在一起训练会让模型在两个模式之间反复摇摆。ffill()是向前填充缺失值适合于小时级数据里偶尔缺一两个点的情况如果连续缺失超过几十个小时建议直接删掉这段避免把虚构数据喂给模型。参数说明判断缺失要不要填充先看缺的是不是连续时段。凌晨低谷时段缺一两个点用前向填充影响不大如果缺的是早晚高峰时段我会更倾向把当天整段数据剔除或者用上周同一天同时刻的值补位。2.2 特征构造与归一化历史电价、负荷与温度怎么进模型电价序列最依赖的特征不是天气而是它自己过去的值。我一般会构造三个滞后特征昨天同一时刻电价、上周同一时刻电价、昨天同一时刻负荷。前两个覆盖 24 小时日周期和 168 小时周周期第三个把负荷当作外生变量因为负荷升高往往先于电价尖峰出现模型可以借用这个趋势。from sklearn.preprocessing import MinMaxScaler # 构造跨小时的自相关特征 df[price_lag_24] df[price].shift(24) # 昨天同一时刻电价 df[price_lag_168] df[price].shift(168) # 上周同一时刻电价 df[load_lag_24] df[load].shift(24) # 昨天同一时刻负荷 df df.dropna() # 特征列历史电价、负荷、温度和时间编码 feature_cols [price_lag_24, price_lag_168, load_lag_24, load, temp, hour, dayofweek] target_col price X_raw df[feature_cols].values y_raw df[[target_col]].values # 归一化只对连续变量缩放时间编码保持原样 scaler_x MinMaxScaler() scaler_y MinMaxScaler() X_norm scaler_x.fit_transform(X_raw[:, :5]) # 连续变量五列 time_feats X_raw[:, 5:] # hour, dayofweek X np.hstack([X_norm, time_feats]) y_norm scaler_y.fit_transform(y_raw).ravel()逻辑说明shift(24)和shift(168)直接取前 24 小时和前 168 小时的值这是电价预测里最朴素也最有效的特征。把连续变量和整数时间编码分开处理是因为 hour 和 dayofweek 是周期变量MinMaxScaler 会把 0 和 23 拉成 0 和 1但在真实语义里 23 点之后是 0 点距离是 1 而不是 23所以这两个特征保持原样更合理。参数说明这里为了展示特征构造过程暂时省略了数据集切分步骤实际跑实验时必须先切分训练集和测试集再对训练集 fit scaler测试集只做 transform。顺序反了就是时序泄漏第 5 章会专门展开。注意fillna在较新版本的 pandas 里推荐用df.ffill()而不是fillna(methodffill)后者虽然能用但会提示废弃警告写新代码时直接养成用ffill()的习惯。2.3 滑窗切分与数据集划分lookback168 与 horizon24 的取值逻辑把连续序列拆成独立样本用的是滑窗。窗口长度 lookback 决定模型看多长的历史horizon 决定预测未来多少个时点。我常用的组合是 lookback168、horizon24也就是用过去一周的小时数据预测未来一天。这个组合的好处是 168 恰好覆盖一个完整周周期模型在每个样本里都能看到上个星期同一天同时刻的电价。def make_samples(X, y, lookback168, horizon24): samples, targets [], [] for i in range(lookback, len(X) - horizon 1): samples.append(X[i - lookback:i]) targets.append(y[i:i horizon]) return np.array(samples), np.array(targets) # 按时间顺序切分训练 70%、验证 15%、测试 15% train_len int(len(X) * 0.7) val_len int(len(X) * 0.15) X_train, y_train make_samples(X[:train_len], y_norm[:train_len]) X_val, y_val make_samples(X[train_len:train_len val_len], y_norm[train_len:train_len val_len]) X_test, y_test make_samples(X[train_len val_len:], y_norm[train_len val_len:])逻辑说明make_samples里每次取X[i - lookback:i]作为输入y[i:i horizon]作为目标i 从 lookback 开始滑到倒数第 horizon 个位置数组形状变成三维和二维。切分必须按时间顺序不能像分类任务那样随机打乱否则训练集里混进测试时段的信息验证指标会虚高。参数说明lookback 取 168 不是唯一答案如果数据量不够比如只有三个月可以把 lookback 缩到 120 或 72牺牲周周期信息保住样本量horizon 取 24 能匹配电力市场的日清算节奏需要做日内滚动修正时也可以拆成 4 个 6 小时段分别预测。3. 双模型对比LSTM 与时序卷积网络TCN的结构差异与 PyTorch 实现3.1 为什么先选 LSTM门结构适合电价这种长周期时间模式LSTM 在电价预测里几乎是默认起点核心原因是它的遗忘门和输入门能分别处理不同尺度的记忆。电价序列的规律不是单一周期日内有早晚双峰周内有工作日和周末之分季度里有冬夏负荷差异。传统循环神经网络很难同时保留这么多尺度的上下文LSTM 通过门控机制把短期波动和长期趋势分开存储昨天同一时刻的价差和上周同一天的模式都能保留下来。LSTM 的代价也很明显串行计算导致训练慢序列越长梯度路径越深即使有门控也可能丢信息。所以实际实验里我会先把 LSTM 跑通再用 TCN 对比两者 MAPE 差距通常在 0.5 到 1.5 个百分点之间数据集越大越能看出 TCN 的优势。3.2 为什么备选 TCN因果卷积如何避免偷看未来数据TCN 全称时序卷积网络核心是因果卷积加膨胀卷积。因果卷积保证 t 时刻的输出只依赖 t 时刻及之前的数据从结构上杜绝未来信息泄漏膨胀卷积则通过 dilation 参数指数扩大感受野两层、三层之后就能覆盖几百个小时的历史。和 LSTM 比TCN 的训练是高度并行的同样一份数据在 GPU 上通常快两三倍。TCN 的翻车点在于膨胀卷积的超参比较敏感。dilation 从 1、2、4 一路翻倍时感受野覆盖得很快但如果 kernel_size 太小、层数不够模型只能看到局部短期模式抓不住周周期。这就是为什么我一般在 TCN 里至少叠三层并把通道数逐层增大。3.3 核心实现LSTM 与 TCN 的 PyTorch 代码与参数表两个模型都写成标准的 PyTorchnn.Module输入都是三维张量形状为 (batch, lookback, features)。LSTM 取最后一个时间步的隐状态TCN 取最后一个时间步的特征图最后都过一个全连接头输出 24 个价格点。import torch from torch import nn class LSTMPrice(nn.Module): def __init__(self, n_features, hidden_size64, num_layers2, horizon24): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.head nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, horizon) ) def forward(self, x): out, _ self.lstm(x) # out 形状: (batch, lookback, hidden_size) last out[:, -1, :] # 只取最后一个时间步的隐状态 return self.head(last)逻辑说明batch_firstTrue让输入张量的第一个维度是 batch方便和数据加载器对齐out[:, -1, :]取序列最后一个时点的隐状态这个时点理论上浓缩了整个 168 小时的历史信息再接两层全连接直接输出 24 个值。参数说明hidden_size 取 64 是经验值数据量超过几万样本时可以升到 128num_layers 不建议超过 3 层训练时间翻倍但精度收益很小。from torch.nn.utils import weight_norm class TCNBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, dilation2, dropout0.1): super().__init__() self.padding (kernel_size - 1) * dilation self.pad nn.ConstantPad1d((self.padding, 0), 0.0) # 只在左侧补零 self.conv weight_norm(nn.Conv1d( in_channels, out_channels, kernel_size, dilationdilation )) self.relu nn.ReLU() self.dropout nn.Dropout(dropout) def forward(self, x): x self.pad(x) x self.conv(x) return self.dropout(self.relu(x)) class TCNPrice(nn.Module): def __init__(self, n_features, channels[32, 64, 128], kernel_size3, horizon24): super().__init__() blocks [] in_ch n_features for i, out_ch in enumerate(channels): blocks.append(TCNBlock( in_ch, out_ch, kernel_size, dilation2 ** i )) in_ch out_ch self.tcn nn.Sequential(*blocks) self.head nn.Sequential( nn.Linear(channels[-1], 64), nn.ReLU(), nn.Linear(64, horizon) ) def forward(self, x): x x.transpose(1, 2) # 转成 (batch, features, lookback) x self.tcn(x) x x[:, :, -1] # 取最后一个时间步 return self.head(x)逻辑说明TCN 的输入要先转置因为 Conv1d 期望的通道维在第二个位置。ConstantPad1d只在序列左侧补零保证因果性不补右侧是因为不需要看到未来。dilation 按 2 的幂递增第一层感受野小、关注局部波动后面几层逐渐扩大范围最后覆盖整个 168 小时窗口。参数说明channels 列表代表每一层的输出通道数我常用 [32, 64, 128]三层足够。kernel_size 取 3 最稳取 5 能加快感受野增长但容易过拟合。dropout 放在每个卷积块后面防止深层网络在小数据集上训不动。配置项LSTMTCN核心结构循环单元加门控因果卷积加膨胀卷积常用规模hidden_size64num_layers2channels32→64→128感受野控制靠层数堆叠、记忆数百步dilation 按 2 的幂扩大训练速度较慢串行计算较快可并行超参搜索重点num_layers、hidden_sizekernel_size、channels、dilation4. 训练闭环损失函数、评估指标与早停的工程化配置4.1 损失函数怎么选MSE 还是 MAE/MAPE电价预测里损失函数选错模型会被极端尖峰带偏。MSE 对误差平方惩罚一个超大误差比如尖峰日差了 100 元抵得上几百个普通时段的误差模型为了降低 loss 会把预测值压向中间区域结果日常时段看起来还行尖峰一个都抓不住。我一般首选 L1Loss也就是 MAE它对异常值更稳健训练出的模型在尖峰日不会过分保守。criterion nn.L1Loss()逻辑说明L1Loss 对应 MAE梯度是常数不会因为某个样本误差特别大而让整个 batch 的梯度失控。如果业务上更关注尖峰时段的偏差也可以把 L1Loss 和 L2Loss 按 0.7 比 0.3 加权混合但那是调参后期的事第一版先用纯 L1Loss 打底。4.2 评估指标MAPE 才是业务真正关心的数字模型对比阶段我同时看 MAE 和 MAPE但最终向业务汇报时只看 MAPE。MAPE 是平均绝对百分比误差公式是每个时点绝对误差除以真实电价再取平均直接反映预测值偏离真实价的比例。电价低的时候比如凌晨 2 点电价只有几十元绝对误差 20 元就会产生很高的百分比误差所以 MAPE 天然偏向惩罚低谷时段的高相对误差这符合交易员的直觉。注意观察 MAPE 时要分开看峰时段和谷时段整体 MAPE 在 5% 以内就算不错超过 10% 的模型基本没法直接用于报价决策。4.3 训练循环工程化早停、ReduceLROnPlateau 与梯度裁剪模型训练不是简单把损失函数跑几十轮就结束需要配套早停和学习率调度否则很容易过拟合或者停在局部最优。我习惯用 ReduceLROnPlateau 监测验证集 loss连续 5 轮不下降就把学习率减半再配合早停连续 10 轮验证 loss 不创新低就终止训练保存最好的那一次权重。from torch.utils.data import TensorDataset, DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMPrice(n_featuresX_train.shape[-1]).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 ) train_ds TensorDataset(torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.float32)) train_loader DataLoader(train_ds, batch_size128, shuffleFalse) best_loss float(inf) patience 10 bad_epochs 0 for epoch in range(200): model.train() epoch_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() epoch_loss loss.item() val_loss evaluate(model, X_val, y_val) # 自定义评估函数见资源内脚本 scheduler.step(val_loss) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_model.pt) bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: break逻辑说明clip_grad_norm_把梯度的 L2 范数限制在 5.0 以内LSTM 在长序列上容易梯度爆炸加了裁剪基本没再遇到 loss 变 NaN 的情况。ReduceLROnPlateau的 patience 设为 5早停的总 patience 设为 10也就是说学习率先减半一次要是还不见起色就停止训练这个节奏不会浪费太多时间在无效迭代上。参数说明batch_size 我取 128样本量几万时显存和内存都友好学习率从 1e-3 起步如果训练 loss 一开始就震荡先降到 5e-4 再看weight_decay 可选 1e-5数据量小时能压住过拟合。超参推荐值说明优化器Adam默认 beta 即可收敛快初始学习率1e-3震荡就降到 5e-4batch_size128大样本用 256小样本用 64梯度裁剪5.0防止梯度爆炸导致 NaN早停 patience10连续 10 轮验证集不降即停学习率调度每 5 轮减半ReduceLROnPlateau 的 patience55. 避坑排查电价预测中五个最隐蔽的翻车场景5.1 翻车点一归一化逆变换时把最大最小值用错窗口现象预测出的曲线形状和真实电价基本吻合但数值整体偏移有时候全比真实值低有时候全高。原因训练时对训练集做了 MinMaxScaler预测后却用了全量数据或者测试集单独 fit 的 scaler 做逆变换量纲压根对不上。解决整套流程里 scaler 只 fit 一次保存成 pickle 文件训练、验证、测试统一使用同一个对象做逆变换。scaler_y MinMaxScaler() y_train_scaled scaler_y.fit_transform(y_train.reshape(-1, 1)) # 逆变换时只用同一个 scaler_y不要再 fit pred_price scaler_y.inverse_transform(pred.reshape(-1, 1))5.2 翻车点二时序泄漏用未来数据构造特征而不自知现象验证集上 MAPE 只有 3%换到线上真实预测变成 15% 以上。原因最常见的两种情况一是对整个数据集做了标准化或者滑动均值之后才切分二是在滑窗切分时不小心把目标时段的真实值放进了特征列。解决先按时间顺序切分再对训练集 fit scaler特征构造顺序要仔细检查shift(-k)这类往未来方向移动的函数一律禁止出现在特征里。5.3 翻车点三测试集只选了一段好日子指标虚高现象换一批数据重新测试MAPE 从 4% 变成 9%。原因测试集被有意无意选在春秋季或者负荷平稳的月份没有覆盖寒潮、高温、节假日这些极端场景。解决测试集至少包含一整年的数据或者明确留出 6 月到 8 月这种高负荷季节在报告里分开给出峰季和非峰季的 MAPE。5.4 翻车点四递归多步预测导致误差指数级放大现象前 6 小时预测误差很小到第 20 小时预测值明显偏离有时候直接飞到负值。原因部分实现把每一步的预测值当作下一步输入误差沿着时间轴累计最后完全失去参考。解决用多输出结构一次性生成未来 24 小时也就是本文 2.3 节里的 horizon 设计如果一定要递归每步修正一次使用最近的真实观测值作为输入。5.5 翻车点五LSTM 随机性导致实验不可比现象同样的超参跑两次MAPE 差出两个点。原因LSTM 的权重初始化和数据加载都有随机性没固定种子时每次实验结果都不一样调参根本没法判断是超参的作用还是随机运气。解决训练前固定 NumPy 和 PyTorch 的随机种子并把 cudnn 的确定性模式打开。import random import numpy as np import torch random.seed(0) np.random.seed(0) torch.manual_seed(0) torch.cuda.manual_seed_all(0) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False6. 进阶技巧用日内滚动窗口把测试集跑成仿真值班表6.1 滚动预测模拟每天零点的决策固定测试集一次性预测只能验证模型精度不能模拟真实业务场景。实际做短期电价预测时每天零点都要基于最新数据重新预测未来 24 小时这样滚动跑出来的指标才是真实可用度。我一般在测试集上按 step24 滑动每次只取最近 168 小时作为输入记录当天的预测结果后跳到下一天整个预测序列拼接后和真实电价对齐计算最终 MAPE。def rolling_forecast(model, X_seq, y_seq, scaler_y, start_idx, horizon24, step24): model.eval() preds [] for i in range(start_idx, len(X_seq) - horizon 1, step): xb torch.tensor(X_seq[i:i1], dtypetorch.float32).to(device) with torch.no_grad(): pred model(xb).cpu().numpy().ravel() # 逆变换回真实电价单位 preds.append(scaler_y.inverse_transform( pred.reshape(-1, 1)).ravel()) return np.concatenate(preds, axis0)逻辑说明循环从 start_idx 开始每次移动 24 小时保证预测窗口之间没有重叠模拟的是每天只做一次决策的真实节奏。scaler_y.inverse_transform把归一化后的预测值还原成电价单位。这个函数的输出再和真实测试序列逐一对齐算出来的 MAPE 才是能写进汇报里的数字。6.2 模型融合把 LSTM 和 TCN 的预测均值化单个模型总是有系统性偏差LSTM 对尖峰反应慢半拍TCN 在低谷时段略偏保守。把两个模型的预测结果按 0.5 比 0.5 加权平均往往能同时拉低峰时和谷时的误差。我在多个数据集上验证过融合后的 MAPE 比单模型平均低 0.5 到 1 个百分点代价只是推理时间多一倍训练完成之后这部分成本完全可接受。pred_fusion 0.5 * pred_lstm 0.5 * pred_tcn之前我调好一组参数以为万事大吉结果第二天换了一台机器跑同一份数据MAPE 差了快两个点后来才意识到随机种子和 cudnn 配置根本没固定。从那以后我每次跑对比实验都会先把种子、设备信息、数据划分版本记在实验日志里再开始调参。做短期电价预测这个方向数据史永远比模型技巧先决定结果把滚动预测和融合验证跑完整你的模型才算真正落地。希望帮到你。本文还有配套的精品资源点击获取