简介:这份资源是面向计算机、人工智能、数据科学及电子信息等专业学生与从业者的短期光伏预测实战项目,核心采用LSTM网络对光伏发电功率进行时序建模与预测,适合作为课程设计、毕业设计、大作业或初期项目立项的参考范例,也便于初学者通过完整代码与数据快速上手深度学习预测流程。压缩包共11个文件,约3.89MB,包含6个ipynb交互式笔记本、1个py脚本、1个xlsx数据集、1个jpg与1个png示意图以及1个md说明文档,覆盖数据读取、模型搭建、训练与结果可视化等环节,并附带园区光伏与负荷预测的测试案例及储能规则集相关探索。目前已有1022人学习下载,代码经过运行验证,功能正常,读者可据此理解LSTM在新能源预测中的特征构造、参数调优与评估思路,并在此基础上迁移到风电、负荷等时序预测任务,具备较高的学习借鉴与二次开发价值。
1. 光伏功率预测为什么总在下午三点翻车:从 LSTM 短期预测说起
做过光伏电站运维的人大概都有个共同体验:早上九点模型预测曲线跟实测贴合得挺好,一到下午两三点云层翻滚的时候,预测值就开始飘,误差能冲到 20% 以上。这不是模型不行,而是短期光伏预测本身就是一个被气象随机性反复捶打的场景。基于 LSTM 的短期光伏预测算法,核心思路是用历史功率序列加上气象因子(辐照度、温度、湿度、风速),训练一个能记住时序依赖关系的循环网络,输出未来 1 到 4 小时的功率值。它适合谁?适合手里有电站 SCADA 数据、想做超短期功率预测的算法工程师,也适合刚学完 LSTM 时间序列预测、想找一个真实数据集练手的 Python 入门者。这篇笔记不讲论文里的公式推导,只讲一件事:拿到一份 LSTM 光伏预测的 Python 源码和数据集之后,怎么把它跑通、调好、避开那些让误差突然爆炸的坑。
2. 数据到手先别急着喂模型:光伏功率序列的清洗与特征工程
2.1 光伏数据集长什么样,先做一次体检
一份典型的短期光伏预测数据集,通常包含时间戳、实际功率(kW 或 MW)、辐照度(W/m²)、组件温度、环境温度、湿度、风速这几列。采样间隔常见的是 5 分钟、15 分钟或 1 小时。拿到数据第一件事不是写模型,而是做一次数据体检。我一般会先跑下面这段代码,把缺失率、异常值范围、夜间零值占比一次性看清楚。
import pandas as pd import numpy as np df = pd.read_csv('pv_data.csv', parse_dates=['timestamp']) df = df.set_index('timestamp').sort_index() # 缺失率 print("缺失率:\n", df.isnull().mean()) # 功率为负的异常点(逆变器采样噪声常见) neg = df[df['power'] < 0] print("负功率点数:", len(neg)) # 夜间零值占比(辐照度为0时功率应为0) night = df[df['irradiance'] <= 5] print("夜间零值占比:", (night['power'] == 0).mean()) # 功率突变检测:相邻点差值超过装机容量30% cap = df['power'].max() jump = df['power'].diff().abs() > 0.3 * cap print("突变点数:", jump.sum())这段代码的逻辑很直接:缺失率决定你要不要插补,负功率点说明传感器有噪声,夜间零值占比低说明数据采集有问题,突变点则可能是云遮或者设备故障。参数上,irradiance <= 5这个阈值不是固定的,如果你拿到的辐照度单位是 kW/m²,阈值要改成 0.005。装机容量cap用历史最大值近似,如果数据里包含限电时段,最好用额定容量而不是最大值。
2.2 缺失值插补和异常值处理,别用均值一刀切
光伏功率序列的缺失插补有个基本原则:白天用线性插值或前向填充,夜间直接补零。因为夜间功率本来就是零,你用均值插补反而会引入虚假的非零功率。异常值处理我习惯用「物理约束 + 统计约束」双管齐下:物理上功率不能为负、不能超过装机容量;统计上用 IQR 或者 3σ 找出离群点,但要注意云遮导致的功率骤降是真实物理现象,不能当异常值删掉。
# 夜间补零,白天线性插值 mask_night = df['irradiance'] <= 5 df.loc[mask_night, 'power'] = df.loc[mask_night, 'power'].fillna(0) df['power'] = df['power'].interpolate(method='linear', limit=6) # 物理约束裁剪 df['power'] = df['power'].clip(lower=0, upper=cap) # 3σ 异常检测(仅对白天数据) day = df[df['irradiance'] > 5].copy() mu, sigma = day['power'].mean(), day['power'].std() outlier = (day['power'] - mu).abs() > 3 * sigma print("3σ离群点数:", outlier.sum())limit=6的意思是连续缺失超过 6 个点就不插补了,保留 NaN 让后续处理决定丢弃还是其他方式填充。这个值根据采样间隔调整,15 分钟采样的话 6 个点就是 1.5 小时,超过这个长度的缺失插补意义不大。
2.3 特征工程:把时间戳拆成模型能吃的数字
LSTM 虽然能学时序依赖,但不会自动理解「下午三点」和「早上九点」的区别。时间特征必须手动构造。我一般会加这几类:小时的正弦余弦编码(避免 23 点和 0 点被当成距离很远)、辐照度的滞后特征(前 1 到 4 个时刻)、功率的滑动统计量(过去 1 小时均值、方差)。
df['hour_sin'] = np.sin(2 * np.pi * df.index.hour / 24) df['hour_cos'] = np.cos(2 * np.pi * df.index.hour / 24) for lag in [1, 2, 3, 4]: df[f'irradiance_lag{lag}'] = df['irradiance'].shift(lag) df[f'power_lag{lag}'] = df['power'].shift(lag) df['power_roll_mean_4'] = df['power'].rolling(4).mean() df['power_roll_std_4'] = df['power'].rolling(4).std() df = df.dropna()滞后阶数选 4 是因为 15 分钟采样下 4 个点正好覆盖过去 1 小时,这个窗口对短期预测足够。如果你做的是超短期光伏功率预测(未来 15 分钟),滞后阶数可以减到 2。滑动统计量的窗口同理,不要盲目拉长,否则会引入太多历史信息反而稀释了近期变化。
3. LSTM 模型搭起来:从 PyTorch 源码结构到训练参数
3.1 网络结构怎么定:层数、隐藏单元、Dropout 的取舍
一份常见的 LSTM 光伏预测源码,网络结构大概是这样的:输入层接特征维度,一到两层 LSTM,最后接一个全连接层输出预测值。层数不是越多越好,光伏功率序列的时序依赖通常在几小时以内,两层 LSTM 已经能覆盖,再深容易过拟合。隐藏单元数我一般从 64 开始试,数据量大(超过 10 万条)可以上 128,数据量小就降到 32。
import torch import torch.nn as nn class PVLSTM(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0 ) self.fc = nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :])batch_first=True表示输入张量形状是(batch, seq_len, input_dim),这是 PyTorch 里比较符合直觉的排列。dropout只在多层 LSTM 时生效,单层设了也没用。最后取out[:, -1, :]是拿最后一个时间步的隐藏状态做预测,如果你要做多步预测,这里要改成对每个时间步都输出。
3.2 滑动窗口构造样本:seq_len 和 pred_len 怎么配
LSTM 的输入不是单条记录,而是滑动窗口切出来的序列。假设你用过去 8 个时刻(2 小时)预测未来 1 个时刻(15 分钟),那seq_len=8、pred_len=1。这个比例没有绝对标准,但经验上输入窗口至少覆盖 1 到 2 小时,预测步长不超过输入窗口的四分之一。
def make_sequences(data, seq_len=8, pred_len=1): X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): X.append(data[i:i+seq_len, :-1]) # 最后一列是目标 y.append(data[i+seq_len:i+seq_len+pred_len, -1]) return np.array(X), np.array(y) X, y = make_sequences(scaled_data, seq_len=8, pred_len=1) print("样本形状:", X.shape, y.shape)data[:, :-1]是特征列,data[:, -1]是功率目标列。这个顺序在标准化之前就要确定好,否则后面列对不上。样本数会减少seq_len + pred_len - 1条,数据量小的时候要注意别切得太狠。
3.3 训练循环里的三个关键参数:学习率、批次大小、早停
训练 LSTM 最容易翻车的地方不是网络结构,而是学习率和批次大小。学习率太大,loss 震荡不收敛;太小,训练慢到怀疑人生。我一般用 1e-3 起步,配合ReduceLROnPlateau自动降。批次大小 32 或 64 比较稳,太小梯度噪声大,太大泛化差。
from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau model = PVLSTM(input_dim=X.shape[2]) optimizer = Adam(model.parameters(), lr=1e-3) scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5) criterion = nn.MSELoss() best_loss = float('inf') patience_counter = 0 for epoch in range(100): model.train() for xb, yb in train_loader: pred = model(xb) loss = criterion(pred, yb) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() val_loss = 0 with torch.no_grad(): for xb, yb in val_loader: val_loss += criterion(model(xb), yb).item() val_loss /= len(val_loader) scheduler.step(val_loss) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), 'best_pv_lstm.pt') patience_counter = 0 else: patience_counter += 1 if patience_counter >= 10: print(f"早停于 epoch {epoch}") breakclip_grad_norm_是 LSTM 训练的后悔药,梯度爆炸在循环网络里太常见了,加上这个能稳很多。patience=5是 scheduler 的耐心,patience_counter >= 10是早停的耐心,两个别搞混。早停保存的是验证集 loss 最低的模型,不是最后一个 epoch 的模型,这个细节很多源码里会写错。
4. 预测结果不对劲?先查这五个地方
4.1 现象:预测曲线整体平移,误差稳定在某个方向
原因通常是标准化没做对。训练集和测试集必须用同一个 scaler,而且 scaler 只能在训练集上 fit。如果测试集单独 fit 了,分布偏移会导致预测值整体偏高或偏低。解决方法是把 scaler 保存下来,测试时直接 transform。
from sklearn.preprocessing import MinMaxScaler import joblib scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_data) test_scaled = scaler.transform(test_data) joblib.dump(scaler, 'pv_scaler.pkl')4.2 现象:夜间预测值不为零,甚至出现负功率
原因是模型没有学到「辐照度为 0 时功率必须为 0」这个物理约束。纯数据驱动的 LSTM 不会自动遵守物理规律。解决方法有两个:一是在后处理阶段加一个 mask,夜间直接置零;二是在 loss 里加惩罚项,对夜间非零预测加大惩罚。
def masked_mse(pred, target, irradiance): loss = (pred - target) ** 2 night_mask = (irradiance <= 5).float() loss = loss * (1 + 5 * night_mask) # 夜间误差权重放大5倍 return loss.mean()4.3 现象:训练 loss 降得很低,验证 loss 却往上走
典型的过拟合。光伏数据如果只用了某一两个月的,模型会把那个季节的天气模式背下来。解决方法是增加 Dropout、减小隐藏单元、加 L2 正则,或者最直接的——扩充数据,把一年四季的数据都放进去。如果数据实在不够,可以用早停强行截断。
4.4 现象:预测值比实测值滞后一个时间步
这是 LSTM 做时间序列预测的经典问题,模型倾向于输出上一个时刻的值。原因是自回归特征(power_lag1)权重太大,模型偷懒直接抄。解决方法是在训练时对滞后特征做随机 mask,强迫模型学其他特征。
# 训练时以20%概率将power_lag1置零 if np.random.rand() < 0.2: xb[:, :, lag1_idx] = 04.5 现象:不同季节误差差异巨大,夏天准冬天崩
光伏功率的季节性极强,夏天辐照度高、波动大,冬天辐照度低、云层多。如果模型只在夏天数据上训练,冬天必然翻车。解决方法是要么按季节分别建模,要么在特征里加入月份或季节的 one-hot 编码,让模型自己学季节差异。
5. 把误差再压两个点:多步预测与模型融合的实战技巧
单步预测跑通之后,真正有价值的是多步预测——预测未来 1 小时、2 小时甚至 4 小时的功率曲线。直接让 LSTM 输出多个值,误差会累积得很快。我一般用两种策略:一是直接多输出,把fc层输出维度改成pred_len;二是滚动预测,用预测值喂回输入继续预测下一步。前者误差稳定但精度有限,后者短期精度高但几步之后就会漂。
# 直接多输出:fc 输出 pred_len 个值 self.fc = nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Linear(32, pred_len) ) # 滚动预测 def rolling_predict(model, init_seq, steps): preds = [] seq = init_seq.clone() for _ in range(steps): with torch.no_grad(): p = model(seq) preds.append(p.item()) # 把预测值拼到序列末尾,去掉最早的时刻 new_step = seq[:, -1, :].clone() new_step[:, -1] = p seq = torch.cat([seq[:, 1:, :], new_step.unsqueeze(1)], dim=1) return preds滚动预测里new_step[:, -1] = p是把预测的功率值替换掉最后一维,其他特征(辐照度、温度)保持不变。这个假设在短期预测里勉强成立,但如果预测步长超过 1 小时,气象特征也需要同步预测,否则误差会从特征端传入。
模型融合是另一个压误差的手段。LSTM 对时序依赖学得好,但对手工特征不敏感;XGBoost 或 LightGBM 对特征工程敏感,但学不到长程依赖。把两者的预测结果做加权平均,往往能比单模型低 1 到 2 个百分点。权重不用复杂优化,按验证集误差的倒数分配就行。
w_lstm = 1 / mse_lstm w_gbdt = 1 / mse_gbdt pred_final = (w_lstm * pred_lstm + w_gbdt * pred_gbdt) / (w_lstm + w_gbdt)验证方法上,我习惯用「按天滚动」而不是随机划分。光伏数据的时序性太强,随机划分会让同一天的数据同时出现在训练集和测试集里,指标虚高。按天滚动就是前 80% 的天数做训练,后 20% 做测试,中间不留重叠。如果要做更严格的评估,可以用滑动窗口交叉验证,每次用过去 N 天训练、下一天测试,滚动前进。
最后说一个我踩过的坑:有一次模型在测试集上 MAPE 只有 6%,上线之后实际误差却到了 15%。排查了半天发现是数据集的功率单位是 kW,而电站 SCADA 传过来的是 MW,差了 1000 倍。所以拿到新数据第一件事永远是确认单位,别信文件名,打开数据看一眼数值范围。这个习惯帮我省了不止一次通宵排查。希望帮到你。
本文还有配套的精品资源,点击获取