做风电短期功率预测的朋友应该都有体会:模型不是调不好,而是“有时候好,有时候坏”。尤其是换一个风电场、换一个季节,之前跑得不错的网络结构立刻“现原形”。我去年在一套现场数据上调了好几版模型,最深的感受是——如果只盯着网络结构,忽略数据本身的多模态特性,精度很难再有本质提升。后面把高斯混合模型(GMM)聚类作为前置环节,再用CNN-BiLSTM-attention组合模型做预测,Python和MATLAB两条线都完整实现了,才算是把这件事彻底跑通。
这篇内容就是把我的完整做法拆开讲一遍:从为什么需要GMM聚类、聚类特征怎么选,到CNN-BiLSTM-attention每一层在干什么,再到双语言落地的代码骨架和实际踩坑。适合正在做新能源功率预测的研究生、风电场功率预测系统的开发工程师,也适合想给自己的时序预测任务加上“场景自适应”能力的朋友参考。
1. 风功率预测难在哪:单一模型为什么会在不同天气下“忽好忽坏”
1.1 风电出力数据自带三个“刺头”特性
风电功率序列不是平稳的工业信号,它天生带着几个让模型头疼的属性。
第一个是波动性与间歇性。风速受地形、气压系统、昼夜热力差异影响,分钟级和小时级波动都非常明显。风机功率曲线本身又是一段分段非线性映射:切入风速以下出力恒为0,额定风速以上出力被钳在额定值,切出风速附近又可能直接甩负荷。这个分段特性导致功率目标变量不是一个光滑的连续函数,某些区间的预测天然更难。
第二个是场景异质性。同样是风速8m/s的样本,一个来自春季大风过程,一个来自夏季阵性天气,背后的气压梯度、湍流强度、风向稳定度完全不同。数据分布根本不是理想化的“单峰模式”,而是多个概率分布的混合体。如果拿一个全局模型去拟合所有样本,模型只会在各个模式之间取一个“折中解”。
第三个是强时序依赖与突变。功率爬坡事件就是典型例子:十几分钟内出力从10%跳到90%。这种事件在训练样本里占比不高,但一旦出现,常规模型往往来不及反应。因为普通模型更擅长学到“平稳状态下的关系”,对极端转移过程缺乏敏感度。
1.2 “一个模型吃天下”的失败模式
很多人一开始的做法是直接把历史功率、风速、风向等特征一股脑喂给深度网络。训练阶段loss下降顺利,验证集指标也不算差,但一旦进入评估阶段,就会发现一个规律:晴天小风时预测得挺好,大风过程前几个小时预测完全跟不上,紧接着的功率爬坡段更是整体滞后。
这个现象的本质不是网络容量不够,而是训练数据里混合了多种分布,模型被迫用一套参数去拟合多个互不相容的模式。深度学习虽然能逼近任意复杂函数,但那只是在数据来自同一个连续分布的前提下成立。当数据是“若干个不同天气状态的叠加态”时,全局模型容易学会平均规律,牺牲掉少数但重要的极端模式。
所以我在第二次迭代时改成了“先分场景、再建模型”的思路:先用高斯混合模型(GMM)把历史样本按天气-出力状态软聚类成几个子集,再在每个子集上让CNN-BiLSTM-attention模型去学习各自的映射规律。这就是整个方案的核心逻辑——不是用更复杂的网络在烂数据上硬扛,而是先把不可分的问题变成几个更可分的问题。
2. 用GMM给天气场景“分桶”:特征构造、聚类数与软归属的工程选择
2.1 为什么选GMM而不是K-Means
提到聚类,很多人第一时间想到K-Means。但在风功率场景聚类这个任务上,GMM具备两个关键优势。
K-Means是硬聚类,每个样本只能属于一个簇,距离度量是欧氏距离。风电数据里不同天气过程往往存在重叠区,一个样本可能同时具备大风过程和平稳过程的特征,硬分类会造成样本被强行归到某一类,边界处信息丢失。
GMM是生成式模型,它假设整体数据由若干个高斯分布叠加生成,输出的不是0/1归属标签,而是每个样本属于每个簇的后验概率。这是一种软聚类,样本可以“八成属于大风过程、两成属于过渡过程”。对于预测场景,这个概率信息本身就有价值,可以直接作为附加特征使用。
从数学直觉看,GMM的概率密度表示为:
p(x) = Σπ_k · N(x | μ_k, Σ_k)
其中π_k是第k个高斯分量的权重,μ_k和Σ_k是该分量的均值和协方差矩阵。每个分量可以理解成一种“天气运行状态”的统计描述。
2.2 聚类特征怎么构造才不踩泄漏
聚类使用的特征直接决定场景划分的物理含义。我建议从两组变量里挑选。
第一组是气象相关特征,包括测风塔或机舱的风速、风向、温度、气压、湿度,如果有NWP(数值天气预报)场数据,还可以加入预报风速和预报风向。第二组是历史功率的统计特征,包括过去1小时平均功率、功率标准差、最大变化率、上一时刻功率。
这里有几个必须注意的细节。
风向是角度变量,不能直接用数值喂进去。349度和3度在数值上差346,但物理上只差14度,直接用原始值会导致聚类错误地分成两个方向。处理方式是把风向拆成sin和cos两个分量。
归一化要用训练集的均值和方差去变换验证集和测试集。很多人图省事,把全部数据放在一起做StandardScaler,这在传统机器学习里也许问题不大,但在时序预测里属于信息泄漏——测试阶段的统计量被训练阶段看到了,评估结果会虚高。
最关键的一条:聚类特征里绝对不能混入未来时刻的真实功率。如果在聚类的输入里包含“未来6小时平均功率”,聚类结果会异常干净,模型预测效果也“看上去很美”,但实际部署时根本拿不到这个量,整个方案就是空中楼阁。
2.3 聚类数怎么选:既要看指标也要看样本量
GMM的聚类数k需要人为指定,选多了场景碎、每个子集样本少,模型容易过拟合;选少了场景划分太粗糙,起不到分而治之的效果。
我的做法是先用BIC曲线辅助判断:对k从2到8逐个训练GMM,计算BIC值,找拐点位置。BIC考虑了模型复杂度和似然度的平衡,比AIC更偏好简单模型,对风电场景比较合适。同时参考轮廓系数做一个交叉验证,但轮廓系数在概率软聚类里意义有限,只能当辅助参考。
更重要的约束来自业务端:每个聚类子集的样本量至少要能支撑一个深度网络的训练。我一般要求子集样本不少于5000条,否则宁可把k调小1个。实际项目中k落在3到6之间比较常见,多数场景下k=4就能覆盖“大风过程、平稳小风、过渡爬坡、极端切出”这几类典型状态。
聚类之后,模型落地有两种方式。第一种是每个聚类单独训练一个专属模型,预测时对NWP气象特征做GMM后验推断,取最大概率对应的模型。第二种是把隶属度向量(样本属于每个簇的概率)拼接到网络输入特征里,只保留一个全局模型。
从我的实测经验看,第二种方式更稳,鲁棒性高。它的好处是模型不会因为归属判断失误导致完全偏离,聚类信息只是引导模型“更重视某种模式”。如果各场景的数据分布差异极大,再考虑第一种方式。折中方案是在共享模型基础上加入场景ID的embedding,效果接近细粒度建模,但工程复杂度更高。
3. CNN-BiLSTM-Attention网络拆解:三个模块如何分工处理功率序列
3.1 输入输出结构与多步预测策略
整个预测任务的设定是:输入过去6小时的历史功率和气象数据,预测未来24小时的功率曲线,时间分辨率为15分钟,也就是用时间窗长度T=24个点,预测未来H=96个点。
输入张量形状是[样本数, 24, 特征数],特征数由气象变量和历史功率组成,比如风速、风向sin、风向cos、温度、气压、历史功率,再算上聚类隶属度,通常是8到12维。如果是短临预测,还会在这段时间窗上拼接NWP未来时段的预报气象特征,这个可以按项目数据条件决定。
多步预测有两种主流做法:递归预测把上一步输出塞回输入端,循环多次得到未来96个点;直接多步预测则让网络最后输出一个96维向量。递归预测实现简单但误差会随步长累积,几步之后输出曲线要么偏向均值、要么振幅失真。我在这个项目里用直接多步预测,虽然网络输出层参数变多,但训练稳定、预测曲线的相关性明显更好,推荐优先考虑。
3.2 CNN层:在时间轴上抓功率突变和局部气象事件
网络输入先进入一维卷积模块。这里CNN处理的对象不是图像,而是时间窗口内的序列,卷积核在时间维度上滑动,相当于在寻找短时间内的局部模式。
我用了两层Conv1d,第一层把原始特征从8维映射到32维,第二层从32维映射到64维,卷积核大小取3,padding设为1以保证序列长度不变。激活函数用ReLU。这里刻意不用池化层,因为时序预测任务非常依赖时间位置的精度,池化虽然能降低计算量,但会模糊掉爬坡发生的准确时刻,属于得不偿失。
风功率序列里的局部事件是CNN最擅长捕捉的信息:风速在半小时间隔里突然从6m/s抬到12m/s,对应功率从20%冲到70%,这种突变模式在一维卷积的感受野里非常明显。
3.3 BiLSTM层:用双向记忆建模长程依赖关系
卷积层之后接入双向长短时记忆网络。常规LSTM只能利用过去的信息,而BiLSTM同时考虑过去和未来两个方向的时间依赖,对风功率序列意义很大。
风电出力经常有“爬坡前平缓、爬坡后维持高位”的结构,单向LSTM在低层可以捕获爬坡前的趋势,但很难意识到当前时刻和后续时段的联动。BiLSTM让每个历史时刻的隐状态都携带前后双向的上下文信息,相当于是用“回看”和“前瞻”两种视角同时建模这段序列。
我在结构上设定了隐层单元数64,双向输出维度变成128(两个方向拼接),LSTM层数设为1。层数不是越多越好,风电数据的时间窗口只有24步,堆两层以上参数量大增,收敛变慢,还容易过拟合。这里用一层就够了。
3.4 Attention层:自适应决定“哪个时刻更重要”
BiLSTM输出的每个时间步都对应着一个128维的隐状态向量,但并非所有时间步对预测未来的贡献一样大。Attention模块负责学习一组权重,对时间步做加权求和。
我采用的是加性注意力,计算方式为:
score_t = v^T · tanh(W · h_t + b)
权重系数通过softmax归一化得到α_t,最终上下文向量c = Σα_t · h_t。这个权重可以直观理解成模型自己学会了“什么时候应该重点看”:比如在爬坡事件来临时,历史窗口中功率剧烈变化的几个时间步会被赋予更高的注意力权重;在平稳小风时段,模型则会把注意力分散到更多常规时间步。
attention还有一个附加好处:它的输出可以可视化。预测完成后把α_t画出来,能直接看到模型在预测未来96点时,重点参考的是过去哪个时段的数据。这对排查模型行为、向非技术同事解释预测结果都很实用。
CNN-BiLSTM-attention的整体参数配置可以整理成一张表:
| 网络层 | 输出维度 | 关键参数 | 作用 |
|---|---|---|---|
| Conv1d | [24, 32] | kernel=3, ReLU | 提取时间局部事件特征 |
| Conv1d | [24, 64] | kernel=3, ReLU | 二次抽象局部特征 |
| BiLSTM | [24, 128] | hidden=64, 双向 | 建模双向时序依赖 |
| Attention | [1, 128] | 加性注意力 | 加权融合关键时间步 |
| FC | [1, 96] | 全连接 | 直接输出未来96点功率 |
3.5 训练参数与优化策略
训练时损失函数用均方误差MSE,优化器用Adam,初始学习率1e-3,batch size设为64,epoch上限200。配套了早停策略:验证集loss连续10个epoch不下降就停止训练,同时用ReduceLROnPlateau在loss平台期把学习率降到1e-4。
这里有一个容易忽略的细节:数据集划分不能随机打乱。时间序列存在自相关性,随机划分会让训练集里混入测试时段附近的数据,属于信息泄漏。正确做法是按时间顺序切分:前70%作训练集,中间10%作验证集,最后20%作测试集,并且在训练时才打开shuffle,验证和测试阶段保持原始顺序。
4. Python与MATLAB双版本落地:数据接口、核心代码与训练策略
4.1 Python侧:GMM聚类与PyTorch模型骨架
Python侧我以PyTorch为主,配合sklearn的GaussianMixture。数据统一从CSV加载,列包括时间戳、风速、风向、温度、气压、历史功率等。
先做特征工程和GMM聚类:
import pandas as pd import numpy as np from sklearn.mixture import GaussianMixture from sklearn.preprocessing import StandardScaler data = pd.read_csv('wind_farm_data.csv', parse_dates=['timestamp']) data['hour'] = data['timestamp'].dt.hour # 风向分解为sin/cos分量 data['wd_sin'] = np.sin(np.deg2rad(data['wind_direction'])) data['wd_cos'] = np.cos(np.deg2rad(data['wind_direction'])) feature_cols = ['wind_speed', 'wd_sin', 'wd_cos', 'temperature', 'pressure', 'power', 'hour'] features = data[feature_cols].values # 只用训练集拟合scaler,测试集用同一组参数变换 train_len = int(len(features) * 0.7) scaler = StandardScaler().fit(features[:train_len]) feat_scaled = scaler.transform(features) # GMM聚类:k=4,full协方差 gmm = GaussianMixture(n_components=4, covariance_type='full', random_state=42, reg_covar=1e-4) cluster_label = gmm.fit_predict(feat_scaled[:train_len]) cluster_prob = gmm.predict_proba(feat_scaled[:train_len]) # 将隶属度向量作为额外特征写入后续模型输入 data['cluster_prob'] = np.concatenate([ cluster_prob, gmm.predict_proba(feat_scaled[train_len:]) ])这里reg_covar=1e-4重点是给协方差矩阵对角线加正则,防止某个聚类子集样本太少导致协方差奇异。这个参数在实际数据里几乎默认要设置。
模型定义部分:
import torch import torch.nn as nn import torch.nn.functional as F class CNNBiLSTMAttention(nn.Module): def __init__(self, feat_dim=8, hidden=64, output_steps=96): super().__init__() self.conv1 = nn.Conv1d(feat_dim, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv1d(32, 64, kernel_size=3, padding=1) self.lstm = nn.LSTM(64, hidden, batch_first=True, bidirectional=True) self.attn = nn.Linear(hidden * 2, 1) self.fc = nn.Linear(hidden * 2, output_steps) def forward(self, x): # x: (B, T, F) x = x.permute(0, 2, 1) # (B, F, T) x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = x.permute(0, 2, 1) # (B, T, 64) lstm_out, _ = self.lstm(x) # (B, T, 128) attn_weight = torch.softmax(self.attn(lstm_out), dim=1) context = torch.sum(attn_weight * lstm_out, dim=1) return self.fc(context)forward里的维度流转值得多说两句。Conv1d默认在最后一维做卷积,所以输入要先通过permute把特征维换到第二位。卷积后再permute回序列在最后一位的格式,交给LSTM处理。attention的self.attn把每个时间步的128维向量压成1个分数,softmax在时间步维度做归一化,最后加权求和得到上下文向量,再通过全连接输出96个预测点。
训练循环里比较重要的一个细节是:loss.backward之后一定要做梯度裁剪,torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)。BiLSTM反向传播路径长,梯度容易爆炸,不裁剪的话训练loss会在个别batch上突然跳到NaN。
4.2 MATLAB侧:fitgmdist聚类与自定义注意力层
MATLAB版本用于快速验证和在已有仿真环境里做结果对比。GMM聚类直接用Statistics and Machine Learning Toolbox的fitgmdist:
% 读取并构造特征矩阵 data = readtable('wind_farm_data.csv'); features = [data.wind_speed, sin(deg2rad(data.wind_direction)), ... cos(deg2rad(data.wind_direction)), data.temperature, ... data.pressure, data.power]; % 训练集归一化 trainLen = round(height(data) * 0.7); mu = mean(features(1:trainLen, :)); sigma = std(features(1:trainLen, :)); featScaled = (features - mu) ./ sigma; % GMM聚类 gmmModel = fitgmdist(featScaled(1:trainLen, :), 4, ... 'RegularizationValue', 1e-4); clusterLabel = cluster(gmmModel, featScaled); posteriorProb = posterior(gmmModel, featScaled);网络的实现有两种路径。如果你的MATLAB版本较新,Deep Learning Toolbox里可以直接搭建sequenceInputLayer、convolution1dLayer、bilstmLayer、fullyConnectedLayer的组合。attention层没有直接对应的内置层,我的做法是写一个自定义层。核心逻辑就是前面说的加性注意力,在predict函数里用softmax计算归一化权重,再对LSTM输出做加权平均。
使用trainNetwork训练时,要注意sequenceInputLayer和convolution1dLayer对数据格式要求。sequenceInputLayer默认接受[特征数, 时间步, 样本数]的数据排列,这和Python里习惯的[batch, time, feature]正好相反,做数据维度转换时非常容易搞反。建议在代码入口处加一行assert对维度做显式检查,省得训练到一半才发现维度出错。
4.3 双版本结果一致性的把握
双语言实现最大的疑问是:Python和MATLAB跑出来的结果能完全一致吗?我的结论是:趋势和指标量级一致,数值不会逐位相同。
原因有两层。一是LSTM的初始化策略在两种框架里基于不同的随机数生成逻辑,即使固定随机种子也不可能做到每个浮点都一致。二是CNN卷积核的初始化、数值库的BLAS实现差异都会带来微小偏差。对功率预测这种任务,我们追求的是评估指标在合理误差范围内一致,比如RMSE相差不超过0.5%,就可以认为双版本验证通过。
如果想尽量对齐,需要注意三点:使用完全相同的数据文件;归一化参数完全一致;学习率、batch size、epoch数保持一致。我在项目里用一套通用配置参数文件同时输出给两个版本调用,避免人为误操作导致参数不一致。
5. 实测效果对比与避坑清单:从指标改看到踩过的坑
5.1 加了GMM聚类和attention,效果到底差在哪
评估指标我用四个:RMSE、MAE、NMAE(归一化平均绝对误差,用装机容量做归一化)、R²。短期功率预测里NMAE是行业最常用的指标,因为它可以直接对比不同机型的场站。
我手上的数据是某并网风电场的现场运行数据,装机容量按标准值归一化处理。用同一份数据对比了几种模型,结果大致是这样的量级:
| 模型配置 | NMAE | RMSE(归一化) | 大风爬坡段表现 |
|---|---|---|---|
| 单一BiLSTM | 12.1% | 0.171 | 滞后明显 |
| CNN-BiLSTM | 11.2% | 0.158 | 略有改善 |
| CNN-BiLSTM-Attention | 10.4% | 0.149 | 爬坡段跟随性更好 |
| GMM+CNN-BiLSTM-Attention | 9.1% | 0.132 | 大风过程预测稳定 |
具体数值每个场站会有差异,但规律是一致的:GMM聚类的贡献主要在大风过程、过渡爬坡这类非平稳段,attention的贡献在早晚切换、风速突变段更明显。单纯增加网络复杂度带来的收益有限,真正拉开差距的是“把数据区分清楚”这个前置动作。
5.2 这个方案最容易踩的五个坑
第一个坑是聚类特征混入未来信息。只要聚类特征里包含未来功率的统计量,训练集里各路指标都会非常好,但一次滚动前向验证就会露馅。检查方法是把预测过程改成滚动模式:只用当前时刻及之前的信息,重新计算聚类归属,再看指标是否保持。
第二个坑是风向角度不做周期处理。直接用原始角度做聚类或当特征,一旦某段时间风向正好跨越0/360度,聚出来的簇就多出一个“假方向”,预测也会跟着出现奇怪的跳变。处理方式前面说过,sin/cos分解是标配。
第三个坑是时间序列数据集随机划分。有人习惯用train_test_split随机切分,这在普通机器学习任务里没问题,但风电数据是强时序数据,随机切分相当于告诉模型“未来已经看到过”,验证集指标严重失真。
第四个坑是预测阶段聚类归属错误。很多实现只在训练阶段聚类,测试时直接把测试数据的真实功率塞进GMM算归属,这等于又用了未来信息。正确做法是预测时用NWP气象特征或当前时刻已知的数据来推断归属,保持与部署环境一致。
第五个坑是忽略聚类子集样本量的健康度。GMM把数据软分配到多个分量之后,理论上所有样本都有后验概率,但实际操作中某个分量可能会出现“几乎没有主要归属样本”的情况。这时模型在该场景下的训练样本稀疏,预测质量无从保证。我的处理方式是对每个分量统计最大后验概率超过0.7的样本数,如果某个分量占比低于5%,就降低聚类数重新训练。
5.3 我调整这个方案的几个实际操作体会
预测结果出来后,我会加两步后处理:一是把预测功率限幅在0到额定容量之间,防止激活函数或梯度问题导致输出越界;二是做轻微的中值滤波,去掉因为attention权重突变导致的单点尖峰。这两步虽然简单,但对接电网调度平台时非常重要,否则平台侧很容易把个别尖峰误判成爬坡事件,触发不必要的考核。
关于持续优化的方向,我个人认为这个框架的下一步扩展点是区间预测。聚类本身把场景分开了,但每个场景内部的预测不确定性仍然存在,可以改loss为分位数损失输出90%置信区间,对调度决策更有实际意义。另外,把GMM升级为时变版本、引入更丰富的NWP场数据,也能进一步提高场景分割的精度。
每次重跑这套流程我都有一个很深的体会:风电功率预测问题的关键往往不在模型有多新,而在是否诚实地处理了数据里那些“说不清楚”的部分。GMM聚类做的事情就是给模型画出了哪些数据是一类“脾气”,而attention更像是模型自己告诉我们它记住了哪些关键时刻——两者配合,比盲目增大LSTM层数实在得多。