☰
LSTM光伏功率预测实战:数据清洗、特征工程与PyTorch实现
2026/10/2 20:57:42 网站建设 项目流程

简介:一套基于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(drop=True) 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(method="linear", limit=6, limit_area="inside") # 夜间功率清零 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的limit=6表示最多连续填补 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(drop=True)

shift产生的滞后值天然会在每天开头产生 NaN,因为前一天的最后一个值不能作为今天第一个值的滞后信息,所以最后统一dropna()。这里有个容易忽略的点:如果只用历史功率做预测,模型能学到惯性却学不到突变;有条件拿到数值天气预报辐照度的话,把未来时刻的预报辐照度作为外部特征拼进输入窗口,预测精度会明显提高。这是从“能跑通”到“能落地”的关键一步,也是后面调参时最值得投入的方向。

3. 用 PyTorch 把 LSTM 光伏预测模型跑起来:网络结构、滑窗与训练参数

3.1 滑窗:用过去 12 个点预测未来 6 个点

短期光伏预测通常用序列到序列的思路:给模型过去几小时的功率和气象特征,让它一次吐出未来几小时的功率曲线。我用seq_len=12,在 15 分钟粒度下对应过去 3 小时;pred_len=6,对应未来 1.5 小时。这个配置兼顾并网申报的时效和模型难度,预测时长再拉长到 4 小时以上时,单靠 LSTM 的误差会明显变大,需要换结构。

def make_sequences(data_x, data_y, seq_len=12, pred_len=6): 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_state=42),验证集 loss 低到离谱,模型一上线立刻失效,原因就是验证集里混进了训练样本的“近亲”,模型等于开卷考试。

3.2 网络结构:hidden_size、num_layers 怎么定

LSTM 做光伏预测的网络结构不需要很复杂。我的基线方案是一层 LSTM 加一层全连接,hidden_size=64。数据量超过几万条时可以加到两层,但num_layers每加一层,训练时间几乎翻倍,收益却很小。光伏功率预测的常见经验是第一层 LSTM 学日周期的形状,第二层学天气突变的局部特征,两层足够,三层以上在中小数据集上基本都是过拟合。

import torch import torch.nn as nn class PVLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=1, pred_len=6, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout 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, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32) X_val_t = torch.tensor(X_val, dtype=torch.float32) y_val_t = torch.tensor(y_val, dtype=torch.float32) train_loader = DataLoader( TensorDataset(X_train_t, y_train_t), batch_size=64, shuffle=True ) model = PVLSTM(input_size=X_train.shape[2], hidden_size=64, num_layers=1, pred_len=6) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5) criterion = nn.MSELoss()

这里batch_size=64是个稳妥起步值,数据量小就降到 32。shuffle=True只用在训练集,验证集绝不打乱顺序。损失函数选 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数据量大可加到 128
num_layers1~2超过 2 层中小数据集易过拟合
batch_size64数据少降到 32
初始学习率1e-3loss 震荡就降到 3e-4
patience10验证集噪声大就加大

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, steps=48): model.eval() seq = init_seq.copy() preds = [] with torch.no_grad(): for _ in range(steps): x = torch.tensor(seq, dtype=torch.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, axis=0) seq[-1] = new_row return np.concatenate(preds)

这段代码里有个容易出错的细节:滚动预测时,输入的功率特征用的是模型自己的预测值,这会累积误差,预测步数越多偏差越大,所以滚动预测只适合步数不超过 48 个点(12 小时)的场景。后面的new_row构造逻辑必须严格对齐特征列顺序,否则模型输入的含义就变了。每次预测后要做scaler_y.inverse_transform,把结果还原成物理功率值,画图和评估都用还原后的值,用归一化值直接算误差看不出来量级问题。

误差分析我通常按天气类型分三组来算指标:晴天、多云、雨天。只看整体 RMSE 会掩盖模型在多云天的失败;按组计算后你会发现,晴天 RMSE 很小、多云天很大。如果项目要写论文,用表格列出分组误差,再附上一张典型日的预测曲线对比图,模型的优缺点就一目了然,答辩时也更容易讲清楚。

这几年的习惯是:每次跑完实验先画预测曲线,再按小时分段算误差,最后才看整体指标,因为平均值会骗人。光伏预测是一个“数据质量决定上限”的方向,把时间顺序、归一化、夜间时段这三件事管好,LSTM 就已经能超过一大半的基线模型。希望这些踩坑经验能帮你在毕业设计或实际项目中少走一点弯路,尽快把预测曲线稳定下来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询