LSTM单变量光伏功率预测实战:零值处理与滑动窗口设计
发布时间:2026/9/28 14:43:21来源:尧图网络
简介本资源是一套基于LSTM神经网络的短期光伏发电功率预测完整实现方案面向计算机、人工智能、自动化及能源类相关专业的在校学生、教师与工程技术人员尤其适合作为毕业设计、课程设计或科研入门项目。包内共11个文件含6个Jupyter Notebook含光伏单变量预测、园区实测数据建模、储能协同框架等核心实验、1个Python工具脚本、1个Excel实测数据集、2张关键流程图规则集与SOC曲线以及结构清晰的README.md说明文档总大小3.89MB轻量易部署。已有201人下载学习代码均经实际运行验证通过答辩平均分达96分具备良好可复现性与教学示范性。用户可直接复现LSTM建模全流程深入理解时间序列特征处理、模型训练调参、多场景预测对比及结果可视化方法并可基于现有框架快速拓展至负荷预测或混合能源系统建模。1. 这不是调包跑个 loss 就完事的 LSTM它用真实园区 7 天实测数据跑通超短期光伏功率预测单变量模型 RMSE 压到 0.082归一化后附完整训练-验证-部署链路和答辩级文档你手头那份“LSTM 光伏预测”教程是不是还在用 synthetically generated sine wave 模拟光照或者拿某公开气象 API 的粗糙辐照数据凑数这套 PV-Predict-main.zip 不是 demo是真刀真枪跑过园区逆变器 SCADA 数据的毕设落地项目——SOC_1101-1107.xlsx 里存的是 2023 年 11 月 1 日至 7 日、每 15 分钟一采的实时光伏出力kW、环境温度℃、组件背板温度℃、水平面总辐照度W/m²原始记录连 clearoutside_url.py 都是为自动清洗爬取的实时天气 URL 设计的。它不讲抽象理论只解决三个硬问题怎么把带突变、零值、夜间断点的光伏曲线喂给 LSTM 而不崩怎么用单变量仅历史功率在无气象输入时仍保持可用精度怎么把训练好的模型塞进 .ipynb 里一键复现从数据加载→滑动窗口构造→归一化→训练→反归一化→误差可视化全链路。适合正在写课设/毕设、需要可答辩、可演示、可改参数的 Python 工程师也适合想搞懂“为什么我的 LSTM 在光伏上总 overfit”的实战派——它没用 PyTorch Lightning 封装所有 torch.nn.LSTM 层、optimizer.step()、scheduler.step() 都裸写在用园区的数据测试光伏预测-单变量.ipynb 里连 hidden_size64、num_layers2、seq_len96即 24 小时历史这些关键数字都标在注释里。别被“短期预测”四个字骗了它真正价值在于告诉你当数据只有功率序列、没有辐照/温度、且存在大量夜间零值时LSTM 的 input_size 怎么设、loss 怎么加权、early stopping 怎么防过拟合。2. 从原始 Excel 到 LSTM 输入张量数据预处理链路拆解与滑动窗口构造逻辑2.1 原始数据结构解析为什么 SOC_1101-1107.xlsx 不能直接丢进 DataLoader打开 SOC_1101-1107.xlsx你会看到四列Timedatetime 格式如2023/11/01 00:00、Power_kW实测功率含大量 0 值、Temp_C环境温度、GHI_Wm2水平面总辐照度。注意这不是标准时间序列 CSV——Time列存在跳点如某天 13:15 缺失、Power_kW在夜间17:00–05:00恒为 0但 LSTM 对连续等间隔输入敏感。直接用 pandas.read_excel 读入后不做处理就切片会导致seq_len96时张量维度错乱。项目中clearoutside_url.py名字有误导性它实际承担两个任务一是用pandas.date_range补全缺失时间戳插值用前向填充因光伏功率不可外推二是将Power_kW中非零值做 min-max 归一化X_norm (X - X.min()) / (X.max() - X.min())而零值单独标记为-1非归一化值避免 LSTM 把夜间静默误学为“低功率状态”。这步在用园区的数据测试光伏预测-Copy1.ipynb第 3 cell 显式写出# 清洗并补全时间序列关键 df pd.read_excel(SOC_1101-1107.xlsx, parse_dates[Time]) df df.set_index(Time).resample(15T).first().fillna(methodffill) # 15分钟等频重采样 df[Power_kW] df[Power_kW].apply(lambda x: -1 if x 0 else x) # 零值打标 # 归一化非零功率注意max/min 只取非零样本 nonzero_power df[df[Power_kW] ! -1][Power_kW] df.loc[df[Power_kW] ! -1, Power_norm] (nonzero_power - nonzero_power.min()) / (nonzero_power.max() - nonzero_power.min()) df[Power_norm] df[Power_norm].fillna(-1) # 零值位置仍为 -1提示resample(15T)是强制对齐的关键15T表示 15 分钟频率比15Min更稳定fillna(methodffill)用前向填充而非线性插值因光伏功率突变常见云层遮挡线性插值会伪造平滑过渡。2.2 滑动窗口构造为什么 seq_len96 且 target_step1以及零值掩码如何参与 loss 计算LSTM 输入要求三维张量(batch_size, seq_len, features)。本项目features1单变量预测seq_len96对应 24 小时历史96×15min24htarget_step1表示预测下一个 15 分钟点。难点在于当窗口内含-1夜间零值时若直接丢弃该窗口训练样本锐减 40%若保留则-1会污染梯度。解决方案在用园区的数据测试光伏预测-单变量.ipynb第 5 cell 实现def create_sequences(data, seq_len96, target_step1): xs, ys [], [] for i in range(len(data) - seq_len - target_step 1): x data[i:(i seq_len)] y data[i seq_len target_step - 1] # 仅当 y ! -1即预测点非夜间才保留该样本 if y ! -1: # x 中的 -1历史零值保留但训练时 mask 掉其 loss 贡献 xs.append(x) ys.append(y) return np.array(xs), np.array(ys) # 构造训练集仅用 Power_norm 列 X_train, y_train create_sequences(df[Power_norm].values, seq_len96, target_step1) # X_train.shape (N, 96, 1), y_train.shape (N,)这里create_sequences的核心逻辑是只过滤 y目标值是否为有效功率点不筛 x历史窗口。x 中的-1作为有效历史状态表示“过去 24 小时都在夜间”让 LSTM 学习“连续零值后大概率仍为零”的时序模式。后续 loss 计算时再用 mask 区分贡献# 训练循环中的 loss 计算简化版 criterion nn.MSELoss(reductionnone) y_pred model(X_batch) # y_pred.shape (batch, 1) loss_per_sample criterion(y_pred.squeeze(), y_batch) # (batch,) # 构造 masky_batch ! -1 的位置为 1否则为 0 mask (y_batch ! -1).float() loss (loss_per_sample * mask).sum() / mask.sum() # 仅对有效点计算 loss注意mask.sum()防止除零reductionnone是必须的否则无法逐样本加权。这是本项目能用单变量跑出 RMSE0.082 的关键设计——它没回避零值而是把零值转化为可学习的时序状态。2.3 归一化与反归一化的边界陷阱为什么 max/min 必须用训练集全局统计量新手常犯错误对整个SOC_1101-1107.xlsx做一次归一化再按 8:2 切分训练/测试集。这会导致数据泄露——测试集的 min/max 已参与归一化参数计算。正确做法是仅用训练集样本计算power_min,power_max测试集用同一组参数反推。项目在用园区的数据测试光伏预测-Copy1.ipynb第 4 cell 明确分离# 假设 train_end_idx int(len(df)*0.8) train_df df.iloc[:train_end_idx] test_df df.iloc[train_end_idx:] # 仅从 train_df 计算归一化参数 train_power train_df[train_df[Power_kW] ! 0][Power_kW] # 非零功率 power_min, power_max train_power.min(), train_power.max() # 应用于全量 df含 test_df df[Power_norm] df[Power_kW].apply( lambda x: -1 if x 0 else (x - power_min) / (power_max - power_min) )反归一化时同样严格# 预测后反归一化仅对非 -1 值操作 y_pred_real np.where(y_pred_norm -1, 0, y_pred_norm * (power_max - power_min) power_min)提示power_min/power_max是 scalar不是数组np.where比pd.Series.mask更快适合 numpy array 场景。3. LSTM 模型构建与训练从 torch.nn.LSTM 到早停策略的完整实现细节3.1 模型结构设计为什么 hidden_size64、num_layers2以及 dropout 放在哪一层项目采用最简 LSTM 架构单向、两层、64 隐藏单元、全连接输出层。hidden_size64是经验平衡点——小于 32 时欠拟合RMSE 0.12大于 128 时显存溢出RTX 3060 12G 下 batch_size32 会 OOM。num_layers2是为捕获长周期模式如阴天持续 2 天后的功率恢复但第二层 LSTM 的 dropout 必须设为 0.2否则梯度消失严重。模型定义在用园区的数据测试光伏预测-单变量.ipynb第 6 cellclass PV_LSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 # 仅多层时启用 dropout ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x.shape (batch, seq_len, 1) lstm_out, _ self.lstm(x) # lstm_out.shape (batch, seq_len, hidden_size) # 取最后一个时间步的输出 last_output lstm_out[:, -1, :] # (batch, hidden_size) return self.fc(last_output) # (batch, 1)注意batch_firstTrue是必须的否则x维度需为(seq_len, batch, features)与create_sequences输出不匹配dropout仅在num_layers 1时生效因单层 LSTM 无内部层间连接。3.2 训练循环与优化器配置为什么用 AdamW 而非 Adamweight_decay1e-5 的作用AdamW 是 Adam 的改进版能更好抑制过拟合。项目设置lr0.001weight_decay1e-5L2 正则betas(0.9, 0.999)。关键细节在用园区的数据测试光伏预测-Copy1.ipynb第 7 cell 的训练循环model PV_LSTM(input_size1, hidden_size64, num_layers2) optimizer torch.optim.AdamW(model.parameters(), lr0.001, weight_decay1e-5) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.9) # 每 10 epoch 降 lr for epoch in range(100): model.train() total_loss 0 for X_batch, y_batch in train_loader: X_batch X_batch.float().to(device) # (batch, 96, 1) y_batch y_batch.float().to(device) # (batch,) optimizer.zero_grad() y_pred model(X_batch).squeeze() # (batch,) loss masked_mse_loss(y_pred, y_batch) # 自定义带 mask 的 loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 梯度裁剪防爆炸 optimizer.step() scheduler.step() # 验证逻辑略torch.nn.utils.clip_grad_norm_是必须的——LSTM 易梯度爆炸max_norm1.0经实测最优0.5过激2.0无效。3.3 早停Early Stopping与验证集设计为什么 val_loss 连续 5 epoch 不降就停验证集从训练集末尾切出 10%非随机打乱保持时序性。早停条件val_loss连续 5 个 epoch 未下降且当前val_loss小于历史最小值1e-4防浮点抖动误判。代码在第 8 cellbest_val_loss float(inf) patience_counter 0 patience 5 for epoch in range(100): # ... 训练 ... # 验证 model.eval() val_loss 0 with torch.no_grad(): for X_val, y_val in val_loader: X_val X_val.float().to(device) y_val y_val.float().to(device) y_pred_val model(X_val).squeeze() val_loss masked_mse_loss(y_pred_val, y_val).item() val_loss / len(val_loader) if val_loss best_val_loss - 1e-4: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_model.pth) # 保存最优模型 else: patience_counter 1 if patience_counter patience: print(fEarly stopping at epoch {epoch}) break注意masked_mse_loss同样使用y_val ! -1的 mask确保验证 loss 与训练 loss 可比。4. 预测结果可视化与误差分析从 plot_curve 到 RMSE/MAE 计算全流程4.1 预测曲线绘制为什么用 matplotlib 而非 plotly以及如何对齐时间轴项目用matplotlib.pyplot绘图非交互式因部署环境常无浏览器。关键难点预测结果y_pred_real是 numpy array长度为len(test_df) - seq_len而test_df的 index 是 datetime。需用test_df.index[seq_len:]对齐横轴。代码在用园区的数据测试光伏预测-Copy1.ipynb第 10 cell# 获取测试集真实值去归一化后 y_true_real [] for i in range(seq_len, len(test_df)): true_val test_df.iloc[i][Power_kW] y_true_real.append(true_val) y_true_real np.array(y_true_real) # 绘制 plt.figure(figsize(12, 5)) plt.plot(test_df.index[seq_len:], y_true_real, labelTrue, alpha0.7) plt.plot(test_df.index[seq_len:], y_pred_real, labelPredicted, alpha0.7, linestyle--) plt.xlabel(Time) plt.ylabel(Power (kW)) plt.title(PV Power Prediction: True vs Predicted) plt.legend() plt.grid(True) plt.xticks(rotation30) plt.tight_layout() plt.show()提示test_df.index[seq_len:]确保起点对齐——第seq_len行对应第一个可预测点因需前 96 点历史。4.2 误差指标计算RMSE/MAE/MAPD 的手撕实现与业务含义项目计算三个指标RMSE均方根误差对大误差敏感反映模型稳定性MAE平均绝对误差直观易懂单位同 kWMAPD平均绝对百分比误差( |pred-true| / true ) * 100%但 true0 时跳过光伏夜间为 0此时 MAPD 无意义。def calc_metrics(y_true, y_pred): # 过滤掉 true0 的点夜间 mask y_true ! 0 y_true_f y_true[mask] y_pred_f y_pred[mask] rmse np.sqrt(np.mean((y_true_f - y_pred_f) ** 2)) mae np.mean(np.abs(y_true_f - y_pred_f)) mapd np.mean(np.abs((y_true_f - y_pred_f) / y_true_f)) * 100 return rmse, mae, mapd rmse, mae, mapd calc_metrics(y_true_real, y_pred_real) print(fRMSE: {rmse:.3f} kW | MAE: {mae:.3f} kW | MAPD: {mapd:.2f}%)项目实测结果RMSE0.082归一化后对应原始数据约1.2 kW因power_max≈14.6 kWMAPD4.7%符合超短期预测1h工程要求5%。4.3 误差分布直方图为什么用 seaborn.histplot 而非 plt.histseaborn.histplot自动处理 bin 数量与密度且支持statdensity显示概率密度便于观察误差是否近似正态。代码import seaborn as sns errors y_true_real - y_pred_real plt.figure(figsize(10, 4)) sns.histplot(errors, kdeTrue, statdensity, bins50, colorskyblue) plt.xlabel(Prediction Error (kW)) plt.ylabel(Density) plt.title(Error Distribution) plt.axvline(0, colorred, linestyle--, alpha0.7) plt.grid(True, alpha0.3) plt.show()若直方图右偏正误差多说明模型系统性低估如云层突袭未捕获左偏则高估如晴天功率爬升过快。本项目直方图近对称验证了模型无系统性偏差。5. 避坑指南5 个真实踩过的坑与血泪解决方案5.1 现象训练 loss 从 0.1 降到 0.001 后突然 NaN验证 loss 为 inf原因LSTM 输出未加 sigmoid/tanh 激活且归一化后Power_norm范围是[0,1]但模型输出可能超出此范围如y_pred 1或0反归一化时(y_pred * (max-min) min)产生极大值后续 MSE loss 计算溢出。解决在模型forward最后加torch.clamp限制输出范围return torch.clamp(self.fc(last_output), min0.0, max1.0) # 强制 [0,1]5.2 现象预测曲线平滑如 sine wave完全丢失功率突变如云层遮挡原因seq_len过小如设为 24 即 6 小时LSTM 无法捕获长周期天气模式或hidden_size过小特征提取能力不足。解决seq_len必须 ≥9624 小时hidden_size≥64并在数据预处理时对Power_kW做差分df[Power_diff] df[Power_kW].diff()后归一化让 LSTM 学习变化率而非绝对值。5.3 现象torch.cuda.OutOfMemoryError即使 batch_size1原因DataLoader的num_workers0时每个 worker 加载数据会复制一份df导致内存翻倍或X_train未转为torch.float32默认float64占双倍显存。解决DataLoader设num_workers0X_train torch.tensor(X_train, dtypetorch.float32)训练前加torch.cuda.empty_cache()。5.4 现象验证 loss 持续下降但预测曲线与真实值完全不重合原因验证集y_val未用与训练集相同的power_min/power_max反归一化导致y_val值域错误loss 计算失效。解决验证阶段y_val必须用训练集power_min/power_max反归一化后再计算 loss而非用验证集自身统计量。5.5 现象clearoutside_url.py执行报urllib.error.HTTPError: HTTP Error 403原因该脚本原为爬取某天气网站但网站已加反爬项目中它实际未被.ipynb调用仅作备用。解决直接注释掉clearoutside_url.py相关调用所有实验均基于SOC_1101-1107.xlsx离线数据无需联网。6. 进阶技巧如何用该框架快速适配你的光伏电站数据含完整迁移 checklist6.1 数据格式迁移 checklist三步完成自有数据接入你的电站数据若为 CSV/数据库导出按此 checklist 适配时间列标准化确保首列为Time格式为YYYY-MM-DD HH:MM如2024-03-15 08:15用pd.to_datetime(df[Time])转换功率列清洗重命名功率列为Power_kW将夜间/故障零值统一设为0非 NaN其他异常值如负值、超限值用df[Power_kW] df[Power_kW].clip(lower0, upperdf[Power_kW].quantile(0.99))截断文件替换将清洗后 CSV 保存为SOC_1101-1107.xlsx覆盖原文件修改用园区的数据测试光伏预测-单变量.ipynb中pd.read_excel路径即可。提示若你的数据采样间隔非 15 分钟如 5 分钟需调整seq_lenseq_len (小时数 × 60) // 采样间隔分钟数如 5 分钟采样24 小时需seq_len288。6.2 模型轻量化部署如何把 .pth 模型转为 ONNX 并用 OpenCV DNN 加载为嵌入边缘设备如园区网关需导出 ONNX# 导出 ONNX在训练完后执行 dummy_input torch.randn(1, 96, 1) # batch1, seq_len96, features1 torch.onnx.export( model, dummy_input, pv_lstm.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )然后用 OpenCV DNN 模块加载无需 PyTorch 环境import cv2 net cv2.dnn.readNetFromONNX(pv_lstm.onnx) # 准备输入shape(1, 96, 1)dtypefloat32 blob cv2.dnn.blobFromImages([X_test[0]], 1.0, (1, 96), (0, 0, 0), swapRBTrue, cropFalse) net.setInput(blob) pred net.forward() # pred.shape (1, 1)6.3 多变量预测扩展如何加入温度/辐照特征而不崩模型若你有Temp_C和GHI_Wm2只需三处修改数据预处理features3X_train构造时拼接三列X_train np.stack([power_norm, temp_norm, ghi_norm], axis-1)模型输入PV_LSTM(input_size3)归一化Temp_C和GHI_Wm2各自用训练集 min/max 归一化非共享参数。但注意多变量时seq_len可降至 4812 小时因气象特征提供强先验减少对长历史依赖。从那以后我每次接手新光伏项目都强制走一遍这个 checklist先用SOC_1101-1107.xlsx跑通 baseline再换数据、调参数、加特征——它像一把标尺让我一眼看出是数据问题还是模型问题。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网