简介面向光伏发电预测的深度学习毕业设计项目整合CNN-GRU、CNN-LSTM、GRU、LSTM-transform与LSTM等多种模型覆盖从数据处理、模型构建到结果对比的完整流程可作为电力预测课程设计与科研入门参考。压缩包共70个文件、约10MB主要包含Python源码、数据集CSV、训练权重H5、特征缩放PKL以及预测对比图PNG等结构清晰便于直接运行与二次开发。已有89人学习适合电力预测、时间序列分析方向的学生或工程师。项目不仅提供了各模型在测试集上的预测结果与真实值对比还附带训练过程曲线和评估指标文件可直观比较不同结构在光伏发电量预测任务上的表现省去重复搭建环境的时间。通过这套代码与数据读者能快速理解CNN提取特征与LSTM/GRU捕捉时序依赖的配合方式并迁移至其他可再生能源预测场景具有较强的工程实用性。1. 光伏发电量预测项目拆解CNN-LSTM 混合模型凭什么把测试集误差压到个位数在做光伏发电量预测这个课题之前我一开始觉得不就是个时间序列回归问题吗套个 LSTM 就完事了。实际跑完这个基于深度学习的项目才发现所谓“光伏发电量预测”核心难点根本不在于模型本身有多深而在于怎么处理天气突变、怎么切分时序样本、怎么设置滑窗才能避免数据泄漏。这份资源把 CNN、LSTM、GRU 五种模型全部训练并对比了一遍测试集的预测结果、真实值曲线和误差指标都齐齐全全。适合正在做毕业设计、或者想在光伏功率预测方向快速落地一个基线模型的人。不用从零开始搭环境、调超参照着跑一遍就能搞清楚源码的价值在哪我后面所有关于 lstm 时间序列预测 python 的实践细节也都从这份资源里延伸出来。2. 数据与预处理光伏发电量数据集的特征工程和归一化陷阱2.1 数据集结构与关键特征这份资源自带的光伏发电量数据集是典型的光伏电站采样记录按时间戳逐行排列。字段大致包括时间、辐照度、环境温度、组件温度、湿度、风速、风向和实际发电功率。不同电站采样间隔不一样这份数据集大约每 15 分钟一条记录一天 96 个点连续记录时间在一年左右。拿到数据先看字段和缺失值情况这是最基础也最容易被跳过的一步。import pandas as pd import numpy as np df pd.read_csv(pv_power.csv, parse_dates[time]) df.set_index(time, inplaceTrue) print(df.info()) print(df.isnull().sum())输出里最常见的问题是辐照度为 0 的夜间时段占比高个别传感器断线导致功率字段为空。我的习惯是用前向填充把缺失值补上再把连续缺失超过 10 条的记录直接丢弃避免把异常序列塞进模型。补缺失值时要小心不能不分位置一律 ffill。# 前向填充用前一条有效记录补齐后一条缺失 df[power_kw] df[power_kw].fillna(methodffill) # 连续缺失过多时直接去掉该段 mask df[power_kw].isna() df df[~mask]逻辑说明这里的 ffill 是拿前一条有效记录填充后一条缺失适用于传感器短暂掉线如果缺失发生在功率上升或下降的陡坡段ffill 会引入延迟误差所以同时限制连续缺失数量连续缺失超过阈值就整段丢弃。参数说明fillna 的 method 参数只能是 ffill 或 bfillffill 表示用前面最近的有效值bfill 用后面。连续缺失阈值一般结合采样间隔来定15 分钟采一条时连续 10 条就是两个半小时超过这个时长基本说明当天数据不可用。特征选择方面我一般先用相关性热力图看一遍再决定模型输入。光伏发电量预测本质上是多变量时序回归辐照度是最主要的驱动因素温度影响组件转换效率风速和湿度影响散热与云层移动。import seaborn as sns import matplotlib.pyplot as plt feature_cols [irradiance, temp, humidity, wind_speed, power_kw] # 计算皮尔逊相关系数并可视化 corr df[feature_cols].corr() sns.heatmap(corr, annotTrue, cmapcoolwarm) plt.show()从相关性结果看辐照度与发电功率的相关性通常能到 0.9 以上温度在 0.5 左右湿度和风速偏低。但偏低不代表没用短期云层移动带来的功率波动靠风速和湿度能捕捉一部分。做特征工程时不要只盯着相关系数还要看物理逻辑。2.2 滑窗切片与归一化顺序时序模型不能把全部数据一次性丢进网络要按固定窗口切块。光伏发电量预测最常见的是用过去 24 小时数据预测下一小时采样间隔 15 分钟时 seq_len 就设为 96。我这里用过去 24 个点预测下一个点方便演示。def create_sequences(data, seq_len24, pred_len1): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): # 取连续 seq_len 个时间步作为输入 X.append(data[i:i seq_len, :]) # 取窗口结束后的 pred_len 个功率值作为标签 y.append(data[i seq_len:i seq_len pred_len, -1]) return np.array(X), np.array(y) seq_len 24 X_train, y_train create_sequences(scaled_train, seq_len) X_val, y_val create_sequences(scaled_val, seq_len) X_test, y_test create_sequences(scaled_test, seq_len)逻辑说明滑窗每向后移动一步就生成一个新样本所以样本数约等于总点数减去窗口长度。X 的形状是 (样本数, seq_len, 特征数)第三个维度包含辐照度、温度、湿度等所有特征y 只取最后一列功率值这里预测的是下一时刻功率。参数说明seq_len 决定模型能看到多长的历史pred_len 是预测步长预测未来一小时就把 pred_len 改成 4。seq_len 太小模型看不到云的移动趋势太大则把昨天同一时刻的周期信息也卷进来反而增加训练成本。实际调参时我一般试 12、24、48 三档。归一化顺序是个高发坑。正确做法是只对训练集调用 fit_transform验证集和测试集用同一个 scaler 做 transform。如果先对整个数据集 fit测试集的最大最小值会被模型“偷看”到测试误差会比真实水平低论文里写出来的数字也就不诚实了。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) # 只用训练集拟合归一化参数 scaled_train scaler.fit_transform(data[:train_len]) # 验证集和测试集直接使用训练集拟合好的 scaler scaled_val scaler.transform(data[train_len:val_len]) scaled_test scaler.transform(data[val_len:])这里有个坑MinMaxScaler 默认对每个特征独立缩放光伏功率夜间为 0白昼峰值可能到几十 kW缩放后区间自然压到 [0,1]。反变换预测值时需要再调用 scaler.inverse_transform并且要先把预测结果拼回原特征维度不能直接传一列数据否则列数对不上会报错。2.3 训练/验证/测试集划分策略分类任务可以直接随机打乱划分时间序列不行。光伏发电量有明显的昼夜周期和季节性随机切分会把白天和黑夜的样本混在一起训练模型根本学不到时序依赖。按时间顺序切分是最基本的约束。total_len len(data) # 按时间顺序切分不能随机打乱 train_len int(total_len * 0.7) val_len int(total_len * 0.15) train_data data[:train_len] val_data data[train_len:train_len val_len] test_data data[train_len val_len:]逻辑说明这里按 70% 训练、15% 验证、15% 测试切分验证集用于早停和调超参测试集只用于最终评估。注意切分顺序不能乱验证集必须紧跟训练集之后模拟“未来”的数据。参数说明比例不是固定死的。如果数据只有半年我建议 80% 训练、10% 验证、10% 测试训练样本太少模型容易欠拟合数据达到一年以上再回到 70/15/15。切完数据后还要处理滑窗边界create_sequences 默认从第 0 个点开始滑窗口跨天在物理上是合理的因为预测本就要用最近时刻的信息但如果按自然日做批量预测需要按天切块后再分别滑窗。3. 模型选型LSTM、GRU、CNN-LSTM、CNN-GRU 的取舍逻辑3.1 LSTM 与 GRU门控机制差异决定了什么LSTM 全称长短期记忆网络靠输入门、遗忘门和输出门三个门控控制信息流动。核心状态是 cell state可以理解成一条贯穿网络的传送带门控决定哪些信息能写入、哪些需要遗忘、哪些在当下输出。GRU 是 LSTM 的简化版把三个门合并成更新门和重置门两个参数量更少训练更快。选型时要考虑数据规模和训练资源。光伏发电量序列长度不算特别长单序列一小时一个点一天 24 个点LSTM 和 GRU 在精度上差距很小。但如果你要把历史风速、云量等更多特征一起输入GRU 在训练速度上的优势就会体现出来。我平时做基线对比先跑 GRU因为它收敛快、翻车概率低等确认数据没问题了再上 LSTM 提一点精度。建一个标准的 LSTM 预测模型from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model_lstm Sequential([ # 第一层 LSTM 返回完整序列供第二层继续处理 LSTM(64, return_sequencesTrue, input_shape(seq_len, n_features)), Dropout(0.2), # 第二层只返回最后一个时间步的输出 LSTM(32), Dropout(0.2), Dense(1) ]) model_lstm.compile(optimizeradam, lossmse, metrics[mae])逻辑说明第一层 LSTM 把 return_sequences 设为 True是为了把完整的隐藏状态序列传给第二层最后一层不返回序列只输出最后一个时间步的隐藏状态再接全连接层输出预测功率。Dropout 放在 LSTM 层之后可以抑制过拟合特别是训练轮数一多、验证损失不再下降的时候。参数说明LSTM 的 units 表示该层输出的隐藏特征维度64 对光伏功率序列够用。如果换用 128模型表达能力更强但训练更慢数据量不足时还容易过拟合。Dropout 的 0.2 表示每轮训练随机丢弃 20% 的神经元连接。GRU 模型结构几乎一样把 LSTM 换成 GRU 就行from tensorflow.keras.layers import GRU model_gru Sequential([ GRU(64, return_sequencesTrue, input_shape(seq_len, n_features)), Dropout(0.2), GRU(32), Dropout(0.2), Dense(1) ])GRU 的参数量大约是 LSTM 的四分之三训练速度更快。实际对比中GRU 在光伏功率这种中等长度序列上精度几乎持平 LSTM尤其当数据包含大量夜间 0 功率样本时GRU 更不容易被这些冗余信息拖累。3.2 CNN-LSTM 混合结构特征提取与时序建模的衔接方式CNN 擅长提取局部特征一维卷积作用于时序序列时可以看作滑动的特征抽取器。光伏发电量受云层遮挡影响功率曲线在短时间尺度上会出现频繁的尖峰和凹陷CNN 卷积核能自动捕捉这种局部形态变化再交给 LSTM 学习跨时间步的长程依赖。CNN-LSTM 的常见接法是先用一维卷积对原始多变量序列做特征映射再经过池化压缩序列长度最后进入 LSTM。池化这一步很多人忽略其实它决定了 LSTM 能看到的时间范围。如果卷积把序列长度从 96 压缩到 48LSTM 的每个时间步对应的就是原始两个时间步的信息。from tensorflow.keras.layers import Conv1D, MaxPooling1D model_cnn_lstm Sequential([ # 一维卷积提取局部时序特征 Conv1D(filters64, kernel_size3, activationrelu, input_shape(seq_len, n_features)), MaxPooling1D(pool_size2), # 第二层卷积继续压缩特征 Conv1D(filters32, kernel_size3, activationrelu), MaxPooling1D(pool_size2), # 压缩后的序列交给 LSTM LSTM(32, return_sequencesTrue), LSTM(16), Dense(1) ])逻辑说明两层 Conv1D 先从原始特征里提取局部模式每次池化把序列长度减半LSTM 再对压缩后的序列建模。这样 CNN 负责识别“过去两小时辐照度连续上升且风速突变”这类模式LSTM 负责把模式组合成对未来的预测。参数说明kernel_size3 代表卷积核覆盖 3 个连续时间步filters 是卷积核数量数值越大提取模式越多但训练越慢pool_size2 是下采样步长如果 seq_len 是 24经过两层池化序列长度变成 6LSTM 的计算量大幅下降。注意点Conv1D 的输入必须是三维张量 (batch, seq_len, features)和 LSTM 输入格式一致这也是它能直接拼接的原因。有些教程先把数据从 (batch, seq_len, features) reshape 成 (batch, seq_len×features) 再进全连接层那就不叫时间卷积了time step 对应的语义被破坏效果一般也差不少。CNN-GRU 就是把 LSTM 换回 GRU前面卷积部分完全一致。常见效果是 CNN-LSTM 比 CNN-GRU 好一点点但 CNN-GRU 训练速度快适合做交叉验证时的快速验证模型。如果你手头数据量大、时间充裕两个都可以跑数据量有限时我优先推荐 CNN-GRU。3.3 五种模型的适用边界与选择建议这份项目里一共跑了 LSTM、GRU、LSTM-Transformer、CNN-LSTM、CNN-GRU 五种模型。LSTM-Transformer 在序列长度较长、数据量充足时表现最好但对小数据集容易过拟合训练也慢。CNN-LSTM 和 CNN-GRU 是稳健的折中方案既保留时序建模能力又靠 CNN 先压缩了输入泛化能力更好。选择建议如果数据只有两三个月不要碰 Transformer直接上 GRU如果想在论文里做模型对比至少保留 LSTM、GRU、CNN-LSTM 三个基线如果要做未来一小时以上预测优先 CNN-LSTM因为卷积压缩能有效降低长输入带来的梯度问题。模型输入处理时序建模参数量适用数据量训练速度LSTM原始序列三门控中大中中GRU原始序列双门控小中小快CNN-LSTM一维卷积压缩三门控中中中CNN-GRU一维卷积压缩双门控小中快LSTM-TransformerSelf-Attention自注意力大很大慢这个表是我在多次对比中总结出来的不是绝对定论。核心逻辑是模型复杂度要和数据量匹配光伏功率序列本身信噪比不高复杂模型如果没有足够数据支撑测试集上反而更容易翻车。4. 训练与评估超参设置、损失函数和五个模型的测试集对比4.1 训练超参的设置与含义模型搭好之后训练环节决定最终效果。常见超参包括 batch_size、epochs、learning_rate、早停 patience。光伏发电量数据集一般几千到几万条样本batch_size 32 或 64 比较常见learning_rate 默认 0.001Adam 优化器epochs 不要硬设太大配合早停让模型自己决定何时停止。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) checkpoint ModelCheckpoint(best_model.h5, monitorval_loss, save_best_onlyTrue) history model.fit( X_train, y_train, validation_data(X_val, y_val), batch_size32, epochs100, callbacks[early_stop, checkpoint], verbose1 )逻辑说明ModelCheckpoint 把验证集损失最小的那一轮权重存下来EarlyStopping 在连续 10 轮 val_loss 没有下降时终止训练并恢复到最佳权重。这两个回调配合使用是防止过拟合和浪费时间最有效的组合。参数说明patience 是容忍连续多少轮不改善数据量大时可设 15batch_size 影响梯度下降的平滑程度太大容易跑进局部极小值附近的平坦区域太小则训练震荡剧烈光伏数据我一般先试 32。loss 用 mse 是因为回归任务对逐点误差敏感换 mae 也可以但梯度更新行为不同。4.2 评估指标MAE、RMSE、MAPE 怎么选回归预测最常用的三个指标是 MAE、RMSE、MAPE。MAE 是平均绝对误差直观反映误差大小RMSE 对大误差更敏感适合用来发现预测中的极端偏离MAPE 是百分比误差方便不同量纲之间比较但功率为 0 的夜间时段算出来的百分比会变成无穷大所以做 MAPE 时通常只统计白天有功率的时间段。from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(y_test_inv, y_pred_inv) rmse np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) # MAPE 只算白天功率大于阈值的点避免除零 mask y_test_inv 0.1 mape np.mean(np.abs((y_test_inv[mask] - y_pred_inv[mask]) / y_test_inv[mask])) * 100 print(fMAE: {mae:.3f}, RMSE: {rmse:.3f}, MAPE: {mape:.2f}%)逻辑说明测试集预测结果需要先做 inverse_transform 还原成功率值再计算指标否则算出来的是缩放后的误差无法对应到实际 kW 单位。mask 把功率接近 0 的点排除掉避免 MAPE 出现极端值。参数说明y_test_inv 和 y_pred_inv 是一维数组inverse_transform 后取功率列。阈值 0.1 是根据归一化前的最小非零功率定的实际使用时可以按电站装机容量的 1% 来设置比如装机 100kW 就取 1kW 为界。4.3 五个模型的测试集结果对比这份资源的对比结果按测试集的预测曲线来看可以总结成几条。LSTM 作为 baseline整体表现稳定晴天误差小多云天气误差偏大GRU 在同样超参下训练时间约是 LSTM 的七成精度几乎持平CNN-LSTM 在多云时段和功率快速波动时明显优于 LSTM因为卷积层先提取了局部变化特征CNN-GRU 和 CNN-LSTM 接近但收敛更快LSTM-Transformer 需要更多训练轮数在本题数据集上并没有取得压倒性优势反而偶尔出现过拟合。具体误差数值我这里不放绝对数字因为不同数据集切分和特征组合会导致结果变化很大。但有一个稳定规律混合模型CNN-LSTM、CNN-GRU的 RMSE 通常比纯 LSTM 低 5% 到 15%尤其在测试集跨季时优势会更明显。实际的模型对比可以打印成一张 DataFrameresults pd.DataFrame({ Model: [LSTM, GRU, CNN-LSTM, CNN-GRU, LSTM-Transformer], MAE: [mae_lstm, mae_gru, mae_cnn_lstm, mae_cnn_gru, mae_trans], RMSE: [rmse_lstm, rmse_gru, rmse_cnn_lstm, rmse_cnn_gru, rmse_trans], MAPE: [mape_lstm, mape_gru, mape_cnn_lstm, mape_cnn_gru, mape_trans] }) print(results.sort_values(RMSE))逻辑说明把五个模型在同一个测试集上算出的指标放在一起才能公平对比。需要注意每个模型训练时用同一个随机种子和同样的数据切分否则对比没有意义。跑对比实验前先把随机种子固定住我一般写成 np.random.seed(42)省得后面怀疑结果是不是随机波动。从结果上看LSTM-Transformer 在数据量不足时翻车概率高这是我在多个数据集上反复踩出来的经验。Transformer 的注意力机制需要大量样本才能学到稳定的依赖关系小数据上不如 LSTM 类模型稳。5. 光伏发电量预测的避坑指南数据泄漏、极端天气与过拟合5.1 数据泄漏归一化顺序搞反精度全是假的现象训练完的模型测试集误差低得离谱RMSE 比合理范围小一个数量级曲线贴合得像作弊。原因我在早期调模型时图省事把整个数据集先 MinMaxScaler 一遍再划分训练测试集scaler 在 fit 时已经看了测试集的最大最小值测试集被“剧透”了。模型相当于提前知道了测试集的数值范围损失当然低。解决严格按训练集 fit、测试集 transform 的顺序来。测试集在训练过程中应当对任何代码路径都是“未来”包括数据预处理这一步。写代码时我建议把 scaler 的 fit 单独放到一个函数里训练和推理共用同一个已经序列化的 scaler 对象不要每次预测都重新 fit。5.2 极端天气样本让预测值大幅偏离现象模型在晴天日子预测很准一到暴雨或大风天气预测值严重偏低误差扩大好几倍。原因数据集中晴天样本占比过高雨天样本稀少模型没有见过足够的极端天气模式学习到的权重天然偏向多数类。光伏发电量预测受天气影响最大但数据集往往按自然时间记录晴天和阴天的样本比例完全由采集期气候决定。解决先按天气标签对样本做重采样或对极端天气样本加权损失函数。第二种做法在 PyTorch 里容易改Keras 里可以用 sample_weight 参数把雨天样本的权重调高到 2-3 倍。如果数据集里有天气编码字段用 groupby 统计一下各类别数量很容易看出样本不平衡程度。5.3 过拟合与欠拟合怎么从损失曲线上判断现象训练损失下降稳定验证损失却在中途开始反弹或者训练集和验证集误差都很高。原因过拟合是模型容量太大而数据量不足欠拟合是容量太小或特征不够。光伏数据通常只有一年左右LSTM 堆到三层以上就开始出现过拟合迹象。解决看历史曲线的走向就能区分。训练损失持续下降但 val_loss 拐头向上这是过拟合这时增加 Dropout、减小 units、或提高 L2 正则两边都不降这是欠拟合先检查特征有没有做差分、滑窗是否太小再考虑加层数。我一般先把 units 减半试一轮如果 val_loss 明显改善说明之前确实过拟合了。5.4 训练不收敛与显存不足现象loss 一直停在初始值附近或训练到一半报错 OOM。原因不收敛通常是学习率太大导致梯度波动或者输入数据未归一化导致数值范围过大OOM 是 batch_size 或序列长度太大显存装不下。解决把 learning_rate 从默认 0.001 降到 0.0001 再试显存不够时先降 batch_size还不够再加 Gradient Accumulation。如果输入特征是辐照度和功率混合一定要确认 scikit-learn 的 StandardScaler 或 MinMaxScaler 已经把数值压到合理范围。我遇到过一回去年做类似项目因为温度特征没归一化模型 loss 在 0.5 附近震荡了两百轮都没降下来。6. 模型落地加载权重做预测与二次开发的三个实操技巧6.1 加载训练好的模型进行推理训练完不需要每次都从头跑把最佳权重保存下来预测阶段直接加载就行。from tensorflow.keras.models import load_model model load_model(best_model.h5) # 新数据使用训练时保存的 scaler 做归一化 new_scaled scaler.transform(new_data) X_new, _ create_sequences(new_scaled, seq_len) y_pred_scaled model.predict(X_new) # inverse_transform 需要原始特征维度先拼接再取功率列 y_pred scaler.inverse_transform( np.concatenate([np.zeros((len(y_pred_scaled), n_features - 1)), y_pred_scaled], axis1) )[:, -1]注意inverse_transform 需要原始特征维度拼接回去再取功率列这是最容易写错的地方少拼一列或者顺序反了都会得到错得离谱的结果。6.2 预测结果可视化与误差分析画一张真实值 vs 预测值的曲线图比看指标直观得多。测试集选连续一周的数据曲线贴合程度能直接看出模型在哪个时段失效。import matplotlib.pyplot as plt plt.figure(figsize(12, 4)) plt.plot(y_test_inv, labelactual, linewidth1) plt.plot(y_pred_inv, labelpredicted, linewidth1, alpha0.8) plt.legend() plt.title(PV Power Prediction - CNN-LSTM) plt.show()如果预测曲线在夜间段的 0 功率附近抖动说明模型对零值区间的拟合还不够干净可以考虑在损失函数里对夜间样本降权。6.3 三个实战调整技巧我一直保留的习惯是任何模型调参前先把 baseline 的 RMSE 记下来再做任何改动都以是否低于这个值为标准避免陷入调参玄学。第二个技巧是预测未来多步时用递归预测把预测值当作下一步输入比一次性输出多步更稳定缺点是误差会累积所以步数多时反而要用序列生成式的 transformer 结构。第三个技巧是模型保存时文件名带上指标例如 cnn_lstm_rmse_12.3.h5这样后续对比多个实验时不会把权重搞混。从那以后我每次跑光伏数据集都会强制走一遍“先手工检查数据泄漏 → 再单步模型 → 最后上混合模型”的流程这个顺序帮我避开过不少翻车现场希望帮到你。本文还有配套的精品资源点击获取