简介基于Python与LSTM的短期光伏预测算法实现资源面向计算机、人工智能、通信工程、自动化等专业的在校学生与开发者可用于毕设、课程设计或算法入门。资源围绕园区光伏数据展开包含单变量、多变量光伏预测流程以及负荷预测LSTM示例并延伸了基于规则集的储能框架覆盖数据读取、特征处理、模型训练与结果可视化等关键环节。压缩包共11个文件核心为6个Jupyter Notebook分步演示算法实现另附1个可直接运行的Python脚本、1份Excel测试数据、2张实验过程图片和1份README说明文档整体大小约3.89MB目录结构清晰便于按需查阅与二次开发。已有202人浏览学习代码经作者测试运行成功支持远程教学讲解适合希望快速上手LSTM时序预测或需要参考完整项目后自行扩展修改的初学者与进阶者。1. 光伏预测为什么非LSTM不可先说结论再给方法做光伏功率预测的人都会遇到同一个尴尬天气预报明明说晴天中午云层一过功率曲线直接跳水20%。这种辐照度突变靠物理模型很难追上因为云的运动本身就是一个混沌过程。而基于LSTM的短期光伏预测算法恰好擅长捕捉这种“前一小时的变化趋势会延续到下一小时”的时间依赖它不看绝对数值看的是序列的演变模式。这套用Python实现的方案输入历史功率、辐照度、温度输出未来1到4小时的功率曲线在中型电站的实测中RMSE通常能压到装机容量的8%到12%。适合手里有SCADA或电站监控数据、想自己搭一套预测服务的新能源工程师和数据挖掘从业者。下文直接用可复现代码讲透整条链路不绕弯子。2. 短期光伏预测的选型逻辑LSTM凭什么比传统RNN和Transformer更稳2.1 光伏功率序列的三个统计特征决定了模型选型先看数据再谈模型。光伏功率序列有三个绕不开的特征强周期性、强自相关性、非平稳突变。日出日落让序列呈钟形曲线所以每天的功率数据天然就是“两头低、中间高”相邻两个采样点之间的功率值相关性极高15分钟粒度下自相关系数常超过0.95而云的遮挡会在几分钟内让功率跌掉一半以上产生大量陡峭的阶跃点。这三个特征直接排除了很多模型。线性回归把突变当噪声扛不住传统RNN对长序列的梯度消失问题让它在50步以上的输入上明显乏力而Transformer理论上能捕捉长距离依赖但光伏预测的训练样本通常只是单个电站的一年数据几十万条级别喂给动辄几千万参数的Transformer很快就会过拟合而且推理延迟在边缘端部署也不友好。LSTM通过门控机制记住了前一天同时刻的功率形态又对短时突变敏感是短期光伏预测里性价比最高的选择。2.2 与GRU、TCN、Transformer的适用边界对比模型参数量对突变响应训练速度边缘部署友好度在光伏短期预测上的典型表现普通RNN小差梯度消失快高预测曲线滞后明显GRU小中略好于RNN快高与LSTM接近但收敛慢LSTM中好门控能保留突变信息中高综合最优工程落地最多TCN中好感受野可调中中适合长序列但调参较敏感Transformer大依赖注意力机制慢低数据不足时易过拟合我做过的项目里GRU和LSTM在15分钟粒度的超短期光伏功率预测上精度差距通常小于2%但LSTM的PyTorch实现和文档资料更全团队协作时沟通成本低所以最终方案还是锁定了LSTM。如果你的场景是未来5分钟级别的超短期预测TCN也可以纳入考虑但如果目标是未来1小时以上LSTM的稳定性更值得信赖。2.3 输入输出窗口怎么定从物理意义而不是拍脑袋出发输入窗口长度不能随便定。光伏功率和辐照度的变化周期在晴天是平缓的钟形在云天则是分钟级跳变。输入太长模型学到的是“昨天同一时刻”的静态模式对突变不敏感输入太短又丢失了趋势信息。常见做法是看数据的采样粒度与预测时效。15分钟采样的数据预测未来1小时输入窗口取前12个点3小时最稳妥预测未来4小时输入窗口可以放宽到前24个点6小时让模型有足够的上下文去判断云层移动的大方向。输出窗口则按业务需求来。电力调度要求的是未来1小时到4小时的功率曲线所以输出长度一般取4到16个点。输出超过16个点误差会指数级放大这是LSTM的马尔可夫性质决定的——它把预测值当成下一步的输入误差会不断累积。因此不建议在单模型上强行拉长预测链条后面第6章会讲怎么用多步策略去缓解。3. 数据处理是决定预测精度的第一道分水岭从SCADA原始表到滑窗样本的完整管线3.1 原始数据清洗剔除夜间和对零值时段否则模型会被“零”带偏很多人拿到电站数据就直接切开训练结果模型在夜间输出一堆毛刺。原因很简单夜间功率恒为0这些零值在数据集里占了近一半模型发现“输出0”就能拿到很低的损失于是学会了偷懒。所以第一步必须把夜间和零辐照度时段剔除掉只保留辐照度大于某阈值的样本。import pandas as pd import numpy as np df pd.read_csv(scada_pv_data.csv, parse_dates[timestamp]) df[irradiance] df[ghi].clip(lower0) # 全局水平辐照度 # 剔除夜间与近乎零辐照度时段阈值为 20 W/m^2 daytime_mask df[irradiance] 20 df_daytime df[daytime_mask].copy() # 剔除连续零功率超过30分钟的点这类点通常是停机或传感器故障 zero_runs (df_daytime[power] 0).astype(int).groupby( (df_daytime[power] ! 0).astype(int).cumsum() ).transform(sum) df_daytime df_daytime[zero_runs 2] # 15分钟粒度2个点等于30分钟这段代码里有几个参数值得注意。辐照度阈值20 W/m²不是拍脑袋定的日出日落前后的辐照度在10到50之间波动低于20时组件输出本身就极低对预测没有意义但会污染样本分布。连续零功率的窗口保留2个点原因是光伏组件在云遮住时功率骤降但不会是严格的0如果连续两个点都是0大概率是逆变器限电或通信中断属于故障样本而非气象样本。清洗之后务必做一步可视化检查把清洗前后的功率分布图画出来对比确认钟形曲线保留完整没有把傍晚的低功率点误删。血泪经验是阈值设太高会把春秋季节的弱光时段全部删光导致训练集缺失低辐照度场景雨天预测能力直接垮掉。3.2 滑窗切分与特征拼接造出LSTM需要的三维张量清洗后的数据仍然是一维时间序列。LSTM的输入要求是(样本数, 时间步数, 特征数)所以需要一个滑窗函数把时序数据切成长度为seq_len的样本片段并把未来pred_len个点的功率作为标签。def make_sequence_samples(data, seq_len12, pred_len4, step1): 构造滑窗样本。 data: DataFrame包含特征列和功率列 seq_len: 输入时间步数 pred_len: 预测未来几个点 step: 滑动步长建议1保留最大样本量 feature_cols [irradiance, temperature, humidity, wind_speed, power] X, y [], [] values data[feature_cols].values for i in range(len(values) - seq_len - pred_len 1): X.append(values[i:iseq_len, :]) # 过去seq_len步的全部特征 y.append(values[iseq_len:iseq_lenpred_len, -1]) # 未来pred_len步的功率 return np.array(X), np.array(y) X, y make_sequence_samples(df_daytime, seq_len12, pred_len4) print(样本形状:, X.shape, y.shape) # 样本形状: (15432, 12, 5) (15432, 4)这里一个关键的工程细节特征列里把历史功率也放进去了。LSTM的自回归特性就在这里体现——它不仅要看辐照度预报还要用功率的惯性来修正辐照度变化带来的偏差。如果你有数值天气预报NWP的辐照度预报数据可以替换掉irradiance列效果会更好但注意NWP数据的时效性一般是小时级和SCADA的分钟级数据做对齐时要按时间戳重采样。滑窗的step参数默认设为1不要为了减小数据集而设成2或3。光伏序列的相邻样本高度重叠步长设为2会丢失大量突变细节而降采样带来的信息损失比训练时间节省更得不偿失。如果你的数据量实在太大比如秒级数据先做15分钟聚合并插值补缺而不是减少滑窗步长。3.3 归一化的正确姿势训练集fit验证集和测试集只能transform归一化是另一个翻车重灾区。常见错法是把整个数据集合并后做MinMaxScaler.fit()这等于让模型在训练时就“偷看”了未来的最大值和最小值测试集的分布信息被泄漏进训练过程导致离线指标虚高上线后直接崩。from sklearn.preprocessing import MinMaxScaler feature_cols [irradiance, temperature, humidity, wind_speed, power] scaler MinMaxScaler() # 先划分再归一化按时间顺序切不能随机切 train_ratio, val_ratio 0.7, 0.15 train_len int(len(X_total) * train_ratio) val_len int(len(X_total) * val_ratio) X_train, X_val, X_test X_total[:train_len], X_total[train_len:train_lenval_len], X_total[train_lenval_len:] y_train, y_val, y_test y_total[:train_len], y_total[train_len:train_lenval_len], y_total[train_lenval_len:] # 关键scaler只用训练集拟合验证/测试集只transform scaler.fit(X_train.reshape(-1, X_train.shape[-1])) X_train_norm scaler.transform(X_train.reshape(-1, X_train.shape[-1])).reshape(X_train.shape) X_val_norm scaler.transform(X_val.reshape(-1, X_val.shape[-1])).reshape(X_val.shape) X_test_norm scaler.transform(X_test.reshape(-1, X_test.shape[-1])).reshape(X_test.shape) # y同样处理 y_scaler MinMaxScaler() y_scaler.fit(y_train.reshape(-1, 1)) y_train_norm y_scaler.transform(y_train.reshape(-1, 1)).reshape(y_train.shape)注意这里的二维转三维再转回来的操作因为MinMaxScaler只接受二维输入而LSTM需要三维所以必须reshape过渡。y_train归一化时单独用了另一个scaler这个细节很多人会漏掉——功率和辐照度的量纲不同如果共用同一个scaler功率的数值范围会被辐照度带偏。训练时用归一化后的数据评估时记得inverse_transform还原成功率值单位kW再算误差。4. 构建LSTM预测模型PyTorch实现与必调参数详解4.1 网络结构设计双层LSTM加全连接输出不加复杂注意力LSTM的模型结构不需要花哨。基于我在光伏场景的实践经验双层LSTM加一层全连接输出效果已经超过了市面上大部分加注意力机制的变体。原因是光伏序列的依赖模式比较固定——前一天同时段、前一小时趋势、当前辐照度突变——这些模式用两层的隐藏状态就能表达加注意力反而引入额外参数数据量不够时会过拟合。import torch import torch.nn as nn class PVLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_len, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.regressor nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_size // 2, output_len) ) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, (h_n, c_n) self.lstm(x) # 取最后一层最后一个时间步的隐藏状态 last_hidden h_n[-1] # (batch, hidden_size) out self.regressor(last_hidden) return out这里的结构有两个设计要点。使用h_n[-1]而不是lstm_out[:, -1, :]因为lstm_out的最后一维是每个时间步的输出取最后一个时间步等价于看整个序列编码后的最终状态而h_n[-1]是最后一个LSTM层的隐藏状态两者多数情况下语义相近但h_n[-1]更明确地表达“整个序列的总结”。regressor里加了一个中间全连接层而不是直接线性映射是为了让模型有足够的非线性去拟合功率的钟形曲线。4.2 训练配置Huber损失对抗突变早停防止过拟合损失函数的选择直接决定模型对突变的敏感度。MSE会对云遮导致的极端误差值给予过大的惩罚让模型宁可把预测值折中也不愿做出锐利的突变响应。Huber损失在误差小于阈值时是平方损失大于阈值时是线性损失对离群点天然稳健。import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset def train_model(model, X_train, y_train, X_val, y_val, epochs80, batch_size64, lr1e-3): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) train_dataset TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) val_dataset TensorDataset(torch.FloatTensor(X_val), torch.FloatTensor(y_val)) train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse) optimizer optim.Adam(model.parameters(), lrlr, weight_decay1e-5) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) criterion nn.HuberLoss(delta1.0) best_val_loss float(inf) patience_counter 0 for epoch in range(epochs): model.train() train_loss 0.0 for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() pred model(batch_x) loss criterion(pred, batch_y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * batch_x.size(0) train_loss / len(train_loader.dataset) model.eval() val_loss 0.0 with torch.no_grad(): for batch_x, batch_y in val_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) pred model(batch_x) loss criterion(pred, batch_y) val_loss loss.item() * batch_x.size(0) val_loss / len(val_loader.dataset) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_pv_lstm.pth) else: patience_counter 1 if patience_counter 10: break print(fEpoch {epoch1:03d}: train_loss{train_loss:.5f}, val_loss{val_loss:.5f})训练配置里有几个参数是光伏预测特有的调参经验。HuberLoss(delta1.0)的delta设1.0意味着归一化后的误差绝对值在1以内按平方处理超过1按线性处理——这个阈值正好对应功率突变前后误差明显放大的场景。clip_grad_norm_(max_norm1.0)是防止某个突变的batch把梯度撑爆光伏序列中偶发强突变会导致loss局部激增不裁剪梯度的话模型可能直接训练发散。ReduceLROnPlateau的factor设0.5、patience设5意思是验证损失连续5轮不下降就降一半学习率比固定学习率衰减更适合光伏这种非平稳数据。4.3 评估指标RMSE、MAE和R²之外必须看归一化RMSE光看RMSE没有意义因为不同装机容量的电站RMSE差好几倍。行业里常用的对比指标是归一化RMSEnRMSE计算公式为sqrt(mean((y_true - y_pred)^2)) / (P_installed)即用RMSE除以电站装机容量。这个值能跨电站比较模型优劣。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score def evaluate_model(model, X_test, y_test, y_scaler, installed_capacity): device torch.device(cuda if torch.cuda.is_available() else cpu) model.eval() with torch.no_grad(): pred model(torch.FloatTensor(X_test).to(device)).cpu().numpy() # 还原真实功率值 pred_real y_scaler.inverse_transform(pred.reshape(-1, 1)).reshape(pred.shape) y_test_real y_scaler.inverse_transform(y_test.reshape(-1, 1)).reshape(y_test.shape) rmse np.sqrt(mean_squared_error(y_test_real.flatten(), pred_real.flatten())) mae mean_absolute_error(y_test_real.flatten(), pred_real.flatten()) r2 r2_score(y_test_real.flatten(), pred_real.flatten()) nrmse rmse / installed_capacity print(fRMSE: {rmse:.2f} kW) print(fMAE: {mae:.2f} kW) print(fR²: {r2:.4f}) print(fnRMSE: {nrmse:.4f} ({nrmse*100:.2f}%)) return {rmse: rmse, mae: mae, r2: r2, nrmse: nrmse}nRMSE小于10%说明模型已经具备实用价值10%到15%是正常水平超过20%就要回头查数据处理了。评估时注意把model.eval()和torch.no_grad()都打开否则dropout层会在推理时随机丢弃节点导致多次评估结果波动。如果发现R²很高但nRMSE也很高大概率是测试集里包含了太多低功率时段R²被大量接近0的真值拉高了单独看R²会被误导。5. 常见问题排查五个让预测翻车的高频坑5.1 夜间数据没删干净模型学会了「躺平」现象夜间预测输出不为0而是在0附近随机抖动白天预测曲线整体偏低。测试集nRMSE异常高。原因数据清洗时删夜间数据用的是时间判断比如hour 6但不同季节日出时间差两小时固定时间截断会留下大量弱光时段。模型看到输入特征是低辐照度直接输出一个接近0的值就能拿到较低的损失于是权重被“教坏”了。解决不要按时间判断按辐照度判断。统一用irradiance 20做mask且把温度低于-10℃的极端天气样本一并剔除因为低温下组件IV曲线畸变功率与辐照度的线性关系不再成立。5.2 归一化时数据泄漏离线精度虚高现象训练时验证集loss很低但拿到新数据上预测误差比测试集高出一倍不止。原因对全量数据做了scaler.fit()验证集和测试集的最大值、最小值参与了归一化的统计计算。模型在训练时见过这些极值的“形状”等于把未来信息泄漏给了参数更新方向。解决严格按时间顺序划分训练、验证、测试集后再对训练集做fit其余两个集只做transform。同时检查MinMaxScaler的data_range_属性确认拟合所用数据的极值与原始真实范围一致。5.3 滑窗时随机shuffle把未来信息灌进了训练集现象模型验证集指标极好但前一天的数据预测第二天的数据时误差明显放大。原因滑窗生成样本后直接调了DataLoader(shuffleTrue)而样本本身就带着时间属性随机打乱后训练集里混入了测试时间段内的样本片段模型在训练时就见过了“未来的天气”。解决先按时间顺序切出训练、验证、测试区间只在训练集的DataLoader里开shuffleTrue验证和测试集保持时间顺序。判断代码是否正确可以检查torch.FloatTensor(X_train)里的时间戳列是否严格连续且小于验证集的最小时间戳。5.4 预测曲线整体滞后一个时间步现象绘制的预测曲线形状正确但相对于真实值整体向右平移了一个采样点像错了一位。原因LSTM学到的是“上一时刻的功率延续”本质上是把t时刻的功率值复制到t1时刻这种自回归式滞后在时序预测里非常常见。通常是因为输入窗口太短、噪声太大模型找不到比“复制”更强的特征。解决首先把输入窗口从12个点加长到24个点让模型看到更长的趋势然后检查损失函数是否被低功率样本主导尝试对训练样本按功率值加权最后考虑在特征里加入辐照度预报的数值外生变量是缓解滞后最有效的手段。5.5 辐照度传感器脏污导致坏点模型拟合到噪声上现象训练loss正常但验证loss在某几个时段突然飙升单独看那些时段的数据辐照度曲线有毛刺。原因电站现场的辐照度传感器长期风吹日晒表面落灰或鸟粪会导致读数偏低但功率曲线是真实的光伏输出两者出现矛盾。模型试图同时拟合辐照度和功率会被脏污时段的数据反复拉扯。解决增加一个前置的异常检测步骤用滑动窗口计算辐照度与功率的相关系数当窗口内相关系数低于0.7就标记为可疑样本人工确认后剔除。同时可以用功率除以辐照度得到“系统效率比”这个比值超出正常区间比如0.05到0.9的点直接删掉。6. 上线前的最后一公里多步预测验证与模型瘦身短期光伏预测的工程落地真正考验人的是部署阶段。网格化预测的误差累积是首要问题——单步预测nRMSE只有8%迭代预测4小时后可以飙到18%。解决思路是分而治之对第1小时用直接多步预测模型同时输出4个点对第2到4小时用迭代多步预测把前一小时的预测输出拼到输入里继续预测。实测中这种混合策略比纯迭代误差低3到5个百分点。部署前的验证不要用全量测试集指标糊弄自己要按天气类型分别统计晴天、多云、雨天、阵雨四种场景的nRMSE各自是多少。雨天如果nRMSE超过25%说明模型对低辐照度突变的学习不足需要补充特征或增加雨天样本的权重。这个分类验证脚本值得花一天时间写它能帮你精准定位模型的短板。模型瘦身方面LSTM本身参数不多一个两层、hidden_size64的模型只有几十万个参数不需要做量化剪枝。真正的性能瓶颈在预处理环节——建议把滑窗切片和归一化写成可并行的numpy版本或者用Polars替换pandas数据读取速度能提升5倍以上。推理阶段用batch推理一次处理当天全部采样点比单点预测快一个数量级。我自己的习惯是每次训练完把验证集上预测最差的那天画成图打印出来贴在工位上研究两天。有一次发现连续雨天预测偏低排查了两天后发现是清洗时把低辐照度样本删过头了导致雨天样本只有晴天的十分之一。调整数据配比后整体nRMSE从13.5%降到了10.8%。这个教训让我记住了光伏预测的瓶颈通常在数据侧而不在模型侧——模型是工具数据才是地基。希望这篇从数据清洗到部署验证的完整拆解能帮你少走几个月的弯路让你的LSTM预测模型真正跑起来、扛得住电站现场的各种天气刁难。本文还有配套的精品资源点击获取