风功率预测这件事,做过的朋友都清楚,难的不是模型跑不通,而是“好用”。我一开始从单一LSTM入手,发现换季时预测曲线直接糊成一团;后来加了CNN提取爬坡特征,有一点起色,但风况一复杂、数据分布一变,误差还是上蹿下跳。真正把我这套方案精度稳住的改动,是在模型前面加了一步——用高斯混合模型(GMM)对历史样本做场景聚类,再配合CNN-BiLSTM-attention分段建模,最后用Python训练、Matlab做部署验证,两条语言链路都能跑。这个“聚类+组合深度学习”的思路不是论文里的架空架构,实测在A风电场的数据集上能把归一化均方根误差从0.102压到0.089左右,非常值得复现。无论你是做新能源功率预测的工程师,还是准备算法复现的学生,这篇内容都可以直接对标你自己的数据跑一遍。
1. 为什么把风功率预测做成GMM聚类+CNN-BiLSTM-attention的组合拳
1.1 风功率预测的难点:序列非平稳+气象多模态
风电功率序列和普通工业时序有个本质区别:它不是单一平稳分布下产生的数据。一天之内可能经历大风的持续输出、小风的平缓波动、风向骤变导致的爬坡事件,甚至不同季节的气压场还会让风速分布整体漂移。如果你拿一个模型去硬拟合所有工况,模型就被迫在“各种分布”之间取折中,表现就是预测曲线在大风段偏低、在小风段偏高,整体看起来哪都不对。
我在早期实验里做过对比,单一LSTM模型在连续一个月的测试集上,MAPE大约在12%到14%之间,但按日拆开看,晴天大风日能到8%,极端风况日直接超过20%。这说明不是模型能力不够,而是训练数据把多种完全不同的物理场景混在了一起,迫使一套权重去适应所有场景。这就好比让同一个售货员同时管理生鲜区和家电区,不可能同时把两边的库存都算准。
所以问题的核心慢慢清晰了:预测方法必须先解决数据分场景的问题,再谈用多强的深度学习模型去拟合。
1.2 高斯混合模型解决的是“分场景建模”问题
分场景建模最朴素的思路是K-means先分类,然后每类训练一个预测模型。但K-means有两个问题:一是硬分类,边界处的样本被强制分到某一类,误差很大;二是它假设簇是球形结构,对风电这种特征维度之间高度耦合的数据并不合适。
高斯混合模型在同一任务上更合适,原因有二。第一,GMM本质是多个高斯分布的概率叠加,假如数据真有5种“典型天气形态”,那每个形态用一个高斯分布来近似,整体就能表示成几个高斯分布的加权和;第二,它给的是后验概率,而不是生硬的标签。对某个处于两个场景边界上的样本,它可以输出“属于场景A的概率0.6,属于场景B的概率0.4”,这个软概率在后面做模型融合时非常有用。
实际应用中,我把NWP气象特征(风速、风向、温度、湿度、气压)和历史功率特征拼成一个聚类特征矩阵,用GMM分成5个簇,每个簇对应一种隐含的风况形态。训练阶段,每个簇单独训练一套CNN-BiLSTM-attention模型;预测阶段,先计算当前输入窗口属于各簇的概率,然后按概率加权融合各模型的输出,相当于让最适合当前风况的模型多“说话”。
1.3 CNN、BiLSTM与Attention各司其职,为什么拼在一起
先说CNN。它的作用不是替代循环网络去建模时间依赖,而是提取短窗口内的局部变化模式。风功率序列里的爬坡事件、功率突升突降,本质上就是局部窗口内的一种“形状特征”。Conv1D通过在时间维度上滑动卷积核,能自动找出“风速连续上升三拍后功率开始响应”之类的局部模式,参数共享还让它不容易过拟合。
然后是BiLSTM。LSTM大家都熟,它的作用是把长距离时间依赖编码进隐藏状态。这里用双向结构,是在训练阶段让每个时间步同时看到序列前后两个方向的信息。有人会问:预测未来时未来信息不存在,双向有什么用?关键在于,这里处理的是“已知完整历史窗口的特征提取”问题。比如当前窗口长度为24步,这24步全部是已知观测值,双向LSTM可以让窗口中间的历史观测借助前后文信息得到更好的编码,Attention再从中挑出真正影响未来功率的那几步。如果嫌双向训练慢,也可以退化为单向LSTM,代价大约是精度下降0.003到0.005的NRMSE。
最后是Attention。BiLSTM虽然能处理长序列,但隐藏状态对早期信息的记忆会衰减。Attention层的思路是:不要求LSTM把所有信息都压缩进最后一个状态,而是保留全部时间步的输出,然后让网络自己学一组权重,对时间步进行加权求和。权重越大,代表该时间步对预测结果的影响越大。在风功率预测里,Attention权重能直观反映模型关注了哪个历史时刻的风速变化,这种可解释性在工程汇报里也很加分。
三者串起来就成了一个特征加工的流水线:CNN负责抓局部形状,BiLSTM负责编码全局上下文,Attention负责“决策”哪些时刻值得参考,最后由全连接层输出预测功率。加上GMM聚类在前面做场景分流,整个方案才算闭环。
2. 数据准备与特征工程:决定预测上限的基础
2.1 用哪些数据:SCADA与NWP气象特征的选择
在A风电场实操时,我用的是SCADA历史数据加NWP数值天气预报数据。风电场SCADA系统记录了每台机组的风速、风向、有功功率、机舱温度等运行数据,时间粒度为10分钟。NWP数据则提供未来几小时到几天的风速、风向、温度、湿度、气压预报值,是短期功率预测最重要的输入之一。
注意,SCADA里的风速是机组轮毂高度处的实测风速,NWP里的风速是网格预报值。两个数据源在空间和时间上不能天然对齐,需要做匹配。我通常把NWP插值到风电场机位坐标,再按时间戳对齐到SCADA的10分钟粒度。特征选择上不要贪多,核心特征保留:NWP风速、NWP风向(拆成sin和cos)、温度、湿度、气压,以及历史功率序列。实测增加“历史功率一阶差分”这个特征对爬坡预测帮助很大,因为它直接刻画了功率的变化趋势。
2.2 数据清洗:区分真实功率为0与异常功率为0
风功率数据里最坑的是“0值”。风速很小时风机不发电,功率是0,这属于正常物理状态;风机故障停机、检修限电时功率也是0,但背后的原因完全不同。如果不清洗,模型会学到“风速低的时候功率为0、风速高的时候功率也可能为0”,直接把预测值拉低。
清洗逻辑按顺序做三道:第一,剔除风速高于切出风速(通常25m/s)或低于切入风速(通常3m/s)且功率为0的正常状态,这类点不代表预测误差;第二,剔除功率超过额定功率1.1倍或小于0的明显越限点;第三,结合SCADA的机组运行状态码,把故障停机、通讯中断产生的记录整体删除。清洗后数据集大约会少掉8%到12%的样本,这是正常的。
另外要留意连续0值段。如果一小段区间功率一直为0,而风速在额定风速附近,那多半是限电或者脱网,不可简单删除,最好做标记。限电时段在预测时本身不可预测,硬让模型学反而是毒药,我一般是单独打标签,不参与训练。
2.3 特征处理与归一化:风向不能直接做成数值特征
风向是角度量,0度和360度物理含义相同,如果直接把它当数值特征送进模型,模型会认为0和360差距很大,产生一个错误的突变边界。必须把角度拆成两个分量:“风向正弦”和“风向余弦”,这样任意角度都被映射到一个连续圆周上,角度相近的风向在特征空间中的距离也相近。
归一化这一步,很多人直接用全数据集的min/max,这在时序预测里是严重错误。因为测试集是未来的数据,训练时不应该“偷看”未来数据的统计信息。正确做法是只在训练集上计算均值、方差或min/max,然后用同一套参数去变换验证集和测试集。NWP数据和历史功率数据最好分别归一化,因为两者量纲和分布差异很大,合并归一化会把一些特征压缩得几乎没有区分度。功率序列我会额外做一次平方根变换,让功率峰值没有原始数据那么尖锐,模型拟合起来更平滑。
2.4 滑动窗口构造与数据划分的防泄漏细节
时序预测的标准做法是滑动窗口构造样本。以10分钟一条数据为例,如果输入窗口取24步,就是4小时的历史信息;输出步长取6步,就是预测未来1小时。构造后每个样本的shape为(24, 特征数),标签shape为(6,)。
需要再三强调:划分训练集、验证集、测试集时不能随机打乱。随机打乱会让模型“见过”未来区间内的信息,验证结果虚高,部署后完全不是那回事。我按时间排序切分为:2018到2019年做训练,2020年前8个月做验证,后4个月做测试,三个区间没有重叠。GMM聚类是在训练集上完成的,验证集和测试集用同一套GMM参数计算后验概率,这也是很多人踩过的泄漏点。
另外,样本构造时相邻窗口有重叠是正常的,但要注意验证集和测试集不能包含与训练集重叠的时间片。比如预测步长为6步,训练集最后6个样本的时间区间会延伸到验证集,因此切分时训练集末尾要丢弃至少“窗口长度+步长”个样本,确保严格无重叠。
3. 高斯混合模型聚类的原理与实现细节
3.1 GMM的核心数学原理与EM参数求解
高斯混合模型是K个高斯分布的概率组合,概率密度函数为:
p(x) = Σ(k=1到K) π_k · N(x | μ_k, Σ_k)
其中π_k是第k个分量的混合系数,满足非负且和为1;μ_k是均值向量,Σ_k是协方差矩阵。整个模型要估计的参数就是每个分量的π、μ、Σ。直接用极大似然估计求解析解不可能,因为样本属于哪个分量是不可观测的隐变量,所以标准解法是EM算法。
EM算法分两步交替:E步根据当前参数计算每个样本属于每个分量的后验概率(责任度);M步根据责任度重新加权更新π、μ、Σ,让似然函数增大。反复迭代到似然变化小于阈值即收敛。在Python里,sklearn的GaussianMixture已经把EM封装好了,但你最好知道它是在干什么,否则连要调哪个参数都不知道。
实现时还有两个细节影响很大:一是协方差类型。默认是full,也就是每个分量有独立的完整协方差矩阵,拟合能力强但参数量大;如果特征维度高、数据量不足,可以用diag减少参数。二是初始值。EM对初始值敏感,最好用k-means结果做初始化,或者设置多个随机起点取似然最大的那个,sklearn里的init_params='kmeans'就是干这个的。
3.2 聚类分量数的确定:用BIC而不是盲目拍脑袋
GMM的簇数K需要提前指定,选多少不能拍脑袋。常用方法是BIC(贝叶斯信息准则)。BIC在似然值的基础上加上对参数数量的惩罚项,平衡“拟合得好不好”和“模型复不复杂”。K太小,模型表达不了数据的多模态结构;K太大,参数爆炸且容易过拟合。
实际操作时,我跑K=2到K=10,画出BIC曲线,选择BIC最低或曲线下降速率明显变缓的“肘点”。A风电场的数据在K=5时BIC明显低于K=4,K=6以后下降就平缓了,所以定5个分量。实现代码很短:
from sklearn.mixture import GaussianMixture # X_cluster: 聚类特征矩阵,建议包含NWP特征+历史功率+时间特征 X_cluster = ... # shape: (n_samples, n_features) bics = [] for n in range(2, 11): gmm = GaussianMixture(n_components=n, covariance_type='full', random_state=42, max_iter=200) gmm.fit(X_cluster) bics.append(gmm.bic(X_cluster)) print(f"K={n}, BIC={bics[-1]:.2f}")如果分类结果里有某个簇样本量太小,比如不足总样本的5%,我一般有两种处理:一是把相邻簇合并,二是降低K重新聚类。宁可少分一个场景,也不要让某个模型在少量样本上硬训练。
3.3 软聚类概率如何参与后续建模:分模型与特征注入两种方式
GMM聚类结果有两种用法,对应两种建模方案。
第一种是“分模型”,就是本文主推的:把训练样本按最大后验概率分到5个子集,每个子集训练一套CNN-BiLSTM-attention模型。预测时计算输入窗口属于每个簇的后验概率,然后选概率最大的模型输出,或用概率加权融合多个模型的结果。这种方式的好处是每个模型专注一种风况,针对性极强,缺点是训练成本翻倍。
第二种是“特征注入”,把GMM后验概率拼接到每个样本的特征尾部,作为附加特征输入同一个模型。这种方式轻量,实现简单,但模型需要自己学会利用聚类信息,效果不如分模型那么直接。
我的倾向是:如果数据量充足,用分模型;如果训练数据本身只有一两年的量,用特征注入更稳。预测阶段的融合代码如下:
gmm_proba = gmm.predict_proba(X_window.reshape(1, -1)) pred = sum(p * model_predict(model_k, X_window) for p, model_k in zip(gmm_proba.flatten(), models))这里把5个模型的输出按后验概率加权平均,效果比选概率最大的单模型要稳,尤其当输入窗口正好落在两个场景边界上的时候。
3.4 Python与Matlab中的GMM实现
Python端直接用sklearn,这个没有太多讨论空间。Matlab端则是Statistics Toolbox里的fitgmdist,功能完备:
% 假设 X 是聚类特征矩阵,每行一个样本 rng(42); gmm_model = fitgmdist(X, 5, 'RegularizationValue', 1e-6); [labels, ~, post] = cluster(gmm_model, X); % 硬标签 post = posterior(gmm_model, X); % 后验概率很多人用Matlab做风功率预测是因为项目整体部署在Matlab环境里,训练好的GMM参数可以导出成.mat文件,预测时直接用posterior函数计算各簇概率,和Python端保持一致。有一点要注意,fitgmdist默认用full协方差,特征维度五六十以上时容易报“ill-conditioned covariance”错误,加一个很小的RegularizationValue就能缓解。
4. CNN-BiLSTM-attention模型搭建与训练实操
4.1 模型结构拆解与每一层的设置逻辑
整套模型的输入是滑动窗口样本,shape为(batch_size, 24, n_features)。以一个具体配置为例:24个时间步,15个特征(NWP风速、风向sin、风向cos、温度、湿度、气压、历史功率、功率差分及若干时间特征)。
第一层是Conv1D,64个卷积核,卷积核大小3,padding保持序列长度不变,激活函数ReLU。卷积核大小为3的含义是每次看连续的3个时间步,能够捕捉短时的爬坡趋势。加上一层MaxPooling1D(pool_size=2),把序列长度压缩到12,作用是降低计算量并让后续网络关注更粗粒度的模式。第二层Conv1D增加到128个卷积核,进一步提取更复杂的局部特征,后面接BatchNormalization,防止深层网络梯度不稳定。
经过卷积池化后,序列进入BiLSTM层,隐藏单元128,return_sequences=True,保留每个时间步的输出。然后再接一个自定义Attention层,对不同时间步加权求和,输出一个上下文向量。最后是两个全连接层,先是64维ReLU,加Dropout(0.2),再输出6维线性层对应未来6个时间步的功率值。
这个结构里,每一层的取舍都有讲究。比如第一层卷积核数不适合一开始就设128,因为特征还比较原始,卷积核多反而容易过拟合;BiLSTM隐藏单元设128,是因为样本量在十万级时,256单元的训练时间几乎翻倍,精度提升只有零点几个点,不划算;Attention放在BiLSTM之后而不是CNN之后,是因为要在语义层面加权时间步,而不是在局部形态层面。
自定义Attention层的Keras实现如下:
import tensorflow as tf from tensorflow.keras import layers class AttentionLayer(layers.Layer): def __init__(self, **kwargs): super(AttentionLayer, self).__init__(**kwargs) def build(self, input_shape): self.W = self.add_weight(name='att_weight', shape=(input_shape[-1], input_shape[-1]), initializer='glorot_uniform', trainable=True) self.b = self.add_weight(name='att_bias', shape=(input_shape[-1],), initializer='zeros', trainable=True) self.u = self.add_weight(name='att_u', shape=(input_shape[-1],), initializer='glorot_uniform', trainable=True) super(AttentionLayer, self).build(input_shape) def call(self, x): uit = tf.tanh(tf.tensordot(x, self.W, axes=1) + self.b) ait = tf.tensordot(uit, self.u, axes=1) a = tf.nn.softmax(ait, axis=1) a = tf.expand_dims(a, -1) return tf.reduce_sum(x * a, axis=1)4.2 损失函数、优化器与评价指标的选择
损失函数我推荐Huber损失。它的特点是当误差小于某个阈值δ时等价于MSE,误差大于δ时退化为MAE,不会让个别极端风况的大误差主导整个训练。风电数据里极端样本偏偏不少,用纯MSE训练出来的模型常常为了压住一两个离群点,牺牲一大片正常样本的精度。δ取经验值1.0。
优化器首选Adam,初始学习率1e-3。我习惯再加一个学习率衰减策略:验证损失连续5个epoch不下降时,学习率乘以0.5。这个操作粗糙但极有效,它能让模型前期快速收敛,后期精细下降,避免在最优解附近来回震荡。
评价指标不要只看MAPE。风功率预测的MAPE有个天然缺陷:实际功率接近0时,再小的绝对误差都会导致MAPE爆炸。行业里更常用的是归一化均方根误差NRMSE和归一化平均绝对误差NMAE,分母用装机容量:
NRMSE = sqrt(mean((y_true - y_pred)^2)) / P_installed
NMAE = mean(abs(y_true - y_pred)) / P_installed
此外要同时报告R²,防止NRMSE好看但趋势完全跟不上。
4.3 训练配置与调参:batch_size、学习率、早停和交叉验证
训练配置直接给一组在我项目中稳定工作的值:batch_size=64,epochs上限100,EarlyStopping的patience=10,即验证损失连续10个epoch没有下降就停止训练,恢复最优权重。数据集约12万样本时,一个模型大约训练3到5分钟(单张消费级GPU),5个簇的模型串行训练总共也就20分钟左右,完全可以接受。
batch_size这里有个细节。太大(比如256)虽然收敛快,但会让模型偏向高频样本,极端风况模式容易被稀释;太小(比如16)训练噪声太大,收敛不稳定。64是风电样本量下的均衡点。
如果你担心某个簇的样本量太小,可以在分模型训练时对该簇样本做轻微的过采样,或者对损失做样本加权。我在做GMM聚类后发现5个簇占比大致是28%、24%、21%、16%、11%,最少的簇也有1.3万样本,独立训练没有问题。如果某个簇样本量低于8000,我会果断把它合并到特征最接近的相邻簇。
4.4 主程序中的建模与验证代码
整个训练主流程可以概括为:加载并清洗数据、构造特征和滑窗样本、GMM聚类、循环训练5个模型、验证集评估融合效果。核心代码如下:
# X_train: 滑窗样本 (n, 24, features) # y_train: 标签 (n, 6) from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, Conv1D, MaxPooling1D, BatchNormalization, Bidirectional, LSTM, Dense, Dropout models = [] for k in range(n_components): idx = np.where(train_cluster_labels == k)[0] Xk, yk = X_train[idx], y_train[idx] inp = Input(shape=(24, X_train.shape[-1])) x = Conv1D(64, 3, padding='same', activation='relu')(inp) x = MaxPooling1D(pool_size=2)(x) x = Conv1D(128, 3, padding='same', activation='relu')(x) x = BatchNormalization()(x) x = Bidirectional(LSTM(128, return_sequences=True, dropout=0.1))(x) x = AttentionLayer()(x) x = Dense(64, activation='relu')(x) x = Dropout(0.2)(x) out = Dense(6)(x) model = Model(inp, out) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss=tf.keras.losses.Huber(delta=1.0), metrics=[tf.keras.metrics.RootMeanSquaredError()]) model.fit(Xk, yk, validation_data=(X_val_k, y_val_k), batch_size=64, epochs=100, callbacks=[tf.keras.callbacks.EarlyStopping(patience=10), tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)]) models.append(model)验证阶段,把每个样本先过GMM求后验概率,再按概率融合模型输出,最后统一反归一化。反归一化时要记得功率平方根变换的逆操作,否则预测结果会系统性偏低。
5. Python训练、Matlab复现与部署的衔接方法
5.1 Matlab深度学习环境准备与网络搭建
很多实际项目最终要交给运行部门,而运行部门的环境可能只有Matlab。所以我会在Python里把模型训练好,再在Matlab里做复现或直接导入权重。Matlab端需要Deep Learning Toolbox,网络搭建有两点和Python显著不同。
第一,Matlab的网络层顺序用layer数组声明,而不是像Keras那样函数式拼接。第二,Matlab的bilstmLayer是自带支持的双向LSTM层,你不需要像Keras那样包一层Bidirectional。一个可用的简化版网络定义如下:
numFeatures = 15; numResponses = 6; layers = [ sequenceInputLayer(numFeatures) convolution1dLayer(3, 64, 'Padding', 'same') reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(3, 128, 'Padding', 'same') batchNormalizationLayer reluLayer bilstmLayer(128, 'OutputMode', 'sequence') fullyConnectedLayer(64) reluLayer dropoutLayer(0.2) fullyConnectedLayer(numResponses) regressionLayer ]; options = trainingOptions('adam', ... 'InitialLearnRate', 1e-3, ... 'MaxEpochs', 80, ... 'MiniBatchSize', 64, ... 'ValidationData', {XVal, YVal}, ... 'Plots', 'training-progress'); net = trainNetwork(XTrain, YTrain, layers, options);注意Matlab的序列输入默认是cell数组,每个cell是一条样本、一个T×C矩阵。而我Python端准备的数据是(batch, time, features),导入Matlab前需要转成cell数组,具体做法是用reshape和mat2cell绕一步,这步常常被人忽略。
5.2 自定义Attention层在Matlab中的处理思路
Matlab的自定义层比Python麻烦一些,需要继承nnet.layer.Layer,实现predict和forward两个方法。Attention层虽然数学不复杂,但要处理的维度变化和反向传播写法容易出错。我的建议是如果只是为了部署而非训练,不要在Matlab里重写Attention。
更省事的方案:在Python端把BiLSTM各时间步输出序列保存下来,在Matlab用硬编码的加权公式复现Attention的前向计算。Attention的权重矩阵和向量导出成.mat文件后,Matlab需要做的只是几个矩阵乘法和softmax,完全不需要自定义层,也不会踩梯度检查的坑。只有当你想在Matlab里从零训练时,再考虑自定义层:
classdef attentionLayer < nnet.layer.Layer % 简化版Attention层,仅支持前向 properties (Learnable) Weights Bias end methods function layer = attentionLayer(args) % 构造函数省略 end function Z = predict(layer, X) U = tanh(X * layer.Weights + layer.Bias); A = softmax(U, 1); Z = sum(X .* A, 1); end end end这个方法在维度shape以及批量数据size上有不少细节要处理,我也是查了好几轮文档才把单条样本跑通。如果你只是要快速验证效果,建议优先走Python导入权重的路线。
5.3 结果对比:Python与Matlab版本的一致性验证
我在同一份测试集上分别用Python和Matlab做了预测,结果对比相当接近。
| 实现平台 | NRMSE | NMAE | R² |
|---|---|---|---|
| Python(keras训练+预测) | 0.089 | 0.065 | 0.91 |
| Matlab(导入权重+前向预测) | 0.090 | 0.066 | 0.91 |
| Matlab(从零训练简化网络) | 0.096 | 0.071 | 0.89 |
Python和Matlab之间的微小差异主要来自浮点数累加顺序和softmax实现的细微差别,工程上完全可以接受。但Matlab从零训练简化网络时,因为去掉了Attention层,NRMSE会高0.005到0.008,这说明Attention在这个任务里确实贡献了一部分精度。
这里提供一条经验:把Python训练好的模型权重导出为.mat文件时,命名一定要统一,我习惯用“W_conv1”、“b_lstm_fw”这种前缀命名,Matlab端直接load进来对应赋值,避免对错层。一开始我吃过一次亏,权重错位后预测结果完全随机,排查了很久才发现是变量名对不上。
6. 常见问题与调优实录
6.1 预测功率曲线整体偏低,峰值跟不上怎么办
这个现象在我第一次完整跑通时也出现过,预测功率在大风段明显偏低,小风段倒是贴合。排查后原因主要有三个。
第一是损失函数的问题。MSE对峰值附近的大误差惩罚很大,但训练样本里大风时段占比本来就少,MSE会让模型偏向多数的小风样本。换成Huber损失、把阈值调高一点,峰值部分会改善很多。第二是特征表达问题,如果输入里没有“历史功率差分”这个特征,模型很难判断当前是爬坡还是平稳,峰值自然跟不上。第三是样本不平衡,GMM聚类后大风簇样本偏少,我的方案是给大风簇的损失乘以一个1.2的权重,或者对该簇做时间窗口重叠采样,扩大样本量。
修复后再看测试集,预测峰值能贴得比较紧,NRMSE从0.098降到了0.089左右。
6.2 聚类簇数对预测精度的影响有多大
簇数直接决定了分模型的数量,影响巨大。我用BIC选了5个簇,但为了验证这个选择,我还试过K=3、4、6、7,结果是K=5时NRMSE最低。K=3时不同风况混在一起,模型泛化差;K=6时出现了两个簇特征高度重叠,几乎等于把一个场景拆两半,模型间冗余度高,融合收益下降;K=7时有一个簇样本量不到总体的5%,那个模型的预测结果明显抖动。
如果你的数据量偏少,或者BIC曲线没有明显拐点,我建议保守一点,从3个簇起步,先跑通全套流程再逐步增加簇数。另外,聚类特征里如果只放NWP特征不放历史功率,聚类结果容易和实际功率脱节,分出来的场景并不能反映功率状态,这一点要特别留意。
6.3 梯度爆炸与过拟合的排查思路
我在调参过程中遇到过两次训练不收敛的情况。一次是BiLSTM单元数从128调到256后,验证损失突然跳到很大值然后震荡,明显是梯度爆炸。这种问题优先检查两件事:学习率是不是太高,有没有做梯度裁剪。我在BiLSTM层加上dropout并设置clipnorm=1.0后,问题就消失了。Keras里可以通过compile时传入optimizer=Adam(clipnorm=1.0)实现,很省事。
另一次是过拟合,具体表现是训练损失持续降低,但验证损失在20个epoch后开始回升。这通常是模型容量太大而数据量不够导致的。解决办法是增加Dropout比例、减少BiLSTM单元数,或者干脆增加EarlyStopping的patience并恢复最佳权重。我不是很推荐一上来就套用很大的网络结构,对风电这种信噪比不算高的时序,128单元往往比256单元泛化更稳定。
6.4 多步预测误差累积:能预测几小时是关键问题
短期功率预测通常要预测未来1到4小时,但多步预测有一个绕不开的问题:越往后的时间步误差越大。我对比了两种预测方式,一是直接多步输出,模型一次输出6个时间步;另一种是滚动预测,把前一步的预测值当作输入的一部分,一步一步往后推。实测下来滚动预测在前两步时误差略小,但从第3步开始误差反而急剧变大,因为预测误差被当作“真实观测”喂回模型,逐级污染后续输出。
在A风电场测试集上,直接多步的规律是:预测未来10分钟NRMSE约0.072,1小时约0.089,2小时约0.105,4小时约0.128。如果业务要求预测4小时以上,建议把模型输出步数加大,或者改用Seq2Seq加Attention解码的结构,而不是死磕迭代预测。如果只预测未来1小时内,当前这套方案完全够用。
最后再分享一个小技巧:预测之后一定要做一次功率曲线合理性校验。如果某个时段风力平稳、但模型突然输出一个大爬坡,多半是输入数据出了问题,比如某个NWP特征跳变、或者SCADA数据有坏点。我在部署链路里加了一个简单的卡边界逻辑:预测功率超过装机容量或低于0时置为边界值;如果连续3个时间步的预测功率变化率超过物理极限,就会触发报警提示人工检查数据源。这套辅助校验避免了多次线上误报,很推荐你也加一道。
按照我这几轮调参的体会,这套GMM聚类加CNN-BiLSTM-attention的方案并不是模型越复杂越好,而是每一步都要对着数据特征做取舍。先跑通一条最简单的链路,观察误差集中在哪些时段,再针对性地加聚类、加特征、加Attention,效果会比你一次性堆一个大模型稳定得多。