
简介这份资源是面向计算机、人工智能、数据科学及电子信息等专业学生与从业者的短期光伏预测实战项目核心采用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(methodlinear, limit6) # 物理约束裁剪 df[power] df[power].clip(lower0, uppercap) # 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())limit6的意思是连续缺失超过 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[firradiance_lag{lag}] df[irradiance].shift(lag) df[fpower_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_dim64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, dropoutdropout 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_firstTrue表示输入张量形状是(batch, seq_len, input_dim)这是 PyTorch 里比较符合直觉的排列。dropout只在多层 LSTM 时生效单层设了也没用。最后取out[:, -1, :]是拿最后一个时间步的隐藏状态做预测如果你要做多步预测这里要改成对每个时间步都输出。3.2 滑动窗口构造样本seq_len 和 pred_len 怎么配LSTM 的输入不是单条记录而是滑动窗口切出来的序列。假设你用过去 8 个时刻2 小时预测未来 1 个时刻15 分钟那seq_len8、pred_len1。这个比例没有绝对标准但经验上输入窗口至少覆盖 1 到 2 小时预测步长不超过输入窗口的四分之一。def make_sequences(data, seq_len8, pred_len1): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data[i:iseq_len, :-1]) # 最后一列是目标 y.append(data[iseq_len:iseq_lenpred_len, -1]) return np.array(X), np.array(y) X, y make_sequences(scaled_data, seq_len8, pred_len1) 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_dimX.shape[2]) optimizer Adam(model.parameters(), lr1e-3) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) 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_norm1.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 训练的后悔药梯度爆炸在循环网络里太常见了加上这个能稳很多。patience5是 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)], dim1) 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 倍。所以拿到新数据第一件事永远是确认单位别信文件名打开数据看一眼数值范围。这个习惯帮我省了不止一次通宵排查。希望帮到你。本文还有配套的精品资源点击获取