ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LSTM光伏功率预测实战:数据清洗、特征工程与PyTorch实现

LSTM光伏功率预测实战:数据清洗、特征工程与PyTorch实现 简介一套基于LSTM的短期光伏预测毕业设计项目面向计算机、人工智能、自动化等专业学生及开发者可用于毕设、课程设计或项目初期演示。项目包含Python源码与光伏数据集覆盖数据预处理、模型构建、训练预测等环节代码测试可运行支持二次修改。压缩包共402个文件约12.46MB除核心Python代码与数据集外还有Java、JavaScript、HTML等页面与辅助脚本以及SQL、配置文件和详细文档目录清晰便于按需查阅。目前已有2668人学习下载内容预览中包含详细文档、统计页面和样式资源可帮助理解系统前后端协作。整体适合希望快速上手LSTM时序预测、完成光伏功率实验的初学者与毕设学生。1. 短期光伏预测模型为什么选 LSTM非平稳时序需要一个带记忆的网络做过光伏电站数据的人都有体会功率曲线白天像锯齿、晴天像拱桥、阴天像噪声你要预测的不是一个稳定的数而是一段会随云层快速变化的序列。短期光伏预测这件事本质上是在历史功率和气象之间找记忆关联而 LSTM 的门控结构正好让模型学会“阳光好的时候功率该涨云来了之后别追着跌”这类长距离依赖。相比 ARIMA 和普通全连接网络LSTM 不需要人工指定滞后阶数输入几小时窗口就能自动提取日周期形状这也是手里有 python 源码和光伏数据集时最快能出效果的方向。它适合两类人一是毕业设计需要完整跑通训练、评估、可视化流程的学生二是刚接手电站功率预测、想先做一个可解释基线模型的工程师。这套流程不依赖任何私有接口数据用电站 SCADA 导出的 CSV 就能跑模型层用 PyTorch 实现几十行代码就能得到一条像样的预测曲线。后面我会按数据清洗、特征工程、滑窗、模型搭建、训练调参的顺序把完整链路拆开讲最后落在光伏预测最容易翻车的问题上。2. 先把数据收拾干净光伏数据集的时间分辨率与特征工程2.1 光伏数据集的原始字段和常见格式光伏电站的 SCADA 系统一般按 5 分钟或 15 分钟粒度采集一条记录字段大致包括逆变器有功功率、组件背板温度、环境温度、湿度、水平辐照度、风速风向。公开数据集里最常见的格式是 CSV 或 Excel时间列是 ISO 字符串。拿到数据的第一步不是建模而是先把时间列解析成 datetime 类型并按时间排序因为后续所有滑窗操作都依赖时间顺序索引乱掉会让数据泄漏问题变得完全不可控。import pandas as pd import numpy as np df pd.read_csv(pv_data.csv, parse_dates[timestamp]) df df.sort_values(timestamp).reset_index(dropTrue) print(df.head()) print(df.dtypes) print(时间跨度, df[timestamp].min(), -, df[timestamp].max()) print(采样间隔统计) print(df[timestamp].diff().dropna().value_counts().head())这段代码里parse_dates会把时间字符串转成 Pandas 的 datetime 类型sort_values保证数据按时间递增。最后的diff().value_counts()用来检查采样间隔是否均匀正常情况应该只有一种间隔值比如 15 分钟如果出现多种间隔说明原始数据里有丢点或重复采集需要先处理采样频率问题否则后面构造窗口时同一个窗口里可能混入不同时间粒度的样本模型会把时间步长理解成不固定的物理含义。2.2 数据清洗缺测、夜间零值、突变点光伏数据有三类脏数据来源通讯中断产生的小段缺失表现为 NaN夜间辐照很低但功率不为零通常是逆变器待机功耗或数据误码云层边缘出现功率骤降又回升看起来像噪声但其实是真实物理现象不能当成异常削掉。我见过不少人把突变点用中值滤波抹平结果模型学不到最关键的天气变化信息预测结果在突变日直接失效。missing df.isna().sum() print(missing[missing 0]) # 线性插值只处理短于 6 个采样点的缺失 df df.interpolate(methodlinear, limit6, limit_areainside) # 夜间功率清零 df[hour] df[timestamp].dt.hour df[timestamp].dt.minute / 60 night_mask (df[hour] 5) | (df[hour] 19) df.loc[night_mask, power] 0 # 功率负值与超出额定功率 1.2 倍的异常点 df.loc[df[power] 0, power] 0 df.loc[df[power] df[rated_power] * 1.2, power] df[rated_power]interpolate的limit6表示最多连续填补 6 个缺失点超过就保留 NaN后续窗口切分时遇到 NaN 直接丢弃避免把一段长时间通讯中断硬擦成平滑曲线。夜间功率清零不是拍脑袋光伏组件在无辐照时输出本来就接近零保留待机功率反而会给模型一个错误的“夜间也在发电”的信号。超额定功率 1.2 倍的数据在逆变器限功率运行或更换组件后可能出现这种点不是正常出力直接压回额定值比删掉更好因为删掉会破坏时间连续性。2.3 特征构造时间戳编码与辐照度滞后特征模型输入窗口里时间戳本身不能直接喂给网络需要先转换成模型能理解的数值特征。我常用两组特征周期性时间特征把小时和一年中的第几天做正弦余弦编码让 23 点和 0 点的距离变近辐照度滞后特征辐照仪响应也有滞后把前 1 到 2 个时刻的辐照度及其变化率放进去能帮助模型判断云层是正在靠近还是已经过去。def add_time_features(df): hour df[timestamp].dt.hour df[timestamp].dt.minute / 60 day df[timestamp].dt.dayofyear df[hour_sin] np.sin(2 * np.pi * hour / 24) df[hour_cos] np.cos(2 * np.pi * hour / 24) df[day_sin] np.sin(2 * np.pi * day / 365) df[day_cos] np.cos(2 * np.pi * day / 365) return df def add_lag_features(df, cols, lags(1, 2)): for c in cols: for lag in lags: df[f{c}_lag{lag}] df[c].shift(lag) return df df add_time_features(df) df add_lag_features(df, [power, irradiance]) df df.dropna().reset_index(dropTrue)shift产生的滞后值天然会在每天开头产生 NaN因为前一天的最后一个值不能作为今天第一个值的滞后信息所以最后统一dropna()。这里有个容易忽略的点如果只用历史功率做预测模型能学到惯性却学不到突变有条件拿到数值天气预报辐照度的话把未来时刻的预报辐照度作为外部特征拼进输入窗口预测精度会明显提高。这是从“能跑通”到“能落地”的关键一步也是后面调参时最值得投入的方向。3. 用 PyTorch 把 LSTM 光伏预测模型跑起来网络结构、滑窗与训练参数3.1 滑窗用过去 12 个点预测未来 6 个点短期光伏预测通常用序列到序列的思路给模型过去几小时的功率和气象特征让它一次吐出未来几小时的功率曲线。我用seq_len12在 15 分钟粒度下对应过去 3 小时pred_len6对应未来 1.5 小时。这个配置兼顾并网申报的时效和模型难度预测时长再拉长到 4 小时以上时单靠 LSTM 的误差会明显变大需要换结构。def make_sequences(data_x, data_y, seq_len12, pred_len6): X, Y [], [] for i in range(len(data_x) - seq_len - pred_len 1): X.append(data_x[i:i seq_len]) Y.append(data_y[i seq_len:i seq_len pred_len]) return np.array(X), np.array(Y)这段滑动窗口代码里X的形状是样本数seq_len特征数Y的形状是样本数pred_len。注意这里没有做样本去重相邻窗口共享大量历史数据这是时序预测的正常做法但正因如此划分训练集和验证集时绝对不能随机洗牌必须按时间顺序切。我见过一个实际项目里用了train_test_split(random_state42)验证集 loss 低到离谱模型一上线立刻失效原因就是验证集里混进了训练样本的“近亲”模型等于开卷考试。3.2 网络结构hidden_size、num_layers 怎么定LSTM 做光伏预测的网络结构不需要很复杂。我的基线方案是一层 LSTM 加一层全连接hidden_size64。数据量超过几万条时可以加到两层但num_layers每加一层训练时间几乎翻倍收益却很小。光伏功率预测的常见经验是第一层 LSTM 学日周期的形状第二层学天气突变的局部特征两层足够三层以上在中小数据集上基本都是过拟合。import torch import torch.nn as nn class PVLSTM(nn.Module): def __init__(self, input_size, hidden_size64, num_layers1, pred_len6, 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, ) self.fc nn.Linear(hidden_size, pred_len) def forward(self, x): out, _ self.lstm(x) last out[:, -1, :] return self.fc(last)这里用的是“直接多步预测”策略只取 LSTM 最后一个时间步的隐藏状态一次输出未来 6 个点而不是把预测值一步步递归回去。递归预测的好处是模型简单但每一步都会累积误差预测 6 个点可能后 3 个点基本没意义。直接多步输出虽然需要全连接层有足够的输出维度但对短期预测来说误差更可控也更容易训练。3.3 训练归一化、损失函数、学习率与 early stopping训练前先做归一化这一步的坑最多。标准做法是用训练集的统计量做StandardScaler然后拿同一个 scaler 去 transform 验证集和测试集绝对不能用全量数据的统计量否则等于把未来的分布泄漏给模型。from sklearn.preprocessing import StandardScaler from torch.utils.data import TensorDataset, DataLoader scaler_x StandardScaler().fit(X_train.reshape(-1, X_train.shape[2])) scaler_y StandardScaler().fit(y_train) X_train scaler_x.transform( X_train.reshape(-1, X_train.shape[2])).reshape(X_train.shape) X_val scaler_x.transform( X_val.reshape(-1, X_val.shape[2])).reshape(X_val.shape) y_train scaler_y.transform(y_train) y_val scaler_y.transform(y_val) X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32) X_val_t torch.tensor(X_val, dtypetorch.float32) y_val_t torch.tensor(y_val, dtypetorch.float32) train_loader DataLoader( TensorDataset(X_train_t, y_train_t), batch_size64, shuffleTrue ) model PVLSTM(input_sizeX_train.shape[2], hidden_size64, num_layers1, pred_len6) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5) criterion nn.MSELoss()这里batch_size64是个稳妥起步值数据量小就降到 32。shuffleTrue只用在训练集验证集绝不打乱顺序。损失函数选 MSE它会对大幅误差施加平方惩罚适合功率预测这类不希望出现极端偏差的场景。光伏功率预测不推荐直接用 MAE因为 MAE 对峰值误差不敏感模型会倾向输出保守的中间值预测曲线看起来“平”得厉害。best_val float(inf) patience_counter 0 for epoch in range(100): model.train() epoch_loss 0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() epoch_loss loss.item() model.eval() with torch.no_grad(): val_pred model(X_val_t) val_loss criterion(val_pred, y_val_t).item() scheduler.step(val_loss) if val_loss best_val: best_val val_loss patience_counter 0 torch.save(model.state_dict(), best_model.pt) else: patience_counter 1 if patience_counter 10: break训练循环里最值得关注的是两个细节。clip_grad_norm_把梯度范数限制在 1.0防止 LSTM 在长序列反向传播时梯度爆炸这是 LSTM 训练的标准手段。early stopping 的 patience 设为 10意思是验证集 loss 连续 10 个 epoch 不下降就停止训练并保留验证集 loss 最低的权重。用torch.save(model.state_dict(), best_model.pt)而不是保存整个模型这样模型结构改动时不需要重新训练旧权重。超参数推荐起始值调整方向seq_len12数据粒度粗就减小pred_len6预测时长越长误差越大hidden_size64数据量大可加到 128num_layers12超过 2 层中小数据集易过拟合batch_size64数据少降到 32初始学习率1e-3loss 震荡就降到 3e-4patience10验证集噪声大就加大4. 光伏预测调参避坑5 个让模型悄悄翻车的问题与排查4.1 loss 降了但预测曲线整体变平现象验证集 loss 一直下降画出预测曲线却发现晴天中午的功率峰值全被削平预测结果像一条缓慢起伏的土坡。原因MSE 对占多数的正常点负责极端峰值在样本里占比小模型没有足够动力去拟合少数高功率时刻加上归一化后输出被压缩到常见功率范围峰值属于少数样本误差权重太低。解决改用加权 MSE 或 Huber loss对预测误差超过阈值的样本加大权重让模型把注意力放到峰值上。另一个更有效的办法是在输入特征里保留当前时刻辐照度让模型知道大峰值出现的外部条件这比在损失函数上做文章更直接。4.2 随机划分样本导致时间泄漏现象训练集和验证集用train_test_split随机划分验证集 loss 小得让人觉得模型已经完美但真实预测时误差翻倍。原因光伏数据自相关性强相邻时刻的样本几乎一样。随机划分让验证集里混进了训练样本时间窗口高度重叠的“近亲”模型记住训练样本就等于记住了验证样本。解决严格按时间顺序划分前 80% 做训练集、后 20% 做验证集如果要做交叉验证用 scikit-learn 的TimeSeriesSplit不要用KFold。这是排在第一位的规则数据泄漏会让后面所有调参工作失去意义。4.3 归一化统计量混入未来信息现象先用全量数据计算均值方差再切分训练验证集验证集表现很好但模型上线后误差很大。原因归一化参数如果包含验证集和测试集的统计量相当于让模型提前知道了未来数据的分布范围测试时自然显得更准。解决只对训练集fit再用同一个 scaler 去transform验证集和测试集。这一点和第 4.2 条一起检查是时序预测项目里最常见的两个隐蔽问题。4.4 夜间时段把平均误差带偏现象整体 RMSE 看起来不错按小时分时段一算夜间误差接近零白天误差其实远高于整体指标。原因夜间功率恒为零或极小占了全天约一半的时间平均指标被大量零值稀释模型白天的真实误差被掩盖了。解决评估时分白天和夜间两组分别计算指标或者用辐照度阈值筛掉夜间样本。更严格一点的做法是只看早上 6 点到晚上 18 点的样本算 RMSE并在论文里说明这个评估口径评审和工程验收时都更有说服力。4.5 天气突变日模型集体失效现象晴天、阴天预测都稳定偏偏在阵雨、多云快速转换的日子误差飙升而这种日子恰恰最需要准确预测。原因LSTM 学的是历史统计规律没有实时云图信息辐照度突变本身是信息不足的问题模型无法从历史功率中推断未来几分钟的云层运动。解决引入外源气象预报特征比如未来 1 小时的总云量预报工程上更现实的折中做法是检测辐照度变化率超过阈值时让模型输出一个低置信度标记调度侧看到标记后切换保守策略。这一步是把毕设项目推向真实落地价值的关键也是可以写进论文的创新点。5. 滚动预测与误差分段统计让模型真正能拿去用训练结束后的模型默认只能预测一个固定窗口但实际使用中往往需要连续预测一整天的出力曲线。滚动预测的做法是把模型输出拼接回输入窗口末尾丢掉窗口最前面的旧数据让新预测值参与下一次预测。下面是模型加载和滚动预测的核心逻辑。def rolling_predict(model, init_seq, scaler_x, scaler_y, steps48): model.eval() seq init_seq.copy() preds [] with torch.no_grad(): for _ in range(steps): x torch.tensor(seq, dtypetorch.float32).unsqueeze(0) out model(x) pred scaler_y.inverse_transform( out.numpy().reshape(-1, out.shape[-1])) preds.append(pred) # 用预测值更新窗口 new_row np.concatenate([pred[0], seq[-1, 1:]])[-1:] seq np.roll(seq, -1, axis0) seq[-1] new_row return np.concatenate(preds)这段代码里有个容易出错的细节滚动预测时输入的功率特征用的是模型自己的预测值这会累积误差预测步数越多偏差越大所以滚动预测只适合步数不超过 48 个点12 小时的场景。后面的new_row构造逻辑必须严格对齐特征列顺序否则模型输入的含义就变了。每次预测后要做scaler_y.inverse_transform把结果还原成物理功率值画图和评估都用还原后的值用归一化值直接算误差看不出来量级问题。误差分析我通常按天气类型分三组来算指标晴天、多云、雨天。只看整体 RMSE 会掩盖模型在多云天的失败按组计算后你会发现晴天 RMSE 很小、多云天很大。如果项目要写论文用表格列出分组误差再附上一张典型日的预测曲线对比图模型的优缺点就一目了然答辩时也更容易讲清楚。这几年的习惯是每次跑完实验先画预测曲线再按小时分段算误差最后才看整体指标因为平均值会骗人。光伏预测是一个“数据质量决定上限”的方向把时间顺序、归一化、夜间时段这三件事管好LSTM 就已经能超过一大半的基线模型。希望这些踩坑经验能帮你在毕业设计或实际项目中少走一点弯路尽快把预测曲线稳定下来。本文还有配套的精品资源点击获取
返回列表