☰
光伏发电功率预测实战:LSTM、CNN-LSTM与XGBoost模型对比
2026/10/3 13:06:29 网站建设 项目流程

上个月刚交付完一个分布式光伏电站的发电功率预测项目,把手头的LSTM、CNN_LSTM和XGBoost三个回归模型完整地对比了一遍。趁着周末有空,我打算把这套从数据处理、特征构造到模型训练、上线的完整流程整理出来,给准备做光伏预测,或者想找一份可直接复用的时序回归任务模板的同学提供一个参考。

这个项目本身不算特别复杂,但很典型:光伏发电功率预测的本质是一个多变量时间序列回归任务,输入是历史功率、气象观测数据,输出是未来一段时间内的功率曲线。因为光伏出力受辐照度、温度、云量这些因素影响大,而且有明显的日内周期性和天气突变特征,所以很适合用来检验不同模型在非线性拟合、时序依赖捕捉上的表现。我这次选了LSTM、CNN_LSTM和XGBoost三条技术路线,分别代表纯循环网络、卷积+循环混合结构、梯度提升树三大类方法,做了一次系统对比。

这篇文章不会讲太多数学推导,重点会放在:数据怎么清洗、特征怎么做、三个模型怎么落地实现、参数怎么调、训练中会踩到哪些坑。所有代码都是Python,使用的库包括Pandas、NumPy、PyTorch、XGBoost和Scikit-learn,在普通PC上就能跑通。

1. 项目整体设计:为什么选这三个模型做对比

1.1 光伏功率预测的任务拆解

光伏发电功率预测,简单说就是“根据过去一段时间的功率和气象数据,预测未来某一时刻的出力”。但真要建模,首先要明确几个关键问题。

第一个问题是预测时间尺度。光伏预测分为超短期(未来0-4小时)、短期(未来1-3天)和中长期(未来一周以上),时间尺度不同,建模思路差别很大。超短期预测主要靠历史功率的趋势外推和最近的辐照度变化,而短期预测则更依赖数值天气预报。我这次做的是国内某分布式电站的短期功率预测,目标是根据过去3天的15分钟粒度数据,预测未来24小时的功率输出。

第二个问题是输出形式。可以是单点预测(预测下一个时刻的功率值),也可以是序列预测(输出未来24小时的一条曲线)。序列预测更实用,但难度也更大。为了公平对比,我这次的方案是:用过去48个时刻(12小时)的功率和气象数据作为输入,预测未来24个时刻(6小时)的功率曲线。这个设定下,三个模型都做多步预测,评估的是“整条曲线”的拟合能力。

第三个问题是模型选型逻辑。为什么把LSTM、CNN_LSTM和XGBoost放在一起对比?因为这三者代表了处理时间序列回归问题的三种完全不同的技术路线:

  • LSTM是纯循环网络,天生为序列建模设计,理论上能捕捉长期依赖;
  • CNN_LSTM是混合结构,先用卷积在时间维度上提取局部特征,再用LSTM建模时序依赖,在很多序列任务上效果优于纯LSTM;
  • XGBoost是梯度提升树模型,本身不关心时间顺序,但通过特征工程把时序问题转换成监督学习问题后,往往能用很少的调参拿到很强的baseline。

把这三者放一起,正好可以回答一个实际问题:我的预测任务到底值不值得上深度网络,还是用一个优秀的树模型就够了。

1.2 技术选型背后的考量:任务痛点与模型优势对应

我设计这套对比方案时,有几点考量。

对于LSTM,我最看重的是它对时间顺序的建模能力。光伏功率曲线有明显的日内“单峰”形态,日出后爬升、午后达峰、日落后归零,这种形态依赖长时间的记忆。LSTM通过输入门、遗忘门、输出门三个门控结构,让梯度可以沿着时间步有效回传,理论上能保留几天前的出力模式信息。

对于CNN_LSTM,我看中的是卷积层对局部模式的提取能力。光伏功率的短期波动往往由云层移动引起,表现为功率曲线在15到30分钟内的快速抖动。一维卷积可以直接在时间轴上提取这些短时形态特征,再交给LSTM做序列依赖建模。相比纯LSTM,这种方式相当于给模型加了一个“局部特征提取器”,在输入序列较长时可以减轻LSTM的负担。

对于XGBoost,传统观点认为树模型不能直接处理序列,但其实只要用滑窗构造滞后特征,XGBoost的表现往往很惊艳。它的优势在三个方面:对异常值更鲁棒,对特征尺度不敏感,自动处理特征交互。缺点是不会隐式利用时间顺序,所以需要在特征工程上把“时间信息”显式给它。

这三者的对比,不是简单的“谁比谁强”,而是给日后做类似任务提供一个参考:当数据量小、特征工程完善时,XGBoost是性价比极高的选择;当数据量充足、希望自动化提取特征时,LSTM系模型更有潜力。

一个细节值得提一下:我一开始还想过加ARIMA、Prophet这类传统统计模型做对比,但最后放弃了。原因是它们对多变量气象输入的融合能力太弱,而光伏预测恰恰需要把功率序列和辐照度、温度、湿度、风速这些外生变量一起建模。统计模型单序列还有优势,多变量场景下和树模型、深度模型差距太大,比不出参考价值。

2. 数据准备与特征工程:预测精度差距的真正来源

2.1 原始数据构成与清洗方法

数据是预测项目的根本。我这次使用的数据集包含三个部分:电站实际发电功率、电站现场气象站观测数据、经纬度对应的辐照度数据。时间分辨率是15分钟,覆盖了过去一年多的记录,总样本量大概2.5万条左右。

原始的CSV长这样:

time,power_kw,irradiance_wm2,temp_c,humidity_rh,windspeed_ms 2024-01-01 00:00:00,0,0,-2.3,68.5,2.1 2024-01-01 00:15:00,0,0,-2.5,69.2,2.4 2024-01-01 06:00:00,1.2,82.5,-0.8,64.3,2.9 2024-01-01 12:00:00,128.6,798.4,12.6,38.2,4.2 2024-01-01 12:15:00,131.2,812.7,13.1,37.5,4.5

拿到手的数据远没有这么干净,我花了大概两天时间做清洗,主要处理了四类问题。

第一类是夜间数据。很多光伏电站夜间功率并不是0,逆变器待机、厂用电会产生一个很小的背景功耗,大约在0.5到2千瓦之间。这些数据对预测没有实际意义,会干扰模型训练,我直接按“辐照度低于5W/m²且功率低于额定功率3%”的规则把夜间样本剔除。

第二类是异常尖峰和毛刺。光伏功率曲线本应是平滑的,但实际数据里常出现瞬时跳变,可能是传感器受干扰或者逆变器限功率导致。我用滚动窗口中位数检测:如果某个点的功率偏离前后12个点(3小时)的中位数超过30%,就标记为异常。对孤立异常点,用前后时刻的中位数填充;对连续异常的区间,直接剔除然后做线性插值。

第三类是缺失值。由于通讯中断、采集器故障,数据里存在零散的缺失。采样的15分钟粒度下,短至15分钟的缺失可以用前后线性插值解决,超过2小时的连续缺失则直接删掉这段区间,避免引入不真实的锚点。

第四类是数据对齐问题。功率数据和气象数据来源不同,时间戳可能错位。我统一把所有序列按15分钟对齐,然后重采样成标准时间轴,再在特征上做滞后对齐,确保当前时刻的特征对应的是预测起点之前的信息,防止未来数据泄漏进特征。

清洗完成后,先做一波可视化:画出功率曲线的日变化形态、辐照度与功率的散点关系、不同季节下出力曲线差异。这些工作能帮你直观地发现很多模型层面看不到的问题——比如某个月数据的整体出力异常偏低,去查发现是电站进行了停机检修;某段时间功率长期为0,其实是通信中断而不是“没太阳”。可视化这一环,建议不要跳过。

2.2 特征构造:从原始数据到模型输入

特征工程是整个项目里投入产出比最高的一环。我构造的特征分为四类。

第一类是外源气象特征。辐照度是光伏功率的最强相关变量,必须作为核心输入;温度次之,因为光伏组件温度升高时转换效率会下降;湿度、风速影响组件散热和云层状态,属于辅助特征。所有气象特征都直接用原始值。

第二类是统计特征。如果直接用最近48个时刻的序列作为输入,模型很难感知“最近一段时间到底是晴是阴”。因此我计算了滞后1小时、3小时、6小时、12小时、24小时的平均功率和辐照度,作为全局状态特征。这些统计量对XGBoost这类树模型尤其重要,因为树模型对滑动窗口内的原始序列建模能力较弱。

第三类是时间编码。光伏出力有极强的时间周期性,所以我把时间信息分解成“小时正弦/余弦”和“一年中的第几天”:

df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) df['dayofyear_sin'] = np.sin(2 * np.pi * df['dayofyear'] / 365.25) df['dayofyear_cos'] = np.cos(2 * np.pi * df['dayofyear'] / 365.25)

用正弦/余弦编码而不是直接输入“小时整数值”的原因是:23点和0点之间的真实距离是1小时,但如果用整数编码,距离变成了23,会误导模型学习。正弦/余弦编码天然保留了周期性。

第四类是滞后功率特征。对LSTM和CNN_LSTM,滞后功率是以“序列”形式输入模型的;对XGBoost,则需要把滞后功率拉平成特征列。我构造了滞后1步(15分钟)、2步、4步、8步、16步、24步、32步、48步、96步的功率值,覆盖从15分钟到24小时的历史信息。

特征构造完成后,需要对连续特征做标准化。这里有个容易踩坑的细节:标准化参数只能用训练集拟合,绝不能在全部数据上计算均值和标准差,否则验证集和测试集的信息会在预处理阶段泄漏到训练过程,导致评估结果虚高。我用Scikit-learn的StandardScaler,先fit训练集,再对验证集、测试集分别transform。

最终,每个样本的输入维度是:

  • 气象+统计+时间特征:约15个特征
  • 功率滞后特征:48个时步(对序列模型)或48列(对XGBoost)
  • 预测目标:未来24个时刻的功率值

数据集按时间顺序划分,前70%做训练集,中间15%做验证集,最后15%做测试集。因为光伏预测是按照“预测未来”的方式使用的,窗口式交叉验证在这里容易引入未来信息,所以我只用了简单的时间顺序切分。

3. 三种回归模型的原理与Python实现

3.1 LSTM:用门控机制记住功率序列的长期规律

LSTM(长短期记忆网络)是循环神经网络的一种改进结构。标准RNN在反向传播时梯度容易指数衰减或爆炸,很难学到长距离依赖。LSTM引入了一个贯穿时间步的“记忆单元(cell state)”,配合输入门、遗忘门、输出门三个门控,实现了对信息的保留和丢弃——遗忘门决定旧的记忆要丢多少,输入门决定新的信息要记多少,输出门决定当前时刻要输出什么。

放到光伏预测场景里,LSTM可以在序列中保留“昨天中午功率达到峰值”的长期状态,又不会让“云遮住太阳导致功率骤降”的短期变化被淹没。

我用的LSTM结构是PyTorch实现的,核心代码如下:

import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.2): super(LSTMPredictor, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, bidirectional=False ) self.regressor = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, output_size) ) def forward(self, x): # x shape: (batch, seq_len, input_size) lstm_out, (hidden, cell) = self.lstm(x) # 使用最后一个时间步的输出 last_out = lstm_out[:, -1, :] out = self.regressor(last_out) return out

模型的关键参数设置:

  • input_size=15,对应气象、统计、时间特征(功率滞后序列单独作为序列维度输入)
  • hidden_size=64,隐藏层神经元数量
  • num_layers=2,两层堆叠LSTM
  • output_size=24,未来24个15分钟间隔的功率预测值

训练中我用MSE(均方误差)作为损失函数。选择MSE而不是MAE,是因为MSE对大误差的惩罚更重,能强制模型更关注晴天、阴天转换等预测难度大的时段。如果更看重预测曲线的整体波动形态,可以换成HuberLoss(平滑平均绝对误差),它对异常值不那么敏感。这个选择没有绝对的对错,取决于你最终业务上更在意哪种误差。

3.2 CNN_LSTM:先提取局部特征,再做时序建模

CNN_LSTM是卷积神经网络和循环神经网络的结合。它的设计思路是一个常见的工程直觉:一维卷积擅长提取短窗口内的局部模式,而LSTM擅长建模长距离依赖。两者结合,可以让模型在捕捉功率短期抖动的基础上,继续学习整体趋势。

在光伏预测里,CNN模块可以这样理解——就像一个人先快速扫一眼最近30分钟的变化曲线(局部特征),再结合一整天的走势(时序依赖)来判断接下来的功率。

我的实现里,CNN部分使用一维卷积:

import torch.nn as nn class CNNLSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size, seq_len, dropout=0.2): super(CNNLSTMPredictor, self).__init__() # 一维卷积提取局部特征 self.conv1 = nn.Conv1d(in_channels=input_size, out_channels=32, kernel_size=3, padding=1) self.conv2 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1) self.relu = nn.ReLU() self.pool = nn.MaxPool1d(kernel_size=2) self.batchnorm = nn.BatchNorm1d(64) # 经CNN后的序列长度会减半 cnn_seq_len = seq_len // 2 self.lstm = nn.LSTM( input_size=64, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.regressor = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, output_size) ) def forward(self, x): # x: (batch, seq_len, input_size) # Conv1d 期望输入 (batch, channels, length),需要转置 x = x.permute(0, 2, 1) x = self.relu(self.conv1(x)) x = self.relu(self.conv2(x)) x = self.pool(x) x = self.batchnorm(x) # 转回 (batch, seq_len, channels) x = x.permute(0, 2, 1) lstm_out, _ = self.lstm(x) last_out = lstm_out[:, -1, :] out = self.regressor(last_out) return out

这里有个容易忽略的细节:Conv1d的输入格式是(batch, channels, length),而LSTM期望的是(batch, length, features),所以中间需要两次permute转置。很多从RNN切到CNN_LSTM的人都会在这里报size mismatch的错。

CNN部分的参数选择也有讲究。卷积核大小kernel_size=3意味着每次卷积覆盖3个时间点(45分钟),这刚好能捕捉云层快速移动导致的功率变化模式。MaxPool1d(kernel_size=2)把序列长度压缩一半,相当于减小了LSTM的输入长度,在信息无损的情况下降低计算量。

不过要提醒一点:池化层会把分辨率降低,如果序列中隐藏的状态变化非常快,池化可能会丢失关键信息。我试过把池化层去掉、只保留步长卷积的做法,效果和带池化基本相当,但训练时间更长。对光伏功率序列这种本身相对平滑的信号,池化带来的信息损失可以接受。

3.3 XGBoost:用特征工程把时序转成监督学习

前三节两种方法都是深度学习路线。XGBoost(极端梯度提升)走的是另一条路线——树模型。它的核心思想是不断在残差方向上训练新的决策树,并按梯度方向来拟合上一轮的预测误差,最后把所有树的结果加权求和作为最终预测。

XGBoost对时间序列的处理方式和LSTM完全不同。LSTM是把数据当作“序列”逐时间步输入,XGBoost则把每个预测时刻当作独立的样本,用特征工程手工构造滞后变量来传递历史信息。换个更直白的说法:LSTM是自己“读序列”,XGBoost是别人把序列的关键信息“喂”给它。

代码实现上,先用Pandas构造监督学习格式:

import pandas as pd import numpy as np def create_supervised_features(df, input_len=48, output_len=24): """ 构造滑窗监督学习特征。 """ feature_list = [] target_list = [] for i in range(input_len, len(df) - output_len + 1): # 历史input_len个时刻的功率 hist_power = df['power_kw'].iloc[i - input_len:i].values # 历史气象特征 hist_weather = df[['irradiance_wm2', 'temp_c', 'humidity_rh', 'windspeed_ms']].iloc[i - input_len:i].values # 扁平化历史序列 flat_power = hist_power.flatten() flat_weather = hist_weather.flatten() # 当前时刻的状态特征 current_features = df[['hour_sin', 'hour_cos', 'dayofyear_sin', 'dayofyear_cos']].iloc[i].values # 组合 features = np.concatenate([flat_power, flat_weather, current_features]) target = df['power_kw'].iloc[i:i + output_len].values feature_list.append(features) target_list.append(target) X = np.array(feature_list) y = np.array(target_list) return X, y

构造完成后,XGBoost回归可以直接训练:

import xgboost as xgb # 把多步预测拆成多个单步模型,这里以第一个预测时刻为例 model_1h = xgb.XGBRegressor( n_estimators=500, learning_rate=0.05, max_depth=5, min_child_weight=2, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42, eval_metric='mae' ) model_1h.fit(X_train, y_train[:, 0], eval_set=[(X_val, y_val[:, 0])], verbose=False)

树模型的训练有几个优化点:

一是对特征做归一化的需求很低。XGBoost基于决策树分裂,分裂点只和特征的大小顺序有关,不会因为特征量纲不同而受影响。所以我可以把原始辐照度、温度、功率直接喂进去。

二是正则化参数很关键。reg_alpha是L1正则化系数,reg_lambda是L2正则化系数。光伏预测的特征维度达到几百维时会引入大量噪声特征,正则化可以压制噪声的权重。

三是早停策略。early_stopping_rounds设置的时候,如果验证集上的MAE连续50轮不再下降,训练提前终止,这一步能防止过拟合。

四是多步预测的处理方式。我这里是独立训练24个模型,分别预测未来24个时刻。也可以用“递归预测”的策略,把上一步的预测结果作为下一步的特征,但误差会累积,容易越滚越偏。还有一种方式是用多输出回归器(MultiOutputRegressor)一次输出24个值,但XGBoost原生支持多输出,我实测效果和独立训练差不多。选择独立模型的好处是,可以给不同预测步长定制不同的超参数,比如预测第24个时刻的模型需要更强的正则化,因为远期不确定性更高。

4. 模型训练、评估与结果对比

4.1 训练策略:损失函数、优化器、早停

深度学习模型部分,我统一用Adam优化器,初始学习率0.001,训练100个epoch。每个epoch结束后在验证集上计算MAE,如果连续15个epoch验证集误差不再下降,就触发早停并恢复最佳权重。这个策略虽然简单,但非常有效,可以避免过拟合和无效训练。

Batch size我设为64。光伏数据相邻样本高度相似(相隔15分钟的样本输入输出差别都不大),如果batch size选太小,梯度更新的方向会比较随机,模型不容易收敛;选太大,一个epoch迭代次数少,训练也不充分。64是一个平衡得比较好的值。

学习率衰减上,我用StepLR策略,每30个epoch学习率乘以0.5。实际跑下来,从0.001开始,到第60个epoch时学习率降为0.00025,训练曲线比较平滑。学习率衰减不是必需的,但加上之后能明显看到验证集后期误差下降更稳,不容易在最优参数附近震荡。

XGBoost部分不需要epoch训练,但我用5折交叉验证来挑选max_depth、learning_rate、n_estimators三个核心参数。先固定learning_rate=0.05,然后网格搜索max_depth取3/5/7的三种情况,每组跑5次,比较验证集MAE的均值。最终选定的参数是max_depth=5、n_estimators=300、learning_rate=0.05。

这里有个实践心得:XGBoost的learning_rate和学习轮数n_estimators是互相关联的,学习率越小需要的树越多,训练时间越长,但精度通常更高。0.05配合300棵树是我实测的性价比较优方案,再调小学习率到0.01,精度提升不到2%,训练时间翻倍,投入产出比已经很低了。

4.2 多步预测的评估指标解读

回归任务的评估指标我用了四个:MAE、RMSE、MAPE和R²。

  • MAE(平均绝对误差):所有预测误差绝对值的平均值,单位是千瓦。它的含义最直观——平均每个时刻的预测偏差是多少。
  • RMSE(均方根误差):误差平方后再取平均、开方。RMSE比MAE更大,因为它给大误差更高的权重。如果RMSE明显大于MAE,说明预测误差的分布较分散,存在个别严重偏离的预测点。
  • MAPE(平均绝对百分比误差):误差占真实值的百分比。MAPE最直观,但有个坑:真实值很接近零的时候会放大误差。光伏功率在日出和日落时段本来就接近0,所以我计算MAPE时只统计辐照度高于200W/m²的时段,避免分母过小带来的数值失真。
  • R²(决定系数):表示模型对输出方差的解释程度,最大为1。实际操作中,因为存在传感器噪声和不可预测的天气突变,光伏预测的R²很难逼近1,训练集上0.98、测试集上0.93已经算很好的表现。

还有个指标容易被忽略:预测曲线的“峰值时刻偏差”。光伏预测不仅要看功率大小准不准,还要看峰值的出现时间偏不偏。这块我额外计算了每个预测日和实际日的峰值时刻差,三个模型在这项指标上差别不大,峰值时刻偏差都在30分钟以内。

评估时一定要分时段统计,不要只看全天的平均指标。我习惯把每天的预测误差按“上午、中午、下午”三个时段分别计算,因为上午辐照度快速上升、下午快速下降,同一模型在这三个时段的误差结构完全不同。XGBoost最明显——上午和下午误差接近,中午的误差最小,因为中午辐照度高、功率大,相对误差自然小。

4.3 三个模型的最终表现对比

经过完整的训练和评估,我在测试集上得到的结果大概是这样(数据脱敏处理过,功率单位是kW):

模型MAE (kW)RMSE (kW)MAPE (%)R²训练时长
LSTM8.6213.519.8%0.93约6分钟
CNN_LSTM7.9412.188.7%0.94约8分钟
XGBoost7.6311.928.6%0.94约2分钟

三个模型在测试集上的表现没有拉开数量级差距,但有几个值得注意的规律。

XGBoost在这个任务上表现最好,MAE和RMSE都最低。这和我一开始的判断相符:光伏功率预测的特征工程已经做得比较完善,气象变量和滞后变量对功率的影响在树模型的分裂过程中被充分建模,深度网络在这种“中等规模、特征干净”的回归任务上优势并不明显。

CNN_LSTM优于LSTM,说明卷积层确实帮助模型提取了短时局部特征。纯LSTM在输入序列被压缩到48个时间步后,仍然要靠隐藏状态记住所有信息,信息瓶颈比较明显。CNN先把序列压缩成更紧凑的特征表示,LSTM的信息负担显著降低。

误差的时间分布差异明显。把8月份一个晴天的预测曲线画出来,三个模型在中午时段几乎重合,误差主要体现在上午10点到11点和下午16点到17点两个爬坡/下坡阶段。在这两个时段,LSTM的预测曲线要比实际值滞后约15到30分钟,CNN_LSTM的滞后较小,XGBoost几乎无滞后——树模型是基于“最近功率+气象条件”直接回归出目标,没有时序惯性。

天气转换日的表现差异更大。在一个多云的天气突变日里,LSTM的RMSE比晴天高约40%,XGBoost高约35%,因为云层变化导致的辐照度剧烈波动是当前特征无法完全提前反映的。这一块也是后续可以优化的重点方向。

5. 训练中的问题与排查实录

5.1 时间顺序打乱导致的数据泄漏

项目早期犯过一个挺典型的错误:为了“增强训练样本的多样性”,我把所有样本随机打乱后再划分训练集和测试集。看似没问题,实际后果是模型在测试集上的MAE只有2.8 kW,我一度以为模型效果特别好。

但细想就觉得不正常。把时间顺序打乱后,训练集里包含了许多“未来”的信息,模型只要记住前一天的模式,就能在测试集上“猜”出答案——这不是预测,更像在作弊。打乱样本顺序等于让模型提前看到了测试集时期的天气和功率信息,评估结果完全失真。

修正方案很简单:按时间先后顺序切分数据,训练集、验证集、测试集严格按时间排列。修改后测试集MAE从2.8 kW回升到7.63 kW,这才是真实水平。这个坑在时间序列预测里非常常见,做任何时序任务时,train_test_split的shuffle参数必须设为False。

5.2 归一化后数值反算错误导致的“零误差”假象

第二个坑出在标准化上。我把特征和目标变量都做了标准化,标准化之后预测值域是均值为0、方差为1的分布。训练时,我用标准化后的目标值计算损失,模型收敛很快,验证集R²达到0.99。

但在反标准化计算真实功率值时,我却顺手用了训练集的标准化参数,而不是对应测试集的参数。测试集和训练集的均值和标准差不同,导致反算出来的功率数值整体偏移,误差瞬间大了好几倍。排查了整整一个下午才发现是标准化的transform和inverse_transform参数搞混了。

这类问题最好的规避方式是把数据预处理封装成一个统一的类或函数,每个阶段的“标准化→训练→预测→逆标准化”全部走同一个逻辑,避免手工多次调用导致参数错位。我后来写了一个pipeline,测试集数据从解析到最终输出只经过一条链路,再没出过这类问题。

5.3 夜间零值样本导致的MAPE失真

MAPE指标有一个很经典的坑:当真实值为0或接近0时,百分比误差会趋向无穷大。光伏夜间功率为0,光伏功率预测在这个时段的“预测误差”自然会是极大的百分比。如果不做处理,MAPE会被夜间样本拉高到离谱。

我的处理方式有两层。第一层是在模型训练阶段直接剔除夜间样本,只保留辐照度高于5W/m²的时段,让模型专注于有实际预测意义的场景。第二层是在评估阶段,MAPE也只计算辐照度高于200W/m²的时段,确保评估指标有业务含义。这个处理不是“美化指标”,而是让指标真正反映业务关心的白天功率预测精度。

其实还有个更隐蔽的坑:上午日出和傍晚日落前后,功率从0缓慢上升或下降到0,真实值很小,即使绝对误差只有1 kW,百分比误差也很高。后来我把评估按辐照度分层,统计了“低辐照度区间(200-400W/m²)”、“中辐照度区间(400-600W/m²)”、“高辐照度区间(600W/m²以上)”的MAE,能更清楚地看到模型在哪个辐照度区间误差最大。实测下来,三个模型都在低辐照度区间误差最大,这部分误差主要来自功率从0开始爬升时的不确定性。

5.4 天气突变时的预测偏差

测试集里包含了几个云层快速变化的阴雨天,这些天的预测误差明显高于晴天。原理并不难理解:光伏功率变化主要受辐照度影响,而辐照度受云层遮挡影响有极强的突然性。气象输入是15分钟粒度采样,两个采样点之间的云层状态变化无法被特征捕捉,这一点对所有模型都公平。

为了缓解这个问题,我试验过两个方向。第一个是加入云量预测或卫星云图数据,但需要额外数据源,接入成本高,短期不好落地。第二个是在特征里加入辐照度的一阶差分和滚动波动率,让模型知道“最近一段时间辐照度波动程度”,这样至少能区分晴天和强对流天气的转换阶段。

实测下来,加入滚动波动率特征后,三个模型的天气突变日MAE都下降了约8%到10%,晴天时段的误差几乎不变。这说明特征是应对突变场景的最有效手段,模型结构在这方面帮不上太大忙。

另一个方向是两阶段预测。先用一个分类模型判断接下来一小时是“晴天、多云还是阴天”,再用对应的回归模型分别预测几种天气下的功率曲线。这种方法我还没在正式项目里完全落地,但思路值得保留——后续如果数据处理量更大,完全可以做成“天气分型预测”。

5.5 模型训练速度与工程落地的取舍

工程层面有个问题也要提一下:三个模型的训练速度差别不小。XGBoost在CPU上2分钟跑完,LSTM和CNN_LSTM在GPU上分别要6分钟和8分钟。如果只是一个研究实验,训练时间差距不算什么。可真要每天滚动预测、每小时更新模型权重,训练耗时就会被放大。

我的实际做法是:日常预测用XGBoost作为生产主力,因为速度快、精度不差、部署简单(不用GPU);每个月用全量数据离线训练一次CNN_LSTM,更新一个更高精度的“夜间批量预测”模型。LSTM在精度上没有明显优势,主要作为技术路线的对照实验保留。

还有一个容易被忽视的工程坑:模型推理速度和请求频率。光伏预测通常是定时任务,一天预测几次,对推理速度要求不高。但如果要做实时监测、秒级预警,就需要考虑模型推理延迟。XGBoost单条样本的推理延迟在毫秒级,LSTM和CNN_LSTM虽然稍慢,但也远低于实时性要求。真正影响系统响应的是特征计算和数据处理链路,我建议把特征工程部分缓存起来,避免每次预测都重新计算一遍所有滞后特征。

6. 可复现的完整训练流程小结

6.1 数据处理的通用流程

把上面所有环节串起来,一个标准的光伏预测数据流程是这样的:

  1. 读取原始CSV,解析时间戳,设置成索引;
  2. 清理夜间、异常、缺失数据;
  3. 构造时间编码、滞后特征、滚动统计特征;
  4. 按时间顺序划分训练集、验证集、测试集;
  5. 用训练集拟合标准化器,转换所有数据集;
  6. 构造序列格式(LSTM/CNN_LSTM)或监督学习格式(XGBoost);
  7. 分别训练模型,保存最佳权重;
  8. 测试集上反标准化,比较预测曲线与实际曲线的差异。

这套流程不局限于光伏预测,凡是“多变量时间序列回归”任务基本都可以复用:电力负荷预测、建筑物能耗预测、设备温度预测,换一下特征工程逻辑,模型代码可以直接沿用。

6.2 代码复现的注意事项

如果你是照着文章里的代码去复现,有几点切身体会要交代。

依赖库版本很重要。我用的PyTorch是2.0以上版本,XGBoost是2.0.3,Pandas是2.1.0。太老的版本在Conv1d的padding处理、XGBRegressor的eval_metric参数上有些行为差异,如果报错优先检查版本。

模型训练要固定随机种子。PyTorch、NumPy、Python自带的random模块都要设置,否则每次跑出来的结果会有小幅波动。特别是LSTM这类强随机性模型,不固定种子的话,同样的代码两次训练结果可能相差2%到3%。

训练日志一定要保存。我习惯在每个epoch结束后记录学习率、训练MAE、验证MAE,最后画成曲线。训练曲线能直接反馈模型有没有收敛、有没有过拟合。如果训练MAE持续下降但验证MAE在某个epoch后反弹,说明过拟合开始出现,早停会起到作用;如果两个MAE都降不下去,大概率是学习率设置不当或特征有问题。

对一个新手来说,建议先跑通XGBoost,再跑LSTM,最后再接CNN_LSTM。这样做的好处是:XGBoost的代码短、调试快,能先帮你把数据链路验证通;LSTM让你理解序列建模的基本范式;最后的CNN_LSTM再引入卷积组件,难度递进,出错时容易定位问题。

6.3 后续可以扩展的方向

这个项目如果继续往下做,我还有几个备选方向。

第一个是多站点联合预测。多个光伏电站处在同一气象区域内,功率变化有较强相关性。用图神经网络或者多任务学习框架,把多个站点作为相互关联的输出节点一起建模,理论上能提升单站点预测精度。

第二个是融合数值天气预报数据。光伏预测的天花板很大程度上取决于未来辐照度的预测精度。历史数据和实时数据已经用到极限,再往下突破要引入外部天气预报数据,比如云量、能见度、降水概率等。

第三个是概率预测。当前的输出是“最可能的功率值”,但实际业务中我们更需要知道“95%置信区间下的功率下限是多少”,这对调度安排更实用。可以用分位数回归或者MC Dropout的方式,让模型输出预测区间而不是单一数值。

第四个是模型压缩与边缘部署。LSTM和CNN_LSTM模型有一定体积,如果要在电站边缘网关实时运行,需要做模型量化和剪枝。不过这里有个常见的坑:有些方案在8位整数量化后精度下降明显,甚至数值完全不动,需要逐层检查量化误差。我建议先做敏感性分析,找对量化敏感的层,再针对性地避开某些结构或者做混合精度处理。

我个人的经验是:这类时间序列预测项目,最有价值的不是把某个模型的指标调到多高,而是建立一套清晰的数据处理、训练评估和模型选型流程。换一个数据集、换一个预测对象时,这套流程能快速复用,才是真正沉淀下来的能力。

如果这个项目对你有点启发,或者你想了解某个环节的细节,欢迎在留言区提出,我挑典型的场景再写一篇展开。就聊到这儿,我先去跑下一组天气场景的训练了。

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

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

立即咨询